diff options
Diffstat (limited to 'actionview')
20 files changed, 202 insertions, 62 deletions
diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index 185f420472..f4a12360d7 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,18 @@ +* Add `PartialIteration` object used when rendering collections. + + The iteration object is available as the local variable + `#{template_name}_iteration` when rendering partials with collections. + + It gives access to the `size` of the collection being iterated over, + the current `index` and two convenience methods `first?` and `last?`. + + *Joel Junström*, *Lucas Uyezu* + +* Return an absolute instead of relative path from an asset url in the case + of the `asset_host` proc returning nil + + *Jolyon Pawlyn* + * Fix `html_escape_once` to properly handle hex escape sequences (e.g. ᨫ) *John F. Douthat* @@ -96,7 +111,7 @@ * Remove wrapping div with inline styles for hidden form fields. We are dropping HTML 4.01 and XHTML strict compliance since input tags directly - inside a form are valid HTML5, and the absense of inline styles help in validating + inside a form are valid HTML5, and the absence of inline styles help in validating for Content Security Policy. *Joost Baaij* diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb index 72d79735ae..1f103786cb 100644 --- a/actionview/lib/action_view/digestor.rb +++ b/actionview/lib/action_view/digestor.rb @@ -60,7 +60,7 @@ module ActionView def digest Digest::MD5.hexdigest("#{source}-#{dependency_digest}").tap do |digest| - logger.try :info, " Cache digest for #{template.inspect}: #{digest}" + logger.try :debug, " Cache digest for #{template.inspect}: #{digest}" end rescue ActionView::MissingTemplate logger.try :error, " Couldn't find template for digesting: #{name}" diff --git a/actionview/lib/action_view/helpers/asset_url_helper.rb b/actionview/lib/action_view/helpers/asset_url_helper.rb index 469f7c16bd..9e8d005ec7 100644 --- a/actionview/lib/action_view/helpers/asset_url_helper.rb +++ b/actionview/lib/action_view/helpers/asset_url_helper.rb @@ -203,7 +203,6 @@ module ActionView request = self.request if respond_to?(:request) host = options[:host] host ||= config.asset_host if defined? config.asset_host - host ||= request.base_url if request && options[:protocol] == :request if host.respond_to?(:call) arity = host.respond_to?(:arity) ? host.arity : host.method(:call).arity @@ -214,6 +213,7 @@ module ActionView host = host % (Zlib.crc32(source) % 4) end + host ||= request.base_url if request && options[:protocol] == :request return unless host if host =~ URI_REGEXP diff --git a/actionview/lib/action_view/helpers/form_options_helper.rb b/actionview/lib/action_view/helpers/form_options_helper.rb index 48f42947db..528e2828a1 100644 --- a/actionview/lib/action_view/helpers/form_options_helper.rb +++ b/actionview/lib/action_view/helpers/form_options_helper.rb @@ -152,11 +152,9 @@ module ActionView # To prevent this the helper generates an auxiliary hidden field before # every multiple select. The hidden field has the same name as multiple select and blank value. # - # This way, the client either sends only the hidden field (representing - # the deselected multiple select box), 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. + # <b>Note:</b> The client either sends only the hidden field (representing + # the deselected multiple select box), or both fields. This means that the resulting array + # always contains a blank string. # # In case if you don't want the helper to generate this hidden field you can specify # <tt>include_hidden: false</tt> option. diff --git a/actionview/lib/action_view/helpers/output_safety_helper.rb b/actionview/lib/action_view/helpers/output_safety_helper.rb index b0d9c7c7f9..f03362d0f5 100644 --- a/actionview/lib/action_view/helpers/output_safety_helper.rb +++ b/actionview/lib/action_view/helpers/output_safety_helper.rb @@ -17,7 +17,7 @@ module ActionView #:nodoc: stringish.to_s.html_safe end - # This method returns a html safe string similar to what <tt>Array#join</tt> + # This method returns an html safe string similar to what <tt>Array#join</tt> # would return. The array is flattened, and all items, including # the supplied separator, are html escaped unless they are html # safe, and the returned string is marked as html safe. diff --git a/actionview/lib/action_view/helpers/rendering_helper.rb b/actionview/lib/action_view/helpers/rendering_helper.rb index ebfc35a4c7..6cd6e858dd 100644 --- a/actionview/lib/action_view/helpers/rendering_helper.rb +++ b/actionview/lib/action_view/helpers/rendering_helper.rb @@ -13,13 +13,13 @@ module ActionView # * <tt>:inline</tt> - Renders an inline template similar to how it's done in the controller. # * <tt>:text</tt> - Renders the text passed in out. # * <tt>:plain</tt> - Renders the text passed in out. Setting the content - # type as <tt>text/plain</tt>. + # type as <tt>text/plain</tt>. # * <tt>:html</tt> - Renders the html safe string passed in out, otherwise - # performs html escape on the string first. Setting the content type as - # <tt>text/html</tt>. + # performs html escape on the string first. Setting the content type as + # <tt>text/html</tt>. # * <tt>:body</tt> - Renders the text passed in, and inherits the content - # type of <tt>text/html</tt> from <tt>ActionDispatch::Response</tt> - # object. + # type of <tt>text/html</tt> from <tt>ActionDispatch::Response</tt> + # object. # # If no options hash is passed or :update specified, the default is to render a partial and use the second parameter # as the locals hash. diff --git a/actionview/lib/action_view/helpers/tag_helper.rb b/actionview/lib/action_view/helpers/tag_helper.rb index 35444bcfb4..268558669e 100644 --- a/actionview/lib/action_view/helpers/tag_helper.rb +++ b/actionview/lib/action_view/helpers/tag_helper.rb @@ -9,6 +9,7 @@ module ActionView module TagHelper extend ActiveSupport::Concern include CaptureHelper + include OutputSafetyHelper BOOLEAN_ATTRIBUTES = %w(disabled readonly multiple checked autobuffer autoplay controls loop selected hidden scoped async diff --git a/actionview/lib/action_view/log_subscriber.rb b/actionview/lib/action_view/log_subscriber.rb index 6c8d9cb5bf..9047dbdd85 100644 --- a/actionview/lib/action_view/log_subscriber.rb +++ b/actionview/lib/action_view/log_subscriber.rb @@ -13,11 +13,11 @@ module ActionView end def render_template(event) - return unless logger.info? - message = " Rendered #{from_rails_root(event.payload[:identifier])}" - message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout] - message << " (#{event.duration.round(1)}ms)" - info(message) + info do + message = " Rendered #{from_rails_root(event.payload[:identifier])}" + message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout] + message << " (#{event.duration.round(1)}ms)" + end end alias :render_partial :render_template alias :render_collection :render_template diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb index 5fff6b0771..ea687d9cca 100644 --- a/actionview/lib/action_view/lookup_context.rb +++ b/actionview/lib/action_view/lookup_context.rb @@ -66,10 +66,7 @@ module ActionView def self.get(details) if details[:formats] details = details.dup - syms = Set.new Mime::SET.symbols - details[:formats] = details[:formats].select { |v| - syms.include? v - } + details[:formats] &= Mime::SET.symbols end @details_keys[details] ||= new end diff --git a/actionview/lib/action_view/renderer/abstract_renderer.rb b/actionview/lib/action_view/renderer/abstract_renderer.rb index 73c19a0ae2..1f122f9bc6 100644 --- a/actionview/lib/action_view/renderer/abstract_renderer.rb +++ b/actionview/lib/action_view/renderer/abstract_renderer.rb @@ -29,8 +29,9 @@ module ActionView def extract_details(options) @lookup_context.registered_details.each_with_object({}) do |key, details| - next unless value = options[key] - details[key] = Array(value) + value = options[key] + + details[key] = Array(value) if value end end @@ -41,6 +42,7 @@ module ActionView def prepend_formats(formats) formats = Array(formats) return if formats.empty? || @lookup_context.html_fallback_for_js + @lookup_context.formats = formats | @lookup_context.formats end end diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb index 36f17f01fd..0407632435 100644 --- a/actionview/lib/action_view/renderer/partial_renderer.rb +++ b/actionview/lib/action_view/renderer/partial_renderer.rb @@ -1,6 +1,33 @@ require 'thread_safe' module ActionView + class PartialIteration + # The number of iterations that will be done by the partial. + attr_reader :size + + # The current iteration of the partial. + attr_reader :index + + def initialize(size) + @size = size + @index = 0 + end + + # Check if this is the first iteration of the partial. + def first? + index == 0 + end + + # Check if this is the last iteration of the partial. + def last? + index == size - 1 + end + + def iterate! # :nodoc: + @index += 1 + end + end + # = Action View Partials # # There's also a convenience method for rendering sub templates within the current controller that depends on a @@ -56,8 +83,12 @@ module ActionView # <%= render partial: "ad", collection: @advertisements %> # # This will render "advertiser/_ad.html.erb" and pass the local variable +ad+ to the template for display. An - # iteration counter will automatically be made available to the template with a name of the form - # +partial_name_counter+. In the case of the example above, the template would be fed +ad_counter+. + # iteration object will automatically be made available to the template with a name of the form + # +partial_name_iteration+. The iteration object has knowledge about which index the current object has in + # the collection and the total size of the collection. The iteration object also has two convenience methods, + # +first?+ and +last?+. In the case of the example above, the template would be fed +ad_iteration+. + # For backwards compatibility the +partial_name_counter+ is still present and is mapped to the iteration's + # +index+ method. # # The <tt>:as</tt> option may be used when rendering partials. # @@ -281,6 +312,8 @@ module ActionView end end + private + def render_collection return nil if @collection.blank? @@ -322,25 +355,27 @@ module ActionView # respond to +to_partial_path+ in order to setup the path. def setup(context, options, block) @view = context - partial = options[:partial] - @options = options - @locals = options[:locals] || {} @block = block + + @locals = options[:locals] || {} @details = extract_details(options) prepend_formats(options[:formats]) + partial = options[:partial] + if String === partial @object = options[:object] + @collection = collection_from_options @path = partial - @collection = collection else @object = partial + @collection = collection_from_object || collection_from_options - if @collection = collection_from_object || collection + if @collection paths = @collection_data = @collection.map { |o| partial_path(o) } - @path = paths.uniq.size == 1 ? paths.first : nil + @path = paths.uniq.one? ? paths.first : nil else @path = partial_path end @@ -352,7 +387,7 @@ module ActionView end if @path - @variable, @variable_counter = retrieve_variable(@path, as) + @variable, @variable_counter, @variable_iteration = retrieve_variable(@path, as) @template_keys = retrieve_template_keys else paths.map! { |path| retrieve_variable(path, as).unshift(path) } @@ -361,7 +396,7 @@ module ActionView self end - def collection + def collection_from_options if @options.key?(:collection) collection = @options[:collection] collection.respond_to?(:to_ary) ? collection.to_ary : [] @@ -373,9 +408,7 @@ module ActionView end def find_partial - if path = @path - find_template(path, @template_keys) - end + find_template(@path, @template_keys) if @path end def find_template(path, locals) @@ -385,19 +418,22 @@ module ActionView def collection_with_template view, locals, template = @view, @locals, @template - as, counter = @variable, @variable_counter + as, counter, iteration = @variable, @variable_counter, @variable_iteration if layout = @options[:layout] layout = find_template(layout, @template_keys) end - index = -1 + partial_iteration = PartialIteration.new(@collection.size) + locals[iteration] = partial_iteration + @collection.map do |object| - locals[as] = object - locals[counter] = (index += 1) + locals[as] = object + locals[counter] = partial_iteration.index content = template.render(view, locals) content = layout.render(view, locals) { content } if layout + partial_iteration.iterate! content end end @@ -407,16 +443,20 @@ module ActionView cache = {} keys = @locals.keys - index = -1 + partial_iteration = PartialIteration.new(@collection.size) + @collection.map do |object| - index += 1 - path, as, counter = collection_data[index] + index = partial_iteration.index + path, as, counter, iteration = collection_data[index] - locals[as] = object - locals[counter] = index + locals[as] = object + locals[counter] = index + locals[iteration] = partial_iteration template = (cache[path] ||= find_template(path, keys + [as, counter])) - template.render(view, locals) + content = template.render(view, locals) + partial_iteration.iterate! + content end end @@ -466,8 +506,11 @@ module ActionView def retrieve_template_keys keys = @locals.keys - keys << @variable if @object || @collection - keys << @variable_counter if @collection + keys << @variable if @object || @collection + if @collection + keys << @variable_counter + keys << @variable_iteration + end keys end @@ -477,8 +520,11 @@ module ActionView raise_invalid_identifier(path) unless base =~ /\A_?([a-z]\w*)(\.\w+)*\z/ $1.to_sym end - variable_counter = :"#{variable}_counter" if @collection - [variable, variable_counter] + if @collection + variable_counter = :"#{variable}_counter" + variable_iteration = :"#{variable}_iteration" + end + [variable, variable_counter, variable_iteration] end IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " + diff --git a/actionview/lib/action_view/renderer/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb index be17097428..f3a48ecfa0 100644 --- a/actionview/lib/action_view/renderer/template_renderer.rb +++ b/actionview/lib/action_view/renderer/template_renderer.rb @@ -6,19 +6,18 @@ module ActionView @view = context @details = extract_details(options) template = determine_template(options) - context = @lookup_context prepend_formats(template.formats) - unless context.rendered_format - context.rendered_format = template.formats.first || formats.first - end + @lookup_context.rendered_format ||= (template.formats.first || formats.first) render_template(template, options[:layout], options[:locals]) end + private + # Determine the template to be rendered using the given options. - def determine_template(options) #:nodoc: + def determine_template(options) keys = options.fetch(:locals, {}).keys if options.key?(:body) diff --git a/actionview/test/actionpack/controller/render_test.rb b/actionview/test/actionpack/controller/render_test.rb index ab7b961ed2..b3b51ae583 100644 --- a/actionview/test/actionpack/controller/render_test.rb +++ b/actionview/test/actionpack/controller/render_test.rb @@ -536,6 +536,14 @@ class TestController < ApplicationController render :partial => "customer_with_var", :collection => [ Customer.new("david"), Customer.new("mary") ], :as => :customer end + def partial_collection_with_iteration + render partial: "customer_iteration", collection: [ Customer.new("david"), Customer.new("mary"), Customer.new('christine') ] + end + + def partial_collection_with_as_and_iteration + render partial: "customer_iteration_with_as", collection: [ Customer.new("david"), Customer.new("mary"), Customer.new('christine') ], as: :client + end + def partial_collection_with_counter render :partial => "customer_counter", :collection => [ Customer.new("david"), Customer.new("mary") ] end @@ -839,7 +847,7 @@ class RenderTest < ActionController::TestCase def test_render_text_with_nil get :render_text_with_nil assert_response 200 - assert_equal ' ', @response.body + assert_equal '', @response.body end # :ported: @@ -1027,7 +1035,7 @@ class RenderTest < ActionController::TestCase def test_rendering_nothing_on_layout get :rendering_nothing_on_layout - assert_equal " ", @response.body + assert_equal '', @response.body end def test_render_to_string_doesnt_break_assigns @@ -1237,6 +1245,16 @@ class RenderTest < ActionController::TestCase assert_equal "david david davidmary mary mary", @response.body end + def test_partial_collection_with_iteration + get :partial_collection_with_iteration + assert_equal "3-0: david-first3-1: mary3-2: christine-last", @response.body + end + + def test_partial_collection_with_as_and_iteration + get :partial_collection_with_as_and_iteration + assert_equal "3-0: david-first3-1: mary3-2: christine-last", @response.body + end + def test_partial_collection_with_counter get :partial_collection_with_counter assert_equal "david0mary1", @response.body diff --git a/actionview/test/fixtures/actionpack/test/_customer_iteration.erb b/actionview/test/fixtures/actionpack/test/_customer_iteration.erb new file mode 100644 index 0000000000..fb530b04a7 --- /dev/null +++ b/actionview/test/fixtures/actionpack/test/_customer_iteration.erb @@ -0,0 +1 @@ +<%= customer_iteration_iteration.size %>-<%= customer_iteration_iteration.index %>: <%= customer_iteration.name %><%= '-first' if customer_iteration_iteration.first? %><%= '-last' if customer_iteration_iteration.last? %>
\ No newline at end of file diff --git a/actionview/test/fixtures/actionpack/test/_customer_iteration_with_as.erb b/actionview/test/fixtures/actionpack/test/_customer_iteration_with_as.erb new file mode 100644 index 0000000000..57297d0564 --- /dev/null +++ b/actionview/test/fixtures/actionpack/test/_customer_iteration_with_as.erb @@ -0,0 +1 @@ +<%= client_iteration.size %>-<%= client_iteration.index %>: <%= client.name %><%= '-first' if client_iteration.first? %><%= '-last' if client_iteration.last? %>
\ No newline at end of file diff --git a/actionview/test/template/asset_tag_helper_test.rb b/actionview/test/template/asset_tag_helper_test.rb index 343681b5a9..d789a5ca27 100644 --- a/actionview/test/template/asset_tag_helper_test.rb +++ b/actionview/test/template/asset_tag_helper_test.rb @@ -546,6 +546,14 @@ class AssetTagHelperTest < ActionView::TestCase assert_equal "http://cdn.example.com/images/file.png", image_path("file.png") end + def test_image_url_with_asset_host_proc_returning_nil + @controller.config.asset_host = Proc.new { nil } + @controller.request = Struct.new(:base_url, :script_name).new("http://www.example.com", nil) + + assert_equal "/images/rails.png", image_path("rails.png") + assert_equal "http://www.example.com/images/rails.png", image_url("rails.png") + end + def test_caching_image_path_with_caching_and_proc_asset_host_using_request @controller.config.asset_host = Proc.new do |source, request| if request.ssl? diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb index 3e39dadcf1..a9f137aec6 100644 --- a/actionview/test/template/form_helper_test.rb +++ b/actionview/test/template/form_helper_test.rb @@ -694,6 +694,13 @@ class FormHelperTest < ActionView::TestCase ) end + def test_text_area_with_value_before_type_cast + assert_dom_equal( + %{<textarea id="post_id" name="post[id]">\n123</textarea>}, + text_area("post", "id") + ) + end + def test_text_area_with_html_entities @post.body = "The HTML Entity for & is &" assert_dom_equal( diff --git a/actionview/test/template/partial_iteration_test.rb b/actionview/test/template/partial_iteration_test.rb new file mode 100644 index 0000000000..695f9f1bef --- /dev/null +++ b/actionview/test/template/partial_iteration_test.rb @@ -0,0 +1,33 @@ +require 'abstract_unit' +require 'action_view/renderer/partial_renderer' + +class PartialIterationTest < ActiveSupport::TestCase + def test_has_size_and_index + iteration = ActionView::PartialIteration.new 3 + assert_equal 0, iteration.index, "should be at the first index" + assert_equal 3, iteration.size, "should have the size" + end + + def test_first_is_true_when_current_is_at_the_first_index + iteration = ActionView::PartialIteration.new 3 + assert iteration.first?, "first when current is 0" + end + + def test_first_is_false_unless_current_is_at_the_first_index + iteration = ActionView::PartialIteration.new 3 + iteration.iterate! + assert !iteration.first?, "not first when current is 1" + end + + def test_last_is_true_when_current_is_at_the_last_index + iteration = ActionView::PartialIteration.new 3 + iteration.iterate! + iteration.iterate! + assert iteration.last?, "last when current is 2" + end + + def test_last_is_false_unless_current_is_at_the_last_index + iteration = ActionView::PartialIteration.new 3 + assert !iteration.last?, "not last when current is 0" + end +end diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb index 67f1aabbd2..85817119ba 100644 --- a/actionview/test/template/render_test.rb +++ b/actionview/test/template/render_test.rb @@ -256,7 +256,7 @@ module RenderTestCases end def test_render_partial_collection_without_as - assert_equal "local_inspector,local_inspector_counter", + assert_equal "local_inspector,local_inspector_counter,local_inspector_iteration", @view.render(:partial => "test/local_inspector", :collection => [ Customer.new("mary") ]) end @@ -324,11 +324,16 @@ module RenderTestCases @controller_view.render(customers, :greeting => "Hello") end + def test_render_partial_using_collection_without_path + assert_equal "hi good customer: david0", @controller_view.render([ GoodCustomer.new("david") ], greeting: "hi") + end + def test_render_partial_without_object_or_collection_does_not_generate_partial_name_local_variable exception = assert_raises ActionView::Template::Error do @controller_view.render("partial_name_local_variable") end - assert_match "undefined local variable or method `partial_name_local_variable'", exception.message + assert_instance_of NameError, exception.original_exception + assert_equal :partial_name_local_variable, exception.original_exception.name end # TODO: The reason for this test is unclear, improve documentation @@ -387,6 +392,14 @@ module RenderTestCases ActionView::Template.unregister_template_handler :foo end + def test_render_body + assert_equal 'some body', @view.render(body: 'some body') + end + + def test_render_plain + assert_equal 'some plaintext', @view.render(plain: 'some plaintext') + end + def test_render_knows_about_types_registered_when_extensions_are_checked_earlier_in_initialization ActionView::Template::Handlers.extensions ActionView::Template.register_template_handler :foo, CustomHandler diff --git a/actionview/test/template/test_case_test.rb b/actionview/test/template/test_case_test.rb index 4ee0930341..4582fa13ee 100644 --- a/actionview/test/template/test_case_test.rb +++ b/actionview/test/template/test_case_test.rb @@ -1,4 +1,5 @@ require 'abstract_unit' +require 'rails/engine' module ActionView @@ -223,7 +224,7 @@ module ActionView test "is able to use mounted routes" do with_routing do |set| - app = Class.new do + app = Class.new(Rails::Engine) do def self.routes @routes ||= ActionDispatch::Routing::RouteSet.new end |