diff options
Diffstat (limited to 'actionpack/lib')
57 files changed, 821 insertions, 336 deletions
diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb index fffe3edac2..44c9ea34ba 100644 --- a/actionpack/lib/abstract_controller/callbacks.rb +++ b/actionpack/lib/abstract_controller/callbacks.rb @@ -8,7 +8,7 @@ module AbstractController include ActiveSupport::Callbacks included do - define_callbacks :process_action, :terminator => "response_body" + define_callbacks :process_action, :terminator => "response_body", :skip_after_callbacks_if_terminated => true end # Override AbstractController::Base's process_action to run the @@ -21,11 +21,9 @@ module AbstractController module ClassMethods # If :only or :except are used, convert the options into the - # primitive form (:per_key) used by ActiveSupport::Callbacks. + # :unless and :if options of ActiveSupport::Callbacks. # The basic idea is that :only => :index gets converted to - # :if => proc {|c| c.action_name == "index" }, but that the - # proc is only evaluated once per action for the lifetime of - # a Rails process. + # :if => proc {|c| c.action_name == "index" }. # # ==== Options # * <tt>only</tt> - The callback should be run only for this action @@ -33,11 +31,11 @@ module AbstractController def _normalize_callback_options(options) if only = options[:only] only = Array(only).map {|o| "action_name == '#{o}'"}.join(" || ") - options[:per_key] = {:if => only} + options[:if] = Array(options[:if]) << only end if except = options[:except] except = Array(except).map {|e| "action_name == '#{e}'"}.join(" || ") - options[:per_key] = {:unless => except} + options[:unless] = Array(options[:unless]) << except end end @@ -167,7 +165,6 @@ module AbstractController # for details on the allowed parameters. def #{filter}_filter(*names, &blk) # def before_filter(*names, &blk) _insert_callbacks(names, blk) do |name, options| # _insert_callbacks(names, blk) do |name, options| - options[:if] = (Array(options[:if]) << "!halted") if #{filter == :after} # options[:if] = (Array(options[:if]) << "!halted") if false set_callback(:process_action, :#{filter}, name, options) # set_callback(:process_action, :before, name, options) end # end end # end @@ -176,7 +173,6 @@ module AbstractController # for details on the allowed parameters. def prepend_#{filter}_filter(*names, &blk) # def prepend_before_filter(*names, &blk) _insert_callbacks(names, blk) do |name, options| # _insert_callbacks(names, blk) do |name, options| - options[:if] = (Array(options[:if]) << "!halted") if #{filter == :after} # options[:if] = (Array(options[:if]) << "!halted") if false set_callback(:process_action, :#{filter}, name, options.merge(:prepend => true)) # set_callback(:process_action, :before, name, options.merge(:prepend => true)) end # end end # end diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index f4eaa2fd1b..a0c54dbd84 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -40,7 +40,6 @@ module ActionController autoload :Integration, 'action_controller/deprecated/integration_test' autoload :IntegrationTest, 'action_controller/deprecated/integration_test' autoload :PerformanceTest, 'action_controller/deprecated/performance_test' - autoload :UrlWriter, 'action_controller/deprecated' autoload :Routing, 'action_controller/deprecated' autoload :TestCase, 'action_controller/test_case' autoload :TemplateAssertions, 'action_controller/test_case' diff --git a/actionpack/lib/action_controller/caching/actions.rb b/actionpack/lib/action_controller/caching/actions.rb index e76a79f710..bd3b0b5df3 100644 --- a/actionpack/lib/action_controller/caching/actions.rb +++ b/actionpack/lib/action_controller/caching/actions.rb @@ -47,7 +47,8 @@ module ActionController #:nodoc: # And you can also use <tt>:if</tt> (or <tt>:unless</tt>) to pass a # proc that specifies when the action should be cached. # - # Finally, if you are using memcached, you can also pass <tt>:expires_in</tt>. + # As of Rails 3.0, you can also pass <tt>:expires_in</tt> with a time + # interval (in seconds) to schedule expiration of the cached item. # # The following example depicts some of the points made above: # diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index 3aab77a069..92433ab462 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -181,9 +181,13 @@ module ActionController @_status = Rack::Utils.status_code(status) end - def response_body=(val) - body = (val.nil? || val.respond_to?(:each)) ? val : [val] - super body + def response_body=(body) + body = [body] unless body.nil? || body.respond_to?(:each) + super + end + + def performed? + !!response_body end def dispatch(name, request) #:nodoc: diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb index b45f211e83..69e37d8713 100644 --- a/actionpack/lib/action_controller/metal/force_ssl.rb +++ b/actionpack/lib/action_controller/metal/force_ssl.rb @@ -29,6 +29,7 @@ module ActionController if !request.ssl? && !Rails.env.development? redirect_options = {:protocol => 'https://', :status => :moved_permanently} redirect_options.merge!(:host => host) if host + redirect_options.merge!(:params => request.query_parameters) flash.keep redirect_to redirect_options end diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index 4972c6bede..3d46163b74 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -67,7 +67,7 @@ module ActionController # class PostsController < ApplicationController # REALM = "SuperSecret" # USERS = {"dhh" => "secret", #plain text password - # "dap" => Digest::MD5.hexdigest(["dap",REALM,"secret"].join(":")) #ha1 digest password + # "dap" => Digest::MD5.hexdigest(["dap",REALM,"secret"].join(":"))} #ha1 digest password # # before_filter :authenticate, :except => [:index] # diff --git a/actionpack/lib/action_controller/metal/implicit_render.rb b/actionpack/lib/action_controller/metal/implicit_render.rb index e8e465d3ba..ae04b53825 100644 --- a/actionpack/lib/action_controller/metal/implicit_render.rb +++ b/actionpack/lib/action_controller/metal/implicit_render.rb @@ -2,7 +2,7 @@ module ActionController module ImplicitRender def send_action(method, *args) ret = super - default_render unless response_body + default_render unless performed? ret end diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index ca383be76b..80ecc16d53 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -191,8 +191,9 @@ module ActionController #:nodoc: def respond_to(*mimes, &block) raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given? - if response = retrieve_response_from_mimes(mimes, &block) - response.call(nil) + if collector = retrieve_collector_from_mimes(mimes, &block) + response = collector.response + response ? response.call : default_render({}) end end @@ -232,10 +233,18 @@ module ActionController #:nodoc: raise "In order to use respond_with, first you need to declare the formats your " << "controller responds to in the class level" if self.class.mimes_for_respond_to.empty? - if response = retrieve_response_from_mimes(&block) + if collector = retrieve_collector_from_mimes(&block) options = resources.size == 1 ? {} : resources.extract_options! - options.merge!(:default_response => response) - (options.delete(:responder) || self.class.responder).call(self, resources, options) + + if defined_response = collector.response + if action = options.delete(:action) + render :action => action + else + defined_response.call + end + else + (options.delete(:responder) || self.class.responder).call(self, resources, options) + end end end @@ -263,15 +272,16 @@ module ActionController #:nodoc: # Collects mimes and return the response for the negotiated format. Returns # nil if :not_acceptable was sent to the client. # - def retrieve_response_from_mimes(mimes=nil, &block) #:nodoc: + def retrieve_collector_from_mimes(mimes=nil, &block) #:nodoc: mimes ||= collect_mimes_from_class_level - collector = Collector.new(mimes) { |options| default_render(options || {}) } + collector = Collector.new(mimes) block.call(collector) if block_given? + format = collector.negotiate_format(request) - if format = request.negotiate_mime(collector.order) + if format self.content_type ||= format.to_s lookup_context.freeze_formats([format.to_sym]) - collector.response_for(format) + collector else head :not_acceptable nil @@ -280,10 +290,10 @@ module ActionController #:nodoc: class Collector #:nodoc: include AbstractController::Collector - attr_accessor :order + attr_accessor :order, :format - def initialize(mimes, &block) - @order, @responses, @default_response = [], {}, block + def initialize(mimes) + @order, @responses = [], {} mimes.each { |mime| send(mime) } end @@ -302,8 +312,12 @@ module ActionController #:nodoc: @responses[mime_type] ||= block end - def response_for(mime) - @responses[mime] || @responses[Mime::ALL] || @default_response + def response + @responses[format] || @responses[Mime::ALL] + end + + def negotiate_format(request) + @format = request.negotiate_mime(order) end end end diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb index 9500a349cb..4ad64bff20 100644 --- a/actionpack/lib/action_controller/metal/responder.rb +++ b/actionpack/lib/action_controller/metal/responder.rb @@ -129,7 +129,6 @@ module ActionController #:nodoc: @resources = resources @options = options @action = options.delete(:action) - @default_response = options.delete(:default_response) end delegate :head, :render, :redirect_to, :to => :controller @@ -226,7 +225,7 @@ module ActionController #:nodoc: # controller. # def default_render - @default_response.call(options) + controller.default_render(options) end # Display is just a shortcut to render a resource with the current format. diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index fce6e29d5f..1e226fc336 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -351,7 +351,7 @@ module ActionController def tests(controller_class) case controller_class when String, Symbol - self.controller_class = "#{controller_class.to_s.underscore}_controller".camelize.constantize + self.controller_class = "#{controller_class.to_s.camelize}Controller".constantize when Class self.controller_class = controller_class else diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index 25affb9f50..2152351703 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -82,6 +82,7 @@ module Mime class << self TRAILING_STAR_REGEXP = /(text|application)\/\*/ + Q_SEPARATOR_REGEXP = /;\s*q=/ def lookup(string) LOOKUP[string] @@ -108,6 +109,7 @@ module Mime def parse(accept_header) if accept_header !~ /,/ + accept_header = accept_header.split(Q_SEPARATOR_REGEXP).first if accept_header =~ TRAILING_STAR_REGEXP parse_data_with_trailing_star($1) else @@ -117,7 +119,7 @@ module Mime # keep track of creation order to keep the subsequent sort stable list, index = [], 0 accept_header.split(/,/).each do |header| - params, q = header.split(/;\s*q=/) + params, q = header.split(Q_SEPARATOR_REGEXP) if params.present? params.strip! diff --git a/actionpack/lib/action_dispatch/http/mime_types.rb b/actionpack/lib/action_dispatch/http/mime_types.rb index 3da4f91051..a6b3aee5e7 100644 --- a/actionpack/lib/action_dispatch/http/mime_types.rb +++ b/actionpack/lib/action_dispatch/http/mime_types.rb @@ -9,7 +9,7 @@ Mime::Type.register "text/calendar", :ics Mime::Type.register "text/csv", :csv Mime::Type.register "image/png", :png, [], %w(png) -Mime::Type.register "image/jpeg", :jpeg, [], %w(jpg jpeg jpe) +Mime::Type.register "image/jpeg", :jpeg, [], %w(jpg jpeg jpe pjpeg) Mime::Type.register "image/gif", :gif, [], %w(gif) Mime::Type.register "image/bmp", :bmp, [], %w(bmp) Mime::Type.register "image/tiff", :tiff, [], %w(tif tiff) diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 39ff58a447..25f1db8228 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -191,6 +191,15 @@ module ActionDispatch value end + # Whether the given cookie is to be deleted by this CookieJar. + # Like <tt>[]=</tt>, you can pass in an options hash to test if a + # deletion applies to a specific <tt>:path</tt>, <tt>:domain</tt> etc. + def deleted?(key, options = {}) + options.symbolize_keys! + handle_options(options) + @delete_cookies[key.to_s] == options + end + # Removes all cookies on the client machine by calling <tt>delete</tt> for each cookie def clear(options = {}) @cookies.each_key{ |k| delete(k, options) } diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb index 030ccb2017..d924f21fad 100644 --- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb +++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb @@ -18,11 +18,13 @@ module ActionDispatch def initialize(app, check_ip_spoofing = true, custom_proxies = nil) @app = app @check_ip = check_ip_spoofing - if custom_proxies - custom_regexp = Regexp.new(custom_proxies) - @proxies = Regexp.union(TRUSTED_PROXIES, custom_regexp) + @proxies = case custom_proxies + when Regexp + custom_proxies + when nil + TRUSTED_PROXIES else - @proxies = TRUSTED_PROXIES + Regexp.union(TRUSTED_PROXIES, custom_proxies) end end @@ -57,7 +59,7 @@ module ActionDispatch "HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect}" end - not_proxy = client_ip || forwarded_ips.last || remote_addrs.first + not_proxy = client_ip || forwarded_ips.first || remote_addrs.first # Return first REMOTE_ADDR if there are no other options not_proxy || ips_from('REMOTE_ADDR', :allow_proxies).first diff --git a/actionpack/lib/action_dispatch/middleware/stack.rb b/actionpack/lib/action_dispatch/middleware/stack.rb index a4308f528c..28e8fbdab8 100644 --- a/actionpack/lib/action_dispatch/middleware/stack.rb +++ b/actionpack/lib/action_dispatch/middleware/stack.rb @@ -93,8 +93,9 @@ module ActionDispatch end def swap(target, *args, &block) - insert_before(target, *args, &block) - delete(target) + index = assert_index(target, :before) + insert(index, *args, &block) + middlewares.delete_at(index + 1) end def delete(target) diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb index 2f6b9d266d..107fe80d1f 100644 --- a/actionpack/lib/action_dispatch/routing.rb +++ b/actionpack/lib/action_dispatch/routing.rb @@ -190,7 +190,7 @@ module ActionDispatch # Examples: # # match 'post/:id' => 'posts#show', :via => :get - # match 'post/:id' => "posts#create_comment', :via => :post + # match 'post/:id' => 'posts#create_comment', :via => :post # # Now, if you POST to <tt>/posts/:id</tt>, it will route to the <tt>create_comment</tt> action. A GET on the same # URL will route to the <tt>show</tt> action. @@ -203,7 +203,7 @@ module ActionDispatch # Examples: # # get 'post/:id' => 'posts#show' - # post 'post/:id' => "posts#create_comment' + # post 'post/:id' => 'posts#create_comment' # # This syntax is less verbose and the intention is more apparent to someone else reading your code, # however if your route needs to respond to more than one HTTP method (or all methods) then using the diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index db1e3198b3..cf9c0d7b6a 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -54,6 +54,7 @@ module ActionDispatch def initialize(set, scope, path, options) @set, @scope = set, scope + @segment_keys = nil @options = (@scope[:options] || {}).merge(options) @path = normalize_path(path) normalize_options! @@ -213,7 +214,9 @@ module ActionDispatch end def segment_keys - @segment_keys ||= Journey::Path::Pattern.new( + return @segment_keys if @segment_keys + + @segment_keys = Journey::Path::Pattern.new( Journey::Router::Strexp.compile(@path, requirements, SEPARATORS) ).names end @@ -464,7 +467,7 @@ module ActionDispatch # # get 'bacon', :to => 'food#bacon' def get(*args, &block) - map_method(:get, *args, &block) + map_method(:get, args, &block) end # Define a route that only recognizes HTTP POST. @@ -474,7 +477,7 @@ module ActionDispatch # # post 'bacon', :to => 'food#bacon' def post(*args, &block) - map_method(:post, *args, &block) + map_method(:post, args, &block) end # Define a route that only recognizes HTTP PUT. @@ -484,25 +487,24 @@ module ActionDispatch # # put 'bacon', :to => 'food#bacon' def put(*args, &block) - map_method(:put, *args, &block) + map_method(:put, args, &block) end - # Define a route that only recognizes HTTP PUT. + # Define a route that only recognizes HTTP DELETE. # For supported arguments, see <tt>Base#match</tt>. # # Example: # # delete 'broccoli', :to => 'food#broccoli' def delete(*args, &block) - map_method(:delete, *args, &block) + map_method(:delete, args, &block) end private - def map_method(method, *args, &block) + def map_method(method, args, &block) options = args.extract_options! options[:via] = method - args.push(options) - match(*args, &block) + match(*args, options, &block) self end end @@ -1247,6 +1249,9 @@ module ActionDispatch parent_resource.instance_of?(Resource) && @scope[:shallow] end + # match 'path' => 'controller#action' + # match 'path', to: 'controller#action' + # match 'path', 'otherpath', on: :member, via: :get def match(path, *rest) if rest.empty? && Hash === path options = path diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 2c21887220..6c189fdba6 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -31,6 +31,7 @@ module ActionDispatch end def prepare_params!(params) + normalize_controller!(params) merge_default_action!(params) split_glob_param!(params) if @glob_param end @@ -66,6 +67,10 @@ module ActionDispatch controller.action(action).call(env) end + def normalize_controller!(params) + params[:controller] = params[:controller].underscore if params.key?(:controller) + end + def merge_default_action!(params) params[:action] ||= 'index' end @@ -360,7 +365,26 @@ module ActionDispatch SEPARATORS, anchor) - Journey::Path::Pattern.new(strexp) + pattern = Journey::Path::Pattern.new(strexp) + + builder = Journey::GTG::Builder.new pattern.spec + + # Get all the symbol nodes followed by literals that are not the + # dummy node. + symbols = pattern.spec.grep(Journey::Nodes::Symbol).find_all { |n| + builder.followpos(n).first.literal? + } + + # Get all the symbol nodes preceded by literals. + symbols.concat pattern.spec.find_all(&:literal?).map { |n| + builder.followpos(n).first + }.find_all(&:symbol?) + + symbols.each { |x| + x.regexp = /(?:#{Regexp.union(x.regexp, '-')})+/ + } + + pattern end private :build_path @@ -463,7 +487,7 @@ module ActionDispatch # if the current controller is "foo/bar/baz" and :controller => "baz/bat" # is specified, the controller becomes "foo/baz/bat" def use_relative_controller! - if !named_route && different_controller? + if !named_route && different_controller? && !controller.start_with?("/") old_parts = current_controller.split('/') size = controller.count("/") + 1 parts = old_parts[0...-size] << controller @@ -548,6 +572,7 @@ module ActionDispatch path_addition, params = generate(path_options, path_segments || {}) path << path_addition + params.merge!(options[:params] || {}) ActionDispatch::Http::URL.url_for(options.merge!({ :path => path, @@ -579,7 +604,8 @@ module ActionDispatch params[key] = URI.parser.unescape(value) end end - + old_params = env[::ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] + env[::ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] = (old_params || {}).merge(params) dispatcher = route.app while dispatcher.is_a?(Mapper::Constraints) && dispatcher.matches?(env) do dispatcher = dispatcher.app diff --git a/actionpack/lib/action_dispatch/testing/assertions/selector.rb b/actionpack/lib/action_dispatch/testing/assertions/selector.rb index 4d963803e6..8eed85bce2 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/selector.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/selector.rb @@ -340,8 +340,8 @@ module ActionDispatch # element +encoded+. It then calls the block with all un-encoded elements. # # ==== Examples - # # Selects all bold tags from within the title of an ATOM feed's entries (perhaps to nab a section name prefix) - # assert_select_feed :atom, 1.0 do + # # Selects all bold tags from within the title of an Atom feed's entries (perhaps to nab a section name prefix) + # assert_select "feed[xmlns='http://www.w3.org/2005/Atom']" do # # Select each entry item and then the title item # assert_select "entry>title" do # # Run assertions on the encoded title elements @@ -353,7 +353,7 @@ module ActionDispatch # # # # Selects all paragraph tags from within the description of an RSS feed - # assert_select_feed :rss, 2.0 do + # assert_select "rss[version=2.0]" do # # Select description element of each feed item. # assert_select "channel>item>description" do # # Run assertions on the encoded elements. @@ -417,8 +417,8 @@ module ActionDispatch deliveries = ActionMailer::Base.deliveries assert !deliveries.empty?, "No e-mail in delivery list" - for delivery in deliveries - for part in (delivery.parts.empty? ? [delivery] : delivery.parts) + deliveries.each do |delivery| + (delivery.parts.empty? ? [delivery] : delivery.parts).each do |part| if part["Content-Type"].to_s =~ /^text\/html\W/ root = HTML::Document.new(part.body.to_s).root assert_select root, ":root", &block diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb index 122dc9db7f..23329d7f35 100644 --- a/actionpack/lib/action_view/base.rb +++ b/actionpack/lib/action_view/base.rb @@ -147,7 +147,6 @@ module ActionView #:nodoc: class << self delegate :erb_trim_mode=, :to => 'ActionView::Template::Handlers::ERB' - delegate :logger, :to => 'ActionController::Base', :allow_nil => true def cache_template_loading ActionView::Resolver.caching? diff --git a/actionpack/lib/action_view/flows.rb b/actionpack/lib/action_view/flows.rb index a8f740713f..c0e458cd41 100644 --- a/actionpack/lib/action_view/flows.rb +++ b/actionpack/lib/action_view/flows.rb @@ -22,11 +22,8 @@ module ActionView def append(key, value) @content[key] << value end + alias_method :append!, :append - # Called by provide - def append!(key, value) - @content[key] << value - end end class StreamingFlow < OutputFlow #:nodoc: diff --git a/actionpack/lib/action_view/helpers/active_model_helper.rb b/actionpack/lib/action_view/helpers/active_model_helper.rb index 973135e2ea..e27111012d 100644 --- a/actionpack/lib/action_view/helpers/active_model_helper.rb +++ b/actionpack/lib/action_view/helpers/active_model_helper.rb @@ -16,7 +16,9 @@ module ActionView end end - module_eval "def content_tag(*) error_wrapping(super) end", __FILE__, __LINE__ + def content_tag(*) + error_wrapping(super) + end def tag(type, options, *) tag_generate_errors?(options) ? error_wrapping(super) : super @@ -37,7 +39,7 @@ module ActionView private def object_has_errors? - object.respond_to?(:errors) && object.errors.respond_to?(:full_messages) && error_message.any? + object.respond_to?(:errors) && object.errors.respond_to?(:[]) && error_message.present? end def tag_generate_errors?(options) diff --git a/actionpack/lib/action_view/helpers/asset_tag_helper.rb b/actionpack/lib/action_view/helpers/asset_tag_helper.rb index 5dbba3c4a7..662adbe183 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helper.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/array/extract_options' +require 'active_support/core_ext/hash/keys' require 'action_view/helpers/asset_tag_helpers/javascript_tag_helpers' require 'action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers' require 'action_view/helpers/asset_tag_helpers/asset_paths' @@ -196,7 +198,7 @@ module ActionView include JavascriptTagHelpers include StylesheetTagHelpers # Returns a link tag that browsers and news readers can use to auto-detect - # an RSS or ATOM feed. The +type+ can either be <tt>:rss</tt> (default) or + # an RSS or Atom feed. The +type+ can either be <tt>:rss</tt> (default) or # <tt>:atom</tt>. Control the link options in url_for format using the # +url_options+. You can modify the LINK tag itself in +tag_options+. # @@ -232,7 +234,7 @@ module ActionView # # generates # - # <link href="/favicon.ico" rel="shortcut icon" type="image/vnd.microsoft.icon" /> + # <link href="/assets/favicon.ico" rel="shortcut icon" type="image/vnd.microsoft.icon" /> # # You may specify a different file in the first argument: # @@ -250,7 +252,7 @@ module ActionView # # <%= favicon_link_tag 'mb-icon.png', :rel => 'apple-touch-icon', :type => 'image/png' %> # - def favicon_link_tag(source='/favicon.ico', options={}) + def favicon_link_tag(source='favicon.ico', options={}) tag('link', { :rel => 'shortcut icon', :type => 'image/vnd.microsoft.icon', @@ -276,6 +278,13 @@ module ActionView end alias_method :path_to_image, :image_path # aliased to avoid conflicts with an image_path named route + # Computes the full URL to an image asset in the public images directory. + # This will use +image_path+ internally, so most of their behaviors will be the same. + def image_url(source) + URI.join(current_host, path_to_image(source)).to_s + end + alias_method :url_to_image, :image_url # aliased to avoid conflicts with an image_url named route + # Computes the path to a video asset in the public videos directory. # Full paths from the document root will be passed through. # Used internally by +video_tag+ to build the video path. @@ -291,6 +300,13 @@ module ActionView end alias_method :path_to_video, :video_path # aliased to avoid conflicts with a video_path named route + # 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. + def video_url(source) + URI.join(current_host, path_to_video(source)).to_s + end + alias_method :url_to_video, :video_url # aliased to avoid conflicts with an video_url named route + # Computes the path to an audio asset in the public audios directory. # Full paths from the document root will be passed through. # Used internally by +audio_tag+ to build the audio path. @@ -306,6 +322,13 @@ module ActionView end alias_method :path_to_audio, :audio_path # aliased to avoid conflicts with an audio_path named route + # Computes the full URL to a audio asset in the public audios directory. + # This will use +audio_path+ internally, so most of their behaviors will be the same. + def audio_url(source) + URI.join(current_host, path_to_audio(source)).to_s + end + alias_method :url_to_audio, :audio_url # aliased to avoid conflicts with an audio_url named route + # Computes the path to a font asset in the public fonts directory. # Full paths from the document root will be passed through. # @@ -320,6 +343,13 @@ module ActionView end alias_method :path_to_font, :font_path # aliased to avoid conflicts with an font_path named route + # Computes the full URL to a font asset in the public fonts directory. + # This will use +font_path+ internally, so most of their behaviors will be the same. + def font_url(source) + URI.join(current_host, path_to_font(source)).to_s + end + alias_method :url_to_font, :font_url # aliased to avoid conflicts with an font_url named route + # Returns an html image tag for the +source+. The +source+ can be a full # path or a file that exists in your public images directory. # @@ -353,8 +383,8 @@ module ActionView # <img src="/images/mouse.png" onmouseover="this.src='/images/mouse_over.png'" onmouseout="this.src='/images/mouse.png'" alt="Mouse" /> # image_tag("mouse.png", :mouseover => image_path("mouse_over.png")) # => # <img src="/images/mouse.png" onmouseover="this.src='/images/mouse_over.png'" onmouseout="this.src='/images/mouse.png'" alt="Mouse" /> - def image_tag(source, options = {}) - options.symbolize_keys! + def image_tag(source, options={}) + options = options.dup.symbolize_keys! src = options[:src] = path_to_image(source) @@ -407,26 +437,19 @@ module ActionView # <video src="/trailers/hd.avi" width="16" height="16" /> # video_tag("/trailers/hd.avi", :height => '32', :width => '32') # => # <video height="32" src="/trailers/hd.avi" width="32" /> + # video_tag("trailer.ogg", "trailer.flv") # => + # <video><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video> # video_tag(["trailer.ogg", "trailer.flv"]) # => - # <video><source src="trailer.ogg" /><source src="trailer.ogg" /><source src="trailer.flv" /></video> + # <video><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video> # video_tag(["trailer.ogg", "trailer.flv"] :size => "160x120") # => - # <video height="120" width="160"><source src="trailer.ogg" /><source src="trailer.flv" /></video> - def video_tag(sources, options = {}) - options.symbolize_keys! - - options[:poster] = path_to_image(options[:poster]) if options[:poster] - - if size = options.delete(:size) - options[:width], options[:height] = size.split("x") if size =~ %r{^\d+x\d+$} - end + # <video height="120" width="160"><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video> + def video_tag(*sources) + multiple_sources_tag('video', sources) do |options| + options[:poster] = path_to_image(options[:poster]) if options[:poster] - if sources.is_a?(Array) - content_tag("video", options) do - sources.map { |source| tag("source", :src => source) }.join.html_safe + if size = options.delete(:size) + options[:width], options[:height] = size.split("x") if size =~ %r{^\d+x\d+$} end - else - options[:src] = path_to_video(sources) - tag("video", options) end end @@ -441,10 +464,10 @@ module ActionView # <audio src="/audios/sound.wav" /> # audio_tag("sound.wav", :autoplay => true, :controls => true) # => # <audio autoplay="autoplay" controls="controls" src="/audios/sound.wav" /> - def audio_tag(source, options = {}) - options.symbolize_keys! - options[:src] = path_to_audio(source) - tag("audio", options) + # audio_tag("sound.wav", "sound.mid") # => + # <audio><source src="/audios/sound.wav" /><source src="/audios/sound.mid" /></audio> + def audio_tag(*sources) + multiple_sources_tag('audio', sources) end private @@ -452,6 +475,26 @@ module ActionView def asset_paths @asset_paths ||= AssetTagHelper::AssetPaths.new(config, controller) end + + def multiple_sources_tag(type, sources) + options = sources.extract_options!.dup.symbolize_keys! + sources.flatten! + + yield options if block_given? + + if sources.size > 1 + content_tag(type, options) do + safe_join sources.map { |source| tag("source", :src => send("path_to_#{type}", source)) } + end + else + options[:src] = send("path_to_#{type}", sources.first) + content_tag(type, nil, options) + end + end + + def current_host + url_for(:only_path => false) + end end end end diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb index d9f1f88ade..c67f81dcf4 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb @@ -87,6 +87,13 @@ module ActionView end alias_method :path_to_javascript, :javascript_path # aliased to avoid conflicts with a javascript_path named route + # 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. + def javascript_url(source) + URI.join(current_host, path_to_javascript(source)).to_s + end + alias_method :url_to_javascript, :javascript_url # aliased to avoid conflicts with a javascript_url named route + # Returns an HTML script tag for each of the +sources+ provided. # # Sources may be paths to JavaScript files. Relative paths are assumed to be relative diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb index 41958c6559..2584b67548 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb @@ -65,6 +65,13 @@ module ActionView end alias_method :path_to_stylesheet, :stylesheet_path # aliased to avoid conflicts with a stylesheet_path named route + # 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. + def stylesheet_url(source) + URI.join(current_host, path_to_stylesheet(source)).to_s + end + alias_method :url_to_stylesheet, :stylesheet_url # aliased to avoid conflicts with a stylesheet_url named route + # Returns a stylesheet link tag for the sources specified as arguments. If # you don't specify an extension, <tt>.css</tt> will be appended automatically. # You can modify the link attributes by passing a hash as the last argument. diff --git a/actionpack/lib/action_view/helpers/csrf_helper.rb b/actionpack/lib/action_view/helpers/csrf_helper.rb index 1f2bc28cac..eeb0ed94b9 100644 --- a/actionpack/lib/action_view/helpers/csrf_helper.rb +++ b/actionpack/lib/action_view/helpers/csrf_helper.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/string/strip' - module ActionView # = Action View CSRF Helper module Helpers diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb index f5077b034a..2d37923825 100644 --- a/actionpack/lib/action_view/helpers/date_helper.rb +++ b/actionpack/lib/action_view/helpers/date_helper.rb @@ -142,6 +142,8 @@ module ActionView # ==== Options # * <tt>:use_month_numbers</tt> - Set to true if you want to use month numbers rather than month names (e.g. # "2" instead of "February"). + # * <tt>:use_two_digit_numbers</tt> - Set to true if you want to display two digit month and day numbers (e.g. + # "02" instead of "February" and "08" instead of "8"). # * <tt>:use_short_month</tt> - Set to true if you want to use abbreviated month names instead of full # month names (e.g. "Feb" instead of "February"). # * <tt>:add_month_numbers</tt> - Set to true if you want to use both month numbers and month names (e.g. @@ -189,6 +191,10 @@ module ActionView # date_select("article", "written_on", :start_year => 1995, :use_month_numbers => true, # :discard_day => true, :include_blank => true) # + # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute, + # # with two digit numbers used for months and days. + # date_select("article", "written_on", :use_two_digit_numbers => true) + # # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute # # with the fields ordered as day, month, year rather than month, day, year. # date_select("article", "written_on", :order => [:day, :month, :year]) @@ -502,6 +508,7 @@ module ActionView # Returns a select tag with options for each of the days 1 through 31 with the current day selected. # The <tt>date</tt> can also be substituted for a day number. + # If you want to display days with a leading zero set the <tt>:use_two_digit_numbers</tt> key in +options+ to true. # Override the field name using the <tt>:field_name</tt> option, 'day' by default. # # ==== Examples @@ -513,6 +520,9 @@ module ActionView # # Generates a select field for days that defaults to the number given. # select_day(5) # + # # Generates a select field for days that defaults to the number given, but displays it with two digits. + # select_day(5, :use_two_digit_numbers => true) + # # # Generates a select field for days that defaults to the day for the date in my_date # # that is named 'due' rather than 'day'. # select_day(my_time, :field_name => 'due') @@ -532,6 +542,7 @@ module ActionView # want both numbers and names, set the <tt>:add_month_numbers</tt> key in +options+ to true. If you would prefer # to show month names as abbreviations, set the <tt>:use_short_month</tt> key in +options+ to true. If you want # to use your own month names, set the <tt>:use_month_names</tt> key in +options+ to an array of 12 month names. + # If you want to display months with a leading zero set the <tt>:use_two_digit_numbers</tt> key in +options+ to true. # Override the field name using the <tt>:field_name</tt> option, 'month' by default. # # ==== Examples @@ -559,6 +570,10 @@ module ActionView # # will use keys like "Januar", "Marts." # select_month(Date.today, :use_month_names => %w(Januar Februar Marts ...)) # + # # Generates a select field for months that defaults to the current month that + # # will use keys with two digit numbers like "01", "03". + # select_month(Date.today, :use_two_digit_numbers => true) + # # # Generates a select field for months with a custom prompt. Use <tt>:prompt => true</tt> for a # # generic prompt. # select_month(14, :prompt => 'Choose month') @@ -734,7 +749,7 @@ module ActionView def select_day if @options[:use_hidden] || @options[:discard_day] - build_hidden(:day, day) + build_hidden(:day, day || 1) else build_options_and_select(:day, day, :start => 1, :end => 31, :leading_zeros => false, :use_two_digit_numbers => @options[:use_two_digit_numbers]) end @@ -742,7 +757,7 @@ module ActionView def select_month if @options[:use_hidden] || @options[:discard_month] - build_hidden(:month, month) + build_hidden(:month, month || 1) else month_options = [] 1.upto(12) do |month_number| @@ -756,7 +771,7 @@ module ActionView def select_year if !@datetime || @datetime == 0 - val = '' + val = '1' middle_year = Date.today.year else val = middle_year = year @@ -817,6 +832,9 @@ module ActionView # If <tt>:use_month_numbers</tt> option is passed # month_name(1) => 1 # + # If <tt>:use_two_month_numbers</tt> option is passed + # month_name(1) => '01' + # # If <tt>:add_month_numbers</tt> option is passed # month_name(1) => "1 - January" def month_name(number) @@ -836,7 +854,15 @@ module ActionView end def translated_date_order - I18n.translate(:'date.order', :locale => @options[:locale]) || [] + date_order = I18n.translate(:'date.order', :locale => @options[:locale], :default => []) + + forbidden_elements = date_order - [:year, :month, :day] + if forbidden_elements.any? + raise StandardError, + "#{@options[:locale]}.date.order only accepts :year, :month and :day" + end + + date_order end # Build full select tag from date type and options. @@ -850,6 +876,12 @@ module ActionView # <option value="2">2</option> # <option value="3">3</option>..." # + # If <tt>:use_two_digit_numbers => true</tt> option is passed + # build_options(15, :start => 1, :end => 31, :use_two_digit_numbers => true) + # => "<option value="1">01</option> + # <option value="2">02</option> + # <option value="3">03</option>..." + # # If <tt>:step</tt> options is passed # build_options(15, :start => 1, :end => 31, :step => 2) # => "<option value="1">1</option> @@ -958,18 +990,12 @@ module ActionView # Returns the separator for a given datetime component. def separator(type) case type - when :year - @options[:discard_year] ? "" : @options[:date_separator] - when :month - @options[:discard_month] ? "" : @options[:date_separator] - when :day - @options[:discard_day] ? "" : @options[:date_separator] + when :year, :month, :day + @options[:"discard_#{type}"] ? "" : @options[:date_separator] when :hour (@options[:discard_year] && @options[:discard_day]) ? "" : @options[:datetime_separator] - when :minute - @options[:discard_minute] ? "" : @options[:time_separator] - when :second - @options[:include_seconds] ? @options[:time_separator] : "" + when :minute, :second + @options[:"discard_#{type}"] ? "" : @options[:time_separator] end end end diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index bdfef920c5..44e24fecd1 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -889,6 +889,24 @@ module ActionView end alias phone_field telephone_field + # Returns a text_field of type "date". + # + # 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" + # 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. + # + # @user.born_on = Date.new(1984, 1, 27) + # date_field("user", "born_on", value: "1984-05-12") + # # => <input id="user_born_on" name="user[born_on]" type="date" value="1984-05-12" /> + # + def date_field(object_name, method, options = {}) + Tags::DateField.new(object_name, method, self, options).render + end + # Returns a text_field of type "url". # # url_field("user", "homepage") diff --git a/actionpack/lib/action_view/helpers/form_options_helper.rb b/actionpack/lib/action_view/helpers/form_options_helper.rb index e323350608..bc03a1cf83 100644 --- a/actionpack/lib/action_view/helpers/form_options_helper.rb +++ b/actionpack/lib/action_view/helpers/form_options_helper.rb @@ -164,7 +164,9 @@ module ActionView # # The <tt>:value_method</tt> and <tt>:text_method</tt> parameters are methods to be called on each member # of +collection+. The return values are used as the +value+ attribute and contents of each - # <tt><option></tt> tag, respectively. + # <tt><option></tt> tag, respectively. They can also be any object that responds to +call+, such + # as a +proc+, that will be called for each member of the +collection+ to + # retrieve the value/text. # # Example object structure for use with this method: # class Post < ActiveRecord::Base @@ -334,7 +336,7 @@ module ActionView end.join("\n").html_safe end - # Returns a string of option tags that have been compiled by iterating over the +collection+ and assigning the + # Returns a string of option tags that have been compiled by iterating over the +collection+ and assigning # the result of a call to the +value_method+ as the option value and the +text_method+ as the option text. # Example: # options_from_collection_for_select(@people, 'id', 'name') @@ -360,12 +362,13 @@ module ActionView # should produce the desired results. def options_from_collection_for_select(collection, value_method, text_method, selected = nil) options = collection.map do |element| - [element.send(text_method), element.send(value_method)] + [value_for_collection(element, text_method), value_for_collection(element, value_method)] end selected, disabled = extract_selected_and_disabled(selected) - select_deselect = {} - select_deselect[:selected] = extract_values_from_collection(collection, value_method, selected) - select_deselect[:disabled] = extract_values_from_collection(collection, value_method, disabled) + select_deselect = { + :selected => extract_values_from_collection(collection, value_method, selected), + :disabled => extract_values_from_collection(collection, value_method, disabled) + } options_for_select(options, select_deselect) end @@ -418,10 +421,10 @@ module ActionView # wrap the output in an appropriate <tt><select></tt> tag. def option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, selected_key = nil) collection.map do |group| - group_label_string = eval("group.#{group_label_method}") - "<optgroup label=\"#{ERB::Util.html_escape(group_label_string)}\">" + - options_from_collection_for_select(eval("group.#{group_method}"), option_key_method, option_value_method, selected_key) + - '</optgroup>' + 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)) end.join.html_safe end @@ -519,14 +522,138 @@ module ActionView zone_options.html_safe end + # Returns radio button tags for the collection of existing return values + # of +method+ for +object+'s class. The value returned from calling + # +method+ on the instance +object+ will be selected. If calling +method+ + # returns +nil+, no selection is made. + # + # The <tt>:value_method</tt> and <tt>:text_method</tt> parameters are + # methods to be called on each member of +collection+. The return values + # are used as the +value+ attribute and contents of each radio button tag, + # respectively. They can also be any object that responds to +call+, such + # as a +proc+, that will be called for each member of the +collection+ to + # retrieve the value/text. + # + # Example object structure for use with this method: + # class Post < ActiveRecord::Base + # belongs_to :author + # end + # class Author < ActiveRecord::Base + # has_many :posts + # def name_with_initial + # "#{first_name.first}. #{last_name}" + # end + # end + # + # Sample usage (selecting the associated Author for an instance of Post, <tt>@post</tt>): + # collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) + # + # If <tt>@post.author_id</tt> is already <tt>1</tt>, this would return: + # <input id="post_author_id_1" name="post[author_id]" type="radio" value="1" checked="checked" /> + # <label for="post_author_id_1">D. Heinemeier Hansson</label> + # <input id="post_author_id_2" name="post[author_id]" type="radio" value="2" /> + # <label for="post_author_id_2">D. Thomas</label> + # <input id="post_author_id_3" name="post[author_id]" type="radio" value="3" /> + # <label for="post_author_id_3">M. Clark</label> + # + # It is also possible to customize the way the elements will be shown by + # giving a block to the method: + # collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) do |b| + # b.label { b.radio_button } + # end + # + # The argument passed to the block is a special kind of builder for this + # collection, which has the ability to generate the label and radio button + # for the current item in the collection, with proper text and value. + # Using it, you can change the label and radio button display order or + # even use the label as wrapper, as in the example above. + # + # The builder methods <tt>label</tt> and <tt>radio_button</tt> also accept + # extra html options: + # collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) do |b| + # b.label(:class => "radio_button") { b.radio_button(:class => "radio_button") } + # end + # + # There are also two special methods available: <tt>text</tt> and + # <tt>value</tt>, which are the current text and value methods for the + # item being rendered, respectively. You can use them like this: + # 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 + 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 + + # Returns check box tags for the collection of existing return values of + # +method+ for +object+'s class. The value returned from calling +method+ + # on the instance +object+ will be selected. If calling +method+ returns + # +nil+, no selection is made. + # + # The <tt>:value_method</tt> and <tt>:text_method</tt> parameters are + # methods to be called on each member of +collection+. The return values + # are used as the +value+ attribute and contents of each check box tag, + # respectively. They can also be any object that responds to +call+, such + # as a +proc+, that will be called for each member of the +collection+ to + # retrieve the value/text. + # + # Example object structure for use with this method: + # class Post < ActiveRecord::Base + # has_and_belongs_to_many :author + # end + # class Author < ActiveRecord::Base + # has_and_belongs_to_many :posts + # def name_with_initial + # "#{first_name.first}. #{last_name}" + # end + # end + # + # Sample usage (selecting the associated Author for an instance of Post, <tt>@post</tt>): + # collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial) + # + # If <tt>@post.author_ids</tt> is already <tt>[1]</tt>, this would return: + # <input id="post_author_ids_1" name="post[author_ids][]" type="checkbox" value="1" checked="checked" /> + # <label for="post_author_ids_1">D. Heinemeier Hansson</label> + # <input id="post_author_ids_2" name="post[author_ids][]" type="checkbox" value="2" /> + # <label for="post_author_ids_2">D. Thomas</label> + # <input id="post_author_ids_3" name="post[author_ids][]" type="checkbox" value="3" /> + # <label for="post_author_ids_3">M. Clark</label> + # <input name="post[author_ids][]" type="hidden" value="" /> + # + # It is also possible to customize the way the elements will be shown by + # giving a block to the method: + # collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial) do |b| + # b.label { b.check_box } + # end + # + # The argument passed to the block is a special kind of builder for this + # collection, which has the ability to generate the label and check box + # for the current item in the collection, with proper text and value. + # Using it, you can change the label and check box display order or even + # use the label as wrapper, as in the example above. + # + # The builder methods <tt>label</tt> and <tt>check_box</tt> also accept + # extra html options: + # collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial) do |b| + # b.label(:class => "check_box") { b.check_box(:class => "check_box") } + # end + # + # There are also two special methods available: <tt>text</tt> and + # <tt>value</tt>, which are the current text and value methods for the + # item being rendered, respectively. You can use them like this: + # 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 + 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 + private def option_html_attributes(element) return "" unless Array === element - html_attributes = [] - element.select { |e| Hash === e }.reduce({}, :merge).each do |k, v| - html_attributes << " #{k}=\"#{ERB::Util.html_escape(v.to_s)}\"" - end - html_attributes.join + + element.select { |e| Hash === e }.reduce({}, :merge).map do |k, v| + " #{k}=\"#{ERB::Util.html_escape(v.to_s)}\"" + end.join end def option_text_and_value(option) @@ -552,12 +679,12 @@ module ActionView def extract_selected_and_disabled(selected) if selected.is_a?(Proc) - [ selected, nil ] + [selected, nil] else selected = Array.wrap(selected) options = selected.extract_options!.symbolize_keys - selected_items = options.include?(:selected) ? options[:selected] : selected - [ selected_items, options[:disabled] ] + selected_items = options.fetch(:selected, selected) + [selected_items, options[:disabled]] end end @@ -570,6 +697,10 @@ module ActionView selected end end + + def value_for_collection(item, value) + value.respond_to?(:call) ? value.call(item) : item.send(value) + end end class FormBuilder @@ -588,6 +719,14 @@ module ActionView def time_zone_select(method, priority_zones = nil, options = {}, html_options = {}) @template.time_zone_select(@object_name, method, priority_zones, objectify_options(options), @default_options.merge(html_options)) end + + def collection_check_boxes(method, collection, value_method, text_method, options = {}, html_options = {}) + @template.collection_check_boxes(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options)) + end + + def collection_radio_buttons(method, collection, value_method, text_method, options = {}, html_options = {}) + @template.collection_radio_buttons(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options)) + end end end end diff --git a/actionpack/lib/action_view/helpers/form_tag_helper.rb b/actionpack/lib/action_view/helpers/form_tag_helper.rb index 57b90a9c42..53fd189c39 100644 --- a/actionpack/lib/action_view/helpers/form_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/form_tag_helper.rb @@ -549,6 +549,14 @@ module ActionView end alias phone_field_tag telephone_field_tag + # Creates a text field of type "date". + # + # ==== Options + # * Accepts the same options as text_field_tag. + def date_field_tag(name, value = nil, options = {}) + text_field_tag(name, value, options.stringify_keys.update("type" => "date")) + end + # Creates a text field of type "url". # # ==== Options @@ -627,7 +635,7 @@ module ActionView token_tag(authenticity_token) else html_options["method"] = "post" - tag(:input, :type => "hidden", :name => "_method", :value => method) + token_tag(authenticity_token) + method_tag(method) + token_tag(authenticity_token) end tags = utf8_enforcer_tag << method_tag @@ -646,15 +654,6 @@ module ActionView output.safe_concat("</form>") end - def token_tag(token) - if token == false || !protect_against_forgery? - '' - else - token ||= form_authenticity_token - tag(:input, :type => "hidden", :name => request_forgery_protection_token.to_s, :value => token) - end - end - # see http://www.w3.org/TR/html4/types.html#type-name def sanitize_to_id(name) name.to_s.gsub(']','').gsub(/[^-a-zA-Z0-9:.]/, "_") diff --git a/actionpack/lib/action_view/helpers/javascript_helper.rb b/actionpack/lib/action_view/helpers/javascript_helper.rb index 309923490c..ac9e530f01 100644 --- a/actionpack/lib/action_view/helpers/javascript_helper.rb +++ b/actionpack/lib/action_view/helpers/javascript_helper.rb @@ -1,5 +1,4 @@ require 'action_view/helpers/tag_helper' -require 'active_support/core_ext/string/encoding' module ActionView module Helpers diff --git a/actionpack/lib/action_view/helpers/number_helper.rb b/actionpack/lib/action_view/helpers/number_helper.rb index 43122ef2ba..b0860f87c4 100644 --- a/actionpack/lib/action_view/helpers/number_helper.rb +++ b/actionpack/lib/action_view/helpers/number_helper.rb @@ -57,15 +57,11 @@ module ActionView # # => +1.123.555.1234 x 1343 def number_to_phone(number, options = {}) return unless number + options = options.symbolize_keys - begin - Float(number) - rescue ArgumentError, TypeError - raise InvalidNumberError, number - end if options[:raise] + parse_float(number, true) if options[:raise] number = number.to_s.strip - options = options.symbolize_keys area_code = options[:area_code] delimiter = options[:delimiter] || "-" extension = options[:extension] @@ -75,7 +71,7 @@ module ActionView number.gsub!(/(\d{1,3})(\d{3})(\d{4}$)/,"(\\1) \\2#{delimiter}\\3") else number.gsub!(/(\d{0,3})(\d{3})(\d{4})$/,"\\1#{delimiter}\\2#{delimiter}\\3") - number.slice!(0, 1) if number.starts_with?(delimiter) && !delimiter.blank? + number.slice!(0, 1) if number.start_with?(delimiter) && !delimiter.blank? end str = [] @@ -122,14 +118,12 @@ module ActionView # # => 1234567890,50 £ def number_to_currency(number, options = {}) return unless number + options = options.symbolize_keys - options.symbolize_keys! - - defaults = I18n.translate(:'number.format', :locale => options[:locale], :default => {}) - currency = I18n.translate(:'number.currency.format', :locale => options[:locale], :default => {}) + currency = translations_for('currency', options[:locale]) currency[:negative_format] ||= "-" + currency[:format] if currency[:format] - defaults = DEFAULT_CURRENCY_VALUES.merge(defaults).merge!(currency) + defaults = DEFAULT_CURRENCY_VALUES.merge(defaults_translations(options[:locale])).merge!(currency) defaults[:negative_format] = "-" + options[:format] if options[:format] options = defaults.merge!(options) @@ -152,7 +146,6 @@ module ActionView e.number.to_s.html_safe? ? formatted_number.html_safe : formatted_number end end - end # Formats a +number+ as a percentage string (e.g., 65%). You can customize the format in the +options+ hash. @@ -169,6 +162,8 @@ module ActionView # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to ""). # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes insignificant zeros after the decimal separator # (defaults to +false+). + # * <tt>:format</tt> - Specifies the format of the percentage string + # The number field is <tt>%n</tt> (defaults to "%n%"). # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when the argument is invalid. # # ==== Examples @@ -180,26 +175,27 @@ 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("98a", :raise => true) # => InvalidNumberError def number_to_percentage(number, options = {}) return unless number + options = options.symbolize_keys - options.symbolize_keys! + defaults = format_translations('percentage', options[:locale]) + options = defaults.merge!(options) - defaults = I18n.translate(:'number.format', :locale => options[:locale], :default => {}) - percentage = I18n.translate(:'number.percentage.format', :locale => options[:locale], :default => {}) - defaults = defaults.merge(percentage) - - options = options.reverse_merge(defaults) + format = options[:format] || "%n%" begin - "#{number_with_precision(number, options.merge(:raise => true))}%".html_safe + value = number_with_precision(number, options.merge(:raise => true)) + format.gsub(/%n/, value).html_safe rescue InvalidNumberError => e if options[:raise] raise else - e.number.to_s.html_safe? ? "#{e.number}%".html_safe : "#{e.number}%" + formatted_number = format.gsub(/%n/, e.number) + e.number.to_s.html_safe? ? formatted_number.html_safe : formatted_number end end end @@ -229,25 +225,15 @@ module ActionView # # number_with_delimiter("112a", :raise => true) # => raise InvalidNumberError def number_with_delimiter(number, options = {}) - options.symbolize_keys! + options = options.symbolize_keys - begin - Float(number) - rescue ArgumentError, TypeError - if options[:raise] - raise InvalidNumberError, number - else - return number - end - end + parse_float(number, options[:raise]) or return number - defaults = I18n.translate(:'number.format', :locale => options[:locale], :default => {}) - options = options.reverse_merge(defaults) + options = defaults_translations(options[:locale]).merge(options) parts = number.to_s.to_str.split('.') parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{options[:delimiter]}") parts.join(options[:separator]).html_safe - end # Formats a +number+ with the specified level of <tt>:precision</tt> (e.g., 112.32 has a precision @@ -264,6 +250,7 @@ module ActionView # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to ""). # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes insignificant zeros after the decimal separator # (defaults to +false+). + # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when the argument is invalid. # # ==== Examples # number_with_precision(111.2345) # => 111.235 @@ -282,23 +269,13 @@ module ActionView # number_with_precision(1111.2345, :precision => 2, :separator => ',', :delimiter => '.') # # => 1.111,23 def number_with_precision(number, options = {}) - options.symbolize_keys! + options = options.symbolize_keys - number = begin - Float(number) - rescue ArgumentError, TypeError - if options[:raise] - raise InvalidNumberError, number - else - return number - end - end + number = (parse_float(number, options[:raise]) or return number) - defaults = I18n.translate(:'number.format', :locale => options[:locale], :default => {}) - precision_defaults = I18n.translate(:'number.precision.format', :locale => options[:locale], :default => {}) - defaults = defaults.merge(precision_defaults) + defaults = format_translations('precision', options[:locale]) + options = defaults.merge!(options) - options = options.reverse_merge(defaults) # Allow the user to unset default values: Eg.: :significant => false precision = options.delete :precision significant = options.delete :significant strip_insignificant_zeros = options.delete :strip_insignificant_zeros @@ -323,7 +300,6 @@ module ActionView else formatted_number end - end STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb].freeze @@ -343,6 +319,7 @@ module ActionView # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to ""). # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes insignificant zeros after the decimal separator (defaults to +true+) # * <tt>:prefix</tt> - If +:si+ formats the number using the SI prefix (defaults to :binary) + # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when the argument is invalid. # ==== Examples # number_to_human_size(123) # => 123 Bytes # number_to_human_size(1234) # => 1.21 KB @@ -359,23 +336,13 @@ module ActionView # number_to_human_size(1234567890123, :precision => 5) # => "1.1229 TB" # number_to_human_size(524288000, :precision => 5) # => "500 MB" def number_to_human_size(number, options = {}) - options.symbolize_keys! + options = options.symbolize_keys - number = begin - Float(number) - rescue ArgumentError, TypeError - if options[:raise] - raise InvalidNumberError, number - else - return number - end - end + number = (parse_float(number, options[:raise]) or return number) - defaults = I18n.translate(:'number.format', :locale => options[:locale], :default => {}) - human = I18n.translate(:'number.human.format', :locale => options[:locale], :default => {}) - defaults = defaults.merge(human) + defaults = format_translations('human', options[:locale]) + options = defaults.merge!(options) - options = options.reverse_merge(defaults) #for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files options[:strip_insignificant_zeros] = true if not options.key?(:strip_insignificant_zeros) @@ -424,6 +391,7 @@ module ActionView # * *integers*: <tt>:unit</tt>, <tt>:ten</tt>, <tt>:hundred</tt>, <tt>:thousand</tt>, <tt>:million</tt>, <tt>:billion</tt>, <tt>:trillion</tt>, <tt>:quadrillion</tt> # * *fractionals*: <tt>:deci</tt>, <tt>:centi</tt>, <tt>:mili</tt>, <tt>:micro</tt>, <tt>:nano</tt>, <tt>:pico</tt>, <tt>:femto</tt> # * <tt>:format</tt> - Sets the format of the output string (defaults to "%n %u"). The field types are: + # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when the argument is invalid. # # %u The quantifier (ex.: 'thousand') # %n The number @@ -478,23 +446,13 @@ module ActionView # number_to_human(0.34, :units => :distance) # => "34 centimeters" # def number_to_human(number, options = {}) - options.symbolize_keys! + options = options.symbolize_keys - number = begin - Float(number) - rescue ArgumentError, TypeError - if options[:raise] - raise InvalidNumberError, number - else - return number - end - end + number = (parse_float(number, options[:raise]) or return number) - defaults = I18n.translate(:'number.format', :locale => options[:locale], :default => {}) - human = I18n.translate(:'number.human.format', :locale => options[:locale], :default => {}) - defaults = defaults.merge(human) + defaults = format_translations('human', options[:locale]) + options = defaults.merge!(options) - options = options.reverse_merge(defaults) #for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files options[:strip_insignificant_zeros] = true if not options.key?(:strip_insignificant_zeros) @@ -530,6 +488,25 @@ module ActionView decimal_format.gsub(/%n/, formatted_number).gsub(/%u/, unit).strip.html_safe end + private + + def format_translations(namespace, locale) + defaults_translations(locale).merge(translations_for(namespace, locale)) + end + + def defaults_translations(locale) + I18n.translate(:'number.format', :locale => locale, :default => {}) + end + + def translations_for(namespace, locale) + I18n.translate(:"number.#{namespace}.format", :locale => locale, :default => {}) + end + + def parse_float(number, raise_error) + Float(number) + rescue ArgumentError, TypeError + raise InvalidNumberError, number if raise_error + end end end end diff --git a/actionpack/lib/action_view/helpers/tag_helper.rb b/actionpack/lib/action_view/helpers/tag_helper.rb index d7a2651bad..ecd26891d6 100644 --- a/actionpack/lib/action_view/helpers/tag_helper.rb +++ b/actionpack/lib/action_view/helpers/tag_helper.rb @@ -118,7 +118,7 @@ module ActionView # escape_once("<< Accept & Checkout") # # => "<< Accept & Checkout" def escape_once(html) - html.to_s.gsub(/[\"><]|&(?!([a-zA-Z]+|(#\d+));)/) { |special| ERB::Util::HTML_ESCAPE[special] } + ERB::Util.html_escape_once(html) end private diff --git a/actionpack/lib/action_view/helpers/tags.rb b/actionpack/lib/action_view/helpers/tags.rb index 89b3efda5f..3cf762877f 100644 --- a/actionpack/lib/action_view/helpers/tags.rb +++ b/actionpack/lib/action_view/helpers/tags.rb @@ -1,28 +1,33 @@ module ActionView module Helpers - module Tags - autoload :Base, 'action_view/helpers/tags/base' - autoload :Label, 'action_view/helpers/tags/label' - autoload :TextField, 'action_view/helpers/tags/text_field' - autoload :PasswordField, 'action_view/helpers/tags/password_field' - autoload :HiddenField, 'action_view/helpers/tags/hidden_field' - autoload :FileField, 'action_view/helpers/tags/file_field' - autoload :SearchField, 'action_view/helpers/tags/search_field' - autoload :TelField, 'action_view/helpers/tags/tel_field' - autoload :UrlField, 'action_view/helpers/tags/url_field' - autoload :EmailField, 'action_view/helpers/tags/email_field' - autoload :NumberField, 'action_view/helpers/tags/number_field' - autoload :RangeField, 'action_view/helpers/tags/range_field' - autoload :TextArea, 'action_view/helpers/tags/text_area' - autoload :CheckBox, 'action_view/helpers/tags/check_box' - autoload :RadioButton, 'action_view/helpers/tags/radio_button' - autoload :Select, 'action_view/helpers/tags/select' - autoload :CollectionSelect, 'action_view/helpers/tags/collection_select' - autoload :GroupedCollectionSelect, 'action_view/helpers/tags/grouped_collection_select' - autoload :TimeZoneSelect, 'action_view/helpers/tags/time_zone_select' - autoload :DateSelect, 'action_view/helpers/tags/date_select' - autoload :TimeSelect, 'action_view/helpers/tags/time_select' - autoload :DatetimeSelect, 'action_view/helpers/tags/datetime_select' + module Tags #:nodoc: + extend ActiveSupport::Autoload + + autoload :Base + autoload :CheckBox + autoload :CollectionCheckBoxes + autoload :CollectionRadioButtons + autoload :CollectionSelect + autoload :DateField + autoload :DateSelect + autoload :DatetimeSelect + autoload :EmailField + autoload :FileField + autoload :GroupedCollectionSelect + autoload :HiddenField + autoload :Label + autoload :NumberField + autoload :PasswordField + autoload :RadioButton + autoload :RangeField + autoload :SearchField + autoload :Select + autoload :TelField + autoload :TextArea + autoload :TextField + autoload :TimeSelect + autoload :TimeZoneSelect + autoload :UrlField end end end diff --git a/actionpack/lib/action_view/helpers/tags/base.rb b/actionpack/lib/action_view/helpers/tags/base.rb index 24956beb9c..1ece0ad2fc 100644 --- a/actionpack/lib/action_view/helpers/tags/base.rb +++ b/actionpack/lib/action_view/helpers/tags/base.rb @@ -19,8 +19,9 @@ module ActionView @auto_index = retrieve_autoindex(Regexp.last_match.pre_match) if Regexp.last_match end - def render(&block) - raise "Abstract Method called" + # This is what child classes implement. + def render + raise NotImplementedError, "Subclasses must implement a render method" end private @@ -31,9 +32,11 @@ module ActionView def value_before_type_cast(object) unless object.nil? - object.respond_to?(@method_name + "_before_type_cast") ? - object.send(@method_name + "_before_type_cast") : - object.send(@method_name) + method_before_type_cast = @method_name + "_before_type_cast" + + object.respond_to?(method_before_type_cast) ? + object.send(method_before_type_cast) : + value(object) end end @@ -58,13 +61,15 @@ module ActionView end def add_default_name_and_id_for_value(tag_value, options) - unless tag_value.nil? - pretty_tag_value = tag_value.to_s.gsub(/\s/, "_").gsub(/[^-\w]/, "").downcase - specified_id = options["id"] + if tag_value.nil? add_default_name_and_id(options) - options["id"] += "_#{pretty_tag_value}" if specified_id.blank? && options["id"].present? else + specified_id = options["id"] add_default_name_and_id(options) + + if specified_id.blank? && options["id"].present? + options["id"] += "_#{sanitized_value(tag_value)}" + end end end @@ -77,7 +82,7 @@ module ActionView options["name"] ||= tag_name_with_index(@auto_index) options["id"] = options.fetch("id"){ tag_id_with_index(@auto_index) } else - options["name"] ||= tag_name + (options['multiple'] ? '[]' : '') + options["name"] ||= options['multiple'] ? tag_name_multiple : tag_name options["id"] = options.fetch("id"){ tag_id } end options["id"] = [options.delete('namespace'), options["id"]].compact.join("_").presence @@ -87,6 +92,10 @@ module ActionView "#{@object_name}[#{sanitized_method_name}]" end + def tag_name_multiple + "#{tag_name}[]" + end + def tag_name_with_index(index) "#{@object_name}[#{index}][#{sanitized_method_name}]" end @@ -107,6 +116,10 @@ module ActionView @sanitized_method_name ||= @method_name.sub(/\?$/,"") end + def sanitized_value(value) + value.to_s.gsub(/\s/, "_").gsub(/[^-\w]/, "").downcase + end + def select_content_tag(option_tags, options, html_options) html_options = html_options.stringify_keys add_default_name_and_id(html_options) diff --git a/actionpack/lib/action_view/helpers/tags/check_box.rb b/actionpack/lib/action_view/helpers/tags/check_box.rb index b3bd6eb2ad..579cdb9fc9 100644 --- a/actionpack/lib/action_view/helpers/tags/check_box.rb +++ b/actionpack/lib/action_view/helpers/tags/check_box.rb @@ -25,7 +25,7 @@ module ActionView add_default_name_and_id(options) end - hidden = @unchecked_value ? tag("input", "name" => options["name"], "type" => "hidden", "value" => @unchecked_value, "disabled" => options["disabled"]) : "" + hidden = hidden_field_for_checkbox(options) checkbox = tag("input", options) hidden + checkbox end @@ -48,6 +48,10 @@ module ActionView value.to_i != 0 end end + + def hidden_field_for_checkbox(options) + @unchecked_value ? tag("input", options.slice("name", "disabled", "form").merge!("type" => "hidden", "value" => @unchecked_value)) : "".html_safe + end end end end diff --git a/actionpack/lib/action_view/helpers/tags/collection_check_boxes.rb b/actionpack/lib/action_view/helpers/tags/collection_check_boxes.rb new file mode 100644 index 0000000000..5f1e9ec026 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/collection_check_boxes.rb @@ -0,0 +1,37 @@ +require 'action_view/helpers/tags/collection_helpers' + +module ActionView + module Helpers + module Tags + class CollectionCheckBoxes < Base + include CollectionHelpers + + class CheckBoxBuilder < Builder + def check_box(extra_html_options={}) + html_options = extra_html_options.merge(@input_html_options) + @template_object.check_box(@object_name, @method_name, html_options, @value, nil) + end + end + + def render + rendered_collection = render_collection do |value, text, default_html_options| + default_html_options[:multiple] = true + builder = instantiate_builder(CheckBoxBuilder, value, text, default_html_options) + + if block_given? + yield builder + else + builder.check_box + builder.label + end + end + + # Append a hidden field to make sure something will be sent back to the + # server if all check boxes are unchecked. + hidden = @template_object.hidden_field_tag(tag_name_multiple, "", :id => nil) + + rendered_collection + hidden + end + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/collection_helpers.rb b/actionpack/lib/action_view/helpers/tags/collection_helpers.rb new file mode 100644 index 0000000000..1e2e77dde1 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/collection_helpers.rb @@ -0,0 +1,80 @@ +module ActionView + module Helpers + module Tags + module CollectionHelpers + class Builder + attr_reader :text, :value + + def initialize(template_object, object_name, method_name, + sanitized_attribute_name, text, value, input_html_options) + @template_object = template_object + @object_name = object_name + @method_name = method_name + @sanitized_attribute_name = sanitized_attribute_name + @text = text + @value = value + @input_html_options = input_html_options + end + + def label(label_html_options={}, &block) + @template_object.label(@object_name, @sanitized_attribute_name, @text, label_html_options, &block) + end + end + + def initialize(object_name, method_name, template_object, collection, value_method, text_method, options, html_options) + @collection = collection + @value_method = value_method + @text_method = text_method + @html_options = html_options + + super(object_name, method_name, template_object, options) + end + + private + + def instantiate_builder(builder_class, value, text, html_options) + builder_class.new(@template_object, @object_name, @method_name, + sanitize_attribute_name(value), text, value, html_options) + end + + # Generate default options for collection helpers, such as :checked and + # :disabled. + def default_html_options_for_collection(item, value) #:nodoc: + html_options = @html_options.dup + + [:checked, :selected, :disabled].each do |option| + next unless current_value = @options[option] + + accept = if current_value.respond_to?(:call) + current_value.call(item) + else + Array(current_value).include?(value) + end + + if accept + html_options[option] = true + elsif option == :checked + html_options[option] = false + end + end + + html_options + end + + def sanitize_attribute_name(value) #:nodoc: + "#{sanitized_method_name}_#{sanitized_value(value)}" + end + + def render_collection #:nodoc: + @collection.map do |item| + value = value_for_collection(item, @value_method) + text = value_for_collection(item, @text_method) + default_html_options = default_html_options_for_collection(item, value) + + yield value, text, default_html_options + end.join.html_safe + end + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/collection_radio_buttons.rb b/actionpack/lib/action_view/helpers/tags/collection_radio_buttons.rb new file mode 100644 index 0000000000..8e7aeeed63 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/collection_radio_buttons.rb @@ -0,0 +1,30 @@ +require 'action_view/helpers/tags/collection_helpers' + +module ActionView + module Helpers + module Tags + class CollectionRadioButtons < Base + include CollectionHelpers + + class RadioButtonBuilder < Builder + def radio_button(extra_html_options={}) + html_options = extra_html_options.merge(@input_html_options) + @template_object.radio_button(@object_name, @method_name, @value, html_options) + end + end + + def render + render_collection do |value, text, default_html_options| + builder = instantiate_builder(RadioButtonBuilder, value, text, default_html_options) + + if block_given? + yield builder + else + builder.radio_button + builder.label + end + end + end + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/collection_select.rb b/actionpack/lib/action_view/helpers/tags/collection_select.rb index f84140d8d0..ec78e6e5f9 100644 --- a/actionpack/lib/action_view/helpers/tags/collection_select.rb +++ b/actionpack/lib/action_view/helpers/tags/collection_select.rb @@ -12,9 +12,14 @@ module ActionView end def render - selected_value = @options.has_key?(:selected) ? @options[:selected] : value(@object) + option_tags_options = { + :selected => @options.fetch(:selected) { value(@object) }, + :disabled => @options[:disabled] + } + select_content_tag( - options_from_collection_for_select(@collection, @value_method, @text_method, :selected => selected_value, :disabled => @options[:disabled]), @options, @html_options + options_from_collection_for_select(@collection, @value_method, @text_method, option_tags_options), + @options, @html_options ) end end diff --git a/actionpack/lib/action_view/helpers/tags/date_field.rb b/actionpack/lib/action_view/helpers/tags/date_field.rb new file mode 100644 index 0000000000..bb968e9f39 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/date_field.rb @@ -0,0 +1,15 @@ +module ActionView + module Helpers + module Tags + class DateField < TextField #:nodoc: + def render + options = @options.stringify_keys + options["value"] = @options.fetch("value") { value(object).try(:to_date) } + options["size"] = nil + @options = options + super + end + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/date_select.rb b/actionpack/lib/action_view/helpers/tags/date_select.rb index 5912598ca1..5d706087b0 100644 --- a/actionpack/lib/action_view/helpers/tags/date_select.rb +++ b/actionpack/lib/action_view/helpers/tags/date_select.rb @@ -12,10 +12,16 @@ module ActionView error_wrapping(datetime_selector(@options, @html_options).send("select_#{select_type}").html_safe) end + class << self + def select_type + @select_type ||= self.name.split("::").last.sub("Select", "").downcase + end + end + private def select_type - self.class.name.split("::").last.sub("Select", "").downcase + self.class.select_type end def datetime_selector(options, html_options) diff --git a/actionpack/lib/action_view/helpers/tags/label.rb b/actionpack/lib/action_view/helpers/tags/label.rb index 74ac92ee18..1bd71c2778 100644 --- a/actionpack/lib/action_view/helpers/tags/label.rb +++ b/actionpack/lib/action_view/helpers/tags/label.rb @@ -30,7 +30,7 @@ module ActionView add_default_name_and_id_for_value(tag_value, name_and_id) options.delete("index") options.delete("namespace") - options["for"] ||= name_and_id["id"] + options["for"] = name_and_id["id"] unless options.key?("for") if block_given? @template_object.label_tag(name_and_id["id"], options, &block) diff --git a/actionpack/lib/action_view/helpers/tags/select.rb b/actionpack/lib/action_view/helpers/tags/select.rb index 71fd4d04b7..53a108b7e6 100644 --- a/actionpack/lib/action_view/helpers/tags/select.rb +++ b/actionpack/lib/action_view/helpers/tags/select.rb @@ -4,27 +4,37 @@ module ActionView class Select < Base #:nodoc: def initialize(object_name, method_name, template_object, choices, options, html_options) @choices = choices + @choices = @choices.to_a if @choices.is_a?(Range) @html_options = html_options super(object_name, method_name, template_object, options) end def render - selected_value = @options.has_key?(:selected) ? @options[:selected] : value(@object) + option_tags_options = { + :selected => @options.fetch(:selected) { value(@object) }, + :disabled => @options[:disabled] + } - # Grouped choices look like this: - # - # [nil, []] - # { nil => [] } - # - if !@choices.empty? && @choices.first.respond_to?(:last) && Array === @choices.first.last - option_tags = grouped_options_for_select(@choices, :selected => selected_value, :disabled => @options[:disabled]) + option_tags = if grouped_choices? + grouped_options_for_select(@choices, option_tags_options) else - option_tags = options_for_select(@choices, :selected => selected_value, :disabled => @options[:disabled]) + options_for_select(@choices, option_tags_options) end select_content_tag(option_tags, @options, @html_options) end + + private + + # Grouped choices look like this: + # + # [nil, []] + # { nil => [] } + # + def grouped_choices? + !@choices.empty? && @choices.first.respond_to?(:last) && Array === @choices.first.last + end end end end diff --git a/actionpack/lib/action_view/helpers/tags/text_field.rb b/actionpack/lib/action_view/helpers/tags/text_field.rb index 0f81726eb4..ce5182d20f 100644 --- a/actionpack/lib/action_view/helpers/tags/text_field.rb +++ b/actionpack/lib/action_view/helpers/tags/text_field.rb @@ -13,10 +13,16 @@ module ActionView tag("input", options) end + class << self + def field_type + @field_type ||= self.name.split("::").last.sub("Field", "").downcase + end + end + private def field_type - @field_type ||= self.class.name.split("::").last.sub("Field", "").downcase + self.class.field_type end end end diff --git a/actionpack/lib/action_view/helpers/text_helper.rb b/actionpack/lib/action_view/helpers/text_helper.rb index ce79a3da48..3dc651501e 100644 --- a/actionpack/lib/action_view/helpers/text_helper.rb +++ b/actionpack/lib/action_view/helpers/text_helper.rb @@ -90,11 +90,11 @@ 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 \1 where the phrase is to be inserted (defaults to - # '<strong class="highlight">\1</strong>') + # '<mark>\1</mark>') # # ==== Examples # highlight('You searched for: rails', 'rails') - # # => You searched for: <strong class="highlight">rails</strong> + # # => You searched for: <mark>rails</mark> # # highlight('You searched for: ruby, rails, dhh', 'actionpack') # # => You searched for: ruby, rails, dhh @@ -111,9 +111,9 @@ module ActionView def highlight(text, phrases, *args) options = args.extract_options! unless args.empty? - options[:highlighter] = args[0] || '<strong class="highlight">\1</strong>' + options[:highlighter] = args[0] || '<mark>\1</mark>' end - options.reverse_merge!(:highlighter => '<strong class="highlight">\1</strong>') + options.reverse_merge!(:highlighter => '<mark>\1</mark>') text = sanitize(text) unless options[:sanitize] == false if text.blank? || phrases.blank? diff --git a/actionpack/lib/action_view/helpers/url_helper.rb b/actionpack/lib/action_view/helpers/url_helper.rb index ebd1f280a8..b5fc882e31 100644 --- a/actionpack/lib/action_view/helpers/url_helper.rb +++ b/actionpack/lib/action_view/helpers/url_helper.rb @@ -323,33 +323,24 @@ module ActionView # # def button_to(name, options = {}, html_options = {}) html_options = html_options.stringify_keys - convert_boolean_attributes!(html_options, %w( disabled )) + convert_boolean_attributes!(html_options, %w(disabled)) - method_tag = '' - if (method = html_options.delete('method')) && %w{put delete}.include?(method.to_s) - method_tag = tag('input', :type => 'hidden', :name => '_method', :value => method.to_s) - end + url = options.is_a?(String) ? options : url_for(options) + remote = html_options.delete('remote') + + method = html_options.delete('method').to_s + method_tag = %w{put delete}.include?(method) ? method_tag(method) : "" - form_method = method.to_s == 'get' ? 'get' : 'post' + 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 - remote = html_options.delete('remote') - - request_token_tag = '' - if form_method == 'post' && protect_against_forgery? - request_token_tag = tag(:input, :type => "hidden", :name => request_forgery_protection_token.to_s, :value => form_authenticity_token) - end - - url = options.is_a?(String) ? options : self.url_for(options) - name ||= url + request_token_tag = form_method == 'post' ? token_tag : '' html_options = convert_options_to_data_attributes(options, html_options) - - html_options.merge!("type" => "submit", "value" => name) - - form_options.merge!(:method => form_method, :action => url) - form_options.merge!("data-remote" => "true") if remote + html_options.merge!("type" => "submit", "value" => name || url) "#{tag(:form, form_options, true)}<div>#{method_tag}#{tag("input", html_options)}#{request_token_tag}</div></form>".html_safe end @@ -476,7 +467,7 @@ module ActionView # string given as the value. # * <tt>:subject</tt> - Preset the subject line of the email. # * <tt>:body</tt> - Preset the body of the email. - # * <tt>:cc</tt> - Carbon Copy addition recipients on the email. + # * <tt>:cc</tt> - Carbon Copy additional recipients on the email. # * <tt>:bcc</tt> - Blind Carbon Copy additional recipients on the email. # # ==== Examples @@ -599,11 +590,7 @@ module ActionView # We ignore any extra parameters in the request_uri if the # submitted url doesn't have any either. This lets the function # work with things like ?order=asc - if url_string.index("?") - request_uri = request.fullpath - else - request_uri = request.path - end + request_uri = url_string.index("?") ? request.fullpath : request.path if url_string =~ /^\w+:\/\// url_string == "#{request.protocol}#{request.host_with_port}#{request_uri}" @@ -633,12 +620,12 @@ module ActionView end def link_to_remote_options?(options) - options.is_a?(Hash) && options.key?('remote') && options.delete('remote') + options.is_a?(Hash) && options.delete('remote') end def add_method_to_attributes!(html_options, method) if method && method.to_s.downcase != "get" && html_options["rel"] !~ /nofollow/ - html_options["rel"] = "#{html_options["rel"]} nofollow".strip + html_options["rel"] = "#{html_options["rel"]} nofollow".lstrip end html_options["data-method"] = method end @@ -670,6 +657,19 @@ module ActionView 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? + '' + else + token ||= form_authenticity_token + tag(:input, :type => "hidden", :name => request_forgery_protection_token.to_s, :value => token) + end + end + + def method_tag(method) + tag('input', :type => 'hidden', :name => '_method', :value => method.to_s) + end end end end diff --git a/actionpack/lib/action_view/locale/en.yml b/actionpack/lib/action_view/locale/en.yml index f2a83b92a9..7cca7d969a 100644 --- a/actionpack/lib/action_view/locale/en.yml +++ b/actionpack/lib/action_view/locale/en.yml @@ -37,6 +37,7 @@ # precision: # significant: false # strip_insignificant_zeros: false + format: "%n%" # Used in number_to_precision() precision: diff --git a/actionpack/lib/action_view/renderer/partial_renderer.rb b/actionpack/lib/action_view/renderer/partial_renderer.rb index e231aade01..3033294883 100644 --- a/actionpack/lib/action_view/renderer/partial_renderer.rb +++ b/actionpack/lib/action_view/renderer/partial_renderer.rb @@ -85,8 +85,7 @@ module ActionView # == Rendering objects that respond to `to_partial_path` # # Instead of explicitly naming the location of a partial, you can also let PartialRenderer do the work - # and pick the proper path by checking `to_proper_path` method. If the object passed to render is a collection, - # all objects must return the same path. + # and pick the proper path by checking `to_partial_path` method. # # # @account.to_partial_path returns 'accounts/account', so it can be used to replace: # # <%= render :partial => "accounts/account", :locals => { :account => @account} %> diff --git a/actionpack/lib/action_view/template.rb b/actionpack/lib/action_view/template.rb index 593eaa2abf..edb3d427d5 100644 --- a/actionpack/lib/action_view/template.rb +++ b/actionpack/lib/action_view/template.rb @@ -288,7 +288,7 @@ module ActionView logger.debug "Backtrace: #{e.backtrace.join("\n")}" end - raise ActionView::Template::Error.new(self, {}, e) + raise ActionView::Template::Error.new(self, e) end end @@ -297,13 +297,12 @@ module ActionView e.sub_template_of(self) raise e else - assigns = view.respond_to?(:assigns) ? view.assigns : {} template = self unless template.source template = refresh(view) template.encode! end - raise Template::Error.new(template, assigns, e) + raise Template::Error.new(template, e) end end diff --git a/actionpack/lib/action_view/template/error.rb b/actionpack/lib/action_view/template/error.rb index 83df2604bb..d8258f7b11 100644 --- a/actionpack/lib/action_view/template/error.rb +++ b/actionpack/lib/action_view/template/error.rb @@ -55,9 +55,9 @@ module ActionView attr_reader :original_exception, :backtrace - def initialize(template, assigns, original_exception) + def initialize(template, original_exception) super(original_exception.message) - @template, @assigns, @original_exception = template, assigns.dup, original_exception + @template, @original_exception = template, original_exception @sub_templates = nil @backtrace = original_exception.backtrace end diff --git a/actionpack/lib/action_view/template/handlers.rb b/actionpack/lib/action_view/template/handlers.rb index aa693335e3..67978ada7e 100644 --- a/actionpack/lib/action_view/template/handlers.rb +++ b/actionpack/lib/action_view/template/handlers.rb @@ -17,15 +17,12 @@ module ActionView #:nodoc: @@template_extensions ||= @@template_handlers.keys end - # Register a class that knows how to handle template files with the given + # Register an object that knows how to handle template files with the given # extension. This can be used to implement new template types. - # The constructor for the class must take the ActiveView::Base instance - # as a parameter, and the class must implement a +render+ method that - # takes the contents of the template to render as well as the Hash of - # local assigns available to the template. The +render+ method ought to - # return the rendered template as a string. - def register_template_handler(extension, klass) - @@template_handlers[extension.to_sym] = klass + # The handler must respond to `:call`, which will be passed the template + # and should return the rendered template as a String. + def register_template_handler(extension, handler) + @@template_handlers[extension.to_sym] = handler end def template_handler_extensions diff --git a/actionpack/lib/action_view/test_case.rb b/actionpack/lib/action_view/test_case.rb index b00f69e636..fece499c94 100644 --- a/actionpack/lib/action_view/test_case.rb +++ b/actionpack/lib/action_view/test_case.rb @@ -59,8 +59,10 @@ module ActionView end def determine_default_helper_class(name) - mod = name.sub(/Test$/, '').safe_constantize + mod = name.sub(/Test$/, '').constantize mod.is_a?(Class) ? nil : mod + rescue NameError + nil end def helper_method(*methods) diff --git a/actionpack/lib/sprockets/assets.rake b/actionpack/lib/sprockets/assets.rake index f3547359cd..2e92fe416b 100644 --- a/actionpack/lib/sprockets/assets.rake +++ b/actionpack/lib/sprockets/assets.rake @@ -6,7 +6,11 @@ namespace :assets do groups = ENV['RAILS_GROUPS'] || 'assets' args = [$0, task,"RAILS_ENV=#{env}","RAILS_GROUPS=#{groups}"] args << "--trace" if Rake.application.options.trace - fork ? ruby(*args) : Kernel.exec(FileUtils::RUBY, *args) + if $0 =~ /rake\.bat\Z/i + Kernel.exec $0, *args + else + fork ? ruby(*args) : Kernel.exec(FileUtils::RUBY, *args) + end end # We are currently running with no explicit bundler group diff --git a/actionpack/lib/sprockets/compressors.rb b/actionpack/lib/sprockets/compressors.rb index cb3e13314b..8b728d6570 100644 --- a/actionpack/lib/sprockets/compressors.rb +++ b/actionpack/lib/sprockets/compressors.rb @@ -1,38 +1,28 @@ module Sprockets module Compressors + extend self + @@css_compressors = {} @@js_compressors = {} @@default_css_compressor = nil @@default_js_compressor = nil - def self.register_css_compressor(name, klass, options = {}) + def register_css_compressor(name, klass, options = {}) @@default_css_compressor = name.to_sym if options[:default] || @@default_css_compressor.nil? - @@css_compressors[name.to_sym] = {:klass => klass.to_s, :require => options[:require]} + @@css_compressors[name.to_sym] = { :klass => klass.to_s, :require => options[:require] } end - def self.register_js_compressor(name, klass, options = {}) + def register_js_compressor(name, klass, options = {}) @@default_js_compressor = name.to_sym if options[:default] || @@default_js_compressor.nil? - @@js_compressors[name.to_sym] = {:klass => klass.to_s, :require => options[:require]} + @@js_compressors[name.to_sym] = { :klass => klass.to_s, :require => options[:require] } end - def self.registered_css_compressor(name) - if name.respond_to?(:to_sym) - compressor = @@css_compressors[name.to_sym] || @@css_compressors[@@default_css_compressor] - require compressor[:require] if compressor[:require] - compressor[:klass].constantize.new - else - name - end + def registered_css_compressor(name) + find_registered_compressor name, @@css_compressors, @@default_css_compressor end - def self.registered_js_compressor(name) - if name.respond_to?(:to_sym) - compressor = @@js_compressors[name.to_sym] || @@js_compressors[@@default_js_compressor] - require compressor[:require] if compressor[:require] - compressor[:klass].constantize.new - else - name - end + def registered_js_compressor(name) + find_registered_compressor name, @@js_compressors, @@default_js_compressor end # The default compressors must be registered in default plugins (ex. Sass-Rails) @@ -43,6 +33,18 @@ module Sprockets register_css_compressor(:yui, 'YUI::CssCompressor', :require => 'yui/compressor') register_js_compressor(:closure, 'Closure::Compiler', :require => 'closure-compiler') register_js_compressor(:yui, 'YUI::JavaScriptCompressor', :require => 'yui/compressor') + + private + + def find_registered_compressor(name, compressors_hash, default_compressor_name) + if name.respond_to?(:to_sym) + compressor = compressors_hash[name.to_sym] || compressors_hash[default_compressor_name] + require compressor[:require] if compressor[:require] + compressor[:klass].constantize.new + else + name + end + end end # An asset compressor which does nothing. diff --git a/actionpack/lib/sprockets/railtie.rb b/actionpack/lib/sprockets/railtie.rb index 44ddab0950..2bc482a39d 100644 --- a/actionpack/lib/sprockets/railtie.rb +++ b/actionpack/lib/sprockets/railtie.rb @@ -24,7 +24,7 @@ module Sprockets env.version = ::Rails.env + "-#{config.assets.version}" if config.assets.logger != false - env.logger = config.assets.logger || ::Rails.logger + env.logger = config.assets.logger || ::Rails.logger end if config.assets.cache_store != false diff --git a/actionpack/lib/sprockets/static_compiler.rb b/actionpack/lib/sprockets/static_compiler.rb index 719df0bd51..9bbb464474 100644 --- a/actionpack/lib/sprockets/static_compiler.rb +++ b/actionpack/lib/sprockets/static_compiler.rb @@ -8,8 +8,8 @@ module Sprockets @env = env @target = target @paths = paths - @digest = options.key?(:digest) ? options.delete(:digest) : true - @manifest = options.key?(:manifest) ? options.delete(:manifest) : true + @digest = options.fetch(:digest, true) + @manifest = options.fetch(:manifest, true) @manifest_path = options.delete(:manifest_path) || target @zip_files = options.delete(:zip_files) || /\.(?:css|html|js|svg|txt|xml)$/ end |