aboutsummaryrefslogtreecommitdiffstats
path: root/actionview/lib
diff options
context:
space:
mode:
Diffstat (limited to 'actionview/lib')
-rw-r--r--actionview/lib/action_view/base.rb12
-rw-r--r--actionview/lib/action_view/dependency_tracker.rb61
-rw-r--r--actionview/lib/action_view/digestor.rb25
-rw-r--r--actionview/lib/action_view/helpers/asset_tag_helper.rb16
-rw-r--r--actionview/lib/action_view/helpers/asset_url_helper.rb63
-rw-r--r--actionview/lib/action_view/helpers/atom_feed_helper.rb9
-rw-r--r--actionview/lib/action_view/helpers/cache_helper.rb109
-rw-r--r--actionview/lib/action_view/helpers/capture_helper.rb6
-rw-r--r--actionview/lib/action_view/helpers/controller_helper.rb1
-rw-r--r--actionview/lib/action_view/helpers/date_helper.rb40
-rw-r--r--actionview/lib/action_view/helpers/debug_helper.rb2
-rw-r--r--actionview/lib/action_view/helpers/form_helper.rb105
-rw-r--r--actionview/lib/action_view/helpers/form_options_helper.rb107
-rw-r--r--actionview/lib/action_view/helpers/form_tag_helper.rb68
-rw-r--r--actionview/lib/action_view/helpers/javascript_helper.rb8
-rw-r--r--actionview/lib/action_view/helpers/number_helper.rb22
-rw-r--r--actionview/lib/action_view/helpers/record_tag_helper.rb111
-rw-r--r--actionview/lib/action_view/helpers/rendering_helper.rb2
-rw-r--r--actionview/lib/action_view/helpers/sanitize_helper.rb131
-rw-r--r--actionview/lib/action_view/helpers/tag_helper.rb27
-rw-r--r--actionview/lib/action_view/helpers/tags.rb1
-rw-r--r--actionview/lib/action_view/helpers/tags/base.rb61
-rw-r--r--actionview/lib/action_view/helpers/tags/collection_check_boxes.rb32
-rw-r--r--actionview/lib/action_view/helpers/tags/collection_helpers.rb28
-rw-r--r--actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb10
-rw-r--r--actionview/lib/action_view/helpers/tags/label.rb16
-rw-r--r--actionview/lib/action_view/helpers/tags/placeholderable.rb18
-rw-r--r--actionview/lib/action_view/helpers/tags/translator.rb40
-rw-r--r--actionview/lib/action_view/helpers/text_helper.rb32
-rw-r--r--actionview/lib/action_view/helpers/translation_helper.rb88
-rw-r--r--actionview/lib/action_view/helpers/url_helper.rb89
-rw-r--r--actionview/lib/action_view/layouts.rb19
-rw-r--r--actionview/lib/action_view/lookup_context.rb41
-rw-r--r--actionview/lib/action_view/model_naming.rb2
-rw-r--r--actionview/lib/action_view/path_set.rb9
-rw-r--r--actionview/lib/action_view/railtie.rb22
-rw-r--r--actionview/lib/action_view/record_identifier.rb4
-rw-r--r--actionview/lib/action_view/renderer/partial_renderer.rb43
-rw-r--r--actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb70
-rw-r--r--actionview/lib/action_view/renderer/renderer.rb2
-rw-r--r--actionview/lib/action_view/renderer/streaming_template_renderer.rb2
-rw-r--r--actionview/lib/action_view/renderer/template_renderer.rb21
-rw-r--r--actionview/lib/action_view/rendering.rb13
-rw-r--r--actionview/lib/action_view/routing_url_for.rb29
-rw-r--r--actionview/lib/action_view/tasks/dependencies.rake16
-rw-r--r--actionview/lib/action_view/template.rb55
-rw-r--r--actionview/lib/action_view/template/handlers/erb.rb25
-rw-r--r--actionview/lib/action_view/template/handlers/raw.rb2
-rw-r--r--actionview/lib/action_view/template/resolver.rb42
-rw-r--r--actionview/lib/action_view/test_case.rb18
-rw-r--r--actionview/lib/action_view/view_paths.rb6
51 files changed, 1081 insertions, 700 deletions
diff --git a/actionview/lib/action_view/base.rb b/actionview/lib/action_view/base.rb
index 1feafc1094..ad1cb1a4be 100644
--- a/actionview/lib/action_view/base.rb
+++ b/actionview/lib/action_view/base.rb
@@ -70,6 +70,14 @@ module ActionView #:nodoc:
# Headline: <%= headline %>
# First name: <%= person.first_name %>
#
+ # The local variables passed to sub templates can be accessed as a hash using the <tt>local_assigns</tt> hash. This lets you access the
+ # variables as:
+ #
+ # Headline: <%= local_assigns[:headline] %>
+ #
+ # This is useful in cases where you aren't sure if the local variable has been assigned. Alternatively, you could also use
+ # <tt>defined? headline</tt> to first check if the variable has been assigned before using it.
+ #
# === Template caching
#
# By default, Rails will compile each template to a method in order to render it. When you alter a template,
@@ -153,6 +161,10 @@ module ActionView #:nodoc:
cattr_accessor :raise_on_missing_translations
@@raise_on_missing_translations = false
+ # Specify whether submit_tag should automatically disable on click
+ cattr_accessor :automatically_disable_submit_tag
+ @@automatically_disable_submit_tag = true
+
class_attribute :_routes
class_attribute :logger
diff --git a/actionview/lib/action_view/dependency_tracker.rb b/actionview/lib/action_view/dependency_tracker.rb
index e34bdd4a46..7716955fd9 100644
--- a/actionview/lib/action_view/dependency_tracker.rb
+++ b/actionview/lib/action_view/dependency_tracker.rb
@@ -1,16 +1,18 @@
-require 'thread_safe'
+require 'concurrent'
+require 'action_view/path_set'
module ActionView
class DependencyTracker # :nodoc:
- @trackers = ThreadSafe::Cache.new
+ @trackers = Concurrent::Map.new
- def self.find_dependencies(name, template)
+ def self.find_dependencies(name, template, view_paths = nil)
tracker = @trackers[template.handler]
+ return [] unless tracker.present?
- if tracker.present?
- tracker.call(name, template)
+ if tracker.respond_to?(:supports_view_paths?) && tracker.supports_view_paths?
+ tracker.call(name, template, view_paths)
else
- []
+ tracker.call(name, template)
end
end
@@ -76,12 +78,22 @@ module ActionView
(?:#{STRING}|#{VARIABLE_OR_METHOD_CHAIN}) # finally, the dependency name of interest
/xm
- def self.call(name, template)
- new(name, template).dependencies
+ LAYOUT_DEPENDENCY = /\A
+ (?:\s*\(?\s*) # optional opening paren surrounded by spaces
+ (?:.*?#{LAYOUT_HASH_KEY}) # check if the line has layout key declaration
+ (?:#{STRING}|#{VARIABLE_OR_METHOD_CHAIN}) # finally, the dependency name of interest
+ /xm
+
+ def self.supports_view_paths? # :nodoc:
+ true
+ end
+
+ def self.call(name, template, view_paths = nil)
+ new(name, template, view_paths).dependencies
end
- def initialize(name, template)
- @name, @template = name, template
+ def initialize(name, template, view_paths = nil)
+ @name, @template, @view_paths = name, template, view_paths
end
def dependencies
@@ -106,15 +118,20 @@ module ActionView
render_calls = source.split(/\brender\b/).drop(1)
render_calls.each do |arguments|
- arguments.scan(RENDER_ARGUMENTS) do
- add_dynamic_dependency(render_dependencies, Regexp.last_match[:dynamic])
- add_static_dependency(render_dependencies, Regexp.last_match[:static])
- end
+ add_dependencies(render_dependencies, arguments, LAYOUT_DEPENDENCY)
+ add_dependencies(render_dependencies, arguments, RENDER_ARGUMENTS)
end
render_dependencies.uniq
end
+ def add_dependencies(render_dependencies, arguments, pattern)
+ arguments.scan(pattern) do
+ add_dynamic_dependency(render_dependencies, Regexp.last_match[:dynamic])
+ add_static_dependency(render_dependencies, Regexp.last_match[:static])
+ end
+ end
+
def add_dynamic_dependency(dependencies, dependency)
if dependency
dependencies << "#{dependency.pluralize}/#{dependency.singularize}"
@@ -131,8 +148,22 @@ module ActionView
end
end
+ 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}"
+ end
+ end
+ end
+
def explicit_dependencies
- source.scan(EXPLICIT_DEPENDENCY).flatten.uniq
+ dependencies = source.scan(EXPLICIT_DEPENDENCY).flatten.uniq
+
+ wildcards, explicits = dependencies.partition { |dependency| dependency[-1] == '*' }
+
+ (explicits + resolve_directories(wildcards)).uniq
end
end
diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb
index 1f103786cb..12e9723a02 100644
--- a/actionview/lib/action_view/digestor.rb
+++ b/actionview/lib/action_view/digestor.rb
@@ -1,18 +1,25 @@
-require 'thread_safe'
+require 'concurrent'
require 'action_view/dependency_tracker'
require 'monitor'
module ActionView
class Digestor
cattr_reader(:cache)
- @@cache = ThreadSafe::Cache.new
+ @@cache = Concurrent::Map.new
@@digest_monitor = Monitor.new
+ class PerRequestDigestCacheExpiry < Struct.new(:app) # :nodoc:
+ def call(env)
+ ActionView::Digestor.cache.clear
+ app.call(env)
+ end
+ end
+
class << self
# Supported options:
#
# * <tt>name</tt> - Template name
- # * <tt>finder</tt> - An instance of ActionView::LookupContext
+ # * <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)
@@ -21,7 +28,7 @@ module ActionView
cache_key = ([ options[:name], options[:finder].details_key.hash ].compact + Array.wrap(options[:dependencies])).join('.')
# this is a correctly done double-checked locking idiom
- # (ThreadSafe::Cache's lookups have volatile semantics)
+ # (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)
@@ -41,10 +48,7 @@ module ActionView
Digestor
end
- digest = klass.new(options).digest
- # Store the actual digest if config.cache_template_loading is true
- @@cache[cache_key] = stored_digest = digest if ActionView::Resolver.caching?
- digest
+ @@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
@@ -68,9 +72,10 @@ module ActionView
end
def dependencies
- DependencyTracker.find_dependencies(name, template)
+ DependencyTracker.find_dependencies(name, template, finder.view_paths)
rescue ActionView::MissingTemplate
- [] # File doesn't exist, so no dependencies
+ logger.try :error, " '#{name}' file doesn't exist, so no dependencies"
+ []
end
def nested_dependencies
diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb
index b7fdc16a9d..fa46a22500 100644
--- a/actionview/lib/action_view/helpers/asset_tag_helper.rb
+++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb
@@ -60,7 +60,7 @@ module ActionView
tag_options = {
"src" => path_to_javascript(source, path_options)
}.merge!(options)
- content_tag(:script, "", tag_options)
+ content_tag("script".freeze, "", tag_options)
}.join("\n").html_safe
end
@@ -127,7 +127,7 @@ module ActionView
# auto_discovery_link_tag(:rss, {controller: "news", action: "feed"})
# # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/news/feed" />
# auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", {title: "Example RSS"})
- # # => <link rel="alternate" type="application/rss+xml" title="Example RSS" href="http://www.example.com/feed" />
+ # # => <link rel="alternate" type="application/rss+xml" title="Example RSS" href="http://www.example.com/feed.rss" />
def auto_discovery_link_tag(type = :rss, url_options = {}, tag_options = {})
if !(type == :rss || type == :atom) && tag_options[:type].blank?
raise ArgumentError.new("You should pass :type tag_option key explicitly, because you have passed #{type} type other than :rss or :atom.")
@@ -136,7 +136,7 @@ module ActionView
tag(
"link",
"rel" => tag_options[:rel] || "alternate",
- "type" => tag_options[:type] || Mime::Type.lookup_by_extension(type.to_s).to_s,
+ "type" => tag_options[:type] || Mime[type].to_s,
"title" => tag_options[:title] || type.to_s.upcase,
"href" => url_options.is_a?(Hash) ? url_for(url_options.merge(:only_path => false)) : url_options
)
@@ -207,6 +207,7 @@ module ActionView
# # => <img alt="Icon" class="menu_icon" src="/icons/icon.gif" />
def image_tag(source, options={})
options = options.symbolize_keys
+ check_for_image_tag_errors(options)
src = options[:src] = path_to_image(source)
@@ -236,7 +237,7 @@ module ActionView
# image_alt('underscored_file_name.png')
# # => Underscored file name
def image_alt(src)
- File.basename(src, '.*').sub(/-[[:xdigit:]]{32}\z/, '').tr('-_', ' ').capitalize
+ File.basename(src, '.*'.freeze).sub(/-[[:xdigit:]]{32}\z/, ''.freeze).tr('-_'.freeze, ' '.freeze).capitalize
end
# Returns an HTML video tag for the +sources+. If +sources+ is a string,
@@ -318,12 +319,19 @@ module ActionView
end
def extract_dimensions(size)
+ size = size.to_s
if size =~ %r{\A\d+x\d+\z}
size.split('x')
elsif size =~ %r{\A\d+\z}
[size, size]
end
end
+
+ def check_for_image_tag_errors(options)
+ if options[:size] && (options[:height] || options[:width])
+ raise ArgumentError, "Cannot pass a :size option with a :height or :width option"
+ end
+ end
end
end
end
diff --git a/actionview/lib/action_view/helpers/asset_url_helper.rb b/actionview/lib/action_view/helpers/asset_url_helper.rb
index 29733442c1..717b326740 100644
--- a/actionview/lib/action_view/helpers/asset_url_helper.rb
+++ b/actionview/lib/action_view/helpers/asset_url_helper.rb
@@ -31,26 +31,33 @@ module ActionView
# stylesheet_link_tag("application")
# # => <link href="http://assets.example.com/assets/application.css" media="screen" rel="stylesheet" />
#
- # Browsers typically open at most two simultaneous connections to a single
- # host, which means your assets often have to wait for other assets to finish
- # downloading. You can alleviate this by using a <tt>%d</tt> wildcard in the
- # +asset_host+. For example, "assets%d.example.com". If that wildcard is
- # present Rails distributes asset requests among the corresponding four hosts
- # "assets0.example.com", ..., "assets3.example.com". With this trick browsers
- # will open eight simultaneous connections rather than two.
+ # Browsers open a limited number of simultaneous connections to a single
+ # host. The exact number varies by browser and version. This limit may cause
+ # some asset downloads to wait for previous assets to finish before they can
+ # begin. You can use the <tt>%d</tt> wildcard in the +asset_host+ to
+ # distribute the requests over four hosts. For example,
+ # <tt>assets%d.example.com<tt> will spread the asset requests over
+ # "assets0.example.com", ..., "assets3.example.com".
#
# image_tag("rails.png")
# # => <img alt="Rails" src="http://assets0.example.com/assets/rails.png" />
# stylesheet_link_tag("application")
# # => <link href="http://assets2.example.com/assets/application.css" media="screen" rel="stylesheet" />
#
- # To do this, you can either setup four actual hosts, or you can use wildcard
- # DNS to CNAME the wildcard to a single asset host. You can read more about
- # setting up your DNS CNAME records from your ISP.
+ # This may improve the asset loading performance of your application.
+ # It is also possible the combination of additional connection overhead
+ # (DNS, SSL) and the overall browser connection limits may result in this
+ # solution being slower. You should be sure to measure your actual
+ # performance across targeted browsers both before and after this change.
+ #
+ # To implement the corresponding hosts you can either setup four actual
+ # hosts or use wildcard DNS to CNAME the wildcard to a single asset host.
+ # You can read more about setting up your DNS CNAME records from your ISP.
#
# Note: This is purely a browser performance optimization and is not meant
# for server load balancing. See http://www.die.net/musings/page_load_time/
- # for background.
+ # for background and http://www.browserscope.org/?category=network for
+ # connection limit data.
#
# Alternatively, you can exert more control over the asset host by setting
# +asset_host+ to a proc like this:
@@ -121,11 +128,13 @@ module ActionView
# asset_path "application", type: :stylesheet # => /assets/application.css
# asset_path "http://www.example.com/js/xmlhr.js" # => http://www.example.com/js/xmlhr.js
def asset_path(source, options = {})
+ raise ArgumentError, "nil is not a valid asset source" if source.nil?
+
source = source.to_s
return "" unless source.present?
return source if source =~ URI_REGEXP
- tail, source = source[/([\?#].+)$/], source.sub(/([\?#].+)$/, '')
+ tail, source = source[/([\?#].+)$/], source.sub(/([\?#].+)$/, ''.freeze)
if extname = compute_asset_extname(source, options)
source = "#{source}#{extname}"
@@ -248,6 +257,11 @@ module ActionView
# Computes the full URL to a JavaScript asset in the public javascripts directory.
# This will use +javascript_path+ internally, so most of their behaviors will be the same.
+ # Since +javascript_url+ is based on +asset_url+ method you can set :host options. If :host
+ # options is set, it overwrites global +config.action_controller.asset_host+ setting.
+ #
+ # javascript_url "js/xmlhr.js", host: "http://stage.example.com" # => http://stage.example.com/assets/dir/xmlhr.js
+ #
def javascript_url(source, options = {})
url_to_asset(source, {type: :javascript}.merge!(options))
end
@@ -270,6 +284,11 @@ module ActionView
# Computes the full URL to a stylesheet asset in the public stylesheets directory.
# This will use +stylesheet_path+ internally, so most of their behaviors will be the same.
+ # Since +stylesheet_url+ is based on +asset_url+ method you can set :host options. If :host
+ # options is set, it overwrites global +config.action_controller.asset_host+ setting.
+ #
+ # stylesheet_url "css/style.css", host: "http://stage.example.com" # => http://stage.example.com/css/style.css
+ #
def stylesheet_url(source, options = {})
url_to_asset(source, {type: :stylesheet}.merge!(options))
end
@@ -295,6 +314,11 @@ module ActionView
# Computes the full URL to an image asset.
# This will use +image_path+ internally, so most of their behaviors will be the same.
+ # Since +image_url+ is based on +asset_url+ method you can set :host options. If :host
+ # options is set, it overwrites global +config.action_controller.asset_host+ setting.
+ #
+ # image_url "edit.png", host: "http://stage.example.com" # => http://stage.example.com/edit.png
+ #
def image_url(source, options = {})
url_to_asset(source, {type: :image}.merge!(options))
end
@@ -316,6 +340,11 @@ module ActionView
# Computes the full URL to a video asset in the public videos directory.
# This will use +video_path+ internally, so most of their behaviors will be the same.
+ # Since +video_url+ is based on +asset_url+ method you can set :host options. If :host
+ # options is set, it overwrites global +config.action_controller.asset_host+ setting.
+ #
+ # video_url "hd.avi", host: "http://stage.example.com" # => http://stage.example.com/hd.avi
+ #
def video_url(source, options = {})
url_to_asset(source, {type: :video}.merge!(options))
end
@@ -337,6 +366,11 @@ module ActionView
# Computes the full URL to an audio asset in the public audios directory.
# This will use +audio_path+ internally, so most of their behaviors will be the same.
+ # Since +audio_url+ is based on +asset_url+ method you can set :host options. If :host
+ # options is set, it overwrites global +config.action_controller.asset_host+ setting.
+ #
+ # audio_url "horse.wav", host: "http://stage.example.com" # => http://stage.example.com/horse.wav
+ #
def audio_url(source, options = {})
url_to_asset(source, {type: :audio}.merge!(options))
end
@@ -357,6 +391,11 @@ module ActionView
# Computes the full URL to a font asset.
# This will use +font_path+ internally, so most of their behaviors will be the same.
+ # Since +font_url+ is based on +asset_url+ method you can set :host options. If :host
+ # options is set, it overwrites global +config.action_controller.asset_host+ setting.
+ #
+ # font_url "font.ttf", host: "http://stage.example.com" # => http://stage.example.com/font.ttf
+ #
def font_url(source, options = {})
url_to_asset(source, {type: :font}.merge!(options))
end
diff --git a/actionview/lib/action_view/helpers/atom_feed_helper.rb b/actionview/lib/action_view/helpers/atom_feed_helper.rb
index 227ad4cdfa..bb1cdd0f8d 100644
--- a/actionview/lib/action_view/helpers/atom_feed_helper.rb
+++ b/actionview/lib/action_view/helpers/atom_feed_helper.rb
@@ -16,7 +16,7 @@ module ActionView
# end
#
# app/controllers/posts_controller.rb:
- # class PostsController < ApplicationController::Base
+ # class PostsController < ApplicationController
# # GET /posts.html
# # GET /posts.atom
# def index
@@ -51,7 +51,7 @@ module ActionView
# * <tt>:language</tt>: Defaults to "en-US".
# * <tt>:root_url</tt>: The HTML alternative that this feed is doubling for. Defaults to / on the current host.
# * <tt>:url</tt>: The URL for this feed. Defaults to the current URL.
- # * <tt>:id</tt>: The id for this feed. Defaults to "tag:#{request.host},#{options[:schema_date]}:#{request.fullpath.split(".")[0]}"
+ # * <tt>:id</tt>: The id for this feed. Defaults to "tag:localhost,2005:/posts", in this case.
# * <tt>:schema_date</tt>: The date at which the tag scheme for the feed was first used. A good default is the year you
# created the feed. See http://feedvalidator.org/docs/error/InvalidTAG.html for more information. If not specified,
# 2005 is used (as an "I don't care" value).
@@ -174,7 +174,7 @@ module ActionView
#
# * <tt>:published</tt>: Time first published. Defaults to the created_at attribute on the record if one such exists.
# * <tt>:updated</tt>: Time of update. Defaults to the updated_at attribute on the record if one such exists.
- # * <tt>:url</tt>: The URL for this entry. Defaults to the polymorphic_url for the record.
+ # * <tt>:url</tt>: The URL for this entry or false or nil for not having a link tag. Defaults to the polymorphic_url for the record.
# * <tt>:id</tt>: The ID for this entry. Defaults to "tag:#{@view.request.host},#{@feed_options[:schema_date]}:#{record.class}/#{record.id}"
# * <tt>:type</tt>: The TYPE for this entry. Defaults to "text/html".
def entry(record, options = {})
@@ -191,7 +191,8 @@ module ActionView
type = options.fetch(:type, 'text/html')
- @xml.link(:rel => 'alternate', :type => type, :href => options[:url] || @view.polymorphic_url(record))
+ url = options.fetch(:url) { @view.polymorphic_url(record) }
+ @xml.link(:rel => 'alternate', :type => type, :href => url) if url
yield AtomBuilder.new(@xml)
end
diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb
index 4db8930a26..e473aeaea9 100644
--- a/actionview/lib/action_view/helpers/cache_helper.rb
+++ b/actionview/lib/action_view/helpers/cache_helper.rb
@@ -39,7 +39,7 @@ module ActionView
# This will include both records as part of the cache key and updating either of them will
# expire the cache.
#
- # ==== Template digest
+ # ==== \Template digest
#
# The template digest that's added to the cache key is computed by taking an md5 of the
# contents of the entire template file. This ensures that your caches will automatically
@@ -75,7 +75,8 @@ module ActionView
# render(topics) => render("topics/topic")
# render(message.topics) => render("topics/topic")
#
- # It's not possible to derive all render calls like that, though. Here are a few examples of things that can't be derived:
+ # It's not possible to derive all render calls like that, though.
+ # Here are a few examples of things that can't be derived:
#
# render group_of_attachments
# render @project.documents.where(published: true).order('created_at')
@@ -97,21 +98,74 @@ module ActionView
# <%# Template Dependency: todolists/todolist %>
# <%= render_sortable_todolists @project.todolists %>
#
- # The pattern used to match these is /# Template Dependency: ([^ ]+)/, so it's important that you type it out just so.
+ # In some cases, like a single table inheritance setup, you might have
+ # a bunch of explicit dependencies. Instead of writing every template out,
+ # you can use a wildcard to match any template in a directory:
+ #
+ # <%# Template Dependency: events/* %>
+ # <%= render_categorizable_events @person.events %>
+ #
+ # This marks every template in the directory as a dependency. To find those
+ # templates, the wildcard path must be absolutely defined from app/views or paths
+ # otherwise added with +prepend_view_path+ or +append_view_path+.
+ # This way the wildcard for `app/views/recordings/events` would be `recordings/events/*` etc.
+ #
+ # The pattern used to match explicit dependencies is <tt>/# Template Dependency: (\S+)/</tt>,
+ # so it's important that you type it out just so.
# You can only declare one template dependency per line.
#
# === External dependencies
#
- # If you use a helper method, for example, inside of a cached block and you then update that helper,
- # you'll have to bump the cache as well. It doesn't really matter how you do it, but the md5 of the template file
+ # If you use a helper method, for example, inside a cached block and
+ # you then update that helper, you'll have to bump the cache as well.
+ # It doesn't really matter how you do it, but the md5 of the template file
# must change. One recommendation is to simply be explicit in a comment, like:
#
# <%# Helper Dependency Updated: May 6, 2012 at 6pm %>
# <%= some_helper_method(person) %>
#
- # Now all you'll have to do is change that timestamp when the helper method changes.
- def cache(name = {}, options = nil, &block)
- if controller.perform_caching
+ # Now all you have to do is change that timestamp when the helper method changes.
+ #
+ # === Automatic Collection Caching
+ #
+ # When rendering collections such as:
+ #
+ # <%= render @notifications %>
+ # <%= render partial: 'notifications/notification', collection: @notifications %>
+ #
+ # If the notifications/_notification partial starts with a cache call as:
+ #
+ # <% 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:
+ #
+ # <%= render @notifications, cache: false %>
+ #
+ # === 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 %>
+ # <% 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.
+ 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))
else
yield
@@ -122,11 +176,11 @@ module ActionView
# Cache fragments of a view if +condition+ is true
#
- # <%= cache_if admin?, project do %>
+ # <% cache_if admin?, project do %>
# <b>All the topics on this project</b>
# <%= render project.topics %>
# <% end %>
- def cache_if(condition, name = {}, options = nil, &block)
+ def cache_if(condition, name = {}, options = {}, &block)
if condition
cache(name, options, &block)
else
@@ -138,37 +192,46 @@ module ActionView
# Cache fragments of a view unless +condition+ is true
#
- # <%= cache_unless admin?, project do %>
+ # <% cache_unless admin?, project do %>
# <b>All the topics on this project</b>
# <%= render project.topics %>
# <% end %>
- def cache_unless(condition, name = {}, options = nil, &block)
+ def cache_unless(condition, name = {}, options = {}, &block)
cache_if !condition, name, options, &block
end
# This helper returns the name of a cache key for a given fragment cache
- # call. By supplying skip_digest: true to cache, the digestion of cache
+ # call. By supplying +skip_digest:+ true to cache, the digestion of cache
# fragments can be manually bypassed. This is useful when cache fragments
# cannot be manually expired unless you know the exact key which is the
# case when using memcached.
- def cache_fragment_name(name = {}, options = nil)
- skip_digest = options && options[:skip_digest]
-
+ #
+ # The digest will be generated using +virtual_path:+ if it is provided.
+ #
+ def cache_fragment_name(name = {}, skip_digest: nil, virtual_path: nil)
if skip_digest
name
else
- fragment_name_with_digest(name)
+ fragment_name_with_digest(name, virtual_path)
end
end
- private
+ # Given a key (as described in ActionController::Caching::Fragments.expire_fragment),
+ # returns a key suitable for use in reading, writing, or expiring a
+ # cached fragment. All keys are prefixed with <tt>views/</tt> and uses
+ # ActiveSupport::Cache.expand_cache_key for the expansion.
+ def fragment_cache_key(key)
+ ActiveSupport::Cache.expand_cache_key(key.is_a?(Hash) ? url_for(key).split("://").last : key, :views)
+ end
- def fragment_name_with_digest(name) #:nodoc:
- if @virtual_path
- names = Array(name.is_a?(Hash) ? controller.url_for(name).split("://").last : name)
- digest = Digestor.digest name: @virtual_path, finder: lookup_context, dependencies: view_cache_dependencies
+ private
- [ *names, digest ]
+ def fragment_name_with_digest(name, virtual_path) #:nodoc:
+ virtual_path ||= @virtual_path
+ if virtual_path
+ name = controller.url_for(name).split("://").last if name.is_a?(Hash)
+ digest = Digestor.digest name: virtual_path, finder: lookup_context, dependencies: view_cache_dependencies
+ [ name, digest ]
else
name
end
diff --git a/actionview/lib/action_view/helpers/capture_helper.rb b/actionview/lib/action_view/helpers/capture_helper.rb
index 5a3223968f..93c7cba395 100644
--- a/actionview/lib/action_view/helpers/capture_helper.rb
+++ b/actionview/lib/action_view/helpers/capture_helper.rb
@@ -115,7 +115,7 @@ module ActionView
# <li><%= link_to 'Home', action: 'index' %></li>
# <% end %>
#
- # And in other place:
+ # And in another place:
#
# <% content_for :navigation do %>
# <li><%= link_to 'Login', action: 'login' %></li>
@@ -195,7 +195,9 @@ module ActionView
def with_output_buffer(buf = nil) #:nodoc:
unless buf
buf = ActionView::OutputBuffer.new
- buf.force_encoding(output_buffer.encoding) if output_buffer
+ if output_buffer && output_buffer.respond_to?(:encoding)
+ buf.force_encoding(output_buffer.encoding)
+ end
end
self.output_buffer, old_buffer = buf, output_buffer
yield
diff --git a/actionview/lib/action_view/helpers/controller_helper.rb b/actionview/lib/action_view/helpers/controller_helper.rb
index 74ef25f7c1..3569fba8c6 100644
--- a/actionview/lib/action_view/helpers/controller_helper.rb
+++ b/actionview/lib/action_view/helpers/controller_helper.rb
@@ -14,6 +14,7 @@ module ActionView
if @_controller = controller
@_request = controller.request if controller.respond_to?(:request)
@_config = controller.config.inheritable_copy if controller.respond_to?(:config)
+ @_default_form_builder = controller.default_form_builder if controller.respond_to?(:default_form_builder)
end
end
diff --git a/actionview/lib/action_view/helpers/date_helper.rb b/actionview/lib/action_view/helpers/date_helper.rb
index 4b4f0ae577..312e41ee48 100644
--- a/actionview/lib/action_view/helpers/date_helper.rb
+++ b/actionview/lib/action_view/helpers/date_helper.rb
@@ -68,6 +68,27 @@ module ActionView
# distance_of_time_in_words(from_time, to_time, include_seconds: true) # => about 6 years
# distance_of_time_in_words(to_time, from_time, include_seconds: true) # => about 6 years
# distance_of_time_in_words(Time.now, Time.now) # => less than a minute
+ #
+ # With the <tt>scope</tt> option, you can define a custom scope for Rails
+ # to look up the translation.
+ #
+ # For example you can define the following in your locale (e.g. en.yml).
+ #
+ # datetime:
+ # distance_in_words:
+ # short:
+ # about_x_hours:
+ # one: 'an hour'
+ # other: '%{count} hours'
+ #
+ # See https://github.com/svenfuchs/rails-i18n/blob/master/rails/locale/en.yml
+ # for more examples.
+ #
+ # Which will then result in the following:
+ #
+ # from_time = Time.now
+ # distance_of_time_in_words(from_time, from_time + 50.minutes, scope: 'datetime.distance_in_words.short') # => "an hour"
+ # distance_of_time_in_words(from_time, from_time + 3.hours, scope: 'datetime.distance_in_words.short') # => "3 hours"
def distance_of_time_in_words(from_time, to_time = 0, options = {})
options = {
scope: :'datetime.distance_in_words'
@@ -177,6 +198,8 @@ module ActionView
# and +:name+ (string). A format string would be something like "%{name} (%<number>02d)" for example.
# See <tt>Kernel.sprintf</tt> for documentation on format sequences.
# * <tt>:date_separator</tt> - Specifies a string to separate the date fields. Default is "" (i.e. nothing).
+ # * <tt>:time_separator</tt> - Specifies a string to separate the time fields. Default is "" (i.e. nothing).
+ # * <tt>:datetime_separator</tt>- Specifies a string to separate the date and time fields. Default is "" (i.e. nothing).
# * <tt>:start_year</tt> - Set the start year for the year select. Default is <tt>Date.today.year - 5</tt> if
# you are creating new record. While editing existing record, <tt>:start_year</tt> defaults to
# the current selected year minus 5.
@@ -205,6 +228,7 @@ module ActionView
# or the given prompt string.
# * <tt>:with_css_classes</tt> - Set to true if you want assign different styles for 'select' tags. This option
# automatically set classes 'year', 'month', 'day', 'hour', 'minute' and 'second' for your 'select' tags.
+ # * <tt>:use_hidden</tt> - Set to true if you only want to generate hidden input tags.
#
# If anything is passed in the +html_options+ hash it will be applied to every select tag in the set.
#
@@ -462,7 +486,7 @@ module ActionView
# The <tt>datetime</tt> can be either a +Time+ or +DateTime+ object or an integer.
# Override the field name using the <tt>:field_name</tt> option, 'second' by default.
#
- # my_time = Time.now + 16.minutes
+ # my_time = Time.now + 16.seconds
#
# # Generates a select field for seconds that defaults to the seconds for the time in my_time.
# select_second(my_time)
@@ -486,7 +510,7 @@ module ActionView
# selected. The <tt>datetime</tt> can be either a +Time+ or +DateTime+ object or an integer.
# Override the field name using the <tt>:field_name</tt> option, 'minute' by default.
#
- # my_time = Time.now + 6.hours
+ # my_time = Time.now + 10.minutes
#
# # Generates a select field for minutes that defaults to the minutes for the time in my_time.
# select_minute(my_time)
@@ -658,7 +682,7 @@ module ActionView
content = args.first || I18n.l(date_or_time, :format => format)
datetime = date_or_time.acts_like?(:time) ? date_or_time.xmlschema : date_or_time.iso8601
- content_tag(:time, content, options.reverse_merge(:datetime => datetime), &block)
+ content_tag("time".freeze, content, options.reverse_merge(:datetime => datetime), &block)
end
end
@@ -786,7 +810,7 @@ module ActionView
1.upto(12) do |month_number|
options = { :value => month_number }
options[:selected] = "selected" if month == month_number
- month_options << content_tag(:option, month_name(month_number), options) + "\n"
+ month_options << content_tag("option".freeze, month_name(month_number), options) + "\n"
end
build_select(:month, month_options.join)
end
@@ -948,7 +972,7 @@ module ActionView
tag_options[:selected] = "selected" if selected == i
text = options[:use_two_digit_numbers] ? sprintf("%02d", i) : value
text = options[:ampm] ? AMPM_TRANSLATION[i] : text
- select_options << content_tag(:option, text, tag_options)
+ select_options << content_tag("option".freeze, text, tag_options)
end
(select_options.join("\n") + "\n").html_safe
@@ -968,11 +992,11 @@ module ActionView
select_options[:class] = [select_options[:class], type].compact.join(' ') if @options[:with_css_classes]
select_html = "\n"
- select_html << content_tag(:option, '', :value => '') + "\n" if @options[:include_blank]
+ select_html << content_tag("option".freeze, '', :value => '') + "\n" if @options[:include_blank]
select_html << prompt_option_tag(type, @options[:prompt]) + "\n" if @options[:prompt]
select_html << select_options_as_html
- (content_tag(:select, select_html.html_safe, select_options) + "\n").html_safe
+ (content_tag("select".freeze, select_html.html_safe, select_options) + "\n").html_safe
end
# Builds a prompt option tag with supplied options or from default options.
@@ -989,7 +1013,7 @@ module ActionView
I18n.translate(:"datetime.prompts.#{type}", :locale => @options[:locale])
end
- prompt ? content_tag(:option, prompt, :value => '') : ''
+ prompt ? content_tag("option".freeze, prompt, :value => '') : ''
end
# Builds hidden input tag for date part and value.
diff --git a/actionview/lib/action_view/helpers/debug_helper.rb b/actionview/lib/action_view/helpers/debug_helper.rb
index ba47eee9ba..e9dccbad1c 100644
--- a/actionview/lib/action_view/helpers/debug_helper.rb
+++ b/actionview/lib/action_view/helpers/debug_helper.rb
@@ -26,7 +26,7 @@ module ActionView
Marshal::dump(object)
object = ERB::Util.html_escape(object.to_yaml)
content_tag(:pre, object, :class => "debug_dump")
- rescue Exception # errors from Marshal or YAML
+ rescue # errors from Marshal or YAML
# Object couldn't be dumped, perhaps because of singleton methods -- this is the fallback
content_tag(:code, object.inspect, :class => "debug_dump")
end
diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb
index 8d78ba13d5..2a367b85af 100644
--- a/actionview/lib/action_view/helpers/form_helper.rb
+++ b/actionview/lib/action_view/helpers/form_helper.rb
@@ -4,6 +4,7 @@ require 'action_view/helpers/tag_helper'
require 'action_view/helpers/form_tag_helper'
require 'action_view/helpers/active_model_helper'
require 'action_view/model_naming'
+require 'action_view/record_identifier'
require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/core_ext/hash/slice'
require 'active_support/core_ext/string/output_safety'
@@ -66,9 +67,10 @@ module ActionView
#
# In particular, thanks to the conventions followed in the generated field names, the
# controller gets a nested hash <tt>params[:person]</tt> with the person attributes
- # set in the form. That hash is ready to be passed to <tt>Person.create</tt>:
+ # set in the form. That hash is ready to be passed to <tt>Person.new</tt>:
#
- # if @person = Person.create(params[:person])
+ # @person = Person.new(params[:person])
+ # if @person.save
# # success
# else
# # error handling
@@ -110,6 +112,9 @@ module ActionView
include FormTagHelper
include UrlHelper
include ModelNaming
+ include RecordIdentifier
+
+ attr_internal :default_form_builder
# Creates a form that allows the user to create or update the attributes
# of a specific model object.
@@ -138,6 +143,7 @@ module ActionView
# will get expanded to
#
# <%= text_field :person, :first_name %>
+ #
# which results in an HTML <tt><input></tt> tag whose +name+ attribute is
# <tt>person[first_name]</tt>. This means that when the form is submitted,
# the value entered by the user will be available in the controller as
@@ -843,8 +849,8 @@ module ActionView
# file_field(:user, :avatar)
# # => <input type="file" id="user_avatar" name="user[avatar]" />
#
- # file_field(:post, :image, :multiple => true)
- # # => <input type="file" id="post_image" name="post[image]" multiple="true" />
+ # file_field(:post, :image, multiple: true)
+ # # => <input type="file" id="post_image" name="post[image][]" multiple="multiple" />
#
# file_field(:post, :attached, accept: 'text/html')
# # => <input accept="text/html" type="file" id="post_attached" name="post[attached]" />
@@ -1032,7 +1038,7 @@ module ActionView
# date_field("user", "born_on")
# # => <input id="user_born_on" name="user[born_on]" type="date" />
#
- # The default value is generated by trying to call "to_date"
+ # The default value is generated by trying to call +strftime+ with "%Y-%m-%d"
# on the object's value, which makes it behave as expected for instances
# of DateTime and ActiveSupport::TimeWithZone. You can still override that
# by passing the "value" option explicitly, e.g.
@@ -1224,12 +1230,12 @@ module ActionView
object_name = model_name_from_record_or_class(object).param_key
end
- builder = options[:builder] || default_form_builder
+ builder = options[:builder] || default_form_builder_class
builder.new(object_name, object, self, options)
end
- def default_form_builder
- builder = ActionView::Base.default_form_builder
+ def default_form_builder_class
+ builder = default_form_builder || ActionView::Base.default_form_builder
builder.respond_to?(:constantize) ? builder.constantize : builder
end
end
@@ -1244,7 +1250,7 @@ module ActionView
# Admin: <%= person_form.check_box :admin %>
# <% end %>
#
- # In the above block, the a +FormBuilder+ object is yielded as the
+ # In the above block, a +FormBuilder+ object is yielded as the
# +person_form+ variable. This allows you to generate the +text_field+
# and +check_box+ fields by specifying their eponymous methods, which
# modify the underlying template and associates the +@person+ model object
@@ -1265,6 +1271,7 @@ module ActionView
# )
# )
# end
+ # end
#
# The above code creates a new method +div_radio_button+ which wraps a div
# around the new radio button. Note that when options are passed in, you
@@ -1610,7 +1617,14 @@ module ActionView
@auto_index
end
- record_name = index ? "#{object_name}[#{index}][#{record_name}]" : "#{object_name}[#{record_name}]"
+ record_name = if index
+ "#{object_name}[#{index}][#{record_name}]"
+ elsif record_name.to_s.end_with?('[]')
+ record_name = record_name.to_s.sub(/(.*)\[\]$/, "[\\1][#{record_object.id}]")
+ "#{object_name}#{record_name}"
+ else
+ "#{object_name}[#{record_name}]"
+ end
fields_options[:child_index] = index
@template.fields_for(record_name, record_object, fields_options, &block)
@@ -1624,7 +1638,7 @@ module ActionView
# target labels for radio_button tags (where the value is used in the ID of the input tag).
#
# ==== Examples
- # label(:post, :title)
+ # label(:title)
# # => <label for="post_title">Title</label>
#
# You can localize your labels based on model and attribute names.
@@ -1637,7 +1651,7 @@ module ActionView
#
# Which then will result in
#
- # label(:post, :body)
+ # label(:body)
# # => <label for="post_body">Write your entire text here</label>
#
# Localization can also be based purely on the translation of the attribute-name
@@ -1648,21 +1662,22 @@ module ActionView
# post:
# cost: "Total cost"
#
- # label(:post, :cost)
+ # label(:cost)
# # => <label for="post_cost">Total cost</label>
#
- # label(:post, :title, "A short title")
+ # label(:title, "A short title")
# # => <label for="post_title">A short title</label>
#
- # label(:post, :title, "A short title", class: "title_label")
+ # label(:title, "A short title", class: "title_label")
# # => <label for="post_title" class="title_label">A short title</label>
#
- # label(:post, :privacy, "Public Post", value: "public")
+ # label(:privacy, "Public Post", value: "public")
# # => <label for="post_privacy_public">Public Post</label>
#
- # label(:post, :terms) do
+ # label(:terms) do
# 'Accept <a href="/terms">Terms</a>.'.html_safe
# end
+ # # => <label for="post_terms">Accept <a href="/terms">Terms</a>.</label>
def label(method, text = nil, options = {}, &block)
@template.label(@object_name, method, text, objectify_options(options), &block)
end
@@ -1711,16 +1726,17 @@ module ActionView
# hashes instead of arrays.
#
# # Let's say that @post.validated? is 1:
- # check_box("post", "validated")
+ # check_box("validated")
# # => <input name="post[validated]" type="hidden" value="0" />
# # <input checked="checked" type="checkbox" id="post_validated" name="post[validated]" value="1" />
#
# # Let's say that @puppy.gooddog is "no":
- # check_box("puppy", "gooddog", {}, "yes", "no")
+ # check_box("gooddog", {}, "yes", "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")
+ # # Let's say that @eula.accepted is "no":
+ # check_box("accepted", { class: 'eula_check' }, "yes", "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(method, options = {}, checked_value = "1", unchecked_value = "0")
@@ -1735,13 +1751,14 @@ module ActionView
# +options+ hash. You may pass HTML options there as well.
#
# # Let's say that @post.category returns "rails":
- # radio_button("post", "category", "rails")
- # radio_button("post", "category", "java")
+ # radio_button("category", "rails")
+ # radio_button("category", "java")
# # => <input type="radio" id="post_category_rails" name="post[category]" value="rails" checked="checked" />
# # <input type="radio" id="post_category_java" name="post[category]" value="java" />
#
- # radio_button("user", "receive_newsletter", "yes")
- # radio_button("user", "receive_newsletter", "no")
+ # # Let's say that @user.category returns "no":
+ # radio_button("receive_newsletter", "yes")
+ # radio_button("receive_newsletter", "no")
# # => <input type="radio" id="user_receive_newsletter_yes" name="user[receive_newsletter]" value="yes" />
# # <input type="radio" id="user_receive_newsletter_no" name="user[receive_newsletter]" value="no" checked="checked" />
def radio_button(method, tag_value, options = {})
@@ -1754,14 +1771,17 @@ module ActionView
# shown.
#
# ==== Examples
- # hidden_field(:signup, :pass_confirm)
- # # => <input type="hidden" id="signup_pass_confirm" name="signup[pass_confirm]" value="#{@signup.pass_confirm}" />
+ # # Let's say that @signup.pass_confirm returns true:
+ # hidden_field(:pass_confirm)
+ # # => <input type="hidden" id="signup_pass_confirm" name="signup[pass_confirm]" value="true" />
#
- # hidden_field(:post, :tag_list)
- # # => <input type="hidden" id="post_tag_list" name="post[tag_list]" value="#{@post.tag_list}" />
+ # # Let's say that @post.tag_list returns "blog, ruby":
+ # hidden_field(:tag_list)
+ # # => <input type="hidden" id="post_tag_list" name="post[tag_list]" value="blog, ruby" />
#
- # hidden_field(:user, :token)
- # # => <input type="hidden" id="user_token" name="user[token]" value="#{@user.token}" />
+ # # Let's say that @user.token returns "abcde":
+ # hidden_field(:token)
+ # # => <input type="hidden" id="user_token" name="user[token]" value="abcde" />
#
def hidden_field(method, options = {})
@emitted_hidden_id = true if method == :id
@@ -1782,19 +1802,24 @@ module ActionView
# * <tt>:accept</tt> - If set to one or multiple mime-types, the user will be suggested a filter when choosing a file. You still need to set up model validations.
#
# ==== Examples
- # file_field(:user, :avatar)
+ # # Let's say that @user has avatar:
+ # file_field(:avatar)
# # => <input type="file" id="user_avatar" name="user[avatar]" />
#
- # file_field(:post, :image, :multiple => true)
- # # => <input type="file" id="post_image" name="post[image]" multiple="true" />
+ # # Let's say that @post has image:
+ # file_field(:image, :multiple => true)
+ # # => <input type="file" id="post_image" name="post[image][]" multiple="multiple" />
#
- # file_field(:post, :attached, accept: 'text/html')
+ # # Let's say that @post has attached:
+ # file_field(:attached, accept: 'text/html')
# # => <input accept="text/html" type="file" id="post_attached" name="post[attached]" />
#
- # file_field(:post, :image, accept: 'image/png,image/gif,image/jpeg')
+ # # Let's say that @post has image:
+ # file_field(:image, accept: 'image/png,image/gif,image/jpeg')
# # => <input type="file" id="post_image" name="post[image]" accept="image/png,image/gif,image/jpeg" />
#
- # file_field(:attachment, :file, class: 'file_input')
+ # # Let's say that @attachment has file:
+ # file_field(:file, class: 'file_input')
# # => <input type="file" id="attachment_file" name="attachment[file]" class="file_input" />
def file_field(method, options = {})
self.multipart = true
@@ -1862,7 +1887,7 @@ module ActionView
# create: "Add %{model}"
#
# ==== Examples
- # button("Create a post")
+ # button("Create post")
# # => <button name='button' type='submit'>Create post</button>
#
# button do
@@ -1923,7 +1948,11 @@ module ActionView
explicit_child_index = options[:child_index]
output = ActiveSupport::SafeBuffer.new
association.each do |child|
- options[:child_index] = nested_child_index(name) unless explicit_child_index
+ if explicit_child_index
+ options[:child_index] = explicit_child_index.call if explicit_child_index.respond_to?(:call)
+ else
+ options[:child_index] = nested_child_index(name)
+ end
output << fields_for_nested_model("#{name}[#{options[:child_index]}]", child, options, block)
end
output
diff --git a/actionview/lib/action_view/helpers/form_options_helper.rb b/actionview/lib/action_view/helpers/form_options_helper.rb
index bbfbf482a4..430051379d 100644
--- a/actionview/lib/action_view/helpers/form_options_helper.rb
+++ b/actionview/lib/action_view/helpers/form_options_helper.rb
@@ -18,10 +18,10 @@ module ActionView
#
# could become:
#
- # <select name="post[category]">
- # <option></option>
- # <option>joke</option>
- # <option>poem</option>
+ # <select name="post[category]" id="post_category">
+ # <option value=""></option>
+ # <option value="joke">joke</option>
+ # <option value="poem">poem</option>
# </select>
#
# Another common case is a select tag for a <tt>belongs_to</tt>-associated object.
@@ -32,11 +32,11 @@ module ActionView
#
# could become:
#
- # <select name="post[person_id]">
+ # <select name="post[person_id]" id="post_person_id">
# <option value="">None</option>
# <option value="1">David</option>
- # <option value="2" selected="selected">Sam</option>
- # <option value="3">Tobias</option>
+ # <option value="2" selected="selected">Eileen</option>
+ # <option value="3">Rafael</option>
# </select>
#
# * <tt>:prompt</tt> - set to true or a prompt string. When the select element doesn't have a value yet, this prepends an option with a generic prompt -- "Please select" -- or the given prompt string.
@@ -45,11 +45,11 @@ module ActionView
#
# could become:
#
- # <select name="post[person_id]">
+ # <select name="post[person_id]" id="post_person_id">
# <option value="">Select Person</option>
# <option value="1">David</option>
- # <option value="2">Sam</option>
- # <option value="3">Tobias</option>
+ # <option value="2">Eileen</option>
+ # <option value="3">Rafael</option>
# </select>
#
# * <tt>:index</tt> - like the other form helpers, +select+ can accept an <tt>:index</tt> option to manually set the ID used in the resulting output. Unlike other helpers, +select+ expects this
@@ -71,19 +71,19 @@ module ActionView
#
# could become:
#
- # <select name="post[category]">
- # <option></option>
- # <option>joke</option>
- # <option>poem</option>
- # <option disabled="disabled">restricted</option>
+ # <select name="post[category]" id="post_category">
+ # <option value=""></option>
+ # <option value="joke">joke</option>
+ # <option value="poem">poem</option>
+ # <option disabled="disabled" value="restricted">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.
#
- # collection_select(:post, :category_id, Category.all, :id, :name, {disabled: lambda{|category| category.archived? }})
+ # collection_select(:post, :category_id, Category.all, :id, :name, {disabled: -> (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]">
+ # <select name="post[category_id]" id="post_category_id">
# <option value="1" disabled="disabled">2008 stuff</option>
# <option value="2" disabled="disabled">Christmas</option>
# <option value="3">Jokes</option>
@@ -109,11 +109,11 @@ module ActionView
#
# would become:
#
- # <select name="post[person_id]">
+ # <select name="post[person_id]" id="post_person_id">
# <option value=""></option>
# <option value="1" selected="selected">David</option>
- # <option value="2">Sam</option>
- # <option value="3">Tobias</option>
+ # <option value="2">Eileen</option>
+ # <option value="3">Rafael</option>
# </select>
#
# assuming the associated person has ID 1.
@@ -192,7 +192,7 @@ module ActionView
# 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]">
+ # <select name="post[author_id]" id="post_author_id">
# <option value="">Please select</option>
# <option value="1" selected="selected">D. Heinemeier Hansson</option>
# <option value="2">D. Thomas</option>
@@ -243,7 +243,7 @@ module ActionView
#
# Possible output:
#
- # <select name="city[country_id]">
+ # <select name="city[country_id]" id="city_country_id">
# <optgroup label="Africa">
# <option value="1">South Africa</option>
# <option value="3">Somalia</option>
@@ -302,17 +302,17 @@ module ActionView
# # => <option value="DKK">Kroner</option>
#
# options_for_select([ "VISA", "MasterCard" ], "MasterCard")
- # # => <option>VISA</option>
- # # => <option selected="selected">MasterCard</option>
+ # # => <option value="VISA">VISA</option>
+ # # => <option selected="selected" value="MasterCard">MasterCard</option>
#
# options_for_select({ "Basic" => "$20", "Plus" => "$40" }, "$40")
# # => <option value="$20">Basic</option>
# # => <option value="$40" selected="selected">Plus</option>
#
# options_for_select([ "VISA", "MasterCard", "Discover" ], ["VISA", "Discover"])
- # # => <option selected="selected">VISA</option>
- # # => <option>MasterCard</option>
- # # => <option selected="selected">Discover</option>
+ # # => <option selected="selected" value="VISA">VISA</option>
+ # # => <option value="MasterCard">MasterCard</option>
+ # # => <option selected="selected" value="Discover">Discover</option>
#
# You can optionally provide HTML attributes as the last element of the array.
#
@@ -410,7 +410,7 @@ module ActionView
# * +collection+ - An array of objects representing the <tt><optgroup></tt> tags.
# * +group_method+ - The name of a method which, when called on a member of +collection+, returns an
# array of child objects representing the <tt><option></tt> tags.
- # * group_label_method+ - The name of a method which, when called on a member of +collection+, returns a
+ # * +group_label_method+ - The name of a method which, when called on a member of +collection+, returns a
# string to be used as the +label+ attribute for its <tt><optgroup></tt> tag.
# * +option_key_method+ - The name of a method which, when called on a child object of a member of
# +collection+, returns a value to be used as the +value+ attribute for its <tt><option></tt> tag.
@@ -456,7 +456,7 @@ module ActionView
option_tags = options_from_collection_for_select(
group.send(group_method), option_key_method, option_value_method, selected_key)
- content_tag(:optgroup, option_tags, label: group.send(group_label_method))
+ content_tag("optgroup".freeze, option_tags, label: group.send(group_label_method))
end.join.html_safe
end
@@ -528,7 +528,7 @@ module ActionView
body = "".html_safe
if prompt
- body.safe_concat content_tag(:option, prompt_text(prompt), value: "")
+ body.safe_concat content_tag("option".freeze, prompt_text(prompt), value: "")
end
grouped_options.each do |container|
@@ -541,14 +541,14 @@ module ActionView
end
html_attributes = { label: label }.merge!(html_attributes)
- body.safe_concat content_tag(:optgroup, options_for_select(container, selected_key), html_attributes)
+ body.safe_concat content_tag("optgroup".freeze, options_for_select(container, selected_key), html_attributes)
end
body
end
# Returns a string of option tags for pretty much any time zone in the
- # world. Supply a ActiveSupport::TimeZone name as +selected+ to have it
+ # world. Supply an ActiveSupport::TimeZone name as +selected+ to have it
# marked as the selected option tag. You can also supply an array of
# ActiveSupport::TimeZone objects as +priority_zones+, so that they will
# be listed above the rest of the (long) list. (You can use
@@ -556,7 +556,7 @@ module ActionView
# of the US time zones, or a Regexp to select the zones of your choice)
#
# The +selected+ parameter must be either +nil+, or a string that names
- # a ActiveSupport::TimeZone.
+ # an ActiveSupport::TimeZone.
#
# By default, +model+ is the ActiveSupport::TimeZone constant (which can
# be obtained in Active Record as a value object). The only requirement
@@ -577,7 +577,7 @@ module ActionView
end
zone_options.safe_concat options_for_select(convert_zones[priority_zones], selected)
- zone_options.safe_concat content_tag(:option, '-------------', value: '', disabled: true)
+ zone_options.safe_concat content_tag("option".freeze, '-------------', value: '', disabled: true)
zone_options.safe_concat "\n"
zones = zones - priority_zones
@@ -644,6 +644,24 @@ module ActionView
# collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) do |b|
# b.label(:"data-value" => b.value) { b.radio_button + b.text }
# end
+ #
+ # ==== Gotcha
+ #
+ # The HTML specification says when nothing is select on a collection of radio buttons
+ # web browsers do not send any value to server.
+ # Unfortunately this introduces a gotcha:
+ # if a +User+ model has a +category_id+ field, and in the form none category is selected no +category_id+ parameter is sent. So,
+ # any strong parameters idiom like
+ #
+ # params.require(:user).permit(...)
+ #
+ # will raise an error since no +{user: ...}+ will be present.
+ #
+ # To prevent this the helper generates an auxiliary hidden field before
+ # every collection of radio buttons. The hidden field has the same name as collection radio button and blank value.
+ #
+ # In case if you don't want the helper to generate this hidden field you can specify
+ # <tt>include_hidden: false</tt> option.
def collection_radio_buttons(object, method, collection, value_method, text_method, options = {}, html_options = {}, &block)
Tags::CollectionRadioButtons.new(object, method, self, collection, value_method, text_method, options, html_options).render(&block)
end
@@ -707,6 +725,27 @@ module ActionView
# collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial) do |b|
# b.label(:"data-value" => b.value) { b.check_box + b.text }
# end
+ #
+ # ==== Gotcha
+ #
+ # When no selection is made for a collection of checkboxes most
+ # web browsers will not send any value.
+ #
+ # For example, if we have a +User+ model with +category_ids+ field and we
+ # have the following code in our update action:
+ #
+ # @user.update(params[:user])
+ #
+ # If no +category_ids+ are selected then we can safely assume this field
+ # will not be updated.
+ #
+ # This is possible thanks to a hidden field generated by the helper method
+ # for every collection of checkboxes.
+ # This hidden field is given the same field name as the checkboxes with a
+ # blank value.
+ #
+ # In the rare case you don't want this hidden field, you can pass the
+ # <tt>include_hidden: false</tt> option to the helper method.
def collection_check_boxes(object, method, collection, value_method, text_method, options = {}, html_options = {}, &block)
Tags::CollectionCheckBoxes.new(object, method, self, collection, value_method, text_method, options, html_options).render(&block)
end
diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb
index 93c04fbec6..0191064326 100644
--- a/actionview/lib/action_view/helpers/form_tag_helper.rb
+++ b/actionview/lib/action_view/helpers/form_tag_helper.rb
@@ -20,7 +20,7 @@ module ActionView
mattr_accessor :embed_authenticity_token_in_remote_forms
self.embed_authenticity_token_in_remote_forms = false
- # Starts a form tag that points the action to an url configured with <tt>url_for_options</tt> just like
+ # Starts a form tag that points the action to a url configured with <tt>url_for_options</tt> just like
# ActionController::Base#url_for. The method for the form defaults to POST.
#
# ==== Options
@@ -80,18 +80,17 @@ module ActionView
# associated records. <tt>option_tags</tt> is a string containing the option tags for the select box.
#
# ==== Options
- # * <tt>:multiple</tt> - If set to true the selection will allow multiple choices.
+ # * <tt>:multiple</tt> - If set to true, the selection will allow multiple choices.
# * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
# * <tt>:include_blank</tt> - If set to true, an empty option will be created. If set to a string, the string will be used as the option's content and the value will be empty.
# * <tt>:prompt</tt> - Create a prompt option with blank value and the text asking user to select something.
- # * <tt>:selected</tt> - Provide a default selected value. It should be of the exact type as the provided options.
# * Any other key creates standard HTML attributes for the tag.
#
# ==== Examples
# select_tag "people", options_from_collection_for_select(@people, "id", "name")
# # <select id="people" name="people"><option value="1">David</option></select>
#
- # select_tag "people", options_from_collection_for_select(@people, "id", "name"), selected: ["1", "David"]
+ # select_tag "people", options_from_collection_for_select(@people, "id", "name", "1")
# # <select id="people" name="people"><option value="1" selected="selected">David</option></select>
#
# select_tag "people", "<option>David</option>".html_safe
@@ -141,15 +140,15 @@ module ActionView
end
if include_blank
- option_tags = content_tag(:option, include_blank, value: '').safe_concat(option_tags)
+ option_tags = content_tag("option".freeze, include_blank, value: '').safe_concat(option_tags)
end
end
if prompt = options.delete(:prompt)
- option_tags = content_tag(:option, prompt, value: '').safe_concat(option_tags)
+ option_tags = content_tag("option".freeze, prompt, value: '').safe_concat(option_tags)
end
- content_tag :select, option_tags, { "name" => html_name, "id" => sanitize_to_id(name) }.update(options.stringify_keys)
+ content_tag "select".freeze, option_tags, { "name" => html_name, "id" => sanitize_to_id(name) }.update(options.stringify_keys)
end
# Creates a standard text field; use these text fields to input smaller chunks of text like a username
@@ -415,42 +414,57 @@ module ActionView
# the form is processed normally, otherwise no action is taken.
# * <tt>:disable_with</tt> - Value of this parameter will be used as the value for a
# disabled version of the submit button when the form is submitted. This feature is
- # provided by the unobtrusive JavaScript driver.
+ # provided by the unobtrusive JavaScript driver. To disable this feature for a single submit tag
+ # pass <tt>:data => { disable_with: false }</tt> Defaults to value attribute.
#
# ==== Examples
# submit_tag
- # # => <input name="commit" type="submit" value="Save changes" />
+ # # => <input name="commit" data-disable-with="Save changes" type="submit" value="Save changes" />
#
# submit_tag "Edit this article"
- # # => <input name="commit" type="submit" value="Edit this article" />
+ # # => <input name="commit" data-disable-with="Edit this article" type="submit" value="Edit this article" />
#
# submit_tag "Save edits", disabled: true
- # # => <input disabled="disabled" name="commit" type="submit" value="Save edits" />
+ # # => <input disabled="disabled" name="commit" data-disable-with="Save edits" type="submit" value="Save edits" />
#
- # submit_tag "Complete sale", data: { disable_with: "Please wait..." }
- # # => <input name="commit" data-disable-with="Please wait..." type="submit" value="Complete sale" />
+ # submit_tag "Complete sale", data: { disable_with: "Submitting..." }
+ # # => <input name="commit" data-disable-with="Submitting..." type="submit" value="Complete sale" />
#
# submit_tag nil, class: "form_submit"
# # => <input class="form_submit" name="commit" type="submit" />
#
# submit_tag "Edit", class: "edit_button"
- # # => <input class="edit_button" name="commit" type="submit" value="Edit" />
+ # # => <input class="edit_button" data-disable-with="Edit" name="commit" type="submit" value="Edit" />
#
# submit_tag "Save", data: { confirm: "Are you sure?" }
- # # => <input name='commit' type='submit' value='Save' data-confirm="Are you sure?" />
+ # # => <input name='commit' type='submit' value='Save' data-disable-with="Save" data-confirm="Are you sure?" />
#
def submit_tag(value = "Save changes", options = {})
options = options.stringify_keys
+ tag_options = { "type" => "submit", "name" => "commit", "value" => value }.update(options)
+
+ if ActionView::Base.automatically_disable_submit_tag
+ unless tag_options["data-disable-with"] == false || (tag_options["data"] && tag_options["data"][:disable_with] == false)
+ disable_with_text = tag_options["data-disable-with"]
+ disable_with_text ||= tag_options["data"][:disable_with] if tag_options["data"]
+ disable_with_text ||= value.clone
+ tag_options.deep_merge!("data" => { "disable_with" => disable_with_text })
+ else
+ tag_options["data"].delete(:disable_with) if tag_options["data"]
+ end
+ tag_options.delete("data-disable-with")
+ end
- tag :input, { "type" => "submit", "name" => "commit", "value" => value }.update(options)
+ tag :input, tag_options
end
# Creates a button element that defines a <tt>submit</tt> button,
# <tt>reset</tt>button or a generic button which can be used in
# JavaScript, for example. You can use the button tag as a regular
# submit tag but it isn't supported in legacy browsers. However,
- # the button tag allows richer labels such as images and emphasis,
- # so this helper will also accept a block.
+ # the button tag does allow for richer labels such as images and emphasis,
+ # so this helper will also accept a block. By default, it will create
+ # a button tag with type `submit`, if type is not given.
#
# ==== Options
# * <tt>:data</tt> - This option can be used to add custom data attributes.
@@ -473,6 +487,15 @@ module ActionView
# button_tag
# # => <button name="button" type="submit">Button</button>
#
+ # button_tag 'Reset', type: 'reset'
+ # # => <button name="button" type="reset">Reset</button>
+ #
+ # button_tag 'Button', type: 'button'
+ # # => <button name="button" type="button">Button</button>
+ #
+ # button_tag 'Reset', type: 'reset', disabled: true
+ # # => <button name="button" type="reset" disabled="disabled">Reset</button>
+ #
# button_tag(type: 'button') do
# content_tag(:strong, 'Ask me!')
# end
@@ -480,6 +503,9 @@ module ActionView
# # <strong>Ask me!</strong>
# # </button>
#
+ # button_tag "Save", data: { confirm: "Are you sure?" }
+ # # => <button name="button" type="submit" data-confirm="Are you sure?">Save</button>
+ #
# button_tag "Checkout", data: { disable_with: "Please wait..." }
# # => <button data-disable-with="Please wait..." name="button" type="submit">Checkout</button>
#
@@ -556,7 +582,7 @@ module ActionView
# # => <fieldset class="format"><p><input id="name" name="name" type="text" /></p></fieldset>
def field_set_tag(legend = nil, options = nil, &block)
output = tag(:fieldset, options, true)
- output.safe_concat(content_tag(:legend, legend)) unless legend.blank?
+ output.safe_concat(content_tag("legend".freeze, legend)) unless legend.blank?
output.concat(capture(&block)) if block_given?
output.safe_concat("</fieldset>")
end
@@ -777,10 +803,10 @@ module ActionView
# # => <input id="quantity" name="quantity" min="1" max="9" type="number" />
#
# number_field_tag 'quantity', nil, min: 1, max: 10
- # # => <input id="quantity" name="quantity" min="1" max="9" type="number" />
+ # # => <input id="quantity" name="quantity" min="1" max="10" type="number" />
#
# number_field_tag 'quantity', nil, min: 1, max: 10, step: 2
- # # => <input id="quantity" name="quantity" min="1" max="9" step="2" type="number" />
+ # # => <input id="quantity" name="quantity" min="1" max="10" step="2" type="number" />
#
# number_field_tag 'quantity', '1', class: 'special_input', disabled: true
# # => <input disabled="disabled" class="special_input" id="quantity" name="quantity" type="number" value="1" />
diff --git a/actionview/lib/action_view/helpers/javascript_helper.rb b/actionview/lib/action_view/helpers/javascript_helper.rb
index 629c447f3f..ed7e882c94 100644
--- a/actionview/lib/action_view/helpers/javascript_helper.rb
+++ b/actionview/lib/action_view/helpers/javascript_helper.rb
@@ -21,7 +21,7 @@ module ActionView
# Also available through the alias j(). This is particularly helpful in JavaScript
# responses, like:
#
- # $('some_element').replaceWith('<%=j render 'some/element_template' %>');
+ # $('some_element').replaceWith('<%= j render 'some/element_template' %>');
def escape_javascript(javascript)
if javascript
result = javascript.gsub(/(\\|<\/|\r\n|\342\200\250|\342\200\251|[\n\r"'])/u) {|match| JS_ESCAPE_MAP[match] }
@@ -47,8 +47,8 @@ module ActionView
# tag.
#
# javascript_tag "alert('All is good')", defer: 'defer'
- #
- # Returns:
+ #
+ # Returns:
# <script defer="defer">
# //<![CDATA[
# alert('All is good')
@@ -70,7 +70,7 @@ module ActionView
content_or_options_with_block
end
- content_tag(:script, javascript_cdata_section(content), html_options)
+ content_tag("script".freeze, javascript_cdata_section(content), html_options)
end
def javascript_cdata_section(content) #:nodoc:
diff --git a/actionview/lib/action_view/helpers/number_helper.rb b/actionview/lib/action_view/helpers/number_helper.rb
index f66dbfe7d3..d7182d1fac 100644
--- a/actionview/lib/action_view/helpers/number_helper.rb
+++ b/actionview/lib/action_view/helpers/number_helper.rb
@@ -1,5 +1,3 @@
-# encoding: utf-8
-
require 'active_support/core_ext/hash/keys'
require 'active_support/core_ext/string/output_safety'
require 'active_support/number_helper'
@@ -117,8 +115,8 @@ module ActionView
# (defaults to current locale).
# * <tt>:precision</tt> - Sets the precision of the number
# (defaults to 3).
- # * <tt>:significant</tt> - If +true+, precision will be the #
- # of significant_digits. If +false+, the # of fractional
+ # * <tt>:significant</tt> - If +true+, precision will be the number
+ # of significant_digits. If +false+, the number of fractional
# digits (defaults to +false+).
# * <tt>:separator</tt> - Sets the separator between the
# fractional and integer digits (defaults to ".").
@@ -141,7 +139,7 @@ module ActionView
# number_to_percentage(302.24398923423, precision: 5) # => 302.24399%
# number_to_percentage(1000, locale: :fr) # => 1 000,000%
# number_to_percentage("98a") # => 98a%
- # number_to_percentage(100, format: "%n %") # => 100 %
+ # number_to_percentage(100, format: "%n %") # => 100.000 %
#
# number_to_percentage("98a", raise: true) # => InvalidNumberError
def number_to_percentage(number, options = {})
@@ -192,8 +190,8 @@ module ActionView
# (defaults to current locale).
# * <tt>:precision</tt> - Sets the precision of the number
# (defaults to 3).
- # * <tt>:significant</tt> - If +true+, precision will be the #
- # of significant_digits. If +false+, the # of fractional
+ # * <tt>:significant</tt> - If +true+, precision will be the number
+ # of significant_digits. If +false+, the number of fractional
# digits (defaults to +false+).
# * <tt>:separator</tt> - Sets the separator between the
# fractional and integer digits (defaults to ".").
@@ -240,8 +238,8 @@ module ActionView
# (defaults to current locale).
# * <tt>:precision</tt> - Sets the precision of the number
# (defaults to 3).
- # * <tt>:significant</tt> - If +true+, precision will be the #
- # of significant_digits. If +false+, the # of fractional
+ # * <tt>:significant</tt> - If +true+, precision will be the number
+ # of significant_digits. If +false+, the number of fractional
# digits (defaults to +true+)
# * <tt>:separator</tt> - Sets the separator between the
# fractional and integer digits (defaults to ".").
@@ -280,7 +278,7 @@ module ActionView
# See <tt>number_to_human_size</tt> if you want to print a file
# size.
#
- # You can also define you own unit-quantifier names if you want
+ # You can also define your own unit-quantifier names if you want
# to use other decimal units (eg.: 1500 becomes "1.5
# kilometers", 0.150 becomes "150 milliliters", etc). You may
# define a wide range of unit quantifiers, even fractional ones
@@ -292,8 +290,8 @@ module ActionView
# (defaults to current locale).
# * <tt>:precision</tt> - Sets the precision of the number
# (defaults to 3).
- # * <tt>:significant</tt> - If +true+, precision will be the #
- # of significant_digits. If +false+, the # of fractional
+ # * <tt>:significant</tt> - If +true+, precision will be the number
+ # of significant_digits. If +false+, the number of fractional
# digits (defaults to +true+)
# * <tt>:separator</tt> - Sets the separator between the
# fractional and integer digits (defaults to ".").
diff --git a/actionview/lib/action_view/helpers/record_tag_helper.rb b/actionview/lib/action_view/helpers/record_tag_helper.rb
index 77c3e6d394..f7ee573035 100644
--- a/actionview/lib/action_view/helpers/record_tag_helper.rb
+++ b/actionview/lib/action_view/helpers/record_tag_helper.rb
@@ -1,108 +1,21 @@
-require 'action_view/record_identifier'
-
module ActionView
- # = Action View Record Tag Helpers
module Helpers
module RecordTagHelper
- include ActionView::RecordIdentifier
-
- # Produces a wrapper DIV element with id and class parameters that
- # relate to the specified Active Record object. Usage example:
- #
- # <%= div_for(@person, class: "foo") do %>
- # <%= @person.name %>
- # <% end %>
- #
- # produces:
- #
- # <div id="person_123" class="person foo"> Joe Bloggs </div>
- #
- # You can also pass an array of Active Record objects, which will then
- # get iterated over and yield each record as an argument for the block.
- # For example:
- #
- # <%= div_for(@people, class: "foo") do |person| %>
- # <%= person.name %>
- # <% end %>
- #
- # produces:
- #
- # <div id="person_123" class="person foo"> Joe Bloggs </div>
- # <div id="person_124" class="person foo"> Jane Bloggs </div>
- #
- def div_for(record, *args, &block)
- content_tag_for(:div, record, *args, &block)
+ def div_for(*)
+ raise NoMethodError, "The `div_for` method has been removed from " \
+ "Rails. To continue using it, add the `record_tag_helper` gem to " \
+ "your Gemfile:\n" \
+ " gem 'record_tag_helper', '~> 1.0'\n" \
+ "Consult the Rails upgrade guide for details."
end
- # content_tag_for creates an HTML element with id and class parameters
- # that relate to the specified Active Record object. For example:
- #
- # <%= content_tag_for(:tr, @person) do %>
- # <td><%= @person.first_name %></td>
- # <td><%= @person.last_name %></td>
- # <% end %>
- #
- # would produce the following HTML (assuming @person is an instance of
- # a Person object, with an id value of 123):
- #
- # <tr id="person_123" class="person">....</tr>
- #
- # If you require the HTML id attribute to have a prefix, you can specify it:
- #
- # <%= content_tag_for(:tr, @person, :foo) do %> ...
- #
- # produces:
- #
- # <tr id="foo_person_123" class="person">...
- #
- # You can also pass an array of objects which this method will loop through
- # and yield the current object to the supplied block, reducing the need for
- # having to iterate through the object (using <tt>each</tt>) beforehand.
- # For example (assuming @people is an array of Person objects):
- #
- # <%= content_tag_for(:tr, @people) do |person| %>
- # <td><%= person.first_name %></td>
- # <td><%= person.last_name %></td>
- # <% end %>
- #
- # produces:
- #
- # <tr id="person_123" class="person">...</tr>
- # <tr id="person_124" class="person">...</tr>
- #
- # content_tag_for also accepts a hash of options, which will be converted to
- # additional HTML attributes. If you specify a <tt>:class</tt> value, it will be combined
- # with the default class name for your object. For example:
- #
- # <%= content_tag_for(:li, @person, class: "bar") %>...
- #
- # produces:
- #
- # <li id="person_123" class="person bar">...
- #
- def content_tag_for(tag_name, single_or_multiple_records, prefix = nil, options = nil, &block)
- options, prefix = prefix, nil if prefix.is_a?(Hash)
-
- Array(single_or_multiple_records).map do |single_record|
- content_tag_for_single_record(tag_name, single_record, prefix, options, &block)
- end.join("\n").html_safe
+ def content_tag_for(*)
+ raise NoMethodError, "The `content_tag_for` method has been removed from " \
+ "Rails. To continue using it, add the `record_tag_helper` gem to " \
+ "your Gemfile:\n" \
+ " gem 'record_tag_helper', '~> 1.0'\n" \
+ "Consult the Rails upgrade guide for details."
end
-
- private
-
- # Called by <tt>content_tag_for</tt> internally to render a content tag
- # for each record.
- def content_tag_for_single_record(tag_name, record, prefix, options, &block)
- options = options ? options.dup : {}
- options[:class] = [ dom_class(record, prefix), options[:class] ].compact
- options[:id] = dom_id(record, prefix)
-
- if block_given?
- content_tag(tag_name, capture(record, &block), options)
- else
- content_tag(tag_name, "", options)
- end
- end
end
end
end
diff --git a/actionview/lib/action_view/helpers/rendering_helper.rb b/actionview/lib/action_view/helpers/rendering_helper.rb
index 827932d8e2..c98f2d74a8 100644
--- a/actionview/lib/action_view/helpers/rendering_helper.rb
+++ b/actionview/lib/action_view/helpers/rendering_helper.rb
@@ -18,7 +18,7 @@ module ActionView
# 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>
+ # type of <tt>text/plain</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
diff --git a/actionview/lib/action_view/helpers/sanitize_helper.rb b/actionview/lib/action_view/helpers/sanitize_helper.rb
index e72e85ee5f..191a881de0 100644
--- a/actionview/lib/action_view/helpers/sanitize_helper.rb
+++ b/actionview/lib/action_view/helpers/sanitize_helper.rb
@@ -8,76 +8,77 @@ module ActionView
# These helper methods extend Action View making them callable within your template files.
module SanitizeHelper
extend ActiveSupport::Concern
- # This +sanitize+ helper will HTML encode all tags and strip all attributes that
- # aren't specifically allowed.
+ # Sanitizes HTML input, stripping all tags and attributes that aren't whitelisted.
#
- # It also strips href/src tags with invalid protocols, like javascript: especially.
- # It does its best to counter any tricks that hackers may use, like throwing in
- # unicode/ascii/hex values to get past the javascript: filters. Check out
- # the extensive test suite.
+ # It also strips href/src attributes with unsafe protocols like
+ # <tt>javascript:</tt>, while also protecting against attempts to use Unicode,
+ # ASCII, and hex character references to work around these protocol filters.
#
- # <%= sanitize @article.body %>
+ # The default sanitizer is Rails::Html::WhiteListSanitizer. See {Rails HTML
+ # Sanitizers}[https://github.com/rails/rails-html-sanitizer] for more information.
#
- # You can add or remove tags/attributes if you want to customize it a bit.
- # See ActionView::Base for full docs on the available options. You can add
- # tags/attributes for single uses of +sanitize+ by passing either the
- # <tt>:attributes</tt> or <tt>:tags</tt> options:
+ # Custom sanitization rules can also be provided.
#
- # Normal Use
- #
- # <%= sanitize @article.body %>
+ # Please note that sanitizing user-provided text does not guarantee that the
+ # resulting markup is valid or even well-formed. For example, the output may still
+ # contain unescaped characters like <tt><</tt>, <tt>></tt>, or <tt>&</tt>.
#
- # Custom Use - Custom Scrubber
- # (supply a Loofah::Scrubber that does the sanitization)
+ # ==== Options
#
- # scrubber can either wrap a block:
- # scrubber = Loofah::Scrubber.new do |node|
- # node.text = "dawn of cats"
- # end
+ # * <tt>:tags</tt> - An array of allowed tags.
+ # * <tt>:attributes</tt> - An array of allowed attributes.
+ # * <tt>:scrubber</tt> - A {Rails::Html scrubber}[https://github.com/rails/rails-html-sanitizer]
+ # or {Loofah::Scrubber}[https://github.com/flavorjones/loofah] object that
+ # defines custom sanitization rules. A custom scrubber takes precedence over
+ # custom tags and attributes.
#
- # or be a subclass of Loofah::Scrubber which responds to scrub:
- # class KittyApocalypse < Loofah::Scrubber
- # def scrub(node)
- # node.text = "dawn of cats"
- # end
- # end
- # scrubber = KittyApocalypse.new
+ # ==== Examples
#
- # <%= sanitize @article.body, scrubber: scrubber %>
+ # Normal use:
#
- # A custom scrubber takes precedence over custom tags and attributes
- # Learn more about scrubbers here: https://github.com/flavorjones/loofah
+ # <%= sanitize @comment.body %>
#
- # Custom Use - tags and attributes
- # (only the mentioned tags and attributes are allowed, nothing else)
+ # Providing custom whitelisted tags and attributes:
#
- # <%= sanitize @article.body, tags: %w(table tr td), attributes: %w(id class style) %>
+ # <%= sanitize @comment.body, tags: %w(strong em a), attributes: %w(href) %>
#
- # Add table tags to the default allowed tags
+ # Providing a custom Rails::Html scrubber:
#
- # class Application < Rails::Application
- # config.action_view.sanitized_allowed_tags = ['table', 'tr', 'td']
- # end
+ # class CommentScrubber < Rails::Html::PermitScrubber
+ # def allowed_node?(node)
+ # !%w(form script comment blockquote).include?(node.name)
+ # end
#
- # Remove tags to the default allowed tags
+ # def skip_node?(node)
+ # node.text?
+ # end
#
- # class Application < Rails::Application
- # config.after_initialize do
- # ActionView::Base.sanitized_allowed_tags.delete 'div'
+ # def scrub_attribute?(name)
+ # name == 'style'
# end
# end
#
- # Change allowed default attributes
+ # <%= sanitize @comment.body, scrubber: CommentScrubber.new %>
+ #
+ # See {Rails HTML Sanitizer}[https://github.com/rails/rails-html-sanitizer] for
+ # documentation about Rails::Html scrubbers.
#
- # class Application < Rails::Application
- # config.action_view.sanitized_allowed_attributes = ['id', 'class', 'style']
+ # Providing a custom Loofah::Scrubber:
+ #
+ # scrubber = Loofah::Scrubber.new do |node|
+ # node.remove if node.name == 'script'
# end
#
- # Please note that sanitizing user-provided text does not guarantee that the
- # resulting markup is valid (conforming to a document type) or even well-formed.
- # The output may still contain e.g. unescaped '<', '>', '&' characters and
- # confuse browsers.
+ # <%= sanitize @comment.body, scrubber: scrubber %>
+ #
+ # See {Loofah's documentation}[https://github.com/flavorjones/loofah] for more
+ # information about defining custom Loofah::Scrubber objects.
#
+ # To set the default allowed tags or attributes across your application:
+ #
+ # # In config/application.rb
+ # config.action_view.sanitized_allowed_tags = ['strong', 'em', 'a']
+ # config.action_view.sanitized_allowed_attributes = ['href', 'title']
def sanitize(html, options = {})
self.class.white_list_sanitizer.sanitize(html, options).try(:html_safe)
end
@@ -87,9 +88,7 @@ module ActionView
self.class.white_list_sanitizer.sanitize_css(style)
end
- # Strips all HTML tags from the +html+, including comments. This uses
- # Nokogiri for tokenization (via Loofah) and so its HTML parsing ability
- # is limited by that of Nokogiri.
+ # Strips all HTML tags from +html+, including comments.
#
# strip_tags("Strip <i>these</i> tags!")
# # => Strip these tags!
@@ -100,10 +99,10 @@ module ActionView
# strip_tags("<div id='top-bar'>Welcome to my website!</div>")
# # => Welcome to my website!
def strip_tags(html)
- self.class.full_sanitizer.sanitize(html)
+ self.class.full_sanitizer.sanitize(html, encode_special_chars: false)
end
- # Strips all link tags from +text+ leaving just the link text.
+ # Strips all link tags from +html+ leaving just the link text.
#
# strip_links('<a href="http://www.rubyonrails.org">Ruby on Rails</a>')
# # => Ruby on Rails
@@ -121,7 +120,7 @@ module ActionView
attr_writer :full_sanitizer, :link_sanitizer, :white_list_sanitizer
# Vendors the full, link and white list sanitizers.
- # Provided strictly for compabitility and can be removed in Rails 5.
+ # Provided strictly for compatibility and can be removed in Rails 5.
def sanitizer_vendor
Rails::Html::Sanitizer
end
@@ -166,30 +165,6 @@ module ActionView
def white_list_sanitizer
@white_list_sanitizer ||= sanitizer_vendor.white_list_sanitizer.new
end
-
- ##
- # :method: sanitized_allowed_tags=
- #
- # :call-seq: sanitized_allowed_tags=(tags)
- #
- # Replaces the allowed tags for the +sanitize+ helper.
- #
- # class Application < Rails::Application
- # config.action_view.sanitized_allowed_tags = ['table', 'tr', 'td']
- # end
- #
-
- ##
- # :method: sanitized_allowed_attributes=
- #
- # :call-seq: sanitized_allowed_attributes=(attributes)
- #
- # Replaces the allowed HTML attributes for the +sanitize+ helper.
- #
- # class Application < Rails::Application
- # config.action_view.sanitized_allowed_attributes = ['onclick', 'longdesc']
- # end
- #
end
end
end
diff --git a/actionview/lib/action_view/helpers/tag_helper.rb b/actionview/lib/action_view/helpers/tag_helper.rb
index a87c223a71..2562504896 100644
--- a/actionview/lib/action_view/helpers/tag_helper.rb
+++ b/actionview/lib/action_view/helpers/tag_helper.rb
@@ -22,9 +22,10 @@ module ActionView
TAG_PREFIXES = ['aria', 'data', :aria, :data].to_set
- PRE_CONTENT_STRINGS = {
- :textarea => "\n"
- }
+ PRE_CONTENT_STRINGS = Hash.new { "".freeze }
+ PRE_CONTENT_STRINGS[:textarea] = "\n"
+ PRE_CONTENT_STRINGS["textarea"] = "\n"
+
# Returns an empty HTML tag of type +name+ which by default is XHTML
# compliant. Set +open+ to true to create an open tag compatible
@@ -143,24 +144,30 @@ module ActionView
def content_tag_string(name, content, options, escape = true)
tag_options = tag_options(options, escape) if options
content = ERB::Util.unwrapped_html_escape(content) if escape
- "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name.to_sym]}#{content}</#{name}>".html_safe
+ "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name]}#{content}</#{name}>".html_safe
end
def tag_options(options, escape = true)
return if options.blank?
- attrs = []
+ output = ""
+ sep = " ".freeze
options.each_pair do |key, value|
if TAG_PREFIXES.include?(key) && value.is_a?(Hash)
value.each_pair do |k, v|
- attrs << prefix_tag_option(key, k, v, escape)
+ output << sep
+ output << prefix_tag_option(key, k, v, escape)
end
elsif BOOLEAN_ATTRIBUTES.include?(key)
- attrs << boolean_tag_option(key) if value
+ if value
+ output << sep
+ output << boolean_tag_option(key)
+ end
elsif !value.nil?
- attrs << tag_option(key, value, escape)
+ output << sep
+ output << tag_option(key, value, escape)
end
end
- " #{attrs * ' '}" unless attrs.empty?
+ output unless output.empty?
end
def prefix_tag_option(prefix, key, value, escape)
@@ -177,7 +184,7 @@ module ActionView
def tag_option(key, value, escape)
if value.is_a?(Array)
- value = escape ? safe_join(value, " ") : value.join(" ")
+ value = escape ? safe_join(value, " ".freeze) : value.join(" ".freeze)
else
value = escape ? ERB::Util.unwrapped_html_escape(value) : value
end
diff --git a/actionview/lib/action_view/helpers/tags.rb b/actionview/lib/action_view/helpers/tags.rb
index 45c75d10c0..a4f6eb0150 100644
--- a/actionview/lib/action_view/helpers/tags.rb
+++ b/actionview/lib/action_view/helpers/tags.rb
@@ -5,6 +5,7 @@ module ActionView
eager_autoload do
autoload :Base
+ autoload :Translator
autoload :CheckBox
autoload :CollectionCheckBoxes
autoload :CollectionRadioButtons
diff --git a/actionview/lib/action_view/helpers/tags/base.rb b/actionview/lib/action_view/helpers/tags/base.rb
index 7740c60eac..d57f26ba4f 100644
--- a/actionview/lib/action_view/helpers/tags/base.rb
+++ b/actionview/lib/action_view/helpers/tags/base.rb
@@ -14,7 +14,7 @@ module ActionView
@object_name.sub!(/\[\]$/,"") || @object_name.sub!(/\[\]\]$/,"]")
@object = retrieve_object(options.delete(:object))
@options = options
- @auto_index = retrieve_autoindex(Regexp.last_match.pre_match) if Regexp.last_match
+ @auto_index = Regexp.last_match ? retrieve_autoindex(Regexp.last_match.pre_match) : nil
end
# This is what child classes implement.
@@ -79,35 +79,30 @@ module ActionView
end
def add_default_name_and_id(options)
- if options.has_key?("index")
- options["name"] ||= options.fetch("name"){ tag_name_with_index(options["index"], options["multiple"]) }
- options["id"] = options.fetch("id"){ tag_id_with_index(options["index"]) }
- options.delete("index")
- elsif defined?(@auto_index)
- options["name"] ||= options.fetch("name"){ tag_name_with_index(@auto_index, options["multiple"]) }
- options["id"] = options.fetch("id"){ tag_id_with_index(@auto_index) }
- else
- options["name"] ||= options.fetch("name"){ tag_name(options["multiple"]) }
- options["id"] = options.fetch("id"){ tag_id }
+ index = name_and_id_index(options)
+ options["name"] = options.fetch("name"){ tag_name(options["multiple"], index) }
+ options["id"] = options.fetch("id"){ tag_id(index) }
+ if namespace = options.delete("namespace")
+ options['id'] = options['id'] ? "#{namespace}_#{options['id']}" : namespace
end
-
- options["id"] = [options.delete('namespace'), options["id"]].compact.join("_").presence
- end
-
- def tag_name(multiple = false)
- "#{@object_name}[#{sanitized_method_name}]#{"[]" if multiple}"
end
- def tag_name_with_index(index, multiple = false)
- "#{@object_name}[#{index}][#{sanitized_method_name}]#{"[]" if multiple}"
- end
-
- def tag_id
- "#{sanitized_object_name}_#{sanitized_method_name}"
+ def tag_name(multiple = false, index = nil)
+ # a little duplication to construct less strings
+ if index
+ "#{@object_name}[#{index}][#{sanitized_method_name}]#{"[]" if multiple}"
+ else
+ "#{@object_name}[#{sanitized_method_name}]#{"[]" if multiple}"
+ end
end
- def tag_id_with_index(index)
- "#{sanitized_object_name}_#{index}_#{sanitized_method_name}"
+ def tag_id(index = nil)
+ # a little duplication to construct less strings
+ if index
+ "#{sanitized_object_name}_#{index}_#{sanitized_method_name}"
+ else
+ "#{sanitized_object_name}_#{sanitized_method_name}"
+ end
end
def sanitized_object_name
@@ -125,7 +120,12 @@ module ActionView
def select_content_tag(option_tags, options, html_options)
html_options = html_options.stringify_keys
add_default_name_and_id(html_options)
- options[:include_blank] ||= true unless options[:prompt] || select_not_required?(html_options)
+
+ if placeholder_required?(html_options)
+ raise ArgumentError, "include_blank cannot be false for a required field." if options[:include_blank] == false
+ options[:include_blank] ||= true unless options[:prompt]
+ end
+
value = options.fetch(:selected) { value(object) }
select = content_tag("select", add_options(option_tags, options, value), html_options)
@@ -136,8 +136,9 @@ module ActionView
end
end
- def select_not_required?(html_options)
- !html_options["required"] || html_options["multiple"] || html_options["size"].to_i > 1
+ def placeholder_required?(html_options)
+ # See https://html.spec.whatwg.org/multipage/forms.html#attr-select-required
+ html_options["required"] && !html_options["multiple"] && html_options.fetch("size", 1).to_i == 1
end
def add_options(option_tags, options, value = nil)
@@ -149,6 +150,10 @@ module ActionView
end
option_tags
end
+
+ def name_and_id_index(options)
+ options.key?("index") ? options.delete("index") || "" : @auto_index
+ end
end
end
end
diff --git a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb
index 6242a2a085..3256d44e18 100644
--- a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb
+++ b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb
@@ -9,29 +9,13 @@ module ActionView
class CheckBoxBuilder < Builder # :nodoc:
def check_box(extra_html_options={})
html_options = extra_html_options.merge(@input_html_options)
+ html_options[:multiple] = true
@template_object.check_box(@object_name, @method_name, html_options, @value, nil)
end
end
def render(&block)
- rendered_collection = render_collection do |item, value, text, default_html_options|
- default_html_options[:multiple] = true
- builder = instantiate_builder(CheckBoxBuilder, item, value, text, default_html_options)
-
- if block_given?
- @template_object.capture(builder, &block)
- else
- render_component(builder)
- end
- end
-
- # Append a hidden field to make sure something will be sent back to the
- # server if all check boxes are unchecked.
- if @options.fetch(:include_hidden, true)
- rendered_collection + hidden_field
- else
- rendered_collection
- end
+ render_collection_for(CheckBoxBuilder, &block)
end
private
@@ -39,18 +23,6 @@ module ActionView
def render_component(builder)
builder.check_box + builder.label
end
-
- def hidden_field
- hidden_name = @html_options[:name]
-
- hidden_name ||= if @options.has_key?(:index)
- "#{tag_name_with_index(@options[:index])}[]"
- else
- "#{tag_name}[]"
- end
-
- @template_object.hidden_field_tag(hidden_name, "", id: nil)
- end
end
end
end
diff --git a/actionview/lib/action_view/helpers/tags/collection_helpers.rb b/actionview/lib/action_view/helpers/tags/collection_helpers.rb
index 8050638363..b87b4281d6 100644
--- a/actionview/lib/action_view/helpers/tags/collection_helpers.rb
+++ b/actionview/lib/action_view/helpers/tags/collection_helpers.rb
@@ -19,6 +19,8 @@ module ActionView
def label(label_html_options={}, &block)
html_options = @input_html_options.slice(:index, :namespace).merge(label_html_options)
+ html_options[:for] ||= @input_html_options[:id] if @input_html_options[:id]
+
@template_object.label(@object_name, @sanitized_attribute_name, @text, html_options, &block)
end
end
@@ -79,6 +81,32 @@ module ActionView
yield item, value, text, default_html_options.merge(additional_html_options)
end.join.html_safe
end
+
+ def render_collection_for(builder_class, &block) #:nodoc:
+ options = @options.stringify_keys
+ rendered_collection = render_collection do |item, value, text, default_html_options|
+ builder = instantiate_builder(builder_class, item, value, text, default_html_options)
+
+ if block_given?
+ @template_object.capture(builder, &block)
+ else
+ render_component(builder)
+ end
+ end
+
+ # Append a hidden field to make sure something will be sent back to the
+ # server if all radio buttons are unchecked.
+ if options.fetch('include_hidden', true)
+ rendered_collection + hidden_field
+ else
+ rendered_collection
+ end
+ end
+
+ def hidden_field #:nodoc:
+ hidden_name = @html_options[:name] || "#{tag_name(false, @options[:index])}[]"
+ @template_object.hidden_field_tag(hidden_name, "", id: nil)
+ end
end
end
end
diff --git a/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb b/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb
index 20be34c1f2..21aaf122f8 100644
--- a/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb
+++ b/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb
@@ -14,15 +14,7 @@ module ActionView
end
def render(&block)
- render_collection do |item, value, text, default_html_options|
- builder = instantiate_builder(RadioButtonBuilder, item, value, text, default_html_options)
-
- if block_given?
- @template_object.capture(builder, &block)
- else
- render_component(builder)
- end
- end
+ render_collection_for(RadioButtonBuilder, &block)
end
private
diff --git a/actionview/lib/action_view/helpers/tags/label.rb b/actionview/lib/action_view/helpers/tags/label.rb
index 08a23e497e..b31d5fda66 100644
--- a/actionview/lib/action_view/helpers/tags/label.rb
+++ b/actionview/lib/action_view/helpers/tags/label.rb
@@ -15,20 +15,10 @@ module ActionView
def translation
method_and_value = @tag_value.present? ? "#{@method_name}.#{@tag_value}" : @method_name
- @object_name.gsub!(/\[(.*)_attributes\]\[\d+\]/, '.\1')
-
- if object.respond_to?(:to_model)
- key = object.model_name.i18n_key
- i18n_default = ["#{key}.#{method_and_value}".to_sym, ""]
- end
-
- i18n_default ||= ""
- content = I18n.t("#{@object_name}.#{method_and_value}", :default => i18n_default, :scope => "helpers.label").presence
-
- content ||= if object && object.class.respond_to?(:human_attribute_name)
- object.class.human_attribute_name(method_and_value)
- end
+ content ||= Translator
+ .new(object, @object_name, method_and_value, scope: "helpers.label")
+ .translate
content ||= @method_name.humanize
content
diff --git a/actionview/lib/action_view/helpers/tags/placeholderable.rb b/actionview/lib/action_view/helpers/tags/placeholderable.rb
index ae67bc13af..cf7b117614 100644
--- a/actionview/lib/action_view/helpers/tags/placeholderable.rb
+++ b/actionview/lib/action_view/helpers/tags/placeholderable.rb
@@ -7,24 +7,12 @@ module ActionView
if tag_value = @options[:placeholder]
placeholder = tag_value if tag_value.is_a?(String)
-
- object_name = @object_name.gsub(/\[(.*)_attributes\]\[\d+\]/, '.\1')
method_and_value = tag_value.is_a?(TrueClass) ? @method_name : "#{@method_name}.#{tag_value}"
- if object.respond_to?(:to_model)
- key = object.class.model_name.i18n_key
- i18n_default = ["#{key}.#{method_and_value}".to_sym, ""]
- end
-
- i18n_default ||= ""
- placeholder ||= I18n.t("#{object_name}.#{method_and_value}", :default => i18n_default, :scope => "helpers.placeholder").presence
-
- placeholder ||= if object && object.class.respond_to?(:human_attribute_name)
- object.class.human_attribute_name(method_and_value)
- end
-
+ placeholder ||= Tags::Translator
+ .new(object, @object_name, method_and_value, scope: "helpers.placeholder")
+ .translate
placeholder ||= @method_name.humanize
-
@options[:placeholder] = placeholder
end
end
diff --git a/actionview/lib/action_view/helpers/tags/translator.rb b/actionview/lib/action_view/helpers/tags/translator.rb
new file mode 100644
index 0000000000..8b6655481d
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/translator.rb
@@ -0,0 +1,40 @@
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ class Translator # :nodoc:
+ def initialize(object, object_name, method_and_value, scope:)
+ @object_name = object_name.gsub(/\[(.*)_attributes\]\[\d+\]/, '.\1')
+ @method_and_value = method_and_value
+ @scope = scope
+ @model = object.respond_to?(:to_model) ? object.to_model : nil
+ end
+
+ def translate
+ translated_attribute = I18n.t("#{object_name}.#{method_and_value}", default: i18n_default, scope: scope).presence
+ translated_attribute || human_attribute_name
+ end
+
+ protected
+
+ attr_reader :object_name, :method_and_value, :scope, :model
+
+ private
+
+ def i18n_default
+ if model
+ key = model.model_name.i18n_key
+ ["#{key}.#{method_and_value}".to_sym, ""]
+ else
+ ""
+ end
+ end
+
+ def human_attribute_name
+ if model && model.class.respond_to?(:human_attribute_name)
+ model.class.human_attribute_name(method_and_value)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/text_helper.rb b/actionview/lib/action_view/helpers/text_helper.rb
index 2c40ed1832..432693bc23 100644
--- a/actionview/lib/action_view/helpers/text_helper.rb
+++ b/actionview/lib/action_view/helpers/text_helper.rb
@@ -103,7 +103,9 @@ module ActionView
# Highlights one or more +phrases+ everywhere in +text+ by inserting it into
# a <tt>:highlighter</tt> string. The highlighter can be specialized by passing <tt>:highlighter</tt>
# as a single-quoted string with <tt>\1</tt> where the phrase is to be inserted (defaults to
- # '<mark>\1</mark>') or passing a block that receives each matched term.
+ # '<mark>\1</mark>') or passing a block that receives each matched term. By default +text+
+ # is sanitized to prevent possible XSS attacks. If the input is trustworthy, passing false
+ # for <tt>:sanitize</tt> will turn sanitizing off.
#
# highlight('You searched for: rails', 'rails')
# # => You searched for: <mark>rails</mark>
@@ -122,6 +124,9 @@ module ActionView
#
# highlight('You searched for: rails', 'rails') { |match| link_to(search_path(q: match, match)) }
# # => You searched for: <a href="search?q=rails">rails</a>
+ #
+ # highlight('<a href="javascript:alert(\'no!\')">ruby</a> on rails', 'rails', sanitize: false)
+ # # => "<a>ruby</a> on <mark>rails</mark>"
def highlight(text, phrases, options = {})
text = sanitize(text) if options.fetch(:sanitize, true)
@@ -201,6 +206,11 @@ module ActionView
# +plural+ is supplied, it will use that when count is > 1, otherwise
# it will use the Inflector to determine the plural form.
#
+ # If passed an optional +locale:+ parameter, the word will be pluralized
+ # using rules defined for that language (you must define your own
+ # inflection rules for languages other than English). See
+ # ActiveSupport::Inflector.pluralize
+ #
# pluralize(1, 'person')
# # => 1 person
#
@@ -212,11 +222,14 @@ module ActionView
#
# pluralize(0, 'person')
# # => 0 people
- def pluralize(count, singular, plural = nil)
+ #
+ # pluralize(2, 'Person', locale: :de)
+ # # => 2 Personen
+ def pluralize(count, singular, plural = nil, locale: nil)
word = if (count == 1 || count =~ /^1(\.0+)?$/)
singular
else
- plural || singular.pluralize
+ plural || singular.pluralize(locale)
end
"#{count || 0} #{word}"
@@ -237,12 +250,15 @@ module ActionView
#
# word_wrap('Once upon a time', line_width: 1)
# # => Once\nupon\na\ntime
- def word_wrap(text, options = {})
- line_width = options.fetch(:line_width, 80)
-
+ #
+ # You can also specify a custom +break_sequence+ ("\n" by default)
+ #
+ # word_wrap('Once upon a time', line_width: 1, break_sequence: "\r\n")
+ # # => Once\r\nupon\r\na\r\ntime
+ def word_wrap(text, line_width: 80, break_sequence: "\n")
text.split("\n").collect! do |line|
- line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1\n").strip : line
- end * "\n"
+ line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1#{break_sequence}").strip : line
+ end * break_sequence
end
# Returns +text+ transformed into HTML using simple formatting rules.
diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb
index 342361217c..dde1ef22ac 100644
--- a/actionview/lib/action_view/helpers/translation_helper.rb
+++ b/actionview/lib/action_view/helpers/translation_helper.rb
@@ -7,48 +7,65 @@ module ActionView
module Helpers
module TranslationHelper
include TagHelper
- # Delegates to <tt>I18n#translate</tt> but also performs three additional functions.
+ # Delegates to <tt>I18n#translate</tt> but also performs three additional
+ # functions.
#
- # First, it will ensure that any thrown +MissingTranslation+ messages will be turned
- # into inline spans that:
+ # First, it will ensure that any thrown +MissingTranslation+ messages will
+ # be rendered as inline spans that:
#
- # * have a "translation-missing" class set,
- # * contain the missing key as a title attribute and
- # * a titleized version of the last key segment as a text.
+ # * Have a <tt>translation-missing</tt> class applied
+ # * Contain the missing key as the value of the +title+ attribute
+ # * Have a titleized version of the last key segment as text
#
- # E.g. the value returned for a missing translation key :"blog.post.title" will be
- # <span class="translation_missing" title="translation missing: en.blog.post.title">Title</span>.
- # This way your views will display rather reasonable strings but it will still
- # be easy to spot missing translations.
+ # For example, the value returned for the missing translation key
+ # <tt>"blog.post.title"</tt> will be:
#
- # Second, it'll scope the key by the current partial if the key starts
- # with a period. So if you call <tt>translate(".foo")</tt> from the
- # <tt>people/index.html.erb</tt> template, you'll actually be calling
- # <tt>I18n.translate("people.index.foo")</tt>. 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.
+ # <span
+ # class="translation_missing"
+ # title="translation missing: en.blog.post.title">Title</span>
#
- # Third, it'll mark the translation as safe HTML if the key has the suffix
- # "_html" or the last element of the key is the word "html". For example,
- # calling translate("footer_html") or translate("footer.html") will return
- # a safe HTML string that won't be escaped by other HTML helper methods. This
- # naming convention helps to identify translations that include HTML tags so that
- # you know what kind of output to expect when you call translate in a template.
+ # This allows for views to display rather reasonable strings while still
+ # giving developers a way to find missing translations.
+ #
+ # If you would prefer missing translations to raise an error, you can
+ # opt out of span-wrapping behavior globally by setting
+ # <tt>ActionView::Base.raise_on_missing_translations = true</tt> or
+ # individually by passing <tt>raise: true</tt> as an option to
+ # <tt>translate</tt>.
+ #
+ # Second, if the key starts with a period <tt>translate</tt> will scope
+ # the key by the current partial. Calling <tt>translate(".foo")</tt> from
+ # the <tt>people/index.html.erb</tt> template is equivalent to calling
+ # <tt>translate("people.index.foo")</tt>. This makes it less
+ # repetitive to translate many keys within the same partial and provides
+ # a convention to scope keys consistently.
+ #
+ # Third, the translation will be marked as <tt>html_safe</tt> if the key
+ # has the suffix "_html" or the last element of the key is "html". Calling
+ # <tt>translate("footer_html")</tt> or <tt>translate("footer.html")</tt>
+ # will return an HTML safe string that won't be escaped by other HTML
+ # helper methods. This naming convention helps to identify translations
+ # that include HTML tags so that you know what kind of output to expect
+ # when you call translate in a template and translators know which keys
+ # they can provide HTML values for.
def translate(key, options = {})
options = options.dup
- remaining_defaults = Array(options.delete(:default))
- options[:default] = remaining_defaults.shift if remaining_defaults.first.kind_of? String
+ has_default = options.has_key?(:default)
+ remaining_defaults = Array(options.delete(:default)).compact
+
+ if has_default && !remaining_defaults.first.kind_of?(Symbol)
+ options[:default] = remaining_defaults
+ end
# If the user has explicitly decided to NOT raise errors, pass that option to I18n.
# Otherwise, tell I18n to raise an exception, which we rescue further in this method.
# Note: `raise_error` refers to us re-raising the error in this method. I18n is forced to raise by default.
- if options[:raise] == false || (options.key?(:rescue_format) && options[:rescue_format].nil?)
+ if options[:raise] == false
raise_error = false
- options[:raise] = false
+ i18n_raise = false
else
- raise_error = options[:raise] || options[:rescue_format] || ActionView::Base.raise_on_missing_translations
- options[:raise] = true
+ raise_error = options[:raise] || ActionView::Base.raise_on_missing_translations
+ i18n_raise = true
end
if html_safe_translation_key?(key)
@@ -58,11 +75,11 @@ module ActionView
html_safe_options[name] = ERB::Util.html_escape(value.to_s)
end
end
- translation = I18n.translate(scope_key_by_partial(key), html_safe_options)
+ translation = I18n.translate(scope_key_by_partial(key), html_safe_options.merge(raise: i18n_raise))
translation.respond_to?(:html_safe) ? translation.html_safe : translation
else
- I18n.translate(scope_key_by_partial(key), options)
+ I18n.translate(scope_key_by_partial(key), options.merge(raise: i18n_raise))
end
rescue I18n::MissingTranslationData => e
if remaining_defaults.present?
@@ -71,7 +88,14 @@ module ActionView
raise e if raise_error
keys = I18n.normalize_keys(e.locale, e.key, e.options[:scope])
- content_tag('span', keys.last.to_s.titleize, :class => 'translation_missing', :title => "translation missing: #{keys.join('.')}")
+ title = "translation missing: #{keys.join('.')}"
+
+ interpolations = options.except(:default)
+ if interpolations.any?
+ title << ", " << interpolations.map { |k, v| "#{k}: #{ERB::Util.html_escape(v)}" }.join(', ')
+ end
+
+ content_tag('span', keys.last.to_s.titleize, class: 'translation_missing', title: title)
end
end
alias :t :translate
diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb
index 8c2d5705f1..5684de35e8 100644
--- a/actionview/lib/action_view/helpers/url_helper.rb
+++ b/actionview/lib/action_view/helpers/url_helper.rb
@@ -46,9 +46,9 @@ module ActionView
end
protected :_back_url
- # Creates a link tag of the given +name+ using a URL created by the set of +options+.
+ # Creates an anchor element of the given +name+ using a URL created by the set of +options+.
# See the valid options in the documentation for +url_for+. It's also possible to
- # pass a String instead of an options hash, which generates a link tag that uses the
+ # pass a String instead of an options hash, which generates an anchor element that uses the
# value of the String as the href for the link. Using a <tt>:back</tt> Symbol instead
# of an options hash will generate a link to the referrer (a JavaScript back link
# will be used in place of a referrer if none exists). If +nil+ is passed as the name
@@ -172,6 +172,11 @@ module ActionView
#
# link_to "Visit Other Site", "http://www.rubyonrails.org/", data: { confirm: "Are you sure?" }
# # => <a href="http://www.rubyonrails.org/" data-confirm="Are you sure?">Visit Other Site</a>
+ #
+ # Also you can set any link attributes such as <tt>target</tt>, <tt>rel</tt>, <tt>type</tt>:
+ #
+ # link_to "External link", "http://www.rubyonrails.org/", target: "_blank", rel: "nofollow"
+ # # => <a href="http://www.rubyonrails.org/" target="_blank" rel="nofollow">External link</a>
def link_to(name = nil, options = nil, html_options = nil, &block)
html_options, options, name = options, name, block if block_given?
options ||= {}
@@ -179,9 +184,9 @@ module ActionView
html_options = convert_options_to_data_attributes(options, html_options)
url = url_for(options)
- html_options['href'] ||= url
+ html_options["href".freeze] ||= url
- content_tag(:a, name || url, html_options, &block)
+ content_tag("a".freeze, name || url, html_options, &block)
end
# Generates a form containing a single button that submits to the URL created
@@ -280,9 +285,7 @@ module ActionView
html_options, options = options, name if block_given?
options ||= {}
html_options ||= {}
-
html_options = html_options.stringify_keys
- convert_boolean_attributes!(html_options, %w(disabled))
url = options.is_a?(String) ? options : url_for(options)
remote = html_options.delete('remote')
@@ -294,8 +297,9 @@ module ActionView
form_method = method == 'get' ? 'get' : 'post'
form_options = html_options.delete('form') || {}
form_options[:class] ||= html_options.delete('form_class') || 'button_to'
- form_options.merge!(method: form_method, action: url)
- form_options.merge!("data-remote" => "true") if remote
+ form_options[:method] = form_method
+ form_options[:action] = url
+ form_options[:'data-remote'] = true if remote
request_token_tag = form_method == 'post' ? token_tag : ''
@@ -459,70 +463,59 @@ module ActionView
html_options = (html_options || {}).stringify_keys
extras = %w{ cc bcc body subject reply_to }.map! { |item|
- option = html_options.delete(item) || next
- "#{item.dasherize}=#{Rack::Utils.escape_path(option)}"
+ option = html_options.delete(item).presence || next
+ "#{item.dasherize}=#{ERB::Util.url_encode(option)}"
}.compact
extras = extras.empty? ? '' : '?' + extras.join('&')
- html_options["href"] = "mailto:#{email_address}#{extras}"
+ encoded_email_address = ERB::Util.url_encode(email_address).gsub("%40", "@")
+ html_options["href"] = "mailto:#{encoded_email_address}#{extras}"
- content_tag(:a, name || email_address, html_options, &block)
+ content_tag("a".freeze, name || email_address, html_options, &block)
end
# True if the current request URI was generated by the given +options+.
#
# ==== Examples
- # Let's say we're in the <tt>http://www.example.com/shop/checkout?order=desc</tt> action.
+ # Let's say we're in the <tt>http://www.example.com/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: 'asc')
- # # => false
- #
# current_page?(action: 'checkout')
# # => true
#
# current_page?(controller: 'library', action: 'checkout')
# # => false
#
- # current_page?('http://www.example.com/shop/checkout')
- # # => true
- #
- # current_page?('/shop/checkout')
+ # current_page?(controller: 'shop', action: 'checkout')
# # => true
#
- # Let's say we're in the <tt>http://www.example.com/shop/checkout?order=desc&page=1</tt> action.
- #
- # current_page?(action: 'process')
+ # current_page?(controller: 'shop', action: 'checkout', order: 'asc')
# # => 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?('http://www.example.com/shop/checkout')
+ # # => true
#
- # current_page?(action: 'checkout')
+ # current_page?('/shop/checkout')
# # => true
#
- # current_page?(controller: 'library', action: 'checkout')
- # # => false
+ # current_page?('http://www.example.com/shop/checkout?order=desc&page=1')
+ # # => true
#
# Let's say we're in the <tt>http://www.example.com/products</tt> action with method POST in case of invalid product.
#
# current_page?(controller: 'product', action: 'index')
# # => false
#
+ # We can also pass in the symbol arguments instead of strings.
+ #
def current_page?(options)
unless request
raise "You cannot use helpers that need to determine the current " \
@@ -576,34 +569,6 @@ module ActionView
html_options["data-method"] = method
end
- # Processes the +html_options+ hash, converting the boolean
- # attributes from true/false form into the form required by
- # HTML/XHTML. (An attribute is considered to be boolean if
- # its name is listed in the given +bool_attrs+ array.)
- #
- # More specifically, for each boolean attribute in +html_options+
- # given as:
- #
- # "attr" => bool_value
- #
- # if the associated +bool_value+ evaluates to true, it is
- # replaced with the attribute's name; otherwise the attribute is
- # removed from the +html_options+ hash. (See the XHTML 1.0 spec,
- # section 4.5 "Attribute Minimization" for more:
- # http://www.w3.org/TR/xhtml1/#h-4.5)
- #
- # Returns the updated +html_options+ hash, which is also modified
- # in place.
- #
- # Example:
- #
- # convert_boolean_attributes!( html_options,
- # %w( checked disabled readonly ) )
- def convert_boolean_attributes!(html_options, bool_attrs)
- bool_attrs.each { |x| html_options[x] = x if html_options.delete(x) }
- html_options
- end
-
def token_tag(token=nil)
if token != false && protect_against_forgery?
token ||= form_authenticity_token
diff --git a/actionview/lib/action_view/layouts.rb b/actionview/lib/action_view/layouts.rb
index 0b5c0b9991..a74a5e05f3 100644
--- a/actionview/lib/action_view/layouts.rb
+++ b/actionview/lib/action_view/layouts.rb
@@ -228,7 +228,7 @@ module ActionView
# set by the <tt>layout</tt> method.
#
# ==== Returns
- # * <tt> Boolean</tt> - True if the action has a layout definition, false otherwise.
+ # * <tt>Boolean</tt> - True if the action has a layout definition, false otherwise.
def _conditional_layout?
return unless super
@@ -277,7 +277,7 @@ module ActionView
remove_possible_method(:_layout)
prefixes = _implied_layout_name =~ /\blayouts/ ? [] : ["layouts"]
- default_behavior = "lookup_context.find_all('#{_implied_layout_name}', #{prefixes.inspect}).first || super"
+ default_behavior = "lookup_context.find_all('#{_implied_layout_name}', #{prefixes.inspect}, false, [], { formats: formats }).first || super"
name_clause = if name
default_behavior
else
@@ -316,7 +316,7 @@ module ActionView
end
self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
- def _layout
+ def _layout(formats)
if _conditional_layout?
#{layout_definition}
else
@@ -372,7 +372,7 @@ module ActionView
end
# This will be overwritten by _write_layout_method
- def _layout; end
+ def _layout(*); end
# Determine the layout for a given name, taking into account the name type.
#
@@ -382,8 +382,8 @@ module ActionView
case name
when String then _normalize_layout(name)
when Proc then name
- when true then Proc.new { _default_layout(true) }
- when :default then Proc.new { _default_layout(false) }
+ when true then Proc.new { |formats| _default_layout(formats, true) }
+ when :default then Proc.new { |formats| _default_layout(formats, false) }
when false, nil then nil
else
raise ArgumentError,
@@ -399,14 +399,15 @@ module ActionView
# Optionally raises an exception if the layout could not be found.
#
# ==== Parameters
+ # * <tt>formats</tt> - The formats accepted to this layout
# * <tt>require_layout</tt> - If set to true and layout is not found,
- # an ArgumentError exception is raised (defaults to false)
+ # an +ArgumentError+ exception is raised (defaults to false)
#
# ==== Returns
# * <tt>template</tt> - The template object for the default layout (or nil)
- def _default_layout(require_layout = false)
+ def _default_layout(formats, require_layout = false)
begin
- value = _layout if action_has_layout?
+ value = _layout(formats) if action_has_layout?
rescue NameError => e
raise e, "Could not render layout: #{e.message}"
end
diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb
index 36855ec3d0..ec6edfaaa3 100644
--- a/actionview/lib/action_view/lookup_context.rb
+++ b/actionview/lib/action_view/lookup_context.rb
@@ -1,4 +1,4 @@
-require 'thread_safe'
+require 'concurrent'
require 'active_support/core_ext/module/remove_method'
require 'active_support/core_ext/module/attribute_accessors'
require 'action_view/template/resolver'
@@ -6,10 +6,11 @@ require 'action_view/template/resolver'
module ActionView
# = Action View Lookup Context
#
- # LookupContext is the object responsible to hold all information required to lookup
- # templates, i.e. view paths and details. The LookupContext is also responsible to
- # generate a key, given to view paths, used in the resolver cache lookup. Since
- # this key is generated just once during the request, it speeds up all cache accesses.
+ # <tt>LookupContext</tt> is the object responsible for holding all information
+ # required for looking up templates, i.e. view paths and details.
+ # <tt>LookupContext</tt> is also responsible for generating a key, given to
+ # view paths, used in the resolver cache lookup. Since this key is generated
+ # only once during the request, it speeds up all cache accesses.
class LookupContext #:nodoc:
attr_accessor :prefixes, :rendered_format
@@ -19,7 +20,7 @@ module ActionView
mattr_accessor :registered_details
self.registered_details = []
- def self.register_detail(name, options = {}, &block)
+ def self.register_detail(name, &block)
self.registered_details << name
initialize = registered_details.map { |n| "@details[:#{n}] = details[:#{n}] || default_#{n}" }
@@ -54,14 +55,14 @@ module ActionView
end
register_detail(:formats) { ActionView::Base.default_formats || [:html, :text, :js, :css, :xml, :json] }
register_detail(:variants) { [] }
- register_detail(:handlers){ Template::Handlers.extensions }
+ register_detail(:handlers) { Template::Handlers.extensions }
class DetailsKey #:nodoc:
alias :eql? :equal?
alias :object_hash :hash
attr_reader :hash
- @details_keys = ThreadSafe::Cache.new
+ @details_keys = Concurrent::Map.new
def self.get(details)
if details[:formats]
@@ -126,7 +127,7 @@ module ActionView
@view_paths.find_all(*args_for_lookup(name, prefixes, partial, keys, options))
end
- def exists?(name, prefixes = [], partial = false, keys = [], options = {})
+ def exists?(name, prefixes = [], partial = false, keys = [], **options)
@view_paths.exists?(*args_for_lookup(name, prefixes, partial, keys, options))
end
alias :template_exists? :exists?
@@ -172,13 +173,13 @@ module ActionView
# name instead of the prefix.
def normalize_name(name, prefixes) #:nodoc:
prefixes = prefixes.presence
- parts = name.to_s.split('/')
+ parts = name.to_s.split('/'.freeze)
parts.shift if parts.first.empty?
name = parts.pop
return name, prefixes || [""] if parts.empty?
- parts = parts.join('/')
+ parts = parts.join('/'.freeze)
prefixes = prefixes ? prefixes.map { |p| "#{p}/#{parts}" } : [parts]
return name, prefixes
@@ -203,7 +204,7 @@ module ActionView
# add :html as fallback to :js.
def formats=(values)
if values
- values.concat(default_formats) if values.delete "*/*"
+ values.concat(default_formats) if values.delete "*/*".freeze
if values == [:js]
values << :html
@html_fallback_for_js = true
@@ -228,21 +229,5 @@ module ActionView
super(default_locale)
end
-
- # Uses the first format in the formats array for layout lookup.
- def with_layout_format
- if formats.size == 1
- yield
- else
- old_formats = formats
- _set_detail(:formats, formats[0,1])
-
- begin
- yield
- ensure
- _set_detail(:formats, old_formats)
- end
- end
- end
end
end
diff --git a/actionview/lib/action_view/model_naming.rb b/actionview/lib/action_view/model_naming.rb
index d42e436b17..b6ed13424e 100644
--- a/actionview/lib/action_view/model_naming.rb
+++ b/actionview/lib/action_view/model_naming.rb
@@ -1,5 +1,5 @@
module ActionView
- module ModelNaming
+ module ModelNaming #:nodoc:
# Converts the given object to an ActiveModel compliant one.
def convert_to_model(object)
object.respond_to?(:to_model) ? object.to_model : object
diff --git a/actionview/lib/action_view/path_set.rb b/actionview/lib/action_view/path_set.rb
index 91ee2ea8f5..7a88f6bc50 100644
--- a/actionview/lib/action_view/path_set.rb
+++ b/actionview/lib/action_view/path_set.rb
@@ -61,6 +61,15 @@ module ActionView #:nodoc:
find_all(path, prefixes, *args).any?
end
+ def find_all_with_query(query) # :nodoc:
+ paths.each do |resolver|
+ templates = resolver.find_all_with_query(query)
+ return templates unless templates.empty?
+ end
+
+ []
+ end
+
private
def typecast(paths)
diff --git a/actionview/lib/action_view/railtie.rb b/actionview/lib/action_view/railtie.rb
index 81f9c40b85..e829d86c99 100644
--- a/actionview/lib/action_view/railtie.rb
+++ b/actionview/lib/action_view/railtie.rb
@@ -36,14 +36,30 @@ module ActionView
end
end
+ initializer "action_view.collection_caching" do |app|
+ ActiveSupport.on_load(:action_controller) do
+ PartialRenderer.collection_cache = app.config.action_controller.cache_store
+ end
+ end
+
+ initializer "action_view.per_request_digest_cache" do |app|
+ ActiveSupport.on_load(:action_view) do
+ if app.config.consider_all_requests_local
+ app.middleware.use ActionView::Digestor::PerRequestDigestCacheExpiry
+ end
+ end
+ end
+
initializer "action_view.setup_action_pack" do |app|
ActiveSupport.on_load(:action_controller) do
- ActionView::RoutingUrlFor.send(:include, ActionDispatch::Routing::UrlFor)
+ ActionView::RoutingUrlFor.include(ActionDispatch::Routing::UrlFor)
end
end
- rake_tasks do
- load "action_view/tasks/dependencies.rake"
+ rake_tasks do |app|
+ unless app.config.api_only
+ load "action_view/tasks/dependencies.rake"
+ end
end
end
end
diff --git a/actionview/lib/action_view/record_identifier.rb b/actionview/lib/action_view/record_identifier.rb
index c8484bed34..4b44eb5520 100644
--- a/actionview/lib/action_view/record_identifier.rb
+++ b/actionview/lib/action_view/record_identifier.rb
@@ -11,7 +11,7 @@ module ActionView
# <%= post.body %>
# <% end %>
#
- # When +post+ is a new, unsaved ActiveRecord::Base intance, the resulting HTML
+ # When +post+ is a new, unsaved ActiveRecord::Base instance, the resulting HTML
# is:
#
# <div id="new_post" class="post">
@@ -103,7 +103,7 @@ module ActionView
# make sure yourself that your dom ids are valid, in case you overwrite this method.
def record_key_for_dom_id(record)
key = convert_to_model(record).to_key
- key ? key.join('_') : key
+ key ? key.join(JOIN) : key
end
end
end
diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb
index 6c3015180a..39c8658ffe 100644
--- a/actionview/lib/action_view/renderer/partial_renderer.rb
+++ b/actionview/lib/action_view/renderer/partial_renderer.rb
@@ -1,4 +1,5 @@
-require 'thread_safe'
+require 'action_view/renderer/partial_renderer/collection_caching'
+require 'concurrent'
module ActionView
class PartialIteration
@@ -153,23 +154,23 @@ module ActionView
# specified globally for the entire action, but they work in a similar fashion. Imagine a list with two types
# of users:
#
- # <%# app/views/users/index.html.erb &>
+ # <%# app/views/users/index.html.erb %>
# Here's the administrator:
# <%= render partial: "user", layout: "administrator", locals: { user: administrator } %>
#
# Here's the editor:
# <%= render partial: "user", layout: "editor", locals: { user: editor } %>
#
- # <%# app/views/users/_user.html.erb &>
+ # <%# app/views/users/_user.html.erb %>
# Name: <%= user.name %>
#
- # <%# app/views/users/_administrator.html.erb &>
+ # <%# app/views/users/_administrator.html.erb %>
# <div id="administrator">
# Budget: $<%= user.budget %>
# <%= yield %>
# </div>
#
- # <%# app/views/users/_editor.html.erb &>
+ # <%# app/views/users/_editor.html.erb %>
# <div id="editor">
# Deadline: <%= user.deadline %>
# <%= yield %>
@@ -232,7 +233,7 @@ module ActionView
#
# You can also apply a layout to a block within any template:
#
- # <%# app/views/users/_chief.html.erb &>
+ # <%# app/views/users/_chief.html.erb %>
# <%= render(layout: "administrator", locals: { user: chief }) do %>
# Title: <%= chief.title %>
# <% end %>
@@ -249,13 +250,13 @@ module ActionView
# If you pass arguments to "yield" then this will be passed to the block. One way to use this is to pass
# an array to layout and treat it as an enumerable.
#
- # <%# app/views/users/_user.html.erb &>
+ # <%# app/views/users/_user.html.erb %>
# <div class="user">
# Budget: $<%= user.budget %>
# <%= yield user %>
# </div>
#
- # <%# app/views/users/index.html.erb &>
+ # <%# app/views/users/index.html.erb %>
# <%= render layout: @users do |user| %>
# Title: <%= user.title %>
# <% end %>
@@ -264,14 +265,14 @@ module ActionView
#
# You can also yield multiple times in one layout and use block arguments to differentiate the sections.
#
- # <%# app/views/users/_user.html.erb &>
+ # <%# app/views/users/_user.html.erb %>
# <div class="user">
# <%= yield user, :header %>
# Budget: $<%= user.budget %>
# <%= yield user, :footer %>
# </div>
#
- # <%# app/views/users/index.html.erb &>
+ # <%# app/views/users/index.html.erb %>
# <%= render layout: @users do |user, section| %>
# <%- case section when :header -%>
# Title: <%= user.title %>
@@ -280,8 +281,10 @@ module ActionView
# <%- end -%>
# <% end %>
class PartialRenderer < AbstractRenderer
- PREFIXED_PARTIAL_NAMES = ThreadSafe::Cache.new do |h, k|
- h[k] = ThreadSafe::Cache.new
+ include CollectionCaching
+
+ PREFIXED_PARTIAL_NAMES = Concurrent::Map.new do |h, k|
+ h[k] = Concurrent::Map.new
end
def initialize(*)
@@ -321,8 +324,9 @@ module ActionView
spacer = find_template(@options[:spacer_template], @locals.keys).render(@view, @locals)
end
- result = @template ? collection_with_template : collection_without_template
- result.join(spacer).html_safe
+ cache_collection_render do
+ @template ? collection_with_template : collection_without_template
+ end.join(spacer).html_safe
end
def render_partial
@@ -334,7 +338,7 @@ module ActionView
end
object ||= locals[as]
- locals[as] = object
+ locals[as] = object if @has_object
content = @template.render(view, locals) do |*name|
view._layout_for(*name, &block)
@@ -344,8 +348,6 @@ module ActionView
content
end
- private
-
# Sets up instance variables needed for rendering a partial. This method
# finds the options and details and extracts them. The method also contains
# logic that handles the type of object passed in as the partial.
@@ -518,8 +520,8 @@ module ActionView
def retrieve_variable(path, as)
variable = as || begin
- base = path[-1] == "/" ? "" : File.basename(path)
- raise_invalid_identifier(path) unless base =~ /\A_?([a-z]\w*)(\.\w+)*\z/
+ base = path[-1] == "/".freeze ? "".freeze : File.basename(path)
+ raise_invalid_identifier(path) unless base =~ /\A_?(.*)(?:\.\w+)*\z/
$1.to_sym
end
if @collection
@@ -530,8 +532,7 @@ module ActionView
end
IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " +
- "make sure your partial name starts with underscore, " +
- "and is followed by any combination of letters, numbers and underscores."
+ "make sure your partial name starts with underscore."
OPTION_AS_ERROR_MESSAGE = "The value (%s) of the option `as` is not a valid Ruby identifier; " +
"make sure it starts with lowercase letter, " +
diff --git a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb
new file mode 100644
index 0000000000..1147963882
--- /dev/null
+++ b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb
@@ -0,0 +1,70 @@
+require 'active_support/core_ext/object/try'
+
+module ActionView
+ module CollectionCaching # :nodoc:
+ extend ActiveSupport::Concern
+
+ included do
+ # Fallback cache store if Action View is used without Rails.
+ # Otherwise overridden in Railtie to use Rails.cache.
+ mattr_accessor(:collection_cache) { ActiveSupport::Cache::MemoryStore.new }
+ end
+
+ private
+ def cache_collection_render
+ return yield unless cache_collection?
+
+ keyed_collection = collection_by_cache_keys
+ partial_cache = collection_cache.read_multi(*keyed_collection.keys)
+
+ @collection = keyed_collection.reject { |key, _| partial_cache.key?(key) }.values
+ rendered_partials = @collection.any? ? yield.dup : []
+
+ fetch_or_cache_partial(partial_cache, order_by: keyed_collection.each_key) do
+ rendered_partials.shift
+ 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
+ end
+ end
+
+ def expanded_cache_key(key)
+ key = @view.fragment_cache_key(@view.cache_fragment_name(key, virtual_path: @template.virtual_path))
+ key.frozen? ? key.dup : key # #read_multi & #write may require mutability, Dalli 2.6.0.
+ 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
+ yield.tap do |rendered_partial|
+ collection_cache.write(key, rendered_partial, cache_options)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/renderer/renderer.rb b/actionview/lib/action_view/renderer/renderer.rb
index 964b18337e..1bee35d80d 100644
--- a/actionview/lib/action_view/renderer/renderer.rb
+++ b/actionview/lib/action_view/renderer/renderer.rb
@@ -37,7 +37,7 @@ module ActionView
end
end
- # Direct accessor to template rendering.
+ # Direct access to template rendering.
def render_template(context, options) #:nodoc:
TemplateRenderer.new(@lookup_context).render(context, options)
end
diff --git a/actionview/lib/action_view/renderer/streaming_template_renderer.rb b/actionview/lib/action_view/renderer/streaming_template_renderer.rb
index 3ab2cd36fc..f38e2764d0 100644
--- a/actionview/lib/action_view/renderer/streaming_template_renderer.rb
+++ b/actionview/lib/action_view/renderer/streaming_template_renderer.rb
@@ -47,7 +47,7 @@ module ActionView
return [super] unless layout_name && template.supports_streaming?
locals ||= {}
- layout = layout_name && find_layout(layout_name, locals.keys)
+ layout = layout_name && find_layout(layout_name, locals.keys, [formats.first])
Body.new do |buffer|
delayed_render(buffer, template, layout, @view, locals)
diff --git a/actionview/lib/action_view/renderer/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb
index cd21d7ab47..75217e1630 100644
--- a/actionview/lib/action_view/renderer/template_renderer.rb
+++ b/actionview/lib/action_view/renderer/template_renderer.rb
@@ -40,7 +40,7 @@ module ActionView
find_template(options[:template], options[:prefixes], false, keys, @details)
end
else
- raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file, :plain, :text or :body option."
+ raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file, :plain, :html, :text or :body option."
end
end
@@ -57,7 +57,7 @@ module ActionView
end
def render_with_layout(path, locals) #:nodoc:
- layout = path && find_layout(path, locals.keys)
+ layout = path && find_layout(path, locals.keys, [formats.first])
content = yield(layout)
if layout
@@ -72,27 +72,28 @@ module ActionView
# This is the method which actually finds the layout using details in the lookup
# context object. If no layout is found, it checks if at least a layout with
# the given name exists across all details before raising the error.
- def find_layout(layout, keys)
- with_layout_format { resolve_layout(layout, keys) }
+ def find_layout(layout, keys, formats)
+ resolve_layout(layout, keys, formats)
end
- def resolve_layout(layout, keys)
+ def resolve_layout(layout, keys, formats)
+ details = @details.dup
+ details[:formats] = formats
+
case layout
when String
begin
if layout =~ /^\//
- with_fallbacks { find_template(layout, nil, false, keys, @details) }
+ with_fallbacks { find_template(layout, nil, false, keys, details) }
else
- find_template(layout, nil, false, keys, @details)
+ find_template(layout, nil, false, keys, details)
end
rescue ActionView::MissingTemplate
all_details = @details.merge(:formats => @lookup_context.default_formats)
raise unless template_exists?(layout, nil, false, keys, all_details)
end
when Proc
- resolve_layout(layout.call, keys)
- when FalseClass
- nil
+ resolve_layout(layout.call(formats), keys, formats)
else
layout
end
diff --git a/actionview/lib/action_view/rendering.rb b/actionview/lib/action_view/rendering.rb
index abd3b77c67..8604637da2 100644
--- a/actionview/lib/action_view/rendering.rb
+++ b/actionview/lib/action_view/rendering.rb
@@ -59,7 +59,7 @@ module ActionView
@_view_context_class ||= self.class.view_context_class
end
- # An instance of a view class. The default view class is ActionView::Base
+ # An instance of a view class. The default view class is ActionView::Base.
#
# The view class must have the following methods:
# View.new[lookup_context, assigns, controller]
@@ -92,16 +92,19 @@ module ActionView
# Find and render a template based on the options given.
# :api: private
def _render_template(options) #:nodoc:
- variant = options[:variant]
+ variant = options.delete(:variant)
+ assigns = options.delete(:assigns)
+ context = view_context
+ context.assign assigns if assigns
lookup_context.rendered_format = nil if options[:formats]
lookup_context.variants = variant if variant
- view_renderer.render(view_context, options)
+ view_renderer.render(context, options)
end
- # Assign the rendered format to lookup context.
- def _process_format(format, options = {}) #:nodoc:
+ # Assign the rendered format to look up context.
+ def _process_format(format) #:nodoc:
super
lookup_context.formats = [format.to_sym]
lookup_context.rendered_format = lookup_context.formats.first
diff --git a/actionview/lib/action_view/routing_url_for.rb b/actionview/lib/action_view/routing_url_for.rb
index f281333a41..45e78d1ad9 100644
--- a/actionview/lib/action_view/routing_url_for.rb
+++ b/actionview/lib/action_view/routing_url_for.rb
@@ -32,7 +32,7 @@ module ActionView
#
# ==== Examples
# <%= url_for(action: 'index') %>
- # # => /blog/
+ # # => /blogs/
#
# <%= url_for(action: 'find', controller: 'books') %>
# # => /books/find
@@ -84,21 +84,24 @@ module ActionView
when Hash
options = options.symbolize_keys
unless options.key?(:only_path)
- if options[:host].nil?
- options[:only_path] = _generate_paths_by_default
- else
- options[:only_path] = false
- end
+ options[:only_path] = only_path?(options[:host])
+ end
+
+ super(options)
+ when ActionController::Parameters
+ unless options.key?(:only_path)
+ options[:only_path] = only_path?(options[:host])
end
super(options)
when :back
_back_url
when Array
+ components = options.dup
if _generate_paths_by_default
- polymorphic_path(options, options.extract_options!)
+ polymorphic_path(components, components.extract_options!)
else
- polymorphic_url(options, options.extract_options!)
+ polymorphic_url(components, components.extract_options!)
end
else
method = _generate_paths_by_default ? :path : :url
@@ -130,5 +133,15 @@ module ActionView
controller.optimize_routes_generation? : super
end
protected :optimize_routes_generation?
+
+ private
+
+ def _generate_paths_by_default
+ true
+ end
+
+ def only_path?(host)
+ _generate_paths_by_default unless host
+ end
end
end
diff --git a/actionview/lib/action_view/tasks/dependencies.rake b/actionview/lib/action_view/tasks/dependencies.rake
index b39f7d583b..f394c319c1 100644
--- a/actionview/lib/action_view/tasks/dependencies.rake
+++ b/actionview/lib/action_view/tasks/dependencies.rake
@@ -2,20 +2,22 @@ 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: template_name, finder: finder).nested_dependencies
+ puts JSON.pretty_generate ActionView::Digestor.new(name: CacheDigests.template_name, finder: CacheDigests.finder).nested_dependencies
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: template_name, finder: finder).dependencies
+ puts JSON.pretty_generate ActionView::Digestor.new(name: CacheDigests.template_name, finder: CacheDigests.finder).dependencies
end
- def template_name
- ENV['TEMPLATE'].split('.', 2).first
- end
+ class CacheDigests
+ def self.template_name
+ ENV['TEMPLATE'].split('.', 2).first
+ end
- def finder
- ApplicationController.new.lookup_context
+ def self.finder
+ ApplicationController.new.lookup_context
+ end
end
end
diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb
index 6b61378a1f..0ed208f27e 100644
--- a/actionview/lib/action_view/template.rb
+++ b/actionview/lib/action_view/template.rb
@@ -87,6 +87,19 @@ module ActionView
# expected_encoding
# )
+ ##
+ # :method: local_assigns
+ #
+ # Returns a hash with the defined local variables.
+ #
+ # Given this sub template rendering:
+ #
+ # <%= render "shared/header", { headline: "Welcome", person: person } %>
+ #
+ # You can use +local_assigns+ in the sub templates to access the local variables:
+ #
+ # local_assigns[:headline] # => "Welcome"
+
eager_autoload do
autoload :Error
autoload :Handlers
@@ -103,7 +116,7 @@ module ActionView
# This finalizer is needed (and exactly with a proc inside another proc)
# otherwise templates leak in development.
- Finalizer = proc do |method_name, mod|
+ Finalizer = proc do |method_name, mod| # :nodoc:
proc do
mod.module_eval do
remove_possible_method method_name
@@ -117,6 +130,7 @@ module ActionView
@source = source
@identifier = identifier
@handler = handler
+ @cache_name = extract_resource_cache_name
@compiled = false
@original_encoding = nil
@locals = details[:locals] || []
@@ -127,7 +141,7 @@ module ActionView
@compile_mutex = Mutex.new
end
- # Returns if the underlying handler supports streaming. If so,
+ # Returns whether the underlying handler supports streaming. If so,
# a streaming buffer *may* be passed when it start rendering.
def supports_streaming?
handler.respond_to?(:supports_streaming?) && handler.supports_streaming?
@@ -140,7 +154,7 @@ module ActionView
# we use a bang in this instrumentation because you don't want to
# consume this in production. This is only slow if it's being listened to.
def render(view, locals, buffer=nil, &block)
- instrument("!render_template") do
+ instrument("!render_template".freeze) do
compile!(view)
view.send(method_name, locals, buffer, &block)
end
@@ -152,6 +166,10 @@ 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
@@ -172,7 +190,7 @@ module ActionView
end
def inspect
- @inspect ||= defined?(Rails.root) ? identifier.sub("#{Rails.root}/", '') : identifier
+ @inspect ||= defined?(Rails.root) ? identifier.sub("#{Rails.root}/", ''.freeze) : identifier
end
# This method is responsible for properly setting the encoding of the
@@ -319,18 +337,41 @@ module ActionView
def method_name #:nodoc:
@method_name ||= begin
m = "_#{identifier_method_name}__#{@identifier.hash}_#{__id__}"
- m.tr!('-', '_')
+ m.tr!('-'.freeze, '_'.freeze)
m
end
end
def identifier_method_name #:nodoc:
- inspect.tr('^a-z_', '_')
+ inspect.tr('^a-z_'.freeze, '_'.freeze)
end
def instrument(action, &block)
payload = { virtual_path: @virtual_path, identifier: @identifier }
- ActiveSupport::Notifications.instrument("#{action}.action_view", payload, &block)
+ case action
+ when "!render_template".freeze
+ ActiveSupport::Notifications.instrument("!render_template.action_view".freeze, payload, &block)
+ else
+ 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/handlers/erb.rb b/actionview/lib/action_view/template/handlers/erb.rb
index 85a100ed4c..1f8459c24b 100644
--- a/actionview/lib/action_view/template/handlers/erb.rb
+++ b/actionview/lib/action_view/template/handlers/erb.rb
@@ -123,6 +123,31 @@ 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/handlers/raw.rb b/actionview/lib/action_view/template/handlers/raw.rb
index 397c86014a..b08fb0870f 100644
--- a/actionview/lib/action_view/template/handlers/raw.rb
+++ b/actionview/lib/action_view/template/handlers/raw.rb
@@ -2,7 +2,7 @@ module ActionView
module Template::Handlers
class Raw
def call(template)
- escaped = template.source.gsub(/:/, '\:')
+ escaped = template.source.gsub(':'.freeze, '\:'.freeze)
'%q:' + escaped + ':;'
end
diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb
index bc0db330ea..7859c58b43 100644
--- a/actionview/lib/action_view/template/resolver.rb
+++ b/actionview/lib/action_view/template/resolver.rb
@@ -3,7 +3,7 @@ require "active_support/core_ext/class"
require "active_support/core_ext/module/attribute_accessors"
require "action_view/template"
require "thread"
-require "thread_safe"
+require "concurrent"
module ActionView
# = Action View Resolver
@@ -35,7 +35,7 @@ module ActionView
# Threadsafe template cache
class Cache #:nodoc:
- class SmallCache < ThreadSafe::Cache
+ class SmallCache < Concurrent::Map
def initialize(options = {})
super(options.merge(:initial_capacity => 2))
end
@@ -52,6 +52,7 @@ module ActionView
def initialize
@data = SmallCache.new(&KEY_BLOCK)
+ @query_cache = SmallCache.new
end
# Cache the templates returned by the block
@@ -70,8 +71,17 @@ module ActionView
end
end
+ def cache_query(query) # :nodoc:
+ if Resolver.caching?
+ @query_cache[query] ||= canonical_no_templates(yield)
+ else
+ yield
+ end
+ end
+
def clear
@data.clear
+ @query_cache.clear
end
private
@@ -116,6 +126,10 @@ module ActionView
end
end
+ def find_all_with_query(query) # :nodoc:
+ @cache.cache_query(query) { find_template_paths(File.join(@path, query)) }
+ end
+
private
delegate :caching?, to: :class
@@ -181,9 +195,9 @@ module ActionView
def query(path, details, formats)
query = build_query(path, details)
- template_paths = find_template_paths query
+ template_paths = find_template_paths(query)
- template_paths.map { |template|
+ template_paths.map do |template|
handler, format, variant = extract_handler_and_format_and_variant(template, formats)
contents = File.binread(template)
@@ -193,36 +207,36 @@ module ActionView
:variant => variant,
:updated_at => mtime(template)
)
- }
+ end
end
def find_template_paths(query)
- Dir[query].reject { |filename|
+ Dir[query].reject do |filename|
File.directory?(filename) ||
# deals with case-insensitive file systems.
!File.fnmatch(query, filename, File::FNM_EXTGLOB)
- }
+ end
end
# Helper for building query glob string based on resolver's pattern.
def build_query(path, details)
query = @pattern.dup
- prefix = path.prefix.empty? ? "" : "#{escape_entry(path.prefix)}\\1"
- query.gsub!(/\:prefix(\/)?/, prefix)
+ prefix = path.prefix.empty? ? '' : "#{escape_entry(path.prefix)}\\1"
+ query.gsub!(/:prefix(\/)?/, prefix)
partial = escape_entry(path.partial? ? "_#{path.name}" : path.name)
- query.gsub!(/\:action/, partial)
+ query.gsub!(/:action/, partial)
details.each do |ext, variants|
- query.gsub!(/\:#{ext}/, "{#{variants.compact.uniq.join(',')}}")
+ query.gsub!(/:#{ext}/, "{#{variants.compact.uniq.join(',')}}")
end
File.expand_path(query, @path)
end
def escape_entry(entry)
- entry.gsub(/[*?{}\[\]]/, '\\\\\\&')
+ entry.gsub(/[*?{}\[\]]/, '\\\\\\&'.freeze)
end
# Returns the file mtime from the filesystem.
@@ -234,7 +248,7 @@ module ActionView
# from the path, or the handler, we should return the array of formats given
# to the resolver.
def extract_handler_and_format_and_variant(path, default_formats)
- pieces = File.basename(path).split(".")
+ pieces = File.basename(path).split('.'.freeze)
pieces.shift
extension = pieces.pop
@@ -270,7 +284,7 @@ module ActionView
#
# ActionController::Base.view_paths = FileSystemResolver.new(
# Rails.root.join("app/views"),
- # ":prefix{/:locale}/:action{.:formats,}{+:variants,}{.:handlers,}"
+ # ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}",
# )
#
# ==== Pattern format and variables
diff --git a/actionview/lib/action_view/test_case.rb b/actionview/lib/action_view/test_case.rb
index 812b011bd7..f6b5696a13 100644
--- a/actionview/lib/action_view/test_case.rb
+++ b/actionview/lib/action_view/test_case.rb
@@ -24,8 +24,8 @@ module ActionView
def initialize
super
self.class.controller_path = ""
- @request = ActionController::TestRequest.new
- @response = ActionController::TestResponse.new
+ @request = ActionController::TestRequest.create
+ @response = ActionDispatch::TestResponse.new
@request.env.delete('PATH_INFO')
@params = {}
@@ -204,7 +204,7 @@ module ActionView
def view
@view ||= begin
view = @controller.view_context
- view.singleton_class.send :include, _helpers
+ view.singleton_class.include(_helpers)
view.extend(Locals)
view.rendered_views = self.rendered_views
view.output_buffer = self.output_buffer
@@ -263,9 +263,15 @@ module ActionView
end
def method_missing(selector, *args)
- if @controller.respond_to?(:_routes) &&
- ( @controller._routes.named_routes.route_defined?(selector) ||
- @controller._routes.mounted_helpers.method_defined?(selector) )
+ begin
+ routes = @controller.respond_to?(:_routes) && @controller._routes
+ rescue
+ # Dont call routes, if there is an error on _routes call
+ end
+
+ if routes &&
+ ( routes.named_routes.route_defined?(selector) ||
+ routes.mounted_helpers.method_defined?(selector) )
@controller.__send__(selector, *args)
else
super
diff --git a/actionview/lib/action_view/view_paths.rb b/actionview/lib/action_view/view_paths.rb
index 492f67f45d..37722013ce 100644
--- a/actionview/lib/action_view/view_paths.rb
+++ b/actionview/lib/action_view/view_paths.rb
@@ -36,9 +36,9 @@ module ActionView
self.class._prefixes
end
- # LookupContext is the object responsible to hold all information required to lookup
- # templates, i.e. view paths and details. Check ActionView::LookupContext for more
- # information.
+ # <tt>LookupContext</tt> is the object responsible for holding all
+ # information required for looking up templates, i.e. view paths and
+ # details. Check <tt>ActionView::LookupContext</tt> for more information.
def lookup_context
@_lookup_context ||=
ActionView::LookupContext.new(self.class._view_paths, details_for_lookup, _prefixes)