diff options
Diffstat (limited to 'actionpack/lib/action_view')
18 files changed, 563 insertions, 156 deletions
diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb index ada9052073..efed19a21d 100644 --- a/actionpack/lib/action_view/base.rb +++ b/actionpack/lib/action_view/base.rb @@ -182,6 +182,15 @@ module ActionView #:nodoc: # that alert()s the caught exception (and then re-raises it). cattr_accessor :debug_rjs + # Specify whether templates should be cached. Otherwise the file we be read everytime it is accessed. + # Automatically reloading templates are not thread safe and should only be used in development mode. + @@cache_template_loading = nil + cattr_accessor :cache_template_loading + + def self.cache_template_loading? + ActionController::Base.allow_concurrency || (cache_template_loading.nil? ? !ActiveSupport::Dependencies.load? : cache_template_loading) + end + attr_internal :request delegate :controller_path, :to => :controller, :allow_nil => true @@ -217,10 +226,12 @@ module ActionView #:nodoc: @formats = formats || [:html] @assigns = assigns_for_first_render @assigns_added = nil - @_render_stack = [] @controller = controller @helpers = ProxyModule.new(self) self.view_paths = view_paths + + @_first_render = nil + @_current_render = nil end attr_reader :view_paths @@ -232,7 +243,25 @@ module ActionView #:nodoc: # Access the current template being rendered. # Returns a ActionView::Template object. def template - @_render_stack.last + @_current_render + end + + def template=(template) #:nodoc: + @_first_render ||= template + @_current_render = template + end + + def with_template(current_template) + last_template, self.template = template, current_template + yield + ensure + self.template = last_template + end + + def punctuate_body!(part) + flush_output_buffer + response.body_parts << part + nil end private diff --git a/actionpack/lib/action_view/helpers/active_record_helper.rb b/actionpack/lib/action_view/helpers/active_record_helper.rb index 8b56d241ae..7c0dfdab10 100644 --- a/actionpack/lib/action_view/helpers/active_record_helper.rb +++ b/actionpack/lib/action_view/helpers/active_record_helper.rb @@ -121,7 +121,7 @@ module ActionView if (obj = (object.respond_to?(:errors) ? object : instance_variable_get("@#{object}"))) && (errors = obj.errors.on(method)) content_tag("div", - "#{options[:prepend_text]}#{errors.is_a?(Array) ? errors.first : errors}#{options[:append_text]}", + "#{options[:prepend_text]}#{ERB::Util.html_escape(errors.is_a?(Array) ? errors.first : errors)}#{options[:append_text]}", :class => options[:css_class] ) else @@ -194,11 +194,11 @@ module ActionView options[:header_message] else object_name = options[:object_name].to_s.gsub('_', ' ') - object_name = I18n.t(object_name, :default => object_name, :scope => [:activerecord, :models], :count => 1) + object_name = I18n.t(options[:object_name].to_s, :default => object_name, :scope => [:activerecord, :models], :count => 1) locale.t :header, :count => count, :model => object_name end message = options.include?(:message) ? options[:message] : locale.t(:body) - error_messages = objects.sum {|object| object.errors.full_messages.map {|msg| content_tag(:li, msg) } }.join + error_messages = objects.sum {|object| object.errors.full_messages.map {|msg| content_tag(:li, ERB::Util.html_escape(msg)) } }.join contents = '' contents << content_tag(options[:header_tag] || :h2, header_message) unless header_message.blank? diff --git a/actionpack/lib/action_view/helpers/asset_tag_helper.rb b/actionpack/lib/action_view/helpers/asset_tag_helper.rb index f6abea38ed..a32beb6100 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helper.rb @@ -516,7 +516,8 @@ module ActionView def compute_public_path(source, dir, ext = nil, include_host = true) has_request = @controller.respond_to?(:request) - if ext && (File.extname(source).blank? || File.exist?(File.join(ASSETS_DIR, dir, "#{source}.#{ext}"))) + source_ext = File.extname(source)[1..-1] + if ext && (source_ext.blank? || (ext != source_ext && File.exist?(File.join(ASSETS_DIR, dir, "#{source}.#{ext}")))) source += ".#{ext}" end diff --git a/actionpack/lib/action_view/helpers/atom_feed_helper.rb b/actionpack/lib/action_view/helpers/atom_feed_helper.rb index cd25684940..dc4497581c 100644 --- a/actionpack/lib/action_view/helpers/atom_feed_helper.rb +++ b/actionpack/lib/action_view/helpers/atom_feed_helper.rb @@ -30,7 +30,7 @@ module ActionView # app/views/posts/index.atom.builder: # atom_feed do |feed| # feed.title("My great blog!") - # feed.updated((@posts.first.created_at)) + # feed.updated(@posts.first.created_at) # # for post in @posts # feed.entry(post) do |entry| diff --git a/actionpack/lib/action_view/helpers/capture_helper.rb b/actionpack/lib/action_view/helpers/capture_helper.rb index e86ca27f31..9e39536653 100644 --- a/actionpack/lib/action_view/helpers/capture_helper.rb +++ b/actionpack/lib/action_view/helpers/capture_helper.rb @@ -131,6 +131,14 @@ module ActionView ensure self.output_buffer = old_buffer end + + # Add the output buffer to the response body and start a new one. + def flush_output_buffer #:nodoc: + if output_buffer && output_buffer != '' + response.body_parts << output_buffer + self.output_buffer = '' + end + end end end end diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb index b4c1adbe76..c74909a360 100644 --- a/actionpack/lib/action_view/helpers/date_helper.rb +++ b/actionpack/lib/action_view/helpers/date_helper.rb @@ -876,8 +876,8 @@ module ActionView input_name_from_type(type).gsub(/([\[\(])|(\]\[)/, '_').gsub(/[\]\)]/, '') end - # Given an ordering of datetime components, create the selection html - # and join them with their appropriate seperators + # Given an ordering of datetime components, create the selection HTML + # and join them with their appropriate separators. def build_selects_from_types(order) select = '' order.reverse.each do |type| @@ -931,7 +931,7 @@ module ActionView end def default_datetime(options) - return if options[:include_blank] + return if options[:include_blank] || options[:prompt] case options[:default] when nil diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index a85751c657..a59829b23f 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -5,17 +5,24 @@ require 'action_view/helpers/form_tag_helper' module ActionView module Helpers - # Form helpers are designed to make working with models much easier compared to using just standard HTML - # elements by providing a set of methods for creating forms based on your models. This helper generates the HTML - # for forms, providing a method for each sort of input (e.g., text, password, select, and so on). When the form - # is submitted (i.e., when the user hits the submit button or <tt>form.submit</tt> is called via JavaScript), the form inputs will be bundled into the <tt>params</tt> object and passed back to the controller. + # Form helpers are designed to make working with models much easier + # compared to using just standard HTML elements by providing a set of + # methods for creating forms based on your models. This helper generates + # the HTML for forms, providing a method for each sort of input + # (e.g., text, password, select, and so on). When the form is submitted + # (i.e., when the user hits the submit button or <tt>form.submit</tt> is + # called via JavaScript), the form inputs will be bundled into the + # <tt>params</tt> object and passed back to the controller. # - # There are two types of form helpers: those that specifically work with model attributes and those that don't. - # This helper deals with those that work with model attributes; to see an example of form helpers that don't work - # with model attributes, check the ActionView::Helpers::FormTagHelper documentation. + # There are two types of form helpers: those that specifically work with + # model attributes and those that don't. This helper deals with those that + # work with model attributes; to see an example of form helpers that don't + # work with model attributes, check the ActionView::Helpers::FormTagHelper + # documentation. # - # The core method of this helper, form_for, gives you the ability to create a form for a model instance; - # for example, let's say that you have a model <tt>Person</tt> and want to create a new instance of it: + # The core method of this helper, form_for, gives you the ability to create + # a form for a model instance; for example, let's say that you have a model + # <tt>Person</tt> and want to create a new instance of it: # # # Note: a @person variable will have been created in the controller. # # For example: @person = Person.new @@ -40,17 +47,22 @@ module ActionView # <%= submit_tag 'Create' %> # <% end %> # - # This example will render the <tt>people/_form</tt> partial, setting a local variable called <tt>form</tt> which references the yielded FormBuilder. - # - # The <tt>params</tt> object created when this form is submitted would look like: + # This example will render the <tt>people/_form</tt> partial, setting a + # local variable called <tt>form</tt> which references the yielded + # FormBuilder. The <tt>params</tt> object created when this form is + # submitted would look like: # # {"action"=>"create", "controller"=>"persons", "person"=>{"first_name"=>"William", "last_name"=>"Smith"}} # - # The params hash has a nested <tt>person</tt> value, which can therefore be accessed with <tt>params[:person]</tt> in the controller. - # If were editing/updating an instance (e.g., <tt>Person.find(1)</tt> rather than <tt>Person.new</tt> in the controller), the objects - # attribute values are filled into the form (e.g., the <tt>person_first_name</tt> field would have that person's first name in it). + # The params hash has a nested <tt>person</tt> value, which can therefore + # be accessed with <tt>params[:person]</tt> in the controller. If were + # editing/updating an instance (e.g., <tt>Person.find(1)</tt> rather than + # <tt>Person.new</tt> in the controller), the objects attribute values are + # filled into the form (e.g., the <tt>person_first_name</tt> field would + # have that person's first name in it). # - # If the object name contains square brackets the id for the object will be inserted. For example: + # If the object name contains square brackets the id for the object will be + # inserted. For example: # # <%= text_field "person[]", "name" %> # @@ -58,8 +70,10 @@ module ActionView # # <input type="text" id="person_<%= @person.id %>_name" name="person[<%= @person.id %>][name]" value="<%= @person.name %>" /> # - # If the helper is being used to generate a repetitive sequence of similar form elements, for example in a partial - # used by <tt>render_collection_of_partials</tt>, the <tt>index</tt> option may come in handy. Example: + # If the helper is being used to generate a repetitive sequence of similar + # form elements, for example in a partial used by + # <tt>render_collection_of_partials</tt>, the <tt>index</tt> option may + # come in handy. Example: # # <%= text_field "person", "name", "index" => 1 %> # @@ -67,14 +81,17 @@ module ActionView # # <input type="text" id="person_1_name" name="person[1][name]" value="<%= @person.name %>" /> # - # An <tt>index</tt> option may also be passed to <tt>form_for</tt> and <tt>fields_for</tt>. This automatically applies - # the <tt>index</tt> to all the nested fields. + # An <tt>index</tt> option may also be passed to <tt>form_for</tt> and + # <tt>fields_for</tt>. This automatically applies the <tt>index</tt> to + # all the nested fields. # - # There are also methods for helping to build form tags in link:classes/ActionView/Helpers/FormOptionsHelper.html, - # link:classes/ActionView/Helpers/DateHelper.html, and link:classes/ActionView/Helpers/ActiveRecordHelper.html + # There are also methods for helping to build form tags in + # link:classes/ActionView/Helpers/FormOptionsHelper.html, + # link:classes/ActionView/Helpers/DateHelper.html, and + # link:classes/ActionView/Helpers/ActiveRecordHelper.html module FormHelper - # Creates a form and a scope around a specific model object that is used as - # a base for questioning about values for the fields. + # Creates a form and a scope around a specific model object that is used + # as a base for questioning about values for the fields. # # Rails provides succinct resource-oriented form generation with +form_for+ # like this: @@ -86,13 +103,15 @@ module ActionView # <%= f.text_field :author %><br /> # <% end %> # - # There, +form_for+ is able to generate the rest of RESTful form parameters - # based on introspection on the record, but to understand what it does we - # need to dig first into the alternative generic usage it is based upon. + # There, +form_for+ is able to generate the rest of RESTful form + # parameters based on introspection on the record, but to understand what + # it does we need to dig first into the alternative generic usage it is + # based upon. # # === Generic form_for # - # The generic way to call +form_for+ yields a form builder around a model: + # The generic way to call +form_for+ yields a form builder around a + # model: # # <% form_for :person, :url => { :action => "update" } do |f| %> # <%= f.error_messages %> @@ -103,8 +122,8 @@ module ActionView # <% end %> # # There, the first argument is a symbol or string with the name of the - # object the form is about, and also the name of the instance variable the - # object is stored in. + # object the form is about, and also the name of the instance variable + # the object is stored in. # # The form builder acts as a regular form helper that somehow carries the # model. Thus, the idea is that @@ -137,17 +156,18 @@ module ActionView # In any of its variants, the rightmost argument to +form_for+ is an # optional hash of options: # - # * <tt>:url</tt> - The URL the form is submitted to. It takes the same fields - # you pass to +url_for+ or +link_to+. In particular you may pass here a - # named route directly as well. Defaults to the current action. + # * <tt>:url</tt> - The URL the form is submitted to. It takes the same + # fields you pass to +url_for+ or +link_to+. In particular you may pass + # here a named route directly as well. Defaults to the current action. # * <tt>:html</tt> - Optional HTML attributes for the form tag. # - # Worth noting is that the +form_for+ tag is called in a ERb evaluation block, - # not an ERb output block. So that's <tt><% %></tt>, not <tt><%= %></tt>. + # Worth noting is that the +form_for+ tag is called in a ERb evaluation + # block, not an ERb output block. So that's <tt><% %></tt>, not + # <tt><%= %></tt>. # # Also note that +form_for+ doesn't create an exclusive scope. It's still - # possible to use both the stand-alone FormHelper methods and methods from - # FormTagHelper. For example: + # possible to use both the stand-alone FormHelper methods and methods + # from FormTagHelper. For example: # # <% form_for :person, @person, :url => { :action => "update" } do |f| %> # First name: <%= f.text_field :first_name %> @@ -156,16 +176,16 @@ module ActionView # Admin? : <%= check_box_tag "person[admin]", @person.company.admin? %> # <% end %> # - # This also works for the methods in FormOptionHelper and DateHelper that are - # designed to work with an object as base, like FormOptionHelper#collection_select - # and DateHelper#datetime_select. + # This also works for the methods in FormOptionHelper and DateHelper that + # are designed to work with an object as base, like + # FormOptionHelper#collection_select and DateHelper#datetime_select. # # === Resource-oriented style # - # As we said above, in addition to manually configuring the +form_for+ call, - # you can rely on automated resource identification, which will use the conventions - # and named routes of that approach. This is the preferred way to use +form_for+ - # nowadays. + # As we said above, in addition to manually configuring the +form_for+ + # call, you can rely on automated resource identification, which will use + # the conventions and named routes of that approach. This is the + # preferred way to use +form_for+ nowadays. # # For example, if <tt>@post</tt> is an existing record you want to edit # @@ -205,8 +225,10 @@ module ActionView # # === Customized form builders # - # You can also build forms using a customized FormBuilder class. Subclass FormBuilder and override or define some more helpers, - # then use your custom builder. For example, let's say you made a helper to automatically add labels to form inputs. + # You can also build forms using a customized FormBuilder class. Subclass + # FormBuilder and override or define some more helpers, then use your + # custom builder. For example, let's say you made a helper to + # automatically add labels to form inputs. # # <% form_for :person, @person, :url => { :action => "update" }, :builder => LabellingFormBuilder do |f| %> # <%= f.text_field :first_name %> @@ -219,16 +241,23 @@ module ActionView # # <%= render :partial => f %> # - # The rendered template is <tt>people/_labelling_form</tt> and the local variable referencing the form builder is called <tt>labelling_form</tt>. + # The rendered template is <tt>people/_labelling_form</tt> and the local + # variable referencing the form builder is called + # <tt>labelling_form</tt>. + # + # The custom FormBuilder class is automatically merged with the options + # of a nested fields_for call, unless it's explicitely set. # - # In many cases you will want to wrap the above in another helper, so you could do something like the following: + # In many cases you will want to wrap the above in another helper, so you + # could do something like the following: # # def labelled_form_for(record_or_name_or_array, *args, &proc) # options = args.extract_options! # form_for(record_or_name_or_array, *(args << options.merge(:builder => LabellingFormBuilder)), &proc) # end # - # If you don't need to attach a form to a model instance, then check out FormTagHelper#form_tag. + # If you don't need to attach a form to a model instance, then check out + # FormTagHelper#form_tag. def form_for(record_or_name_or_array, *args, &proc) raise ArgumentError, "Missing block" unless block_given? @@ -269,10 +298,12 @@ module ActionView options[:url] ||= polymorphic_path(object_or_array) end - # Creates a scope around a specific model object like form_for, but doesn't create the form tags themselves. This makes - # fields_for suitable for specifying additional model objects in the same form: + # Creates a scope around a specific model object like form_for, but + # doesn't create the form tags themselves. This makes fields_for suitable + # for specifying additional model objects in the same form. + # + # === Generic Examples # - # ==== Examples # <% form_for @person, :url => { :action => "update" } do |person_form| %> # First name: <%= person_form.text_field :first_name %> # Last name : <%= person_form.text_field :last_name %> @@ -282,20 +313,166 @@ module ActionView # <% end %> # <% end %> # - # ...or if you have an object that needs to be represented as a different parameter, like a Client that acts as a Person: + # ...or if you have an object that needs to be represented as a different + # parameter, like a Client that acts as a Person: # # <% fields_for :person, @client do |permission_fields| %> # Admin?: <%= permission_fields.check_box :admin %> # <% end %> # - # ...or if you don't have an object, just a name of the parameter + # ...or if you don't have an object, just a name of the parameter: # # <% fields_for :person do |permission_fields| %> # Admin?: <%= permission_fields.check_box :admin %> # <% end %> # - # Note: This also works for the methods in FormOptionHelper and DateHelper that are designed to work with an object as base, - # like FormOptionHelper#collection_select and DateHelper#datetime_select. + # Note: This also works for the methods in FormOptionHelper and + # DateHelper that are designed to work with an object as base, like + # FormOptionHelper#collection_select and DateHelper#datetime_select. + # + # === Nested Attributes Examples + # + # When the object belonging to the current scope has a nested attribute + # writer for a certain attribute, fields_for will yield a new scope + # for that attribute. This allows you to create forms that set or change + # the attributes of a parent object and its associations in one go. + # + # Nested attribute writers are normal setter methods named after an + # association. The most common way of defining these writers is either + # with +accepts_nested_attributes_for+ in a model definition or by + # defining a method with the proper name. For example: the attribute + # writer for the association <tt>:address</tt> is called + # <tt>address_attributes=</tt>. + # + # Whether a one-to-one or one-to-many style form builder will be yielded + # depends on whether the normal reader method returns a _single_ object + # or an _array_ of objects. + # + # ==== One-to-one + # + # Consider a Person class which returns a _single_ Address from the + # <tt>address</tt> reader method and responds to the + # <tt>address_attributes=</tt> writer method: + # + # class Person + # def address + # @address + # end + # + # def address_attributes=(attributes) + # # Process the attributes hash + # end + # end + # + # This model can now be used with a nested fields_for, like so: + # + # <% form_for @person, :url => { :action => "update" } do |person_form| %> + # ... + # <% person_form.fields_for :address do |address_fields| %> + # Street : <%= address_fields.text_field :street %> + # Zip code: <%= address_fields.text_field :zip_code %> + # <% end %> + # <% end %> + # + # When address is already an association on a Person you can use + # +accepts_nested_attributes_for+ to define the writer method for you: + # + # class Person < ActiveRecord::Base + # has_one :address + # accepts_nested_attributes_for :address + # end + # + # If you want to destroy the associated model through the form, you have + # to enable it first using the <tt>:allow_destroy</tt> option for + # +accepts_nested_attributes_for+: + # + # class Person < ActiveRecord::Base + # has_one :address + # accepts_nested_attributes_for :address, :allow_destroy => true + # end + # + # Now, when you use a form element with the <tt>_delete</tt> parameter, + # with a value that evaluates to +true+, you will destroy the associated + # model (eg. 1, '1', true, or 'true'): + # + # <% form_for @person, :url => { :action => "update" } do |person_form| %> + # ... + # <% person_form.fields_for :address do |address_fields| %> + # ... + # Delete: <%= address_fields.check_box :_delete %> + # <% end %> + # <% end %> + # + # ==== One-to-many + # + # Consider a Person class which returns an _array_ of Project instances + # from the <tt>projects</tt> reader method and responds to the + # <tt>projects_attributes=</tt> writer method: + # + # class Person + # def projects + # [@project1, @project2] + # end + # + # def projects_attributes=(attributes) + # # Process the attributes hash + # end + # end + # + # This model can now be used with a nested fields_for. The block given to + # the nested fields_for call will be repeated for each instance in the + # collection: + # + # <% form_for @person, :url => { :action => "update" } do |person_form| %> + # ... + # <% person_form.fields_for :projects do |project_fields| %> + # <% if project_fields.object.active? %> + # Name: <%= project_fields.text_field :name %> + # <% end %> + # <% end %> + # <% end %> + # + # It's also possible to specify the instance to be used: + # + # <% form_for @person, :url => { :action => "update" } do |person_form| %> + # ... + # <% @person.projects.each do |project| %> + # <% if project.active? %> + # <% person_form.fields_for :projects, project do |project_fields| %> + # Name: <%= project_fields.text_field :name %> + # <% end %> + # <% end %> + # <% end %> + # <% end %> + # + # When projects is already an association on Person you can use + # +accepts_nested_attributes_for+ to define the writer method for you: + # + # class Person < ActiveRecord::Base + # has_many :projects + # accepts_nested_attributes_for :projects + # end + # + # If you want to destroy any of the associated models through the + # form, you have to enable it first using the <tt>:allow_destroy</tt> + # option for +accepts_nested_attributes_for+: + # + # class Person < ActiveRecord::Base + # has_many :projects + # accepts_nested_attributes_for :projects, :allow_destroy => true + # end + # + # This will allow you to specify which models to destroy in the + # attributes hash by adding a form element for the <tt>_delete</tt> + # parameter with a value that evaluates to +true+ + # (eg. 1, '1', true, or 'true'): + # + # <% form_for @person, :url => { :action => "update" } do |person_form| %> + # ... + # <% person_form.fields_for :projects do |project_fields| %> + # Delete: <%= project_fields.check_box :_delete %> + # <% end %> + # <% end %> def fields_for(record_or_name_or_array, *args, &block) raise ArgumentError, "Missing block" unless block_given? options = args.extract_options! @@ -451,7 +628,7 @@ module ActionView # # The HTML specification says unchecked check boxes are not successful, and # thus web browsers do not send them. Unfortunately this introduces a gotcha: - # if an Invoice model has a +paid+ flag, and in the form that edits a paid + # if an +Invoice+ model has a +paid+ flag, and in the form that edits a paid # invoice the user unchecks its check box, no +paid+ parameter is sent. So, # any mass-assignment idiom like # @@ -459,12 +636,15 @@ module ActionView # # wouldn't update the flag. # - # To prevent this the helper generates a hidden field with the same name as - # the checkbox after the very check box. So, the client either sends only the - # hidden field (representing the check box is unchecked), or both fields. - # Since the HTML specification says key/value pairs have to be sent in the - # same order they appear in the form and Rails parameters extraction always - # gets the first occurrence of any given key, that works in ordinary forms. + # To prevent this the helper generates an auxiliary hidden field before + # the very check box. The hidden field has the same name and its + # attributes mimick an unchecked check box. + # + # This way, the client either sends only the hidden field (representing + # the check box is unchecked), or both fields. Since the HTML specification + # says key/value pairs have to be sent in the same order they appear in the + # form, and parameters extraction gets the last occurrence of any repeated + # key in the query string, that works for ordinary forms. # # Unfortunately that workaround does not work when the check box goes # within an array-like parameter, as in @@ -475,22 +655,26 @@ module ActionView # <% end %> # # because parameter name repetition is precisely what Rails seeks to distinguish - # the elements of the array. + # the elements of the array. For each item with a checked check box you + # get an extra ghost item with only that attribute, assigned to "0". + # + # In that case it is preferable to either use +check_box_tag+ or to use + # hashes instead of arrays. # # ==== Examples # # Let's say that @post.validated? is 1: # check_box("post", "validated") - # # => <input type="checkbox" id="post_validated" name="post[validated]" value="1" /> - # # <input name="post[validated]" type="hidden" value="0" /> + # # => <input name="post[validated]" type="hidden" value="0" /> + # # <input type="checkbox" id="post_validated" name="post[validated]" value="1" /> # # # Let's say that @puppy.gooddog is "no": # check_box("puppy", "gooddog", {}, "yes", "no") - # # => <input type="checkbox" id="puppy_gooddog" name="puppy[gooddog]" value="yes" /> - # # <input name="puppy[gooddog]" type="hidden" value="no" /> + # # => <input name="puppy[gooddog]" type="hidden" value="no" /> + # # <input type="checkbox" id="puppy_gooddog" name="puppy[gooddog]" value="yes" /> # # check_box("eula", "accepted", { :class => 'eula_check' }, "yes", "no") - # # => <input type="checkbox" class="eula_check" id="eula_accepted" name="eula[accepted]" value="yes" /> - # # <input name="eula[accepted]" type="hidden" value="no" /> + # # => <input name="eula[accepted]" type="hidden" value="no" /> + # # <input type="checkbox" class="eula_check" id="eula_accepted" name="eula[accepted]" value="yes" /> # def check_box(object_name, method, options = {}, checked_value = "1", unchecked_value = "0") InstanceTag.new(object_name, method, self, options.delete(:object)).to_check_box_tag(options, checked_value, unchecked_value) @@ -498,8 +682,10 @@ module ActionView # Returns a radio button tag for accessing a specified attribute (identified by +method+) on an object # assigned to the template (identified by +object+). If the current value of +method+ is +tag_value+ the - # radio button will be checked. Additional options on the input tag can be passed as a - # hash with +options+. + # radio button will be checked. + # + # To force the radio button to be checked pass <tt>:checked => true</tt> in the + # +options+ hash. You may pass HTML options there as well. # # ==== Examples # # Let's say that @post.category returns "rails": @@ -605,7 +791,9 @@ module ActionView end options["checked"] = "checked" if checked add_default_name_and_id(options) - tag("input", options) << tag("input", "name" => options["name"], "type" => "hidden", "value" => options['disabled'] && checked ? checked_value : unchecked_value) + hidden = tag("input", "name" => options["name"], "type" => "hidden", "value" => options['disabled'] && checked ? checked_value : unchecked_value) + checkbox = tag("input", options) + hidden + checkbox end def to_boolean_select_tag(options = {}) @@ -758,9 +946,18 @@ module ActionView index = "" end + if options[:builder] + args << {} unless args.last.is_a?(Hash) + args.last[:builder] ||= options[:builder] + end + case record_or_name_or_array when String, Symbol - name = "#{object_name}#{index}[#{record_or_name_or_array}]" + if nested_attributes_association?(record_or_name_or_array) + return fields_for_with_nested_attributes(record_or_name_or_array, args, block) + else + name = "#{object_name}#{index}[#{record_or_name_or_array}]" + end when Array object = record_or_name_or_array.last name = "#{object_name}#{index}[#{ActionController::RecordIdentifier.singular_class_name(object)}]" @@ -802,6 +999,43 @@ module ActionView def objectify_options(options) @default_options.merge(options.merge(:object => @object)) end + + def nested_attributes_association?(association_name) + @object.respond_to?("#{association_name}_attributes=") + end + + def fields_for_with_nested_attributes(association_name, args, block) + name = "#{object_name}[#{association_name}_attributes]" + association = @object.send(association_name) + explicit_object = args.first if args.first.respond_to?(:new_record?) + + if association.is_a?(Array) + children = explicit_object ? [explicit_object] : association + explicit_child_index = args.last[:child_index] if args.last.is_a?(Hash) + + children.map do |child| + fields_for_nested_model("#{name}[#{explicit_child_index || nested_child_index}]", child, args, block) + end.join + else + fields_for_nested_model(name, explicit_object || association, args, block) + end + end + + def fields_for_nested_model(name, object, args, block) + if object.new_record? + @template.fields_for(name, object, *args, &block) + else + @template.fields_for(name, object, *args) do |builder| + @template.concat builder.hidden_field(:id) + block.call(builder) + end + end + end + + def nested_child_index + @nested_child_index ||= -1 + @nested_child_index += 1 + end end end @@ -809,4 +1043,4 @@ module ActionView cattr_accessor :default_form_builder self.default_form_builder = ::ActionView::Helpers::FormBuilder end -end +end
\ No newline at end of file diff --git a/actionpack/lib/action_view/helpers/form_options_helper.rb b/actionpack/lib/action_view/helpers/form_options_helper.rb index 54c82cbd1d..6b385ef77d 100644 --- a/actionpack/lib/action_view/helpers/form_options_helper.rb +++ b/actionpack/lib/action_view/helpers/form_options_helper.rb @@ -6,9 +6,7 @@ module ActionView module Helpers # Provides a number of methods for turning different kinds of containers into a set of option tags. # == Options - # The <tt>collection_select</tt>, <tt>country_select</tt>, <tt>select</tt>, - # and <tt>time_zone_select</tt> methods take an <tt>options</tt> parameter, - # a hash. + # The <tt>collection_select</tt>, <tt>select</tt> and <tt>time_zone_select</tt> methods take an <tt>options</tt> parameter, a hash: # # * <tt>:include_blank</tt> - set to true or a prompt string if the first option element of the select element is a blank. Useful if there is not a default value required for the select element. # @@ -28,7 +26,7 @@ module ActionView # # Example with @post.person_id => 2: # - # select("post", "person_id", Person.find(:all).collect {|p| [ p.name, p.id ] }, {:include_blank => 'None'}) + # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {:include_blank => 'None'}) # # could become: # @@ -43,7 +41,7 @@ module ActionView # # Example: # - # select("post", "person_id", Person.find(:all).collect {|p| [ p.name, p.id ] }, {:prompt => 'Select Person'}) + # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {:prompt => 'Select Person'}) # # could become: # @@ -68,6 +66,36 @@ module ActionView # <option value="rock">rock</option> # <option value="country">country</option> # </select> + # + # * <tt>:disabled</tt> - can be a single value or an array of values that will be disabled options in the final output. + # + # Example: + # + # select("post", "category", Post::CATEGORIES, {:disabled => 'restricted'}) + # + # could become: + # + # <select name="post[category]"> + # <option></option> + # <option>joke</option> + # <option>poem</option> + # <option disabled="disabled">restricted</option> + # </select> + # + # When used with the <tt>collection_select</tt> helper, <tt>:disabled</tt> can also be a Proc that identifies those options that should be disabled. + # + # Example: + # + # collection_select(:post, :category_id, Category.all, :id, :name, {:disabled => lambda{|category| category.archived? }}) + # + # If the categories "2008 stuff" and "Christmas" return true when the method <tt>archived?</tt> is called, this would return: + # <select name="post[category_id]"> + # <option value="1" disabled="disabled">2008 stuff</option> + # <option value="2" disabled="disabled">Christmas</option> + # <option value="3">Jokes</option> + # <option value="4">Poems</option> + # </select> + # module FormOptionsHelper include ERB::Util @@ -76,7 +104,7 @@ module ActionView # See options_for_select for the required format of the choices parameter. # # Example with @post.person_id => 1: - # select("post", "person_id", Person.find(:all).collect {|p| [ p.name, p.id ] }, { :include_blank => true }) + # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, { :include_blank => true }) # # could become: # @@ -94,7 +122,8 @@ module ActionView # In addition, this allows a single partial to be used to generate form inputs for both edit and create forms. # # By default, <tt>post.person_id</tt> is the selected option. Specify <tt>:selected => value</tt> to use a different selection - # or <tt>:selected => nil</tt> to leave all options unselected. + # or <tt>:selected => nil</tt> to leave all options unselected. Similarly, you can specify values to be disabled in the option + # tags by specifying the <tt>:disabled</tt> option. This can either be a single value or an array of values to be disabled. def select(object, method, choices, options = {}, html_options = {}) InstanceTag.new(object, method, self, options.delete(:object)).to_select_tag(choices, options, html_options) end @@ -120,7 +149,7 @@ module ActionView # end # # Sample usage (selecting the associated Author for an instance of Post, <tt>@post</tt>): - # collection_select(:post, :author_id, Author.find(:all), :id, :name_with_initial, {:prompt => true}) + # collection_select(:post, :author_id, Author.all, :id, :name_with_initial, {:prompt => true}) # # If <tt>@post.author_id</tt> is already <tt>1</tt>, this would return: # <select name="post[author_id]"> @@ -186,14 +215,29 @@ module ActionView # options_for_select([ "VISA", "MasterCard", "Discover" ], ["VISA", "Discover"]) # <option selected="selected">VISA</option>\n<option>MasterCard</option>\n<option selected="selected">Discover</option> # + # If you wish to specify disabled option tags, set +selected+ to be a hash, with <tt>:disabled</tt> being either a value + # or array of values to be disabled. In this case, you can use <tt>:selected</tt> to specify selected option tags. + # + # Examples: + # options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], :disabled => "Super Platinum") + # <option value="Free">Free</option>\n<option value="Basic">Basic</option>\n<option value="Advanced">Advanced</option>\n<option value="Super Platinum" disabled="disabled">Super Platinum</option> + # + # options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], :disabled => ["Advanced", "Super Platinum"]) + # <option value="Free">Free</option>\n<option value="Basic">Basic</option>\n<option value="Advanced" disabled="disabled">Advanced</option>\n<option value="Super Platinum" disabled="disabled">Super Platinum</option> + # + # options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], :selected => "Free", :disabled => "Super Platinum") + # <option value="Free" selected="selected">Free</option>\n<option value="Basic">Basic</option>\n<option value="Advanced">Advanced</option>\n<option value="Super Platinum" disabled="disabled">Super Platinum</option> + # # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag. def options_for_select(container, selected = nil) container = container.to_a if Hash === container + selected, disabled = extract_selected_and_disabled(selected) options_for_select = container.inject([]) do |options, element| text, value = option_text_and_value(element) selected_attribute = ' selected="selected"' if option_value_selected?(value, selected) - options << %(<option value="#{html_escape(value.to_s)}"#{selected_attribute}>#{html_escape(text.to_s)}</option>) + disabled_attribute = ' disabled="disabled"' if disabled && option_value_selected?(value, disabled) + options << %(<option value="#{html_escape(value.to_s)}"#{selected_attribute}#{disabled_attribute}>#{html_escape(text.to_s)}</option>) end options_for_select.join("\n") @@ -209,8 +253,15 @@ module ActionView # This is more often than not used inside a #select_tag like this example: # select_tag 'person', options_from_collection_for_select(@people, 'id', 'name') # - # If +selected+ is specified, the element returning a match on +value_method+ will get the selected option tag. - # Be sure to specify the same class as the +value_method+ when specifying a selected option. + # If +selected+ is specified as a value or array of values, the element(s) returning a match on +value_method+ + # will be selected option tag(s). + # + # If +selected+ is specified as a Proc, those members of the collection that return true for the anonymous + # function are the selected values. + # + # +selected+ can also be a hash, specifying both <tt>:selected</tt> and/or <tt>:disabled</tt> values as required. + # + # Be sure to specify the same class as the +value_method+ when specifying selected or disabled options. # Failure to do this will produce undesired results. Example: # options_from_collection_for_select(@people, 'id', 'name', '1') # Will not select a person with the id of 1 because 1 (an Integer) is not the same as '1' (a string) @@ -220,7 +271,12 @@ module ActionView options = collection.map do |element| [element.send(text_method), element.send(value_method)] end - options_for_select(options, selected) + selected, disabled = extract_selected_and_disabled(selected) + select_deselect = {} + select_deselect[:selected] = extract_values_from_collection(collection, value_method, selected) + select_deselect[:disabled] = extract_values_from_collection(collection, value_method, disabled) + + options_for_select(options, select_deselect) end # Returns a string of <tt><option></tt> tags, like <tt>options_from_collection_for_select</tt>, but @@ -238,7 +294,8 @@ module ActionView # +collection+, returns a value to be used as the contents of its <tt><option></tt> tag. # * +selected_key+ - A value equal to the +value+ attribute for one of the <tt><option></tt> tags, # which will have the +selected+ attribute set. Corresponds to the return value of one of the calls - # to +option_key_method+. If +nil+, no selection is made. + # to +option_key_method+. If +nil+, no selection is made. Can also be a hash if disabled values are + # to be specified. # # Example object structure for use with this method: # class Continent < ActiveRecord::Base @@ -388,6 +445,24 @@ module ActionView value == selected end end + + def extract_selected_and_disabled(selected) + if selected.is_a?(Hash) + [selected[:selected], selected[:disabled]] + else + [selected, nil] + end + end + + def extract_values_from_collection(collection, value_method, selected) + if selected.is_a?(Proc) + collection.map do |element| + element.send(value_method) if selected.call(element) + end.compact + else + selected + end + end end class InstanceTag #:nodoc: @@ -398,16 +473,18 @@ module ActionView add_default_name_and_id(html_options) value = value(object) selected_value = options.has_key?(:selected) ? options[:selected] : value - content_tag("select", add_options(options_for_select(choices, selected_value), options, selected_value), html_options) + disabled_value = options.has_key?(:disabled) ? options[:disabled] : nil + content_tag("select", add_options(options_for_select(choices, :selected => selected_value, :disabled => disabled_value), options, selected_value), html_options) end def to_collection_select_tag(collection, value_method, text_method, options, html_options) html_options = html_options.stringify_keys add_default_name_and_id(html_options) value = value(object) + disabled_value = options.has_key?(:disabled) ? options[:disabled] : nil selected_value = options.has_key?(:selected) ? options[:selected] : value content_tag( - "select", add_options(options_from_collection_for_select(collection, value_method, text_method, selected_value), options, value), html_options + "select", add_options(options_from_collection_for_select(collection, value_method, text_method, :selected => selected_value, :disabled => disabled_value), options, value), html_options ) end diff --git a/actionpack/lib/action_view/helpers/form_tag_helper.rb b/actionpack/lib/action_view/helpers/form_tag_helper.rb index 4646bc118b..6d39a53adc 100644 --- a/actionpack/lib/action_view/helpers/form_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/form_tag_helper.rb @@ -360,8 +360,8 @@ module ActionView end if confirm = options.delete("confirm") - options["onclick"] ||= '' - options["onclick"] << "return #{confirm_javascript_function(confirm)};" + options["onclick"] ||= 'return true;' + options["onclick"] = "if (!#{confirm_javascript_function(confirm)}) return false; #{options['onclick']}" end tag :input, { "type" => "submit", "name" => "commit", "value" => value }.update(options.stringify_keys) diff --git a/actionpack/lib/action_view/helpers/number_helper.rb b/actionpack/lib/action_view/helpers/number_helper.rb index e622f97b9e..dea958deaf 100644 --- a/actionpack/lib/action_view/helpers/number_helper.rb +++ b/actionpack/lib/action_view/helpers/number_helper.rb @@ -15,6 +15,7 @@ module ActionView # * <tt>:country_code</tt> - Sets the country code for the phone number. # # ==== Examples + # number_to_phone(5551234) # => 555-1234 # number_to_phone(1235551234) # => 123-555-1234 # number_to_phone(1235551234, :area_code => true) # => (123) 555-1234 # number_to_phone(1235551234, :delimiter => " ") # => 123 555 1234 @@ -37,7 +38,8 @@ module ActionView str << if area_code number.gsub!(/([0-9]{1,3})([0-9]{3})([0-9]{4}$)/,"(\\1) \\2#{delimiter}\\3") else - number.gsub!(/([0-9]{1,3})([0-9]{3})([0-9]{4})$/,"\\1#{delimiter}\\2#{delimiter}\\3") + number.gsub!(/([0-9]{0,3})([0-9]{3})([0-9]{4})$/,"\\1#{delimiter}\\2#{delimiter}\\3") + number.starts_with?('-') ? number.slice!(1..-1) : number end str << " x #{extension}" unless extension.blank? str @@ -138,7 +140,7 @@ module ActionView # number_with_delimiter(12345678) # => 12,345,678 # number_with_delimiter(12345678.05) # => 12,345,678.05 # number_with_delimiter(12345678, :delimiter => ".") # => 12.345.678 - # number_with_delimiter(12345678, :seperator => ",") # => 12,345,678 + # number_with_delimiter(12345678, :separator => ",") # => 12,345,678 # number_with_delimiter(98765432.98, :delimiter => " ", :separator => ",") # # => 98 765 432,98 # diff --git a/actionpack/lib/action_view/helpers/prototype_helper.rb b/actionpack/lib/action_view/helpers/prototype_helper.rb index 99676a9c27..6bad11e354 100644 --- a/actionpack/lib/action_view/helpers/prototype_helper.rb +++ b/actionpack/lib/action_view/helpers/prototype_helper.rb @@ -107,7 +107,7 @@ module ActionView # on the page in an Ajax response. module PrototypeHelper unless const_defined? :CALLBACKS - CALLBACKS = Set.new([ :uninitialized, :loading, :loaded, + CALLBACKS = Set.new([ :create, :uninitialized, :loading, :loaded, :interactive, :complete, :failure, :success ] + (100..599).to_a) AJAX_OPTIONS = Set.new([ :before, :after, :condition, :url, diff --git a/actionpack/lib/action_view/helpers/text_helper.rb b/actionpack/lib/action_view/helpers/text_helper.rb index b1eb6891fa..573b99b96e 100644 --- a/actionpack/lib/action_view/helpers/text_helper.rb +++ b/actionpack/lib/action_view/helpers/text_helper.rb @@ -107,7 +107,7 @@ module ActionView text else match = Array(phrases).map { |p| Regexp.escape(p) }.join('|') - text.gsub(/(#{match})(?!(?:[^<]*?)?(?:["'])[^<>]*>)/i, options[:highlighter]) + text.gsub(/(#{match})(?!(?:[^<]*?)(?:["'])[^<>]*>)/i, options[:highlighter]) end end @@ -324,7 +324,7 @@ module ActionView # Turns all URLs and e-mail addresses into clickable links. The <tt>:link</tt> option # will limit what should be linked. You can add HTML attributes to the links using - # <tt>:href_options</tt>. Possible values for <tt>:link</tt> are <tt>:all</tt> (default), + # <tt>:html</tt>. Possible values for <tt>:link</tt> are <tt>:all</tt> (default), # <tt>:email_addresses</tt>, and <tt>:urls</tt>. If a block is given, each URL and # e-mail address is yielded and the result is used as the link text. # @@ -341,7 +341,7 @@ module ActionView # # => "Visit http://www.loudthinking.com/ or e-mail <a href=\"mailto:david@loudthinking.com\">david@loudthinking.com</a>" # # post_body = "Welcome to my new blog at http://www.myblog.com/. Please e-mail me at me@email.com." - # auto_link(post_body, :href_options => { :target => '_blank' }) do |text| + # auto_link(post_body, :html => { :target => '_blank' }) do |text| # truncate(text, 15) # end # # => "Welcome to my new blog at <a href=\"http://www.myblog.com/\" target=\"_blank\">http://www.m...</a>. @@ -359,7 +359,7 @@ module ActionView # auto_link(post_body, :all, :target => "_blank") # => Once upon\na time # # => "Welcome to my new blog at <a href=\"http://www.myblog.com/\" target=\"_blank\">http://www.myblog.com</a>. # Please e-mail me at <a href=\"mailto:me@email.com\">me@email.com</a>." - def auto_link(text, *args, &block)#link = :all, href_options = {}, &block) + def auto_link(text, *args, &block)#link = :all, html = {}, &block) return '' if text.blank? options = args.size == 2 ? {} : args.extract_options! # this is necessary because the old auto_link API has a Hash as its last parameter @@ -536,8 +536,9 @@ module ActionView text.gsub(AUTO_LINK_RE) do href = $& punctuation = '' - # detect already linked URLs - if $` =~ /<a\s[^>]*href="$/ + left, right = $`, $' + # detect already linked URLs and URLs in the middle of a tag + if left =~ /<[^>]+$/ && right =~ /^[^>]*>/ # do not change string; URL is alreay linked href else diff --git a/actionpack/lib/action_view/helpers/translation_helper.rb b/actionpack/lib/action_view/helpers/translation_helper.rb index dc41ef5305..4aed10f640 100644 --- a/actionpack/lib/action_view/helpers/translation_helper.rb +++ b/actionpack/lib/action_view/helpers/translation_helper.rb @@ -3,19 +3,37 @@ require 'action_view/helpers/tag_helper' module ActionView module Helpers module TranslationHelper + # Delegates to I18n#translate but also performs two additional functions. First, it'll catch MissingTranslationData exceptions + # and turn them into inline spans that contains the missing key, such that you can see in a view what is missing where. + # + # Second, it'll scope the key by the current partial if the key starts with a period. So if you call translate(".foo") from the + # people/index.html.erb template, you'll actually be calling I18n.translate("people.index.foo"). This makes it less repetitive + # to translate many keys within the same partials and gives you a simple framework for scoping them consistently. If you don't + # prepend the key with a period, nothing is converted. def translate(key, options = {}) options[:raise] = true - I18n.translate(key, options) + I18n.translate(scope_key_by_partial(key), options) rescue I18n::MissingTranslationData => e keys = I18n.send(:normalize_translation_keys, e.locale, e.key, e.options[:scope]) content_tag('span', keys.join(', '), :class => 'translation_missing') end alias :t :translate + # Delegates to I18n.localize with no additional functionality. def localize(*args) I18n.localize *args end alias :l :localize + + + private + def scope_key_by_partial(key) + if key.to_s.first == "." + template.path_without_format_and_extension.gsub(%r{/_?}, ".") + key.to_s + else + key + end + end end end end
\ No newline at end of file diff --git a/actionpack/lib/action_view/helpers/url_helper.rb b/actionpack/lib/action_view/helpers/url_helper.rb index 2e0eb8766b..36e0a78e93 100644 --- a/actionpack/lib/action_view/helpers/url_helper.rb +++ b/actionpack/lib/action_view/helpers/url_helper.rb @@ -507,7 +507,30 @@ module ActionView # current_page?(:controller => 'shop', :action => 'checkout') # # => true # - # current_page?(:controller => 'shop', :action => 'checkout', :order => 'asc) + # current_page?(:controller => 'shop', :action => 'checkout', :order => 'asc') + # # => false + # + # current_page?(:action => 'checkout') + # # => true + # + # current_page?(:controller => 'library', :action => 'checkout') + # # => false + # + # Let's say we're in the <tt>/shop/checkout?order=desc&page=1</tt> action. + # + # current_page?(:action => 'process') + # # => false + # + # current_page?(:controller => 'shop', :action => 'checkout') + # # => true + # + # current_page?(:controller => 'shop', :action => 'checkout', :order => 'desc', :page=>'1') + # # => true + # + # current_page?(:controller => 'shop', :action => 'checkout', :order => 'desc', :page=>'2') + # # => false + # + # current_page?(:controller => 'shop', :action => 'checkout', :order => 'desc') # # => false # # current_page?(:action => 'checkout') @@ -516,7 +539,7 @@ module ActionView # current_page?(:controller => 'library', :action => 'checkout') # # => false def current_page?(options) - url_string = CGI.escapeHTML(url_for(options)) + url_string = CGI.unescapeHTML(url_for(options)) request = @controller.request # We ignore any extra parameters in the request_uri if the # submitted url doesn't have any either. This lets the function diff --git a/actionpack/lib/action_view/paths.rb b/actionpack/lib/action_view/paths.rb index 6c6d2ff979..b6bb9942ee 100644 --- a/actionpack/lib/action_view/paths.rb +++ b/actionpack/lib/action_view/paths.rb @@ -2,7 +2,11 @@ module ActionView #:nodoc: class PathSet < Array #:nodoc: def self.type_cast(obj) if obj.is_a?(String) - Template::EagerPath.new(obj) + if !Object.const_defined?(:Rails) || Rails.configuration.cache_classes + Template::EagerPath.new(obj) + else + Template::Path.new(obj) + end else obj end @@ -56,7 +60,7 @@ module ActionView #:nodoc: false end - def find_template(original_template_path, format = nil) + def find_template(original_template_path, format = nil, html_fallback = true) return original_template_path if original_template_path.respond_to?(:render) template_path = original_template_path.sub(/^\//, '') @@ -64,12 +68,16 @@ module ActionView #:nodoc: if template = load_path.find_by_parts(template_path, format) return template # Try to find html version if the format is javascript - elsif format == :js && template = load_path["#{template_path}.html"] + elsif format == :js && html_fallback && template = load_path["#{template_path}.#{I18n.locale}.html"] + return template + elsif format == :js && html_fallback && template = load_path["#{template_path}.html"] return template end end - Template.new(original_template_path, self) + return Template.new(original_template_path, original_template_path.to_s =~ /\A\// ? "" : ".") if File.file?(original_template_path) + + raise MissingTemplate.new(self, original_template_path, format) end end end diff --git a/actionpack/lib/action_view/render/rendering.rb b/actionpack/lib/action_view/render/rendering.rb index 2d8bdab100..a9b2acecd5 100644 --- a/actionpack/lib/action_view/render/rendering.rb +++ b/actionpack/lib/action_view/render/rendering.rb @@ -62,22 +62,19 @@ module ActionView end def _render_template(template, local_assigns = {}) - @_render_stack.push(template) + with_template(template) do + _evaluate_assigns_and_ivars + _set_controller_content_type(template.mime_type) if template.respond_to?(:mime_type) - _evaluate_assigns_and_ivars - _set_controller_content_type(template.mime_type) if template.respond_to?(:mime_type) - - result = template.render(self, local_assigns) do |*names| - if !instance_variable_defined?(:"@content_for_#{names.first}") && - instance_variable_defined?(:@_proc_for_layout) && (proc = @_proc_for_layout) - capture(*names, &proc) - elsif instance_variable_defined?(ivar = :"@content_for_#{names.first || :layout}") - instance_variable_get(ivar) - end + template.render(self, local_assigns) do |*names| + if !instance_variable_defined?(:"@content_for_#{names.first}") && + instance_variable_defined?(:@_proc_for_layout) && (proc = @_proc_for_layout) + capture(*names, &proc) + elsif instance_variable_defined?(ivar = :"@content_for_#{names.first || :layout}") + instance_variable_get(ivar) + end + end end - - @_render_stack.pop - result rescue Exception => e raise e if template.is_a?(InlineTemplate) || !template.filename if TemplateError === e diff --git a/actionpack/lib/action_view/template/renderable.rb b/actionpack/lib/action_view/template/renderable.rb index fde37544f3..0e8e0c4a38 100644 --- a/actionpack/lib/action_view/template/renderable.rb +++ b/actionpack/lib/action_view/template/renderable.rb @@ -1,3 +1,5 @@ +# encoding: utf-8 + module ActionView # NOTE: The template that this mixin is being included into is frozen # so you cannot set or modify any instance variables diff --git a/actionpack/lib/action_view/template/template.rb b/actionpack/lib/action_view/template/template.rb index 73e319b489..a61c46020f 100644 --- a/actionpack/lib/action_view/template/template.rb +++ b/actionpack/lib/action_view/template/template.rb @@ -50,7 +50,7 @@ module ActionView #:nodoc: def find_by_parts(name, extensions = nil, prefix = nil, partial = nil) path = prefix ? "#{prefix}/" : "" - name = name.split("/") + name = name.to_s.split("/") name[-1] = "_#{name[-1]}" if partial path << name.join("/") @@ -111,7 +111,7 @@ module ActionView #:nodoc: @@exempt_from_layout.merge(regexps) end - attr_accessor :filename, :load_path, :base_path + attr_accessor :template_path, :filename, :load_path, :base_path attr_accessor :locale, :name, :format, :extension delegate :to_s, :to => :path @@ -127,13 +127,20 @@ module ActionView #:nodoc: def accessible_paths paths = [] - paths << path - paths << path_without_extension - if multipart? - formats = format.split(".") - paths << "#{path_without_format_and_extension}.#{formats.first}" - paths << "#{path_without_format_and_extension}.#{formats.second}" + + if valid_extension?(extension) + paths << path + paths << path_without_extension + if multipart? + formats = format.split(".") + paths << "#{path_without_format_and_extension}.#{formats.first}" + paths << "#{path_without_format_and_extension}.#{formats.second}" + end + else + # template without explicit template handler should only be reachable through its exact path + paths << template_path end + paths end @@ -151,7 +158,7 @@ module ActionView #:nodoc: end def mime_type - Mime::Type.lookup_by_extension(format) if format + Mime::Type.lookup_by_extension(format) if format && defined?(::Mime) end memoize :mime_type @@ -230,7 +237,7 @@ module ActionView #:nodoc: # Returns file split into an array # [base_path, name, locale, format, extension] def split(file) - if m = file.match(/^(.*\/)?([^\.]+)\.(.*)$/) + if m = file.to_s.match(/^(.*\/)?([^\.]+)\.(.*)$/) base_path = m[1] name = m[2] extensions = m[3] @@ -242,24 +249,24 @@ module ActionView #:nodoc: format = nil extension = nil - if m = extensions.match(/^(\w+)?\.?(\w+)?\.?(\w+)?\.?/) - if valid_locale?(m[1]) && m[2] && valid_extension?(m[3]) # All three - locale = m[1] - format = m[2] - extension = m[3] - elsif m[1] && m[2] && valid_extension?(m[3]) # Multipart formats - format = "#{m[1]}.#{m[2]}" - extension = m[3] - elsif valid_locale?(m[1]) && valid_extension?(m[2]) # locale and extension - locale = m[1] - extension = m[2] - elsif valid_extension?(m[2]) # format and extension + if m = extensions.split(".") + if valid_locale?(m[0]) && m[1] && valid_extension?(m[2]) # All three + locale = m[0] format = m[1] extension = m[2] - elsif valid_extension?(m[1]) # Just extension + elsif m[0] && m[1] && valid_extension?(m[2]) # Multipart formats + format = "#{m[0]}.#{m[1]}" + extension = m[2] + elsif valid_locale?(m[0]) && valid_extension?(m[1]) # locale and extension + locale = m[0] + extension = m[1] + elsif valid_extension?(m[1]) # format and extension + format = m[0] extension = m[1] + elsif valid_extension?(m[0]) # Just extension + extension = m[0] else # No extension - format = m[1] + format = m[0] end end |