diff options
Diffstat (limited to 'actionview')
36 files changed, 520 insertions, 445 deletions
diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index d85681e0d1..c15fb4304d 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,42 @@ +* Added log "Rendering ...", when starting to render a template to log that + we have started rendering something. This helps to easily identify the origin + of queries in the log whether they came from controller or views. + + *Vipul A M and Prem Sichanugrist* + +## Rails 5.0.0.beta3 (February 24, 2016) ## + +* Collection rendering can cache and fetch multiple partials at once. + + Collections rendered as: + + ```ruby + <%= render partial: 'notifications/notification', collection: @notifications, as: :notification, cached: true %> + ``` + + will read several partials from cache at once. The templates in the collection + that haven't been cached already will automatically be written to cache. Works + great alongside individual template fragment caching. For instance if the + template the collection renders is cached like: + + ```ruby + # notifications/_notification.html.erb + <% cache notification do %> + <%# ... %> + <% end %> + ``` + + Then any collection renders shares that cache when attempting to read multiple + ones at once. + + *Kasper Timm Hansen* + +* Add support for nested hashes/arrays to `:params` option of `button_to` helper. + + *James Coleman* + +## Rails 5.0.0.beta2 (February 01, 2016) ## + * Fix stripping the digest from the automatically generated img tag alt attribute when assets are handled by Sprockets >=3.0. @@ -66,7 +105,7 @@ *Vasiliy Ermolovich* -* Add a `hidden_field` on the `collection_radio_buttons` to avoid raising a error +* Add a `hidden_field` on the `collection_radio_buttons` to avoid raising an error when the only input on the form is the `collection_radio_buttons`. *Mauro George* @@ -191,26 +230,6 @@ *Ulisses Almeida* -* Collection rendering automatically caches and fetches multiple partials. - - Collections rendered as: - - ```ruby - <%= render @notifications %> - <%= render partial: 'notifications/notification', collection: @notifications, as: :notification %> - ``` - - will now read several partials from cache at once, if the template starts with a cache call: - - ```ruby - # notifications/_notification.html.erb - <% cache notification do %> - <%# ... %> - <% end %> - ``` - - *Kasper Timm Hansen* - * Fixed a dependency tracker bug that caused template dependencies not count layouts as dependencies for partials. diff --git a/actionview/Rakefile b/actionview/Rakefile index 93be50721d..d41030c650 100644 --- a/actionview/Rakefile +++ b/actionview/Rakefile @@ -3,6 +3,9 @@ require 'rake/testtask' desc "Default Task" task :default => :test +task :package +task "package:clean" + # Run the unit tests desc "Run all unit tests" diff --git a/actionview/actionview.gemspec b/actionview/actionview.gemspec index 612e94021d..8b0e031dee 100644 --- a/actionview/actionview.gemspec +++ b/actionview/actionview.gemspec @@ -9,11 +9,11 @@ Gem::Specification.new do |s| s.required_ruby_version = '>= 2.2.2' - s.license = 'MIT' + s.license = 'MIT' - s.author = 'David Heinemeier Hansson' - s.email = 'david@loudthinking.com' - s.homepage = 'http://www.rubyonrails.org' + s.author = 'David Heinemeier Hansson' + s.email = 'david@loudthinking.com' + s.homepage = 'http://rubyonrails.org' s.files = Dir['CHANGELOG.md', 'README.rdoc', 'MIT-LICENSE', 'lib/**/*'] s.require_path = 'lib' diff --git a/actionview/lib/action_view/dependency_tracker.rb b/actionview/lib/action_view/dependency_tracker.rb index 5a4c3ea3fe..7731773040 100644 --- a/actionview/lib/action_view/dependency_tracker.rb +++ b/actionview/lib/action_view/dependency_tracker.rb @@ -7,18 +7,20 @@ module ActionView def self.find_dependencies(name, template, view_paths = nil) tracker = @trackers[template.handler] - return [] unless tracker.present? + return [] unless tracker - if tracker.respond_to?(:supports_view_paths?) && tracker.supports_view_paths? - tracker.call(name, template, view_paths) - else - tracker.call(name, template) - end + tracker.call(name, template, view_paths) end def self.register_tracker(extension, tracker) handler = Template.handler_for_extension(extension) - @trackers[handler] = tracker + if tracker.respond_to?(:supports_view_paths?) + @trackers[handler] = tracker + else + @trackers[handler] = lambda { |name, template, _| + tracker.call(name, template) + } + end end def self.remove_tracker(handler) @@ -151,11 +153,11 @@ module ActionView def resolve_directories(wildcard_dependencies) return [] unless @view_paths - wildcard_dependencies.each_with_object([]) do |query, templates| - @view_paths.find_all_with_query(query).each do |template| - templates << "#{File.dirname(query)}/#{File.basename(template).split('.').first}" + wildcard_dependencies.flat_map { |query, templates| + @view_paths.find_all_with_query(query).map do |template| + "#{File.dirname(query)}/#{File.basename(template).split('.').first}" end - end + }.sort end def explicit_dependencies diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb index 6f2f9ca53c..b99d1af998 100644 --- a/actionview/lib/action_view/digestor.rb +++ b/actionview/lib/action_view/digestor.rb @@ -4,13 +4,11 @@ require 'monitor' module ActionView class Digestor - cattr_reader(:cache) - @@cache = Concurrent::Map.new - @@digest_monitor = Monitor.new + @@digest_mutex = Mutex.new class PerRequestDigestCacheExpiry < Struct.new(:app) # :nodoc: def call(env) - ActionView::Digestor.cache.clear + ActionView::LookupContext::DetailsKey.clear app.call(env) end end @@ -22,106 +20,104 @@ module ActionView # * <tt>finder</tt> - An instance of <tt>ActionView::LookupContext</tt> # * <tt>dependencies</tt> - An array of dependent views # * <tt>partial</tt> - Specifies whether the template is a partial - def digest(options) - options.assert_valid_keys(:name, :finder, :dependencies, :partial) - - cache_key = ([ options[:name], options[:finder].details_key.hash ].compact + Array.wrap(options[:dependencies])).join('.') + def digest(name:, finder:, dependencies: []) + dependencies ||= [] + cache_key = ([ name ].compact + dependencies).join('.') # this is a correctly done double-checked locking idiom # (Concurrent::Map's lookups have volatile semantics) - @@cache[cache_key] || @@digest_monitor.synchronize do - @@cache.fetch(cache_key) do # re-check under lock - compute_and_store_digest(cache_key, options) + finder.digest_cache[cache_key] || @@digest_mutex.synchronize do + finder.digest_cache.fetch(cache_key) do # re-check under lock + partial = name.include?("/_") + root = tree(name, finder, partial) + dependencies.each do |injected_dep| + root.children << Injected.new(injected_dep, nil, nil) + end + finder.digest_cache[cache_key] = root.digest(finder) end end end - private - def compute_and_store_digest(cache_key, options) # called under @@digest_monitor lock - klass = if options[:partial] || options[:name].include?("/_") - # Prevent re-entry or else recursive templates will blow the stack. - # There is no need to worry about other threads seeing the +false+ value, - # as they will then have to wait for this thread to let go of the @@digest_monitor lock. - pre_stored = @@cache.put_if_absent(cache_key, false).nil? # put_if_absent returns nil on insertion - PartialDigestor - else - Digestor - end + def logger + ActionView::Base.logger || NullLogger + end - @@cache[cache_key] = stored_digest = klass.new(options).digest - ensure - # something went wrong or ActionView::Resolver.caching? is false, make sure not to corrupt the @@cache - @@cache.delete_pair(cache_key, false) if pre_stored && !stored_digest - end - end + # Create a dependency tree for template named +name+. + def tree(name, finder, partial = false, seen = {}) + logical_name = name.gsub(%r|/_|, "/") - attr_reader :name, :finder, :options + if finder.disable_cache { finder.exists?(logical_name, [], partial) } + template = finder.disable_cache { finder.find(logical_name, [], partial) } - def initialize(options) - @name, @finder = options.values_at(:name, :finder) - @options = options.except(:name, :finder) - end + if node = seen[template.identifier] # handle cycles in the tree + node + else + node = seen[template.identifier] = Node.create(name, logical_name, template, partial) - def digest - Digest::MD5.hexdigest("#{source}-#{dependency_digest}").tap do |digest| - logger.try :debug, " Cache digest for #{template.inspect}: #{digest}" + deps = DependencyTracker.find_dependencies(name, template, finder.view_paths) + deps.uniq { |n| n.gsub(%r|/_|, "/") }.each do |dep_file| + node.children << tree(dep_file, finder, true, seen) + end + node + end + else + logger.error " '#{name}' file doesn't exist, so no dependencies" + logger.error " Couldn't find template for digesting: #{name}" + seen[name] ||= Missing.new(name, logical_name, nil) + end end - rescue ActionView::MissingTemplate - logger.try :error, " Couldn't find template for digesting: #{name}" - '' end - def dependencies - DependencyTracker.find_dependencies(name, template, finder.view_paths) - rescue ActionView::MissingTemplate - logger.try :error, " '#{name}' file doesn't exist, so no dependencies" - [] - end + class Node + attr_reader :name, :logical_name, :template, :children - def nested_dependencies - dependencies.collect do |dependency| - dependencies = PartialDigestor.new(name: dependency, finder: finder).nested_dependencies - dependencies.any? ? { dependency => dependencies } : dependency + def self.create(name, logical_name, template, partial) + klass = partial ? Partial : Node + klass.new(name, logical_name, template, []) end - end - private - def logger - ActionView::Base.logger - end - - def logical_name - name.gsub(%r|/_|, "/") + def initialize(name, logical_name, template, children = []) + @name = name + @logical_name = logical_name + @template = template + @children = children end - def partial? - false + def digest(finder, stack = []) + Digest::MD5.hexdigest("#{template.source}-#{dependency_digest(finder, stack)}") end - def template - @template ||= finder.disable_cache { finder.find(logical_name, [], partial?) } + def dependency_digest(finder, stack) + children.map do |node| + if stack.include?(node) + false + else + finder.digest_cache[node.name] ||= begin + stack.push node + node.digest(finder, stack).tap { stack.pop } + end + end + end.join("-") end - def source - template.source + def to_dep_map + children.any? ? { name => children.map(&:to_dep_map) } : name end + end - def dependency_digest - template_digests = dependencies.collect do |template_name| - Digestor.digest(name: template_name, finder: finder, partial: true) - end + class Partial < Node; end - (template_digests + injected_dependencies).join("-") - end + class Missing < Node + def digest(finder, _ = []) '' end + end - def injected_dependencies - Array.wrap(options[:dependencies]) - end - end + class Injected < Node + def digest(finder, _ = []) name end + end - class PartialDigestor < Digestor # :nodoc: - def partial? - true + class NullLogger + def self.debug(_); end + def self.error(_); end end end end diff --git a/actionview/lib/action_view/gem_version.rb b/actionview/lib/action_view/gem_version.rb index 23d5319579..efb565bf59 100644 --- a/actionview/lib/action_view/gem_version.rb +++ b/actionview/lib/action_view/gem_version.rb @@ -8,7 +8,7 @@ module ActionView MAJOR = 5 MINOR = 0 TINY = 0 - PRE = "beta1.1" + PRE = "beta3" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/actionview/lib/action_view/helpers/atom_feed_helper.rb b/actionview/lib/action_view/helpers/atom_feed_helper.rb index dba70e284e..c875f5870f 100644 --- a/actionview/lib/action_view/helpers/atom_feed_helper.rb +++ b/actionview/lib/action_view/helpers/atom_feed_helper.rb @@ -132,7 +132,7 @@ module ActionView end private - # Delegate to xml builder, first wrapping the element in a xhtml + # Delegate to xml builder, first wrapping the element in an xhtml # namespaced div element if the method and arguments indicate # that an xhtml_block? is desired. def method_missing(method, *arguments, &block) diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb index 18b2102d73..4c7c4b91c6 100644 --- a/actionview/lib/action_view/helpers/cache_helper.rb +++ b/actionview/lib/action_view/helpers/cache_helper.rb @@ -126,47 +126,33 @@ module ActionView # # Now all you have to do is change that timestamp when the helper method changes. # - # === Automatic Collection Caching + # === Collection Caching # - # When rendering collections such as: + # When rendering a collection of objects that each use the same partial, a `cached` + # option can be passed. + # For collections rendered such: # - # <%= render @notifications %> - # <%= render partial: 'notifications/notification', collection: @notifications %> + # <%= render partial: 'notifications/notification', collection: @notifications, cached: true %> # - # If the notifications/_notification partial starts with a cache call as: + # The `cached: true` will make Action View's rendering read several templates + # from cache at once instead of one call per template. # - # <% cache notification do %> - # <%= notification.name %> - # <% end %> - # - # The collection can then automatically use any cached renders for that - # template by reading them at once instead of one by one. - # - # See ActionView::Template::Handlers::ERB.resource_cache_call_pattern for - # more information on what cache calls make a template eligible for this - # collection caching. - # - # The automatic cache multi read can be turned off like so: + # Templates in the collection not already cached are written to cache. # - # <%= render @notifications, cache: false %> + # Works great alongside individual template fragment caching. + # For instance if the template the collection renders is cached like: # - # === Explicit Collection Caching - # - # If the partial template doesn't start with a clean cache call as - # mentioned above, you can still benefit from collection caching by - # adding a special comment format anywhere in the template, like: - # - # <%# Template Collection: notification %> - # <% my_helper_that_calls_cache(some_arg, notification) do %> - # <%= notification.name %> + # # notifications/_notification.html.erb + # <% cache notification do %> + # <%# ... %> # <% end %> # - # The pattern used to match these is <tt>/# Template Collection: (\S+)/</tt>, - # so it's important that you type it out just so. - # You can only declare one collection in a partial template file. + # Any collection renders will find those cached templates when attempting + # to read multiple templates at once. def cache(name = {}, options = {}, &block) if controller.respond_to?(:perform_caching) && controller.perform_caching - safe_concat(fragment_for(cache_fragment_name(name, options), options, &block)) + name_options = options.slice(:skip_digest, :virtual_path) + safe_concat(fragment_for(cache_fragment_name(name, name_options), options, &block)) else yield end diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index c1015ffe89..e91b3443c5 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -1922,8 +1922,6 @@ module ActionView @object_name.to_s.humanize end - model = model.downcase - defaults = [] defaults << :"helpers.submit.#{object_name}.#{key}" defaults << :"helpers.submit.#{key}" diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb index 55dac74d00..82e9ace428 100644 --- a/actionview/lib/action_view/helpers/form_tag_helper.rb +++ b/actionview/lib/action_view/helpers/form_tag_helper.rb @@ -862,13 +862,13 @@ module ActionView def extra_tags_for_form(html_options) authenticity_token = html_options.delete("authenticity_token") - method = html_options.delete("method").to_s + method = html_options.delete("method").to_s.downcase method_tag = case method - when /^get$/i # must be case-insensitive, but can't use downcase as might be nil + when 'get' html_options["method"] = "get" '' - when /^post$/i, "", nil + when 'post', '' html_options["method"] = "post" token_tag(authenticity_token, form_options: { action: html_options["action"], diff --git a/actionview/lib/action_view/helpers/tag_helper.rb b/actionview/lib/action_view/helpers/tag_helper.rb index 2562504896..42e7358a1d 100644 --- a/actionview/lib/action_view/helpers/tag_helper.rb +++ b/actionview/lib/action_view/helpers/tag_helper.rb @@ -154,6 +154,7 @@ module ActionView options.each_pair do |key, value| if TAG_PREFIXES.include?(key) && value.is_a?(Hash) value.each_pair do |k, v| + next if v.nil? output << sep output << prefix_tag_option(key, k, v, escape) end diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb index 3a4561a083..11c7daf4da 100644 --- a/actionview/lib/action_view/helpers/url_helper.rb +++ b/actionview/lib/action_view/helpers/url_helper.rb @@ -302,7 +302,7 @@ module ActionView params = html_options.delete('params') method = html_options.delete('method').to_s - method_tag = BUTTON_TAG_METHOD_VERBS.include?(method) ? method_tag(method) : ''.html_safe + method_tag = BUTTON_TAG_METHOD_VERBS.include?(method) ? method_tag(method) : ''.freeze.html_safe form_method = method == 'get' ? 'get' : 'post' form_options = html_options.delete('form') || {} @@ -312,9 +312,10 @@ module ActionView form_options[:'data-remote'] = true if remote request_token_tag = if form_method == 'post' - token_tag(nil, form_options: form_options) + request_method = method.empty? ? 'post' : method + token_tag(nil, form_options: { action: url, method: request_method }) else - '' + ''.freeze end html_options = convert_options_to_data_attributes(options, html_options) @@ -329,8 +330,8 @@ module ActionView inner_tags = method_tag.safe_concat(button).safe_concat(request_token_tag) if params - params.each do |param_name, value| - inner_tags.safe_concat tag(:input, type: "hidden", name: param_name, value: value.to_param) + to_form_params(params).each do |param| + inner_tags.safe_concat tag(:input, type: "hidden", name: param[:name], value: param[:value]) end end content_tag('form', inner_tags, form_options) @@ -480,7 +481,7 @@ module ActionView option = html_options.delete(item).presence || next "#{item.dasherize}=#{ERB::Util.url_encode(option)}" }.compact - extras = extras.empty? ? '' : '?' + extras.join('&') + extras = extras.empty? ? ''.freeze : '?' + extras.join('&') encoded_email_address = ERB::Util.url_encode(email_address).gsub("%40", "@") html_options["href"] = "mailto:#{encoded_email_address}#{extras}" @@ -558,29 +559,29 @@ module ActionView def convert_options_to_data_attributes(options, html_options) if html_options html_options = html_options.stringify_keys - html_options['data-remote'] = 'true' if link_to_remote_options?(options) || link_to_remote_options?(html_options) + html_options['data-remote'] = 'true'.freeze if link_to_remote_options?(options) || link_to_remote_options?(html_options) - method = html_options.delete('method') + method = html_options.delete('method'.freeze) add_method_to_attributes!(html_options, method) if method html_options else - link_to_remote_options?(options) ? {'data-remote' => 'true'} : {} + link_to_remote_options?(options) ? {'data-remote' => 'true'.freeze} : {} end end def link_to_remote_options?(options) if options.is_a?(Hash) - options.delete('remote') || options.delete(:remote) + options.delete('remote'.freeze) || options.delete(:remote) end end def add_method_to_attributes!(html_options, method) - if method && method.to_s.downcase != "get" && html_options["rel"] !~ /nofollow/ - html_options["rel"] = "#{html_options["rel"]} nofollow".lstrip + if method && method.to_s.downcase != "get".freeze && html_options["rel".freeze] !~ /nofollow/ + html_options["rel".freeze] = "#{html_options["rel".freeze]} nofollow".lstrip end - html_options["data-method"] = method + html_options["data-method".freeze] = method end def token_tag(token=nil, form_options: {}) @@ -588,13 +589,49 @@ module ActionView token ||= form_authenticity_token(form_options: form_options) tag(:input, type: "hidden", name: request_forgery_protection_token.to_s, value: token) else - '' + ''.freeze end end def method_tag(method) tag('input', type: 'hidden', name: '_method', value: method.to_s) end + + # Returns an array of hashes each containing :name and :value keys + # suitable for use as the names and values of form input fields: + # + # to_form_params(name: 'David', nationality: 'Danish') + # # => [{name: :name, value: 'David'}, {name: 'nationality', value: 'Danish'}] + # + # to_form_params(country: {name: 'Denmark'}) + # # => [{name: 'country[name]', value: 'Denmark'}] + # + # to_form_params(countries: ['Denmark', 'Sweden']}) + # # => [{name: 'countries[]', value: 'Denmark'}, {name: 'countries[]', value: 'Sweden'}] + # + # An optional namespace can be passed to enclose key names: + # + # to_form_params({ name: 'Denmark' }, 'country') + # # => [{name: 'country[name]', value: 'Denmark'}] + def to_form_params(attribute, namespace = nil) # :nodoc: + params = [] + case attribute + when Hash + attribute.each do |key, value| + prefix = namespace ? "#{namespace}[#{key}]" : key + params.push(*to_form_params(value, prefix)) + end + when Array + array_prefix = "#{namespace}[]" + attribute.each do |value| + params.push(*to_form_params(value, array_prefix)) + end + else + params << { name: namespace, value: attribute.to_param } + end + + params.sort_by { |pair| pair[:name] } + end end end end diff --git a/actionview/lib/action_view/log_subscriber.rb b/actionview/lib/action_view/log_subscriber.rb index 9047dbdd85..5a29c68214 100644 --- a/actionview/lib/action_view/log_subscriber.rb +++ b/actionview/lib/action_view/log_subscriber.rb @@ -20,7 +20,23 @@ module ActionView end end alias :render_partial :render_template - alias :render_collection :render_template + + def render_collection(event) + identifier = event.payload[:identifier] || 'templates' + + info do + " Rendered collection of #{from_rails_root(identifier)}" \ + " #{render_count(event.payload)} (#{event.duration.round(1)}ms)" + end + end + + def start(name, id, payload) + if name == "render_template.action_view" + log_rendering_start(payload) + end + + super + end def logger ActionView::Base.logger @@ -38,6 +54,24 @@ module ActionView def rails_root @root ||= "#{Rails.root}/" end + + def render_count(payload) + if payload[:cache_hits] + "[#{payload[:cache_hits]} / #{payload[:count]} cache hits]" + else + "[#{payload[:count]} times]" + end + end + + private + + def log_rendering_start(payload) + info do + message = " Rendering #{from_rails_root(payload[:identifier])}" + message << " within #{from_rails_root(payload[:layout])}" if payload[:layout] + message + end + end end end diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb index 6a76d80c47..626c4b8f5e 100644 --- a/actionview/lib/action_view/lookup_context.rb +++ b/actionview/lib/action_view/lookup_context.rb @@ -22,7 +22,7 @@ module ActionView def self.register_detail(name, &block) self.registered_details << name - initialize = registered_details.map { |n| "@details[:#{n}] = details[:#{n}] || default_#{n}" } + Accessors::DEFAULT_PROCS[name] = block Accessors.send :define_method, :"default_#{name}", &block Accessors.module_eval <<-METHOD, __FILE__, __LINE__ + 1 @@ -34,16 +34,12 @@ module ActionView value = value.present? ? Array(value) : default_#{name} _set_detail(:#{name}, value) if value != @details[:#{name}] end - - remove_possible_method :initialize_details - def initialize_details(details) - #{initialize.join("\n")} - end METHOD end # Holds accessors for the registered details. module Accessors #:nodoc: + DEFAULT_PROCS = {} end register_detail(:locale) do @@ -59,9 +55,7 @@ module ActionView class DetailsKey #:nodoc: alias :eql? :equal? - alias :object_hash :hash - attr_reader :hash @details_keys = Concurrent::Map.new def self.get(details) @@ -76,8 +70,14 @@ module ActionView @details_keys.clear end + def self.digest_caches + @details_keys.values.map(&:digest_cache) + end + + attr_reader :digest_cache + def initialize - @hash = object_hash + @digest_cache = Concurrent::Map.new end end @@ -136,6 +136,11 @@ module ActionView end alias :template_exists? :exists? + def any?(name, prefixes = [], partial = false) + @view_paths.exists?(*args_for_any(name, prefixes, partial)) + end + alias :any_templates? :any? + # Adds fallbacks to the view paths. Useful in cases when you are rendering # a :file. def with_fallbacks @@ -172,6 +177,32 @@ module ActionView [user_details, details_key] end + def args_for_any(name, prefixes, partial) # :nodoc: + name, prefixes = normalize_name(name, prefixes) + details, details_key = detail_args_for_any + [name, prefixes, partial || false, details, details_key] + end + + def detail_args_for_any # :nodoc: + @detail_args_for_any ||= begin + details = {} + + registered_details.each do |k| + if k == :variants + details[k] = :any + else + details[k] = Accessors::DEFAULT_PROCS[k].call + end + end + + if @cache + [details, DetailsKey.get(details)] + else + [details, nil] + end + end + end + # Support legacy foo.erb names even though we now ignore .erb # as well as incorrectly putting part of the path in the template # name instead of the prefix. @@ -195,15 +226,27 @@ module ActionView include ViewPaths def initialize(view_paths, details = {}, prefixes = []) - @details, @details_key = {}, nil + @details_key = nil @cache = true @prefixes = prefixes @rendered_format = nil + @details = initialize_details({}, details) self.view_paths = view_paths - initialize_details(details) end + def digest_cache + details_key.digest_cache + end + + def initialize_details(target, details) + registered_details.each do |k| + target[k] = details[k] || Accessors::DEFAULT_PROCS[k].call + end + target + end + private :initialize_details + # Override formats= to expand ["*/*"] values and automatically # add :html as fallback to :js. def formats=(values) diff --git a/actionview/lib/action_view/railtie.rb b/actionview/lib/action_view/railtie.rb index 59d869d92d..df14ae09f4 100644 --- a/actionview/lib/action_view/railtie.rb +++ b/actionview/lib/action_view/railtie.rb @@ -59,7 +59,7 @@ module ActionView rake_tasks do |app| unless app.config.api_only - load "action_view/tasks/dependencies.rake" + load "action_view/tasks/cache_digests.rake" end end end diff --git a/actionview/lib/action_view/renderer/abstract_renderer.rb b/actionview/lib/action_view/renderer/abstract_renderer.rb index aa77a77acf..1dddf53df0 100644 --- a/actionview/lib/action_view/renderer/abstract_renderer.rb +++ b/actionview/lib/action_view/renderer/abstract_renderer.rb @@ -15,7 +15,7 @@ module ActionView # that new object is called in turn. This abstracts the setup and rendering # into a separate classes for partials and templates. class AbstractRenderer #:nodoc: - delegate :find_template, :find_file, :template_exists?, :with_fallbacks, :with_layout_format, :formats, :to => :@lookup_context + delegate :find_template, :find_file, :template_exists?, :any_templates?, :with_fallbacks, :with_layout_format, :formats, :to => :@lookup_context def initialize(lookup_context) @lookup_context = lookup_context @@ -35,8 +35,12 @@ module ActionView end end - def instrument(name, options={}) - ActiveSupport::Notifications.instrument("render_#{name}.action_view", options){ yield } + def instrument(name, **options) + options[:identifier] ||= (@template && @template.identifier) || @path + + ActiveSupport::Notifications.instrument("render_#{name}.action_view", options) do |payload| + yield payload + end end def prepend_formats(formats) diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb index bdbf03191a..13b4ec6133 100644 --- a/actionview/lib/action_view/renderer/partial_renderer.rb +++ b/actionview/lib/action_view/renderer/partial_renderer.rb @@ -294,7 +294,7 @@ module ActionView def render(context, options, block) setup(context, options, block) - identifier = (@template = find_partial) ? @template.identifier : @path + @template = find_partial @lookup_context.rendered_format ||= begin if @template && @template.formats.present? @@ -305,11 +305,9 @@ module ActionView end if @collection - instrument(:collection, :identifier => identifier || "collection", :count => @collection.size) do - render_collection - end + render_collection else - instrument(:partial, :identifier => identifier) do + instrument(:partial) do render_partial end end @@ -318,15 +316,17 @@ module ActionView private def render_collection - return nil if @collection.blank? + instrument(:collection, count: @collection.size) do |payload| + return nil if @collection.blank? - if @options.key?(:spacer_template) - spacer = find_template(@options[:spacer_template], @locals.keys).render(@view, @locals) - end + if @options.key?(:spacer_template) + spacer = find_template(@options[:spacer_template], @locals.keys).render(@view, @locals) + end - cache_collection_render do - @template ? collection_with_template : collection_without_template - end.join(spacer).html_safe + cache_collection_render(payload) do + @template ? collection_with_template : collection_without_template + end.join(spacer).html_safe + end end def render_partial @@ -521,7 +521,7 @@ module ActionView def retrieve_variable(path, as) variable = as || begin base = path[-1] == "/".freeze ? "".freeze : File.basename(path) - raise_invalid_identifier(path) unless base =~ /\A_?(.*)(?:\.\w+)*\z/ + raise_invalid_identifier(path) unless base =~ /\A_?(.*?)(?:\.\w+)*\z/ $1.to_sym end if @collection diff --git a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb index 1147963882..f7deba94ce 100644 --- a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb +++ b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/object/try' - module ActionView module CollectionCaching # :nodoc: extend ActiveSupport::Concern @@ -11,42 +9,25 @@ module ActionView end private - def cache_collection_render - return yield unless cache_collection? + def cache_collection_render(instrumentation_payload) + return yield unless @options[:cached] keyed_collection = collection_by_cache_keys - partial_cache = collection_cache.read_multi(*keyed_collection.keys) + cached_partials = collection_cache.read_multi(*keyed_collection.keys) + instrumentation_payload[:cache_hits] = cached_partials.size - @collection = keyed_collection.reject { |key, _| partial_cache.key?(key) }.values - rendered_partials = @collection.any? ? yield.dup : [] + @collection = keyed_collection.reject { |key, _| cached_partials.key?(key) }.values + rendered_partials = @collection.empty? ? [] : yield - fetch_or_cache_partial(partial_cache, order_by: keyed_collection.each_key) do - rendered_partials.shift + index = 0 + fetch_or_cache_partial(cached_partials, order_by: keyed_collection.each_key) do + rendered_partials[index].tap { index += 1 } end end - def cache_collection? - @options.fetch(:cache, automatic_cache_eligible?) - end - - def automatic_cache_eligible? - single_template_render? && !callable_cache_key? && - @template.eligible_for_collection_caching?(as: @options[:as]) - end - - def single_template_render? - @template # Template is only set when a collection renders one template. - end - - def callable_cache_key? - @options[:cache].respond_to?(:call) - end - def collection_by_cache_keys - seed = callable_cache_key? ? @options[:cache] : ->(i) { i } - @collection.each_with_object({}) do |item, hash| - hash[expanded_cache_key(seed.call(item))] = item + hash[expanded_cache_key(item)] = item end end @@ -56,12 +37,10 @@ module ActionView end def fetch_or_cache_partial(cached_partials, order_by:) - cache_options = @options[:cache_options] || @locals[:cache_options] || {} - - order_by.map do |key| - cached_partials.fetch(key) do + order_by.map do |cache_key| + cached_partials.fetch(cache_key) do yield.tap do |rendered_partial| - collection_cache.write(key, rendered_partial, cache_options) + collection_cache.write(cache_key, rendered_partial) end end end diff --git a/actionview/lib/action_view/tasks/dependencies.rake b/actionview/lib/action_view/tasks/cache_digests.rake index f394c319c1..045bdf5691 100644 --- a/actionview/lib/action_view/tasks/dependencies.rake +++ b/actionview/lib/action_view/tasks/cache_digests.rake @@ -2,13 +2,13 @@ namespace :cache_digests do desc 'Lookup nested dependencies for TEMPLATE (like messages/show or comments/_comment.html)' task :nested_dependencies => :environment do abort 'You must provide TEMPLATE for the task to run' unless ENV['TEMPLATE'].present? - puts JSON.pretty_generate ActionView::Digestor.new(name: CacheDigests.template_name, finder: CacheDigests.finder).nested_dependencies + puts JSON.pretty_generate ActionView::Digestor.tree(CacheDigests.template_name, CacheDigests.finder).children.map(&:to_dep_map) end desc 'Lookup first-level dependencies for TEMPLATE (like messages/show or comments/_comment.html)' task :dependencies => :environment do abort 'You must provide TEMPLATE for the task to run' unless ENV['TEMPLATE'].present? - puts JSON.pretty_generate ActionView::Digestor.new(name: CacheDigests.template_name, finder: CacheDigests.finder).dependencies + puts JSON.pretty_generate ActionView::Digestor.tree(CacheDigests.template_name, CacheDigests.finder).children.map(&:name) end class CacheDigests diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb index 15fc2b71a3..169ee55fdc 100644 --- a/actionview/lib/action_view/template.rb +++ b/actionview/lib/action_view/template.rb @@ -130,7 +130,6 @@ module ActionView @source = source @identifier = identifier @handler = handler - @cache_name = extract_resource_cache_name @compiled = false @original_encoding = nil @locals = details[:locals] || [] @@ -166,10 +165,6 @@ module ActionView @type ||= Types[@formats.first] if @formats.first end - def eligible_for_collection_caching?(as: nil) - @cache_name == (as || inferred_cache_name).to_s - end - # Receives a view object and return a template similar to self by using @virtual_path. # # This method is useful if you have a template object but it does not contain its source @@ -355,23 +350,5 @@ module ActionView ActiveSupport::Notifications.instrument("#{action}.action_view".freeze, payload, &block) end end - - EXPLICIT_COLLECTION = /# Template Collection: (?<resource_name>\w+)/ - - def extract_resource_cache_name - if match = @source.match(EXPLICIT_COLLECTION) || resource_cache_call_match - match[:resource_name] - end - end - - def resource_cache_call_match - if @handler.respond_to?(:resource_cache_call_pattern) - @source.match(@handler.resource_cache_call_pattern) - end - end - - def inferred_cache_name - @inferred_cache_name ||= @virtual_path.split('/'.freeze).last.sub('_'.freeze, ''.freeze) - end end end diff --git a/actionview/lib/action_view/template/error.rb b/actionview/lib/action_view/template/error.rb index b03b197cb5..3f38c3d2b9 100644 --- a/actionview/lib/action_view/template/error.rb +++ b/actionview/lib/action_view/template/error.rb @@ -59,6 +59,9 @@ module ActionView class Error < ActionViewError #:nodoc: SOURCE_CODE_RADIUS = 3 + # Override to prevent #cause resetting during re-raise. + attr_reader :cause + def initialize(template, original_exception = nil) if original_exception ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \ @@ -67,6 +70,7 @@ module ActionView super($!.message) set_backtrace($!.backtrace) + @cause = $! @template, @sub_templates = template, nil end @@ -131,13 +135,13 @@ module ActionView end def formatted_code_for(source_code, line_counter, indent, output) - start_value = (output == :html) ? {} : "" + start_value = (output == :html) ? {} : [] source_code.inject(start_value) do |result, line| line_counter += 1 if output == :html result.update(line_counter.to_s => "%#{indent}s %s\n" % ["", line]) else - result << "%#{indent}s: %s\n" % [line_counter, line] + result << "%#{indent}s: %s" % [line_counter, line] end end end diff --git a/actionview/lib/action_view/template/handlers/erb.rb b/actionview/lib/action_view/template/handlers/erb.rb index 1f8459c24b..85a100ed4c 100644 --- a/actionview/lib/action_view/template/handlers/erb.rb +++ b/actionview/lib/action_view/template/handlers/erb.rb @@ -123,31 +123,6 @@ module ActionView ).src end - # Returns Regexp to extract a cached resource's name from a cache call at the - # first line of a template. - # The extracted cache name is captured as :resource_name. - # - # <% cache notification do %> # => notification - # - # The pattern should support templates with a beginning comment: - # - # <%# Still extractable even though there's a comment %> - # <% cache notification do %> # => notification - # - # But fail to extract a name if a resource association is cached. - # - # <% cache notification.event do %> # => nil - def resource_cache_call_pattern - /\A - (?:<%\#.*%>)* # optional initial comment - \s* # followed by optional spaces or newlines - <%\s*cache[\(\s] # followed by an ERB call to cache - \s* # followed by optional spaces or newlines - (?<resource_name>\w+) # capture the cache call argument as :resource_name - [\s\)] # followed by a space or close paren - /xm - end - private def valid_encoding(string, encoding) diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb index 8a675cd521..f33acc2103 100644 --- a/actionview/lib/action_view/template/resolver.rb +++ b/actionview/lib/action_view/template/resolver.rb @@ -222,7 +222,7 @@ module ActionView end def find_template_paths(query) - Dir[query].reject do |filename| + Dir[query].uniq.reject do |filename| File.directory?(filename) || # deals with case-insensitive file systems. !File.fnmatch(query, filename, File::FNM_EXTGLOB) @@ -245,8 +245,12 @@ module ActionView partial = escape_entry(path.partial? ? "_#{path.name}" : path.name) query.gsub!(/:action/, partial) - details.each do |ext, variants| - query.gsub!(/:#{ext}/, "{#{variants.compact.uniq.join(',')}}") + details.each do |ext, candidates| + if ext == :variants && candidates == :any + query.gsub!(/:#{ext}/, "*") + else + query.gsub!(/:#{ext}/, "{#{candidates.compact.uniq.join(',')}}") + end end File.expand_path(query, @path) @@ -340,7 +344,11 @@ module ActionView query = escape_entry(File.join(@path, path)) exts = EXTENSIONS.map do |ext, prefix| - "{#{details[ext].compact.uniq.map { |e| "#{prefix}#{e}," }.join}}" + if ext == :variants && details[ext] == :any + "{#{prefix}*,}" + else + "{#{details[ext].compact.uniq.map { |e| "#{prefix}#{e}," }.join}}" + end end.join query + exts diff --git a/actionview/lib/action_view/view_paths.rb b/actionview/lib/action_view/view_paths.rb index 37722013ce..717d6866c5 100644 --- a/actionview/lib/action_view/view_paths.rb +++ b/actionview/lib/action_view/view_paths.rb @@ -1,5 +1,3 @@ -require 'action_view/base' - module ActionView module ViewPaths extend ActiveSupport::Concern @@ -10,7 +8,7 @@ module ActionView self._view_paths.freeze end - delegate :template_exists?, :view_paths, :formats, :formats=, + delegate :template_exists?, :any_templates?, :view_paths, :formats, :formats=, :locale, :locale=, :to => :lookup_context module ClassMethods diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb index 79173f730f..3256d8fc4d 100644 --- a/actionview/test/abstract_unit.rb +++ b/actionview/test/abstract_unit.rb @@ -1,5 +1,3 @@ -require File.expand_path('../../../load_paths', __FILE__) - $:.unshift(File.dirname(__FILE__) + '/lib') $:.unshift(File.dirname(__FILE__) + '/fixtures/helpers') $:.unshift(File.dirname(__FILE__) + '/fixtures/alternate_helpers') @@ -95,12 +93,14 @@ module ActionDispatch super return if DrawOnce.drew - SharedTestRoutes.draw do - get ':controller(/:action)' - end + ActiveSupport::Deprecation.silence do + SharedTestRoutes.draw do + get ':controller(/:action)' + end - ActionDispatch::IntegrationTest.app.routes.draw do - get ':controller(/:action)' + ActionDispatch::IntegrationTest.app.routes.draw do + get ':controller(/:action)' + end end DrawOnce.drew = true diff --git a/actionview/test/fixtures/digestor/messages/peek.html.erb b/actionview/test/fixtures/digestor/messages/peek.html.erb new file mode 100644 index 0000000000..84885ab0bc --- /dev/null +++ b/actionview/test/fixtures/digestor/messages/peek.html.erb @@ -0,0 +1,2 @@ +<%# Template Dependency: messages/message %> +<%= render "comments/comments" %> diff --git a/actionview/test/fixtures/test/_customer.mobile.erb b/actionview/test/fixtures/test/_customer.mobile.erb new file mode 100644 index 0000000000..d8220afeda --- /dev/null +++ b/actionview/test/fixtures/test/_customer.mobile.erb @@ -0,0 +1 @@ +Hello: <%= customer.name rescue "Anonymous" %>
\ No newline at end of file diff --git a/actionview/test/lib/controller/fake_models.rb b/actionview/test/lib/controller/fake_models.rb index a122fe17c9..65c68fc34a 100644 --- a/actionview/test/lib/controller/fake_models.rb +++ b/actionview/test/lib/controller/fake_models.rb @@ -31,27 +31,6 @@ end class GoodCustomer < Customer end -class TicketType < Struct.new(:name) - extend ActiveModel::Naming - include ActiveModel::Conversion - extend ActiveModel::Translation - - def initialize(*args) - super - @persisted = false - end - - def persisted=(boolean) - @persisted = boolean - end - - def persisted? - @persisted - end - - attr_accessor :name -end - class Post < Struct.new(:title, :author_name, :body, :secret, :persisted, :written_on, :cost) extend ActiveModel::Naming include ActiveModel::Conversion diff --git a/actionview/test/template/digestor_test.rb b/actionview/test/template/digestor_test.rb index dde757b5a2..d4c5048bde 100644 --- a/actionview/test/template/digestor_test.rb +++ b/actionview/test/template/digestor_test.rb @@ -13,34 +13,11 @@ class FixtureTemplate end end -class FixtureFinder +class FixtureFinder < ActionView::LookupContext FIXTURES_DIR = "#{File.dirname(__FILE__)}/../fixtures/digestor" - attr_reader :details, :view_paths - attr_accessor :formats - attr_accessor :variants - - def initialize - @details = {} - @view_paths = ActionView::PathSet.new(['digestor']) - @formats = [] - @variants = [] - end - - def details_key - details.hash - end - - def find(name, prefixes = [], partial = false, keys = [], options = {}) - partial_name = partial ? name.gsub(%r|/([^/]+)$|, '/_\1') : name - format = @formats.first.to_s - format += "+#{@variants.first}" if @variants.any? - - FixtureTemplate.new("digestor/#{partial_name}.#{format}.erb") - end - - def disable_cache(&block) - yield + def initialize(details = {}) + super(ActionView::PathSet.new(['digestor']), details, []) end end @@ -49,6 +26,7 @@ class TemplateDigestorTest < ActionView::TestCase @cwd = Dir.pwd @tmp_dir = Dir.mktmpdir + ActionView::LookupContext::DetailsKey.clear FileUtils.cp_r FixtureFinder::FIXTURES_DIR, @tmp_dir Dir.chdir @tmp_dir end @@ -56,7 +34,6 @@ class TemplateDigestorTest < ActionView::TestCase def teardown Dir.chdir @cwd FileUtils.rm_r @tmp_dir - ActionView::Digestor.cache.clear end def test_top_level_change_reflected @@ -153,12 +130,27 @@ class TemplateDigestorTest < ActionView::TestCase end end + def test_getting_of_singly_nested_dependencies + singly_nested_dependencies = ["messages/header", "messages/form", "messages/message", "events/event", "comments/comment"] + assert_equal singly_nested_dependencies, nested_dependencies('messages/edit') + end + + def test_getting_of_doubly_nested_dependencies + doubly_nested = [{"comments/comments"=>["comments/comment"]}, "messages/message"] + assert_equal doubly_nested, nested_dependencies('messages/peek') + end + def test_nested_template_directory assert_digest_difference("messages/show") do change_template("messages/actions/_move") end end + def test_nested_template_deps + nested_deps = ["messages/header", {"comments/comments"=>["comments/comment"]}, "messages/actions/move", "events/event", "messages/something_missing", "messages/something_missing_1", "messages/message", "messages/form"] + assert_equal nested_deps, nested_dependencies("messages/show") + end + def test_recursion_in_renders assert digest("level/recursion") # assert recursion is possible assert_not_nil digest("level/recursion") # assert digest is stored @@ -206,13 +198,14 @@ class TemplateDigestorTest < ActionView::TestCase def test_details_are_included_in_cache_key # Cache the template digest. + @finder = FixtureFinder.new({:formats => [:html]}) old_digest = digest("events/_event") # Change the template; the cached digest remains unchanged. change_template("events/_event") # The details are changed, so a new cache key is generated. - finder.details[:foo] = "bar" + @finder = FixtureFinder.new # The cache is busted. assert_not_equal old_digest, digest("events/_event") @@ -313,29 +306,29 @@ class TemplateDigestorTest < ActionView::TestCase def assert_digest_difference(template_name, options = {}) previous_digest = digest(template_name, options) - ActionView::Digestor.cache.clear + finder.digest_cache.clear yield - assert previous_digest != digest(template_name, options), "digest didn't change" - ActionView::Digestor.cache.clear + assert_not_equal previous_digest, digest(template_name, options), "digest didn't change" + finder.digest_cache.clear end def digest(template_name, options = {}) options = options.dup - finder.formats = [:html] finder.variants = options.delete(:variants) || [] - - ActionView::Digestor.digest({ name: template_name, finder: finder }.merge(options)) + ActionView::Digestor.digest(name: template_name, finder: finder, dependencies: (options[:dependencies] || [])) end def dependencies(template_name) - ActionView::Digestor.new({ name: template_name, finder: finder }).dependencies + tree = ActionView::Digestor.tree(template_name, finder) + tree.children.map(&:name) end def nested_dependencies(template_name) - ActionView::Digestor.new({ name: template_name, finder: finder }).nested_dependencies + tree = ActionView::Digestor.tree(template_name, finder) + tree.children.map(&:to_dep_map) end def finder diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb index 034b8a4bf6..e77183e39f 100644 --- a/actionview/test/template/form_helper_test.rb +++ b/actionview/test/template/form_helper_test.rb @@ -128,8 +128,6 @@ class FormHelperTest < ActionView::TestCase @post_delegator.title = 'Hello World' @car = Car.new("#000FFF") - - @ticket_type = TicketType.new end Routes = ActionDispatch::Routing::RouteSet.new @@ -138,8 +136,6 @@ class FormHelperTest < ActionView::TestCase resources :comments end - resources :ticket_types - namespace :admin do resources :posts do resources :comments @@ -1876,20 +1872,6 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end - def test_lowercase_model_name_default_submit_button_value - form_for(@ticket_type) do |f| - concat f.submit - end - - expected = - '<form class="new_ticket_type" id="new_ticket_type" action="/ticket_types" accept-charset="UTF-8" method="post">' + - hidden_fields + - '<input type="submit" name="commit" value="Create ticket type" data-disable-with="Create ticket type" />' + - '</form>' - - assert_dom_equal expected, output_buffer - end - def test_form_for_with_symbol_object_name form_for(@post, as: "other_name", html: { id: "create-post" }) do |f| concat f.label(:title, class: 'post_title') @@ -2257,7 +2239,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form('/posts', 'new_post', 'new_post') do - "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />" + "<input name='commit' data-disable-with='Create Post' type='submit' value='Create Post' />" end assert_dom_equal expected, output_buffer @@ -2272,7 +2254,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do - "<input name='commit' data-disable-with='Confirm post changes' type='submit' value='Confirm post changes' />" + "<input name='commit' data-disable-with='Confirm Post changes' type='submit' value='Confirm Post changes' />" end assert_dom_equal expected, output_buffer @@ -2300,7 +2282,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form('/posts/123', 'edit_another_post', 'edit_another_post', method: 'patch') do - "<input name='commit' data-disable-with='Update your post' type='submit' value='Update your post' />" + "<input name='commit' data-disable-with='Update your Post' type='submit' value='Update your Post' />" end assert_dom_equal expected, output_buffer diff --git a/actionview/test/template/log_subscriber_test.rb b/actionview/test/template/log_subscriber_test.rb index 4776c18b0b..7683444bf0 100644 --- a/actionview/test/template/log_subscriber_test.rb +++ b/actionview/test/template/log_subscriber_test.rb @@ -35,7 +35,8 @@ class AVLogSubscriberTest < ActiveSupport::TestCase @view.render(:file => "test/hello_world") wait - assert_equal 1, @logger.logged(:info).size + assert_equal 2, @logger.logged(:info).size + assert_match(/Rendering test\/hello_world\.erb/, @logger.logged(:info).first) assert_match(/Rendered test\/hello_world\.erb/, @logger.logged(:info).last) end end @@ -45,7 +46,8 @@ class AVLogSubscriberTest < ActiveSupport::TestCase @view.render(:text => "TEXT") wait - assert_equal 1, @logger.logged(:info).size + assert_equal 2, @logger.logged(:info).size + assert_match(/Rendering text template/, @logger.logged(:info).first) assert_match(/Rendered text template/, @logger.logged(:info).last) end end @@ -55,7 +57,8 @@ class AVLogSubscriberTest < ActiveSupport::TestCase @view.render(:inline => "<%= 'TEXT' %>") wait - assert_equal 1, @logger.logged(:info).size + assert_equal 2, @logger.logged(:info).size + assert_match(/Rendering inline template/, @logger.logged(:info).first) assert_match(/Rendered inline template/, @logger.logged(:info).last) end end @@ -86,7 +89,7 @@ class AVLogSubscriberTest < ActiveSupport::TestCase wait assert_equal 1, @logger.logged(:info).size - assert_match(/Rendered test\/_customer.erb/, @logger.logged(:info).last) + assert_match(/Rendered collection of test\/_customer.erb \[2 times\]/, @logger.logged(:info).last) end end @@ -96,7 +99,7 @@ class AVLogSubscriberTest < ActiveSupport::TestCase wait assert_equal 1, @logger.logged(:info).size - assert_match(/Rendered customers\/_customer\.html\.erb/, @logger.logged(:info).last) + assert_match(/Rendered collection of customers\/_customer\.html\.erb \[2 times\]/, @logger.logged(:info).last) end end @@ -106,7 +109,21 @@ class AVLogSubscriberTest < ActiveSupport::TestCase wait assert_equal 1, @logger.logged(:info).size - assert_match(/Rendered collection/, @logger.logged(:info).last) + assert_match(/Rendered collection of templates/, @logger.logged(:info).last) + end + end + + def test_render_collection_with_cached_set + Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do + def @view.view_cache_dependencies; []; end + def @view.fragment_cache_key(*); 'ahoy `controller` dependency'; end + + @view.render(partial: 'customers/customer', collection: [ Customer.new('david'), Customer.new('mary') ], cached: true, + locals: { greeting: 'hi' }) + wait + + assert_equal 1, @logger.logged(:info).size + assert_match(/Rendered collection of customers\/_customer\.html\.erb \[0 \/ 2 cache hits\]/, @logger.logged(:info).last) end end end diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb index 333e0cca11..b417d1ebfa 100644 --- a/actionview/test/template/render_test.rb +++ b/actionview/test/template/render_test.rb @@ -226,13 +226,13 @@ module RenderTestCases assert_match %r!method.*doesnt_exist!, e.message assert_equal "", e.sub_template_message assert_equal "1", e.line_number - assert_equal "1: <%= doesnt_exist %>", e.annoted_source_code.strip + assert_equal "1: <%= doesnt_exist %>", e.annoted_source_code[0].strip assert_equal File.expand_path("#{FIXTURE_LOAD_PATH}/test/_raise.html.erb"), e.file_name end def test_render_error_indentation e = assert_raises(ActionView::Template::Error) { @view.render(:partial => "test/raise_indentation") } - error_lines = e.annoted_source_code.split("\n") + error_lines = e.annoted_source_code assert_match %r!error\shere!, e.message assert_equal "11", e.line_number assert_equal " 9: <p>Ninth paragraph</p>", error_lines.second @@ -252,7 +252,7 @@ module RenderTestCases assert_match %r!method.*doesnt_exist!, e.message assert_equal "", e.sub_template_message assert_equal "1", e.line_number - assert_equal "1: <%= doesnt_exist %>", e.annoted_source_code.strip + assert_equal "1: <%= doesnt_exist %>", e.annoted_source_code[0].strip assert_equal File.expand_path("#{FIXTURE_LOAD_PATH}/test/_raise.html.erb"), e.file_name end @@ -270,6 +270,11 @@ module RenderTestCases assert_equal "Hello: davidHello: mary", @view.render(:partial => "test/customer", :collection => [ Customer.new("david"), Customer.new("mary") ]) end + def test_render_partial_collection_with_partial_name_containing_dot + assert_equal "Hello: davidHello: mary", + @view.render(:partial => "test/customer.mobile", :collection => [ Customer.new("david"), Customer.new("mary") ]) + end + def test_render_partial_collection_as_by_string assert_equal "david david davidmary mary mary", @view.render(:partial => "test/customer_with_var", :collection => [ Customer.new("david"), Customer.new("mary") ], :as => 'customer') @@ -628,56 +633,59 @@ class LazyViewRenderTest < ActiveSupport::TestCase end end -class CachedCollectionViewRenderTest < CachedViewRenderTest +class CachedCollectionViewRenderTest < ActiveSupport::TestCase class CachedCustomer < Customer; end - teardown do - ActionView::PartialRenderer.collection_cache.clear - end + include RenderTestCases - test "with custom key" do - customer = Customer.new("david") - key = cache_key([customer, 'key'], "test/_customer") + # Ensure view path cache is primed + setup do + view_paths = ActionController::Base.view_paths + assert_equal ActionView::OptimizedFileSystemResolver, view_paths.first.class - ActionView::PartialRenderer.collection_cache.write(key, 'Hello') + ActionView::PartialRenderer.collection_cache = ActiveSupport::Cache::MemoryStore.new - assert_equal "Hello", - @view.render(partial: "test/customer", collection: [customer], cache: ->(item) { [item, 'key'] }) + setup_view(view_paths) end - test "with caching with custom key and rendering with different key" do - customer = Customer.new("david") - key = cache_key([customer, 'key'], "test/_customer") + teardown do + GC.start + I18n.reload! + end - ActionView::PartialRenderer.collection_cache.write(key, 'Hello') + test "collection caching does not cache by default" do + customer = Customer.new("david", 1) + key = cache_key(customer, "test/_customer") - assert_equal "Hello: david", - @view.render(partial: "test/customer", collection: [customer], cache: ->(item) { [item, 'another_key'] }) + ActionView::PartialRenderer.collection_cache.write(key, 'Cached') + + assert_not_equal "Cached", + @view.render(partial: "test/customer", collection: [customer]) end - test "automatic caching with inferred cache name" do - customer = CachedCustomer.new("david") - key = cache_key(customer, "test/_cached_customer") + test "collection caching with partial that doesn't use fragment caching" do + customer = Customer.new("david", 1) + key = cache_key(customer, "test/_customer") ActionView::PartialRenderer.collection_cache.write(key, 'Cached') assert_equal "Cached", - @view.render(partial: "test/cached_customer", collection: [customer]) + @view.render(partial: "test/customer", collection: [customer], cached: true) end - test "automatic caching with as name" do - customer = CachedCustomer.new("david") - key = cache_key(customer, "test/_cached_customer_as") + test "collection caching with cached true" do + customer = CachedCustomer.new("david", 1) + key = cache_key(customer, "test/_cached_customer") ActionView::PartialRenderer.collection_cache.write(key, 'Cached') assert_equal "Cached", - @view.render(partial: "test/cached_customer_as", collection: [customer], as: :buyer) + @view.render(partial: "test/cached_customer", collection: [customer], cached: true) end private - def cache_key(names, virtual_path) + def cache_key(*names, virtual_path) digest = ActionView::Digestor.digest name: virtual_path, finder: @view.lookup_context, dependencies: [] - @view.fragment_cache_key([ *Array(names), digest ]) + @view.fragment_cache_key([ *names, digest ]) end end diff --git a/actionview/test/template/resolver_patterns_test.rb b/actionview/test/template/resolver_patterns_test.rb index 575eb9bd28..1a091bd692 100644 --- a/actionview/test/template/resolver_patterns_test.rb +++ b/actionview/test/template/resolver_patterns_test.rb @@ -3,17 +3,17 @@ require 'abstract_unit' class ResolverPatternsTest < ActiveSupport::TestCase def setup path = File.expand_path("../../fixtures/", __FILE__) - pattern = ":prefix/{:formats/,}:action{.:formats,}{.:handlers,}" + pattern = ":prefix/{:formats/,}:action{.:formats,}{+:variants,}{.:handlers,}" @resolver = ActionView::FileSystemResolver.new(path, pattern) end def test_should_return_empty_list_for_unknown_path - templates = @resolver.find_all("unknown", "custom_pattern", false, {:locale => [], :formats => [:html], :handlers => [:erb]}) + templates = @resolver.find_all("unknown", "custom_pattern", false, {locale: [], formats: [:html], variants: [], handlers: [:erb]}) assert_equal [], templates, "expected an empty list of templates" end def test_should_return_template_for_declared_path - templates = @resolver.find_all("path", "custom_pattern", false, {:locale => [], :formats => [:html], :handlers => [:erb]}) + templates = @resolver.find_all("path", "custom_pattern", false, {locale: [], formats: [:html], variants: [], handlers: [:erb]}) assert_equal 1, templates.size, "expected one template" assert_equal "Hello custom patterns!", templates.first.source assert_equal "custom_pattern/path", templates.first.virtual_path @@ -21,11 +21,22 @@ class ResolverPatternsTest < ActiveSupport::TestCase end def test_should_return_all_templates_when_ambiguous_pattern - templates = @resolver.find_all("another", "custom_pattern", false, {:locale => [], :formats => [:html], :handlers => [:erb]}) + templates = @resolver.find_all("another", "custom_pattern", false, {locale: [], formats: [:html], variants: [], handlers: [:erb]}) assert_equal 2, templates.size, "expected two templates" assert_equal "Another template!", templates[0].source assert_equal "custom_pattern/another", templates[0].virtual_path assert_equal "Hello custom patterns!", templates[1].source assert_equal "custom_pattern/another", templates[1].virtual_path end + + def test_should_return_all_variants_for_any + templates = @resolver.find_all("hello_world", "test", false, {locale: [], formats: [:html, :text], variants: :any, handlers: [:erb]}) + assert_equal 3, templates.size, "expected three templates" + assert_equal "Hello phone!", templates[0].source + assert_equal "test/hello_world", templates[0].virtual_path + assert_equal "Hello texty phone!", templates[1].source + assert_equal "test/hello_world", templates[1].virtual_path + assert_equal "Hello world!", templates[2].source + assert_equal "test/hello_world", templates[2].virtual_path + end end diff --git a/actionview/test/template/tag_helper_test.rb b/actionview/test/template/tag_helper_test.rb index 6f7a78ccef..f3956a31f6 100644 --- a/actionview/test/template/tag_helper_test.rb +++ b/actionview/test/template/tag_helper_test.rb @@ -173,4 +173,10 @@ class TagHelperTest < ActionView::TestCase tag('a', { aria => { a_float: 3.14, a_big_decimal: BigDecimal.new("-123.456"), a_number: 1, string: 'hello', symbol: :foo, array: [1, 2, 3], hash: { key: 'value'}, string_with_quotes: 'double"quote"party"' } }) } end + + def test_link_to_data_nil_equal + div_type1 = content_tag(:div, 'test', { 'data-tooltip' => nil }) + div_type2 = content_tag(:div, 'test', { data: {tooltip: nil} }) + assert_dom_equal div_type1, div_type2 + end end diff --git a/actionview/test/template/template_test.rb b/actionview/test/template/template_test.rb index 921011b073..533c1c3219 100644 --- a/actionview/test/template/template_test.rb +++ b/actionview/test/template/template_test.rb @@ -192,38 +192,6 @@ class TestERBTemplate < ActiveSupport::TestCase assert_match(/\xFC/, e.message) end - def test_not_eligible_for_collection_caching_without_cache_call - [ - "<%= 'Hello' %>", - "<% cache_customer = 42 %>", - "<% cache customer.name do %><% end %>", - "<% my_cache customer do %><% end %>" - ].each do |body| - template = new_template(body, virtual_path: "test/foo/_customer") - assert_not template.eligible_for_collection_caching?, "Template #{body.inspect} should not be eligible for collection caching" - end - end - - def test_eligible_for_collection_caching_with_cache_call_or_explicit - [ - "<% cache customer do %><% end %>", - "<% cache(customer) do %><% end %>", - "<% cache( customer) do %><% end %>", - "<% cache( customer ) do %><% end %>", - "<%cache customer do %><% end %>", - "<% cache customer do %><% end %>", - " <% cache customer do %>\n<% end %>\n", - "<%# comment %><% cache customer do %><% end %>", - "<%# comment %>\n<% cache customer do %><% end %>", - "<%# comment\n line 2\n line 3 %>\n<% cache customer do %><% end %>", - "<%# comment 1 %>\n<%# comment 2 %>\n<% cache customer do %><% end %>", - "<%# comment 1 %>\n<%# Template Collection: customer %>\n<% my_cache customer do %><% end %>" - ].each do |body| - template = new_template(body, virtual_path: "test/foo/_customer") - assert template.eligible_for_collection_caching?, "Template #{body.inspect} should be eligible for collection caching" - end - end - def with_external_encoding(encoding) old = Encoding.default_external Encoding::Converter.new old, encoding if old != encoding diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb index 3010656166..ab56d80de3 100644 --- a/actionview/test/template/url_helper_test.rb +++ b/actionview/test/template/url_helper_test.rb @@ -71,6 +71,34 @@ class UrlHelperTest < ActiveSupport::TestCase assert_equal 'javascript:history.back()', url_for(:back) end + def test_to_form_params_with_hash + assert_equal( + [{ name: :name, value: 'David' }, { name: :nationality, value: 'Danish' }], + to_form_params(name: 'David', nationality: 'Danish') + ) + end + + def test_to_form_params_with_nested_hash + assert_equal( + [{ name: 'country[name]', value: 'Denmark' }], + to_form_params(country: { name: 'Denmark' }) + ) + end + + def test_to_form_params_with_array_nested_in_hash + assert_equal( + [{ name: 'countries[]', value: 'Denmark' }, { name: 'countries[]', value: 'Sweden' }], + to_form_params(countries: ['Denmark', 'Sweden']) + ) + end + + def test_to_form_params_with_namespace + assert_equal( + [{ name: 'country[name]', value: 'Denmark' }], + to_form_params({name: 'Denmark'}, 'country') + ) + end + def test_button_to_with_straight_url assert_dom_equal %{<form method="post" action="http://www.example.com" class="button_to"><input type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com") end @@ -189,8 +217,22 @@ class UrlHelperTest < ActiveSupport::TestCase def test_button_to_with_params assert_dom_equal( - %{<form action="http://www.example.com" class="button_to" method="post"><input type="submit" value="Hello" /><input type="hidden" name="foo" value="bar" /><input type="hidden" name="baz" value="quux" /></form>}, - button_to("Hello", "http://www.example.com", params: {foo: :bar, baz: "quux"}) + %{<form action="http://www.example.com" class="button_to" method="post"><input type="submit" value="Hello" /><input type="hidden" name="baz" value="quux" /><input type="hidden" name="foo" value="bar" /></form>}, + button_to("Hello", "http://www.example.com", params: { foo: :bar, baz: "quux" }) + ) + end + + def test_button_to_with_nested_hash_params + assert_dom_equal( + %{<form action="http://www.example.com" class="button_to" method="post"><input type="submit" value="Hello" /><input type="hidden" name="foo[bar]" value="baz" /></form>}, + button_to("Hello", "http://www.example.com", params: { foo: { bar: 'baz' } }) + ) + end + + def test_button_to_with_nested_array_params + assert_dom_equal( + %{<form action="http://www.example.com" class="button_to" method="post"><input type="submit" value="Hello" /><input type="hidden" name="foo[]" value="bar" /></form>}, + button_to("Hello", "http://www.example.com", params: { foo: ['bar'] }) ) end @@ -613,7 +655,9 @@ class UrlHelperControllerTest < ActionController::TestCase to: 'url_helper_controller_test/url_helper#show_named_route', as: :show_named_route - get "/:controller(/:action(/:id))" + ActiveSupport::Deprecation.silence do + get "/:controller(/:action(/:id))" + end get 'url_helper_controller_test/url_helper/normalize_recall_params', to: UrlHelperController.action(:normalize_recall), |