diff options
Diffstat (limited to 'actionpack/lib')
23 files changed, 212 insertions, 90 deletions
diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb index 95992c2698..1943ca4436 100644 --- a/actionpack/lib/abstract_controller/callbacks.rb +++ b/actionpack/lib/abstract_controller/callbacks.rb @@ -14,7 +14,7 @@ module AbstractController # Override AbstractController::Base's process_action to run the # process_action callbacks around the normal behavior. def process_action(method_name, *args) - run_callbacks(:process_action, method_name) do + run_callbacks(:process_action, action_name) do super end end diff --git a/actionpack/lib/abstract_controller/view_paths.rb b/actionpack/lib/abstract_controller/view_paths.rb index 6544c8949a..cea0f5ad1e 100644 --- a/actionpack/lib/abstract_controller/view_paths.rb +++ b/actionpack/lib/abstract_controller/view_paths.rb @@ -36,7 +36,7 @@ module AbstractController # ==== Parameters # * <tt>path</tt> - If a String is provided, it gets converted into # the default view path. You may also provide a custom view path - # (see ActionView::ViewPathSet for more information) + # (see ActionView::PathSet for more information) def append_view_path(path) self.view_paths = view_paths.dup + Array(path) end @@ -46,7 +46,7 @@ module AbstractController # ==== Parameters # * <tt>path</tt> - If a String is provided, it gets converted into # the default view path. You may also provide a custom view path - # (see ActionView::ViewPathSet for more information) + # (see ActionView::PathSet for more information) def prepend_view_path(path) self.view_paths = Array(path) + view_paths.dup end @@ -59,8 +59,8 @@ module AbstractController # Set the view paths. # # ==== Parameters - # * <tt>paths</tt> - If a ViewPathSet is provided, use that; - # otherwise, process the parameter into a ViewPathSet. + # * <tt>paths</tt> - If a PathSet is provided, use that; + # otherwise, process the parameter into a PathSet. def view_paths=(paths) self._view_paths = ActionView::Base.process_view_paths(paths) self._view_paths.freeze diff --git a/actionpack/lib/action_controller/caching/actions.rb b/actionpack/lib/action_controller/caching/actions.rb index a1c582560c..2c8a6e4d4d 100644 --- a/actionpack/lib/action_controller/caching/actions.rb +++ b/actionpack/lib/action_controller/caching/actions.rb @@ -80,7 +80,7 @@ module ActionController #:nodoc: # header the Content-Type of the cached response could be wrong because # no information about the MIME type is stored in the cache key. So, if # you first ask for MIME type M in the Accept header, a cache entry is - # created, and then perform a second resquest to the same resource asking + # created, and then perform a second request to the same resource asking # for a different MIME type, you'd get the content cached for M. # # The <tt>:format</tt> parameter is taken into account though. The safest diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb index b08d9a8434..dc3ea939e6 100644 --- a/actionpack/lib/action_controller/metal/instrumentation.rb +++ b/actionpack/lib/action_controller/metal/instrumentation.rb @@ -78,7 +78,7 @@ module ActionController yield end - # Everytime after an action is processed, this method is invoked + # Every time after an action is processed, this method is invoked # with the payload, so you can add more information. # :api: plugin def append_info_to_payload(payload) #:nodoc: diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 0f43527a56..bc4f8bb9ce 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -172,6 +172,10 @@ module ActionController end def recycle! + write_cookies! + @env.delete('HTTP_COOKIE') if @cookies.blank? + @env.delete('action_dispatch.cookies') + @cookies = nil @formats = nil @env.delete_if { |k, v| k =~ /^(action_dispatch|rack)\.request/ } @env.delete_if { |k, v| k =~ /^action_dispatch\.rescue/ } @@ -301,7 +305,11 @@ module ActionController # and cookies, though. For sessions, you just do: # # @request.session[:key] = "value" - # @request.cookies["key"] = "value" + # @request.cookies[:key] = "value" + # + # To clear the cookies for a test just clear the request's cookies hash: + # + # @request.cookies.clear # # == \Testing named routes # @@ -416,6 +424,7 @@ module ActionController @controller.process_with_new_base_test(@request, @response) @assigns = @controller.respond_to?(:view_assigns) ? @controller.view_assigns : {} @request.session.delete('flash') if @request.session['flash'].blank? + @request.cookies.merge!(@response.cookies) @response end diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb index 1ab48ae04d..8dd1af7f3d 100644 --- a/actionpack/lib/action_dispatch/http/filter_parameters.rb +++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb @@ -5,10 +5,10 @@ require 'active_support/core_ext/object/duplicable' module ActionDispatch module Http # Allows you to specify sensitive parameters which will be replaced from - # the request log by looking in all subhashes of the param hash for keys - # to filter. If a block is given, each key and value of the parameter - # hash and all subhashes is passed to it, the value or key can be replaced - # using String#replace or similar method. + # the request log by looking in the query string of the request and all + # subhashes of the params hash to filter. If a block is given, each key and + # value of the params hash and all subhashes is passed to it, the value + # or key can be replaced using String#replace or similar method. # # Examples: # @@ -38,6 +38,11 @@ module ActionDispatch @filtered_env ||= env_filter.filter(@env) end + # Reconstructed a path with all sensitive GET parameters replaced. + def filtered_path + @filtered_path ||= query_string.empty? ? path : "#{path}?#{filtered_query_string}" + end + protected def parameter_filter @@ -52,6 +57,14 @@ module ActionDispatch @@parameter_filter_for[filters] ||= ParameterFilter.new(filters) end + KV_RE = '[^&;=]+' + PAIR_RE = %r{(#{KV_RE})=(#{KV_RE})} + def filtered_query_string + query_string.gsub(PAIR_RE) do |_| + parameter_filter.filter([[$1, $2]]).first.join("=") + end + end + end end end diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index 535ff42b90..ac0fd9607d 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -41,7 +41,7 @@ module ActionDispatch path = options.delete(:path) || '' params = options[:params] || {} - params.reject! {|k,v| !v } + params.reject! {|k,v| v.to_param.nil? } rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path) rewritten_url << "?#{params.to_query}" unless params.empty? diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 589df218a8..14c424f24b 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -107,7 +107,7 @@ module ActionDispatch if @options[:format] == false @options.delete(:format) path - elsif path.include?(":format") + elsif path.include?(":format") || path.end_with?('/') || path.match(/^\/?\*/) path else "#{path}(.:format)" @@ -195,8 +195,8 @@ module ActionDispatch def request_method_condition if via = @options[:via] - via = Array(via).map { |m| m.to_s.dasherize.upcase } - { :request_method => %r[^#{via.join('|')}$] } + list = Array(via).map { |m| m.to_s.dasherize.upcase } + { :request_method => list } else { } end @@ -243,10 +243,6 @@ module ActionDispatch end module Base - def initialize(set) #:nodoc: - @set = set - end - # You can specify what Rails should route "/" to with the root method: # # root :to => 'pages#main' @@ -368,9 +364,17 @@ module ActionDispatch # match 'path' => 'c#a', :defaults => { :format => 'jpg' } # # See <tt>Scoping#defaults</tt> for its scope equivalent. + # + # [:anchor] + # Boolean to anchor a #match pattern. Default is true. When set to + # false, the pattern matches any request prefixed with the given path. + # + # # Matches any request starting with 'path' + # match 'path' => 'c#a', :anchor => false def match(path, options=nil) - mapping = Mapping.new(@set, @scope, path, options || {}).to_route - @set.add_route(*mapping) + mapping = Mapping.new(@set, @scope, path, options || {}) + app, conditions, requirements, defaults, as, anchor = mapping.to_route + @set.add_route(app, conditions, requirements, defaults, as, anchor) self end @@ -558,11 +562,6 @@ module ActionDispatch # PUT /admin/posts/1 # DELETE /admin/posts/1 module Scoping - def initialize(*args) #:nodoc: - @scope = {} - super - end - # Scopes a set of routes to the given default options. # # Take the following route definition as an example: @@ -700,7 +699,7 @@ module ActionDispatch # Now routes such as +/posts/1+ will no longer be valid, but +/posts/1.1+ will be. # The +id+ parameter must match the constraint passed in for this example. # - # You may use this to also resrict other parameters: + # You may use this to also restrict other parameters: # # resources :posts do # constraints(:post_id => /\d+\.\d+) do @@ -720,7 +719,7 @@ module ActionDispatch # # === Dynamic request matching # - # Requests to routes can be constrained based on specific critera: + # Requests to routes can be constrained based on specific criteria: # # constraints(lambda { |req| req.env["HTTP_USER_AGENT"] =~ /iPhone/ }) do # resources :iphones @@ -956,11 +955,6 @@ module ActionDispatch alias :nested_scope :path end - def initialize(*args) #:nodoc: - super - @scope[:path_names] = @set.resources_path_names - end - def resources_path_names(options) @scope[:path_names].merge!(options) end @@ -1473,6 +1467,11 @@ module ActionDispatch end end + def initialize(set) #:nodoc: + @set = set + @scope = { :path_names => @set.resources_path_names } + end + include Base include HttpHelpers include Redirection diff --git a/actionpack/lib/action_dispatch/routing/route.rb b/actionpack/lib/action_dispatch/routing/route.rb index 08a8408f25..a049510182 100644 --- a/actionpack/lib/action_dispatch/routing/route.rb +++ b/actionpack/lib/action_dispatch/routing/route.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/module/deprecation' + module ActionDispatch module Routing class Route #:nodoc: @@ -10,6 +12,8 @@ module ActionDispatch @defaults = defaults @name = name + # FIXME: we should not be doing this much work in a constructor. + @requirements = requirements.merge(defaults) @requirements.delete(:controller) if @requirements[:controller].is_a?(Regexp) @requirements.delete_if { |k, v| @@ -21,21 +25,22 @@ module ActionDispatch conditions[:path_info] = ::Rack::Mount::Strexp.compile(path, requirements, SEPARATORS, anchor) end - @conditions = Hash[conditions.map { |k,v| [k, Rack::Mount::RegexpWithNamedGroups.new(v)] }] + @verbs = conditions[:request_method] || [] + + @conditions = conditions.dup + + # Rack-Mount requires that :request_method be a regular expression. + # :request_method represents the HTTP verb that matches this route. + # + # Here we munge values before they get sent on to rack-mount. + @conditions[:request_method] = %r[^#{verb}$] unless @verbs.empty? + @conditions[:path_info] = Rack::Mount::RegexpWithNamedGroups.new(@conditions[:path_info]) if @conditions[:path_info] @conditions.delete_if{ |k,v| k != :path_info && !valid_condition?(k) } @requirements.delete_if{ |k,v| !valid_condition?(k) } end def verb - if method = conditions[:request_method] - case method - when Regexp - source = method.source.upcase - source =~ /\A\^[-A-Z|]+\$\Z/ ? source[1..-2] : source - else - method.to_s.upcase - end - end + @verbs.join '|' end def segment_keys @@ -45,6 +50,7 @@ module ActionDispatch def to_a [@app, @conditions, @defaults, @name] end + deprecate :to_a def to_s @to_s ||= begin diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index fc86d52a3a..b28f6c2297 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -1,5 +1,6 @@ require 'rack/mount' require 'forwardable' +require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/to_query' require 'active_support/core_ext/hash/slice' @@ -330,8 +331,9 @@ module ActionDispatch end def add_route(app, conditions = {}, requirements = {}, defaults = {}, name = nil, anchor = true) + raise ArgumentError, "Invalid route name: '#{name}'" unless name.blank? || name.to_s.match(/^[_a-z]\w*$/i) route = Route.new(self, app, conditions, requirements, defaults, name, anchor) - @set.add_route(*route) + @set.add_route(route.app, route.conditions, route.defaults, route.name) named_routes[name] = route if name routes << route route diff --git a/actionpack/lib/action_dispatch/testing/test_process.rb b/actionpack/lib/action_dispatch/testing/test_process.rb index 16f3674164..d430691429 100644 --- a/actionpack/lib/action_dispatch/testing/test_process.rb +++ b/actionpack/lib/action_dispatch/testing/test_process.rb @@ -22,7 +22,7 @@ module ActionDispatch end def cookies - HashWithIndifferentAccess.new(@request.cookies.merge(@response.cookies)) + @request.cookies.merge(@response.cookies).with_indifferent_access end def redirect_to_url diff --git a/actionpack/lib/action_dispatch/testing/test_request.rb b/actionpack/lib/action_dispatch/testing/test_request.rb index cf440a1fad..822adb6a47 100644 --- a/actionpack/lib/action_dispatch/testing/test_request.rb +++ b/actionpack/lib/action_dispatch/testing/test_request.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/object/blank' require 'active_support/core_ext/hash/reverse_merge' +require 'rack/utils' module ActionDispatch class TestRequest < Request @@ -77,10 +78,14 @@ module ActionDispatch private def write_cookies! unless @cookies.blank? - @env['HTTP_COOKIE'] = @cookies.map { |name, value| "#{name}=#{value};" }.join(' ') + @env['HTTP_COOKIE'] = @cookies.map { |name, value| escape_cookie(name, value) }.join('; ') end end + def escape_cookie(name, value) + "#{Rack::Utils.escape(name)}=#{Rack::Utils.escape(value)}" + end + def delete_nil_values! @env.delete_if { |k, v| v.nil? } end diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb index dc8e4bc316..6cd1565031 100644 --- a/actionpack/lib/action_view/helpers/date_helper.rb +++ b/actionpack/lib/action_view/helpers/date_helper.rb @@ -617,7 +617,7 @@ module ActionView @options[:discard_second] ||= true unless @options[:include_seconds] && !@options[:discard_minute] # If the day is hidden and the month is visible, the day should be set to the 1st so all month choices are - # valid (otherwise it could be 31 and february wouldn't be a valid date) + # valid (otherwise it could be 31 and February wouldn't be a valid date) if @datetime && @options[:discard_day] && !@options[:discard_month] @datetime = @datetime.change(:day => 1) end @@ -644,7 +644,7 @@ module ActionView @options[:discard_day] ||= true if @options[:discard_month] || !order.include?(:day) # If the day is hidden and the month is visible, the day should be set to the 1st so all month choices are - # valid (otherwise it could be 31 and february wouldn't be a valid date) + # valid (otherwise it could be 31 and February wouldn't be a valid date) if @datetime && @options[:discard_day] && !@options[:discard_month] @datetime = @datetime.change(:day => 1) end diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index befaa3e8d9..48abf119f1 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -6,6 +6,7 @@ require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/string/output_safety' +require 'active_support/core_ext/array/extract_options' module ActionView # = Action View Form Helpers @@ -262,6 +263,24 @@ module ActionView # ... # </form> # + # === Removing hidden model id's + # + # The form_for method automatically includes the model id as a hidden field in the form. + # This is used to maintain the correlation between the form data and its associated model. + # Some ORM systems do not use IDs on nested models so in this case you want to be able + # to disable the hidden id. + # + # In the following example the Post model has many Comments stored within it in a NoSQL database, + # thus there is no primary key for comments. + # + # Example: + # + # <%= form(@post) do |f| %> + # <% f.fields_for(:comments, :include_id => false) do |cf| %> + # ... + # <% end %> + # <% end %> + # # === Customized form builders # # You can also build forms using a customized FormBuilder class. Subclass @@ -332,7 +351,7 @@ module ActionView options[:html][:remote] = options.delete(:remote) options[:html][:authenticity_token] = options.delete(:authenticity_token) - + builder = options[:parent_builder] = instantiate_builder(object_name, object, options, &proc) fields_for = fields_for(object_name, object, options, &proc) default_options = builder.multipart? ? { :multipart => true } : {} @@ -862,9 +881,9 @@ module ActionView private - def instantiate_builder(record, record_object = nil, options = nil, &block) - options, record_object = record_object, nil if record_object.is_a?(Hash) - options ||= {} + def instantiate_builder(record, *args, &block) + options = args.extract_options! + record_object = args.shift case record when String, Symbol @@ -1326,7 +1345,9 @@ module ActionView def fields_for_nested_model(name, object, options, block) object = convert_to_model(object) - options[:hidden_field_id] = object.persisted? + parent_include_id = self.options.fetch(:include_id, true) + include_id = options.fetch(:include_id, parent_include_id) + options[:hidden_field_id] = object.persisted? && include_id @template.fields_for(name, object, options, &block) end diff --git a/actionpack/lib/action_view/helpers/form_tag_helper.rb b/actionpack/lib/action_view/helpers/form_tag_helper.rb index 71f8534cbf..49aa434020 100644 --- a/actionpack/lib/action_view/helpers/form_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/form_tag_helper.rb @@ -592,7 +592,7 @@ module ActionView options.stringify_keys.tap do |html_options| html_options["enctype"] = "multipart/form-data" if html_options.delete("multipart") # The following URL is unescaped, this is just a hash of options, and it is the - # responsability of the caller to escape all the values. + # responsibility of the caller to escape all the values. html_options["action"] = url_for(url_for_options, *parameters_for_url) html_options["accept-charset"] = "UTF-8" html_options["data-remote"] = true if html_options.delete("remote") diff --git a/actionpack/lib/action_view/helpers/number_helper.rb b/actionpack/lib/action_view/helpers/number_helper.rb index 26f8dce3c3..05a9c5b4f1 100644 --- a/actionpack/lib/action_view/helpers/number_helper.rb +++ b/actionpack/lib/action_view/helpers/number_helper.rb @@ -369,7 +369,7 @@ module ActionView # See <tt>number_to_human_size</tt> if you want to print a file size. # # You can also define you own unit-quantifier names if you want to use other decimal units - # (eg.: 1500 becomes "1.5 kilometers", 0.150 becomes "150 mililiters", etc). You may define + # (eg.: 1500 becomes "1.5 kilometers", 0.150 becomes "150 milliliters", etc). You may define # a wide range of unit quantifiers, even fractional ones (centi, deci, mili, etc). # # ==== Options @@ -425,13 +425,13 @@ module ActionView # thousand: # one: "kilometer" # other: "kilometers" - # billion: "gazilion-distance" + # billion: "gazillion-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(54393498000, :units => :distance) # => "54.4 gazillion-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" @@ -472,7 +472,7 @@ module ActionView end.keys.map{|e_name| inverted_du[e_name] }.sort_by{|e| -e} number_exponent = number != 0 ? Math.log10(number.abs).floor : 0 - display_exponent = unit_exponents.find{|e| number_exponent >= e } + display_exponent = unit_exponents.find{ |e| number_exponent >= e } || 0 number /= 10 ** display_exponent unit = case units diff --git a/actionpack/lib/action_view/helpers/prototype_helper.rb b/actionpack/lib/action_view/helpers/prototype_helper.rb index f3a429fcce..18e303778c 100644 --- a/actionpack/lib/action_view/helpers/prototype_helper.rb +++ b/actionpack/lib/action_view/helpers/prototype_helper.rb @@ -103,7 +103,7 @@ module ActionView :form, :with, :update, :script, :type ]).merge(CALLBACKS) # Returns the JavaScript needed for a remote function. - # See the link_to_remote documentation at http://github.com/rails/prototype_legacy_helper as it takes the same arguments. + # See the link_to_remote documentation at https://github.com/rails/prototype_legacy_helper as it takes the same arguments. # # Example: # # Generates: <select id="options" onchange="new Ajax.Updater('options', @@ -131,7 +131,6 @@ module ActionView "new Ajax.Updater(#{update}, " url_options = options[:url] - url_options = url_options.merge(:escape => false) if url_options.is_a?(Hash) function << "'#{ERB::Util.html_escape(escape_javascript(url_for(url_options)))}'" function << ", #{javascript_options})" diff --git a/actionpack/lib/action_view/helpers/translation_helper.rb b/actionpack/lib/action_view/helpers/translation_helper.rb index e7ec1df2c8..59e6ce878f 100644 --- a/actionpack/lib/action_view/helpers/translation_helper.rb +++ b/actionpack/lib/action_view/helpers/translation_helper.rb @@ -26,7 +26,7 @@ module ActionView # # E.g. the value returned for a missing translation key :"blog.post.title" will be # <span class="translation_missing" title="translation missing: blog.post.title">Title</span>. - # This way your views will display rather reasonableful strings but it will still + # This way your views will display rather reasonable strings but it will still # be easy to spot missing translations. # # Second, it'll scope the key by the current partial if the key starts diff --git a/actionpack/lib/action_view/locale/en.yml b/actionpack/lib/action_view/locale/en.yml index 9004e52c5b..eb816b9446 100644 --- a/actionpack/lib/action_view/locale/en.yml +++ b/actionpack/lib/action_view/locale/en.yml @@ -5,7 +5,7 @@ format: # Sets the separator between the units, for more precision (e.g. 1.0 / 2.0 == 0.5) separator: "." - # Delimets thousands (e.g. 1,000,000 is a million) (always in groups of three) + # Delimits thousands (e.g. 1,000,000 is a million) (always in groups of three) delimiter: "," # Number of decimals, behind the separator (the number 1 with a precision of 2 gives: 1.00) precision: 3 diff --git a/actionpack/lib/action_view/template/handlers/erb.rb b/actionpack/lib/action_view/template/handlers/erb.rb index 0803114f44..a36837afc8 100644 --- a/actionpack/lib/action_view/template/handlers/erb.rb +++ b/actionpack/lib/action_view/template/handlers/erb.rb @@ -63,7 +63,7 @@ module ActionView class_attribute :default_format self.default_format = Mime::HTML - # Default implemenation used. + # Default implementation used. class_attribute :erb_implementation self.erb_implementation = Erubis diff --git a/actionpack/lib/action_view/template/resolver.rb b/actionpack/lib/action_view/template/resolver.rb index 4d999fb3b2..6c1063592f 100644 --- a/actionpack/lib/action_view/template/resolver.rb +++ b/actionpack/lib/action_view/template/resolver.rb @@ -5,6 +5,25 @@ require "action_view/template" module ActionView # = Action View Resolver class Resolver + # Keeps all information about view path and builds virtual path. + class Path < String + attr_reader :name, :prefix, :partial, :virtual + alias_method :partial?, :partial + + def initialize(name, prefix, partial) + @name, @prefix, @partial = name, prefix, partial + rebuild(@name, @prefix, @partial) + end + + def rebuild(name, prefix, partial) + @virtual = "" + @virtual << "#{prefix}/" unless prefix.empty? + @virtual << (partial ? "_#{name}" : name) + + self.replace(@virtual) + end + end + cattr_accessor :caching self.caching = true @@ -36,15 +55,12 @@ module ActionView # because Resolver guarantees that the arguments are present and # normalized. def find_templates(name, prefix, partial, details) - raise NotImplementedError + raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details) method" end # Helpers that builds a path. Useful for building virtual paths. def build_path(name, prefix, partial) - path = "" - path << "#{prefix}/" unless prefix.empty? - path << (partial ? "_#{name}" : name) - path + Path.new(name, prefix, partial) end # Handles templates caching. If a key is given and caching is on @@ -97,25 +113,24 @@ module ActionView end class PathResolver < Resolver - EXTENSION_ORDER = [:locale, :formats, :handlers] + EXTENSIONS = [:locale, :formats, :handlers] + DEFAULT_PATTERN = ":prefix/:action{.:locale,}{.:formats,}{.:handlers,}" + + def initialize(pattern=nil) + @pattern = pattern || DEFAULT_PATTERN + super() + end private def find_templates(name, prefix, partial, details) path = build_path(name, prefix, partial) - query(path, EXTENSION_ORDER.map { |ext| details[ext] }, details[:formats]) + extensions = Hash[EXTENSIONS.map { |ext| [ext, details[ext]] }.flatten(0)] + query(path, extensions, details[:formats]) end def query(path, exts, formats) - query = File.join(@path, path) - - query << exts.map { |ext| - "{#{ext.compact.map { |e| ".#{e}" }.join(',')},}" - }.join - - query.gsub!(/\{\.html,/, "{.html,.text.html,") - query.gsub!(/\{\.text,/, "{.text,.text.plain,") - + query = build_query(path, exts) templates = [] sanitizer = Hash.new { |h,k| h[k] = Dir["#{File.dirname(k)}/*"] } @@ -126,12 +141,28 @@ module ActionView contents = File.open(p, "rb") {|io| io.read } templates << Template.new(contents, File.expand_path(p), handler, - :virtual_path => path, :format => format, :updated_at => mtime(p)) + :virtual_path => path.virtual, :format => format, :updated_at => mtime(p)) end templates end + # Helper for building query glob string based on resolver's pattern. + def build_query(path, exts) + query = @pattern.dup + query.gsub!(/\:prefix(\/)?/, path.prefix.empty? ? "" : "#{path.prefix}\\1") # prefix can be empty... + query.gsub!(/\:action/, path.partial? ? "_#{path.name}" : path.name) + + exts.each { |ext, variants| + query.gsub!(/\:#{ext}/, "{#{variants.compact.uniq.join(',')}}") + } + + query.gsub!(/\.{html,/, ".{html,text.html,") + query.gsub!(/\.{text,/, ".{text,text.plain,") + + File.expand_path(query, @path) + end + # Returns the file mtime from the filesystem. def mtime(p) File.stat(p).mtime @@ -149,11 +180,47 @@ module ActionView end end - # A resolver that loads files from the filesystem. + # A resolver that loads files from the filesystem. It allows to set your own + # resolving pattern. Such pattern can be a glob string supported by some variables. + # + # ==== Examples + # + # Default pattern, loads views the same way as previous versions of rails, eg. when you're + # looking for `users/new` it will produce query glob: `users/new{.{en},}{.{html,js},}{.{erb,haml,rjs},}` + # + # FileSystemResolver.new("/path/to/views", ":prefix/:action{.:locale,}{.:formats,}{.:handlers,}") + # + # This one allows you to keep files with different formats in seperated subdirectories, + # eg. `users/new.html` will be loaded from `users/html/new.erb` or `users/new.html.erb`, + # `users/new.js` from `users/js/new.erb` or `users/new.js.erb`, etc. + # + # FileSystemResolver.new("/path/to/views", ":prefix/{:formats/,}:action{.:locale,}{.:formats,}{.:handlers,}") + # + # If you don't specify pattern then the default will be used. + # + # In order to use any of the customized resolvers above in a Rails application, you just need + # to configure ActionController::Base.view_paths in an initializer, for example: + # + # ActionController::Base.view_paths = FileSystemResolver.new( + # Rails.root.join("app/views"), + # ":prefix{/:locale}/:action{.:formats,}{.:handlers,}" + # ) + # + # ==== Pattern format and variables + # + # Pattern have to be a valid glob string, and it allows you to use the + # following variables: + # + # * <tt>:prefix</tt> - usualy the controller path + # * <tt>:action</tt> - name of the action + # * <tt>:locale</tt> - possible locale versions + # * <tt>:formats</tt> - possible request formats (for example html, json, xml...) + # * <tt>:handlers</tt> - possible handlers (for example erb, haml, builder...) + # class FileSystemResolver < PathResolver - def initialize(path) + def initialize(path, pattern=nil) raise ArgumentError, "path already is a Resolver class" if path.is_a?(Resolver) - super() + super(pattern) @path = File.expand_path(path) end diff --git a/actionpack/lib/action_view/test_case.rb b/actionpack/lib/action_view/test_case.rb index 2ce109ea99..3e2ddffa16 100644 --- a/actionpack/lib/action_view/test_case.rb +++ b/actionpack/lib/action_view/test_case.rb @@ -185,6 +185,7 @@ module ActionView @request @routes @templates + @options @test_passed @view @view_context_class diff --git a/actionpack/lib/action_view/testing/resolvers.rb b/actionpack/lib/action_view/testing/resolvers.rb index 5c5cab7c7d..773dfcbb1d 100644 --- a/actionpack/lib/action_view/testing/resolvers.rb +++ b/actionpack/lib/action_view/testing/resolvers.rb @@ -8,8 +8,8 @@ module ActionView #:nodoc: class FixtureResolver < PathResolver attr_reader :hash - def initialize(hash = {}) - super() + def initialize(hash = {}, pattern=nil) + super(pattern) @hash = hash end @@ -21,8 +21,8 @@ module ActionView #:nodoc: def query(path, exts, formats) query = "" - exts.each do |ext| - query << '(' << ext.map {|e| e && Regexp.escape(".#{e}") }.join('|') << '|)' + EXTENSIONS.each do |ext| + query << '(' << exts[ext].map {|e| e && Regexp.escape(".#{e}") }.join('|') << '|)' end query = /^(#{Regexp.escape(path)})#{query}$/ @@ -32,9 +32,9 @@ module ActionView #:nodoc: next unless _path =~ query handler, format = extract_handler_and_format(_path, formats) templates << Template.new(source, _path, handler, - :virtual_path => $1, :format => format, :updated_at => updated_at) + :virtual_path => path.virtual, :format => format, :updated_at => updated_at) end - + templates.sort_by {|t| -t.identifier.match(/^#{query}$/).captures.reject(&:blank?).size } end end |