Permalink
Browse files

Added <r:javascript> and <r:stylesheet tags to both pages and the tex…

…t assets.

This includes some fancy tracking of dependencies to keep from mimicking the Radiant 5-minute-cache-per-page.
  • Loading branch information...
1 parent 1b6819a commit 14befc8b513867f40c110072389765765f2c0d54 @chrisparrish chrisparrish committed Oct 18, 2008
@@ -40,7 +40,7 @@ def show_uncached_text_asset(filename, asset_class, url)
# set the last modified date based on updated_at time for the asset
# we can do this as long as there is no dynamic content in the assets
response.headers['Last-Modified'] = @text_asset.updated_at
- response.body = @text_asset.content
+ response.body = @text_asset.render
# for text_assets, we cache no matter what (there's no status setting for them)
text_asset_cache.cache_response(url, response) if request.get?
View
@@ -1 +1,18 @@
-class Javascript < TextAsset; end
+class Javascript < TextAsset
+
+ class TagError < StandardError; end
+
+ tag('javascript') do |tag|
+ if name = tag.attr['name']
+ self.dependencies << tag.attr['name'].strip
+ if javascript = Javascript.find_by_filename(tag.attr['name'].strip)
+ javascript.render
+ else
+ raise TagError.new("javascript not found")
+ end
+ else
+ raise TagError.new("`javascript' tag must contain a `name' attribute.") unless tag.attr.has_key?('name')
+ end
+ end
+
+end
View
@@ -1 +1,18 @@
-class Stylesheet < TextAsset; end
+class Stylesheet < TextAsset
+
+ class TagError < StandardError; end
+
+ tag('stylesheet') do |tag|
+ if name = tag.attr['name']
+ self.dependencies << tag.attr['name'].strip
+ if stylesheet = Stylesheet.find_by_filename(tag.attr['name'].strip)
+ stylesheet.render
+ else
+ raise TagError.new("stylesheet not found")
+ end
+ else
+ raise TagError.new("`stylesheet' tag must contain a `name' attribute.") unless tag.attr.has_key?('name')
+ end
+ end
+
+end
View
@@ -1,7 +1,6 @@
class TextAsset < ActiveRecord::Base
set_inheritance_column :class_name
- # Default Order
order_by 'filename'
# Associations
@@ -14,9 +13,72 @@ class TextAsset < ActiveRecord::Base
# the following regexp uses \A and \Z rather than ^ and $ to enforce no "\n" characters
validates_format_of :filename, :with => %r{\A[-_.A-Za-z0-9]*\Z}, :message => 'invalid format'
+ include Radiant::Taggable
+
+ serialize :dependencies, Array
+ before_save :update_dependencies
+
+ # for some reason setting dependencies to serialize as Array still lets this
+ # value be nil. AR should do this step for me. Oh well.
+ def after_initialize
+ self.dependencies = [] if dependencies.nil?
+ end
+
+# after_save :effectively_updated_at
+
def url
StylesNScripts::Config["#{self.class.to_s.underscore}_directory"] +
"/" + self.filename
end
+
+ def update_dependencies
+ self.dependencies = []
+ parse(content, false)
+ self.dependencies.uniq!
+ end
+
+
+ def effectively_updated_at
+ if self.dependencies.empty?
+ self.updated_at
+ else
+ dependency_assets = self.class.find_all_by_filename(self.dependencies).compact
+ dependencies_last_updated_at = dependency_assets.sort_by{|i| i[:updated_at]}.last.updated_at
+ puts dependencies_last_updated_at.to_s
+ end
+ end
+
+
+# def update_dependents_timestamps()
+# self.class.find(:all).each do |text_asset|
+# unless text_asset.dependencies.nil?
+# if text_asset.dependencies.include?(filename)
+# puts "updating #{text_asset.filename} to dependencies_updated_at: #{updated_at}"
+# text_asset.dependencies_updated_at = updated_at
+# TextAsset.record_timestamps = false
+# text_asset.save
+# TextAsset.record_timestamps = true
+# end
+# end
+# end
+# end
+
+ def render
+ text = self.content
+ text = parse(text)
+ end
+
+
+ private
+
+ def parse(text, show_errors = true)
+ unless @parser and @context
+ @context = TextAssetContext.new(self)
+ @parser = Radius::Parser.new(@context, :tag_prefix => 'r')
+ end
+ @context.show_errors = show_errors
+ @parser.parse(text)
+ end
+
end
@@ -0,0 +1,40 @@
+class TextAssetContext < Radius::Context
+
+ def initialize(text_asset)
+ @show_errors = true
+ super()
+ globals.text_asset = text_asset
+ text_asset.tags.each do |name|
+ define_tag(name) { |tag_binding| text_asset.render_tag(name, tag_binding) }
+ end
+ end
+
+
+ def show_errors=(value)
+ @show_errors = value
+ end
+
+
+ def render_tag(name, attributes = {}, &block)
+ super
+ rescue Exception => e
+ raise e if raise_errors?
+ @tag_binding_stack.pop unless @tag_binding_stack.last == binding
+ e.message
+ end
+
+
+ def tag_missing(name, attributes = {}, &block)
+ super
+ rescue Radius::UndefinedTagError => e
+ raise StandardTags::TagError.new(e.message)
+ end
+
+
+ private
+
+ def raise_errors?
+ RAILS_ENV != 'production' && @show_errors
+ end
+
+end
@@ -0,0 +1,74 @@
+module TextAssetTags
+
+ include Radiant::Taggable
+ class TagError < StandardError; end
+
+# [ { :name => 'stylesheet',
+# :asset_class => Stylesheet,
+# :inline_tag => 'style',
+# :sample_file => 'my_file.css' },
+#
+# { :name => 'javascript',
+# :asset_class => Javascript,
+# :inline_tag => 'script',
+# :sample_file => 'my_file.js' }
+# ].each do |current_tag|
+# desc %{
+# Renders the content from or url for a #{current_tag[:name]} asset. The
+# @name@ attribute is required to identify the desired #{current_tag[:name]}.
+# file. Additionally, the @as@ attribute can be used to choose whether to
+# render the tag as one of the following:
+#
+# * @content@ - the content of the #{current_tag[:name]} is rendered (this
+# is the default).
+#
+# * @url@ - the url of the #{current_tag[:name]} file (relative to the
+# web root).
+#
+# * @inline@ - same as @content@ but wraps the content in an (X)HTML
+# &lt;#{current_tag[:inline_tag]}> element. By default, the @type@
+# attribute of the &lt;#{current_tag[:inline_tag]}> element will
+# automatically match the default #{current_tag[:name]} content-type
+# (but you can override this -- see Additional Options below).
+#
+# *Additional Options:*
+# When rendering &lt;r:#{current_tag[:name]} @as@="inline" />, any
+# additional attributes you provide will be passed on directly to the
+# &lt;#{current_tag[:inline_tag]}> element, like:
+# <pre><code><r:#{current_tag[:name]} name="#{current_tag[:sample_file]}" as="inline" id="my_id" type="text/custom" />
+# produces:
+# <#{current_tag[:inline_tag]} type="text/custom" id="my_id">
+# <!--
+# Your #{current_tag[:name]}'s content here...
+# -->
+# </#{current_tag[:inline_tag]}></code></pre>
+#
+# *Usage:*
+# <pre><code><r:#{current_tag[:name]} name="#{current_tag[:sample_file]}" [as="content | inline | url"] /></code></pre>
+# }
+# tag(current_tag[:name]) do |tag|
+# if name = tag.attr['name']
+# if text_asset = current_tag[:asset_class].find_by_filename(tag.attr['name'].strip)
+# case tag.attr['as']
+# when 'content', nil
+# text_asset.render
+# when 'url'
+# text_asset.url
+# when 'inline'
+# mime_type = tag.attr['type'] || StylesNScripts::Config["#{current_tag[:name]}_mime_type"]
+# optional_attribs = tag.attr.except('name', 'as', 'type').inject('') { |s, (k, v)| s << %{#{k.downcase}="#{v}" } }.strip
+# optional_attribs = " #{optional_attribs}" unless optional_attribs.empty?
+# %{<#{current_tag[:inline_tag]} type="#{mime_type}"#{optional_attribs}>\n<!--\n#{text_asset.render}\n-->\n</#{current_tag[:inline_tag]}>}
+# end
+# else
+# raise TagError.new("#{current_tag[:name]} not found")
+# end
+# else
+# raise TagError.new("`#{current_tag[:name]}' tag must contain a `name' attribute.") unless tag.attr.has_key?('name')
+# end
+# end
+#
+# end
+#
+
+end
@@ -4,7 +4,7 @@
<h1>Edit <%= proper_model_name %></h1>
<% end -%>
-<form method="post" action="">
+<% form_tag do %>
<%= hidden_field(model_name, "lock_version") %>
<div class="form-area">
<p class="title">
@@ -25,6 +25,6 @@
<p class="buttons">
<%= submit_tag(@text_asset.new_record? ? "Create #{proper_model_name}" : 'Save Changes', :class => 'button') %> <%= save_model_and_continue_editing_button(@text_asset) %> or <%= link_to("Cancel", send("#{model_name}_index_url") ) %>
</p>
-</form>
+<% end %>
<%= focus("#{model_name}_filename") %>
@@ -0,0 +1,16 @@
+class AddDependenciesToTextAssets < ActiveRecord::Migration
+ def self.up
+ add_column :text_assets, :dependencies, :string
+ # iterate through Javascripts and Stylesheets and save! each to trigger
+ # update_dependencies and save the results to the new dependencies field
+ TextAsset.find(:all).each do |text_asset|
+ text_asset.class.record_timestamps = false
+ text_asset.save!
+ text_asset.class.record_timestamps = true
+ end
+ end
+
+ def self.down
+ remove_column :text_assets, :dependencies
+ end
+end
View
@@ -0,0 +1,71 @@
+module ExtendedPageTags
+ include Radiant::Taggable
+ class TagError < StandardError; end
+
+ [ { :name => 'stylesheet',
+ :asset_class => Stylesheet,
+ :inline_tag => 'style',
+ :sample_file => 'my_file.css' },
+
+ { :name => 'javascript',
+ :asset_class => Javascript,
+ :inline_tag => 'script',
+ :sample_file => 'my_file.js' }
+ ].each do |current_tag|
+ desc %{
+ Renders the content from or url for a #{current_tag[:name]} asset. The
+ @name@ attribute is required to identify the desired #{current_tag[:name]}.
+ file. Additionally, the @as@ attribute can be used to choose whether to
+ render the tag as one of the following:
+
+ * @content@ - the content of the #{current_tag[:name]} is rendered (this
+ is the default).
+
+ * @url@ - the url of the #{current_tag[:name]} file (relative to the
+ web root).
+
+ * @inline@ - same as @content@ but wraps the content in an (X)HTML
+ &lt;#{current_tag[:inline_tag]}> element. By default, the @type@
+ attribute of the &lt;#{current_tag[:inline_tag]}> element will
+ automatically match the default #{current_tag[:name]} content-type
+ (but you can override this -- see Additional Options below).
+
+ *Additional Options:*
+ When rendering &lt;r:#{current_tag[:name]} @as@="inline" />, any
+ additional attributes you provide will be passed on directly to the
+ &lt;#{current_tag[:inline_tag]}> element, like:
+ <pre><code><r:#{current_tag[:name]} name="#{current_tag[:sample_file]}" as="inline" id="my_id" type="text/custom" />
+ produces:
+ <#{current_tag[:inline_tag]} type="text/custom" id="my_id">
+ <!--
+ Your #{current_tag[:name]}'s content here...
+ -->
+ </#{current_tag[:inline_tag]}></code></pre>
+
+ *Usage:*
+ <pre><code><r:#{current_tag[:name]} name="#{current_tag[:sample_file]}" [as="content | inline | url"] /></code></pre>
+ }
+ tag(current_tag[:name]) do |tag|
+ if name = tag.attr['name']
+ if text_asset = current_tag[:asset_class].find_by_filename(tag.attr['name'].strip)
+ case tag.attr['as']
+ when 'content', nil
+ text_asset.render
+ when 'url'
+ text_asset.url
+ when 'inline'
+ mime_type = tag.attr['type'] || StylesNScripts::Config["#{current_tag[:name]}_mime_type"]
+ optional_attribs = tag.attr.except('name', 'as', 'type').inject('') { |s, (k, v)| s << %{#{k.downcase}="#{v}" } }.strip
+ optional_attribs = " #{optional_attribs}" unless optional_attribs.empty?
+ %{<#{current_tag[:inline_tag]} type="#{mime_type}"#{optional_attribs}>\n<!--\n#{text_asset.render}\n-->\n</#{current_tag[:inline_tag]}>}
+ end
+ else
+ raise TagError.new("#{current_tag[:name]} not found")
+ end
+ else
+ raise TagError.new("`#{current_tag[:name]}' tag must contain a `name' attribute.") unless tag.attr.has_key?('name')
+ end
+ end
+
+ end
+end
@@ -0,0 +1,37 @@
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe UserActionObserver do
+ scenario :users, :stylesheets, :javascripts
+
+ before(:each) do
+ @user = users(:existing)
+ UserActionObserver.current_user = @user
+ end
+
+
+ it 'should observe stylesheet creation' do
+ Stylesheet.create!(stylesheet_params).created_by.should == @user
+ end
+
+
+ it 'should observe javascript creation' do
+ Javascript.create!(javascript_params).created_by.should == @user
+ end
+
+
+ it 'should observe stylesheet update' do
+ model = Stylesheet.find_by_filename('main.css')
+ model.attributes = model.attributes.dup
+ model.save.should == true
+ model.updated_by.should == @user
+ end
+
+
+ it 'should observe javascript update' do
+ model = Javascript.find_by_filename('main.js')
+ model.attributes = model.attributes.dup
+ model.save.should == true
+ model.updated_by.should == @user
+ end
+
+end
Oops, something went wrong.

0 comments on commit 14befc8

Please sign in to comment.