diff options
Diffstat (limited to 'actionpack/lib')
50 files changed, 982 insertions, 644 deletions
diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb index f875213afb..53cf6b3931 100644 --- a/actionpack/lib/abstract_controller/helpers.rb +++ b/actionpack/lib/abstract_controller/helpers.rb @@ -6,16 +6,10 @@ module AbstractController include Rendering - def self.next_serial - @helper_serial ||= 0 - @helper_serial += 1 - end - included do - class_attribute :_helpers, :_helper_serial + class_attribute :_helpers delegate :_helpers, :to => :'self.class' self._helpers = Module.new - self._helper_serial = ::AbstractController::Helpers.next_serial end module ClassMethods @@ -95,8 +89,6 @@ module AbstractController # helper(:three, BlindHelper) { def mice() 'mice' end } # def helper(*args, &block) - self._helper_serial = AbstractController::Helpers.next_serial + 1 - modules_for_helpers(args).each do |mod| add_template_helper(mod) end diff --git a/actionpack/lib/abstract_controller/layouts.rb b/actionpack/lib/abstract_controller/layouts.rb index 95a6101109..319472c937 100644 --- a/actionpack/lib/abstract_controller/layouts.rb +++ b/actionpack/lib/abstract_controller/layouts.rb @@ -1,3 +1,5 @@ +require "active_support/core_ext/module/remove_method" + module AbstractController # Layouts reverse the common pattern of including shared headers and footers in many templates to isolate changes in # repeated setups. The inclusion pattern has pages that look like this: @@ -182,7 +184,9 @@ module AbstractController # # ==== Returns # Boolean:: True if the action has a layout, false otherwise. - def _action_has_layout? + def action_has_layout? + return unless super + conditions = _layout_conditions if only = conditions[:only] @@ -237,6 +241,8 @@ module AbstractController # name, return that string. Otherwise, use the superclass' # layout (which might also be implied) def _write_layout_method + remove_possible_method(:_layout) + case defined?(@_layout) ? @_layout : nil when String self.class_eval %{def _layout; #{@_layout.inspect} end} @@ -284,15 +290,20 @@ module AbstractController layout = options.key?(:layout) ? options.delete(:layout) : :default value = _layout_for_option(layout) options[:layout] = (value =~ /\blayouts/ ? value : "layouts/#{value}") if value - - # TODO Layout for partials should be handled here, because inside the - # partial renderer it looks for the layout as a partial. - if options.key?(:partial) && options[:layout] - options[:layout] = view_context.find_layout(options[:layout]) - end end end + attr_writer :action_has_layout + + def initialize(*) + @action_has_layout = true + super + end + + def action_has_layout? + @action_has_layout + end + private # This will be overwritten by _write_layout_method @@ -332,13 +343,13 @@ module AbstractController # Template:: The template object for the default layout (or nil) def _default_layout(require_layout = false) begin - layout_name = _layout if _action_has_layout? + layout_name = _layout if action_has_layout? rescue NameError => e raise NoMethodError, "You specified #{@_layout.inspect} as the layout, but no such method was found" end - if require_layout && _action_has_layout? && !layout_name + if require_layout && action_has_layout? && !layout_name raise ArgumentError, "There was no default layout for #{self.class} in #{view_paths.inspect}" end @@ -349,9 +360,5 @@ module AbstractController def _include_layout?(options) (options.keys & [:text, :inline, :partial]).empty? || options.key?(:layout) end - - def _action_has_layout? - true - end end end diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index 16664098e5..b251bd6405 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -32,7 +32,6 @@ module AbstractController module Rendering extend ActiveSupport::Concern - include AbstractController::Assigns include AbstractController::ViewPaths # Overwrite process to setup I18n proxy. @@ -43,17 +42,48 @@ module AbstractController I18n.config = old_config end + module ClassMethods + def view_context_class + @view_context_class ||= begin + controller = self + Class.new(ActionView::Base) do + if controller.respond_to?(:_helpers) + include controller._helpers + + if controller.respond_to?(:_router) + include controller._router.url_helpers + end + + # TODO: Fix RJS to not require this + self.helpers = controller._helpers + end + end + end + end + end + + attr_writer :view_context_class + + def view_context_class + @view_context_class || self.class.view_context_class + end + + def initialize(*) + @view_context_class = nil + super + end + # An instance of a view class. The default view class is ActionView::Base # # The view class must have the following methods: - # View.for_controller[controller] + # View.new[lookup_context, assigns, controller] # Create a new ActionView instance for a controller - # View#render_template[options] + # View#render[options] # Returns String with the rendered template # # Override this method in a module to change the default behavior. def view_context - @_view_context ||= ActionView::Base.for_controller(self) + view_context_class.new(lookup_context, view_assigns, self) end # Normalize arguments, options and then delegates render_to_body and @@ -76,13 +106,12 @@ module AbstractController # :api: plugin def render_to_string(options={}) _normalize_options(options) - AbstractController::Rendering.body_to_s(render_to_body(options)) + render_to_body(options) end # Find and renders a template based on the options given. # :api: private def _render_template(options) #:nodoc: - _evaluate_assigns(view_context) view_context.render(options) end @@ -91,20 +120,19 @@ module AbstractController controller_path end - # Return a string representation of a Rack-compatible response body. - def self.body_to_s(body) - if body.respond_to?(:to_str) - body - else - strings = [] - body.each { |part| strings << part.to_s } - body.close if body.respond_to?(:close) - strings.join - end - end - private + # This method should return a hash with assigns. + # You can overwrite this configuration per controller. + # :api: public + def view_assigns + hash = {} + variables = instance_variable_names + variables -= protected_instance_variables if respond_to?(:protected_instance_variables) + variables.each { |name| hash[name.to_s[1..-1]] = instance_variable_get(name) } + hash + end + # Normalize options by converting render "foo" to render :action => "foo" and # render "foo/bar" to render :file => "foo/bar". def _normalize_args(action=nil, options={}) diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index ad2b68af21..5797282b41 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -19,6 +19,7 @@ module ActionController include SessionManagement include ActionController::Caching include ActionController::MimeResponds + include ActionController::PolymorphicRoutes # Rails 2.x compatibility include ActionController::Compatibility diff --git a/actionpack/lib/action_controller/caching/actions.rb b/actionpack/lib/action_controller/caching/actions.rb index 35111a4b92..43ddf6435a 100644 --- a/actionpack/lib/action_controller/caching/actions.rb +++ b/actionpack/lib/action_controller/caching/actions.rb @@ -80,6 +80,7 @@ module ActionController #:nodoc: def caches_action(*actions) return unless cache_configured? options = actions.extract_options! + options[:layout] = true unless options.key?(:layout) filter_options = options.extract!(:if, :unless).merge(:only => actions) cache_options = options.extract!(:layout, :cache_path).merge(:store_options => options) @@ -87,14 +88,12 @@ module ActionController #:nodoc: end end - def _render_cache_fragment(cache, extension, layout) - render :text => cache, :layout => layout, :content_type => Mime[extension || :html] - end - - def _save_fragment(name, layout, options) + def _save_fragment(name, options) return unless caching_allowed? - content = layout ? view_context.content_for(:layout) : response_body + content = response_body + content = content.join if content.is_a?(Array) + write_fragment(name, content, options) end @@ -112,7 +111,7 @@ module ActionController #:nodoc: class ActionCacheFilter #:nodoc: def initialize(options, &block) - @cache_path, @store_options, @layout = + @cache_path, @store_options, @cache_layout = options.values_at(:cache_path, :store_options, :layout) end @@ -125,12 +124,19 @@ module ActionController #:nodoc: cache_path = ActionCachePath.new(controller, path_options || {}) - if cache = controller.read_fragment(cache_path.path, @store_options) - controller._render_cache_fragment(cache, cache_path.extension, @layout == false) - else + body = controller.read_fragment(cache_path.path, @store_options) + + unless body + controller.action_has_layout = false unless @cache_layout yield - controller._save_fragment(cache_path.path, @layout == false, @store_options) + controller.action_has_layout = true + body = controller._save_fragment(cache_path.path, @store_options) end + + body = controller.render_to_string(:text => cache, :layout => true) unless @cache_layout + + controller.response_body = body + controller.content_type = Mime[cache_path.extension || :html] end end diff --git a/actionpack/lib/action_controller/caching/fragments.rb b/actionpack/lib/action_controller/caching/fragments.rb index 89787727bd..473a2fe214 100644 --- a/actionpack/lib/action_controller/caching/fragments.rb +++ b/actionpack/lib/action_controller/caching/fragments.rb @@ -34,20 +34,6 @@ module ActionController #:nodoc: ActiveSupport::Cache.expand_cache_key(key.is_a?(Hash) ? url_for(key).split("://").last : key, :views) end - def fragment_for(buffer, name = {}, options = nil, &block) #:nodoc: - if perform_caching - if fragment_exist?(name, options) - buffer.safe_concat(read_fragment(name, options)) - else - pos = buffer.length - block.call - write_fragment(name, buffer[pos..-1], options) - end - else - block.call - end - end - # Writes <tt>content</tt> to the location signified by <tt>key</tt> (see <tt>expire_fragment</tt> for acceptable formats) def write_fragment(key, content, options = nil) return content unless cache_configured? diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index eebd2c943a..30aa34d956 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -34,11 +34,12 @@ module ActionController # and response object available. You might wish to control the # environment and response manually for performance reasons. - attr_internal :status, :headers, :content_type, :response, :request + attr_internal :headers, :response, :request delegate :session, :to => "@_request" def initialize(*) - @_headers = {} + @_headers = {"Content-Type" => "text/html"} + @_status = 200 super end @@ -62,10 +63,19 @@ module ActionController headers["Location"] = url end + def status + @_status + end + def status=(status) @_status = Rack::Utils.status_code(status) end + def response_body=(val) + body = val.respond_to?(:each) ? val : [val] + super body + end + # :api: private def dispatch(name, request) @_request = request diff --git a/actionpack/lib/action_controller/metal/compatibility.rb b/actionpack/lib/action_controller/metal/compatibility.rb index ab8d87b2c4..e6cea483bb 100644 --- a/actionpack/lib/action_controller/metal/compatibility.rb +++ b/actionpack/lib/action_controller/metal/compatibility.rb @@ -40,15 +40,6 @@ module ActionController def initialize_template_class(*) end def assign_shortcuts(*) end - def template - @template ||= view_context - end - - def process_action(*) - template - super - end - def _normalize_options(options) if options[:action] && options[:action].to_s.include?(?/) ActiveSupport::Deprecation.warn "Giving a path to render :action is deprecated. " << diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 2ac199265d..fa69ab2038 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -263,6 +263,7 @@ module ActionController #:nodoc: if format = request.negotiate_mime(collector.order) self.content_type ||= format.to_s self.formats = [format.to_sym] + self.formats.freeze collector.response_for(format) else head :not_acceptable diff --git a/actionpack/lib/action_controller/metal/rack_delegation.rb b/actionpack/lib/action_controller/metal/rack_delegation.rb index 37106733cb..060117756e 100644 --- a/actionpack/lib/action_controller/metal/rack_delegation.rb +++ b/actionpack/lib/action_controller/metal/rack_delegation.rb @@ -5,10 +5,8 @@ module ActionController module RackDelegation extend ActiveSupport::Concern - included do - delegate :headers, :status=, :location=, :content_type=, - :status, :location, :content_type, :to => "@_response" - end + delegate :headers, :status=, :location=, :content_type=, + :status, :location, :content_type, :to => "@_response" def dispatch(action, request) @_response = ActionDispatch::Response.new diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index 25e4e18493..b5f1d23ef0 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -76,7 +76,7 @@ module ActionController # The scheme name consist of a letter followed by any combination of # letters, digits, and the plus ("+"), period ("."), or hyphen ("-") # characters; and is terminated by a colon (":"). - when %r{^\w[\w\d+.-]*:.*} + when %r{^\w[\w+.-]*:.*} options when String request.protocol + request.host_with_port + options diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index 08325b468c..d906e1fb5b 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -87,7 +87,7 @@ module ActionController end add :update do |proc, options| - _evaluate_assigns(view_context) + view_context = self.view_context generator = ActionView::Helpers::PrototypeHelper::JavaScriptGenerator.new(view_context, &proc) self.content_type = Mime::JS self.response_body = generator.to_s diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb index 6178a59029..0b2cee6868 100644 --- a/actionpack/lib/action_controller/metal/responder.rb +++ b/actionpack/lib/action_controller/metal/responder.rb @@ -1,3 +1,5 @@ +require 'active_support/json' + module ActionController #:nodoc: # Responder is responsible for exposing a resource to different mime requests, # usually depending on the HTTP verb. The responder is triggered when diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb index 6a3afbb157..0ec89928af 100644 --- a/actionpack/lib/action_controller/railtie.rb +++ b/actionpack/lib/action_controller/railtie.rb @@ -1,16 +1,17 @@ require "rails" require "action_controller" +require "action_dispatch/railtie" require "action_view/railtie" require "active_support/core_ext/class/subclasses" require "active_support/deprecation/proxy_wrappers" require "active_support/deprecation" +require "action_controller/railties/log_subscriber" +require "action_controller/railties/url_helpers" + module ActionController class Railtie < Rails::Railtie - railtie_name :action_controller - - require "action_controller/railties/log_subscriber" - require "action_controller/railties/url_helpers" + config.action_controller = ActiveSupport::OrderedOptions.new ad = config.action_dispatch config.action_controller.singleton_class.send(:define_method, :session) do @@ -37,7 +38,7 @@ module ActionController ad.session_store = val end - log_subscriber ActionController::Railties::LogSubscriber.new + log_subscriber :action_controller, ActionController::Railties::LogSubscriber.new initializer "action_controller.logger" do ActionController.base_hook { self.logger ||= Rails.logger } @@ -52,7 +53,9 @@ module ActionController ac.stylesheets_dir = paths.public.stylesheets.to_a.first ac.secret = app.config.cookie_secret - ActionController.base_hook { self.config.replace(ac) } + ActionController.base_hook do + self.config.merge!(ac) + end end initializer "action_controller.initialize_framework_caches" do @@ -67,7 +70,7 @@ module ActionController initializer "action_controller.url_helpers" do |app| ActionController.base_hook do - extend ::ActionController::Railtie::UrlHelpers.with(app.routes) + extend ::ActionController::Railties::UrlHelpers.with(app.routes) end message = "ActionController::Routing::Routes is deprecated. " \ diff --git a/actionpack/lib/action_controller/railties/url_helpers.rb b/actionpack/lib/action_controller/railties/url_helpers.rb index ad2a8d4ef3..5f95e1c621 100644 --- a/actionpack/lib/action_controller/railties/url_helpers.rb +++ b/actionpack/lib/action_controller/railties/url_helpers.rb @@ -1,5 +1,5 @@ module ActionController - class Railtie + module Railties module UrlHelpers def self.with(router) Module.new do diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index cdb5db32aa..120f34460e 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -1,7 +1,107 @@ require 'rack/session/abstract/id' -require 'action_view/test_case' module ActionController + module TemplateAssertions + extend ActiveSupport::Concern + + included do + setup :setup_subscriptions + teardown :teardown_subscriptions + end + + def setup_subscriptions + @partials = Hash.new(0) + @templates = Hash.new(0) + @layouts = Hash.new(0) + + ActiveSupport::Notifications.subscribe("action_view.render_template") do |name, start, finish, id, payload| + path = payload[:layout] + @layouts[path] += 1 + end + + ActiveSupport::Notifications.subscribe("action_view.render_template!") do |name, start, finish, id, payload| + path = payload[:virtual_path] + next unless path + partial = path =~ /^.*\/_[^\/]*$/ + if partial + @partials[path] += 1 + @partials[path.split("/").last] += 1 + @templates[path] += 1 + else + @templates[path] += 1 + end + end + end + + def teardown_subscriptions + ActiveSupport::Notifications.unsubscribe("action_view.render_template!") + end + + # Asserts that the request was rendered with the appropriate template file or partials + # + # ==== Examples + # + # # assert that the "new" view template was rendered + # assert_template "new" + # + # # assert that the "_customer" partial was rendered twice + # assert_template :partial => '_customer', :count => 2 + # + # # assert that no partials were rendered + # assert_template :partial => false + # + def assert_template(options = {}, message = nil) + validate_request! + + case options + when NilClass, String + rendered = @templates + msg = build_message(message, + "expecting <?> but rendering with <?>", + options, rendered.keys.join(', ')) + assert_block(msg) do + if options.nil? + @templates.blank? + else + rendered.any? { |t,num| t.match(options) } + end + end + when Hash + if expected_partial = options[:partial] + if expected_count = options[:count] + actual_count = @partials[expected_partial] + # actual_count = found.nil? ? 0 : found[1] + msg = build_message(message, + "expecting ? to be rendered ? time(s) but rendered ? time(s)", + expected_partial, expected_count, actual_count) + assert(actual_count == expected_count.to_i, msg) + elsif options.key?(:layout) + msg = build_message(message, + "expecting layout <?> but action rendered <?>", + expected_layout, @layouts.keys) + + case layout = options[:layout] + when String + assert(@layouts.include?(expected_layout), msg) + when Regexp + assert(@layouts.any? {|l| l =~ layout }, msg) + when nil + assert(@layouts.empty?, msg) + end + else + msg = build_message(message, + "expecting partial <?> but action rendered <?>", + options[:partial], @partials.keys) + assert(@partials.include?(expected_partial), msg) + end + else + assert @partials.empty?, + "Expected no partials to be rendered" + end + end + end + end + class TestRequest < ActionDispatch::TestRequest #:nodoc: def initialize(env = {}) super @@ -181,6 +281,7 @@ module ActionController # assert_redirected_to page_url(:title => 'foo') class TestCase < ActiveSupport::TestCase include ActionDispatch::TestProcess + include ActionController::TemplateAssertions # Executes a request simulating GET HTTP method and set/volley the response def get(action, parameters = nil, session = nil, flash = nil) diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb index 451b79b190..e42b4d09b0 100644 --- a/actionpack/lib/action_dispatch/http/filter_parameters.rb +++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb @@ -25,7 +25,6 @@ module ActionDispatch module FilterParameters extend ActiveSupport::Concern - mattr_reader :compiled_parameter_filter_for @@compiled_parameter_filter_for = {} # Return a hash of parameters with all sensitive data replaced. diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index 13c0f2bad0..3f1a77295d 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -52,12 +52,6 @@ module Mime cattr_reader :browser_generated_types attr_reader :symbol - @@unverifiable_types = Set.new [:text, :json, :csv, :xml, :rss, :atom, :yaml] - def self.unverifiable_types - ActiveSupport::Deprecation.warn("unverifiable_types is deprecated and has no effect", caller) - @@unverifiable_types - end - # A simple helper class used in parsing the accept header class AcceptItem #:nodoc: attr_accessor :order, :name, :q @@ -100,7 +94,7 @@ module Mime end def register(string, symbol, mime_type_synonyms = [], extension_synonyms = [], skip_lookup = false) - Mime.instance_eval { const_set symbol.to_s.upcase, Type.new(string, symbol, mime_type_synonyms) } + Mime.const_set(symbol.to_s.upcase, Type.new(string, symbol, mime_type_synonyms)) SET << Mime.const_get(symbol.to_s.upcase) diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 0dc03a1a7e..ab7130ab08 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -84,6 +84,7 @@ module ActionDispatch options[:path] ||= "/" @set_cookies[key] = options + @delete_cookies.delete(key) value end diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb index e486bd4079..430c90e515 100644 --- a/actionpack/lib/action_dispatch/railtie.rb +++ b/actionpack/lib/action_dispatch/railtie.rb @@ -3,8 +3,7 @@ require "rails" module ActionDispatch class Railtie < Rails::Railtie - railtie_name :action_dispatch - + config.action_dispatch = ActiveSupport::OrderedOptions.new config.action_dispatch.x_sendfile_header = "X-Sendfile" config.action_dispatch.ip_spoofing_check = true diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb index 5bc3205c51..c6e942555f 100644 --- a/actionpack/lib/action_dispatch/routing.rb +++ b/actionpack/lib/action_dispatch/routing.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/object/to_param' require 'active_support/core_ext/regexp' +require 'action_controller/polymorphic_routes' module ActionDispatch # == Routing diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index f9b27a5a03..278cf383ee 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -45,18 +45,21 @@ module ActionDispatch def extract_path_and_options(args) options = args.extract_options! - case - when using_to_shorthand?(args, options) + if using_to_shorthand?(args, options) path, to = options.find { |name, value| name.is_a?(String) } options.merge!(:to => to).delete(path) if path - when using_match_shorthand?(args, options) - path = args.first - options = { :to => path.gsub("/", "#"), :as => path.gsub("/", "_") } else path = args.first end - [ normalize_path(path), options ] + path = normalize_path(path) + + if using_match_shorthand?(path, options) + options[:to] ||= path[1..-1].sub(%r{/([^/]*)$}, '#\1') + options[:as] ||= path[1..-1].gsub("/", "_") + end + + [ path, options ] end # match "account" => "account#index" @@ -65,14 +68,13 @@ module ActionDispatch end # match "account/overview" - def using_match_shorthand?(args, options) - args.present? && options.except(:via, :anchor).empty? && !args.first.include?(':') + def using_match_shorthand?(path, options) + path && options.except(:via, :anchor, :to, :as).empty? && path =~ %r{^/[\w\/]+$} end def normalize_path(path) - path = "#{@scope[:path]}/#{path}" - raise ArgumentError, "path is required" if path.empty? - Mapper.normalize_path(path) + raise ArgumentError, "path is required" if @scope[:path].blank? && path.blank? + Mapper.normalize_path("#{@scope[:path]}/#{path}") end def app @@ -144,8 +146,8 @@ module ActionDispatch def segment_keys @segment_keys ||= Rack::Mount::RegexpWithNamedGroups.new( - Rack::Mount::Strexp.compile(@path, requirements, SEPARATORS) - ).names + Rack::Mount::Strexp.compile(@path, requirements, SEPARATORS) + ).names end def to @@ -301,7 +303,6 @@ module ActionDispatch options = args.extract_options! options = (@scope[:options] || {}).merge(options) - options[:anchor] = true unless options.key?(:anchor) if @scope[:name_prefix] && !options[:as].blank? options[:as] = "#{@scope[:name_prefix]}_#{options[:as]}" @@ -563,6 +564,8 @@ module ActionDispatch def match(*args) options = args.extract_options! + options[:anchor] = true unless options.key?(:anchor) + if args.length > 1 args.each { |path| match(path, options) } return self diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 722be432c7..bb689beed9 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -65,7 +65,7 @@ module ActionDispatch # named routes. class NamedRouteCollection #:nodoc: include Enumerable - attr_reader :routes, :helpers + attr_reader :routes, :helpers, :module def initialize clear! @@ -179,6 +179,7 @@ module ActionDispatch url_for(options) end + protected :#{selector} END_EVAL helpers << selector end @@ -219,14 +220,16 @@ module ActionDispatch end def finalize! + return if @finalized + @finalized = true @set.add_route(NotFound) - install_helpers @set.freeze end def clear! # Clear the controller cache so we may discover new ones @controller_constraints = nil + @finalized = false routes.clear named_routes.clear @set = ::Rack::Mount::RouteSet.new(:parameters_key => PARAMETERS_KEY) @@ -239,21 +242,30 @@ module ActionDispatch def url_helpers @url_helpers ||= begin - router = self + routes = self - Module.new do + helpers = Module.new do extend ActiveSupport::Concern include UrlFor + @routes = routes + class << self + delegate :url_for, :to => '@routes' + end + extend routes.named_routes.module + # ROUTES TODO: install_helpers isn't great... can we make a module with the stuff that # we can include? # Yes plz - JP included do - router.install_helpers(self) + routes.install_helpers(self) + singleton_class.send(:define_method, :_router) { routes } end - define_method(:_router) { router } + define_method(:_router) { routes } end + + helpers end end @@ -406,6 +418,7 @@ module ActionDispatch RESERVED_OPTIONS = [:anchor, :params, :only_path, :host, :protocol, :port, :trailing_slash] def url_for(options) + finalize! options = default_url_options.merge(options || {}) handle_positional_args(options) @@ -437,6 +450,7 @@ module ActionDispatch end def call(env) + finalize! @set.call(env) end diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb index 937c9f48d2..ec5e9efe44 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/response.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb @@ -73,58 +73,6 @@ module ActionDispatch end end - # Asserts that the request was rendered with the appropriate template file or partials - # - # ==== Examples - # - # # assert that the "new" view template was rendered - # assert_template "new" - # - # # assert that the "_customer" partial was rendered twice - # assert_template :partial => '_customer', :count => 2 - # - # # assert that no partials were rendered - # assert_template :partial => false - # - def assert_template(options = {}, message = nil) - validate_request! - - case options - when NilClass, String - rendered = (@controller.template.rendered[:template] || []).map { |t| t.identifier } - msg = build_message(message, - "expecting <?> but rendering with <?>", - options, rendered.join(', ')) - assert_block(msg) do - if options.nil? - @controller.template.rendered[:template].blank? - else - rendered.any? { |t| t.match(options) } - end - end - when Hash - if expected_partial = options[:partial] - partials = @controller.template.rendered[:partials] - if expected_count = options[:count] - found = partials.detect { |p, _| p.identifier.match(expected_partial) } - actual_count = found.nil? ? 0 : found.second - msg = build_message(message, - "expecting ? to be rendered ? time(s) but rendered ? time(s)", - expected_partial, expected_count, actual_count) - assert(actual_count == expected_count.to_i, msg) - else - msg = build_message(message, - "expecting partial <?> but action rendered <?>", - options[:partial], partials.keys) - assert(partials.keys.any? { |p| p.identifier.match(expected_partial) }, msg) - end - else - assert @controller.template.rendered[:partials].empty?, - "Expected no partials to be rendered" - end - end - end - private # Proxy to to_param if the object will respond to it. def parameterize(value) diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb index 1d7e8090e4..1bb81ede3b 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb @@ -145,11 +145,25 @@ module ActionDispatch old_routes, @router = @router, ActionDispatch::Routing::RouteSet.new old_controller, @controller = @controller, @controller.clone if @controller _router = @router - @controller.singleton_class.send(:send, :include, @router.url_helpers) if @controller + + # Unfortunately, there is currently an abstraction leak between AC::Base + # and AV::Base which requires having the URL helpers in both AC and AV. + # To do this safely at runtime for tests, we need to bump up the helper serial + # to that the old AV subclass isn't cached. + # + # TODO: Make this unnecessary + if @controller + @controller.singleton_class.send(:include, _router.url_helpers) + @controller.view_context_class = Class.new(@controller.view_context_class) do + include _router.url_helpers + end + end yield @router ensure @router = old_routes - @controller = old_controller if @controller + if @controller + @controller = old_controller + end end # ROUTES TODO: These assertions should really work in an integration context diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 0aff4250c1..621d63c5e2 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -2,6 +2,7 @@ require 'stringio' require 'uri' require 'active_support/core_ext/object/singleton_class' require 'rack/test' +require 'test/unit/assertions' module ActionDispatch module Integration #:nodoc: @@ -177,14 +178,8 @@ module ActionDispatch reset! end - def url_options - opts = super.reverse_merge( - :host => host, - :protocol => https? ? "https" : "http" - ) - - opts.merge!(:port => 443) if !opts.key?(:port) && https? - opts + def default_url_options + { :host => host, :protocol => https? ? "https" : "http" } end # Resets the instance. This can be used to reset the state information @@ -293,6 +288,8 @@ module ActionDispatch end module Runner + include ActionDispatch::Assertions + def app @app end @@ -300,7 +297,7 @@ module ActionDispatch # Reset the current session. This is useful for testing multiple sessions # in a single test case. def reset! - @integration_session = open_session + @integration_session = Integration::Session.new(app) end %w(get post put head delete cookies assigns @@ -326,30 +323,9 @@ module ActionDispatch # can use this method to open multiple sessions that ought to be tested # simultaneously. def open_session(app = nil) - session = Integration::Session.new(app || self.app) - - # delegate the fixture accessors back to the test instance - extras = Module.new { attr_accessor :delegate, :test_result } - if self.class.respond_to?(:fixture_table_names) - self.class.fixture_table_names.each do |table_name| - name = table_name.tr(".", "_") - next unless respond_to?(name) - extras.__send__(:define_method, name) { |*args| - delegate.send(name, *args) - } - end + dup.tap do |session| + yield session if block_given? end - - # delegate add_assertion to the test case - extras.__send__(:define_method, :add_assertion) { - test_result.add_assertion - } - session.extend(extras) - session.delegate = self - session.test_result = @_result - - yield session if block_given? - session end # Copy the instance variables from the current session instance into the @@ -460,6 +436,7 @@ module ActionDispatch # end class IntegrationTest < ActiveSupport::TestCase include Integration::Runner + include ActionController::TemplateAssertions @@app = nil diff --git a/actionpack/lib/action_view.rb b/actionpack/lib/action_view.rb index afe6386105..5555217ee2 100644 --- a/actionpack/lib/action_view.rb +++ b/actionpack/lib/action_view.rb @@ -44,13 +44,15 @@ module ActionView autoload :Base autoload :LookupContext - autoload :MissingTemplate, 'action_view/base' autoload :Resolver, 'action_view/template/resolver' autoload :PathResolver, 'action_view/template/resolver' autoload :FileSystemResolver, 'action_view/template/resolver' autoload :PathSet, 'action_view/paths' + autoload :MissingTemplate, 'action_view/template/error' + autoload :ActionViewError, 'action_view/template/error' autoload :TemplateError, 'action_view/template/error' + autoload :TemplateHandler, 'action_view/template' autoload :TemplateHandlers, 'action_view/template' end diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb index feaf45c333..919b1e3470 100644 --- a/actionpack/lib/action_view/base.rb +++ b/actionpack/lib/action_view/base.rb @@ -1,27 +1,10 @@ require 'active_support/core_ext/module/attr_internal' require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/class/attribute' +require 'active_support/core_ext/array/wrap' module ActionView #:nodoc: - class ActionViewError < StandardError #:nodoc: - end - - class MissingTemplate < ActionViewError #:nodoc: - attr_reader :path - - def initialize(paths, path, details, partial) - @path = path - display_paths = paths.compact.map{ |p| p.to_s.inspect }.join(", ") - template_type = if partial - "partial" - elsif path =~ /layouts/i - 'layout' - else - 'template' - end - - super("Missing #{template_type} #{path} with #{details.inspect} in view paths #{display_paths}") - end + class NonConcattingString < ActiveSupport::SafeBuffer end # Action View templates can be written in three ways. If the template file has a <tt>.erb</tt> (or <tt>.rhtml</tt>) extension then it uses a mixture of ERb @@ -176,14 +159,13 @@ module ActionView #:nodoc: include Helpers, Rendering, Partials, Layouts, ::ERB::Util, Context extend ActiveSupport::Memoizable - ActionView.run_base_hooks(self) - # Specify whether RJS responses should be wrapped in a try/catch block # that alert()s the caught exception (and then re-raises it). cattr_accessor :debug_rjs @@debug_rjs = false class_attribute :helpers + remove_method :helpers attr_reader :helpers class << self @@ -191,8 +173,10 @@ module ActionView #:nodoc: delegate :logger, :to => 'ActionController::Base', :allow_nil => true end + ActionView.run_base_hooks(self) + attr_accessor :base_path, :assigns, :template_extension, :lookup_context - attr_internal :captures, :request, :layout, :controller, :template, :config + attr_internal :captures, :request, :controller, :template, :config delegate :find_template, :template_exists?, :formats, :formats=, :locale, :locale=, :view_paths, :view_paths=, :with_fallbacks, :update_details, :to => :lookup_context @@ -202,42 +186,17 @@ module ActionView #:nodoc: delegate :logger, :to => :controller, :allow_nil => true + # TODO: HACK FOR RJS + def view_context + self + end + def self.xss_safe? #:nodoc: true end def self.process_view_paths(value) - ActionView::PathSet.new(Array(value)) - end - - def self.for_controller(controller) - @views ||= {} - - # TODO: Decouple this so helpers are a separate concern in AV just like - # they are in AC. - if controller.class.respond_to?(:_helper_serial) - klass = @views[controller.class._helper_serial] ||= Class.new(self) do - # Try to make stack traces clearer - class_eval <<-ruby_eval, __FILE__, __LINE__ + 1 - def self.name - "ActionView for #{controller.class}" - end - - def inspect - "#<#{self.class.name}>" - end - ruby_eval - - if controller.respond_to?(:_helpers) - include controller._helpers - self.helpers = controller._helpers - end - end - else - klass = self - end - - klass.new(controller.lookup_context, {}, controller) + ActionView::PathSet.new(Array.wrap(value)) end def initialize(lookup_context = nil, assigns_for_first_render = {}, controller = nil, formats = nil) #:nodoc: @@ -246,7 +205,7 @@ module ActionView #:nodoc: @helpers = self.class.helpers || Module.new @_controller = controller - @_config = ActiveSupport::InheritableOptions.new(controller.config) if controller + @_config = ActiveSupport::InheritableOptions.new(controller.config) if controller && controller.respond_to?(:config) @_content_for = Hash.new { |h,k| h[k] = ActiveSupport::SafeBuffer.new } @_virtual_path = nil diff --git a/actionpack/lib/action_view/context.rb b/actionpack/lib/action_view/context.rb index df078a7151..61d2e702a7 100644 --- a/actionpack/lib/action_view/context.rb +++ b/actionpack/lib/action_view/context.rb @@ -10,8 +10,6 @@ module ActionView # In order to work with ActionController, a Context # must implement: # - # Context.for_controller[controller] Create a new ActionView instance for a - # controller # Context#render_partial[options] # - responsible for setting options[:_template] # - Returns String with the rendered partial diff --git a/actionpack/lib/action_view/helpers/asset_tag_helper.rb b/actionpack/lib/action_view/helpers/asset_tag_helper.rb index b594002629..02ad41719b 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helper.rb @@ -3,6 +3,7 @@ require 'cgi' require 'action_view/helpers/url_helper' require 'action_view/helpers/tag_helper' require 'active_support/core_ext/file' +require 'active_support/core_ext/object/blank' module ActionView module Helpers #:nodoc: @@ -623,41 +624,37 @@ module ActionView @@cache_asset_timestamps = true private + def rewrite_extension?(source, dir, ext) + source_ext = File.extname(source)[1..-1] + ext && (source_ext.blank? || (ext != source_ext && File.exist?(File.join(config.assets_dir, dir, "#{source}.#{ext}")))) + end + + def rewrite_host_and_protocol(source, has_request) + host = compute_asset_host(source) + if has_request && host.present? && !is_uri?(host) + host = "#{controller.request.protocol}#{host}" + end + "#{host}#{source}" + end + # Add the the extension +ext+ if not present. Return full URLs otherwise untouched. # Prefix with <tt>/dir/</tt> if lacking a leading +/+. Account for relative URL # roots. Rewrite the asset path for cache-busting asset ids. Include # asset host, if configured, with the correct request protocol. def compute_public_path(source, dir, ext = nil, include_host = true) - has_request = controller.respond_to?(:request) - - source_ext = File.extname(source)[1..-1] - if ext && !is_uri?(source) && (source_ext.blank? || (ext != source_ext && File.exist?(File.join(config.assets_dir, dir, "#{source}.#{ext}")))) - source += ".#{ext}" - end + return source if is_uri?(source) - unless is_uri?(source) - source = "/#{dir}/#{source}" unless source[0] == ?/ + source += ".#{ext}" if rewrite_extension?(source, dir, ext) + source = "/#{dir}/#{source}" unless source[0] == ?/ + source = rewrite_asset_path(source) - source = rewrite_asset_path(source) - - if has_request && include_host - unless source =~ %r{^#{controller.config.relative_url_root}/} - source = "#{controller.config.relative_url_root}#{source}" - end - end + has_request = controller.respond_to?(:request) + if has_request && include_host && source !~ %r{^#{controller.config.relative_url_root}/} + source = "#{controller.config.relative_url_root}#{source}" end + source = rewrite_host_and_protocol(source, has_request) if include_host - if include_host && !is_uri?(source) - host = compute_asset_host(source) - - if has_request && !host.blank? && !is_uri?(host) - host = "#{controller.request.protocol}#{host}" - end - - "#{host}#{source}" - else - source - end + source end def is_uri?(path) diff --git a/actionpack/lib/action_view/helpers/cache_helper.rb b/actionpack/lib/action_view/helpers/cache_helper.rb index d5cc14b29a..a904af56bb 100644 --- a/actionpack/lib/action_view/helpers/cache_helper.rb +++ b/actionpack/lib/action_view/helpers/cache_helper.rb @@ -32,7 +32,28 @@ module ActionView # <i>Topics listed alphabetically</i> # <% end %> def cache(name = {}, options = nil, &block) - controller.fragment_for(output_buffer, name, options, &block) + safe_concat fragment_for(name, options, &block) + nil + end + + private + # TODO: Create an object that has caching read/write on it + def fragment_for(name = {}, options = nil, &block) #:nodoc: + if controller.perform_caching + if controller.fragment_exist?(name, options) + controller.read_fragment(name, options) + else + # VIEW TODO: Make #capture usable outside of ERB + # This dance is needed because Builder can't use capture + pos = output_buffer.length + yield + fragment = output_buffer.slice!(pos..-1) + controller.write_fragment(name, fragment, options) + end + else + ret = yield + ActiveSupport::SafeBuffer.new(ret) if ret.is_a?(String) + end end end end diff --git a/actionpack/lib/action_view/helpers/capture_helper.rb b/actionpack/lib/action_view/helpers/capture_helper.rb index 03c7ba5a87..f0be814700 100644 --- a/actionpack/lib/action_view/helpers/capture_helper.rb +++ b/actionpack/lib/action_view/helpers/capture_helper.rb @@ -2,22 +2,22 @@ module ActionView module Helpers # CaptureHelper exposes methods to let you extract generated markup which # can be used in other parts of a template or layout file. - # It provides a method to capture blocks into variables through capture and + # It provides a method to capture blocks into variables through capture and # a way to capture a block of markup for use in a layout through content_for. module CaptureHelper - # The capture method allows you to extract part of a template into a - # variable. You can then use this variable anywhere in your templates or layout. - # + # The capture method allows you to extract part of a template into a + # variable. You can then use this variable anywhere in your templates or layout. + # # ==== Examples # The capture method can be used in ERb templates... - # + # # <% @greeting = capture do %> # Welcome to my shiny new web page! The date and time is # <%= Time.now %> # <% end %> # # ...and Builder (RXML) templates. - # + # # @timestamp = capture do # "The current timestamp is #{Time.now}." # end @@ -32,16 +32,18 @@ module ActionView # def capture(*args) value = nil - buffer = with_output_buffer { value = yield *args } - buffer.presence || value + buffer = with_output_buffer { value = yield(*args) } + if string = buffer.presence || value and string.is_a?(String) + NonConcattingString.new(string) + end end # Calling content_for stores a block of markup in an identifier for later use. # You can make subsequent calls to the stored content in other templates or the layout # by passing the identifier as an argument to <tt>yield</tt>. - # + # # ==== Examples - # + # # <% content_for :not_authorized do %> # alert('You are not authorized to do that!') # <% end %> @@ -75,7 +77,7 @@ module ActionView # # Then, in another view, you could to do something like this: # - # <%= link_to_remote 'Logout', :action => 'logout' %> + # <%= link_to 'Logout', :action => 'logout', :remote => true %> # # <% content_for :script do %> # <%= javascript_include_tag :defaults %> @@ -92,7 +94,7 @@ module ActionView # <% end %> # # <%# Add some other content, or use a different template: %> - # + # # <% content_for :navigation do %> # <li><%= link_to 'Login', :action => 'login' %></li> # <% end %> @@ -109,13 +111,13 @@ module ActionView # for elements that will be fragment cached. def content_for(name, content = nil, &block) content = capture(&block) if block_given? - return @_content_for[name] << content if content - @_content_for[name] + @_content_for[name] << content if content + @_content_for[name] unless content end # content_for? simply checks whether any content has been captured yet using content_for # Useful to render parts of your layout differently based on what is in your views. - # + # # ==== Examples # # Perhaps you will use different css in you layout if no content_for :right_column diff --git a/actionpack/lib/action_view/helpers/deprecated_block_helpers.rb b/actionpack/lib/action_view/helpers/deprecated_block_helpers.rb deleted file mode 100644 index 3d0657e873..0000000000 --- a/actionpack/lib/action_view/helpers/deprecated_block_helpers.rb +++ /dev/null @@ -1,52 +0,0 @@ -module ActionView - module Helpers - module DeprecatedBlockHelpers - extend ActiveSupport::Concern - - include ActionView::Helpers::TagHelper - include ActionView::Helpers::TextHelper - include ActionView::Helpers::JavaScriptHelper - include ActionView::Helpers::FormHelper - - def content_tag(*, &block) - block_called_from_erb?(block) ? safe_concat(super) : super - end - - def javascript_tag(*, &block) - block_called_from_erb?(block) ? safe_concat(super) : super - end - - def form_for(*, &block) - block_called_from_erb?(block) ? safe_concat(super) : super - end - - def form_tag(*, &block) - block_called_from_erb?(block) ? safe_concat(super) : super - end - - def fields_for(*, &block) - block_called_from_erb?(block) ? safe_concat(super) : super - end - - def field_set_tag(*, &block) - block_called_from_erb?(block) ? safe_concat(super) : super - end - - BLOCK_CALLED_FROM_ERB = 'defined? __in_erb_template' - - if RUBY_VERSION < '1.9.0' - # Check whether we're called from an erb template. - # We'd return a string in any other case, but erb <%= ... %> - # can't take an <% end %> later on, so we have to use <% ... %> - # and implicitly concat. - def block_called_from_erb?(block) - block && eval(BLOCK_CALLED_FROM_ERB, block) - end - else - def block_called_from_erb?(block) - block && eval(BLOCK_CALLED_FROM_ERB, block.binding) - end - end - end - end -end
\ No newline at end of file diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index 6176f1fb06..2ba5339b7d 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -1014,7 +1014,7 @@ module ActionView class FormBuilder #:nodoc: # The methods which wrap a form helper call. class_inheritable_accessor :field_helpers - self.field_helpers = (FormHelper.instance_methods - ['form_for']) + self.field_helpers = (FormHelper.instance_method_names - ['form_for']) attr_accessor :object_name, :object, :options @@ -1040,7 +1040,7 @@ module ActionView end (field_helpers - %w(label check_box radio_button fields_for hidden_field)).each do |selector| - src = <<-end_src + src, file, line = <<-end_src, __FILE__, __LINE__ + 1 def #{selector}(method, options = {}) # def text_field(method, options = {}) @template.send( # @template.send( #{selector.inspect}, # "text_field", @@ -1049,7 +1049,7 @@ module ActionView objectify_options(options)) # objectify_options(options)) end # end end_src - class_eval src, __FILE__, __LINE__ + class_eval src, file, line end def fields_for(record_or_name_or_array, *args, &block) diff --git a/actionpack/lib/action_view/helpers/javascript_helper.rb b/actionpack/lib/action_view/helpers/javascript_helper.rb index 07eee3b399..b0a7718f22 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 'action_view/helpers/prototype_helper' module ActionView module Helpers @@ -89,6 +88,93 @@ module ActionView def javascript_cdata_section(content) #:nodoc: "\n//#{cdata_section("\n#{content}\n//")}\n".html_safe end + + # Returns a button with the given +name+ text that'll trigger a JavaScript +function+ using the + # onclick handler. + # + # The first argument +name+ is used as the button's value or display text. + # + # The next arguments are optional and may include the javascript function definition and a hash of html_options. + # + # The +function+ argument can be omitted in favor of an +update_page+ + # block, which evaluates to a string when the template is rendered + # (instead of making an Ajax request first). + # + # The +html_options+ will accept a hash of html attributes for the link tag. Some examples are :class => "nav_button", :id => "articles_nav_button" + # + # Note: if you choose to specify the javascript function in a block, but would like to pass html_options, set the +function+ parameter to nil + # + # Examples: + # button_to_function "Greeting", "alert('Hello world!')" + # button_to_function "Delete", "if (confirm('Really?')) do_delete()" + # button_to_function "Details" do |page| + # page[:details].visual_effect :toggle_slide + # end + # button_to_function "Details", :class => "details_button" do |page| + # page[:details].visual_effect :toggle_slide + # end + def button_to_function(name, *args, &block) + html_options = args.extract_options!.symbolize_keys + + function = block_given? ? update_page(&block) : args[0] || '' + onclick = "#{"#{html_options[:onclick]}; " if html_options[:onclick]}#{function};" + + tag(:input, html_options.merge(:type => 'button', :value => name, :onclick => onclick)) + end + + # Returns a link of the given +name+ that will trigger a JavaScript +function+ using the + # onclick handler and return false after the fact. + # + # The first argument +name+ is used as the link text. + # + # The next arguments are optional and may include the javascript function definition and a hash of html_options. + # + # The +function+ argument can be omitted in favor of an +update_page+ + # block, which evaluates to a string when the template is rendered + # (instead of making an Ajax request first). + # + # The +html_options+ will accept a hash of html attributes for the link tag. Some examples are :class => "nav_button", :id => "articles_nav_button" + # + # Note: if you choose to specify the javascript function in a block, but would like to pass html_options, set the +function+ parameter to nil + # + # + # Examples: + # link_to_function "Greeting", "alert('Hello world!')" + # Produces: + # <a onclick="alert('Hello world!'); return false;" href="#">Greeting</a> + # + # link_to_function(image_tag("delete"), "if (confirm('Really?')) do_delete()") + # Produces: + # <a onclick="if (confirm('Really?')) do_delete(); return false;" href="#"> + # <img src="/images/delete.png?" alt="Delete"/> + # </a> + # + # link_to_function("Show me more", nil, :id => "more_link") do |page| + # page[:details].visual_effect :toggle_blind + # page[:more_link].replace_html "Show me less" + # end + # Produces: + # <a href="#" id="more_link" onclick="try { + # $("details").visualEffect("toggle_blind"); + # $("more_link").update("Show me less"); + # } + # catch (e) { + # alert('RJS error:\n\n' + e.toString()); + # alert('$(\"details\").visualEffect(\"toggle_blind\"); + # \n$(\"more_link\").update(\"Show me less\");'); + # throw e + # }; + # return false;">Show me more</a> + # + def link_to_function(name, *args, &block) + html_options = args.extract_options!.symbolize_keys + + function = block_given? ? update_page(&block) : args[0] || '' + onclick = "#{"#{html_options[:onclick]}; " if html_options[:onclick]}#{function}; return false;" + href = html_options[:href] || '#' + + content_tag(:a, name, html_options.merge(:href => href, :onclick => onclick)) + end end end end diff --git a/actionpack/lib/action_view/helpers/number_helper.rb b/actionpack/lib/action_view/helpers/number_helper.rb index 46e41bc406..719b64b940 100644 --- a/actionpack/lib/action_view/helpers/number_helper.rb +++ b/actionpack/lib/action_view/helpers/number_helper.rb @@ -3,10 +3,24 @@ require 'active_support/core_ext/float/rounding' module ActionView module Helpers #:nodoc: + # Provides methods for converting numbers into formatted strings. # Methods are provided for phone numbers, currency, percentage, - # precision, positional notation, and file size. + # precision, positional notation, file size and pretty printing. + # + # Most methods expect a +number+ argument, and will return it + # unchanged if can't be converted into a valid number. module NumberHelper + + # Raised when argument +number+ param given to the helpers is invalid and + # the option :raise is set to +true+. + class InvalidNumberError < StandardError + attr_accessor :number + def initialize(number) + @number = number + end + end + # Formats a +number+ into a US phone number (e.g., (555) 123-9876). You can customize the format # in the +options+ hash. # @@ -30,6 +44,17 @@ module ActionView def number_to_phone(number, options = {}) return nil if number.nil? + begin + Float(number) + is_number_html_safe = true + rescue ArgumentError, TypeError + if options[:raise] + raise InvalidNumberError, number + else + is_number_html_safe = number.to_s.html_safe? + end + end + number = number.to_s.strip options = options.symbolize_keys area_code = options[:area_code] || nil @@ -46,7 +71,7 @@ module ActionView number.starts_with?('-') ? number.slice!(1..-1) : number end str << " x #{extension}" unless extension.blank? - str + is_number_html_safe ? str.html_safe : str end # Formats a +number+ into a currency string (e.g., $13.65). You can customize the format @@ -72,38 +97,42 @@ module ActionView # number_to_currency(1234567890.50, :unit => "£", :separator => ",", :delimiter => "", :format => "%n %u") # # => 1234567890,50 £ def number_to_currency(number, options = {}) + return nil if number.nil? + options.symbolize_keys! - defaults = I18n.translate(:'number.format', :locale => options[:locale], :raise => true) rescue {} - currency = I18n.translate(:'number.currency.format', :locale => options[:locale], :raise => true) rescue {} + defaults = I18n.translate(:'number.format', :locale => options[:locale], :default => {}) + currency = I18n.translate(:'number.currency.format', :locale => options[:locale], :default => {}) defaults = defaults.merge(currency) - precision = options[:precision] || defaults[:precision] - unit = options[:unit] || defaults[:unit] - separator = options[:separator] || defaults[:separator] - delimiter = options[:delimiter] || defaults[:delimiter] - format = options[:format] || defaults[:format] - separator = '' if precision == 0 + options = options.reverse_merge(defaults) - value = number_with_precision(number, - :precision => precision, - :delimiter => delimiter, - :separator => separator) + unit = options.delete(:unit) + format = options.delete(:format) - if value + begin + value = number_with_precision(number, options.merge(:raise => true)) format.gsub(/%n/, value).gsub(/%u/, unit).html_safe - else - number + rescue InvalidNumberError => e + if options[:raise] + raise + else + formatted_number = format.gsub(/%n/, e.number).gsub(/%u/, unit) + 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. # # ==== Options - # * <tt>:precision</tt> - Sets the level of precision (defaults to 3). - # * <tt>:separator</tt> - Sets the separator between the units (defaults to "."). + # * <tt>:precision</tt> - Sets the precision of the number (defaults to 3). + # * <tt>:significant</tt> - If +true+, precision will be the # of significant_digits. If +false+, the # of fractional digits (defaults to +false+) + # * <tt>:separator</tt> - Sets the separator between the fractional and integer digits (defaults to "."). # * <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+) # # ==== Examples # number_to_percentage(100) # => 100.000% @@ -111,21 +140,25 @@ module ActionView # number_to_percentage(1000, :delimiter => '.', :separator => ',') # => 1.000,000% # number_to_percentage(302.24398923423, :precision => 5) # => 302.24399% def number_to_percentage(number, options = {}) + return nil if number.nil? + options.symbolize_keys! - defaults = I18n.translate(:'number.format', :locale => options[:locale], :raise => true) rescue {} - percentage = I18n.translate(:'number.percentage.format', :locale => options[:locale], :raise => true) rescue {} + defaults = I18n.translate(:'number.format', :locale => options[:locale], :default => {}) + percentage = I18n.translate(:'number.percentage.format', :locale => options[:locale], :default => {}) defaults = defaults.merge(percentage) - precision = options[:precision] || defaults[:precision] - separator = options[:separator] || defaults[:separator] - delimiter = options[:delimiter] || defaults[:delimiter] + options = options.reverse_merge(defaults) - value = number_with_precision(number, - :precision => precision, - :separator => separator, - :delimiter => delimiter) - value ? value + "%" : number + begin + "#{number_with_precision(number, options.merge(:raise => true))}%".html_safe + rescue InvalidNumberError => e + if options[:raise] + raise + else + e.number.to_s.html_safe? ? "#{e.number}%".html_safe : "#{e.number}%" + end + end end # Formats a +number+ with grouped thousands using +delimiter+ (e.g., 12,324). You can @@ -133,7 +166,7 @@ module ActionView # # ==== Options # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to ","). - # * <tt>:separator</tt> - Sets the separator between the units (defaults to "."). + # * <tt>:separator</tt> - Sets the separator between the fractional and integer digits (defaults to "."). # # ==== Examples # number_with_delimiter(12345678) # => 12,345,678 @@ -146,148 +179,186 @@ module ActionView # You can still use <tt>number_with_delimiter</tt> with the old API that accepts the # +delimiter+ as its optional second and the +separator+ as its # optional third parameter: - # number_with_delimiter(12345678, " ") # => 12 345.678 + # number_with_delimiter(12345678, " ") # => 12 345 678 # number_with_delimiter(12345678.05, ".", ",") # => 12.345.678,05 def number_with_delimiter(number, *args) options = args.extract_options! options.symbolize_keys! - defaults = I18n.translate(:'number.format', :locale => options[:locale], :raise => true) rescue {} + begin + Float(number) + rescue ArgumentError, TypeError + if options[:raise] + raise InvalidNumberError, number + else + return number + end + end + + defaults = I18n.translate(:'number.format', :locale => options[:locale], :default => {}) unless args.empty? ActiveSupport::Deprecation.warn('number_with_delimiter takes an option hash ' + 'instead of separate delimiter and precision arguments.', caller) - delimiter = args[0] || defaults[:delimiter] - separator = args[1] || defaults[:separator] + options[:delimiter] ||= args[0] if args[0] + options[:separator] ||= args[1] if args[1] end - delimiter ||= (options[:delimiter] || defaults[:delimiter]) - separator ||= (options[:separator] || defaults[:separator]) + options = options.reverse_merge(defaults) parts = number.to_s.split('.') - if parts[0] - parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{delimiter}") - parts.join(separator) - else - number - end + 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 of 2). + # Formats a +number+ with the specified level of <tt>:precision</tt> (e.g., 112.32 has a precision + # of 2 if +:significant+ is +false+, and 5 if +:significant+ is +true+). # You can customize the format in the +options+ hash. # # ==== Options - # * <tt>:precision</tt> - Sets the level of precision (defaults to 3). - # * <tt>:separator</tt> - Sets the separator between the units (defaults to "."). + # * <tt>:precision</tt> - Sets the precision of the number (defaults to 3). + # * <tt>:significant</tt> - If +true+, precision will be the # of significant_digits. If +false+, the # of fractional digits (defaults to +false+) + # * <tt>:separator</tt> - Sets the separator between the fractional and integer digits (defaults to "."). # * <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+) # # ==== Examples - # number_with_precision(111.2345) # => 111.235 - # number_with_precision(111.2345, :precision => 2) # => 111.23 - # number_with_precision(13, :precision => 5) # => 13.00000 - # number_with_precision(389.32314, :precision => 0) # => 389 + # number_with_precision(111.2345) # => 111.235 + # number_with_precision(111.2345, :precision => 2) # => 111.23 + # number_with_precision(13, :precision => 5) # => 13.00000 + # number_with_precision(389.32314, :precision => 0) # => 389 + # number_with_precision(111.2345, :significant => true) # => 111 + # number_with_precision(111.2345, :precision => 1, :significant => true) # => 100 + # number_with_precision(13, :precision => 5, :significant => true) # => 13.000 + # number_with_precision(13, :precision => 5, :significant => true, strip_insignificant_zeros => true) + # # => 13 + # number_with_precision(389.32314, :precision => 4, :significant => true) # => 389.3 # number_with_precision(1111.2345, :precision => 2, :separator => ',', :delimiter => '.') # # => 1.111,23 # # You can still use <tt>number_with_precision</tt> with the old API that accepts the # +precision+ as its optional second parameter: - # number_with_precision(number_with_precision(111.2345, 2) # => 111.23 + # number_with_precision(111.2345, 2) # => 111.23 def number_with_precision(number, *args) + options = args.extract_options! options.symbolize_keys! - defaults = I18n.translate(:'number.format', :locale => options[:locale], :raise => true) rescue {} - precision_defaults = I18n.translate(:'number.precision.format', :locale => options[:locale], - :raise => true) rescue {} + number = begin + Float(number) + rescue ArgumentError, TypeError + if options[:raise] + raise InvalidNumberError, number + else + return number + end + end + + 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) + #Backwards compatibility unless args.empty? ActiveSupport::Deprecation.warn('number_with_precision takes an option hash ' + 'instead of a separate precision argument.', caller) - precision = args[0] || defaults[:precision] + options[:precision] ||= args[0] if args[0] end - precision ||= (options[:precision] || defaults[:precision]) - separator ||= (options[:separator] || defaults[:separator]) - delimiter ||= (options[:delimiter] || defaults[:delimiter]) + 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 - begin - value = Float(number) - rescue ArgumentError, TypeError - value = nil + if significant and precision > 0 + digits = (Math.log10(number) + 1).floor + rounded_number = BigDecimal.new((number / 10 ** (digits - precision)).to_s).round.to_f * 10 ** (digits - precision) + precision = precision - digits + precision = precision > 0 ? precision : 0 #don't let it be negative + else + rounded_number = BigDecimal.new((number * (10 ** precision)).to_s).round.to_f / 10 ** precision end - - if value - rounded_number = BigDecimal.new((Float(number) * (10 ** precision)).to_s).round.to_f / 10 ** precision - number_with_delimiter("%01.#{precision}f" % rounded_number, - :separator => separator, - :delimiter => delimiter) + formatted_number = number_with_delimiter("%01.#{precision}f" % rounded_number, options) + if strip_insignificant_zeros + escaped_separator = Regexp.escape(options[:separator]) + formatted_number.sub(/(#{escaped_separator})(\d*[1-9])?0+\z/, '\1\2').sub(/#{escaped_separator}\z/, '').html_safe else - number + formatted_number end + end STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb].freeze - # Formats the bytes in +size+ into a more understandable representation + # Formats the bytes in +number+ into a more understandable representation # (e.g., giving it 1500 yields 1.5 KB). This method is useful for - # reporting file sizes to users. This method returns nil if - # +size+ cannot be converted into a number. You can customize the + # reporting file sizes to users. You can customize the # format in the +options+ hash. # + # See <tt>number_to_human</tt> if you want to pretty-print a generic number. + # # ==== Options - # * <tt>:precision</tt> - Sets the level of precision (defaults to 1). - # * <tt>:separator</tt> - Sets the separator between the units (defaults to "."). + # * <tt>:precision</tt> - Sets the precision of the number (defaults to 3). + # * <tt>:significant</tt> - If +true+, precision will be the # of significant_digits. If +false+, the # of fractional digits (defaults to +true+) + # * <tt>:separator</tt> - Sets the separator between the fractional and integer digits (defaults to "."). # * <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+) # ==== Examples # number_to_human_size(123) # => 123 Bytes - # number_to_human_size(1234) # => 1.2 KB + # number_to_human_size(1234) # => 1.21 KB # number_to_human_size(12345) # => 12.1 KB - # number_to_human_size(1234567) # => 1.2 MB - # number_to_human_size(1234567890) # => 1.1 GB - # number_to_human_size(1234567890123) # => 1.1 TB - # number_to_human_size(1234567, :precision => 2) # => 1.18 MB - # number_to_human_size(483989, :precision => 0) # => 473 KB - # number_to_human_size(1234567, :precision => 2, :separator => ',') # => 1,18 MB + # number_to_human_size(1234567) # => 1.18 MB + # number_to_human_size(1234567890) # => 1.15 GB + # number_to_human_size(1234567890123) # => 1.12 TB + # number_to_human_size(1234567, :precision => 2) # => 1.2 MB + # number_to_human_size(483989, :precision => 2) # => 470 KB + # number_to_human_size(1234567, :precision => 2, :separator => ',') # => 1,2 MB # - # Zeros after the decimal point are always stripped out, regardless of the - # specified precision: - # helper.number_to_human_size(1234567890123, :precision => 5) # => "1.12283 TB" - # helper.number_to_human_size(524288000, :precision=>5) # => "500 MB" + # Unsignificant zeros after the fractional separator are stripped out by default (set + # <tt>:strip_insignificant_zeros</tt> to +false+ to change that): + # number_to_human_size(1234567890123, :precision => 5) # => "1.1229 TB" + # number_to_human_size(524288000, :precision=>5) # => "500 MB" # # You can still use <tt>number_to_human_size</tt> with the old API that accepts the # +precision+ as its optional second parameter: - # number_to_human_size(1234567, 2) # => 1.18 MB - # number_to_human_size(483989, 0) # => 473 KB + # number_to_human_size(1234567, 1) # => 1 MB + # number_to_human_size(483989, 2) # => 470 KB def number_to_human_size(number, *args) - return nil if number.nil? - options = args.extract_options! options.symbolize_keys! - defaults = I18n.translate(:'number.format', :locale => options[:locale], :raise => true) rescue {} - human = I18n.translate(:'number.human.format', :locale => options[:locale], :raise => true) rescue {} + number = begin + Float(number) + rescue ArgumentError, TypeError + if options[:raise] + raise InvalidNumberError, number + else + return number + end + end + + defaults = I18n.translate(:'number.format', :locale => options[:locale], :default => {}) + human = I18n.translate(:'number.human.format', :locale => options[:locale], :default => {}) defaults = defaults.merge(human) unless args.empty? ActiveSupport::Deprecation.warn('number_to_human_size takes an option hash ' + 'instead of a separate precision argument.', caller) - precision = args[0] || defaults[:precision] + options[:precision] ||= args[0] if args[0] end - precision ||= (options[:precision] || defaults[:precision]) - separator ||= (options[:separator] || defaults[:separator]) - delimiter ||= (options[:delimiter] || defaults[:delimiter]) + 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) storage_units_format = I18n.translate(:'number.human.storage_units.format', :locale => options[:locale], :raise => true) if number.to_i < 1024 unit = I18n.translate(:'number.human.storage_units.units.byte', :locale => options[:locale], :count => number.to_i, :raise => true) - storage_units_format.gsub(/%n/, number.to_i.to_s).gsub(/%u/, unit) + storage_units_format.gsub(/%n/, number.to_i.to_s).gsub(/%u/, unit).html_safe else max_exp = STORAGE_UNITS.size - 1 - number = Float(number) exponent = (Math.log(number) / Math.log(1024)).to_i # Convert to base 1024 exponent = max_exp if exponent > max_exp # we need this to avoid overflow for the highest unit number /= 1024 ** exponent @@ -295,15 +366,138 @@ module ActionView unit_key = STORAGE_UNITS[exponent] unit = I18n.translate(:"number.human.storage_units.units.#{unit_key}", :locale => options[:locale], :count => number, :raise => true) - escaped_separator = Regexp.escape(separator) - formatted_number = number_with_precision(number, - :precision => precision, - :separator => separator, - :delimiter => delimiter - ).sub(/(#{escaped_separator})(\d*[1-9])?0+\z/, '\1\2').sub(/#{escaped_separator}\z/, '') - storage_units_format.gsub(/%n/, formatted_number).gsub(/%u/, unit) + formatted_number = number_with_precision(number, options) + storage_units_format.gsub(/%n/, formatted_number).gsub(/%u/, unit).html_safe + end + end + + DECIMAL_UNITS = {0 => :unit, 1 => :ten, 2 => :hundred, 3 => :thousand, 6 => :million, 9 => :billion, 12 => :trillion, 15 => :quadrillion, + -1 => :deci, -2 => :centi, -3 => :mili, -6 => :micro, -9 => :nano, -12 => :pico, -15 => :femto}.freeze + + # Pretty prints (formats and approximates) a number in a way it is more readable by humans + # (eg.: 1200000000 becomes "1.2 Billion"). This is useful for numbers that + # can get very large (and too hard to read). + # + # See <tt>number_to_human_size</tt> if you want to print a file size. + # + # You can also define you own unit-quantifier names if you want to use other decimal units + # (eg.: 1500 becomes "1.5 kilometers", 0.150 becomes "150 mililiters", etc). You may define + # a wide range of unit quantifiers, even fractional ones (centi, deci, mili, etc). + # + # ==== Options + # * <tt>:precision</tt> - Sets the precision of the number (defaults to 3). + # * <tt>:significant</tt> - If +true+, precision will be the # of significant_digits. If +false+, the # of fractional digits (defaults to +true+) + # * <tt>:separator</tt> - Sets the separator between the fractional and integer digits (defaults to "."). + # * <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>:units</tt> - A Hash of unit quantifier names. Or a string containing an i18n scope where to find this hash. It might have the following keys: + # * *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: + # + # %u The quantifier (ex.: 'thousand') + # %n The number + # + # ==== Examples + # number_to_human(123) # => "123" + # number_to_human(1234) # => "1.23 Thousand" + # number_to_human(12345) # => "12.3 Thousand" + # number_to_human(1234567) # => "1.23 Million" + # number_to_human(1234567890) # => "1.23 Billion" + # number_to_human(1234567890123) # => "1.23 Trillion" + # number_to_human(1234567890123456) # => "1.23 Quadrillion" + # number_to_human(1234567890123456789) # => "1230 Quadrillion" + # number_to_human(489939, :precision => 2) # => "490 Thousand" + # number_to_human(489939, :precision => 4) # => "489.9 Thousand" + # number_to_human(1234567, :precision => 4, + # :significant => false) # => "1.2346 Million" + # number_to_human(1234567, :precision => 1, + # :separator => ',', + # :significant => false) # => "1,2 Million" + # + # Unsignificant zeros after the decimal separator are stripped out by default (set + # <tt>:strip_insignificant_zeros</tt> to +false+ to change that): + # number_to_human(12345012345, :significant_digits => 6) # => "12.345 Billion" + # number_to_human(500000000, :precision=>5) # => "500 Million" + # + # ==== Custom Unit Quantifiers + # + # You can also use your own custom unit quantifiers: + # number_to_human(500000, :units => {:unit => "ml", :thousand => "lt"}) # => "500 lt" + # + # If in your I18n locale you have: + # distance: + # centi: + # one: "centimeter" + # other: "centimeters" + # unit: + # one: "meter" + # other: "meters" + # thousand: + # one: "kilometer" + # other: "kilometers" + # billion: "gazilion-distance" + # + # Then you could do: + # + # number_to_human(543934, :units => :distance) # => "544 kilometers" + # number_to_human(54393498, :units => :distance) # => "54400 kilometers" + # number_to_human(54393498000, :units => :distance) # => "54.4 gazilion-distance" + # number_to_human(343, :units => :distance, :precision => 1) # => "300 meters" + # number_to_human(1, :units => :distance) # => "1 meter" + # number_to_human(0.34, :units => :distance) # => "34 centimeters" + # + def number_to_human(number, options = {}) + options.symbolize_keys! + + number = begin + Float(number) + rescue ArgumentError, TypeError + if options[:raise] + raise InvalidNumberError, number + else + return number + end end + + defaults = I18n.translate(:'number.format', :locale => options[:locale], :default => {}) + human = I18n.translate(:'number.human.format', :locale => options[:locale], :default => {}) + defaults = defaults.merge(human) + + 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) + + units = options.delete :units + unit_exponents = case units + when Hash + units + when String, Symbol + I18n.translate(:"#{units}", :locale => options[:locale], :raise => true) + when nil + I18n.translate(:"number.human.decimal_units.units", :locale => options[:locale], :raise => true) + else + raise ArgumentError, ":units must be a Hash or String translation scope." + end.keys.map{|e_name| DECIMAL_UNITS.invert[e_name] }.sort_by{|e| -e} + + number_exponent = Math.log10(number).floor + display_exponent = unit_exponents.find{|e| number_exponent >= e } + number /= 10 ** display_exponent + + unit = case units + when Hash + units[DECIMAL_UNITS[display_exponent]] + when String, Symbol + I18n.translate(:"#{units}.#{DECIMAL_UNITS[display_exponent]}", :locale => options[:locale], :count => number.to_i) + else + I18n.translate(:"number.human.decimal_units.units.#{DECIMAL_UNITS[display_exponent]}", :locale => options[:locale], :count => number.to_i) + end + + decimal_format = options[:format] || I18n.translate(:'number.human.decimal_units.format', :locale => options[:locale], :default => "%n %u") + formatted_number = number_with_precision(number, options) + decimal_format.gsub(/%n/, formatted_number).gsub(/%u/, unit).strip.html_safe end + end end end diff --git a/actionpack/lib/action_view/helpers/prototype_helper.rb b/actionpack/lib/action_view/helpers/prototype_helper.rb index dc3c6d88f5..ccdc8181db 100644 --- a/actionpack/lib/action_view/helpers/prototype_helper.rb +++ b/actionpack/lib/action_view/helpers/prototype_helper.rb @@ -102,39 +102,6 @@ module ActionView :form, :with, :update, :script, :type ]).merge(CALLBACKS) end - # Returns a button with the given +name+ text that'll trigger a JavaScript +function+ using the - # onclick handler. - # - # The first argument +name+ is used as the button's value or display text. - # - # The next arguments are optional and may include the javascript function definition and a hash of html_options. - # - # The +function+ argument can be omitted in favor of an +update_page+ - # block, which evaluates to a string when the template is rendered - # (instead of making an Ajax request first). - # - # The +html_options+ will accept a hash of html attributes for the link tag. Some examples are :class => "nav_button", :id => "articles_nav_button" - # - # Note: if you choose to specify the javascript function in a block, but would like to pass html_options, set the +function+ parameter to nil - # - # Examples: - # button_to_function "Greeting", "alert('Hello world!')" - # button_to_function "Delete", "if (confirm('Really?')) do_delete()" - # button_to_function "Details" do |page| - # page[:details].visual_effect :toggle_slide - # end - # button_to_function "Details", :class => "details_button" do |page| - # page[:details].visual_effect :toggle_slide - # end - def button_to_function(name, *args, &block) - html_options = args.extract_options!.symbolize_keys - - function = block_given? ? update_page(&block) : args[0] || '' - onclick = "#{"#{html_options[:onclick]}; " if html_options[:onclick]}#{function};" - - tag(:input, html_options.merge(:type => 'button', :value => name, :onclick => onclick)) - end - # Returns the JavaScript needed for a remote function. # Takes the same arguments as link_to_remote. # @@ -181,11 +148,9 @@ module ActionView class JavaScriptGenerator #:nodoc: def initialize(context, &block) #:nodoc: @context, @lines = context, [] - @context.update_details(:formats => [:js, :html]) do - include_helpers_from_context - @context.with_output_buffer(@lines) do - @context.instance_exec(self, &block) - end + include_helpers_from_context + @context.with_output_buffer(@lines) do + @context.instance_exec(self, &block) end end @@ -615,7 +580,7 @@ module ActionView # page.hide 'spinner' # end def update_page(&block) - JavaScriptGenerator.new(@template, &block).to_s.html_safe + JavaScriptGenerator.new(view_context, &block).to_s.html_safe end # Works like update_page but wraps the generated JavaScript in a <script> @@ -689,6 +654,10 @@ module ActionView @generator << root if root end + def is_a?(klass) + klass == JavaScriptProxy + end + private def method_missing(method, *arguments, &block) if method.to_s =~ /(.*)=$/ @@ -882,5 +851,3 @@ module ActionView end end end - -require 'action_view/helpers/javascript_helper' diff --git a/actionpack/lib/action_view/helpers/text_helper.rb b/actionpack/lib/action_view/helpers/text_helper.rb index e1ce65f90a..27be1690dd 100644 --- a/actionpack/lib/action_view/helpers/text_helper.rb +++ b/actionpack/lib/action_view/helpers/text_helper.rb @@ -29,7 +29,7 @@ module ActionView end def safe_concat(string) - output_buffer.safe_concat(string) + output_buffer.respond_to?(:safe_concat) ? output_buffer.safe_concat(string) : concat(string) end # Truncates a given +text+ after a given <tt>:length</tt> if +text+ is longer than <tt>:length</tt> @@ -576,7 +576,7 @@ module ActionView # each email is yielded and the result is used as the link text. def auto_link_email_addresses(text, html_options = {}) body = text.dup - text.gsub(/([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do + text.gsub(/([\w\.!#\$%\-+]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do text = $1 if body.match(/<a\b[^>]*>(.*)(#{Regexp.escape(text)})(.*)<\/a>/) diff --git a/actionpack/lib/action_view/helpers/translation_helper.rb b/actionpack/lib/action_view/helpers/translation_helper.rb index 8a89ee58a0..457944dbb6 100644 --- a/actionpack/lib/action_view/helpers/translation_helper.rb +++ b/actionpack/lib/action_view/helpers/translation_helper.rb @@ -13,7 +13,7 @@ module ActionView def translate(key, options = {}) options[:raise] = true translation = I18n.translate(scope_key_by_partial(key), options) - translation.is_a?(Array) ? translation.map { |entry| entry.html_safe } : translation.html_safe + (translation.respond_to?(:join) ? translation.join : translation).html_safe rescue I18n::MissingTranslationData => e keys = I18n.normalize_keys(e.locale, e.key, e.options[:scope]) content_tag('span', keys.join(', '), :class => 'translation_missing') @@ -29,9 +29,10 @@ module ActionView private def scope_key_by_partial(key) - if (key.respond_to?(:join) ? key.join : key.to_s).first == "." + strkey = key.respond_to?(:join) ? key.join : key.to_s + if strkey.first == "." if @_virtual_path - @_virtual_path.gsub(%r{/_?}, ".") + key.to_s + @_virtual_path.gsub(%r{/_?}, ".") + strkey else raise "Cannot use t(#{key.inspect}) shortcut because path is not available" end diff --git a/actionpack/lib/action_view/helpers/url_helper.rb b/actionpack/lib/action_view/helpers/url_helper.rb index 131e950b18..b23d5fcb68 100644 --- a/actionpack/lib/action_view/helpers/url_helper.rb +++ b/actionpack/lib/action_view/helpers/url_helper.rb @@ -1,6 +1,7 @@ require 'action_view/helpers/javascript_helper' require 'active_support/core_ext/array/access' require 'active_support/core_ext/hash/keys' +require 'action_dispatch' module ActionView module Helpers #:nodoc: @@ -9,6 +10,9 @@ module ActionView # This allows you to use the same format for links in views # and controllers. module UrlHelper + extend ActiveSupport::Concern + + include ActionDispatch::Routing::UrlFor include JavaScriptHelper # Need to map default url options to controller one. @@ -16,6 +20,10 @@ module ActionView controller.send(:default_url_options, *args) end + def url_options + controller.url_options + end + # Returns the URL for the set of +options+ provided. This takes the # same options as +url_for+ in Action Controller (see the # documentation for <tt>ActionController::Base#url_for</tt>). Note that by default @@ -206,7 +214,7 @@ module ActionView if block_given? options = args.first || {} html_options = args.second - safe_concat(link_to(capture(&block), options, html_options)) + link_to(capture(&block), options, html_options) else name = args[0] options = args[1] || {} @@ -578,8 +586,6 @@ module ActionView add_confirm_to_attributes!(html_options, confirm) if confirm add_method_to_attributes!(html_options, method) if method - html_options["data-url"] = options[:url] if options.is_a?(Hash) && options[:url] - html_options end diff --git a/actionpack/lib/action_view/locale/en.yml b/actionpack/lib/action_view/locale/en.yml index a3548051c1..a3e2230f6f 100644 --- a/actionpack/lib/action_view/locale/en.yml +++ b/actionpack/lib/action_view/locale/en.yml @@ -9,6 +9,11 @@ delimiter: "," # Number of decimals, behind the separator (the number 1 with a precision of 2 gives: 1.00) precision: 3 + # If set to true, precision will mean the number of significant digits instead + # of the number of decimal digits (1234 with precision 2 becomes 1200, 1.23543 becomes 1.2) + significant: false + # If set, the zeros after the decimal separator will always be stripped (eg.: 1.200 will be 1.2) + strip_insignificant_zeros: false # Used in number_to_currency() currency: @@ -16,34 +21,43 @@ # Where is the currency sign? %u is the currency unit, %n the number (default: $5.00) format: "%u%n" unit: "$" - # These three are to override number.format and are optional + # These five are to override number.format and are optional separator: "." delimiter: "," precision: 2 + significant: false + strip_insignificant_zeros: false # Used in number_to_percentage() percentage: format: - # These three are to override number.format and are optional + # These five are to override number.format and are optional # separator: delimiter: "" # precision: + # significant: false + # strip_insignificant_zeros: false # Used in number_to_precision() precision: format: - # These three are to override number.format and are optional + # These five are to override number.format and are optional # separator: delimiter: "" # precision: + # significant: false + # strip_insignificant_zeros: false - # Used in number_to_human_size() + # Used in number_to_human_size() and number_to_human() human: format: - # These three are to override number.format and are optional + # These five are to override number.format and are optional # separator: delimiter: "" - precision: 1 + precision: 3 + significant: true + strip_insignificant_zeros: true + # Used in number_to_human_size() storage_units: # Storage units output formatting. # %u is the storage unit, %n is the number (default: 2 MB) @@ -56,6 +70,31 @@ mb: "MB" gb: "GB" tb: "TB" + # Used in number_to_human() + decimal_units: + format: "%n %u" + # Decimal units output formatting + # By default we will only quantify some of the exponents + # but the commented ones might be defined or overridden + # by the user. + units: + # femto: Quadrillionth + # pico: Trillionth + # nano: Billionth + # micro: Millionth + # mili: Thousandth + # centi: Hundredth + # deci: Tenth + unit: "" + # ten: + # one: Ten + # other: Tens + # hundred: Hundred + thousand: Thousand + million: Million + billion: Billion + trillion: Trillion + quadrillion: Quadrillion # Used in distance_of_time_in_words(), distance_of_time_in_words_to_now(), time_ago_in_words() datetime: diff --git a/actionpack/lib/action_view/lookup_context.rb b/actionpack/lib/action_view/lookup_context.rb index 8eb17bf8f1..10f1911e31 100644 --- a/actionpack/lib/action_view/lookup_context.rb +++ b/actionpack/lib/action_view/lookup_context.rb @@ -1,4 +1,4 @@ -require 'active_support/core_ext/object/try' +require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/object/blank' module ActionView @@ -15,23 +15,23 @@ module ActionView def self.register_detail(name, options = {}, &block) self.registered_details << name + Accessors.send :define_method, :"_#{name}_defaults", &block + Accessors.module_eval <<-METHOD, __FILE__, __LINE__ + 1 + def #{name} + @details[:#{name}] + end - Setters.send :define_method, :"_#{name}_defaults", &block - Setters.module_eval <<-METHOD, __FILE__, __LINE__ + 1 def #{name}=(value) - value = Array(value.presence || _#{name}_defaults) - #{"value << nil unless value.include?(nil)" unless options[:allow_nil] == false} - - unless value == @details[:#{name}] - @details_key, @details = nil, @details.merge(:#{name} => value) - @details.freeze - end + value = Array.wrap(value.presence || _#{name}_defaults) + @details_key = nil unless value == @details[:#{name}] + # Always set the value to handle frozen arrays + @details[:#{name}] = value end METHOD end - # Holds raw setters for the registered details. - module Setters #:nodoc: + # Holds accessors for the registered details. + module Accessors #:nodoc: end register_detail(:formats) { Mime::SET.symbols } @@ -54,9 +54,9 @@ module ActionView end def initialize(view_paths, details = {}) - @details, @details_key = {}, nil + @details, @details_key = { :handlers => default_handlers }, nil self.view_paths = view_paths - self.details = details + self.update_details(details, true) end module ViewPaths @@ -69,16 +69,16 @@ module ActionView end def find(name, prefix = nil, partial = false) - @view_paths.find(name, prefix, partial, details, details_key) + @view_paths.find(*args_for_lookup(name, prefix, partial)) end alias :find_template :find def find_all(name, prefix = nil, partial = false) - @view_paths.find_all(name, prefix, partial, details, details_key) + @view_paths.find_all(*args_for_lookup(name, prefix, partial)) end def exists?(name, prefix = nil, partial = false) - @view_paths.exists?(name, prefix, partial, details, details_key) + @view_paths.exists?(*args_for_lookup(name, prefix, partial)) end alias :template_exists? :exists? @@ -94,51 +94,71 @@ module ActionView ensure added_resolvers.times { view_paths.pop } end - end - module Details - attr_reader :details + protected - def details=(given_details) - registered_details.each { |key| send(:"#{key}=", given_details[key]) } + def args_for_lookup(name, prefix, partial) #:nodoc: + name, prefix = normalize_name(name, prefix) + [name, prefix, partial || false, @details, details_key] end - def details_key - @details_key ||= DetailsKey.get(@details) + # Support legacy foo.erb names even though we now ignore .erb + # as well as incorrectly putting part of the path in the template + # name instead of the prefix. + def normalize_name(name, prefix) #:nodoc: + name = name.to_s.gsub(handlers_regexp, '') + parts = name.split('/') + return parts.pop, [prefix, *parts].compact.join("/") + end + + def default_handlers #:nodoc: + @default_handlers ||= Template::Handlers.extensions end - # Shortcut to read formats from details. - def formats - @details[:formats].compact + def handlers_regexp #:nodoc: + @handlers_regexp ||= /\.(?:#{default_handlers.join('|')})$/ + end + end + + module Details + # Calculate the details key. Remove the handlers from calculation to improve performance + # since the user cannot modify it explicitly. + def details_key #:nodoc: + @details_key ||= DetailsKey.get(@details) end # Overload formats= to reject [:"*/*"] values. - def formats=(value, freeze=true) - value = nil if value == [:"*/*"] + def formats=(value) + value = nil if value == [:"*/*"] + value << :html if value == [:js] super(value) end - # Shortcut to read locale. + # Overload locale to return a symbol instead of array def locale - I18n.locale + @details[:locale].first end # Overload locale= to also set the I18n.locale. If the current I18n.config object responds # to i18n_config, it means that it's has a copy of the original I18n configuration and it's # acting as proxy, which we need to skip. def locale=(value) - value = value.first if value.is_a?(Array) - config = I18n.config.respond_to?(:i18n_config) ? I18n.config.i18n_config : I18n.config - config.locale = value if value + if value + config = I18n.config.respond_to?(:i18n_config) ? I18n.config.i18n_config : I18n.config + config.locale = value + end super(I18n.locale) end # Update the details keys by merging the given hash into the current # details hash. If a block is given, the details are modified just during # the execution of the block and reverted to the previous value after. - def update_details(new_details) - old_details = @details - self.details = old_details.merge(new_details) + def update_details(new_details, force=false) + old_details = @details.dup + + registered_details.each do |key| + send(:"#{key}=", new_details[key]) if force || new_details.key?(key) + end if block_given? begin @@ -150,7 +170,7 @@ module ActionView end end - include Setters + include Accessors include Details include ViewPaths end diff --git a/actionpack/lib/action_view/railtie.rb b/actionpack/lib/action_view/railtie.rb index 2e5d115630..9cf007cd2b 100644 --- a/actionpack/lib/action_view/railtie.rb +++ b/actionpack/lib/action_view/railtie.rb @@ -3,10 +3,10 @@ require "rails" module ActionView class Railtie < Rails::Railtie - railtie_name :action_view + config.action_view = ActiveSupport::OrderedOptions.new require "action_view/railties/log_subscriber" - log_subscriber ActionView::Railties::LogSubscriber.new + log_subscriber :action_view, ActionView::Railties::LogSubscriber.new initializer "action_view.cache_asset_timestamps" do |app| unless app.config.cache_classes @@ -15,5 +15,13 @@ module ActionView end end end + + initializer "action_view.set_configs" do |app| + ActionView.base_hook do + app.config.action_view.each do |k,v| + send "#{k}=", v + end + end + end end end
\ No newline at end of file diff --git a/actionpack/lib/action_view/render/layouts.rb b/actionpack/lib/action_view/render/layouts.rb index 11ff05ce5b..578f39d817 100644 --- a/actionpack/lib/action_view/render/layouts.rb +++ b/actionpack/lib/action_view/render/layouts.rb @@ -43,10 +43,16 @@ module ActionView # This is the method which actually finds the layout using details in the lookup # context object. If no layout is found, it checkes if at least a layout with # the given name exists across all details before raising the error. - def find_layout(layout) #:nodoc: + # + # If self.formats contains several formats, just the first one is considered in + # the layout lookup. + def find_layout(layout) begin - layout =~ /^\// ? - with_fallbacks { find_template(layout) } : find_template(layout) + if formats.size == 1 + _find_layout(layout) + else + update_details(:formats => self.formats.first){ _find_layout(layout) } + end rescue ActionView::MissingTemplate => e update_details(:formats => nil) do raise unless template_exists?(layout) @@ -54,6 +60,11 @@ module ActionView end end + def _find_layout(layout) #:nodoc: + layout =~ /^\// ? + with_fallbacks { find_template(layout) } : find_template(layout) + end + # Contains the logic that actually renders the layout. def _render_layout(layout, locals, &block) #:nodoc: layout.render(self, locals){ |*name| _layout_for(*name, &block) } diff --git a/actionpack/lib/action_view/render/rendering.rb b/actionpack/lib/action_view/render/rendering.rb index 310efe40e2..a8f745afd1 100644 --- a/actionpack/lib/action_view/render/rendering.rb +++ b/actionpack/lib/action_view/render/rendering.rb @@ -16,13 +16,12 @@ module ActionView case options when Hash if block_given? - content = _render_partial(options.merge(:partial => options[:layout]), &block) - safe_concat(content) + _render_partial(options.merge(:partial => options[:layout]), &block) elsif options.key?(:partial) _render_partial(options) else template = _determine_template(options) - self.formats = template.formats + _freeze_formats(template.formats) _render_template(template, options[:layout], options) end when :update @@ -54,18 +53,23 @@ module ActionView layout = find_layout(layout) if layout ActiveSupport::Notifications.instrument("action_view.render_template", - :identifier => template.identifier, :layout => layout.try(:identifier)) do + :identifier => template.identifier, :layout => layout.try(:virtual_path)) do content = template.render(self, locals) { |*name| _layout_for(*name) } @_content_for[:layout] = content - if layout - @_layout = layout.identifier - content = _render_layout(layout, locals) - end - + content = _render_layout(layout, locals) if layout content end end + + # Freeze the current formats in the lookup context. By freezing them, you are guaranteeing + # that next template lookups are not going to modify the formats. The controller can also + # use this, to ensure that formats won't be further modified (as it does in respond_to blocks). + def _freeze_formats(formats) #:nodoc: + return if self.formats.frozen? + self.formats = formats + self.formats.freeze + end end end diff --git a/actionpack/lib/action_view/template.rb b/actionpack/lib/action_view/template.rb index b4fdb49d3b..8abc1633ff 100644 --- a/actionpack/lib/action_view/template.rb +++ b/actionpack/lib/action_view/template.rb @@ -1,8 +1,7 @@ # encoding: utf-8 # This is so that templates compiled in this file are UTF-8 - -require 'set' -require "action_view/template/resolver" +require 'active_support/core_ext/array/wrap' +require 'active_support/core_ext/object/blank' module ActionView class Template @@ -24,19 +23,20 @@ module ActionView @identifier = identifier @handler = handler - @partial = details[:partial] @virtual_path = details[:virtual_path] @method_names = {} - format = details[:format] - format ||= handler.default_format.to_sym if handler.respond_to?(:default_format) - format ||= :html - @formats = [format.to_sym] + format = details[:format] || :html + @formats = Array.wrap(format).map(&:to_sym) end def render(view, locals, &block) - method_name = compile(locals, view) - view.send(method_name, locals, &block) + # Notice that we use a bang in this instrumentation because you don't want to + # consume this in production. This is only slow if it's being listened to. + ActiveSupport::Notifications.instrument("action_view.render_template!", :virtual_path => @virtual_path) do + method_name = compile(locals, view) + view.send(method_name, locals, &block) + end rescue Exception => e if e.is_a?(Template::Error) e.sub_template_of(self) @@ -58,10 +58,6 @@ module ActionView @counter_name ||= "#{variable_name}_counter".to_sym end - def partial? - @partial - end - def inspect if defined?(Rails.root) identifier.sub("#{Rails.root}/", '') diff --git a/actionpack/lib/action_view/template/error.rb b/actionpack/lib/action_view/template/error.rb index 648f708d3d..5222ffa89c 100644 --- a/actionpack/lib/action_view/template/error.rb +++ b/actionpack/lib/action_view/template/error.rb @@ -1,6 +1,26 @@ require "active_support/core_ext/enumerable" module ActionView + class ActionViewError < StandardError #:nodoc: + end + + class MissingTemplate < ActionViewError #:nodoc: + attr_reader :path + + def initialize(paths, path, details, partial) + @path = path + display_paths = paths.compact.map{ |p| p.to_s.inspect }.join(", ") + template_type = if partial + "partial" + elsif path =~ /layouts/i + 'layout' + else + 'template' + end + + super("Missing #{template_type} #{path} with #{details.inspect} in view paths #{display_paths}") + end + end class Template # The Template::Error exception is raised when the compilation of the template fails. This exception then gathers a # bunch of intimate details and uses it to report a very precise exception message. @@ -73,11 +93,11 @@ module ActionView end def to_s - "\n#{self.class} (#{message}) #{source_location}:\n" + + "\n#{self.class} (#{message}) #{source_location}:\n" + "#{source_extract}\n #{clean_backtrace.join("\n ")}\n\n" end - # don't do anything nontrivial here. Any raised exception from here becomes fatal + # don't do anything nontrivial here. Any raised exception from here becomes fatal # (and can't be rescued). def backtrace @backtrace diff --git a/actionpack/lib/action_view/template/handlers/erb.rb b/actionpack/lib/action_view/template/handlers/erb.rb index ac5902cc0e..705c2bf82e 100644 --- a/actionpack/lib/action_view/template/handlers/erb.rb +++ b/actionpack/lib/action_view/template/handlers/erb.rb @@ -8,6 +8,13 @@ module ActionView super(value.to_s) end alias :append= :<< + + def append_if_string=(value) + if value.is_a?(String) && !value.is_a?(NonConcattingString) + ActiveSupport::Deprecation.warn("<% %> style block helpers are deprecated. Please use <%= %>", caller) + self << value + end + end end module Template::Handlers @@ -21,14 +28,24 @@ module ActionView src << "@output_buffer.safe_concat('" << escape_text(text) << "');" end + BLOCK_EXPR = /(do|\{)(\s*\|[^|]*\|)?\s*\Z/ + def add_expr_literal(src, code) - if code =~ /(do|\{)(\s*\|[^|]*\|)?\s*\Z/ + if code =~ BLOCK_EXPR src << '@output_buffer.append= ' << code else src << '@output_buffer.append= (' << code << ');' end end + def add_stmt(src, code) + if code =~ BLOCK_EXPR + src << '@output_buffer.append_if_string= ' << code + else + super + end + end + def add_expr_escaped(src, code) src << '@output_buffer.append= ' << escaped_expr(code) << ';' end diff --git a/actionpack/lib/action_view/template/resolver.rb b/actionpack/lib/action_view/template/resolver.rb index a43597e728..8e8afaa43f 100644 --- a/actionpack/lib/action_view/template/resolver.rb +++ b/actionpack/lib/action_view/template/resolver.rb @@ -1,6 +1,5 @@ require "pathname" require "active_support/core_ext/class" -require "active_support/core_ext/array/wrap" require "action_view/template" module ActionView @@ -14,15 +13,8 @@ module ActionView @cached.clear end - def find(*args) - find_all(*args).first - end - # Normalizes the arguments and passes it on to find_template. def find_all(name, prefix=nil, partial=false, details={}, key=nil) - name, prefix = normalize_name(name, prefix) - details = details.merge(:handlers => default_handlers) - cached(key, prefix, name, partial) do find_templates(name, prefix, partial, details) end @@ -34,10 +26,6 @@ module ActionView @caching ||= !defined?(Rails.application) || Rails.application.config.cache_classes end - def default_handlers - Template::Handlers.extensions + [nil] - end - # This is what child classes implement. No defaults are needed # because Resolver guarantees that the arguments are present and # normalized. @@ -45,17 +33,6 @@ module ActionView raise NotImplementedError end - # Support legacy foo.erb names even though we now ignore .erb - # as well as incorrectly putting part of the path in the template - # name instead of the prefix. - def normalize_name(name, prefix) - handlers = Template::Handlers.extensions.join('|') - name = name.to_s.gsub(/\.(?:#{handlers})$/, '') - - parts = name.split('/') - return parts.pop, [prefix, *parts].compact.join("/") - end - def cached(key, prefix, name, partial) return yield unless key && caching? scope = @cached[key][prefix][name] @@ -79,7 +56,7 @@ module ActionView def find_templates(name, prefix, partial, details) path = build_path(name, prefix, partial, details) - query(partial, path, EXTENSION_ORDER.map { |ext| details[ext] }) + query(path, EXTENSION_ORDER.map { |ext| details[ext] }, details[:formats]) end def build_path(name, prefix, partial, details) @@ -89,26 +66,32 @@ module ActionView path end - def query(partial, path, exts) + def query(path, exts, formats) query = File.join(@path, path) exts.each do |ext| - query << '{' << ext.map {|e| e && ".#{e}" }.join(',') << '}' + query << '{' << ext.map {|e| e && ".#{e}" }.join(',') << ',}' end Dir[query].reject { |p| File.directory?(p) }.map do |p| - handler, format = extract_handler_and_format(p) + handler, format = extract_handler_and_format(p, formats) Template.new(File.read(p), File.expand_path(p), handler, - :partial => partial, :virtual_path => path, :format => format) + :virtual_path => path, :format => format) end end - def extract_handler_and_format(path) + # Extract handler and formats from path. If a format cannot be a found neither + # from the path, or the handler, we should return the array of formats given + # to the resolver. + def extract_handler_and_format(path, default_formats) pieces = File.basename(path).split(".") pieces.shift - handler = Template.handler_class_for_extension(pieces.pop) - format = pieces.last && Mime[pieces.last] && pieces.pop.to_sym + handler = Template.handler_class_for_extension(pieces.pop) + format = pieces.last && Mime[pieces.last] && pieces.pop.to_sym + format ||= handler.default_format if handler.respond_to?(:default_format) + format ||= default_formats + [handler, format] end end diff --git a/actionpack/lib/action_view/test_case.rb b/actionpack/lib/action_view/test_case.rb index 1578ac9479..b0ababe344 100644 --- a/actionpack/lib/action_view/test_case.rb +++ b/actionpack/lib/action_view/test_case.rb @@ -2,29 +2,10 @@ require 'action_controller/test_case' require 'action_view' module ActionView - class Base - alias_method :initialize_without_template_tracking, :initialize - def initialize(*args) - @_rendered = { :template => nil, :partials => Hash.new(0) } - initialize_without_template_tracking(*args) - end - - attr_internal :rendered - end - - class Template - alias_method :render_without_tracking, :render - def render(view, locals, &blk) - rendered = view.rendered - rendered[:partials][self] += 1 if partial? - rendered[:template] ||= [] - rendered[:template] << self - render_without_tracking(view, locals, &blk) - end - end - class TestCase < ActiveSupport::TestCase class TestController < ActionController::Base + include ActionDispatch::TestProcess + attr_accessor :request, :response, :params def self.controller_path @@ -42,6 +23,7 @@ module ActionView end include ActionDispatch::Assertions, ActionDispatch::TestProcess + include ActionController::TemplateAssertions include ActionView::Context include ActionController::PolymorphicRoutes @@ -64,7 +46,7 @@ module ActionView end def config - @controller.config + @controller.config if @controller.respond_to?(:config) end def render(options = {}, local_assigns = {}, &block) @@ -124,7 +106,8 @@ module ActionView def _view view = ActionView::Base.new(ActionController::Base.view_paths, _assigns, @controller) - view.class.send :include, _helpers + view.singleton_class.send :include, _helpers + view.singleton_class.send :include, @controller._router.url_helpers view.output_buffer = self.output_buffer view end |