diff options
author | Gonçalo Silva <goncalossilva@gmail.com> | 2011-03-24 17:21:17 +0000 |
---|---|---|
committer | Gonçalo Silva <goncalossilva@gmail.com> | 2011-03-24 17:21:17 +0000 |
commit | 9887f238871bb2dd73de6ce8855615bcc5d8d079 (patch) | |
tree | 74fa9ff9524a51701cfa23f708b3f777c65b7fe5 /actionpack | |
parent | aff821508a16245ebc03510ba29c70379718dfb7 (diff) | |
parent | 5214e73850916de3c9127d35a4ecee0424d364a3 (diff) | |
download | rails-9887f238871bb2dd73de6ce8855615bcc5d8d079.tar.gz rails-9887f238871bb2dd73de6ce8855615bcc5d8d079.tar.bz2 rails-9887f238871bb2dd73de6ce8855615bcc5d8d079.zip |
Merge branch 'master' of https://github.com/rails/rails
Diffstat (limited to 'actionpack')
327 files changed, 11931 insertions, 7310 deletions
diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG index 81abb8b5f1..5ab92c8cfc 100644 --- a/actionpack/CHANGELOG +++ b/actionpack/CHANGELOG @@ -1,4 +1,74 @@ -*Rails 3.0.0 [release candidate] (July 26th, 2010)* +*Rails 3.1.0 (unreleased)* + +* Sensitive query string parameters (specified in config.filter_parameters) will now be filtered out from the request paths in the log file. [Prem Sichanugrist, fxn] + +* URL parameters which return false for to_param now appear in the query string (previously they were removed) [Andrew White] + +* URL parameters which return nil for to_param are now removed from the query string [Andrew White] + +* ActionDispatch::MiddlewareStack now uses composition over inheritance. It is +no longer an array which means there may be methods missing that were not +tested. + +* Add an :authenticity_token option to form_tag for custom handling or to omit the token (pass :authenticity_token => false). [Jakub Kuźma, Igor Wiedler] + +* HTML5 button_tag helper. [Rizwan Reza] + +* Template lookup now searches further up in the inheritance chain. [Artemave] + +* Brought back config.action_view.cache_template_loading, which allows to decide whether templates should be cached or not. [Piotr Sarnacki] + +* url_for and named url helpers now accept :subdomain and :domain as options, [Josh Kalderimis] + +* The redirect route method now also accepts a hash of options which will only change the parts of the url in question, or an object which responds to call, allowing for redirects to be reused (check the documentation for examples). [Josh Kalderimis] + +* Added config.action_controller.include_all_helpers. By default 'helper :all' is done in ActionController::Base, which includes all the helpers by default. Setting include_all_helpers to false will result in including only application_helper and helper corresponding to controller (like foo_helper for foo_controller). [Piotr Sarnacki] + +* Added a convenience idiom to generate HTML5 data-* attributes in tag helpers from a :data hash of options: + + tag("div", :data => {:name => 'Stephen', :city_state => %w(Chicago IL)}) + # => <div data-name="Stephen" data-city-state="["Chicago","IL"]" /> + + Keys are dasherized. Values are JSON-encoded, except for strings and symbols. [Stephen Celis] + +* Added render :once. You can pass either a string or an array of strings and Rails will ensure they each of them are rendered just once. [José Valim] + +* Deprecate old template handler API. The new API simply requires a template handler to respond to call. [José Valim] + +* :rhtml and :rxml were finally removed as template handlers. [José Valim] + +* Moved etag responsibility from ActionDispatch::Response to the middleware stack. [José Valim] + +* Rely on Rack::Session stores API for more compatibility across the Ruby world. This is backwards incompatible since Rack::Session expects #get_session to accept 4 arguments and requires #destroy_session instead of simply #destroy. [José Valim] + +* file_field automatically adds :multipart => true to the enclosing form. [Santiago Pastorino] + +* Renames csrf_meta_tag -> csrf_meta_tags, and aliases csrf_meta_tag for backwards compatibility. [fxn] + +* Add Rack::Cache to the default stack. Create a Rails store that delegates to the Rails cache, so by default, whatever caching layer you are using will be used for HTTP caching. Note that Rack::Cache will be used if you use #expires_in, #fresh_when or #stale with :public => true. Otherwise, the caching rules will apply to the browser only. [Yehuda Katz, Carl Lerche] + + +*Rails 3.0.2 (unreleased)* + +* The helper number_to_currency accepts a new :negative_format option to be able to configure how to render negative amounts. [Don Wilson] + + +*Rails 3.0.1 (October 15, 2010)* + +* No Changes, just a version bump. + + +*Rails 3.0.0 (August 29, 2010)* + +* password_field renders with nil value by default making the use of passwords secure by default, if you want to render you should do for instance f.password_field(:password, :value => @user.password) [Santiago Pastorino] + +* Symbols and strings in routes should yield the same behavior. Note this may break existing apps that were using symbols with the new routes API. [José Valim] + +* Add clear_helpers as a way to clean up all helpers added to this controller, maintaining just the helper with the same name as the controller. [José Valim] + +* Support routing constraints in functional tests. [Andrew White] + +* Add a header that tells Internet Explorer (all versions) to use the best available standards support. [Yehuda Katz] * Allow stylesheet/javascript extensions to be changed through railties. [Josh Kalderimis] @@ -26,16 +96,13 @@ resources :comments end end - + You can now use comment_path for /comments/1 instead of post_comment_path for /posts/1/comments/1. * Add support for multi-subdomain session by setting cookie host in session cookie so you can share session between www.example.com, example.com and user.example.com. #4818 [Guillermo Álvarez] * Removed textilize, textilize_without_paragraph and markdown helpers. [Santiago Pastorino] - -*Rails 3.0.0 [beta 4] (June 8th, 2010)* - * Remove middleware laziness [José Valim] * Make session stores rely on request.cookie_jar and change set_session semantics to return the cookie value instead of a boolean. [José Valim] @@ -52,9 +119,6 @@ * Changed translate helper so that it doesn’t mark every translation as safe HTML. Only keys with a "_html" suffix and keys named "html" are considered to be safe HTML. All other translations are left untouched. [Craig Davey] - -*Rails 3.0.0 [beta 3] (April 13th, 2010)* - * New option :as added to form_for allows to change the object name. The old <% form_for :client, @post %> becomes <% form_for @post, :as => :client %> [spastorino] * Removed verify method in controllers. [JV] @@ -89,9 +153,6 @@ "HEAD" and #request_method returns "GET" in HEAD requests). This is for compatibility with Rack::Request [YK] - -*Rails 3.0.0 [beta 2] (April 1st, 2010)* - * #concat is now deprecated in favor of using <%= %> helpers [YK] * Block helpers now return Strings, so you can use <%= form_for @foo do |f| %>. @@ -110,7 +171,7 @@ * ActionDispatch::Request#content_type returns a String to be compatible with Rack::Request. Use #content_mime_type for the Mime::Type instance [YK] -* Updated Prototype to 1.6.1 and Scriptaculous to 1.8.3 [ML] +* Updated Prototype to 1.6.1 and Scriptaculous to 1.8.3 [ML] * Change the preferred way that URL helpers are included into a class[YK & CL] @@ -120,9 +181,6 @@ # for just url_for include Rails.application.router.url_for - -*Rails 3.0.0 [beta 1] (February 4, 2010)* - * Fixed that PrototypeHelper#update_page should return html_safe [DHH] * Fixed that much of DateHelper wouldn't return html_safe? strings [DHH] diff --git a/actionpack/MIT-LICENSE b/actionpack/MIT-LICENSE index a345a2419d..7ad1051066 100644 --- a/actionpack/MIT-LICENSE +++ b/actionpack/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2010 David Heinemeier Hansson +Copyright (c) 2004-2011 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actionpack/README.rdoc b/actionpack/README.rdoc index b297ceb0e2..3661d27d51 100644 --- a/actionpack/README.rdoc +++ b/actionpack/README.rdoc @@ -102,10 +102,10 @@ A short rundown of some of the major features: class WeblogController < ActionController::Base # filters as methods before_filter :authenticate, :cache, :audit - + # filter as a proc after_filter { |c| c.response.body = Gzip::compress(c.response.body) } - + # class filter after_filter LocalizeFilter @@ -262,7 +262,7 @@ methods: layout "weblog/layout" def index - @posts = Post.find(:all) + @posts = Post.all end def show @@ -323,7 +323,7 @@ The latest version of Action Pack can be installed with Rubygems: Source code can be downloaded as part of the Rails project on GitHub -* http://github.com/rails/rails/tree/master/actionpack/ +* https://github.com/rails/rails/tree/master/actionpack/ == License diff --git a/actionpack/RUNNING_UNIT_TESTS b/actionpack/RUNNING_UNIT_TESTS index 95a8bc7497..1e3ba7abe7 100644 --- a/actionpack/RUNNING_UNIT_TESTS +++ b/actionpack/RUNNING_UNIT_TESTS @@ -1,22 +1,25 @@ == Running with Rake The easiest way to run the unit tests is through Rake. The default task runs -the entire test suite for all classes. For more information, checkout the +the entire test suite for all classes. For more information, checkout the full array of rake tasks with "rake -T" Rake can be found at http://rake.rubyforge.org == Running by hand -If you only want to run a single test suite, or don't want to bother with Rake, -you can do so with something like: +To run a single test suite - ruby -Itest test/controller/base_tests.rb + rake test TEST=path/to/test.rb -== Dependency on ActiveRecord and database setup +which can be further narrowed down to one test: + + rake test TEST=path/to/test.rb TESTOPTS="--name=test_something" + +== Dependency on Active Record and database setup Test cases in the test/controller/active_record/ directory depend on having -activerecord and sqlite installed. If ActiveRecord is not in +activerecord and sqlite installed. If Active Record is not in actionpack/../activerecord directory, or the sqlite rubygem is not installed, these tests are skipped. diff --git a/actionpack/Rakefile b/actionpack/Rakefile index 4af8ea167a..9030db9f7a 100644..100755 --- a/actionpack/Rakefile +++ b/actionpack/Rakefile @@ -1,8 +1,5 @@ -gem 'rdoc', '>= 2.5.9' -require 'rdoc' -require 'rake' +#!/usr/bin/env rake require 'rake/testtask' -require 'rdoc/task' require 'rake/packagetask' require 'rake/gempackagetask' @@ -21,7 +18,8 @@ Rake::TestTask.new(:test_action_pack) do |t| # this will not happen automatically and the tests (as a whole) will error t.test_files = Dir.glob('test/{abstract,controller,dispatch,template}/**/*_test.rb').sort - # t.warning = true + t.warning = true + t.verbose = true end namespace :test do @@ -36,24 +34,6 @@ Rake::TestTask.new(:test_active_record_integration) do |t| t.test_files = Dir.glob("test/activerecord/*_test.rb") end -# Genereate the RDoc documentation - -RDoc::Task.new { |rdoc| - rdoc.rdoc_dir = 'doc' - rdoc.title = "Action Pack -- On rails from request to response" - rdoc.options << '--charset' << 'utf-8' - rdoc.options << '-f' << 'horo' - rdoc.options << '--main' << 'README.rdoc' - if ENV['DOC_FILES'] - rdoc.rdoc_files.include(ENV['DOC_FILES'].split(/,\s*/)) - else - rdoc.rdoc_files.include('README.rdoc', 'RUNNING_UNIT_TESTS', 'CHANGELOG') - rdoc.rdoc_files.include(Dir['lib/**/*.rb'] - - Dir['lib/*/vendor/**/*.rb']) - rdoc.rdoc_files.exclude('lib/actionpack.rb') - end -} - spec = eval(File.read('actionpack.gemspec')) Rake::GemPackageTask.new(spec) do |p| diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec index 99deff234c..f6bc5e0d37 100644 --- a/actionpack/actionpack.gemspec +++ b/actionpack/actionpack.gemspec @@ -19,13 +19,14 @@ Gem::Specification.new do |s| s.has_rdoc = true - s.add_dependency('activesupport', version) - s.add_dependency('activemodel', version) - s.add_dependency('builder', '~> 2.1.2') - s.add_dependency('i18n', '~> 0.4.1') - s.add_dependency('rack', '~> 1.2.1') - s.add_dependency('rack-test', '~> 0.5.4') - s.add_dependency('rack-mount', '~> 0.6.9') - s.add_dependency('tzinfo', '~> 0.3.22') - s.add_dependency('erubis', '~> 2.6.6') + s.add_dependency('activesupport', version) + s.add_dependency('activemodel', version) + s.add_dependency('rack-cache', '~> 1.0.0') + s.add_dependency('builder', '~> 3.0.0') + s.add_dependency('i18n', '~> 0.5.0') + s.add_dependency('rack', '~> 1.2.1') + s.add_dependency('rack-test', '~> 0.5.7') + s.add_dependency('rack-mount', '~> 0.6.13') + s.add_dependency('tzinfo', '~> 0.3.23') + s.add_dependency('erubis', '~> 2.6.6') end diff --git a/actionpack/lib/abstract_controller.rb b/actionpack/lib/abstract_controller.rb index c565c940a1..cc5878c88e 100644 --- a/actionpack/lib/abstract_controller.rb +++ b/actionpack/lib/abstract_controller.rb @@ -2,6 +2,7 @@ activesupport_path = File.expand_path('../../../activesupport/lib', __FILE__) $:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path) require 'action_pack' +require 'active_support/concern' require 'active_support/ruby/shim' require 'active_support/dependencies/autoload' require 'active_support/core_ext/class/attribute' @@ -23,4 +24,5 @@ module AbstractController autoload :Translation autoload :AssetPaths autoload :ViewPaths + autoload :UrlFor end diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb index db0a6736e0..c384fd0978 100644 --- a/actionpack/lib/abstract_controller/base.rb +++ b/actionpack/lib/abstract_controller/base.rb @@ -6,9 +6,14 @@ module AbstractController class Error < StandardError; end class ActionNotFound < StandardError; end + # <tt>AbstractController::Base</tt> is a low-level API. Nobody should be + # using it directly, and subclasses (like ActionController::Base) are + # expected to provide their own +render+ method, since rendering means + # different things depending on the context. class Base attr_internal :response_body attr_internal :action_name + attr_internal :formats include ActiveSupport::Configurable extend ActiveSupport::DescendantsTracker @@ -26,23 +31,21 @@ module AbstractController # A list of all internal methods for a controller. This finds the first # abstract superclass of a controller, and gets a list of all public # instance methods on that abstract class. Public instance methods of - # a controller would normally be considered action methods, so we - # are removing those methods on classes declared as abstract - # (ActionController::Metal and ActionController::Base are defined - # as abstract) + # a controller would normally be considered action methods, so methods + # declared on abstract classes are being removed. + # (ActionController::Metal and ActionController::Base are defined as abstract) def internal_methods controller = self controller = controller.superclass until controller.abstract? controller.public_instance_methods(true) end - # The list of hidden actions to an empty Array. Defaults to an - # empty Array. This can be modified by other modules or subclasses + # The list of hidden actions to an empty array. Defaults to an + # empty array. This can be modified by other modules or subclasses # to specify particular actions as hidden. # # ==== Returns - # Array[String]:: An array of method names that should not be - # considered actions. + # * <tt>array</tt> - An array of method names that should not be considered actions. def hidden_actions [] end @@ -54,18 +57,17 @@ module AbstractController # itself. Finally, #hidden_actions are removed. # # ==== Returns - # Array[String]:: A list of all methods that should be considered - # actions. + # * <tt>array</tt> - A list of all methods that should be considered actions. def action_methods @action_methods ||= begin # All public instance methods of this class, including ancestors - methods = public_instance_methods(true).map { |m| m.to_s }.to_set - + methods = (public_instance_methods(true) - # Except for public instance methods of Base and its ancestors - internal_methods.map { |m| m.to_s } + + internal_methods + # Be sure to include shadowed public instance methods of this class - public_instance_methods(false).map { |m| m.to_s } - + public_instance_methods(false)).uniq.map { |x| x.to_s } - # And always exclude explicitly hidden actions - hidden_actions + hidden_actions.to_a # Clear out AS callback method pollution methods.reject { |method| method =~ /_one_time_conditions/ } @@ -84,7 +86,7 @@ module AbstractController # controller_name. # # ==== Returns - # String + # * <tt>string</tt> def controller_path @controller_path ||= name.sub(/Controller$/, '').underscore unless anonymous? end @@ -104,12 +106,12 @@ module AbstractController # ActionNotFound error is raised. # # ==== Returns - # self + # * <tt>self</tt> def process(action, *args) @_action_name = action_name = action.to_s unless action_name = method_for_action(action_name) - raise ActionNotFound, "The action '#{action}' could not be found for #{self.class.name}" + raise ActionNotFound, "The action '#{action}' could not be found for #{self.class.name}" end @_response_body = nil @@ -133,10 +135,10 @@ module AbstractController # can be considered an action. # # ==== Parameters - # name<String>:: The name of an action to be tested + # * <tt>name</tt> - The name of an action to be tested # # ==== Returns - # TrueClass, FalseClass + # * <tt>TrueClass</tt>, <tt>FalseClass</tt> def action_method?(name) self.class.action_methods.include?(name) end @@ -180,11 +182,11 @@ module AbstractController # returns nil, an ActionNotFound exception will be raised. # # ==== Parameters - # action_name<String>:: An action name to find a method name for + # * <tt>action_name</tt> - An action name to find a method name for # # ==== Returns - # String:: The name of the method that handles the action - # nil:: No method name could be found. Raise ActionNotFound. + # * <tt>string</tt> - The name of the method that handles the action + # * <tt>nil</tt> - No method name could be found. Raise ActionNotFound. def method_for_action(action_name) if action_method?(action_name) then action_name elsif respond_to?(:action_missing, true) then "_handle_action_missing" diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb index 67efeb7063..1943ca4436 100644 --- a/actionpack/lib/abstract_controller/callbacks.rb +++ b/actionpack/lib/abstract_controller/callbacks.rb @@ -13,8 +13,8 @@ module AbstractController # Override AbstractController::Base's process_action to run the # process_action callbacks around the normal behavior. - def process_action(method_name) - run_callbacks(:process_action, method_name) do + def process_action(method_name, *args) + run_callbacks(:process_action, action_name) do super end end @@ -28,9 +28,8 @@ module AbstractController # a Rails process. # # ==== Options - # :only<#to_s>:: The callback should be run only for this action - # :except<#to_s>:: The callback should be run for all actions - # except this action + # * <tt>only</tt> - The callback should be run only for this action + # * <tt>except<tt> - The callback should be run for all actions except this action def _normalize_callback_options(options) if only = options[:only] only = Array(only).map {|o| "action_name == '#{o}'"}.join(" || ") @@ -45,7 +44,7 @@ module AbstractController # Skip before, after, and around filters matching any of the names # # ==== Parameters - # *names<Object>:: A list of valid names that could be used for + # * <tt>names</tt> - A list of valid names that could be used for # callbacks. Note that skipping uses Ruby equality, so it's # impossible to skip a callback defined using an anonymous proc # using #skip_filter @@ -60,13 +59,13 @@ module AbstractController # the normalization across several methods that use it. # # ==== Parameters - # callbacks<Array[*Object, Hash]>:: A list of callbacks, with an optional + # * <tt>callbacks</tt> - An array of callbacks, with an optional # options hash as the last parameter. - # block<Proc>:: A proc that should be added to the callbacks. + # * <tt>block</tt> - A proc that should be added to the callbacks. # # ==== Block Parameters - # name<Symbol>:: The callback to be added - # options<Hash>:: A list of options to be used when adding the callback + # * <tt>name</tt> - The callback to be added + # * <tt>options</tt> - A hash of options to be used when adding the callback def _insert_callbacks(callbacks, block) options = callbacks.last.is_a?(Hash) ? callbacks.pop : {} _normalize_callback_options(options) @@ -84,6 +83,7 @@ module AbstractController # for details on the allowed parameters. def #{filter}_filter(*names, &blk) _insert_callbacks(names, blk) do |name, options| + options[:if] = (Array.wrap(options[:if]) << "!halted") if #{filter == :after} set_callback(:process_action, :#{filter}, name, options) end end @@ -92,6 +92,7 @@ module AbstractController # for details on the allowed parameters. def prepend_#{filter}_filter(*names, &blk) _insert_callbacks(names, blk) do |name, options| + options[:if] = (Array.wrap(options[:if]) << "!halted") if #{filter == :after} set_callback(:process_action, :#{filter}, name, options.merge(:prepend => true)) end end diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb index 4374b439d0..20f8601a8e 100644 --- a/actionpack/lib/abstract_controller/helpers.rb +++ b/actionpack/lib/abstract_controller/helpers.rb @@ -9,6 +9,9 @@ module AbstractController included do class_attribute :_helpers self._helpers = Module.new + + class_attribute :_helper_methods + self._helper_methods = Array.new end module ClassMethods @@ -40,10 +43,13 @@ module AbstractController # <% if logged_in? -%>Welcome, <%= current_user.name %><% end -%> # # ==== Parameters - # meths<Array[#to_s]>:: The name of a method on the controller + # * <tt>method[, method]</tt> - A name or names of a method on the controller # to be made available on the view. def helper_method(*meths) - meths.flatten.each do |meth| + meths.flatten! + self._helper_methods += meths + + meths.each do |meth| _helpers.class_eval <<-ruby_eval, __FILE__, __LINE__ + 1 def #{meth}(*args, &blk) controller.send(%(#{meth}), *args, &blk) @@ -55,8 +61,8 @@ module AbstractController # The +helper+ class method can take a series of helper module names, a block, or both. # # ==== Parameters - # *args<Array[Module, Symbol, String, :all]> - # block<Block>:: A block defining helper methods + # * <tt>*args</tt> - Module, Symbol, String, :all + # * <tt>block</tt> - A block defining helper methods # # ==== Examples # When the argument is a module it will be included directly in the template class. @@ -95,12 +101,23 @@ module AbstractController _helpers.module_eval(&block) if block_given? end + # Clears up all existing helpers in this class, only keeping the helper + # with the same name as this class. + def clear_helpers + inherited_helper_methods = _helper_methods + self._helpers = Module.new + self._helper_methods = Array.new + + inherited_helper_methods.each { |meth| helper_method meth } + default_helper_module! unless anonymous? + end + private # Makes all the (instance) methods in the helper module available to templates # rendered through this controller. # # ==== Parameters - # mod<Module>:: The module to include into the current helper module + # * <tt>module</tt> - The module to include into the current helper module # for the class def add_template_helper(mod) _helpers.module_eval { include mod } @@ -118,10 +135,10 @@ module AbstractController # are returned. # # ==== Parameters - # args<Array[String, Symbol, Module]>:: A list of helpers + # * <tt>args</tt> - An array of helpers # # ==== Returns - # Array[Module]:: A normalized list of modules for the list of + # * <tt>Array</tt> - A normalized list of modules for the list of # helpers provided. def modules_for_helpers(args) args.flatten.map! do |arg| diff --git a/actionpack/lib/abstract_controller/layouts.rb b/actionpack/lib/abstract_controller/layouts.rb index 5cd7a90ab5..4ee54474cc 100644 --- a/actionpack/lib/abstract_controller/layouts.rb +++ b/actionpack/lib/abstract_controller/layouts.rb @@ -114,11 +114,13 @@ module AbstractController # # class WeblogController < ActionController::Base # layout proc{ |controller| controller.logged_in? ? "writer_layout" : "reader_layout" } + # end # # Of course, the most common way of specifying a layout is still just as a plain template name: # # class WeblogController < ActionController::Base # layout "weblog_standard" + # end # # If no directory is specified for the template name, the template will by default be looked for in <tt>app/views/layouts/</tt>. # Otherwise, it will be looked up relative to the template root. @@ -183,7 +185,7 @@ module AbstractController # layout. # # ==== Returns - # Boolean:: True if the action has a layout, false otherwise. + # * <tt> Boolean</tt> - True if the action has a layout, false otherwise. def action_has_layout? return unless super @@ -209,11 +211,11 @@ module AbstractController # true:: raise an ArgumentError # # ==== Parameters - # layout<String, Symbol, false)>:: The layout to use. + # * <tt>String, Symbol, false</tt> - The layout to use. # # ==== Options (conditions) - # :only<#to_s, Array[#to_s]>:: A list of actions to apply this layout to. - # :except<#to_s, Array[#to_s]>:: Apply this layout to all actions but this one + # * :only - A list of actions to apply this layout to. + # * :except - Apply this layout to all actions but this one. def layout(layout, conditions = {}) include LayoutConditions unless conditions.empty? @@ -228,18 +230,15 @@ module AbstractController # value of this method. # # ==== Returns - # String:: A template name + # * <tt>String</tt> - A template name def _implied_layout_name controller_path end - # Takes the specified layout and creates a _layout method to be called - # by _default_layout + # Creates a _layout method to be called by _default_layout . # - # If there is no explicit layout specified: - # If a layout is found in the view paths with the controller's - # name, return that string. Otherwise, use the superclass' - # layout (which might also be implied) + # If a layout is not explicitly mentioned then look for a layout with the controller's name. + # if nothing is found then try same procedure to find super class's layout. def _write_layout_method remove_possible_method(:_layout) @@ -266,11 +265,11 @@ module AbstractController raise ArgumentError, "Layouts must be specified as a String, Symbol, false, or nil" when nil if name - _prefix = "layouts" unless _implied_layout_name =~ /\blayouts/ + _prefixes = _implied_layout_name =~ /\blayouts/ ? [] : ["layouts"] self.class_eval <<-RUBY, __FILE__, __LINE__ + 1 def _layout - if template_exists?("#{_implied_layout_name}", #{_prefix.inspect}) + if template_exists?("#{_implied_layout_name}", #{_prefixes.inspect}) "#{_implied_layout_name}" else super @@ -313,8 +312,8 @@ module AbstractController # the name type. # # ==== Parameters - # name<String|TrueClass|FalseClass|Symbol>:: The name of the template - # details<Hash{Symbol => Object}>:: A list of details to restrict + # * <tt>name</tt> - The name of the template + # * <tt>details</tt> - A list of details to restrict # the lookup to. By default, layout lookup is limited to the # formats specified for the current request. def _layout_for_option(name) @@ -333,20 +332,19 @@ module AbstractController # Optionally raises an exception if the layout could not be found. # # ==== Parameters - # details<Hash>:: A list of details to restrict the search by. This + # * <tt>details</tt> - A list of details to restrict the search by. This # might include details like the format or locale of the template. - # require_layout<Boolean>:: If this is true, raise an ArgumentError + # * <tt>require_logout</tt> - If this is true, raise an ArgumentError # with details about the fact that the exception could not be # found (defaults to false) # # ==== Returns - # Template:: The template object for the default layout (or nil) + # * <tt>template</tt> - The template object for the default layout (or nil) def _default_layout(require_layout = false) begin 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" + raise e, "Could not render layout: #{e.message}" end if require_layout && action_has_layout? && !layout_name diff --git a/actionpack/lib/abstract_controller/railties/routes_helpers.rb b/actionpack/lib/abstract_controller/railties/routes_helpers.rb new file mode 100644 index 0000000000..dec1e9d6d9 --- /dev/null +++ b/actionpack/lib/abstract_controller/railties/routes_helpers.rb @@ -0,0 +1,18 @@ +module AbstractController + module Railties + module RoutesHelpers + def self.with(routes) + Module.new do + define_method(:inherited) do |klass| + super(klass) + if namespace = klass.parents.detect {|m| m.respond_to?(:_railtie) } + klass.send(:include, namespace._railtie.routes.url_helpers) + else + klass.send(:include, routes.url_helpers) + end + end + end + end + end + end +end diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index b81d5954eb..691310d5d2 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -12,16 +12,16 @@ module AbstractController # This is a class to fix I18n global state. Whenever you provide I18n.locale during a request, # it will trigger the lookup_context and consequently expire the cache. - # TODO Add some deprecation warnings to remove I18n.locale from controllers class I18nProxy < ::I18n::Config #:nodoc: - attr_reader :i18n_config, :lookup_context + attr_reader :original_config, :lookup_context - def initialize(i18n_config, lookup_context) - @i18n_config, @lookup_context = i18n_config, lookup_context + def initialize(original_config, lookup_context) + original_config = original_config.original_config if original_config.respond_to?(:original_config) + @original_config, @lookup_context = original_config, lookup_context end def locale - @i18n_config.locale + @original_config.locale end def locale=(value) @@ -47,19 +47,34 @@ module AbstractController @view_context_class ||= begin controller = self Class.new(ActionView::Base) do + if controller.respond_to?(:_routes) && controller._routes + include controller._routes.url_helpers + include controller._routes.mounted_helpers + end + if controller.respond_to?(:_helpers) include controller._helpers - if controller.respond_to?(:_routes) - include controller._routes.url_helpers - end - # TODO: Fix RJS to not require this self.helpers = controller._helpers end end end end + + def parent_prefixes + @parent_prefixes ||= begin + parent_controller = superclass + prefixes = [] + + until parent_controller.abstract? + prefixes << parent_controller.controller_path + parent_controller = parent_controller.superclass + end + + prefixes + end + end end attr_writer :view_context_class @@ -98,7 +113,7 @@ module AbstractController def render_to_string(*args, &block) options = _normalize_args(*args, &block) _normalize_options(options) - render_to_body(options) + render_to_body(options).tap { self.response_body = nil } end # Raw rendering of a template to a Rack-compatible body. @@ -114,12 +129,15 @@ module AbstractController view_context.render(options) end - # The prefix used in render "foo" shortcuts. - def _prefix - controller_path + # The prefixes used in render "foo" shortcuts. + def _prefixes + @_prefixes ||= begin + parent_prefixes = self.class.parent_prefixes + parent_prefixes.dup.unshift(controller_path) + end end - private + private # This method should return a hash with assigns. # You can overwrite this configuration per controller. @@ -128,7 +146,7 @@ module AbstractController 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) } + variables.each { |name| hash[name.to_s[1, name.length]] = instance_variable_get(name) } hash end @@ -138,13 +156,13 @@ module AbstractController case action when NilClass when Hash - options, action = action, nil + options = action when String, Symbol action = action.to_s key = action.include?(?/) ? :file : :action options[key] = action else - options.merge!(:partial => action) + options[:partial] = action end options @@ -155,8 +173,8 @@ module AbstractController options[:partial] = action_name end - if (options.keys & [:partial, :file, :template]).empty? - options[:prefix] ||= _prefix + if (options.keys & [:partial, :file, :template, :once]).empty? + options[:prefixes] ||= _prefixes end options[:template] ||= (options[:action] || action_name).to_s diff --git a/actionpack/lib/abstract_controller/url_for.rb b/actionpack/lib/abstract_controller/url_for.rb new file mode 100644 index 0000000000..e5d5bef6b4 --- /dev/null +++ b/actionpack/lib/abstract_controller/url_for.rb @@ -0,0 +1,27 @@ +module AbstractController + module UrlFor + extend ActiveSupport::Concern + include ActionDispatch::Routing::UrlFor + + def _routes + raise "In order to use #url_for, you must include routing helpers explicitly. " \ + "For instance, `include Rails.application.routes.url_helpers" + end + + module ClassMethods + def _routes + nil + end + + def action_methods + @action_methods ||= begin + if _routes + super - _routes.named_routes.helper_names + else + super + end + end + end + end + end +end diff --git a/actionpack/lib/abstract_controller/view_paths.rb b/actionpack/lib/abstract_controller/view_paths.rb index b552a649d1..cea0f5ad1e 100644 --- a/actionpack/lib/abstract_controller/view_paths.rb +++ b/actionpack/lib/abstract_controller/view_paths.rb @@ -34,9 +34,9 @@ module AbstractController # Append a path to the list of view paths for this controller. # # ==== Parameters - # path<String, ViewPath>:: 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) + # * <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::PathSet for more information) def append_view_path(path) self.view_paths = view_paths.dup + Array(path) end @@ -44,9 +44,9 @@ module AbstractController # Prepend a path to the list of view paths for this controller. # # ==== Parameters - # path<String, ViewPath>:: 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) + # * <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::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 - # paths<ViewPathSet, Object>:: 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.rb b/actionpack/lib/action_controller.rb index ca0e5d6ff6..5b81cd39f4 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -34,7 +34,6 @@ module ActionController autoload :UrlFor end - autoload :Dispatcher, 'action_controller/deprecated/dispatcher' autoload :Integration, 'action_controller/deprecated/integration_test' autoload :IntegrationTest, 'action_controller/deprecated/integration_test' autoload :PerformanceTest, 'action_controller/deprecated/performance_test' @@ -73,4 +72,5 @@ require 'active_support/core_ext/load_error' require 'active_support/core_ext/module/attr_internal' require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/name_error' +require 'active_support/core_ext/uri' require 'active_support/inflector' diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 9dfffced75..81c0698fb8 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -1,6 +1,171 @@ require "action_controller/log_subscriber" module ActionController + # Action Controllers are the core of a web request in \Rails. They are made up of one or more actions that are executed + # on request and then either render a template or redirect to another action. An action is defined as a public method + # on the controller, which will automatically be made accessible to the web-server through \Rails Routes. + # + # By default, only the ApplicationController in a \Rails application inherits from <tt>ActionController::Base</tt>. All other + # controllers in turn inherit from ApplicationController. This gives you one class to configure things such as + # request forgery protection and filtering of sensitive request parameters. + # + # A sample controller could look like this: + # + # class PostsController < ApplicationController + # def index + # @posts = Post.all + # end + # + # def create + # @post = Post.create params[:post] + # redirect_to posts_path + # end + # end + # + # Actions, by default, render a template in the <tt>app/views</tt> directory corresponding to the name of the controller and action + # after executing code in the action. For example, the +index+ action of the PostsController would render the + # template <tt>app/views/posts/index.html.erb</tt> by default after populating the <tt>@posts</tt> instance variable. + # + # Unlike index, the create action will not render a template. After performing its main purpose (creating a + # new post), it initiates a redirect instead. This redirect works by returning an external + # "302 Moved" HTTP response that takes the user to the index action. + # + # These two methods represent the two basic action archetypes used in Action Controllers. Get-and-show and do-and-redirect. + # Most actions are variations of these themes. + # + # == Requests + # + # For every request, the router determines the value of the +controller+ and +action+ keys. These determine which controller + # and action are called. The remaining request parameters, the session (if one is available), and the full request with + # all the HTTP headers are made available to the action through accessor methods. Then the action is performed. + # + # The full request object is available via the request accessor and is primarily used to query for HTTP headers: + # + # def server_ip + # location = request.env["SERVER_ADDR"] + # render :text => "This server hosted at #{location}" + # end + # + # == Parameters + # + # All request parameters, whether they come from a GET or POST request, or from the URL, are available through the params method + # which returns a hash. For example, an action that was performed through <tt>/posts?category=All&limit=5</tt> will include + # <tt>{ "category" => "All", "limit" => 5 }</tt> in params. + # + # It's also possible to construct multi-dimensional parameter hashes by specifying keys using brackets, such as: + # + # <input type="text" name="post[name]" value="david"> + # <input type="text" name="post[address]" value="hyacintvej"> + # + # A request stemming from a form holding these inputs will include <tt>{ "post" => { "name" => "david", "address" => "hyacintvej" } }</tt>. + # If the address input had been named "post[address][street]", the params would have included + # <tt>{ "post" => { "address" => { "street" => "hyacintvej" } } }</tt>. There's no limit to the depth of the nesting. + # + # == Sessions + # + # Sessions allows you to store objects in between requests. This is useful for objects that are not yet ready to be persisted, + # such as a Signup object constructed in a multi-paged process, or objects that don't change much and are needed all the time, such + # as a User object for a system that requires login. The session should not be used, however, as a cache for objects where it's likely + # they could be changed unknowingly. It's usually too much work to keep it all synchronized -- something databases already excel at. + # + # You can place objects in the session by using the <tt>session</tt> method, which accesses a hash: + # + # session[:person] = Person.authenticate(user_name, password) + # + # And retrieved again through the same hash: + # + # Hello #{session[:person]} + # + # For removing objects from the session, you can either assign a single key to +nil+: + # + # # removes :person from session + # session[:person] = nil + # + # or you can remove the entire session with +reset_session+. + # + # Sessions are stored by default in a browser cookie that's cryptographically signed, but unencrypted. + # This prevents the user from tampering with the session but also allows him to see its contents. + # + # Do not put secret information in cookie-based sessions! + # + # Other options for session storage: + # + # * ActiveRecord::SessionStore - Sessions are stored in your database, which works better than PStore with multiple app servers and, + # unlike CookieStore, hides your session contents from the user. To use ActiveRecord::SessionStore, set + # + # MyApplication::Application.config.session_store :active_record_store + # + # in your <tt>config/initializers/session_store.rb</tt> and run <tt>script/rails g session_migration</tt>. + # + # == Responses + # + # Each action results in a response, which holds the headers and document to be sent to the user's browser. The actual response + # object is generated automatically through the use of renders and redirects and requires no user intervention. + # + # == Renders + # + # Action Controller sends content to the user by using one of five rendering methods. The most versatile and common is the rendering + # of a template. Included in the Action Pack is the Action View, which enables rendering of ERb templates. It's automatically configured. + # The controller passes objects to the view by assigning instance variables: + # + # def show + # @post = Post.find(params[:id]) + # end + # + # Which are then automatically available to the view: + # + # Title: <%= @post.title %> + # + # You don't have to rely on the automated rendering. Especially actions that could result in the rendering of different templates will use + # the manual rendering methods: + # + # def search + # @results = Search.find(params[:query]) + # case @results + # when 0 then render :action => "no_results" + # when 1 then render :action => "show" + # when 2..10 then render :action => "show_many" + # end + # end + # + # Read more about writing ERb and Builder templates in ActionView::Base. + # + # == Redirects + # + # Redirects are used to move from one action to another. For example, after a <tt>create</tt> action, which stores a blog entry to a database, + # we might like to show the user the new entry. Because we're following good DRY principles (Don't Repeat Yourself), we're going to reuse (and redirect to) + # a <tt>show</tt> action that we'll assume has already been created. The code might look like this: + # + # def create + # @entry = Entry.new(params[:entry]) + # if @entry.save + # # The entry was saved correctly, redirect to show + # redirect_to :action => 'show', :id => @entry.id + # else + # # things didn't go so well, do something else + # end + # end + # + # In this case, after saving our new entry to the database, the user is redirected to the <tt>show</tt> method which is then executed. + # + # Learn more about <tt>redirect_to</tt> and what options you have in ActionController::Redirecting. + # + # == Calling multiple redirects or renders + # + # An action may contain only a single render or a single redirect. Attempting to try to do either again will result in a DoubleRenderError: + # + # def do_something + # redirect_to :action => "elsewhere" + # render :action => "overthere" # raises DoubleRenderError + # end + # + # If you need to redirect on the condition of something, then be sure to add "and return" to halt execution. + # + # def do_something + # redirect_to(:action => "elsewhere") and return if monkeys.nil? + # render :action => "overthere" # won't be called if monkeys is nil + # end + # class Base < Metal abstract! @@ -58,13 +223,6 @@ module ActionController # Rails 2.x compatibility include ActionController::Compatibility - def self.inherited(klass) - super - klass.helper :all - end - ActiveSupport.run_load_hooks(:action_controller, self) end end - -require "action_controller/deprecated/base" diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb index 4105f9e14f..14137f2886 100644 --- a/actionpack/lib/action_controller/caching.rb +++ b/actionpack/lib/action_controller/caching.rb @@ -3,7 +3,7 @@ require 'uri' require 'set' module ActionController #:nodoc: - # Caching is a cheap way of speeding up slow applications by keeping the result of + # \Caching is a cheap way of speeding up slow applications by keeping the result of # calculations, renderings, and database calls around for subsequent requests. # Action Controller affords you three approaches in varying levels of granularity: # Page, Action, Fragment. @@ -14,7 +14,7 @@ module ActionController #:nodoc: # Note: To turn off all caching and sweeping, set # config.action_controller.perform_caching = false. # - # == Caching stores + # == \Caching stores # # All the caching stores from ActiveSupport::Cache are available to be used as backends # for Action Controller caching. This setting only affects action and fragment caching diff --git a/actionpack/lib/action_controller/caching/actions.rb b/actionpack/lib/action_controller/caching/actions.rb index 546f043c58..2c8a6e4d4d 100644 --- a/actionpack/lib/action_controller/caching/actions.rb +++ b/actionpack/lib/action_controller/caching/actions.rb @@ -4,53 +4,58 @@ module ActionController #:nodoc: module Caching # Action caching is similar to page caching by the fact that the entire # output of the response is cached, but unlike page caching, every - # request still goes through the Action Pack. The key benefit - # of this is that filters are run before the cache is served, which - # allows for authentication and other restrictions on whether someone - # is allowed to see the cache. Example: + # request still goes through Action Pack. The key benefit of this is + # that filters run before the cache is served, which allows for + # authentication and other restrictions on whether someone is allowed + # to execute such action. Example: # # class ListsController < ApplicationController # before_filter :authenticate, :except => :public + # # caches_page :public - # caches_action :index, :show, :feed + # caches_action :index, :show # end # - # In this example, the public action doesn't require authentication, - # so it's possible to use the faster page caching method. But both - # the show and feed action are to be shielded behind the authenticate - # filter, so we need to implement those as action caches. - # - # Action caching internally uses the fragment caching and an around - # filter to do the job. The fragment cache is named according to both - # the current host and the path. So a page that is accessed at - # http://david.somewhere.com/lists/show/1 will result in a fragment named - # "david.somewhere.com/lists/show/1". This allows the cacher to - # differentiate between "david.somewhere.com/lists/" and - # "jamis.somewhere.com/lists/" -- which is a helpful way of assisting + # In this example, the +public+ action doesn't require authentication + # so it's possible to use the faster page caching. On the other hand + # +index+ and +show+ require authentication. They can still be cached, + # but we need action caching for them. + # + # Action caching uses fragment caching internally and an around + # filter to do the job. The fragment cache is named according to + # the host and path of the request. A page that is accessed at + # <tt>http://david.example.com/lists/show/1</tt> will result in a fragment named + # <tt>david.example.com/lists/show/1</tt>. This allows the cacher to + # differentiate between <tt>david.example.com/lists/</tt> and + # <tt>jamis.example.com/lists/</tt> -- which is a helpful way of assisting # the subdomain-as-account-key pattern. # # Different representations of the same resource, e.g. - # <tt>http://david.somewhere.com/lists</tt> and - # <tt>http://david.somewhere.com/lists.xml</tt> + # <tt>http://david.example.com/lists</tt> and + # <tt>http://david.example.com/lists.xml</tt> # are treated like separate requests and so are cached separately. # Keep in mind when expiring an action cache that # <tt>:action => 'lists'</tt> is not the same as # <tt>:action => 'list', :format => :xml</tt>. # # You can set modify the default action cache path by passing a - # :cache_path option. This will be passed directly to - # ActionCachePath.path_for. This is handy for actions with multiple - # possible routes that should be cached differently. If a block is - # given, it is called with the current controller instance. + # <tt>:cache_path</tt> option. This will be passed directly to + # <tt>ActionCachePath.path_for</tt>. This is handy for actions with + # multiple possible routes that should be cached differently. If a + # block is given, it is called with the current controller instance. + # + # And you can also use <tt>:if</tt> (or <tt>:unless</tt>) to pass a + # proc that specifies when the action should be cached. # - # And you can also use :if (or :unless) to pass a Proc that - # specifies when the action should be cached. + # Finally, if you are using memcached, you can also pass <tt>:expires_in</tt>. # - # Finally, if you are using memcached, you can also pass :expires_in. + # The following example depicts some of the points made above: # # class ListsController < ApplicationController # before_filter :authenticate, :except => :public - # caches_page :public + # + # caches_page :public + # # caches_action :index, :if => proc do |c| # !c.request.format.json? # cache if is not a JSON request # end @@ -58,19 +63,28 @@ module ActionController #:nodoc: # caches_action :show, :cache_path => { :project => 1 }, # :expires_in => 1.hour # - # caches_action :feed, :cache_path => proc do |controller| - # if controller.params[:user_id] - # controller.send(:user_list_url, - # controller.params[:user_id], controller.params[:id]) + # caches_action :feed, :cache_path => proc do |c| + # if c.params[:user_id] + # c.send(:user_list_url, + # c.params[:user_id], c.params[:id]) # else - # controller.send(:list_url, controller.params[:id]) + # c.send(:list_url, c.params[:id]) # end # end # end # - # If you pass :layout => false, it will only cache your action - # content. It is useful when your layout has dynamic information. + # If you pass <tt>:layout => false</tt>, it will only cache your action + # content. That's useful when your layout has dynamic information. + # + # Warning: If the format of the request is determined by the Accept HTTP + # 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 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 + # way to cache by MIME type is to pass the format in the route. module Actions extend ActiveSupport::Concern @@ -89,12 +103,14 @@ module ActionController #:nodoc: end def _save_fragment(name, options) - return unless caching_allowed? - content = response_body content = content.join if content.is_a?(Array) - write_fragment(name, content, options) + if caching_allowed? + write_fragment(name, content, options) + else + content + end end protected @@ -144,7 +160,7 @@ module ActionController #:nodoc: attr_reader :path, :extension # If +infer_extension+ is true, the cache path extension is looked up from the request's - # path & format. This is desirable when reading and writing the cache, but not when + # path and format. This is desirable when reading and writing the cache, but not when # expiring the cache - expire_action should expire the same files regardless of the # request format. def initialize(controller, options = {}, infer_extension = true) @@ -161,7 +177,7 @@ module ActionController #:nodoc: def normalize!(path) path << 'index' if path[-1] == ?/ path << ".#{extension}" if extension and !path.ends_with?(extension) - URI.unescape(path) + URI.parser.unescape(path) end end end diff --git a/actionpack/lib/action_controller/caching/fragments.rb b/actionpack/lib/action_controller/caching/fragments.rb index 460273dac1..0be04b70a1 100644 --- a/actionpack/lib/action_controller/caching/fragments.rb +++ b/actionpack/lib/action_controller/caching/fragments.rb @@ -1,52 +1,72 @@ module ActionController #:nodoc: module Caching - # Fragment caching is used for caching various blocks within templates without caching the entire action as a whole. This is useful when - # certain elements of an action change frequently or depend on complicated state while other parts rarely change or can be shared amongst multiple - # parties. The caching is done using the cache helper available in the Action View. A template with caching might look something like: + # Fragment caching is used for caching various blocks within + # views without caching the entire action as a whole. This is + # useful when certain elements of an action change frequently or + # depend on complicated state while other parts rarely change or + # can be shared amongst multiple parties. The caching is done using + # the <tt>cache</tt> helper available in the Action View. A + # template with fragment caching might look like: # # <b>Hello <%= @name %></b> + # # <% cache do %> # All the topics in the system: # <%= render :partial => "topic", :collection => Topic.find(:all) %> # <% end %> # - # This cache will bind to the name of the action that called it, so if this code was part of the view for the topics/list action, you would - # be able to invalidate it using <tt>expire_fragment(:controller => "topics", :action => "list")</tt>. + # This cache will bind the name of the action that called it, so if + # this code was part of the view for the topics/list action, you + # would be able to invalidate it using: + # + # expire_fragment(:controller => "topics", :action => "list") # - # This default behavior is of limited use if you need to cache multiple fragments per action or if the action itself is cached using - # <tt>caches_action</tt>, so we also have the option to qualify the name of the cached fragment with something like: + # This default behavior is limited if you need to cache multiple + # fragments per action or if the action itself is cached using + # <tt>caches_action</tt>. To remedy this, there is an option to + # qualify the name of the cached fragment by using the + # <tt>:action_suffix</tt> option: # # <% cache(:action => "list", :action_suffix => "all_topics") do %> # - # That would result in a name such as "/topics/list/all_topics", avoiding conflicts with the action cache and with any fragments that use a - # different suffix. Note that the URL doesn't have to really exist or be callable - the url_for system is just used to generate unique - # cache names that we can refer to when we need to expire the cache. + # That would result in a name such as + # <tt>/topics/list/all_topics</tt>, avoiding conflicts with the + # action cache and with any fragments that use a different suffix. + # Note that the URL doesn't have to really exist or be callable + # - the url_for system is just used to generate unique cache names + # that we can refer to when we need to expire the cache. # # The expiration call for this example is: # - # expire_fragment(:controller => "topics", :action => "list", :action_suffix => "all_topics") + # expire_fragment(:controller => "topics", + # :action => "list", + # :action_suffix => "all_topics") module Fragments - # Given a key (as described in <tt>expire_fragment</tt>), returns a key suitable for use in reading, - # writing, or expiring a cached fragment. If the key is a hash, the generated key is the return - # value of url_for on that hash (without the protocol). All keys are prefixed with "views/" and uses + # Given a key (as described in <tt>expire_fragment</tt>), returns + # a key suitable for use in reading, writing, or expiring a + # cached fragment. If the key is a hash, the generated key is the + # return value of url_for on that hash (without the protocol). + # All keys are prefixed with <tt>views/</tt> and uses # ActiveSupport::Cache.expand_cache_key for the expansion. def fragment_cache_key(key) ActiveSupport::Cache.expand_cache_key(key.is_a?(Hash) ? url_for(key).split("://").last : key, :views) end - # Writes <tt>content</tt> to the location signified by <tt>key</tt> (see <tt>expire_fragment</tt> for acceptable formats) + # 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? key = fragment_cache_key(key) instrument_fragment_cache :write_fragment, key do - content = content.html_safe.to_str if content.respond_to?(:html_safe) + content = content.to_str cache_store.write(key, content, options) end content end - # Reads a cached fragment from the location signified by <tt>key</tt> (see <tt>expire_fragment</tt> for acceptable formats) + # Reads a cached fragment from the location signified by <tt>key</tt> + # (see <tt>expire_fragment</tt> for acceptable formats). def read_fragment(key, options = nil) return unless cache_configured? @@ -57,7 +77,8 @@ module ActionController #:nodoc: end end - # Check if a cached fragment from the location signified by <tt>key</tt> exists (see <tt>expire_fragment</tt> for acceptable formats) + # Check if a cached fragment from the location signified by + # <tt>key</tt> exists (see <tt>expire_fragment</tt> for acceptable formats) def fragment_exist?(key, options = nil) return unless cache_configured? key = fragment_cache_key(key) @@ -70,8 +91,9 @@ module ActionController #:nodoc: # Removes fragments from the cache. # # +key+ can take one of three forms: + # # * String - This would normally take the form of a path, like - # <tt>"pages/45/notes"</tt>. + # <tt>pages/45/notes</tt>. # * Hash - Treated as an implicit call to +url_for+, like # <tt>{:controller => "pages", :action => "notes", :id => 45}</tt> # * Regexp - Will remove any fragment that matches, so diff --git a/actionpack/lib/action_controller/caching/pages.rb b/actionpack/lib/action_controller/caching/pages.rb index 4f7a5d3f55..8c583c7ce0 100644 --- a/actionpack/lib/action_controller/caching/pages.rb +++ b/actionpack/lib/action_controller/caching/pages.rb @@ -1,5 +1,4 @@ require 'fileutils' -require 'uri' require 'active_support/core_ext/class/attribute_accessors' module ActionController #:nodoc: @@ -71,9 +70,9 @@ module ActionController #:nodoc: # Manually cache the +content+ in the key determined by +path+. Example: # cache_page "I'm the cached content", "/lists/show" - def cache_page(content, path) + def cache_page(content, path, extension = nil) return unless perform_caching - path = page_cache_path(path) + path = page_cache_path(path, extension) instrument_page_cache :write_page, path do FileUtils.makedirs(File.dirname(path)) @@ -98,14 +97,16 @@ module ActionController #:nodoc: end private - def page_cache_file(path) - name = (path.empty? || path == "/") ? "/index" : URI.unescape(path.chomp('/')) - name << page_cache_extension unless (name.split('/').last || name).include? '.' + def page_cache_file(path, extension) + name = (path.empty? || path == "/") ? "/index" : URI.parser.unescape(path.chomp('/')) + unless (name.split('/').last || name).include? '.' + name << (extension || self.page_cache_extension) + end return name end - def page_cache_path(path) - page_cache_directory + page_cache_file(path) + def page_cache_path(path, extension = nil) + page_cache_directory.to_s + page_cache_file(path, extension) end def instrument_page_cache(name, path) @@ -135,7 +136,7 @@ module ActionController #:nodoc: # If no options are provided, the requested url is used. Example: # cache_page "I'm the cached content", :controller => "lists", :action => "show" def cache_page(content = nil, options = nil) - return unless self.class.perform_caching && caching_allowed + return unless self.class.perform_caching && caching_allowed? path = case options when Hash @@ -146,13 +147,13 @@ module ActionController #:nodoc: request.path end - self.class.cache_page(content || response.body, path) + if (type = Mime::LOOKUP[self.content_type]) && (type_symbol = type.symbol).present? + extension = ".#{type_symbol}" + end + + self.class.cache_page(content || response.body, path, extension) end - private - def caching_allowed - request.get? && response.status.to_i == 200 - end end end end diff --git a/actionpack/lib/action_controller/deprecated.rb b/actionpack/lib/action_controller/deprecated.rb index 9f2de57033..aa0cfc9395 100644 --- a/actionpack/lib/action_controller/deprecated.rb +++ b/actionpack/lib/action_controller/deprecated.rb @@ -1,3 +1,3 @@ ActionController::AbstractRequest = ActionController::Request = ActionDispatch::Request ActionController::AbstractResponse = ActionController::Response = ActionDispatch::Response -ActionController::Routing = ActionDispatch::Routing +ActionController::Routing = ActionDispatch::Routing
\ No newline at end of file diff --git a/actionpack/lib/action_controller/deprecated/base.rb b/actionpack/lib/action_controller/deprecated/base.rb deleted file mode 100644 index 3975afcaf0..0000000000 --- a/actionpack/lib/action_controller/deprecated/base.rb +++ /dev/null @@ -1,133 +0,0 @@ -module ActionController - class Base - # Deprecated methods. Wrap them in a module so they can be overwritten by plugins - # (like the verify method.) - module DeprecatedBehavior #:nodoc: - def relative_url_root - ActiveSupport::Deprecation.warn "ActionController::Base.relative_url_root is ineffective. " << - "Please stop using it.", caller - end - - def relative_url_root= - ActiveSupport::Deprecation.warn "ActionController::Base.relative_url_root= is ineffective. " << - "Please stop using it.", caller - end - - def consider_all_requests_local - ActiveSupport::Deprecation.warn "ActionController::Base.consider_all_requests_local is deprecated, " << - "use Rails.application.config.consider_all_requests_local instead", caller - Rails.application.config.consider_all_requests_local - end - - def consider_all_requests_local=(value) - ActiveSupport::Deprecation.warn "ActionController::Base.consider_all_requests_local= is deprecated. " << - "Please configure it on your application with config.consider_all_requests_local=", caller - Rails.application.config.consider_all_requests_local = value - end - - def allow_concurrency - ActiveSupport::Deprecation.warn "ActionController::Base.allow_concurrency is deprecated, " << - "use Rails.application.config.allow_concurrency instead", caller - Rails.application.config.allow_concurrency - end - - def allow_concurrency=(value) - ActiveSupport::Deprecation.warn "ActionController::Base.allow_concurrency= is deprecated. " << - "Please configure it on your application with config.allow_concurrency=", caller - Rails.application.config.allow_concurrency = value - end - - def ip_spoofing_check=(value) - ActiveSupport::Deprecation.warn "ActionController::Base.ip_spoofing_check= is deprecated. " << - "Please configure it on your application with config.action_dispatch.ip_spoofing_check=", caller - Rails.application.config.action_dispatch.ip_spoofing_check = value - end - - def ip_spoofing_check - ActiveSupport::Deprecation.warn "ActionController::Base.ip_spoofing_check is deprecated. " << - "Configuring ip_spoofing_check on the application configures a middleware.", caller - Rails.application.config.action_dispatch.ip_spoofing_check - end - - def cookie_verifier_secret=(value) - ActiveSupport::Deprecation.warn "ActionController::Base.cookie_verifier_secret= is deprecated. " << - "Please configure it on your application with config.secret_token=", caller - end - - def cookie_verifier_secret - ActiveSupport::Deprecation.warn "ActionController::Base.cookie_verifier_secret is deprecated.", caller - end - - def trusted_proxies=(value) - ActiveSupport::Deprecation.warn "ActionController::Base.trusted_proxies= is deprecated. " << - "Please configure it on your application with config.action_dispatch.trusted_proxies=", caller - Rails.application.config.action_dispatch.ip_spoofing_check = value - end - - def trusted_proxies - ActiveSupport::Deprecation.warn "ActionController::Base.trusted_proxies is deprecated. " << - "Configuring trusted_proxies on the application configures a middleware.", caller - Rails.application.config.action_dispatch.ip_spoofing_check = value - end - - def session(*args) - ActiveSupport::Deprecation.warn( - "Disabling sessions for a single controller has been deprecated. " + - "Sessions are now lazy loaded. So if you don't access them, " + - "consider them off. You can still modify the session cookie " + - "options with request.session_options.", caller) - end - - def session=(value) - ActiveSupport::Deprecation.warn "ActionController::Base.session= is deprecated. " << - "Please configure it on your application with config.session_store :cookie_store, :key => '....'", caller - if value.delete(:disabled) - Rails.application.config.session_store :disabled - else - store = Rails.application.config.session_store - Rails.application.config.session_store store, value - end - end - - # Controls the resource action separator - def resource_action_separator - @resource_action_separator ||= "/" - end - - def resource_action_separator=(val) - ActiveSupport::Deprecation.warn "ActionController::Base.resource_action_separator is deprecated and only " \ - "works with the deprecated router DSL." - @resource_action_separator = val - end - - def use_accept_header - ActiveSupport::Deprecation.warn "ActionController::Base.use_accept_header doesn't do anything anymore. " \ - "The accept header is always taken into account." - end - - def use_accept_header=(val) - use_accept_header - end - - # This method has been moved to ActionDispatch::Request.filter_parameters - def filter_parameter_logging(*args, &block) - ActiveSupport::Deprecation.warn("Setting filter_parameter_logging in ActionController is deprecated and has no longer effect, please set 'config.filter_parameters' in config/application.rb instead", caller) - filter = Rails.application.config.filter_parameters - filter.concat(args) - filter << block if block - filter - end - - # This was moved to a plugin - def verify(*args) - ActiveSupport::Deprecation.warn "verify was removed from Rails and is now available as a plugin. " << - "Please install it with `rails plugin install git://github.com/rails/verification.git`.", caller - end - end - - extend DeprecatedBehavior - - delegate :consider_all_requests_local, :consider_all_requests_local=, - :allow_concurrency, :allow_concurrency=, :to => :"self.class" - end -end diff --git a/actionpack/lib/action_controller/deprecated/dispatcher.rb b/actionpack/lib/action_controller/deprecated/dispatcher.rb deleted file mode 100644 index 8c21e375dd..0000000000 --- a/actionpack/lib/action_controller/deprecated/dispatcher.rb +++ /dev/null @@ -1,28 +0,0 @@ -module ActionController - class Dispatcher - class << self - def before_dispatch(*args, &block) - ActiveSupport::Deprecation.warn "ActionController::Dispatcher.before_dispatch is deprecated. " << - "Please use ActionDispatch::Callbacks.before instead.", caller - ActionDispatch::Callbacks.before(*args, &block) - end - - def after_dispatch(*args, &block) - ActiveSupport::Deprecation.warn "ActionController::Dispatcher.after_dispatch is deprecated. " << - "Please use ActionDispatch::Callbacks.after instead.", caller - ActionDispatch::Callbacks.after(*args, &block) - end - - def to_prepare(*args, &block) - ActiveSupport::Deprecation.warn "ActionController::Dispatcher.to_prepare is deprecated. " << - "Please use config.to_prepare instead", caller - ActionDispatch::Callbacks.after(*args, &block) - end - - def new - ActiveSupport::Deprecation.warn "ActionController::Dispatcher.new is deprecated, use Rails.application instead." - Rails.application - end - end - end -end diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index ece270b3ce..3fae697cc3 100644 --- a/actionpack/lib/action_controller/log_subscriber.rb +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -16,7 +16,11 @@ module ActionController payload = event.payload additions = ActionController::Base.log_process_action(payload) - message = "Completed #{payload[:status]} #{Rack::Utils::HTTP_STATUS_CODES[payload[:status]]} in %.0fms" % event.duration + status = payload[:status] + if status.nil? && payload[:exception].present? + status = Rack::Utils.status_code(ActionDispatch::ShowExceptions.rescue_responses[payload[:exception].first]) rescue nil + end + message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in %.0fms" % event.duration message << " (#{additions.join(" | ")})" unless additions.blank? info(message) @@ -42,7 +46,7 @@ module ActionController def #{method}(event) key_or_path = event.payload[:key] || event.payload[:path] human_name = #{method.to_s.humanize.inspect} - info("\#{human_name} \#{key_or_path} (%.1fms)" % event.duration) + info("\#{human_name} \#{key_or_path} \#{"(%.1fms)" % event.duration}") end METHOD end diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index 2281c500c5..e5db31061b 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -12,7 +12,7 @@ module ActionController # class MiddlewareStack < ActionDispatch::MiddlewareStack #:nodoc: class Middleware < ActionDispatch::MiddlewareStack::Middleware #:nodoc: - def initialize(klass, *args) + def initialize(klass, *args, &block) options = args.extract_options! @only = Array(options.delete(:only)).map(&:to_s) @except = Array(options.delete(:except)).map(&:to_s) @@ -36,35 +36,88 @@ module ActionController action = action.to_s raise "MiddlewareStack#build requires an app" unless app - reverse.inject(app) do |a, middleware| + middlewares.reverse.inject(app) do |a, middleware| middleware.valid?(action) ? middleware.build(a) : a end end end - # ActionController::Metal provides a way to get a valid Rack application from a controller. + # <tt>ActionController::Metal</tt> is the simplest possible controller, providing a + # valid Rack interface without the additional niceties provided by + # <tt>ActionController::Base</tt>. + # + # A sample metal controller might look like this: + # + # class HelloController < ActionController::Metal + # def index + # self.response_body = "Hello World!" + # end + # end + # + # And then to route requests to your metal controller, you would add + # something like this to <tt>config/routes.rb</tt>: + # + # match 'hello', :to => HelloController.action(:index) + # + # The +action+ method returns a valid Rack application for the \Rails + # router to dispatch to. + # + # == Rendering Helpers + # + # <tt>ActionController::Metal</tt> by default provides no utilities for rendering + # views, partials, or other responses aside from explicitly calling of + # <tt>response_body=</tt>, <tt>content_type=</tt>, and <tt>status=</tt>. To + # add the render helpers you're used to having in a normal controller, you + # can do the following: + # + # class HelloController < ActionController::Metal + # include ActionController::Rendering + # append_view_path "#{Rails.root}/app/views" + # + # def index + # render "hello/index" + # end + # end + # + # == Redirection Helpers + # + # To add redirection helpers to your metal controller, do the following: + # + # class HelloController < ActionController::Metal + # include ActionController::Redirecting + # include Rails.application.routes.url_helpers + # + # def index + # redirect_to root_url + # end + # end + # + # == Other Helpers + # + # You can refer to the modules included in <tt>ActionController::Base</tt> to see + # other features you can bring into your metal controller. # - # In AbstractController, dispatching is triggered directly by calling #process on a new controller. - # ActionController::Metal provides an #action method that returns a valid Rack application for a - # given action. Other rack builders, such as Rack::Builder, Rack::URLMap, and the Rails router, - # can dispatch directly to the action returned by FooController.action(:index). class Metal < AbstractController::Base abstract! - attr_internal :env + attr_internal_writer :env + + def env + @_env ||= {} + end # Returns the last part of the controller's name, underscored, without the ending - # "Controller". For instance, MyApp::MyPostsController would return "my_posts" for - # controller_name + # <tt>Controller</tt>. For instance, PostsController returns <tt>posts</tt>. + # Namespaces are left out, so Admin::PostsController returns <tt>posts</tt> as well. # # ==== Returns - # String + # * <tt>string</tt> def self.controller_name @controller_name ||= self.name.demodulize.sub(/Controller$/, '').underscore end - # Delegates to the class' #controller_name + # Delegates to the class' <tt>controller_name</tt> def controller_name self.class.controller_name end @@ -81,6 +134,9 @@ module ActionController def initialize(*) @_headers = {"Content-Type" => "text/html"} @_status = 200 + @_request = nil + @_response = nil + @_routes = nil super end @@ -112,6 +168,11 @@ module ActionController headers["Location"] = url end + # basic url_for that can be overridden for more robust functionality + def url_for(string) + string + end + def status @_status end @@ -121,12 +182,11 @@ module ActionController end def response_body=(val) - body = val.respond_to?(:each) ? val : [val] + body = val.nil? ? nil : (val.respond_to?(:each) ? val : [val]) super body end - # :api: private - def dispatch(name, request) + def dispatch(name, request) #:nodoc: @_request = request @_env = request.env @_env['action_controller.instance'] = self @@ -134,8 +194,7 @@ module ActionController to_a end - # :api: private - def to_a + def to_a #:nodoc: response ? response.to_a : [status, headers, response_body] end @@ -147,8 +206,8 @@ module ActionController super end - def self.use(*args) - middleware_stack.use(*args) + def self.use(*args, &block) + middleware_stack.use(*args, &block) end def self.middleware @@ -164,10 +223,10 @@ module ActionController # for the same action. # # ==== Parameters - # action<#to_s>:: An action name + # * <tt>action</tt> - An action name # # ==== Returns - # Proc:: A rack application + # * <tt>proc</tt> - A rack application def self.action(name, klass = ActionDispatch::Request) middleware_stack.build(name.to_s) do |env| new.dispatch(name, klass.new(env)) diff --git a/actionpack/lib/action_controller/metal/compatibility.rb b/actionpack/lib/action_controller/metal/compatibility.rb index d49465fa0b..006b9fd456 100644 --- a/actionpack/lib/action_controller/metal/compatibility.rb +++ b/actionpack/lib/action_controller/metal/compatibility.rb @@ -5,9 +5,6 @@ module ActionController class ::ActionController::ActionControllerError < StandardError #:nodoc: end - module ClassMethods - end - # Temporary hax included do ::ActionController::UnknownAction = ::AbstractController::ActionNotFound @@ -39,12 +36,6 @@ module ActionController def assign_shortcuts(*) end def _normalize_options(options) - if options[:action] && options[:action].to_s.include?(?/) - ActiveSupport::Deprecation.warn "Giving a path to render :action is deprecated. " << - "Please use render :template instead", caller - options[:template] = options.delete(:action) - end - options[:text] = nil if options.delete(:nothing) == true options[:text] = " " if options.key?(:text) && options[:text].nil? super diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb index 61e7ece90d..a5e37172c9 100644 --- a/actionpack/lib/action_controller/metal/conditional_get.rb +++ b/actionpack/lib/action_controller/metal/conditional_get.rb @@ -6,7 +6,7 @@ module ActionController include Head # Sets the etag, last_modified, or both on the response and renders a - # "304 Not Modified" response if the request is already fresh. + # <tt>304 Not Modified</tt> response if the request is already fresh. # # Parameters: # * <tt>:etag</tt> @@ -17,11 +17,11 @@ module ActionController # # def show # @article = Article.find(params[:id]) - # fresh_when(:etag => @article, :last_modified => @article.created_at.utc, :public => true) + # fresh_when(:etag => @article, :last_modified => @article.created_at, :public => true) # end # # This will render the show template if the request isn't sending a matching etag or - # If-Modified-Since header and just a "304 Not Modified" response if there's a match. + # If-Modified-Since header and just a <tt>304 Not Modified</tt> response if there's a match. # def fresh_when(options) options.assert_valid_keys(:etag, :last_modified, :public) @@ -36,7 +36,7 @@ module ActionController # Sets the etag and/or last_modified on the response and checks it against # the client request. If the request doesn't match the options provided, the # request is considered stale and should be generated from scratch. Otherwise, - # it's fresh and we don't need to generate anything and a reply of "304 Not Modified" is sent. + # it's fresh and we don't need to generate anything and a reply of <tt>304 Not Modified</tt> is sent. # # Parameters: # * <tt>:etag</tt> @@ -48,7 +48,7 @@ module ActionController # def show # @article = Article.find(params[:id]) # - # if stale?(:etag => @article, :last_modified => @article.created_at.utc) + # if stale?(:etag => @article, :last_modified => @article.created_at) # @statistics = @article.really_expensive_call # respond_to do |format| # # all the supported formats @@ -60,13 +60,13 @@ module ActionController !request.fresh?(response) end - # Sets a HTTP 1.1 Cache-Control header. Defaults to issuing a "private" instruction, so that - # intermediate caches shouldn't cache the response. + # Sets a HTTP 1.1 Cache-Control header. Defaults to issuing a <tt>private</tt> instruction, so that + # intermediate caches must not cache the response. # # Examples: # expires_in 20.minutes # expires_in 3.hours, :public => true - # expires in 3.hours, 'max-stale' => 5.hours, :public => true + # expires_in 3.hours, 'max-stale' => 5.hours, :public => true # # This method will overwrite an existing Cache-Control header. # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities. @@ -77,7 +77,7 @@ module ActionController response.cache_control[:extras] = options.map {|k,v| "#{k}=#{v}"} end - # Sets a HTTP 1.1 Cache-Control header of "no-cache" so no caching should occur by the browser or + # Sets a HTTP 1.1 Cache-Control header of <tt>no-cache</tt> so no caching should occur by the browser or # intermediate caches (like caching proxy servers). def expires_now #:doc: response.cache_control.replace(:no_cache => true) diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb index a5c9910d68..8abcad55a2 100644 --- a/actionpack/lib/action_controller/metal/head.rb +++ b/actionpack/lib/action_controller/metal/head.rb @@ -2,8 +2,6 @@ module ActionController module Head extend ActiveSupport::Concern - include ActionController::UrlFor - # Return a response that has no content (merely headers). The options # argument is interpreted to be a hash of header names and values. # This allows you to easily return a response that consists only of @@ -22,13 +20,13 @@ module ActionController location = options.delete(:location) options.each do |key, value| - headers[key.to_s.dasherize.split(/-/).map { |v| v.capitalize }.join("-")] = value.to_s + headers[key.to_s.dasherize.split('-').each { |v| v[0] = v[0].chr.upcase }.join('-')] = value.to_s end self.status = status self.location = url_for(location) if location - self.content_type = Mime[formats.first] + self.content_type = Mime[formats.first] if formats self.response_body = " " end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index e0bc47318a..91a88ab68a 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -2,21 +2,21 @@ require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/class/attribute' module ActionController - # The Rails framework provides a large number of helpers for working with +assets+, +dates+, +forms+, - # +numbers+ and model objects, to name a few. These helpers are available to all templates + # The \Rails framework provides a large number of helpers for working with assets, dates, forms, + # numbers and model objects, to name a few. These helpers are available to all templates # by default. # - # In addition to using the standard template helpers provided in the Rails framework, creating custom helpers to + # In addition to using the standard template helpers provided, creating custom helpers to # extract complicated logic or reusable functionality is strongly encouraged. By default, the controller will # include a helper whose name matches that of the controller, e.g., <tt>MyController</tt> will automatically # include <tt>MyHelper</tt>. # - # Additional helpers can be specified using the +helper+ class method in <tt>ActionController::Base</tt> or any + # Additional helpers can be specified using the +helper+ class method in ActionController::Base or any # controller which inherits from it. # # ==== Examples - # The +to_s+ method from the Time class can be wrapped in a helper method to display a custom message if - # the Time object is blank: + # The +to_s+ method from the \Time class can be wrapped in a helper method to display a custom message if + # a \Time object is blank: # # module FormattedTimeHelper # def format_time(time, format=:long, blank_message=" ") @@ -53,30 +53,20 @@ module ActionController include AbstractController::Helpers included do - config_accessor :helpers_path + config_accessor :helpers_path, :include_all_helpers self.helpers_path ||= [] + self.include_all_helpers = true end module ClassMethods - def helpers_dir - ActiveSupport::Deprecation.warn "helpers_dir is deprecated, use helpers_path instead", caller - self.helpers_path - end - - def helpers_dir=(value) - ActiveSupport::Deprecation.warn "helpers_dir= is deprecated, use helpers_path= instead", caller - self.helpers_path = Array.wrap(value) - end - # Declares helper accessors for controller attributes. For example, the # following adds new +name+ and <tt>name=</tt> instance methods to a # controller and makes them available to the view: - # helper_attr :name # attr_accessor :name + # helper_attr :name # # ==== Parameters - # *attrs<Array[String, Symbol]>:: Names of attributes to be converted - # into helpers. + # * <tt>attrs</tt> - Names of attributes to be converted into helpers. def helper_attr(*attrs) attrs.flatten.each { |attr| helper_method(attr, "#{attr}=") } end @@ -88,25 +78,28 @@ module ActionController private # Overwrite modules_for_helpers to accept :all as argument, which loads - # all helpers in helpers_dir. + # all helpers in helpers_path. # # ==== Parameters - # args<Array[String, Symbol, Module, all]>:: A list of helpers + # * <tt>args</tt> - A list of helpers # # ==== Returns - # Array[Module]:: A normalized list of modules for the list of - # helpers provided. + # * <tt>array</tt> - A normalized list of modules for the list of helpers provided. def modules_for_helpers(args) args += all_application_helpers if args.delete(:all) super(args) end - # Extract helper names from files in app/helpers/**/*_helper.rb + # Extract helper names from files in <tt>app/helpers/**/*_helper.rb</tt> def all_application_helpers + all_helpers_from_path(helpers_path) + end + + def all_helpers_from_path(path) helpers = [] - Array.wrap(helpers_path).each do |path| - extract = /^#{Regexp.quote(path.to_s)}\/?(.*)_helper.rb$/ - helpers += Dir["#{path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1') } + Array.wrap(path).each do |_path| + extract = /^#{Regexp.quote(_path.to_s)}\/?(.*)_helper.rb$/ + helpers += Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1') } end helpers.sort! helpers.uniq! diff --git a/actionpack/lib/action_controller/metal/hide_actions.rb b/actionpack/lib/action_controller/metal/hide_actions.rb index 32d7a96701..b55c4643be 100644 --- a/actionpack/lib/action_controller/metal/hide_actions.rb +++ b/actionpack/lib/action_controller/metal/hide_actions.rb @@ -1,8 +1,7 @@ require 'active_support/core_ext/class/attribute' module ActionController - # ActionController::HideActions adds the ability to prevent public methods on a controller - # to be called as actions. + # Adds the ability to prevent public methods on a controller to be called as actions. module HideActions extend ActiveSupport::Concern @@ -23,7 +22,7 @@ module ActionController # Sets all of the actions passed in as hidden actions. # # ==== Parameters - # *args<#to_s>:: A list of actions + # * <tt>args</tt> - A list of actions def hide_action(*args) self.hidden_actions = hidden_actions.dup.merge(args.map(&:to_s)).freeze end diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index b0eb24a4a8..39c804d707 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -3,9 +3,9 @@ require 'active_support/core_ext/object/blank' module ActionController module HttpAuthentication - # Makes it dead easy to do HTTP Basic authentication. + # Makes it dead easy to do HTTP \Basic and \Digest authentication. # - # Simple Basic example: + # === Simple \Basic example # # class PostsController < ApplicationController # USER_NAME, PASSWORD = "dhh", "secret" @@ -29,7 +29,9 @@ module ActionController # end # # - # Here is a more advanced Basic example where only Atom feeds and the XML API is protected by HTTP authentication, + # === Advanced \Basic example + # + # Here is a more advanced \Basic example where only Atom feeds and the XML API is protected by HTTP authentication, # the regular HTML interface is protected by a session approach: # # class ApplicationController < ActionController::Base @@ -69,7 +71,7 @@ module ActionController # assert_equal 200, status # end # - # Simple Digest example: + # === Simple \Digest example # # require 'digest/md5' # class PostsController < ApplicationController @@ -95,18 +97,20 @@ module ActionController # end # end # - # NOTE: The +authenticate_or_request_with_http_digest+ block must return the user's password or the ha1 digest hash so the framework can appropriately - # hash to check the user's credentials. Returning +nil+ will cause authentication to fail. - # Storing the ha1 hash: MD5(username:realm:password), is better than storing a plain password. If - # the password file or database is compromised, the attacker would be able to use the ha1 hash to - # authenticate as the user at this +realm+, but would not have the user's password to try using at - # other sites. + # === Notes # - # On shared hosts, Apache sometimes doesn't pass authentication headers to - # FCGI instances. If your environment matches this description and you cannot - # authenticate, try this rule in your Apache setup: + # The +authenticate_or_request_with_http_digest+ block must return the user's password + # or the ha1 digest hash so the framework can appropriately hash to check the user's + # credentials. Returning +nil+ will cause authentication to fail. # - # RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L] + # Storing the ha1 hash: MD5(username:realm:password), is better than storing a plain password. If + # the password file or database is compromised, the attacker would be able to use the ha1 hash to + # authenticate as the user at this +realm+, but would not have the user's password to try using at + # other sites. + # + # In rare instances, web servers or front proxies strip authorization headers before + # they reach your application. You can debug this situation by logging all environment + # variables, and check for HTTP_AUTHORIZATION, amongst others. module Basic extend self @@ -210,7 +214,7 @@ module ActionController def encode_credentials(http_method, credentials, password, password_is_ha1) credentials[:response] = expected_response(http_method, credentials[:uri], credentials, password, password_is_ha1) - "Digest " + credentials.sort_by {|x| x[0].to_s }.inject([]) {|a, v| a << "#{v[0]}='#{v[1]}'" }.join(', ') + "Digest " + credentials.sort_by {|x| x[0].to_s }.map {|v| "#{v[0]}='#{v[1]}'" }.join(', ') end def decode_credentials_header(request) @@ -218,11 +222,10 @@ module ActionController end def decode_credentials(header) - header.to_s.gsub(/^Digest\s+/,'').split(',').inject({}) do |hash, pair| + Hash[header.to_s.gsub(/^Digest\s+/,'').split(',').map do |pair| key, value = pair.split('=', 2) - hash[key.strip.to_sym] = value.to_s.gsub(/^"|"$/,'').gsub(/'/, '') - hash - end + [key.strip.to_sym, value.to_s.gsub(/^"|"$/,'').gsub(/'/, '')] + end] end def authentication_header(controller, realm) @@ -392,11 +395,11 @@ module ActionController end end - # If token Authorization header is present, call the login procedure with + # If token Authorization header is present, call the login procedure with # the present token and options. # # controller - ActionController::Base instance for the current request. - # login_procedure - Proc to call if a token is present. The Proc should + # login_procedure - Proc to call if a token is present. The Proc should # take 2 arguments: # authenticate(controller) { |token, options| ... } # @@ -404,7 +407,7 @@ module ActionController # Returns nil if no token is found. def authenticate(controller, &login_procedure) token, options = token_and_options(controller.request) - if !token.blank? + unless token.blank? login_procedure.call(token, options) end end @@ -414,20 +417,19 @@ module ActionController # Authorization: Token token="abc", nonce="def" # Then the returned token is "abc", and the options is {:nonce => "def"} # - # request - ActionController::Request instance with the current headers. + # request - ActionDispatch::Request instance with the current headers. # # Returns an Array of [String, Hash] if a token is present. # Returns nil if no token is found. def token_and_options(request) if header = request.authorization.to_s[/^Token (.*)/] - values = $1.split(','). - inject({}) do |memo, value| - value.strip! # remove any spaces between commas and values - key, value = value.split(/\=\"?/) # split key=value pairs - value.chomp!('"') # chomp trailing " in value - value.gsub!(/\\\"/, '"') # unescape remaining quotes - memo.update(key => value) - end + values = Hash[$1.split(',').map do |value| + value.strip! # remove any spaces between commas and values + key, value = value.split(/\=\"?/) # split key=value pairs + value.chomp!('"') # chomp trailing " in value + value.gsub!(/\\\"/, '"') # unescape remaining quotes + [key, value] + end] [values.delete("token"), values.with_indifferent_access] end end @@ -439,9 +441,8 @@ module ActionController # # Returns String. def encode_credentials(token, options = {}) - values = ["token=#{token.to_s.inspect}"] - options.each do |key, value| - values << "#{key}=#{value.to_s.inspect}" + values = ["token=#{token.to_s.inspect}"] + options.map do |key, value| + "#{key}=#{value.to_s.inspect}" end "Token #{values * ", "}" end diff --git a/actionpack/lib/action_controller/metal/implicit_render.rb b/actionpack/lib/action_controller/metal/implicit_render.rb index 282dcf66b3..cfa7004048 100644 --- a/actionpack/lib/action_controller/metal/implicit_render.rb +++ b/actionpack/lib/action_controller/metal/implicit_render.rb @@ -12,10 +12,10 @@ module ActionController def method_for_action(action_name) super || begin - if template_exists?(action_name.to_s, _prefix) + if template_exists?(action_name.to_s, _prefixes) "default_render" end end end end -end
\ No newline at end of file +end 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/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index c6d4c6d936..a2e06fe0a6 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -63,13 +63,13 @@ module ActionController #:nodoc: # might look something like this: # # def index - # @people = Person.find(:all) + # @people = Person.all # end # # Here's the same action, with web-service support baked in: # # def index - # @people = Person.find(:all) + # @people = Person.all # # respond_to do |format| # format.html @@ -155,7 +155,7 @@ module ActionController #:nodoc: # Respond to also allows you to specify a common block for different formats by using any: # # def index - # @people = Person.find(:all) + # @people = Person.all # # respond_to do |format| # format.html @@ -178,7 +178,7 @@ module ActionController #:nodoc: # respond_to :html, :xml, :json # # def index - # @people = Person.find(:all) + # @people = Person.all # respond_with(@person) # end # end @@ -208,8 +208,8 @@ module ActionController #:nodoc: # It also accepts a block to be given. It's used to overwrite a default # response: # - # def destroy - # @user = User.find(params[:id]) + # def create + # @user = User.new(params[:user]) # flash[:notice] = "User was successfully created." if @user.save # # respond_with(@user) do |format| @@ -227,7 +227,7 @@ module ActionController #:nodoc: "controller responds to in the class level" if self.class.mimes_for_respond_to.empty? if response = retrieve_response_from_mimes(&block) - options = resources.extract_options! + options = resources.size == 1 ? {} : resources.extract_options! options.merge!(:default_response => response) (options.delete(:responder) || self.class.responder).call(self, resources, options) end @@ -258,9 +258,8 @@ module ActionController #:nodoc: # nil if :not_acceptable was sent to the client. # def retrieve_response_from_mimes(mimes=nil, &block) - collector = Collector.new { default_render } mimes ||= collect_mimes_from_class_level - mimes.each { |mime| collector.send(mime) } + collector = Collector.new(mimes) { default_render } block.call(collector) if block_given? if format = request.negotiate_mime(collector.order) @@ -277,8 +276,9 @@ module ActionController #:nodoc: include AbstractController::Collector attr_accessor :order - def initialize(&block) + def initialize(mimes, &block) @order, @responses, @default_response = [], {}, block + mimes.each { |mime| send(mime) } end def any(*args, &block) @@ -291,7 +291,7 @@ module ActionController #:nodoc: alias :all :any def custom(mime_type, &block) - mime_type = mime_type.is_a?(Mime::Type) ? mime_type : Mime::Type.lookup(mime_type.to_s) + mime_type = Mime::Type.lookup(mime_type.to_s) unless mime_type.is_a?(Mime::Type) @order << mime_type @responses[mime_type] ||= block end diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index b5f1d23ef0..55c650df6c 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -20,6 +20,7 @@ module ActionController # * <tt>Record</tt> - The URL will be generated by calling url_for with the +options+, which will reference a named URL for that record. # * <tt>String</tt> starting with <tt>protocol://</tt> (like <tt>http://</tt>) - Is passed straight through as the target for redirection. # * <tt>String</tt> not containing a protocol - The current protocol and host is prepended to the string. + # * <tt>Proc</tt> - A block that will be executed in the controller's context. Should return any option accepted by +redirect_to+. # * <tt>:back</tt> - Back to the page that issued the request. Useful for forms that are triggered from multiple places. # Short-hand for <tt>redirect_to(request.env["HTTP_REFERER"])</tt> # @@ -30,6 +31,7 @@ module ActionController # redirect_to "/images/screenshot.jpg" # redirect_to articles_url # redirect_to :back + # redirect_to proc { edit_post_url(@post) } # # The redirection happens as a "302 Moved" header unless otherwise specified. # @@ -39,6 +41,9 @@ module ActionController # redirect_to post_url(@post), :status => 301 # redirect_to :action=>'atom', :status => 302 # + # The status code can either be a standard {HTTP Status code}[http://www.iana.org/assignments/http-status-codes] as an + # integer, or a symbol representing the downcased, underscored and symbolized description. + # # It is also possible to assign a flash message as part of the redirection. There are two special accessors for commonly used the flash names # +alert+ and +notice+ as well as a general purpose +flash+ bucket. # @@ -48,8 +53,7 @@ module ActionController # redirect_to post_url(@post), :status => 301, :flash => { :updated_post_id => @post.id } # redirect_to { :action=>'atom' }, :alert => "Something serious happened" # - # When using <tt>redirect_to :back</tt>, if there is no referrer, - # RedirectBackError will be raised. You may specify some fallback + # When using <tt>redirect_to :back</tt>, if there is no referrer, RedirectBackError will be raised. You may specify some fallback # behavior for this case by rescuing RedirectBackError. def redirect_to(options = {}, response_status = {}) #:doc: raise ActionControllerError.new("Cannot redirect to nil!") if options.nil? @@ -83,6 +87,8 @@ module ActionController when :back raise RedirectBackError unless refer = request.headers["Referer"] refer + when Proc + _compute_redirect_to_location options.call else url_for(options) end.gsub(/[\r\n]/, '') diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index 0be07cd1fc..38711c8462 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -2,6 +2,7 @@ require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/object/blank' module ActionController + # See <tt>Renderers.add</tt> def self.add_renderer(key, &block) Renderers.add(key, &block) end @@ -15,30 +16,12 @@ module ActionController end module ClassMethods - def _write_render_options - renderers = _renderers.map do |name, value| - <<-RUBY_EVAL - if options.key?(:#{name}) - _process_options(options) - return _render_option_#{name}(options.delete(:#{name}), options) - end - RUBY_EVAL - end - - class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 - def _handle_render_options(options) - #{renderers.join} - end - RUBY_EVAL - end - def use_renderers(*args) new = _renderers.dup args.each do |key| new[key] = RENDERERS[key] end self._renderers = new.freeze - _write_render_options end alias use_renderer use_renderers end @@ -47,31 +30,69 @@ module ActionController _handle_render_options(options) || super end + def _handle_render_options(options) + _renderers.each do |name, value| + if options.key?(name.to_sym) + _process_options(options) + return send("_render_option_#{name}", options.delete(name.to_sym), options) + end + end + nil + end + + # Hash of available renderers, mapping a renderer name to its proc. + # Default keys are :json, :js, :xml and :update. RENDERERS = {} + + # Adds a new renderer to call within controller actions. + # A renderer is invoked by passing its name as an option to + # <tt>AbstractController::Rendering#render</tt>. To create a renderer + # pass it a name and a block. The block takes two arguments, the first + # is the value paired with its key and the second is the remaining + # hash of options passed to +render+. + # + # === Example + # Create a csv renderer: + # + # ActionController::Renderers.add :csv do |obj, options| + # filename = options[:filename] || 'data' + # str = obj.respond_to?(:to_csv) ? obj.to_csv : obj.to_s + # send_data str, :type => Mime::CSV, + # :disposition => "attachment; filename=#{filename}.csv" + # end + # + # Note that we used Mime::CSV for the csv mime type as it comes with Rails. + # For a custom renderer, you'll need to register a mime type with + # <tt>Mime::Type.register</tt>. + # + # To use the csv renderer in a controller action: + # + # def show + # @csvable = Csvable.find(params[:id]) + # respond_to do |format| + # format.html + # format.csv { render :csv => @csvable, :filename => @csvable.name } + # } + # end + # To use renderers and their mime types in more concise ways, see + # <tt>ActionController::MimeResponds::ClassMethods.respond_to</tt> and + # <tt>ActionController::MimeResponds#respond_with</tt> def self.add(key, &block) define_method("_render_option_#{key}", &block) RENDERERS[key] = block - All._write_render_options end module All extend ActiveSupport::Concern include Renderers - INCLUDED = [] included do self._renderers = RENDERERS - _write_render_options - INCLUDED << self - end - - def self._write_render_options - INCLUDED.each(&:_write_render_options) end end add :json do |json, options| - json = ActiveSupport::JSON.encode(json, options) unless json.respond_to?(:to_str) + json = json.to_json(options) unless json.kind_of?(String) json = "#{options[:callback]}(#{json})" unless options[:callback].blank? self.content_type ||= Mime::JSON self.response_body = json diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index 86bb810947..32d52c84c4 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -2,12 +2,11 @@ module ActionController module Rendering extend ActiveSupport::Concern - include ActionController::RackDelegation include AbstractController::Rendering # Before processing, set the request formats in current controller formats. def process_action(*) #:nodoc: - self.formats = request.formats.map { |x| x.to_sym } + self.formats = request.formats.map { |x| x.ref } super end @@ -21,36 +20,35 @@ module ActionController private - # Normalize arguments by catching blocks and setting them on :update. - def _normalize_args(action=nil, options={}, &blk) #:nodoc: - options = super - options[:update] = blk if block_given? - options - end - - # Normalize both text and status options. - def _normalize_options(options) #:nodoc: - if options.key?(:text) && options[:text].respond_to?(:to_text) - options[:text] = options[:text].to_text - end + # Normalize arguments by catching blocks and setting them on :update. + def _normalize_args(action=nil, options={}, &blk) #:nodoc: + options = super + options[:update] = blk if block_given? + options + end - if options[:status] - options[:status] = Rack::Utils.status_code(options[:status]) - end + # Normalize both text and status options. + def _normalize_options(options) #:nodoc: + if options.key?(:text) && options[:text].respond_to?(:to_text) + options[:text] = options[:text].to_text + end - super + if options[:status] + options[:status] = Rack::Utils.status_code(options[:status]) end - # Process controller specific options, as status, content-type and location. - def _process_options(options) #:nodoc: - status, content_type, location = options.values_at(:status, :content_type, :location) + super + end - self.status = status if status - self.content_type = content_type if content_type - self.headers["Location"] = url_for(location) if location + # Process controller specific options, as status, content-type and location. + def _process_options(options) #:nodoc: + status, content_type, location = options.values_at(:status, :content_type, :location) - super - end + self.status = status if status + self.content_type = content_type if content_type + self.headers["Location"] = url_for(location) if location + super + end end end diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index b632e7aab6..1cd93a188c 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -4,45 +4,27 @@ module ActionController #:nodoc: class InvalidAuthenticityToken < ActionControllerError #:nodoc: end - # Protecting controller actions from CSRF attacks by ensuring that all forms are coming from the current - # web application, not a forged link from another site, is done by embedding a token based on a random - # string stored in the session (which an attacker wouldn't know) in all forms and Ajax requests generated - # by Rails and then verifying the authenticity of that token in the controller. Only HTML/JavaScript - # requests are checked, so this will not protect your XML API (presumably you'll have a different - # authentication scheme there anyway). Also, GET requests are not protected as these should be - # idempotent anyway. + # Controller actions are protected from Cross-Site Request Forgery (CSRF) attacks + # by including a token in the rendered html for your application. This token is + # stored as a random string in the session, to which an attacker does not have + # access. When a request reaches your application, \Rails then verifies the received + # token with the token in the session. Only HTML and javascript requests are checked, + # so this will not protect your XML API (presumably you'll have a different + # authentication scheme there anyway). Also, GET requests are not protected as these + # should be idempotent. # - # This is turned on with the <tt>protect_from_forgery</tt> method, which will check the token and raise an - # ActionController::InvalidAuthenticityToken if it doesn't match what was expected. You can customize the - # error message in production by editing public/422.html. A call to this method in ApplicationController is - # generated by default in post-Rails 2.0 applications. + # CSRF protection is turned on with the <tt>protect_from_forgery</tt> method, + # which will check the token and raise an ActionController::InvalidAuthenticityToken + # if it doesn't match what was expected. A call to this method is generated for new + # \Rails applications by default. You can customize the error message by editing + # public/422.html. # - # The token parameter is named <tt>authenticity_token</tt> by default. If you are generating an HTML form - # manually (without the use of Rails' <tt>form_for</tt>, <tt>form_tag</tt> or other helpers), you have to - # include a hidden field named like that and set its value to what is returned by - # <tt>form_authenticity_token</tt>. - # - # Request forgery protection is disabled by default in test environment. If you are upgrading from Rails - # 1.x, add this to config/environments/test.rb: - # - # # Disable request forgery protection in test environment - # config.action_controller.allow_forgery_protection = false - # - # == Learn more about CSRF (Cross-Site Request Forgery) attacks - # - # Here are some resources: - # * http://isc.sans.org/diary.html?storyid=1750 - # * http://en.wikipedia.org/wiki/Cross-site_request_forgery - # - # Keep in mind, this is NOT a silver-bullet, plug 'n' play, warm security blanket for your rails application. - # There are a few guidelines you should follow: - # - # * Keep your GET requests safe and idempotent. More reading material: - # * http://www.xml.com/pub/a/2002/04/24/deviant.html - # * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1 - # * Make sure the session cookies that Rails creates are non-persistent. Check in Firefox and look - # for "Expires: at end of session" + # The token parameter is named <tt>authenticity_token</tt> by default. The name and + # value of this token must be added to every layout that renders forms by including + # <tt>csrf_meta_tags</tt> in the html +head+. # + # Learn more about CSRF attacks and securing your application in the + # {Ruby on Rails Security Guide}[http://guides.rubyonrails.org/security.html]. module RequestForgeryProtection extend ActiveSupport::Concern @@ -71,39 +53,42 @@ module ActionController #:nodoc: # class FooController < ApplicationController # protect_from_forgery :except => :index # - # # you can disable csrf protection on controller-by-controller basis: - # skip_before_filter :verify_authenticity_token - # end + # You can disable csrf protection on controller-by-controller basis: + # + # skip_before_filter :verify_authenticity_token + # + # It can also be disabled for specific controller actions: + # + # skip_before_filter :verify_authenticity_token, :except => [:create] # # Valid Options: # # * <tt>:only/:except</tt> - Passed to the <tt>before_filter</tt> call. Set which actions are verified. def protect_from_forgery(options = {}) self.request_forgery_protection_token ||= :authenticity_token - before_filter :verify_authenticity_token, options + prepend_before_filter :verify_authenticity_token, options end end protected - - def protect_from_forgery(options = {}) - self.request_forgery_protection_token ||= :authenticity_token - before_filter :verify_authenticity_token, options - end - # The actual before_filter that is used. Modify this to change how you handle unverified requests. def verify_authenticity_token - verified_request? || raise(ActionController::InvalidAuthenticityToken) + verified_request? || handle_unverified_request + end + + def handle_unverified_request + reset_session end # Returns true or false if a request is verified. Checks: # - # * is the format restricted? By default, only HTML requests are checked. # * is it a GET request? Gets should be safe and idempotent # * Does the form_authenticity_token match the given token value from the params? + # * Does the X-CSRF-Token header match the form_authenticity_token def verified_request? - !protect_against_forgery? || request.forgery_whitelisted? || - form_authenticity_token == params[request_forgery_protection_token] + !protect_against_forgery? || request.get? || + form_authenticity_token == params[request_forgery_protection_token] || + form_authenticity_token == request.headers['X-CSRF-Token'] end # Sets the token value for the current session. diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb index cb644dfd16..4b45413cf8 100644 --- a/actionpack/lib/action_controller/metal/responder.rb +++ b/actionpack/lib/action_controller/metal/responder.rb @@ -1,7 +1,7 @@ require 'active_support/json' module ActionController #:nodoc: - # Responder is responsible for exposing a resource to different mime requests, + # Responsible for exposing a resource to different mime requests, # usually depending on the HTTP verb. The responder is triggered when # <code>respond_with</code> is called. The simplest case to study is a GET request: # @@ -24,10 +24,10 @@ module ActionController #:nodoc: # # === Builtin HTTP verb semantics # - # The default Rails responder holds semantics for each HTTP verb. Depending on the + # The default \Rails responder holds semantics for each HTTP verb. Depending on the # content type, verb and the resource status, it will behave differently. # - # Using Rails default responder, a POST request for creating an object could + # Using \Rails default responder, a POST request for creating an object could # be written as: # # def create @@ -77,8 +77,6 @@ module ActionController #:nodoc: # # respond_with(@project, :manager, @task) # - # Check <code>polymorphic_url</code> documentation for more examples. - # class Responder attr_reader :controller, :request, :format, :resource, :resources, :options @@ -89,6 +87,8 @@ module ActionController #:nodoc: def initialize(controller, resources, options={}) @controller = controller + @request = @controller.request + @format = @controller.formats.first @resource = resources.last @resources = resources @options = options @@ -99,14 +99,6 @@ module ActionController #:nodoc: delegate :head, :render, :redirect_to, :to => :controller delegate :get?, :post?, :put?, :delete?, :to => :request - def request - @request ||= @controller.request - end - - def format - @format ||= @controller.formats.first - end - # Undefine :to_json and :to_yaml since it's defined on Object undef_method(:to_json) if method_defined?(:to_json) undef_method(:to_yaml) if method_defined?(:to_yaml) @@ -121,7 +113,7 @@ module ActionController #:nodoc: # Main entry point for responder responsible to dispatch to the proper format. # def respond - method = :"to_#{format}" + method = "to_#{format}" respond_to?(method) ? send(method) : to_format end @@ -146,7 +138,7 @@ module ActionController #:nodoc: protected - # This is the common behavior for "navigation" requests, like :html, :iphone and so forth. + # This is the common behavior for formats associated with browsing, like :html, :iphone and so forth. def navigation_behavior(error) if get? raise error @@ -157,7 +149,7 @@ module ActionController #:nodoc: end end - # This is the common behavior for "API" requests, like :xml and :json. + # This is the common behavior for formats associated with APIs, such as :xml and :json. def api_behavior(error) raise error unless resourceful? @@ -167,6 +159,8 @@ module ActionController #:nodoc: display resource.errors, :status => :unprocessable_entity elsif post? display resource, :status => :created, :location => api_location + elsif has_empty_resource_definition? + display empty_resource, :status => :ok else head :ok end @@ -175,7 +169,7 @@ module ActionController #:nodoc: # Checks whether the resource responds to the current format or not. # def resourceful? - resource.respond_to?(:"to_#{format}") + resource.respond_to?("to_#{format}") end # Returns the resource location by retrieving it from the options or @@ -227,5 +221,23 @@ module ActionController #:nodoc: def default_action @action ||= ACTIONS_FOR_VERBS[request.request_method_symbol] end + + # Check whether resource needs a specific definition of empty resource to be valid + # + def has_empty_resource_definition? + respond_to?("empty_#{format}_resource") + end + + # Delegate to proper empty resource method + # + def empty_resource + send("empty_#{format}_resource") + end + + # Return a valid empty JSON resource + # + def empty_json_resource + "{}" + end end end diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb index d75b46dace..312dc8eb3e 100644 --- a/actionpack/lib/action_controller/metal/streaming.rb +++ b/actionpack/lib/action_controller/metal/streaming.rb @@ -69,10 +69,6 @@ module ActionController #:nodoc: options[:filename] ||= File.basename(path) unless options[:url_based_filename] send_file_headers! options - if options[:x_sendfile] - ActiveSupport::Deprecation.warn(":x_sendfile is no longer needed in send_file", caller) - end - self.status = options[:status] || 200 self.content_type = options[:content_type] if options.key?(:content_type) self.response_body = File.open(path, "rb") @@ -105,10 +101,6 @@ module ActionController #:nodoc: # send_data image.data, :type => image.content_type, :disposition => 'inline' # # See +send_file+ for more information on HTTP Content-* headers and caching. - # - # <b>Tip:</b> if you want to stream large amounts of on-the-fly generated - # data to the browser, then use <tt>render :text => proc { ... }</tt> - # instead. See ActionController::Base#render for more information. def send_data(data, options = {}) #:doc: send_file_headers! options.dup render options.slice(:status, :content_type).merge(:text => data) @@ -121,10 +113,6 @@ module ActionController #:nodoc: raise ArgumentError, ":#{arg} option required" if options[arg].nil? end - if options.key?(:length) - ActiveSupport::Deprecation.warn("You do not need to provide the file's length", caller) - end - disposition = options[:disposition] disposition += %(; filename="#{options[:filename]}") if options[:filename] diff --git a/actionpack/lib/action_controller/metal/testing.rb b/actionpack/lib/action_controller/metal/testing.rb index 4b8c452d50..f4efeb33ba 100644 --- a/actionpack/lib/action_controller/metal/testing.rb +++ b/actionpack/lib/action_controller/metal/testing.rb @@ -14,18 +14,9 @@ module ActionController cookies.write(@_response) end @_response.prepare! - set_test_assigns ret end - def set_test_assigns - @assigns = {} - (instance_variable_names - self.class.protected_instance_variables).each do |var| - name, value = var[1..-1], instance_variable_get(var) - @assigns[name] = value - end - end - # TODO : Rewrite tests using controller.headers= to use Rack env def headers=(new_headers) @_response ||= ActionDispatch::Response.new diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb index a51fc5b8e4..6fc0cf1fb8 100644 --- a/actionpack/lib/action_controller/metal/url_for.rb +++ b/actionpack/lib/action_controller/metal/url_for.rb @@ -2,27 +2,25 @@ module ActionController module UrlFor extend ActiveSupport::Concern - include ActionDispatch::Routing::UrlFor + include AbstractController::UrlFor def url_options - super.reverse_merge( - :host => request.host_with_port, + @_url_options ||= super.reverse_merge( + :host => request.host, + :port => request.optional_port, :protocol => request.protocol, :_path_segments => request.symbolized_path_parameters - ).merge(:script_name => request.script_name) - end - - def _routes - raise "In order to use #url_for, you must include routing helpers explicitly. " \ - "For instance, `include Rails.application.routes.url_helpers" - end + ).freeze - module ClassMethods - def action_methods - @action_methods ||= begin - super - _routes.named_routes.helper_names + if _routes.equal?(env["action_dispatch.routes"]) + @_url_options.dup.tap do |options| + options[:script_name] = request.script_name.dup + options.freeze end + else + @_url_options end end + end end diff --git a/actionpack/lib/action_controller/middleware.rb b/actionpack/lib/action_controller/middleware.rb index 2115b07b3e..437fec3dc6 100644 --- a/actionpack/lib/action_controller/middleware.rb +++ b/actionpack/lib/action_controller/middleware.rb @@ -31,7 +31,7 @@ module ActionController super() @_app = app end - + def index call(env) end diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb index cd2dfafbe6..f0c29825ba 100644 --- a/actionpack/lib/action_controller/railtie.rb +++ b/actionpack/lib/action_controller/railtie.rb @@ -2,35 +2,13 @@ require "rails" require "action_controller" require "action_dispatch/railtie" require "action_view/railtie" -require "active_support/deprecation/proxy_wrappers" -require "active_support/deprecation" +require "abstract_controller/railties/routes_helpers" +require "action_controller/railties/paths" module ActionController class Railtie < Rails::Railtie config.action_controller = ActiveSupport::OrderedOptions.new - config.action_controller.singleton_class.tap do |d| - d.send(:define_method, :session) do - ActiveSupport::Deprecation.warn "config.action_controller.session has been deprecated. " << - "Please use Rails.application.config.session_store instead.", caller - end - - d.send(:define_method, :session=) do |val| - ActiveSupport::Deprecation.warn "config.action_controller.session= has been deprecated. " << - "Please use config.session_store(name, options) instead.", caller - end - - d.send(:define_method, :session_store) do - ActiveSupport::Deprecation.warn "config.action_controller.session_store has been deprecated. " << - "Please use Rails.application.config.session_store instead.", caller - end - - d.send(:define_method, :session_store=) do |val| - ActiveSupport::Deprecation.warn "config.action_controller.session_store= has been deprecated. " << - "Please use config.session_store(name, options) instead.", caller - end - end - initializer "action_controller.logger" do ActiveSupport.on_load(:action_controller) { self.logger ||= Rails.logger } end @@ -43,24 +21,27 @@ module ActionController paths = app.config.paths options = app.config.action_controller - options.assets_dir ||= paths.public.to_a.first - options.javascripts_dir ||= paths.public.javascripts.to_a.first - options.stylesheets_dir ||= paths.public.stylesheets.to_a.first - options.page_cache_directory ||= paths.public.to_a.first - options.helpers_path ||= paths.app.helpers.to_a + options.assets_dir ||= paths["public"].first + options.javascripts_dir ||= paths["public/javascripts"].first + options.stylesheets_dir ||= paths["public/stylesheets"].first + options.page_cache_directory ||= paths["public"].first + + # make sure readers methods get compiled + options.asset_path ||= app.config.asset_path + options.asset_host ||= app.config.asset_host ActiveSupport.on_load(:action_controller) do - include app.routes.url_helpers + include app.routes.mounted_helpers + extend ::AbstractController::Railties::RoutesHelpers.with(app.routes) + extend ::ActionController::Railties::Paths.with(app) options.each { |k,v| send("#{k}=", v) } end end - initializer "action_controller.deprecated_routes" do |app| - message = "ActionController::Routing::Routes is deprecated. " \ - "Instead, use Rails.application.routes" - - proxy = ActiveSupport::Deprecation::DeprecatedObjectProxy.new(app.routes, message) - ActionController::Routing::Routes = proxy + initializer "action_controller.compile_config_methods" do + ActiveSupport.on_load(:action_controller) do + config.compile_methods! if config.respond_to?(:compile_methods!) + end end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_controller/railties/paths.rb b/actionpack/lib/action_controller/railties/paths.rb new file mode 100644 index 0000000000..dce3c2fe88 --- /dev/null +++ b/actionpack/lib/action_controller/railties/paths.rb @@ -0,0 +1,32 @@ +module ActionController + module Railties + module Paths + def self.with(app) + Module.new do + define_method(:inherited) do |klass| + super(klass) + + if namespace = klass.parents.detect {|m| m.respond_to?(:_railtie) } + paths = namespace._railtie.paths["app/helpers"].existent + else + paths = app.config.helpers_paths + end + + klass.helpers_path = paths + if klass.superclass == ActionController::Base && ActionController::Base.include_all_helpers + klass.helper :all + end + + if app.config.serve_static_assets && namespace + paths = namespace._railtie.config.paths + + klass.config.assets_dir = paths["public"].first + klass.config.javascripts_dir = paths["public/javascripts"].first + klass.config.stylesheets_dir = paths["public/stylesheets"].first + end + end + end + end + end + end +end diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index e306697f4b..bc4f8bb9ce 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -1,6 +1,7 @@ require 'rack/session/abstract/id' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/to_query' +require 'active_support/core_ext/class/attribute' module ActionController module TemplateAssertions @@ -40,6 +41,13 @@ module ActionController ActiveSupport::Notifications.unsubscribe("!render_template.action_view") end + def process(*args) + @partials = Hash.new(0) + @templates = Hash.new(0) + @layouts = Hash.new(0) + super + end + # Asserts that the request was rendered with the appropriate template file or partials. # # ==== Examples @@ -127,7 +135,7 @@ module ActionController class Result < ::Array #:nodoc: def to_s() join '/' end def self.new_escaped(strings) - new strings.collect {|str| URI.unescape str} + new strings.collect {|str| uri_parser.unescape str} end end @@ -164,9 +172,14 @@ 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/ } + @symbolized_path_params = nil @method = @request_method = nil @fullpath = @ip = @remote_ip = nil @env['action_dispatch.request.query_parameters'] = {} @@ -186,20 +199,23 @@ module ActionController end end - class TestSession < ActionDispatch::Session::AbstractStore::SessionHash #:nodoc: - DEFAULT_OPTIONS = ActionDispatch::Session::AbstractStore::DEFAULT_OPTIONS + class TestSession < Rack::Session::Abstract::SessionHash #:nodoc: + DEFAULT_OPTIONS = Rack::Session::Abstract::ID::DEFAULT_OPTIONS def initialize(session = {}) + @env, @by = nil, nil replace(session.stringify_keys) @loaded = true end - def exists?; true; end + def exists? + true + end end # Superclass for ActionController functional tests. Functional tests allow you to # test a single controller action per test method. This should not be confused with - # integration tests (see ActionController::IntegrationTest), which are more like + # integration tests (see ActionDispatch::IntegrationTest), which are more like # "stories" that can involve multiple controllers and multiple actions (i.e. multiple # different HTTP requests). # @@ -244,7 +260,7 @@ module ActionController # after calling +post+. If the various assert methods are not sufficient, then you # may use this object to inspect the HTTP response in detail. # - # (Earlier versions of Rails required each functional test to subclass + # (Earlier versions of \Rails required each functional test to subclass # Test::Unit::TestCase and define @controller, @request, @response in +setup+.) # # == Controller is automatically inferred @@ -257,7 +273,7 @@ module ActionController # tests WidgetController # end # - # == Testing controller internals + # == \Testing controller internals # # In addition to these specific assertions, you also have easy access to various collections that the regular test/unit assertions # can be used against. These collections are: @@ -265,7 +281,7 @@ module ActionController # * assigns: Instance variables assigned in the action that are available for the view. # * session: Objects being saved in the session. # * flash: The flash objects currently in the session. - # * cookies: Cookies being sent to the user on this request. + # * cookies: \Cookies being sent to the user on this request. # # These collections can be used just like any other hash: # @@ -289,9 +305,13 @@ 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 + # == \Testing named routes # # If you're using named routes, they can be easily tested using the original named routes' methods straight in the test case. # Example: @@ -311,14 +331,14 @@ module ActionController def tests(controller_class) self.controller_class = controller_class end - + def controller_class=(new_class) prepare_controller_class(new_class) if new_class - write_inheritable_attribute(:controller_class, new_class) + self._controller_class = new_class end def controller_class - if current_controller_class = read_inheritable_attribute(:controller_class) + if current_controller_class = self._controller_class current_controller_class else self.controller_class = determine_default_controller_class(name) @@ -393,16 +413,18 @@ module ActionController parameters ||= {} @request.assign_parameters(@routes, @controller.class.name.underscore.sub(/_controller$/, ''), action.to_s, parameters) - @request.session = ActionController::TestSession.new(session) unless session.nil? + @request.session = ActionController::TestSession.new(session) if session @request.session["flash"] = @request.flash.update(flash || {}) @request.session["flash"].sweep @controller.request = @request @controller.params.merge!(parameters) build_request_uri(action, parameters) - Base.class_eval { include Testing } + @controller.class.class_eval { include Testing } @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 @@ -416,7 +438,7 @@ module ActionController @request.env.delete('PATH_INFO') - if @controller + if defined?(@controller) && @controller @controller.request = @request @controller.params = {} end @@ -430,6 +452,7 @@ module ActionController included do include ActionController::TemplateAssertions include ActionDispatch::Assertions + class_attribute :_controller_class setup :setup_controller_request_and_response end @@ -437,7 +460,7 @@ module ActionController def build_request_uri(action, parameters) unless @request.env["PATH_INFO"] - options = @controller.__send__(:url_options).merge(parameters) + options = @controller.respond_to?(:url_options) ? @controller.__send__(:url_options).merge(parameters) : parameters options.update( :only_path => true, :action => action, @@ -461,9 +484,11 @@ module ActionController # The exception is stored in the exception accessor for further inspection. module RaiseActionExceptions def self.included(base) - base.class_eval do - attr_accessor :exception - protected :exception, :exception= + unless base.method_defined?(:exception) && base.method_defined?(:exception=) + base.class_eval do + attr_accessor :exception + protected :exception, :exception= + end end end diff --git a/actionpack/lib/action_controller/vendor/html-scanner/html/document.rb b/actionpack/lib/action_controller/vendor/html-scanner/html/document.rb index b8d73c350d..7fa3aead82 100644 --- a/actionpack/lib/action_controller/vendor/html-scanner/html/document.rb +++ b/actionpack/lib/action_controller/vendor/html-scanner/html/document.rb @@ -48,7 +48,7 @@ EOF end end end - + # Search the tree for (and return) the first node that matches the given # conditions. The conditions are interpreted differently for different node # types, see HTML::Text#find and HTML::Tag#find. @@ -62,7 +62,7 @@ EOF def find_all(conditions) @root.find_all(conditions) end - + end end diff --git a/actionpack/lib/action_controller/vendor/html-scanner/html/node.rb b/actionpack/lib/action_controller/vendor/html-scanner/html/node.rb index a874519978..22b3243104 100644 --- a/actionpack/lib/action_controller/vendor/html-scanner/html/node.rb +++ b/actionpack/lib/action_controller/vendor/html-scanner/html/node.rb @@ -1,7 +1,7 @@ require 'strscan' module HTML #:nodoc: - + class Conditions < Hash #:nodoc: def initialize(hash) super() @@ -18,14 +18,14 @@ module HTML #:nodoc: hash[k] = Conditions.new(v) when :children hash[k] = v = keys_to_symbols(v) - v.each do |k,v2| - case k + v.each do |key,value| + case key when :count, :greater_than, :less_than # keys are valid, and require no further processing when :only - v[k] = Conditions.new(v2) + v[key] = Conditions.new(value) else - raise "illegal key #{k.inspect} => #{v2.inspect}" + raise "illegal key #{key.inspect} => #{value.inspect}" end end else @@ -38,18 +38,14 @@ module HTML #:nodoc: private def keys_to_strings(hash) - hash.keys.inject({}) do |h,k| - h[k.to_s] = hash[k] - h - end + Hash[hash.keys.map {|k| [k.to_s, hash[k]]}] end def keys_to_symbols(hash) - hash.keys.inject({}) do |h,k| + Hash[hash.keys.map do |k| raise "illegal key #{k.inspect}" unless k.respond_to?(:to_sym) - h[k.to_sym] = hash[k] - h - end + [k.to_sym, hash[k]] + end] end end @@ -57,17 +53,17 @@ module HTML #:nodoc: class Node #:nodoc: # The array of children of this node. Not all nodes have children. attr_reader :children - + # The parent node of this node. All nodes have a parent, except for the # root node. attr_reader :parent - + # The line number of the input where this node was begun attr_reader :line - + # The byte position in the input where this node was begun attr_reader :position - + # Create a new node as a child of the given parent. def initialize(parent, line=0, pos=0) @parent = parent @@ -77,9 +73,7 @@ module HTML #:nodoc: # Return a textual representation of the node. def to_s - s = "" - @children.each { |child| s << child.to_s } - s + @children.join() end # Return false (subclasses must override this to provide specific matching @@ -92,7 +86,7 @@ module HTML #:nodoc: # returns non +nil+. Returns the result of the #find call that succeeded. def find(conditions) conditions = validate_conditions(conditions) - @children.each do |child| + @children.each do |child| node = child.find(conditions) return node if node end @@ -133,7 +127,7 @@ module HTML #:nodoc: equivalent end - + class <<self def parse(parent, line, pos, content, strict=true) if content !~ /^<\S/ @@ -160,11 +154,11 @@ module HTML #:nodoc: return CDATA.new(parent, line, pos, scanner.pre_match.gsub(/<!\[CDATA\[/, '')) end - + closing = ( scanner.scan(/\//) ? :close : nil ) return Text.new(parent, line, pos, content) unless name = scanner.scan(/[\w:-]+/) name.downcase! - + unless closing scanner.skip(/\s*/) attributes = {} @@ -191,13 +185,13 @@ module HTML #:nodoc: attributes[attr.downcase] = value scanner.skip(/\s*/) end - + closing = ( scanner.scan(/\//) ? :self : nil ) end - + unless scanner.scan(/\s*>/) if strict - raise "expected > (got #{scanner.rest.inspect} for #{content}, #{attributes.inspect})" + raise "expected > (got #{scanner.rest.inspect} for #{content}, #{attributes.inspect})" else # throw away all text until we find what we're looking for scanner.skip_until(/>/) or scanner.terminate @@ -212,9 +206,9 @@ module HTML #:nodoc: # A node that represents text, rather than markup. class Text < Node #:nodoc: - + attr_reader :content - + # Creates a new text node as a child of the given parent, with the given # content. def initialize(parent, line, pos, content) @@ -240,7 +234,7 @@ module HTML #:nodoc: def find(conditions) match(conditions) && self end - + # Returns non-+nil+ if this node meets the given conditions, or +nil+ # otherwise. See the discussion of #find for the valid conditions. def match(conditions) @@ -268,7 +262,7 @@ module HTML #:nodoc: content == node.content end end - + # A CDATA node is simply a text node with a specialized way of displaying # itself. class CDATA < Text #:nodoc: @@ -281,16 +275,16 @@ module HTML #:nodoc: # closing tag, or a self-closing tag. It has a name, and may have a hash of # attributes. class Tag < Node #:nodoc: - + # Either +nil+, <tt>:close</tt>, or <tt>:self</tt> attr_reader :closing - + # Either +nil+, or a hash of attributes for this node. attr_reader :attributes # The name of this tag. attr_reader :name - + # Create a new node as a child of the given parent, using the given content # to describe the node. It will be parsed and the node name, attributes and # closing status extracted. @@ -344,7 +338,7 @@ module HTML #:nodoc: def tag? true end - + # Returns +true+ if the node meets any of the given conditions. The # +conditions+ parameter must be a hash of any of the following keys # (all are optional): @@ -404,7 +398,7 @@ module HTML #:nodoc: # node.match :descendant => { :tag => "strong" } # # # test if the node has between 2 and 4 span tags as immediate children - # node.match :children => { :count => 2..4, :only => { :tag => "span" } } + # node.match :children => { :count => 2..4, :only => { :tag => "span" } } # # # get funky: test to see if the node is a "div", has a "ul" ancestor # # and an "li" parent (with "class" = "enum"), and whether or not it has @@ -439,7 +433,7 @@ module HTML #:nodoc: # test children return false unless children.find { |child| child.match(conditions[:child]) } if conditions[:child] - + # test ancestors if conditions[:ancestor] return false unless catch :found do @@ -457,13 +451,13 @@ module HTML #:nodoc: child.match(:descendant => conditions[:descendant]) end end - + # count children if opts = conditions[:children] matches = children.select do |c| (c.kind_of?(HTML::Tag) and (c.closing == :self or ! c.childless?)) end - + matches = matches.select { |c| c.match(opts[:only]) } if opts[:only] opts.each do |key, value| next if key == :only @@ -489,24 +483,24 @@ module HTML #:nodoc: self_index = siblings.index(self) if conditions[:sibling] - return false unless siblings.detect do |s| + return false unless siblings.detect do |s| s != self && s.match(conditions[:sibling]) end end if conditions[:before] - return false unless siblings[self_index+1..-1].detect do |s| + return false unless siblings[self_index+1..-1].detect do |s| s != self && s.match(conditions[:before]) end end if conditions[:after] - return false unless siblings[0,self_index].detect do |s| + return false unless siblings[0,self_index].detect do |s| s != self && s.match(conditions[:after]) end end end - + true end @@ -515,7 +509,7 @@ module HTML #:nodoc: return false unless closing == node.closing && self.name == node.name attributes == node.attributes end - + private # Match the given value to the given condition. def match_condition(value, condition) diff --git a/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb b/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb index 51e0868995..09dd08898c 100644 --- a/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb +++ b/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb @@ -1,5 +1,5 @@ require 'set' -require 'active_support/core_ext/class/inheritable_attributes' +require 'active_support/core_ext/class/attribute' module HTML class Sanitizer @@ -7,11 +7,11 @@ module HTML return text unless sanitizeable?(text) tokenize(text, options).join end - + def sanitizeable?(text) !(text.nil? || text.empty? || !text.index("<")) end - + protected def tokenize(text, options) tokenizer = HTML::Tokenizer.new(text) @@ -22,12 +22,12 @@ module HTML end result end - + def process_node(node, result, options) result << node.to_s end end - + class FullSanitizer < Sanitizer def sanitize(text, options = {}) result = super @@ -37,12 +37,12 @@ module HTML # Recurse - handle all dirty nested tags result == text ? result : sanitize(result, options) end - + def process_node(node, result, options) result << node.to_s if node.class == HTML::Text end end - + class LinkSanitizer < FullSanitizer cattr_accessor :included_tags, :instance_writer => false self.included_tags = Set.new(%w(a href)) @@ -50,51 +50,51 @@ module HTML def sanitizeable?(text) !(text.nil? || text.empty? || !((text.index("<a") || text.index("<href")) && text.index(">"))) end - + protected def process_node(node, result, options) - result << node.to_s unless node.is_a?(HTML::Tag) && included_tags.include?(node.name) + result << node.to_s unless node.is_a?(HTML::Tag) && included_tags.include?(node.name) end end - + class WhiteListSanitizer < Sanitizer [:protocol_separator, :uri_attributes, :allowed_attributes, :allowed_tags, :allowed_protocols, :bad_tags, :allowed_css_properties, :allowed_css_keywords, :shorthand_css_properties].each do |attr| - class_inheritable_accessor attr, :instance_writer => false + class_attribute attr, :instance_writer => false end # A regular expression of the valid characters used to separate protocols like # the ':' in 'http://foo.com' self.protocol_separator = /:|(�*58)|(p)|(%|%)3A/ - + # Specifies a Set of HTML attributes that can have URIs. self.uri_attributes = Set.new(%w(href src cite action longdesc xlink:href lowsrc)) # Specifies a Set of 'bad' tags that the #sanitize helper will remove completely, as opposed # to just escaping harmless tags like <font> self.bad_tags = Set.new(%w(script)) - + # Specifies the default Set of tags that the #sanitize helper will allow unscathed. - self.allowed_tags = Set.new(%w(strong em b i p code pre tt samp kbd var sub - sup dfn cite big small address hr br div span h1 h2 h3 h4 h5 h6 ul ol li dl dt dd abbr + self.allowed_tags = Set.new(%w(strong em b i p code pre tt samp kbd var sub + sup dfn cite big small address hr br div span h1 h2 h3 h4 h5 h6 ul ol li dl dt dd abbr acronym a img blockquote del ins)) - # Specifies the default Set of html attributes that the #sanitize helper will leave + # Specifies the default Set of html attributes that the #sanitize helper will leave # in the allowed tag. self.allowed_attributes = Set.new(%w(href src width height alt cite datetime title class name xml:lang abbr)) - + # Specifies the default Set of acceptable css properties that #sanitize and #sanitize_css will accept. - self.allowed_protocols = Set.new(%w(ed2k ftp http https irc mailto news gopher nntp telnet webcal xmpp callto + self.allowed_protocols = Set.new(%w(ed2k ftp http https irc mailto news gopher nntp telnet webcal xmpp callto feed svn urn aim rsync tag ssh sftp rtsp afs)) - + # Specifies the default Set of acceptable css keywords that #sanitize and #sanitize_css will accept. - self.allowed_css_properties = Set.new(%w(azimuth background-color border-bottom-color border-collapse - border-color border-left-color border-right-color border-top-color clear color cursor direction display + self.allowed_css_properties = Set.new(%w(azimuth background-color border-bottom-color border-collapse + border-color border-left-color border-right-color border-top-color clear color cursor direction display elevation float font font-family font-size font-style font-variant font-weight height letter-spacing line-height overflow pause pause-after pause-before pitch pitch-range richness speak speak-header speak-numeral speak-punctuation speech-rate stress text-align text-decoration text-indent unicode-bidi vertical-align voice-family volume white-space width)) - + # Specifies the default Set of acceptable css keywords that #sanitize and #sanitize_css will accept. self.allowed_css_keywords = Set.new(%w(auto aqua black block blue bold both bottom brown center collapse dashed dotted fuchsia gray green !important italic left lime maroon medium none navy normal @@ -118,9 +118,9 @@ module HTML style.scan(/([-\w]+)\s*:\s*([^:;]*)/) do |prop,val| if allowed_css_properties.include?(prop.downcase) clean << prop + ': ' + val + ';' - elsif shorthand_css_properties.include?(prop.split('-')[0].downcase) + elsif shorthand_css_properties.include?(prop.split('-')[0].downcase) unless val.split().any? do |keyword| - !allowed_css_keywords.include?(keyword) && + !allowed_css_keywords.include?(keyword) && keyword !~ /^(#[0-9a-f]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)$/ end clean << prop + ': ' + val + ';' @@ -146,7 +146,7 @@ module HTML else options[:parent].unshift node.name end - + process_attributes_for node, options options[:tags].include?(node.name) ? node : nil @@ -154,7 +154,7 @@ module HTML bad_tags.include?(options[:parent].first) ? nil : node.to_s.gsub(/</, "<") end end - + def process_attributes_for(node, options) return unless node.attributes node.attributes.keys.each do |attr_name| @@ -169,8 +169,8 @@ module HTML end def contains_bad_protocols?(attr_name, value) - uri_attributes.include?(attr_name) && - (value =~ /(^[^\/:]*):|(�*58)|(p)|(%|%)3A/ && !allowed_protocols.include?(value.split(protocol_separator).first)) + uri_attributes.include?(attr_name) && + (value =~ /(^[^\/:]*):|(�*58)|(p)|(%|%)3A/ && !allowed_protocols.include?(value.split(protocol_separator).first.downcase)) end end end diff --git a/actionpack/lib/action_controller/vendor/html-scanner/html/selector.rb b/actionpack/lib/action_controller/vendor/html-scanner/html/selector.rb index e2c49c284f..0fe2e6d1a6 100644 --- a/actionpack/lib/action_controller/vendor/html-scanner/html/selector.rb +++ b/actionpack/lib/action_controller/vendor/html-scanner/html/selector.rb @@ -182,7 +182,7 @@ module HTML # not another using <tt>:not</tt>. For example: # p:not(.post) # Matches all paragraphs that do not have the class <tt>.post</tt>. - # + # # === Substitution Values # # You can use substitution with identifiers, class names and element values. diff --git a/actionpack/lib/action_controller/vendor/html-scanner/html/tokenizer.rb b/actionpack/lib/action_controller/vendor/html-scanner/html/tokenizer.rb index 240dc1890f..c252e01cf5 100644 --- a/actionpack/lib/action_controller/vendor/html-scanner/html/tokenizer.rb +++ b/actionpack/lib/action_controller/vendor/html-scanner/html/tokenizer.rb @@ -1,7 +1,7 @@ require 'strscan' module HTML #:nodoc: - + # A simple HTML tokenizer. It simply breaks a stream of text into tokens, where each # token is a string. Each string represents either "text", or an HTML element. # @@ -14,13 +14,13 @@ module HTML #:nodoc: # p token # end class Tokenizer #:nodoc: - + # The current (byte) position in the text attr_reader :position - + # The current line number attr_reader :line - + # Create a new Tokenizer for the given text. def initialize(text) text.encode! if text.encoding_aware? @@ -42,7 +42,7 @@ module HTML #:nodoc: update_current_line(scan_text) end end - + private # Treat the text at the current position as a tag, and scan it. Supports @@ -69,13 +69,13 @@ module HTML #:nodoc: def scan_text "#{@scanner.getch}#{@scanner.scan(/[^<]*/)}" end - + # Counts the number of newlines in the text and updates the current line # accordingly. def update_current_line(text) text.scan(/\r?\n/) { @current_line += 1 } end - + # Skips over quoted strings, so that less-than and greater-than characters # within the strings are ignored. def consume_quoted_regions @@ -103,5 +103,5 @@ module HTML #:nodoc: text end end - + end diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index aeec934be8..49971fc9f8 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2010 David Heinemeier Hansson +# Copyright (c) 2004-2011 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -53,6 +53,7 @@ module ActionDispatch autoload :Flash autoload :Head autoload :ParamsParser + autoload :Reloader autoload :RemoteIp autoload :Rescue autoload :ShowExceptions @@ -85,6 +86,7 @@ module ActionDispatch autoload_under 'testing' do autoload :Assertions autoload :Integration + autoload :IntegrationTest, 'action_dispatch/testing/integration' autoload :PerformanceTest autoload :TestProcess autoload :TestRequest diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb index e9fdf75cc8..4f4cb96a74 100644 --- a/actionpack/lib/action_dispatch/http/cache.rb +++ b/actionpack/lib/action_dispatch/http/cache.rb @@ -39,10 +39,11 @@ module ActionDispatch end module Response - attr_reader :cache_control + attr_reader :cache_control, :etag + alias :etag? :etag def initialize(*) - status, header, body = super + super @cache_control = {} @etag = self["ETag"] @@ -50,8 +51,7 @@ module ActionDispatch if cache_control = self["Cache-Control"] cache_control.split(/,\s*/).each do |segment| first, last = segment.split("=") - last ||= true - @cache_control[first.to_sym] = last + @cache_control[first.to_sym] = last || true end end end @@ -70,14 +70,6 @@ module ActionDispatch headers['Last-Modified'] = utc_time.httpdate end - def etag - @etag - end - - def etag? - @etag - end - def etag=(etag) key = ActiveSupport::Cache.expand_cache_key(etag) @etag = self["ETag"] = %("#{Digest::MD5.hexdigest(key)}") @@ -88,38 +80,19 @@ module ActionDispatch def handle_conditional_get! if etag? || last_modified? || !@cache_control.empty? set_conditional_cache_control! - elsif nonempty_ok_response? - self.etag = body - - if request && request.etag_matches?(etag) - self.status = 304 - self.body = [] - end - - set_conditional_cache_control! - else - headers["Cache-Control"] = "no-cache" end end - def nonempty_ok_response? - @status == 200 && string_body? - end - - def string_body? - !@blank && @body.respond_to?(:all?) && @body.all? { |part| part.is_a?(String) } - end - DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate" def set_conditional_cache_control! - control = @cache_control - return if self["Cache-Control"].present? + control = @cache_control + if control.empty? headers["Cache-Control"] = DEFAULT_CACHE_CONTROL - elsif @cache_control[:no_cache] + elsif control[:no_cache] headers["Cache-Control"] = "no-cache" else extras = control[:extras] 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/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index 4082770b85..68ba1a81b5 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -32,23 +32,25 @@ module ActionDispatch end end - # Returns the Mime type for the \format used in the request. + # Returns the MIME type for the \format used in the request. # # GET /posts/5.xml | request.format => Mime::XML # GET /posts/5.xhtml | request.format => Mime::HTML - # GET /posts/5 | request.format => Mime::HTML or MIME::JS, or request.accepts.first depending on the value of <tt>ActionController::Base.use_accept_header</tt> + # GET /posts/5 | request.format => Mime::HTML or MIME::JS, or request.accepts.first # def format(view_path = []) formats.first end + BROWSER_LIKE_ACCEPTS = /,\s*\*\/\*|\*\/\*\s*,/ + def formats accept = @env['HTTP_ACCEPT'] @env["action_dispatch.request.formats"] ||= if parameters[:format] Array(Mime[parameters[:format]]) - elsif xhr? || (accept && accept !~ /,\s*\*\/\*/) + elsif xhr? || (accept && accept !~ BROWSER_LIKE_ACCEPTS) accepts else [Mime::HTML] diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index c6fc582851..7c9ebe7c7b 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -80,6 +80,9 @@ module Mime end class << self + + TRAILING_STAR_REGEXP = /(text|application)\/\*/ + def lookup(string) LOOKUP[string] end @@ -105,15 +108,28 @@ module Mime def parse(accept_header) if accept_header !~ /,/ - [Mime::Type.lookup(accept_header)] + if accept_header =~ TRAILING_STAR_REGEXP + parse_data_with_trailing_star($1) + else + [Mime::Type.lookup(accept_header)] + end else # keep track of creation order to keep the subsequent sort stable - list = [] - accept_header.split(/,/).each_with_index do |header, index| - params, q = header.split(/;\s*q=/) - if params - params.strip! - list << AcceptItem.new(index, params, q) unless params.empty? + list, index = [], 0 + accept_header.split(/,/).each do |header| + params, q = header.split(/;\s*q=/) + if params.present? + params.strip! + + if params =~ TRAILING_STAR_REGEXP + parse_data_with_trailing_star($1).each do |m| + list << AcceptItem.new(index, m.to_s, q) + index += 1 + end + else + list << AcceptItem.new(index, params, q) + index += 1 + end end end list.sort! @@ -160,23 +176,51 @@ module Mime list end end + + # input: 'text' + # returned value: [Mime::JSON, Mime::XML, Mime::ICS, Mime::HTML, Mime::CSS, Mime::CSV, Mime::JS, Mime::YAML, Mime::TEXT] + # + # input: 'application' + # returned value: [Mime::HTML, Mime::JS, Mime::XML, Mime::YAML, Mime::ATOM, Mime::JSON, Mime::RSS, Mime::URL_ENCODED_FORM] + def parse_data_with_trailing_star(input) + Mime::SET.select { |m| m =~ input } + end + + # This method is opposite of register method. + # + # Usage: + # + # Mime::Type.unregister(:mobile) + def unregister(symbol) + symbol = symbol.to_s.upcase + mime = Mime.const_get(symbol) + Mime.instance_eval { remove_const(symbol) } + + SET.delete_if { |v| v.eql?(mime) } + LOOKUP.delete_if { |k,v| v.eql?(mime) } + EXTENSION_LOOKUP.delete_if { |k,v| v.eql?(mime) } + end end - + def initialize(string, symbol = nil, synonyms = []) @symbol, @synonyms = symbol, synonyms @string = string end - + def to_s @string end - + def to_str to_s end - + def to_sym - @symbol || @string.to_sym + @symbol + end + + def ref + to_sym || to_s end def ===(list) @@ -186,11 +230,11 @@ module Mime super end end - + def ==(mime_type) return false if mime_type.blank? - (@synonyms + [ self ]).any? do |synonym| - synonym.to_s == mime_type.to_s || synonym.to_sym == mime_type.to_sym + (@synonyms + [ self ]).any? do |synonym| + synonym.to_s == mime_type.to_s || synonym.to_sym == mime_type.to_sym end end diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index add8cab2ab..ef5d207b26 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -15,14 +15,14 @@ module ActionDispatch alias :params :parameters def path_parameters=(parameters) #:nodoc: - @env.delete("action_dispatch.request.symbolized_path_parameters") + @symbolized_path_params = nil @env.delete("action_dispatch.request.parameters") @env["action_dispatch.request.path_parameters"] = parameters end # The same as <tt>path_parameters</tt> with explicitly symbolized keys. def symbolized_path_parameters - @env["action_dispatch.request.symbolized_path_parameters"] ||= path_parameters.symbolize_keys + @symbolized_path_params ||= path_parameters.symbolize_keys end # Returns a hash with the \parameters used to form the \path of the request. diff --git a/actionpack/lib/action_dispatch/http/rack_cache.rb b/actionpack/lib/action_dispatch/http/rack_cache.rb new file mode 100644 index 0000000000..b5c1435903 --- /dev/null +++ b/actionpack/lib/action_dispatch/http/rack_cache.rb @@ -0,0 +1,58 @@ +require "rack/cache" +require "rack/cache/context" +require "active_support/cache" + +module ActionDispatch + class RailsMetaStore < Rack::Cache::MetaStore + def self.resolve(uri) + new + end + + # TODO: Finally deal with the RAILS_CACHE global + def initialize(store = RAILS_CACHE) + @store = store + end + + def read(key) + @store.read(key) || [] + end + + def write(key, value) + @store.write(key, value) + end + + ::Rack::Cache::MetaStore::RAILS = self + end + + class RailsEntityStore < Rack::Cache::EntityStore + def self.resolve(uri) + new + end + + def initialize(store = RAILS_CACHE) + @store = store + end + + def exist?(key) + @store.exist?(key) + end + + def open(key) + @store.read(key) + end + + def read(key) + body = open(key) + body.join if body + end + + def write(body) + buf = [] + key, size = slurp(body) { |part| buf << part } + @store.write(key, buf) + [key, size] + end + + ::Rack::Cache::EntityStore::RAILS = self + end +end diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index fd23b1df79..f07ac44f7a 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -2,8 +2,10 @@ require 'tempfile' require 'stringio' require 'strscan' +require 'active_support/core_ext/module/deprecation' require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/string/access' +require 'active_support/inflector' require 'action_dispatch/http/headers' module ActionDispatch @@ -15,6 +17,8 @@ module ActionDispatch include ActionDispatch::Http::Upload include ActionDispatch::Http::URL + LOCALHOST = [/^127\.0\.0\.\d{1,3}$/, "::1", /^0:0:0:0:0:0:0:1(%.*)?$/].freeze + %w[ AUTH_TYPE GATEWAY_INTERFACE PATH_TRANSLATED REMOTE_HOST REMOTE_IDENT REMOTE_USER REMOTE_ADDR @@ -42,8 +46,24 @@ module ActionDispatch @env.key?(key) end - HTTP_METHODS = %w(get head put post delete options) - HTTP_METHOD_LOOKUP = HTTP_METHODS.inject({}) { |h, m| h[m] = h[m.upcase] = m.to_sym; h } + # List of HTTP request methods from the following RFCs: + # Hypertext Transfer Protocol -- HTTP/1.1 (http://www.ietf.org/rfc/rfc2616.txt) + # HTTP Extensions for Distributed Authoring -- WEBDAV (http://www.ietf.org/rfc/rfc2518.txt) + # Versioning Extensions to WebDAV (http://www.ietf.org/rfc/rfc3253.txt) + # Ordered Collections Protocol (WebDAV) (http://www.ietf.org/rfc/rfc3648.txt) + # Web Distributed Authoring and Versioning (WebDAV) Access Control Protocol (http://www.ietf.org/rfc/rfc3744.txt) + # Web Distributed Authoring and Versioning (WebDAV) SEARCH (http://www.ietf.org/rfc/rfc5323.txt) + # PATCH Method for HTTP (http://www.ietf.org/rfc/rfc5789.txt) + RFC2616 = %w(OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT) + RFC2518 = %w(PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK) + RFC3253 = %w(VERSION-CONTROL REPORT CHECKOUT CHECKIN UNCHECKOUT MKWORKSPACE UPDATE LABEL MERGE BASELINE-CONTROL MKACTIVITY) + RFC3648 = %w(ORDERPATCH) + RFC3744 = %w(ACL) + RFC5323 = %w(SEARCH) + RFC5789 = %w(PATCH) + + HTTP_METHODS = RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC5789 + HTTP_METHOD_LOOKUP = Hash.new { |h, m| h[m] = m.underscore.to_sym if HTTP_METHODS.include?(m) } # Returns the HTTP \method that the application should see. # In the case where the \method was overridden by a middleware @@ -52,11 +72,7 @@ module ActionDispatch # the application should use), this \method returns the overridden # value, not the original. def request_method - @request_method ||= begin - method = env["REQUEST_METHOD"] - HTTP_METHOD_LOOKUP[method] || raise(ActionController::UnknownHttpMethod, "#{method}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}") - method - end + @request_method ||= check_method(env["REQUEST_METHOD"]) end # Returns a symbol form of the #request_method @@ -68,11 +84,7 @@ module ActionDispatch # even if it was overridden by middleware. See #request_method for # more information. def method - @method ||= begin - method = env["rack.methodoverride.original_method"] || env['REQUEST_METHOD'] - HTTP_METHOD_LOOKUP[method] || raise(ActionController::UnknownHttpMethod, "#{method}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}") - method - end + @method ||= check_method(env["rack.methodoverride.original_method"] || env['REQUEST_METHOD']) end # Returns a symbol form of the #method @@ -122,8 +134,9 @@ module ActionDispatch end def forgery_whitelisted? - get? || xhr? || content_mime_type.nil? || !content_mime_type.verify_request? + get? end + deprecate :forgery_whitelisted? => "it is just an alias for 'get?' now, update your code" def media_type content_mime_type.to_s @@ -134,11 +147,11 @@ module ActionDispatch super.to_i end - # Returns true if the request's "X-Requested-With" header contains - # "XMLHttpRequest". (The Prototype Javascript library sends this header with - # every Ajax request.) + # Returns true if the "X-Requested-With" header contains "XMLHttpRequest" + # (case-insensitive). All major JavaScript libraries send this header with + # every Ajax request. def xml_http_request? - !(@env['HTTP_X_REQUESTED_WITH'] !~ /XMLHttpRequest/i) + @env['HTTP_X_REQUESTED_WITH'] =~ /XMLHttpRequest/i end alias :xhr? :xml_http_request? @@ -147,8 +160,16 @@ module ActionDispatch end # Which IP addresses are "trusted proxies" that can be stripped from - # the right-hand-side of X-Forwarded-For - TRUSTED_PROXIES = /^127\.0\.0\.1$|^(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\./i + # the right-hand-side of X-Forwarded-For. + # + # http://en.wikipedia.org/wiki/Private_network#Private_IPv4_address_spaces. + TRUSTED_PROXIES = %r{ + ^127\.0\.0\.1$ | # localhost + ^(10 | # private IP 10.x.x.x + 172\.(1[6-9]|2[0-9]|3[0-1]) | # private IP in the range 172.16.0.0 .. 172.31.255.255 + 192\.168 # private IP 192.168.x.x + )\. + }x # Determines originating IP address. REMOTE_ADDR is the standard # but will fail if the user is behind a proxy. HTTP_CLIENT_IP and/or @@ -197,7 +218,7 @@ module ActionDispatch # TODO This should be broken apart into AD::Request::Session and probably # be included by the session middleware. def reset_session - session.destroy if session + session.destroy if session && session.respond_to?(:destroy) self.session = {} @env['action_dispatch.request.flash_hash'] = nil end @@ -212,13 +233,13 @@ module ActionDispatch # Override Rack's GET method to support indifferent access def GET - @env["action_dispatch.request.query_parameters"] ||= normalize_parameters(super) + @env["action_dispatch.request.query_parameters"] ||= (normalize_parameters(super) || {}) end alias :query_parameters :GET # Override Rack's POST method to support indifferent access def POST - @env["action_dispatch.request.request_parameters"] ||= normalize_parameters(super) + @env["action_dispatch.request.request_parameters"] ||= (normalize_parameters(super) || {}) end alias :request_parameters :POST @@ -231,5 +252,17 @@ module ActionDispatch @env['X_HTTP_AUTHORIZATION'] || @env['REDIRECT_X_HTTP_AUTHORIZATION'] end + + # True if the request came from localhost, 127.0.0.1. + def local? + LOCALHOST.any? { |local_ip| local_ip === remote_addr && local_ip === remote_ip } + end + + private + + def check_method(name) + HTTP_METHOD_LOOKUP[name] || raise(ActionController::UnknownHttpMethod, "#{name}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}") + name + end end end diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index 3b85a98576..8e03a7879f 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -4,27 +4,26 @@ require 'active_support/core_ext/object/blank' require 'active_support/core_ext/class/attribute_accessors' module ActionDispatch # :nodoc: - # Represents an HTTP response generated by a controller action. One can use - # an ActionDispatch::Response object to retrieve the current state - # of the response, or customize the response. An Response object can - # either represent a "real" HTTP response (i.e. one that is meant to be sent - # back to the web browser) or a test response (i.e. one that is generated - # from integration tests). See CgiResponse and TestResponse, respectively. + # Represents an HTTP response generated by a controller action. Use it to + # retrieve the current state of the response, or customize the response. It can + # either represent a real HTTP response (i.e. one that is meant to be sent + # back to the web browser) or a TestResponse (i.e. one that is generated + # from integration tests). # - # Response is mostly a Ruby on Rails framework implement detail, and + # \Response is mostly a Ruby on \Rails framework implementation detail, and # should never be used directly in controllers. Controllers should use the # methods defined in ActionController::Base instead. For example, if you want # to set the HTTP response's content MIME type, then use # ActionControllerBase#headers instead of Response#headers. # # Nevertheless, integration tests may want to inspect controller responses in - # more detail, and that's when Response can be useful for application + # more detail, and that's when \Response can be useful for application # developers. Integration test methods such as # ActionDispatch::Integration::Session#get and # ActionDispatch::Integration::Session#post return objects of type - # TestResponse (which are of course also of type Response). + # TestResponse (which are of course also of type \Response). # - # For example, the following demo integration "test" prints the body of the + # For example, the following demo integration test prints the body of the # controller response to the console: # # class DemoControllerTest < ActionDispatch::IntegrationTest @@ -45,8 +44,8 @@ module ActionDispatch # :nodoc: @block = nil @length = 0 - @status, @header = status, header - self.body = body + @header = header + self.body, self.status = body, status @cookie = [] @sending_file = false @@ -133,7 +132,7 @@ module ActionDispatch # :nodoc: # information. attr_accessor :charset, :content_type - CONTENT_TYPE = "Content-Type" + CONTENT_TYPE = "Content-Type" cattr_accessor(:default_charset) { "utf-8" } @@ -141,7 +140,6 @@ module ActionDispatch # :nodoc: assign_default_content_type_and_charset! handle_conditional_get! self["Set-Cookie"] = self["Set-Cookie"].join("\n") if self["Set-Cookie"].respond_to?(:join) - self["ETag"] = @_etag if @_etag super end diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb index 8ee4b81cdd..37effade4f 100644 --- a/actionpack/lib/action_dispatch/http/upload.rb +++ b/actionpack/lib/action_dispatch/http/upload.rb @@ -1,32 +1,34 @@ -require 'active_support/core_ext/object/blank' - module ActionDispatch module Http - module UploadedFile - def self.extended(object) - object.class_eval do - attr_accessor :original_path, :content_type - alias_method :local_path, :path if method_defined?(:path) - end + class UploadedFile + attr_accessor :original_filename, :content_type, :tempfile, :headers + + def initialize(hash) + @original_filename = hash[:filename] + @content_type = hash[:type] + @headers = hash[:head] + @tempfile = hash[:tempfile] + raise(ArgumentError, ':tempfile is required') unless @tempfile end - # Take the basename of the upload's original filename. - # This handles the full Windows paths given by Internet Explorer - # (and perhaps other broken user agents) without affecting - # those which give the lone filename. - # The Windows regexp is adapted from Perl's File::Basename. - def original_filename - unless defined? @original_filename - @original_filename = - unless original_path.blank? - if original_path =~ /^(?:.*[:\\\/])?(.*)/m - $1 - else - File.basename original_path - end - end - end - @original_filename + def open + @tempfile.open + end + + def path + @tempfile.path + end + + def read(*args) + @tempfile.read(*args) + end + + def rewind + @tempfile.rewind + end + + def size + @tempfile.size end end @@ -35,11 +37,7 @@ module ActionDispatch # file upload hash with UploadedFile objects def normalize_parameters(value) if Hash === value && value.has_key?(:tempfile) - upload = value[:tempfile] - upload.extend(UploadedFile) - upload.original_path = value[:filename] - upload.content_type = value[:type] - upload + UploadedFile.new(value) else super end @@ -47,4 +45,4 @@ module ActionDispatch private :normalize_parameters end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index b64a83c62e..ac0fd9607d 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -1,6 +1,81 @@ module ActionDispatch module Http module URL + mattr_accessor :tld_length + self.tld_length = 1 + + class << self + def extract_domain(host, tld_length = @@tld_length) + return nil unless named_host?(host) + host.split('.').last(1 + tld_length).join('.') + end + + def extract_subdomains(host, tld_length = @@tld_length) + return [] unless named_host?(host) + parts = host.split('.') + parts[0..-(tld_length+2)] + end + + def extract_subdomain(host, tld_length = @@tld_length) + extract_subdomains(host, tld_length).join('.') + end + + def url_for(options = {}) + unless options[:host].present? || options[:only_path].present? + raise ArgumentError, 'Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true' + end + + rewritten_url = "" + + unless options[:only_path] + unless options[:protocol] == false + rewritten_url << (options[:protocol] || "http") + rewritten_url << ":" unless rewritten_url.match(%r{:|//}) + end + rewritten_url << "//" unless rewritten_url.match("//") + rewritten_url << rewrite_authentication(options) + rewritten_url << host_or_subdomain_and_domain(options) + rewritten_url << ":#{options.delete(:port)}" if options[:port] + end + + path = options.delete(:path) || '' + + params = options[:params] || {} + 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? + rewritten_url << "##{Rack::Mount::Utils.escape_uri(options[:anchor].to_param.to_s)}" if options[:anchor] + rewritten_url + end + + private + + def named_host?(host) + !(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host)) + end + + def rewrite_authentication(options) + if options[:user] && options[:password] + "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@" + else + "" + end + end + + def host_or_subdomain_and_domain(options) + return options[:host] unless options[:subdomain] || options[:domain] + + tld_length = options[:tld_length] || @@tld_length + + host = "" + host << (options[:subdomain] || extract_subdomain(options[:host], tld_length)) + host << "." + host << (options[:domain] || extract_domain(options[:host], tld_length)) + host + end + end + # Returns the complete URL used for this request. def url protocol + host_with_port + fullpath @@ -8,12 +83,7 @@ module ActionDispatch # Returns 'https://' if this is an SSL request and 'http://' otherwise. def protocol - ssl? ? 'https://' : 'http://' - end - - # Is this an SSL request? - def ssl? - @env['HTTPS'] == 'on' || @env['HTTP_X_FORWARDED_PROTO'] == 'https' + @protocol ||= ssl? ? 'https://' : 'http://' end # Returns the \host for this request, such as "example.com". @@ -38,10 +108,12 @@ module ActionDispatch # Returns the port number of this request as an integer. def port - if raw_host_with_port =~ /:(\d+)$/ - $1.to_i - else - standard_port + @port ||= begin + if raw_host_with_port =~ /:(\d+)$/ + $1.to_i + else + standard_port + end end end @@ -53,10 +125,21 @@ module ActionDispatch end end - # Returns a \port suffix like ":8080" if the \port number of this request + # Returns whether this request is using the standard port + def standard_port? + port == standard_port + end + + # Returns a number \port suffix like 8080 if the \port number of this request # is not the default HTTP \port 80 or HTTPS \port 443. + def optional_port + standard_port? ? nil : port + end + + # Returns a string \port suffix, including colon, like ":8080" if the \port + # number of this request is not the default HTTP \port 80 or HTTPS \port 443. def port_string - port == standard_port ? '' : ":#{port}" + standard_port? ? '' : ":#{port}" end def server_port @@ -65,38 +148,25 @@ module ActionDispatch # Returns the \domain part of a \host, such as "rubyonrails.org" in "www.rubyonrails.org". You can specify # a different <tt>tld_length</tt>, such as 2 to catch rubyonrails.co.uk in "www.rubyonrails.co.uk". - def domain(tld_length = 1) - return nil unless named_host?(host) - - host.split('.').last(1 + tld_length).join('.') + def domain(tld_length = @@tld_length) + ActionDispatch::Http::URL.extract_domain(host, tld_length) end # Returns all the \subdomains as an array, so <tt>["dev", "www"]</tt> would be # returned for "dev.www.rubyonrails.org". You can specify a different <tt>tld_length</tt>, # such as 2 to catch <tt>["www"]</tt> instead of <tt>["www", "rubyonrails"]</tt> # in "www.rubyonrails.co.uk". - def subdomains(tld_length = 1) - return [] unless named_host?(host) - parts = host.split('.') - parts[0..-(tld_length+2)] - end - - def subdomain(tld_length = 1) - subdomains(tld_length).join('.') - end - - # Returns the request URI, accounting for server idiosyncrasies. - # WEBrick includes the full URL. IIS leaves REQUEST_URI blank. - def request_uri - ActiveSupport::Deprecation.warn "Using #request_uri is deprecated. Use fullpath instead.", caller - fullpath + def subdomains(tld_length = @@tld_length) + ActionDispatch::Http::URL.extract_subdomains(host, tld_length) end - private - - def named_host?(host) - !(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host)) + # Returns all the \subdomains as a string, so <tt>"dev.www"</tt> would be + # returned for "dev.www.rubyonrails.org". You can specify a different <tt>tld_length</tt>, + # such as 2 to catch <tt>["www"]</tt> instead of <tt>"www.rubyonrails"</tt> + # in "www.rubyonrails.co.uk". + def subdomain(tld_length = @@tld_length) + subdomains(tld_length) end end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_dispatch/middleware/callbacks.rb b/actionpack/lib/action_dispatch/middleware/callbacks.rb index e4ae480bfb..1bb2ad7f67 100644 --- a/actionpack/lib/action_dispatch/middleware/callbacks.rb +++ b/actionpack/lib/action_dispatch/middleware/callbacks.rb @@ -1,30 +1,14 @@ +require 'active_support/core_ext/module/delegation' + module ActionDispatch # Provide callbacks to be executed before and after the request dispatch. - # - # It also provides a to_prepare callback, which is performed in all requests - # in development by only once in production and notification callback for async - # operations. - # class Callbacks include ActiveSupport::Callbacks define_callbacks :call, :rescuable => true - define_callbacks :prepare, :scope => :name - # Add a preparation callback. Preparation callbacks are run before every - # request in development mode, and before the first request in production mode. - # - # If a symbol with a block is given, the symbol is used as an identifier. - # That allows to_prepare to be called again with the same identifier to - # replace the existing callback. Passing an identifier is a suggested - # practice if the code adding a preparation block may be reloaded. - def self.to_prepare(*args, &block) - if args.first.is_a?(Symbol) && block_given? - define_method :"__#{args.first}", &block - set_callback(:prepare, :"__#{args.first}") - else - set_callback(:prepare, *args, &block) - end + class << self + delegate :to_prepare, :to_cleanup, :to => "ActionDispatch::Reloader" end def self.before(*args, &block) @@ -35,14 +19,13 @@ module ActionDispatch set_callback(:call, :after, *args, &block) end - def initialize(app, prepare_each_request = false) - @app, @prepare_each_request = app, prepare_each_request - _run_prepare_callbacks + def initialize(app, unused = nil) + ActiveSupport::Deprecation.warn "Passing a second argument to ActionDispatch::Callbacks.new is deprecated." unless unused.nil? + @app = app end def call(env) - _run_call_callbacks do - _run_prepare_callbacks if @prepare_each_request + run_callbacks :call do @app.call(env) end end diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 4d33cd3b0c..7ac608f0a8 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -7,7 +7,7 @@ module ActionDispatch end end - # Cookies are read and written through ActionController#cookies. + # \Cookies are read and written through ActionController#cookies. # # The cookies being read are the ones received along with the request, the cookies # being written will be sent out with the response. Reading a cookie does not get @@ -16,15 +16,31 @@ module ActionDispatch # Examples for writing: # # # Sets a simple session cookie. + # # This cookie will be deleted when the user's browser is closed. # cookies[:user_name] = "david" # + # # Assign an array of values to a cookie. + # cookies[:lat_lon] = [47.68, -122.37] + # # # Sets a cookie that expires in 1 hour. # cookies[:login] = { :value => "XJ-122", :expires => 1.hour.from_now } # + # # Sets a signed cookie, which prevents a user from tampering with its value. + # # The cookie is signed by your app's <tt>config.secret_token</tt> value. + # # Rails generates this value by default when you create a new Rails app. + # cookies.signed[:user_id] = current_user.id + # + # # Sets a "permanent" cookie (which expires in 20 years from now). + # cookies.permanent[:login] = "XJ-122" + # + # # You can also chain these methods: + # cookies.permanent.signed[:login] = "XJ-122" + # # Examples for reading: # # cookies[:user_name] # => "david" # cookies.size # => 2 + # cookies[:lat_lon] # => [47.68, -122.37] # # Example for deleting: # @@ -55,7 +71,7 @@ module ActionDispatch # :domain => :all # Allow the cookie for the top most level # domain and subdomains. # - # * <tt>:expires</tt> - The time at which this cookie expires, as a Time object. + # * <tt>:expires</tt> - The time at which this cookie expires, as a \Time object. # * <tt>:secure</tt> - Whether this cookie is a only transmitted to HTTPS servers. # Default is +false+. # * <tt>:httponly</tt> - Whether this cookie is accessible via scripting or @@ -69,27 +85,36 @@ module ActionDispatch class CookieJar < Hash #:nodoc: - # This regular expression is used to split the levels of a domain - # So www.example.co.uk gives: - # $1 => www. - # $2 => example - # $3 => co.uk - DOMAIN_REGEXP = /^(.*\.)*(.*)\.(...|...\...|....|..\...|..)$/ + # This regular expression is used to split the levels of a domain. + # The top level domain can be any string without a period or + # **.**, ***.** style TLDs like co.uk or com.au + # + # www.example.co.uk gives: + # $& => example.co.uk + # + # example.com gives: + # $& => example.com + # + # lots.of.subdomains.example.local gives: + # $& => example.local + DOMAIN_REGEXP = /[^.]*\.([^.]*|..\...|...\...)$/ def self.build(request) secret = request.env[TOKEN_KEY] - host = request.env["HTTP_HOST"] + host = request.host + secure = request.ssl? - new(secret, host).tap do |hash| + new(secret, host, secure).tap do |hash| hash.update(request.cookies) end end - def initialize(secret = nil, host = nil) + def initialize(secret = nil, host = nil, secure = false) @secret = secret @set_cookies = {} @delete_cookies = {} @host = host + @secure = secure super() end @@ -103,8 +128,17 @@ module ActionDispatch options[:path] ||= "/" if options[:domain] == :all - @host =~ DOMAIN_REGEXP - options[:domain] = ".#{$2}.#{$3}" + # if there is a provided tld length then we use it otherwise default domain regexp + domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP + + # if host is not ip and matches domain regexp + # (ip confirms to domain regexp so we explicitly check for ip) + options[:domain] = if (@host !~ /^[\d.]+$/) && (@host =~ domain_regexp) + ".#{$&}" + end + elsif options[:domain].is_a? Array + # if host matches one of the supplied domains without a dot in front of it + options[:domain] = options[:domain].find {|domain| @host.include? domain[/^\.?(.*)$/, 1] } end end @@ -174,9 +208,15 @@ module ActionDispatch end def write(headers) - @set_cookies.each { |k, v| ::Rack::Utils.set_cookie_header!(headers, k, v) } + @set_cookies.each { |k, v| ::Rack::Utils.set_cookie_header!(headers, k, v) if write_cookie?(v) } @delete_cookies.each { |k, v| ::Rack::Utils.delete_cookie_header!(headers, k, v) } end + + private + + def write_cookie?(cookie) + @secure || !cookie[:secure] || defined?(Rails.env) && Rails.env.development? + end end class PermanentCookieJar < CookieJar #:nodoc: @@ -248,7 +288,7 @@ module ActionDispatch "integrity hash for cookie session data. Use " + "config.secret_token = \"some secret phrase of at " + "least #{SECRET_MIN_LENGTH} characters\"" + - "in config/application.rb" + "in config/initializers/secret_token.rb" end if secret.length < SECRET_MIN_LENGTH diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index bfa30cf5af..21aeeb217a 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -10,13 +10,13 @@ module ActionDispatch # The flash provides a way to pass temporary objects between actions. Anything you place in the flash will be exposed # to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create - # action that sets <tt>flash[:notice] = "Successfully created"</tt> before redirecting to a display action that can + # action that sets <tt>flash[:notice] = "Post successfully created"</tt> before redirecting to a display action that can # then expose the flash to its template. Actually, that exposure is automatically done. Example: # # class PostsController < ActionController::Base # def create # # save post - # flash[:notice] = "Successfully created post" + # flash[:notice] = "Post successfully created" # redirect_to posts_path(@post) # end # @@ -30,6 +30,11 @@ module ActionDispatch # <div class="notice"><%= flash[:notice] %></div> # <% end %> # + # Since the +notice+ and +alert+ keys are a common idiom, convenience accessors are available: + # + # flash.alert = "You must be logged in" + # flash.notice = "Post successfully created" + # # This example just places a string in the flash, but you can put any object in there. And of course, you can put as # many as you like at a time too. Just remember: They'll be gone by the time the next action has been performed. # diff --git a/actionpack/lib/action_dispatch/middleware/reloader.rb b/actionpack/lib/action_dispatch/middleware/reloader.rb new file mode 100644 index 0000000000..29289a76b4 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/reloader.rb @@ -0,0 +1,76 @@ +module ActionDispatch + # ActionDispatch::Reloader provides prepare and cleanup callbacks, + # intended to assist with code reloading during development. + # + # Prepare callbacks are run before each request, and cleanup callbacks + # after each request. In this respect they are analogs of ActionDispatch::Callback's + # before and after callbacks. However, cleanup callbacks are not called until the + # request is fully complete -- that is, after #close has been called on + # the response body. This is important for streaming responses such as the + # following: + # + # self.response_body = lambda { |response, output| + # # code here which refers to application models + # } + # + # Cleanup callbacks will not be called until after the response_body lambda + # is evaluated, ensuring that it can refer to application models and other + # classes before they are unloaded. + # + # By default, ActionDispatch::Reloader is included in the middleware stack + # only in the development environment; specifically, when config.cache_classes + # is false. Callbacks may be registered even when it is not included in the + # middleware stack, but are executed only when +ActionDispatch::Reloader.prepare!+ + # or +ActionDispatch::Reloader.cleanup!+ are called manually. + # + class Reloader + include ActiveSupport::Callbacks + + define_callbacks :prepare, :scope => :name + define_callbacks :cleanup, :scope => :name + + # Add a prepare callback. Prepare callbacks are run before each request, prior + # to ActionDispatch::Callback's before callbacks. + def self.to_prepare(*args, &block) + set_callback(:prepare, *args, &block) + end + + # Add a cleanup callback. Cleanup callbacks are run after each request is + # complete (after #close is called on the response body). + def self.to_cleanup(*args, &block) + set_callback(:cleanup, *args, &block) + end + + # Execute all prepare callbacks. + def self.prepare! + new(nil).run_callbacks :prepare + end + + # Execute all cleanup callbacks. + def self.cleanup! + new(nil).run_callbacks :cleanup + end + + def initialize(app) + @app = app + end + + module CleanupOnClose + def close + super if defined?(super) + ensure + ActionDispatch::Reloader.cleanup! + end + end + + def call(env) + run_callbacks :prepare + response = @app.call(env) + response[2].extend(CleanupOnClose) + response + rescue Exception + run_callbacks :cleanup + raise + end + end +end diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb index dd82294644..64d3a87fd0 100644 --- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb @@ -1,5 +1,6 @@ require 'rack/utils' require 'rack/request' +require 'rack/session/abstract/id' require 'action_dispatch/middleware/cookies' require 'active_support/core_ext/object/blank' @@ -8,249 +9,76 @@ module ActionDispatch class SessionRestoreError < StandardError #:nodoc: end - class AbstractStore - ENV_SESSION_KEY = 'rack.session'.freeze - ENV_SESSION_OPTIONS_KEY = 'rack.session.options'.freeze - - # thin wrapper around Hash that allows us to lazily - # load session id into session_options - class OptionsHash < Hash - def initialize(by, env, default_options) - @by = by - @env = env - @session_id_loaded = false - merge!(default_options) - end - - def [](key) - if key == :id - load_session_id! unless key?(:id) || has_session_id? - end - super - end - - private - - def has_session_id? - @session_id_loaded - end - - def load_session_id! - self[:id] = @by.send(:extract_session_id, @env) - @session_id_loaded = true - end - end - - class SessionHash < Hash - def initialize(by, env) - super() - @by = by - @env = env - @loaded = false - end - - def [](key) - load_for_read! - super(key.to_s) - end - - def has_key?(key) - load_for_read! - super(key.to_s) - end - - def []=(key, value) - load_for_write! - super(key.to_s, value) - end - - def clear - load_for_write! - super - end - - def to_hash - load_for_read! - h = {}.replace(self) - h.delete_if { |k,v| v.nil? } - h - end - - def update(hash) - load_for_write! - super(hash.stringify_keys) - end - - def delete(key) - load_for_write! - super(key.to_s) - end - - def inspect - load_for_read! - super - end - - def exists? - return @exists if instance_variable_defined?(:@exists) - @exists = @by.send(:exists?, @env) - end - - def loaded? - @loaded - end - - def destroy - clear - @by.send(:destroy, @env) if @by - @env[ENV_SESSION_OPTIONS_KEY][:id] = nil if @env && @env[ENV_SESSION_OPTIONS_KEY] - @loaded = false - end - - private - - def load_for_read! - load! if !loaded? && exists? - end - - def load_for_write! - load! unless loaded? - end - - def load! - id, session = @by.send(:load_session, @env) - @env[ENV_SESSION_OPTIONS_KEY][:id] = id - replace(session.stringify_keys) - @loaded = true - end - + module DestroyableSession + def destroy + clear + options = @env[Rack::Session::Abstract::ENV_SESSION_OPTIONS_KEY] if @env + options ||= {} + @by.send(:destroy_session, @env, options[:id], options) if @by + options[:id] = nil + @loaded = false end + end - DEFAULT_OPTIONS = { - :key => '_session_id', - :path => '/', - :domain => nil, - :expire_after => nil, - :secure => false, - :httponly => true, - :cookie_only => true - } + ::Rack::Session::Abstract::SessionHash.send :include, DestroyableSession + module Compatibility def initialize(app, options = {}) - @app = app - @default_options = DEFAULT_OPTIONS.merge(options) - @key = @default_options.delete(:key).freeze - @cookie_only = @default_options.delete(:cookie_only) - ensure_session_key! + options[:key] ||= '_session_id' + super end - def call(env) - prepare!(env) - response = @app.call(env) - - session_data = env[ENV_SESSION_KEY] - options = env[ENV_SESSION_OPTIONS_KEY] - - if !session_data.is_a?(AbstractStore::SessionHash) || session_data.loaded? || options[:expire_after] - session_data.send(:load!) if session_data.is_a?(AbstractStore::SessionHash) && !session_data.loaded? - - sid = options[:id] || generate_sid - session_data = session_data.to_hash - - value = set_session(env, sid, session_data) - return response unless value - - cookie = { :value => value } - unless options[:expire_after].nil? - cookie[:expires] = Time.now + options.delete(:expire_after) - end - - request = ActionDispatch::Request.new(env) - set_cookie(request, cookie.merge!(options)) - end - - response + def generate_sid + ActiveSupport::SecureRandom.hex(16) end - private - - def prepare!(env) - env[ENV_SESSION_KEY] = SessionHash.new(self, env) - env[ENV_SESSION_OPTIONS_KEY] = OptionsHash.new(self, env, @default_options) - end - - def generate_sid - ActiveSupport::SecureRandom.hex(16) - end - - def set_cookie(request, options) - if request.cookie_jar[@key] != options[:value] || !options[:expires].nil? - request.cookie_jar[@key] = options - end - end - - def load_session(env) - stale_session_check! do - sid = current_session_id(env) - sid, session = get_session(env, sid) - [sid, session] - end - end + protected - def extract_session_id(env) - stale_session_check! do - request = ActionDispatch::Request.new(env) - sid = request.cookies[@key] - sid ||= request.params[@key] unless @cookie_only - sid - end - end + def initialize_sid + @default_options.delete(:sidbits) + @default_options.delete(:secure_random) + end + end - def current_session_id(env) - env[ENV_SESSION_OPTIONS_KEY][:id] - end + module StaleSessionCheck + def load_session(env) + stale_session_check! { super } + end - def ensure_session_key! - if @key.blank? - raise ArgumentError, 'A key is required to write a ' + - 'cookie containing the session data. Use ' + - 'config.session_store SESSION_STORE, { :key => ' + - '"_myapp_session" } in config/application.rb' - end - end + def extract_session_id(env) + stale_session_check! { super } + end - def stale_session_check! - yield - rescue ArgumentError => argument_error - if argument_error.message =~ %r{undefined class/module ([\w:]*\w)} - begin - # Note that the regexp does not allow $1 to end with a ':' - $1.constantize - rescue LoadError, NameError => const_error - raise ActionDispatch::Session::SessionRestoreError, "Session contains objects whose class definition isn't available.\nRemember to require the classes for all objects kept in the session.\n(Original exception: #{const_error.message} [#{const_error.class}])\n" - end - retry - else - raise + def stale_session_check! + yield + rescue ArgumentError => argument_error + if argument_error.message =~ %r{undefined class/module ([\w:]*\w)} + begin + # Note that the regexp does not allow $1 to end with a ':' + $1.constantize + rescue LoadError, NameError => const_error + raise ActionDispatch::Session::SessionRestoreError, "Session contains objects whose class definition isn't available.\nRemember to require the classes for all objects kept in the session.\n(Original exception: #{const_error.message} [#{const_error.class}])\n" end + retry + else + raise end + end + end - def exists?(env) - current_session_id(env).present? - end - - def get_session(env, sid) - raise '#get_session needs to be implemented.' - end + class AbstractStore < Rack::Session::Abstract::ID + include Compatibility + include StaleSessionCheck - def set_session(env, sid, session_data) - raise '#set_session needs to be implemented and should return ' << - 'the value to be stored in the cookie (usually the sid)' - end + def destroy_session(env, sid, options) + ActiveSupport::Deprecation.warn "Implementing #destroy in session stores is deprecated. " << + "Please implement destroy_session(env, session_id, options) instead." + destroy(env) + end - def destroy(env) - raise '#destroy needs to be implemented.' - end + def destroy(env) + raise '#destroy needs to be implemented.' + end end end end diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index ca1494425f..9c9ccc62f5 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -1,5 +1,7 @@ require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/object/blank' +require 'action_dispatch/middleware/session/abstract_store' +require 'rack/session/cookie' module ActionDispatch module Session @@ -38,58 +40,32 @@ module ActionDispatch # "rake secret" and set the key in config/initializers/secret_token.rb. # # Note that changing digest or secret invalidates all existing sessions! - class CookieStore < AbstractStore - - def initialize(app, options = {}) - super(app, options.merge!(:cookie_only => true)) - freeze - end + class CookieStore < Rack::Session::Cookie + include Compatibility + include StaleSessionCheck private - def load_session(env) - data = unpacked_cookie_data(env) - data = persistent_session_id!(data) - [data["session_id"], data] - end - - def extract_session_id(env) - if data = unpacked_cookie_data(env) - data["session_id"] - else - nil - end - end - - def unpacked_cookie_data(env) - env["action_dispatch.request.unsigned_session_cookie"] ||= begin - stale_session_check! do - request = ActionDispatch::Request.new(env) - if data = request.cookie_jar.signed[@key] - data.stringify_keys! - end - data || {} + def unpacked_cookie_data(env) + env["action_dispatch.request.unsigned_session_cookie"] ||= begin + stale_session_check! do + request = ActionDispatch::Request.new(env) + if data = request.cookie_jar.signed[@key] + data.stringify_keys! end + data || {} end end + end - def set_cookie(request, options) - request.cookie_jar.signed[@key] = options - end - - def set_session(env, sid, session_data) - persistent_session_id!(session_data, sid) - end - - def destroy(env) - # session data is stored on client; nothing to do here - end + def set_session(env, sid, session_data, options) + persistent_session_id!(session_data, sid) + end - def persistent_session_id!(data, sid=nil) - data ||= {} - data["session_id"] ||= sid || generate_sid - data - end + def set_cookie(env, session_id, cookie) + request = ActionDispatch::Request.new(env) + request.cookie_jar.signed[@key] = cookie + end end end end diff --git a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb index 28e3dbd732..4dd9a946c2 100644 --- a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb @@ -1,56 +1,17 @@ +require 'action_dispatch/middleware/session/abstract_store' +require 'rack/session/memcache' + module ActionDispatch module Session - class MemCacheStore < AbstractStore + class MemCacheStore < Rack::Session::Memcache + include Compatibility + include StaleSessionCheck + def initialize(app, options = {}) require 'memcache' - - # Support old :expires option options[:expire_after] ||= options[:expires] - - super - - @default_options = { - :namespace => 'rack:session', - :memcache_server => 'localhost:11211' - }.merge(@default_options) - - @pool = options[:cache] || MemCache.new(@default_options[:memcache_server], @default_options) - unless @pool.servers.any? { |s| s.alive? } - raise "#{self} unable to find server during initialization." - end - @mutex = Mutex.new - super end - - private - def get_session(env, sid) - sid ||= generate_sid - begin - session = @pool.get(sid) || {} - rescue MemCache::MemCacheError, Errno::ECONNREFUSED - session = {} - end - [sid, session] - end - - def set_session(env, sid, session_data) - options = env['rack.session.options'] - expiry = options[:expire_after] || 0 - @pool.set(sid, session_data, expiry) - sid - rescue MemCache::MemCacheError, Errno::ECONNREFUSED - false - end - - def destroy(env) - if sid = current_session_id(env) - @pool.delete(sid) - end - rescue MemCache::MemCacheError, Errno::ECONNREFUSED - false - end - end end end diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb index e095b51342..dbe3206808 100644 --- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb @@ -6,8 +6,6 @@ module ActionDispatch # This middleware rescues any exception returned by the application and renders # nice exception pages if it's being rescued locally. class ShowExceptions - LOCALHOST = [/^127\.0\.0\.\d{1,3}$/, "::1", /^0:0:0:0:0:0:0:1(%.*)?$/].freeze - RESCUES_TEMPLATE_PATH = File.join(File.dirname(__FILE__), 'templates') cattr_accessor :rescue_responses @@ -45,28 +43,29 @@ module ActionDispatch end def call(env) - status, headers, body = @app.call(env) - - # Only this middleware cares about RoutingError. So, let's just raise - # it here. - # TODO: refactor this middleware to handle the X-Cascade scenario without - # having to raise an exception. - if headers['X-Cascade'] == 'pass' - raise ActionController::RoutingError, "No route matches #{env['PATH_INFO'].inspect}" + begin + status, headers, body = @app.call(env) + exception = nil + + # Only this middleware cares about RoutingError. So, let's just raise + # it here. + if headers['X-Cascade'] == 'pass' + raise ActionController::RoutingError, "No route matches #{env['PATH_INFO'].inspect}" + end + rescue Exception => exception + raise exception if env['action_dispatch.show_exceptions'] == false end - [status, headers, body] - rescue Exception => exception - raise exception if env['action_dispatch.show_exceptions'] == false - render_exception(env, exception) + exception ? render_exception(env, exception) : [status, headers, body] end private def render_exception(env, exception) log_error(exception) + exception = original_exception(exception) request = Request.new(env) - if @consider_all_requests_local || local_request?(request) + if @consider_all_requests_local || request.local? rescue_action_locally(request, exception) else rescue_action_in_public(exception) @@ -112,11 +111,6 @@ module ActionDispatch end end - # True if the request came from localhost, 127.0.0.1. - def local_request?(request) - LOCALHOST.any? { |local_ip| local_ip === request.remote_addr && local_ip === request.remote_ip } - end - def status_code(exception) Rack::Utils.status_code(@@rescue_responses[exception.class.name]) end @@ -134,7 +128,7 @@ module ActionDispatch ActiveSupport::Deprecation.silence do message = "\n#{exception.class} (#{exception.message}):\n" - message << exception.annoted_source_code if exception.respond_to?(:annoted_source_code) + message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code) message << " " << application_trace(exception).join("\n ") logger.fatal("#{message}\n\n") end @@ -161,5 +155,17 @@ module ActionDispatch def logger defined?(Rails.logger) ? Rails.logger : Logger.new($stderr) end + + def original_exception(exception) + if registered_original_exception?(exception) + exception.original_exception + else + exception + end + end + + def registered_original_exception?(exception) + exception.respond_to?(:original_exception) && @@rescue_responses.has_key?(exception.original_exception.class.name) + end end end diff --git a/actionpack/lib/action_dispatch/middleware/stack.rb b/actionpack/lib/action_dispatch/middleware/stack.rb index 41078eced7..a4308f528c 100644 --- a/actionpack/lib/action_dispatch/middleware/stack.rb +++ b/actionpack/lib/action_dispatch/middleware/stack.rb @@ -1,17 +1,27 @@ require "active_support/inflector/methods" +require "active_support/dependencies" module ActionDispatch - class MiddlewareStack < Array + class MiddlewareStack class Middleware - attr_reader :args, :block + attr_reader :args, :block, :name, :classcache def initialize(klass_or_name, *args, &block) - @ref = ActiveSupport::Dependencies::Reference.new(klass_or_name) + @klass = nil + + if klass_or_name.respond_to?(:name) + @klass = klass_or_name + @name = @klass.name + else + @name = klass_or_name.to_s + end + + @classcache = ActiveSupport::Dependencies::Reference @args, @block = args, block end def klass - @ref.get + @klass || classcache[@name] end def ==(middleware) @@ -21,7 +31,7 @@ module ActionDispatch when Class klass == middleware else - normalize(@ref.name) == normalize(middleware) + normalize(@name) == normalize(middleware) end end @@ -40,15 +50,39 @@ module ActionDispatch end end - def initialize(*args, &block) - super(*args) - block.call(self) if block_given? + include Enumerable + + attr_accessor :middlewares + + def initialize(*args) + @middlewares = [] + yield(self) if block_given? + end + + def each + @middlewares.each { |x| yield x } + end + + def size + middlewares.size + end + + def last + middlewares.last + end + + def [](i) + middlewares[i] + end + + def initialize_copy(other) + self.middlewares = other.middlewares.dup end def insert(index, *args, &block) index = assert_index(index, :before) middleware = self.class::Middleware.new(*args, &block) - super(index, middleware) + middlewares.insert(index, middleware) end alias_method :insert_before, :insert @@ -63,26 +97,25 @@ module ActionDispatch delete(target) end - def use(*args, &block) - middleware = self.class::Middleware.new(*args, &block) - push(middleware) + def delete(target) + middlewares.delete target end - def active - ActiveSupport::Deprecation.warn "All middlewares in the chain are active since the laziness " << - "was removed from the middleware stack", caller + def use(*args, &block) + middleware = self.class::Middleware.new(*args, &block) + middlewares.push(middleware) end def build(app = nil, &block) app ||= block raise "MiddlewareStack#build requires an app" unless app - reverse.inject(app) { |a, e| e.build(a) } + middlewares.reverse.inject(app) { |a, e| e.build(a) } end protected def assert_index(index, where) - i = index.is_a?(Integer) ? index : self.index(index) + i = index.is_a?(Integer) ? index : middlewares.index(index) raise "No such middleware to insert #{where}: #{index.inspect}" unless i i end diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index d7e88a54e4..c57f694c4d 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -1,12 +1,47 @@ require 'rack/utils' module ActionDispatch + class FileHandler + def initialize(at, root) + @at, @root = at.chomp('/'), root.chomp('/') + @compiled_at = @at.blank? ? nil : /^#{Regexp.escape(at)}/ + @compiled_root = /^#{Regexp.escape(root)}/ + @file_server = ::Rack::File.new(@root) + end + + def match?(path) + path = path.dup + if !@compiled_at || path.sub!(@compiled_at, '') + full_path = path.empty? ? @root : File.join(@root, ::Rack::Utils.unescape(path)) + paths = "#{full_path}#{ext}" + + matches = Dir[paths] + match = matches.detect { |m| File.file?(m) } + if match + match.sub!(@compiled_root, '') + match + end + end + end + + def call(env) + @file_server.call(env) + end + + def ext + @ext ||= begin + ext = ::ActionController::Base.page_cache_extension + "{,#{ext},/index#{ext}}" + end + end + end + class Static FILE_METHODS = %w(GET HEAD).freeze - def initialize(app, root) + def initialize(app, roots) @app = app - @file_server = ::Rack::File.new(root) + @file_handlers = create_file_handlers(roots) end def call(env) @@ -14,15 +49,10 @@ module ActionDispatch method = env['REQUEST_METHOD'] if FILE_METHODS.include?(method) - if file_exist?(path) - return @file_server.call(env) - else - cached_path = directory_exist?(path) ? "#{path}/index" : path - cached_path += ::ActionController::Base.page_cache_extension - - if file_exist?(cached_path) - env['PATH_INFO'] = cached_path - return @file_server.call(env) + @file_handlers.each do |file_handler| + if match = file_handler.match?(path) + env["PATH_INFO"] = match + return file_handler.call(env) end end end @@ -31,14 +61,12 @@ module ActionDispatch end private - def file_exist?(path) - full_path = File.join(@file_server.root, ::Rack::Utils.unescape(path)) - File.file?(full_path) && File.readable?(full_path) - end + def create_file_handlers(roots) + roots = { '' => roots } unless roots.is_a?(Hash) - def directory_exist?(path) - full_path = File.join(@file_server.root, ::Rack::Utils.unescape(path)) - File.directory?(full_path) && File.readable?(full_path) + roots.map do |at, root| + FileHandler.new(at, root) if File.exist?(root) + end.compact end end end diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb index e963b04524..97f7cf0bbe 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb @@ -14,7 +14,7 @@ def debug_hash(hash) hash.sort_by { |k, v| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n") - end + end unless self.class.method_defined?(:debug_hash) %> <h2 style="margin-top: 30px">Request</h2> @@ -28,4 +28,4 @@ <h2 style="margin-top: 30px">Response</h2> -<p><b>Headers</b>: <pre><%=h @response ? @response.headers.inspect.gsub(',', ",\n") : 'None' %></pre></p> +<p><b>Headers</b>: <pre><%=h defined?(@response) ? @response.headers.inspect.gsub(',', ",\n") : 'None' %></pre></p> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb index d18b162a93..8771b5fd6d 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb @@ -12,14 +12,14 @@ <div id="traces"> <% names.each do |name| %> <% - show = "document.getElementById('#{name.gsub /\s/, '-'}').style.display='block';" - hide = (names - [name]).collect {|hide_name| "document.getElementById('#{hide_name.gsub /\s/, '-'}').style.display='none';"} + show = "document.getElementById('#{name.gsub(/\s/, '-')}').style.display='block';" + hide = (names - [name]).collect {|hide_name| "document.getElementById('#{hide_name.gsub(/\s/, '-')}').style.display='none';"} %> <a href="#" onclick="<%= hide.join %><%= show %>; return false;"><%= name %></a> <%= '|' unless names.last == name %> <% end %> <% traces.each do |name, trace| %> - <div id="<%= name.gsub /\s/, '-' %>" style="display: <%= name == "Application Trace" ? 'block' : 'none' %>;"> + <div id="<%= name.gsub(/\s/, '-') %>" style="display: <%= (name == "Application Trace") ? 'block' : 'none' %>;"> <pre><code><%=h trace.join "\n" %></code></pre> </div> <% end %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb index bd6ffbab5d..50d8ca9484 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb @@ -6,5 +6,5 @@ </h1> <pre><%=h @exception.message %></pre> -<%= render :file => "rescues/_trace.erb" %> -<%= render :file => "rescues/_request_and_response.erb" %> +<%= render :template => "rescues/_trace" %> +<%= render :template => "rescues/_request_and_response" %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.erb index 02fa18211d..c658559be9 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.erb @@ -13,9 +13,5 @@ <p><%=h @exception.sub_template_message %></p> -<% @real_exception = @exception - @exception = @exception.original_exception || @exception %> -<%= render :file => "rescues/_trace.erb" %> -<% @exception = @real_exception %> - -<%= render :file => "rescues/_request_and_response.erb" %> +<%= render :template => "rescues/_trace" %> +<%= render :template => "rescues/_request_and_response" %> diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb index a3af37947a..0a3bd5fe40 100644 --- a/actionpack/lib/action_dispatch/railtie.rb +++ b/actionpack/lib/action_dispatch/railtie.rb @@ -8,10 +8,11 @@ module ActionDispatch config.action_dispatch.ip_spoofing_check = true config.action_dispatch.show_exceptions = true config.action_dispatch.best_standards_support = true + config.action_dispatch.tld_length = 1 + config.action_dispatch.rack_cache = {:metastore => "rails:/", :entitystore => "rails:/", :verbose => true} - # Prepare dispatcher callbacks and run 'prepare' callbacks - initializer "action_dispatch.prepare_dispatcher" do |app| - ActionDispatch::Callbacks.to_prepare { app.routes_reloader.execute_if_updated } + initializer "action_dispatch.configure" do |app| + ActionDispatch::Http::URL.tld_length = app.config.action_dispatch.tld_length end end end diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb index 683dd72555..43fd93adf6 100644 --- a/actionpack/lib/action_dispatch/routing.rb +++ b/actionpack/lib/action_dispatch/routing.rb @@ -2,31 +2,11 @@ require 'active_support/core_ext/object/to_param' require 'active_support/core_ext/regexp' module ActionDispatch - # = Routing - # # The routing module provides URL rewriting in native Ruby. It's a way to # redirect incoming requests to controllers and actions. This replaces - # mod_rewrite rules. Best of all, Rails' Routing works with any web server. + # mod_rewrite rules. Best of all, Rails' \Routing works with any web server. # Routes are defined in <tt>config/routes.rb</tt>. # - # Consider the following route, which you will find commented out at the - # bottom of your generated <tt>config/routes.rb</tt>: - # - # match ':controller(/:action(/:id(.:format)))' - # - # This route states that it expects requests to consist of a - # <tt>:controller</tt> followed optionally by an <tt>:action</tt> that in - # turn is followed optionally by an <tt>:id</tt>, which in turn is followed - # optionally by a <tt>:format</tt> - # - # Suppose you get an incoming request for <tt>/blog/edit/22</tt>, you'll end - # up with: - # - # params = { :controller => 'blog', - # :action => 'edit', - # :id => '22' - # } - # # Think of creating routes as drawing a map for your requests. The map tells # them where to go based on some predefined pattern: # @@ -43,6 +23,51 @@ module ActionDispatch # # Other names simply map to a parameter as in the case of <tt>:id</tt>. # + # == Resources + # + # Resource routing allows you to quickly declare all of the common routes + # for a given resourceful controller. Instead of declaring separate routes + # for your +index+, +show+, +new+, +edit+, +create+, +update+ and +destroy+ + # actions, a resourceful route declares them in a single line of code: + # + # resources :photos + # + # Sometimes, you have a resource that clients always look up without + # referencing an ID. A common example, /profile always shows the profile of + # the currently logged in user. In this case, you can use a singular resource + # to map /profile (rather than /profile/:id) to the show action. + # + # resource :profile + # + # It's common to have resources that are logically children of other + # resources: + # + # resources :magazines do + # resources :ads + # end + # + # You may wish to organize groups of controllers under a namespace. Most + # commonly, you might group a number of administrative controllers under + # an +admin+ namespace. You would place these controllers under the + # app/controllers/admin directory, and you can group them together in your + # router: + # + # namespace "admin" do + # resources :posts, :comments + # end + # + # Alternately, you can add prefixes to your path without using a separate + # directory by using +scope+. +scope+ takes additional options which + # apply to all enclosed routes. + # + # scope :path => "/cpanel", :as => 'admin' do + # resources :posts, :comments + # end + # + # For more, see <tt>Routing::Mapper::Resources#resources</tt>, + # <tt>Routing::Mapper::Scoping#namespace</tt>, and + # <tt>Routing::Mapper::Scoping#scope</tt>. + # # == Named routes # # Routes can be named by passing an <tt>:as</tt> option, @@ -131,11 +156,35 @@ module ActionDispatch # Encoding regular expression modifiers are silently ignored. The # match will always use the default encoding or ASCII. # + # == Default route + # + # Consider the following route, which you will find commented out at the + # bottom of your generated <tt>config/routes.rb</tt>: + # + # match ':controller(/:action(/:id(.:format)))' + # + # This route states that it expects requests to consist of a + # <tt>:controller</tt> followed optionally by an <tt>:action</tt> that in + # turn is followed optionally by an <tt>:id</tt>, which in turn is followed + # optionally by a <tt>:format</tt>. + # + # Suppose you get an incoming request for <tt>/blog/edit/22</tt>, you'll end + # up with: + # + # params = { :controller => 'blog', + # :action => 'edit', + # :id => '22' + # } + # + # By not relying on default routes, you improve the security of your + # application since not all controller actions, which includes actions you + # might add at a later time, are exposed by default. + # # == HTTP Methods # # Using the <tt>:via</tt> option when specifying a route allows you to restrict it to a specific HTTP method. - # Possible values are <tt>:post</tt>, <tt>:get</tt>, <tt>:put</tt>, <tt>:delete</tt> and <tt>:any</tt>. - # If your route needs to respond to more than one method you can use an array, e.g. <tt>[ :get, :post ]</tt>. + # Possible values are <tt>:post</tt>, <tt>:get</tt>, <tt>:put</tt>, <tt>:delete</tt> and <tt>:any</tt>. + # If your route needs to respond to more than one method you can use an array, e.g. <tt>[ :get, :post ]</tt>. # The default value is <tt>:any</tt> which means that the route will respond to any of the HTTP methods. # # Examples: @@ -144,7 +193,7 @@ module ActionDispatch # match 'post/:id' => "posts#create_comment', :via => :post # # Now, if you POST to <tt>/posts/:id</tt>, it will route to the <tt>create_comment</tt> action. A GET on the same - # URL will route to the <tt>show</tt> action. + # URL will route to the <tt>show</tt> action. # # === HTTP helper methods # @@ -160,6 +209,20 @@ module ActionDispatch # however if your route needs to respond to more than one HTTP method (or all methods) then using the # <tt>:via</tt> option on <tt>match</tt> is preferable. # + # == External redirects + # + # You can redirect any path to another path using the redirect helper in your router: + # + # match "/stories" => redirect("/posts") + # + # == Routing to Rack Applications + # + # Instead of a String, like <tt>posts#index</tt>, which corresponds to the + # index action in the PostsController, you can specify any Rack application + # as the endpoint for a matcher: + # + # match "/application.js" => Sprockets + # # == Reloading routes # # You can reload routes if you feel you must: @@ -208,13 +271,15 @@ module ActionDispatch # # == View a list of all your routes # - # Run <tt>rake routes</tt>. + # rake routes + # + # Target specific controllers by prefixing the command with <tt>CONTROLLER=x</tt>. # module Routing - autoload :DeprecatedMapper, 'action_dispatch/routing/deprecated_mapper' autoload :Mapper, 'action_dispatch/routing/mapper' autoload :Route, 'action_dispatch/routing/route' autoload :RouteSet, 'action_dispatch/routing/route_set' + autoload :RoutesProxy, 'action_dispatch/routing/routes_proxy' autoload :UrlFor, 'action_dispatch/routing/url_for' autoload :PolymorphicRoutes, 'action_dispatch/routing/polymorphic_routes' diff --git a/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb b/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb deleted file mode 100644 index e04062ce8b..0000000000 --- a/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb +++ /dev/null @@ -1,525 +0,0 @@ -require 'active_support/core_ext/object/blank' -require 'active_support/core_ext/object/with_options' -require 'active_support/core_ext/object/try' - -module ActionDispatch - module Routing - class RouteSet - attr_accessor :controller_namespaces - - CONTROLLER_REGEXP = /[_a-zA-Z0-9]+/ - - def controller_constraints - @controller_constraints ||= begin - namespaces = controller_namespaces + in_memory_controller_namespaces - source = namespaces.map { |ns| "#{Regexp.escape(ns)}/#{CONTROLLER_REGEXP.source}" } - source << CONTROLLER_REGEXP.source - Regexp.compile(source.sort.reverse.join('|')) - end - end - - def in_memory_controller_namespaces - namespaces = Set.new - ActionController::Base.descendants.each do |klass| - next if klass.anonymous? - namespaces << klass.name.underscore.split('/')[0...-1].join('/') - end - namespaces.delete('') - namespaces - end - end - - class DeprecatedMapper #:nodoc: - def initialize(set) #:nodoc: - ActiveSupport::Deprecation.warn "You are using the old router DSL which will be removed in Rails 3.1. " << - "Please check how to update your routes file at: http://www.engineyard.com/blog/2010/the-lowdown-on-routes-in-rails-3/" - @set = set - end - - def connect(path, options = {}) - options = options.dup - - if conditions = options.delete(:conditions) - conditions = conditions.dup - method = [conditions.delete(:method)].flatten.compact - method.map! { |m| - m = m.to_s.upcase - - if m == "HEAD" - raise ArgumentError, "HTTP method HEAD is invalid in route conditions. Rails processes HEAD requests the same as GETs, returning just the response headers" - end - - unless HTTP_METHODS.include?(m.downcase.to_sym) - raise ArgumentError, "Invalid HTTP method specified in route conditions" - end - - m - } - - if method.length > 1 - method = Regexp.union(*method) - elsif method.length == 1 - method = method.first - else - method = nil - end - end - - path_prefix = options.delete(:path_prefix) - name_prefix = options.delete(:name_prefix) - namespace = options.delete(:namespace) - - name = options.delete(:_name) - name = "#{name_prefix}#{name}" if name_prefix - - requirements = options.delete(:requirements) || {} - defaults = options.delete(:defaults) || {} - options.each do |k, v| - if v.is_a?(Regexp) - if value = options.delete(k) - requirements[k.to_sym] = value - end - else - value = options.delete(k) - defaults[k.to_sym] = value.is_a?(Symbol) ? value : value.to_param - end - end - - requirements.each do |_, requirement| - if requirement.source =~ %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z} - raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}" - end - if requirement.multiline? - raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}" - end - end - - requirements[:controller] ||= @set.controller_constraints - - if defaults[:controller] - defaults[:action] ||= 'index' - defaults[:controller] = defaults[:controller].to_s - defaults[:controller] = "#{namespace}#{defaults[:controller]}" if namespace - end - - if defaults[:action] - defaults[:action] = defaults[:action].to_s - end - - if path.is_a?(String) - path = "#{path_prefix}/#{path}" if path_prefix - path = path.gsub('.:format', '(.:format)') - path = optionalize_trailing_dynamic_segments(path, requirements, defaults) - glob = $1.to_sym if path =~ /\/\*(\w+)$/ - path = ::Rack::Mount::Utils.normalize_path(path) - - if glob && !defaults[glob].blank? - raise ActionController::RoutingError, "paths cannot have non-empty default values" - end - end - - app = Routing::RouteSet::Dispatcher.new(:defaults => defaults, :glob => glob) - - conditions = {} - conditions[:request_method] = method if method - conditions[:path_info] = path if path - - @set.add_route(app, conditions, requirements, defaults, name) - end - - def optionalize_trailing_dynamic_segments(path, requirements, defaults) #:nodoc: - path = (path =~ /^\//) ? path.dup : "/#{path}" - optional, segments = true, [] - - required_segments = requirements.keys - required_segments -= defaults.keys.compact - - old_segments = path.split('/') - old_segments.shift - length = old_segments.length - - old_segments.reverse.each_with_index do |segment, index| - required_segments.each do |required| - if segment =~ /#{required}/ - optional = false - break - end - end - - if optional - if segment == ":id" && segments.include?(":action") - optional = false - elsif segment == ":controller" || segment == ":action" || segment == ":id" - # Ignore - elsif !(segment =~ /^:\w+$/) && - !(segment =~ /^:\w+\(\.:format\)$/) - optional = false - elsif segment =~ /^:(\w+)$/ - if defaults.has_key?($1.to_sym) - defaults.delete($1.to_sym) if defaults[$1.to_sym].nil? - else - optional = false - end - end - end - - if optional && index < length - 1 - segments.unshift('(/', segment) - segments.push(')') - elsif optional - segments.unshift('/(', segment) - segments.push(')') - else - segments.unshift('/', segment) - end - end - - segments.join - end - private :optionalize_trailing_dynamic_segments - - # Creates a named route called "root" for matching the root level request. - def root(options = {}) - if options.is_a?(Symbol) - if source_route = @set.named_routes.routes[options] - options = source_route.defaults.merge({ :conditions => source_route.conditions }) - end - end - named_route("root", '', options) - end - - def named_route(name, path, options = {}) #:nodoc: - options[:_name] = name - connect(path, options) - end - - def namespace(name, options = {}, &block) - if options[:namespace] - with_options({:path_prefix => "#{options.delete(:path_prefix)}/#{name}", :name_prefix => "#{options.delete(:name_prefix)}#{name}_", :namespace => "#{options.delete(:namespace)}#{name}/" }.merge(options), &block) - else - with_options({:path_prefix => name, :name_prefix => "#{name}_", :namespace => "#{name}/" }.merge(options), &block) - end - end - - def method_missing(route_name, *args, &proc) #:nodoc: - super unless args.length >= 1 && proc.nil? - named_route(route_name, *args) - end - - INHERITABLE_OPTIONS = :namespace, :shallow - - class Resource #:nodoc: - DEFAULT_ACTIONS = :index, :create, :new, :edit, :show, :update, :destroy - - attr_reader :collection_methods, :member_methods, :new_methods - attr_reader :path_prefix, :name_prefix, :path_segment - attr_reader :plural, :singular - attr_reader :options, :defaults - - def initialize(entities, options, defaults) - @plural ||= entities - @singular ||= options[:singular] || plural.to_s.singularize - @path_segment = options.delete(:as) || @plural - - @options = options - @defaults = defaults - - arrange_actions - add_default_actions - set_allowed_actions - set_prefixes - end - - def controller - @controller ||= "#{options[:namespace]}#{(options[:controller] || plural).to_s}" - end - - def requirements(with_id = false) - @requirements ||= @options[:requirements] || {} - @id_requirement ||= { :id => @requirements.delete(:id) || /[^#{Routing::SEPARATORS.join}]+/ } - - with_id ? @requirements.merge(@id_requirement) : @requirements - end - - def conditions - @conditions ||= @options[:conditions] || {} - end - - def path - @path ||= "#{path_prefix}/#{path_segment}" - end - - def new_path - new_action = self.options[:path_names][:new] if self.options[:path_names] - new_action ||= self.defaults[:path_names][:new] - @new_path ||= "#{path}/#{new_action}" - end - - def shallow_path_prefix - @shallow_path_prefix ||= @options[:shallow] ? @options[:namespace].try(:sub, /\/$/, '') : path_prefix - end - - def member_path - @member_path ||= "#{shallow_path_prefix}/#{path_segment}/:id" - end - - def nesting_path_prefix - @nesting_path_prefix ||= "#{shallow_path_prefix}/#{path_segment}/:#{singular}_id" - end - - def shallow_name_prefix - @shallow_name_prefix ||= @options[:shallow] ? @options[:namespace].try(:gsub, /\//, '_') : name_prefix - end - - def nesting_name_prefix - "#{shallow_name_prefix}#{singular}_" - end - - def action_separator - @action_separator ||= ActionController::Base.resource_action_separator - end - - def uncountable? - @singular.to_s == @plural.to_s - end - - def has_action?(action) - !DEFAULT_ACTIONS.include?(action) || action_allowed?(action) - end - - protected - def arrange_actions - @collection_methods = arrange_actions_by_methods(options.delete(:collection)) - @member_methods = arrange_actions_by_methods(options.delete(:member)) - @new_methods = arrange_actions_by_methods(options.delete(:new)) - end - - def add_default_actions - add_default_action(member_methods, :get, :edit) - add_default_action(new_methods, :get, :new) - end - - def set_allowed_actions - only, except = @options.values_at(:only, :except) - @allowed_actions ||= {} - - if only == :all || except == :none - only = nil - except = [] - elsif only == :none || except == :all - only = [] - except = nil - end - - if only - @allowed_actions[:only] = Array(only).map {|a| a.to_sym } - elsif except - @allowed_actions[:except] = Array(except).map {|a| a.to_sym } - end - end - - def action_allowed?(action) - only, except = @allowed_actions.values_at(:only, :except) - (!only || only.include?(action)) && (!except || !except.include?(action)) - end - - def set_prefixes - @path_prefix = options.delete(:path_prefix) - @name_prefix = options.delete(:name_prefix) - end - - def arrange_actions_by_methods(actions) - (actions || {}).inject({}) do |flipped_hash, (key, value)| - (flipped_hash[value] ||= []) << key - flipped_hash - end - end - - def add_default_action(collection, method, action) - (collection[method] ||= []).unshift(action) - end - end - - class SingletonResource < Resource #:nodoc: - def initialize(entity, options, defaults) - @singular = @plural = entity - options[:controller] ||= @singular.to_s.pluralize - super - end - - alias_method :shallow_path_prefix, :path_prefix - alias_method :shallow_name_prefix, :name_prefix - alias_method :member_path, :path - alias_method :nesting_path_prefix, :path - end - - def resources(*entities, &block) - options = entities.extract_options! - entities.each { |entity| map_resource(entity, options.dup, &block) } - end - - def resource(*entities, &block) - options = entities.extract_options! - entities.each { |entity| map_singleton_resource(entity, options.dup, &block) } - end - - private - def map_resource(entities, options = {}, &block) - resource = Resource.new(entities, options, :path_names => @set.resources_path_names) - - with_options :controller => resource.controller do |map| - map_associations(resource, options) - - if block_given? - with_options(options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix), &block) - end - - map_collection_actions(map, resource) - map_default_collection_actions(map, resource) - map_new_actions(map, resource) - map_member_actions(map, resource) - end - end - - def map_singleton_resource(entities, options = {}, &block) - resource = SingletonResource.new(entities, options, :path_names => @set.resources_path_names) - - with_options :controller => resource.controller do |map| - map_associations(resource, options) - - if block_given? - with_options(options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix), &block) - end - - map_collection_actions(map, resource) - map_new_actions(map, resource) - map_member_actions(map, resource) - map_default_singleton_actions(map, resource) - end - end - - def map_associations(resource, options) - map_has_many_associations(resource, options.delete(:has_many), options) if options[:has_many] - - path_prefix = "#{options.delete(:path_prefix)}#{resource.nesting_path_prefix}" - name_prefix = "#{options.delete(:name_prefix)}#{resource.nesting_name_prefix}" - - Array(options[:has_one]).each do |association| - resource(association, options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => path_prefix, :name_prefix => name_prefix)) - end - end - - def map_has_many_associations(resource, associations, options) - case associations - when Hash - associations.each do |association,has_many| - map_has_many_associations(resource, association, options.merge(:has_many => has_many)) - end - when Array - associations.each do |association| - map_has_many_associations(resource, association, options) - end - when Symbol, String - resources(associations, options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix, :has_many => options[:has_many])) - else - end - end - - def map_collection_actions(map, resource) - resource.collection_methods.each do |method, actions| - actions.each do |action| - [method].flatten.each do |m| - action_path = resource.options[:path_names][action] if resource.options[:path_names].is_a?(Hash) - action_path ||= action - - map_resource_routes(map, resource, action, "#{resource.path}#{resource.action_separator}#{action_path}", "#{action}_#{resource.name_prefix}#{resource.plural}", m) - end - end - end - end - - def map_default_collection_actions(map, resource) - index_route_name = "#{resource.name_prefix}#{resource.plural}" - - if resource.uncountable? - index_route_name << "_index" - end - - map_resource_routes(map, resource, :index, resource.path, index_route_name) - map_resource_routes(map, resource, :create, resource.path, index_route_name) - end - - def map_default_singleton_actions(map, resource) - map_resource_routes(map, resource, :create, resource.path, "#{resource.shallow_name_prefix}#{resource.singular}") - end - - def map_new_actions(map, resource) - resource.new_methods.each do |method, actions| - actions.each do |action| - route_path = resource.new_path - route_name = "new_#{resource.name_prefix}#{resource.singular}" - - unless action == :new - route_path = "#{route_path}#{resource.action_separator}#{action}" - route_name = "#{action}_#{route_name}" - end - - map_resource_routes(map, resource, action, route_path, route_name, method) - end - end - end - - def map_member_actions(map, resource) - resource.member_methods.each do |method, actions| - actions.each do |action| - [method].flatten.each do |m| - action_path = resource.options[:path_names][action] if resource.options[:path_names].is_a?(Hash) - action_path ||= @set.resources_path_names[action] || action - - map_resource_routes(map, resource, action, "#{resource.member_path}#{resource.action_separator}#{action_path}", "#{action}_#{resource.shallow_name_prefix}#{resource.singular}", m, { :force_id => true }) - end - end - end - - route_path = "#{resource.shallow_name_prefix}#{resource.singular}" - map_resource_routes(map, resource, :show, resource.member_path, route_path) - map_resource_routes(map, resource, :update, resource.member_path, route_path) - map_resource_routes(map, resource, :destroy, resource.member_path, route_path) - end - - def map_resource_routes(map, resource, action, route_path, route_name = nil, method = nil, resource_options = {} ) - if resource.has_action?(action) - action_options = action_options_for(action, resource, method, resource_options) - formatted_route_path = "#{route_path}.:format" - - if route_name && @set.named_routes[route_name.to_sym].nil? - map.named_route(route_name, formatted_route_path, action_options) - else - map.connect(formatted_route_path, action_options) - end - end - end - - def add_conditions_for(conditions, method) - {:conditions => conditions.dup}.tap do |options| - options[:conditions][:method] = method unless method == :any - end - end - - def action_options_for(action, resource, method = nil, resource_options = {}) - default_options = { :action => action.to_s } - require_id = !resource.kind_of?(SingletonResource) - force_id = resource_options[:force_id] && !resource.kind_of?(SingletonResource) - - case default_options[:action] - when "index", "new"; default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements) - when "create"; default_options.merge(add_conditions_for(resource.conditions, method || :post)).merge(resource.requirements) - when "show", "edit"; default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements(require_id)) - when "update"; default_options.merge(add_conditions_for(resource.conditions, method || :put)).merge(resource.requirements(require_id)) - when "destroy"; default_options.merge(add_conditions_for(resource.conditions, method || :delete)).merge(resource.requirements(require_id)) - else default_options.merge(add_conditions_for(resource.conditions, method)).merge(resource.requirements(force_id)) - end - end - end - end -end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index c118c72440..14c424f24b 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -1,6 +1,8 @@ require 'erb' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/object/blank' +require 'active_support/inflector' +require 'action_dispatch/routing/redirection' module ActionDispatch module Routing @@ -20,27 +22,37 @@ module ActionDispatch @app, @constraints, @request = app, constraints, request end - def call(env) + def matches?(env) req = @request.new(env) @constraints.each { |constraint| if constraint.respond_to?(:matches?) && !constraint.matches?(req) - return [ 404, {'X-Cascade' => 'pass'}, [] ] - elsif constraint.respond_to?(:call) && !constraint.call(req) - return [ 404, {'X-Cascade' => 'pass'}, [] ] + return false + elsif constraint.respond_to?(:call) && !constraint.call(*constraint_args(constraint, req)) + return false end } - @app.call(env) + return true + end + + def call(env) + matches?(env) ? @app.call(env) : [ 404, {'X-Cascade' => 'pass'}, [] ] end + + private + def constraint_args(constraint, request) + constraint.arity == 1 ? [request] : [request.symbolized_path_parameters, request] + end end class Mapping #:nodoc: IGNORE_OPTIONS = [:to, :as, :via, :on, :constraints, :defaults, :only, :except, :anchor, :shallow, :shallow_path, :shallow_prefix] - def initialize(set, scope, args) - @set, @scope = set, scope - @path, @options = extract_path_and_options(args) + def initialize(set, scope, path, options) + @set, @scope = set, scope + @options = (@scope[:options] || {}).merge(options) + @path = normalize_path(path) normalize_options! end @@ -49,28 +61,6 @@ module ActionDispatch end private - def extract_path_and_options(args) - options = args.extract_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 - else - path = args.first - end - - if path.match(':controller') - raise ArgumentError, ":controller segment is not allowed within a namespace block" if @scope[:module] - - # Add a default constraint for :controller path segments that matches namespaced - # controllers with default routes like :controller/:action/:id(.:format), e.g: - # GET /admin/products/show/1 - # => { :controller => 'admin/products', :action => 'show', :id => '1' } - options.reverse_merge!(:controller => /.+?/) - end - - [ normalize_path(path), options ] - end def normalize_options! path_without_format = @path.sub(/\(\.:format\)$/, '') @@ -78,15 +68,21 @@ module ActionDispatch if using_match_shorthand?(path_without_format, @options) to_shorthand = @options[:to].blank? @options[:to] ||= path_without_format[1..-1].sub(%r{/([^/]*)$}, '#\1') - @options[:as] ||= path_without_format[1..-1].gsub("/", "_") end @options.merge!(default_controller_and_action(to_shorthand)) - end - # match "account" => "account#index" - def using_to_shorthand?(args, options) - args.empty? && options.present? + requirements.each do |name, requirement| + # segment_keys.include?(k.to_s) || k == :controller + next unless Regexp === requirement && !constraints[name] + + if requirement.source =~ %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z} + raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}" + end + if requirement.multiline? + raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}" + end + end end # match "account/overview" @@ -95,8 +91,27 @@ module ActionDispatch end def normalize_path(path) - raise ArgumentError, "path is required" if @scope[:path].blank? && path.blank? - Mapper.normalize_path("#{@scope[:path]}/#{path}") + raise ArgumentError, "path is required" if path.blank? + path = Mapper.normalize_path(path) + + if path.match(':controller') + raise ArgumentError, ":controller segment is not allowed within a namespace block" if @scope[:module] + + # Add a default constraint for :controller path segments that matches namespaced + # controllers with default routes like :controller/:action/:id(.:format), e.g: + # GET /admin/products/show/1 + # => { :controller => 'admin/products', :action => 'show', :id => '1' } + @options.reverse_merge!(:controller => /.+?/) + end + + if @options[:format] == false + @options.delete(:format) + path + elsif path.include?(":format") || path.end_with?('/') || path.match(/^\/?\*/) + path + else + "#{path}(.:format)" + end end def app @@ -142,6 +157,10 @@ module ActionDispatch controller = [@scope[:module], controller].compact.join("/").presence end + if controller.is_a?(String) && controller =~ %r{\A/} + raise ArgumentError, "controller name should not start with a slash" + end + controller = controller.to_s unless controller.is_a?(Regexp) action = action.to_s unless action.is_a?(Regexp) @@ -153,21 +172,21 @@ module ActionDispatch raise ArgumentError, "missing :action" end - { :controller => controller, :action => action }.tap do |hash| - hash.delete(:controller) if hash[:controller].blank? - hash.delete(:action) if hash[:action].blank? - end + hash = {} + hash[:controller] = controller unless controller.blank? + hash[:action] = action unless action.blank? + hash end end def blocks + block = @scope[:blocks] || [] + if @options[:constraints].present? && !@options[:constraints].is_a?(Hash) - block = @options[:constraints] - else - block = nil + block << @options[:constraints] end - ((@scope[:blocks] || []) + [ block ]).compact + block end def constraints @@ -176,8 +195,8 @@ module ActionDispatch def request_method_condition if via = @options[:via] - via = Array(via).map { |m| m.to_s.upcase } - { :request_method => Regexp.union(*via) } + list = Array(via).map { |m| m.to_s.dasherize.upcase } + { :request_method => list } else { } end @@ -219,21 +238,165 @@ module ActionDispatch path end - module Base - def initialize(set) #:nodoc: - @set = set - end + def self.normalize_name(name) + normalize_path(name)[1..-1].gsub("/", "_") + end + module Base + # You can specify what Rails should route "/" to with the root method: + # + # root :to => 'pages#main' + # + # For options, see +match+, as +root+ uses it internally. + # + # You should put the root route at the top of <tt>config/routes.rb</tt>, + # because this means it will be matched first. As this is the most popular route + # of most Rails applications, this is beneficial. def root(options = {}) match '/', options.reverse_merge(:as => :root) end - def match(*args) - mapping = Mapping.new(@set, @scope, args).to_route - @set.add_route(*mapping) + # Matches a url pattern to one or more routes. Any symbols in a pattern + # are interpreted as url query parameters and thus available as +params+ + # in an action: + # + # # sets :controller, :action and :id in params + # match ':controller/:action/:id' + # + # Two of these symbols are special, +:controller+ maps to the controller + # and +:action+ to the controller's action. A pattern can also map + # wildcard segments (globs) to params: + # + # match 'songs/*category/:title' => 'songs#show' + # + # # 'songs/rock/classic/stairway-to-heaven' sets + # # params[:category] = 'rock/classic' + # # params[:title] = 'stairway-to-heaven' + # + # When a pattern points to an internal route, the route's +:action+ and + # +:controller+ should be set in options or hash shorthand. Examples: + # + # match 'photos/:id' => 'photos#show' + # match 'photos/:id', :to => 'photos#show' + # match 'photos/:id', :controller => 'photos', :action => 'show' + # + # A pattern can also point to a +Rack+ endpoint i.e. anything that + # responds to +call+: + # + # match 'photos/:id' => lambda {|hash| [200, {}, "Coming soon" } + # match 'photos/:id' => PhotoRackApp + # # Yes, controller actions are just rack endpoints + # match 'photos/:id' => PhotosController.action(:show) + # + # === Options + # + # Any options not seen here are passed on as params with the url. + # + # [:controller] + # The route's controller. + # + # [:action] + # The route's action. + # + # [:path] + # The path prefix for the routes. + # + # [:module] + # The namespace for :controller. + # + # match 'path' => 'c#a', :module => 'sekret', :controller => 'posts' + # #=> Sekret::PostsController + # + # See <tt>Scoping#namespace</tt> for its scope equivalent. + # + # [:as] + # The name used to generate routing helpers. + # + # [:via] + # Allowed HTTP verb(s) for route. + # + # match 'path' => 'c#a', :via => :get + # match 'path' => 'c#a', :via => [:get, :post] + # + # [:to] + # Points to a +Rack+ endpoint. Can be an object that responds to + # +call+ or a string representing a controller's action. + # + # match 'path', :to => 'controller#action' + # match 'path', :to => lambda { [200, {}, "Success!"] } + # match 'path', :to => RackApp + # + # [:on] + # Shorthand for wrapping routes in a specific RESTful context. Valid + # values are :member, :collection, and :new. Only use within + # <tt>resource(s)</tt> block. For example: + # + # resource :bar do + # match 'foo' => 'c#a', :on => :member, :via => [:get, :post] + # end + # + # Is equivalent to: + # + # resource :bar do + # member do + # match 'foo' => 'c#a', :via => [:get, :post] + # end + # end + # + # [:constraints] + # Constrains parameters with a hash of regular expressions or an + # object that responds to #matches? + # + # match 'path/:id', :constraints => { :id => /[A-Z]\d{5}/ } + # + # class Blacklist + # def matches?(request) request.remote_ip == '1.2.3.4' end + # end + # match 'path' => 'c#a', :constraints => Blacklist.new + # + # See <tt>Scoping#constraints</tt> for more examples with its scope + # equivalent. + # + # [:defaults] + # Sets defaults for parameters + # + # # Sets params[:format] to 'jpg' by default + # 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 || {}) + app, conditions, requirements, defaults, as, anchor = mapping.to_route + @set.add_route(app, conditions, requirements, defaults, as, anchor) self end + # Mount a Rack-based application to be used within the application. + # + # mount SomeRackApp, :at => "some_route" + # + # Alternatively: + # + # mount(SomeRackApp => "some_route") + # + # For options, see +match+, as +mount+ uses it internally. + # + # All mounted applications come with routing helpers to access them. + # These are named after the class specified, so for the above example + # the helper is either +some_rack_app_path+ or +some_rack_app_url+. + # To customize this helper's name, use the +:as+ option: + # + # mount(SomeRackApp => "some_route", :as => "exciting") + # + # This will generate the +exciting_path+ and +exciting_url+ helpers + # which can be used to navigate to this mounted app. def mount(app, options = nil) if options path = options.delete(:at) @@ -245,7 +408,11 @@ module ActionDispatch raise "A rack application must be specified" unless path - match(path, options.merge(:to => app, :anchor => false)) + options[:as] ||= app_name(app) + + match(path, options.merge(:to => app, :anchor => false, :format => false)) + + define_generate_prefix(app, options[:as]) self end @@ -253,55 +420,83 @@ module ActionDispatch @set.default_url_options = options end alias_method :default_url_options, :default_url_options= + + def with_default_scope(scope, &block) + scope(scope) do + instance_exec(&block) + end + end + + private + def app_name(app) + return unless app.respond_to?(:routes) + + if app.respond_to?(:railtie_name) + app.railtie_name + else + class_name = app.class.is_a?(Class) ? app.name : app.class.name + ActiveSupport::Inflector.underscore(class_name).gsub("/", "_") + end + end + + def define_generate_prefix(app, name) + return unless app.respond_to?(:routes) && app.routes.respond_to?(:define_mounted_helper) + + _route = @set.named_routes.routes[name.to_sym] + _routes = @set + app.routes.define_mounted_helper(name) + app.routes.class_eval do + define_method :_generate_prefix do |options| + prefix_options = options.slice(*_route.segment_keys) + # we must actually delete prefix segment keys to avoid passing them to next url_for + _route.segment_keys.each { |k| options.delete(k) } + _routes.url_helpers.send("#{name}_path", prefix_options) + end + end + end end module HttpHelpers + # Define a route that only recognizes HTTP GET. + # For supported arguments, see <tt>Base#match</tt>. + # + # Example: + # + # get 'bacon', :to => 'food#bacon' def get(*args, &block) map_method(:get, *args, &block) end + # Define a route that only recognizes HTTP POST. + # For supported arguments, see <tt>Base#match</tt>. + # + # Example: + # + # post 'bacon', :to => 'food#bacon' def post(*args, &block) map_method(:post, *args, &block) end + # Define a route that only recognizes HTTP PUT. + # For supported arguments, see <tt>Base#match</tt>. + # + # Example: + # + # put 'bacon', :to => 'food#bacon' def put(*args, &block) map_method(:put, *args, &block) end + # Define a route that only recognizes HTTP PUT. + # For supported arguments, see <tt>Base#match</tt>. + # + # Example: + # + # delete 'broccoli', :to => 'food#broccoli' def delete(*args, &block) map_method(:delete, *args, &block) end - def redirect(*args, &block) - options = args.last.is_a?(Hash) ? args.pop : {} - - path = args.shift || block - path_proc = path.is_a?(Proc) ? path : proc { |params| path % params } - status = options[:status] || 301 - - lambda do |env| - req = Request.new(env) - - params = [req.symbolized_path_parameters] - params << req if path_proc.arity > 1 - - uri = URI.parse(path_proc.call(*params)) - uri.scheme ||= req.scheme - uri.host ||= req.host - uri.port ||= req.port unless req.port == 80 - - body = %(<html><body>You are being <a href="#{ERB::Util.h(uri.to_s)}">redirected</a>.</body></html>) - - headers = { - 'Location' => uri.to_s, - 'Content-Type' => 'text/html', - 'Content-Length' => body.length.to_s - } - - [ status, headers, [body] ] - end - end - private def map_method(method, *args, &block) options = args.extract_options! @@ -312,28 +507,98 @@ module ActionDispatch end end + # You may wish to organize groups of controllers under a namespace. + # Most commonly, you might group a number of administrative controllers + # under an +admin+ namespace. You would place these controllers under + # the app/controllers/admin directory, and you can group them together + # in your router: + # + # namespace "admin" do + # resources :posts, :comments + # end + # + # This will create a number of routes for each of the posts and comments + # controller. For Admin::PostsController, Rails will create: + # + # GET /admin/posts + # GET /admin/posts/new + # POST /admin/posts + # GET /admin/posts/1 + # GET /admin/posts/1/edit + # PUT /admin/posts/1 + # DELETE /admin/posts/1 + # + # If you want to route /posts (without the prefix /admin) to + # Admin::PostsController, you could use + # + # scope :module => "admin" do + # resources :posts + # end + # + # or, for a single case + # + # resources :posts, :module => "admin" + # + # If you want to route /admin/posts to PostsController + # (without the Admin:: module prefix), you could use + # + # scope "/admin" do + # resources :posts + # end + # + # or, for a single case + # + # resources :posts, :path => "/admin" + # + # In each of these cases, the named routes remain the same as if you did + # not use scope. In the last case, the following paths map to + # PostsController: + # + # GET /admin/posts + # GET /admin/posts/new + # POST /admin/posts + # GET /admin/posts/1 + # GET /admin/posts/1/edit + # 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: + # + # scope :path => ":account_id", :as => "account" do + # resources :projects + # end + # + # This generates helpers such as +account_projects_path+, just like +resources+ does. + # The difference here being that the routes generated are like /rails/projects/2, + # rather than /accounts/rails/projects/2. + # + # === Options + # + # Takes same options as <tt>Base#match</tt> and <tt>Resources#resources</tt>. + # + # === Examples + # + # # route /posts (without the prefix /admin) to Admin::PostsController + # scope :module => "admin" do + # resources :posts + # end + # + # # prefix the posts resource's requests with '/admin' + # scope :path => "/admin" do + # resources :posts + # end + # + # # prefix the routing helper name: sekret_posts_path instead of posts_path + # scope :as => "sekret" do + # resources :posts + # end def scope(*args) options = args.extract_options! options = options.dup - if name_prefix = options.delete(:name_prefix) - options[:as] ||= name_prefix - ActiveSupport::Deprecation.warn ":name_prefix was deprecated in the new router syntax. Use :as instead.", caller - end - - case args.first - when String - options[:path] = args.first - when Symbol - options[:controller] = args.first - end - + options[:path] = args.first if args.first.is_a?(String) recover = {} options[:constraints] ||= {} @@ -365,10 +630,57 @@ module ActionDispatch @scope[:blocks] = recover[:block] end - def controller(controller) - scope(controller.to_sym) { yield } + # Scopes routes to a specific controller + # + # Example: + # controller "food" do + # match "bacon", :action => "bacon" + # end + def controller(controller, options={}) + options[:controller] = controller + scope(options) { yield } end + # Scopes routes to a specific namespace. For example: + # + # namespace :admin do + # resources :posts + # end + # + # This generates the following routes: + # + # admin_posts GET /admin/posts(.:format) {:action=>"index", :controller=>"admin/posts"} + # admin_posts POST /admin/posts(.:format) {:action=>"create", :controller=>"admin/posts"} + # new_admin_post GET /admin/posts/new(.:format) {:action=>"new", :controller=>"admin/posts"} + # edit_admin_post GET /admin/posts/:id/edit(.:format) {:action=>"edit", :controller=>"admin/posts"} + # admin_post GET /admin/posts/:id(.:format) {:action=>"show", :controller=>"admin/posts"} + # admin_post PUT /admin/posts/:id(.:format) {:action=>"update", :controller=>"admin/posts"} + # admin_post DELETE /admin/posts/:id(.:format) {:action=>"destroy", :controller=>"admin/posts"} + # + # === Options + # + # The +:path+, +:as+, +:module+, +:shallow_path+ and +:shallow_prefix+ + # options all default to the name of the namespace. + # + # For options, see <tt>Base#match</tt>. For +:shallow_path+ option, see + # <tt>Resources#resources</tt>. + # + # === Examples + # + # # accessible through /sekret/posts rather than /admin/posts + # namespace :admin, :path => "sekret" do + # resources :posts + # end + # + # # maps to Sekret::PostsController rather than Admin::PostsController + # namespace :admin, :module => "sekret" do + # resources :posts + # end + # + # # generates sekret_posts_path rather than admin_posts_path + # namespace :admin, :as => "sekret" do + # resources :posts + # end def namespace(path, options = {}) path = path.to_s options = { :path => path, :as => path, :module => path, @@ -376,95 +688,178 @@ module ActionDispatch scope(options) { yield } end + # === Parameter Restriction + # Allows you to constrain the nested routes based on a set of rules. + # For instance, in order to change the routes to allow for a dot character in the +id+ parameter: + # + # constraints(:id => /\d+\.\d+) do + # resources :posts + # end + # + # 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 restrict other parameters: + # + # resources :posts do + # constraints(:post_id => /\d+\.\d+) do + # resources :comments + # end + # + # === Restricting based on IP + # + # Routes can also be constrained to an IP or a certain range of IP addresses: + # + # constraints(:ip => /192.168.\d+.\d+/) do + # resources :posts + # end + # + # Any user connecting from the 192.168.* range will be able to see this resource, + # where as any user connecting outside of this range will be told there is no such route. + # + # === Dynamic request matching + # + # Requests to routes can be constrained based on specific criteria: + # + # constraints(lambda { |req| req.env["HTTP_USER_AGENT"] =~ /iPhone/ }) do + # resources :iphones + # end + # + # You are able to move this logic out into a class if it is too complex for routes. + # This class must have a +matches?+ method defined on it which either returns +true+ + # if the user should be given access to that route, or +false+ if the user should not. + # + # class Iphone + # def self.matches(request) + # request.env["HTTP_USER_AGENT"] =~ /iPhone/ + # end + # end + # + # An expected place for this code would be +lib/constraints+. + # + # This class is then used like this: + # + # constraints(Iphone) do + # resources :iphones + # end def constraints(constraints = {}) scope(:constraints => constraints) { yield } end + # Allows you to set default parameters for a route, such as this: + # defaults :id => 'home' do + # match 'scoped_pages/(:id)', :to => 'pages#show' + # end + # Using this, the +:id+ parameter here will default to 'home'. def defaults(defaults = {}) scope(:defaults => defaults) { yield } end - def match(*args) - options = args.extract_options! - - options = (@scope[:options] || {}).merge(options) - - if @scope[:as] && !options[:as].blank? - options[:as] = "#{@scope[:as]}_#{options[:as]}" - elsif @scope[:as] && options[:as] == "" - options[:as] = @scope[:as].to_s - end - - args.push(options) - super(*args) - end - private - def scope_options + def scope_options #:nodoc: @scope_options ||= private_methods.grep(/^merge_(.+)_scope$/) { $1.to_sym } end - def merge_path_scope(parent, child) + def merge_path_scope(parent, child) #:nodoc: Mapper.normalize_path("#{parent}/#{child}") end - def merge_shallow_path_scope(parent, child) + def merge_shallow_path_scope(parent, child) #:nodoc: Mapper.normalize_path("#{parent}/#{child}") end - def merge_as_scope(parent, child) + def merge_as_scope(parent, child) #:nodoc: parent ? "#{parent}_#{child}" : child end - def merge_shallow_prefix_scope(parent, child) + def merge_shallow_prefix_scope(parent, child) #:nodoc: parent ? "#{parent}_#{child}" : child end - def merge_module_scope(parent, child) + def merge_module_scope(parent, child) #:nodoc: parent ? "#{parent}/#{child}" : child end - def merge_controller_scope(parent, child) + def merge_controller_scope(parent, child) #:nodoc: child end - def merge_path_names_scope(parent, child) + def merge_path_names_scope(parent, child) #:nodoc: merge_options_scope(parent, child) end - def merge_constraints_scope(parent, child) + def merge_constraints_scope(parent, child) #:nodoc: merge_options_scope(parent, child) end - def merge_defaults_scope(parent, child) + def merge_defaults_scope(parent, child) #:nodoc: merge_options_scope(parent, child) end - def merge_blocks_scope(parent, child) + def merge_blocks_scope(parent, child) #:nodoc: merged = parent ? parent.dup : [] merged << child if child merged end - def merge_options_scope(parent, child) + def merge_options_scope(parent, child) #:nodoc: (parent || {}).except(*override_keys(child)).merge(child) end - def merge_shallow_scope(parent, child) + def merge_shallow_scope(parent, child) #:nodoc: child ? true : false end - def override_keys(child) + def override_keys(child) #:nodoc: child.key?(:only) || child.key?(:except) ? [:only, :except] : [] end end + # Resource routing allows you to quickly declare all of the common routes + # for a given resourceful controller. Instead of declaring separate routes + # for your +index+, +show+, +new+, +edit+, +create+, +update+ and +destroy+ + # actions, a resourceful route declares them in a single line of code: + # + # resources :photos + # + # Sometimes, you have a resource that clients always look up without + # referencing an ID. A common example, /profile always shows the profile of + # the currently logged in user. In this case, you can use a singular resource + # to map /profile (rather than /profile/:id) to the show action. + # + # resource :profile + # + # It's common to have resources that are logically children of other + # resources: + # + # resources :magazines do + # resources :ads + # end + # + # You may wish to organize groups of controllers under a namespace. Most + # commonly, you might group a number of administrative controllers under + # an +admin+ namespace. You would place these controllers under the + # app/controllers/admin directory, and you can group them together in your + # router: + # + # namespace "admin" do + # resources :posts, :comments + # end + # + # By default the :id parameter doesn't accept dots. If you need to + # use dots as part of the :id parameter add a constraint which + # overrides this restriction, e.g: + # + # resources :articles, :id => /[^\/]+/ + # + # This allows any character other than a slash as part of your :id. + # module Resources # CANONICAL_ACTIONS holds all actions that does not need a prefix or # a path appended since they fit properly in their scope level. - VALID_ON_OPTIONS = [:new, :collection, :member] - CANONICAL_ACTIONS = [:index, :create, :new, :show, :update, :destroy] - RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except] + VALID_ON_OPTIONS = [:new, :collection, :member] + RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except] + CANONICAL_ACTIONS = %w(index create new show update destroy) class Resource #:nodoc: DEFAULT_ACTIONS = [:index, :create, :new, :show, :update, :destroy, :edit] @@ -473,7 +868,7 @@ module ActionDispatch def initialize(entities, options = {}) @name = entities.to_s - @path = options.delete(:path) || @name + @path = (options.delete(:path) || @name).to_s @controller = (options.delete(:controller) || @name).to_s @as = options.delete(:as) @options = options @@ -498,18 +893,17 @@ module ActionDispatch end def plural - name.to_s.pluralize + @plural ||= name.to_s end def singular - name.to_s.singularize + @singular ||= name.to_s.singularize end - def member_name - singular - end + alias :member_name :singular - # Checks for uncountable plurals, and appends "_index" if they're. + # Checks for uncountable plurals, and appends "_index" if the plural + # and singular form are the same. def collection_name singular == plural ? "#{plural}_index" : plural end @@ -518,9 +912,7 @@ module ActionDispatch { :controller => controller } end - def collection_scope - path - end + alias :collection_scope :path def member_scope "#{path}/:id" @@ -540,33 +932,54 @@ module ActionDispatch DEFAULT_ACTIONS = [:show, :create, :update, :destroy, :new, :edit] def initialize(entities, options) + @as = nil @name = entities.to_s - @path = options.delete(:path) || @name + @path = (options.delete(:path) || @name).to_s @controller = (options.delete(:controller) || plural).to_s @as = options.delete(:as) @options = options end - def member_name - name + def plural + @plural ||= name.to_s.pluralize end - alias :collection_name :member_name - def member_scope - path + def singular + @singular ||= name.to_s end - alias :nested_scope :member_scope - end - def initialize(*args) #:nodoc: - super - @scope[:path_names] = @set.resources_path_names + alias :member_name :singular + alias :collection_name :singular + + alias :member_scope :path + alias :nested_scope :path end def resources_path_names(options) @scope[:path_names].merge!(options) end + # Sometimes, you have a resource that clients always look up without + # referencing an ID. A common example, /profile always shows the + # profile of the currently logged in user. In this case, you can use + # a singular resource to map /profile (rather than /profile/:id) to + # the show action: + # + # resource :geocoder + # + # creates six different routes in your application, all mapping to + # the GeoCoders controller (note that the controller is named after + # the plural): + # + # GET /geocoder/new + # POST /geocoder + # GET /geocoder + # GET /geocoder/edit + # PUT /geocoder + # DELETE /geocoder + # + # === Options + # Takes same options as +resources+. def resource(*resources, &block) options = resources.extract_options! @@ -577,25 +990,121 @@ module ActionDispatch resource_scope(SingletonResource.new(resources.pop, options)) do yield if block_given? - collection_scope do + collection do post :create end if parent_resource.actions.include?(:create) - new_scope do + new do get :new end if parent_resource.actions.include?(:new) - member_scope do + member do + get :edit if parent_resource.actions.include?(:edit) get :show if parent_resource.actions.include?(:show) put :update if parent_resource.actions.include?(:update) delete :destroy if parent_resource.actions.include?(:destroy) - get :edit if parent_resource.actions.include?(:edit) end end self end + # In Rails, a resourceful route provides a mapping between HTTP verbs + # and URLs and controller actions. By convention, each action also maps + # to particular CRUD operations in a database. A single entry in the + # routing file, such as + # + # resources :photos + # + # creates seven different routes in your application, all mapping to + # the Photos controller: + # + # GET /photos/new + # POST /photos + # GET /photos/:id + # GET /photos/:id/edit + # PUT /photos/:id + # DELETE /photos/:id + # + # Resources can also be nested infinitely by using this block syntax: + # + # resources :photos do + # resources :comments + # end + # + # This generates the following comments routes: + # + # GET /photos/:id/comments/new + # POST /photos/:id/comments + # GET /photos/:id/comments/:id + # GET /photos/:id/comments/:id/edit + # PUT /photos/:id/comments/:id + # DELETE /photos/:id/comments/:id + # + # === Options + # Takes same options as <tt>Base#match</tt> as well as: + # + # [:path_names] + # Allows you to change the paths of the seven default actions. + # Paths not specified are not changed. + # + # resources :posts, :path_names => { :new => "brand_new" } + # + # The above example will now change /posts/new to /posts/brand_new + # + # [:only] + # Only generate routes for the given actions. + # + # resources :cows, :only => :show + # resources :cows, :only => [:show, :index] + # + # [:except] + # Generate all routes except for the given actions. + # + # resources :cows, :except => :show + # resources :cows, :except => [:show, :index] + # + # [:shallow] + # Generates shallow routes for nested resource(s). When placed on a parent resource, + # generates shallow routes for all nested resources. + # + # resources :posts, :shallow => true do + # resources :comments + # end + # + # Is the same as: + # + # resources :posts do + # resources :comments + # end + # resources :comments + # + # [:shallow_path] + # Prefixes nested shallow routes with the specified path. + # + # scope :shallow_path => "sekret" do + # resources :posts do + # resources :comments, :shallow => true + # end + # end + # + # The +comments+ resource here will have the following routes generated for it: + # + # post_comments GET /sekret/posts/:post_id/comments(.:format) + # post_comments POST /sekret/posts/:post_id/comments(.:format) + # new_post_comment GET /sekret/posts/:post_id/comments/new(.:format) + # edit_comment GET /sekret/comments/:id/edit(.:format) + # comment GET /sekret/comments/:id(.:format) + # comment PUT /sekret/comments/:id(.:format) + # comment DELETE /sekret/comments/:id(.:format) + # + # === Examples + # + # # routes call Admin::PostsController + # resources :posts, :module => "admin" + # + # # resource actions are at /admin/posts. + # resources :posts, :path => "admin" def resources(*resources, &block) options = resources.extract_options! @@ -606,43 +1115,70 @@ module ActionDispatch resource_scope(Resource.new(resources.pop, options)) do yield if block_given? - collection_scope do + collection do get :index if parent_resource.actions.include?(:index) post :create if parent_resource.actions.include?(:create) end - new_scope do + new do get :new end if parent_resource.actions.include?(:new) - member_scope do + member do + get :edit if parent_resource.actions.include?(:edit) get :show if parent_resource.actions.include?(:show) put :update if parent_resource.actions.include?(:update) delete :destroy if parent_resource.actions.include?(:destroy) - get :edit if parent_resource.actions.include?(:edit) end end self end + # To add a route to the collection: + # + # resources :photos do + # collection do + # get 'search' + # end + # end + # + # This will enable Rails to recognize paths such as <tt>/photos/search</tt> + # with GET, and route to the search action of PhotosController. It will also + # create the <tt>search_photos_url</tt> and <tt>search_photos_path</tt> + # route helpers. def collection - unless @scope[:scope_level] == :resources - raise ArgumentError, "can't use collection outside resources scope" + unless resource_scope? + raise ArgumentError, "can't use collection outside resource(s) scope" end - collection_scope do - yield + with_scope_level(:collection) do + scope(parent_resource.collection_scope) do + yield + end end end + # To add a member route, add a member block into the resource block: + # + # resources :photos do + # member do + # get 'preview' + # end + # end + # + # This will recognize <tt>/photos/1/preview</tt> with GET, and route to the + # preview action of PhotosController. It will also create the + # <tt>preview_photo_url</tt> and <tt>preview_photo_path</tt> helpers. def member unless resource_scope? raise ArgumentError, "can't use member outside resource(s) scope" end - member_scope do - yield + with_scope_level(:member) do + scope(parent_resource.member_scope) do + yield + end end end @@ -651,8 +1187,10 @@ module ActionDispatch raise ArgumentError, "can't use new outside resource(s) scope" end - new_scope do - yield + with_scope_level(:new) do + scope(parent_resource.new_scope(action_path(:new))) do + yield + end end end @@ -678,6 +1216,7 @@ module ActionDispatch end end + # See ActionDispatch::Routing::Mapper::Scoping#namespace def namespace(path, options = {}) if resource_scope? nested { super } @@ -687,7 +1226,7 @@ module ActionDispatch end def shallow - scope(:shallow => true) do + scope(:shallow => true, :shallow_path => @scope[:path]) do yield end end @@ -713,46 +1252,36 @@ module ActionDispatch raise ArgumentError, "Unknown scope #{on.inspect} given to :on" end - if @scope[:scope_level] == :resource + if @scope[:scope_level] == :resources + args.push(options) + return nested { match(*args) } + elsif @scope[:scope_level] == :resource args.push(options) return member { match(*args) } end - path = options.delete(:path) action = args.first + path = path_for_action(action, options.delete(:path)) - if action.is_a?(Symbol) - path = path_for_action(action, path) - options[:to] ||= action - options[:as] = name_for_action(action, options[:as]) - - with_exclusive_scope do - return super(path, options) - end - elsif resource_method_scope? - path = path_for_custom_action - options[:as] = name_for_action(options[:as]) if options[:as] - args.push(options) - - with_exclusive_scope do - scope(path) do - return super - end - end + if action.to_s =~ /^[\w\/]+$/ + options[:action] ||= action unless action.to_s.include?("/") + else + action = nil end - if resource_scope? - raise ArgumentError, "can't define route directly in resource(s) scope" + if options.key?(:as) && !options[:as] + options.delete(:as) + else + options[:as] = name_for_action(options[:as], action) end - args.push(options) - super + super(path, options) end def root(options={}) if @scope[:scope_level] == :resources - with_scope_level(:nested) do - scope(parent_resource.path, :as => parent_resource.collection_name) do + with_scope_level(:root) do + scope(parent_resource.path) do super(options) end end @@ -767,12 +1296,21 @@ module ActionDispatch @scope[:scope_level_resource] end - def apply_common_behavior_for(method, resources, options, &block) + def apply_common_behavior_for(method, resources, options, &block) #:nodoc: if resources.length > 1 resources.each { |r| send(method, r, options, &block) } return true end + if resource_scope? + nested { send(method, resources.pop, options, &block) } + return true + end + + options.keys.each do |k| + (options[:constraints] ||= {})[k] = options.delete(k) if options[k].is_a?(Regexp) + end + scope_options = options.slice!(*RESOURCE_OPTIONS) unless scope_options.empty? scope(scope_options) do @@ -785,33 +1323,26 @@ module ActionDispatch options.merge!(scope_action_options) if scope_action_options? end - if resource_scope? - nested do - send(method, resources.pop, options, &block) - end - return true - end - false end - def action_options?(options) + def action_options?(options) #:nodoc: options[:only] || options[:except] end - def scope_action_options? + def scope_action_options? #:nodoc: @scope[:options].is_a?(Hash) && (@scope[:options][:only] || @scope[:options][:except]) end - def scope_action_options + def scope_action_options #:nodoc: @scope[:options].slice(:only, :except) end - def resource_scope? + def resource_scope? #:nodoc: [:resource, :resources].include?(@scope[:scope_level]) end - def resource_method_scope? + def resource_method_scope? #:nodoc: [:collection, :member, :new].include?(@scope[:scope_level]) end @@ -837,7 +1368,7 @@ module ActionDispatch @scope[:scope_level_resource] = old_resource end - def resource_scope(resource) + def resource_scope(resource) #:nodoc: with_scope_level(resource.is_a?(SingletonResource) ? :resource : :resources, resource) do scope(parent_resource.resource_scope) do yield @@ -845,116 +1376,108 @@ module ActionDispatch end end - def new_scope - with_scope_level(:new) do - scope(parent_resource.new_scope(action_path(:new))) do - yield - end - end - end - - def collection_scope - with_scope_level(:collection) do - scope(parent_resource.collection_scope) do - yield - end - end - end - - def member_scope - with_scope_level(:member) do - scope(parent_resource.member_scope) do - yield - end - end - end - - def nested_options + def nested_options #:nodoc: {}.tap do |options| options[:as] = parent_resource.member_name options[:constraints] = { "#{parent_resource.singular}_id".to_sym => id_constraint } if id_constraint? end end - def id_constraint? - @scope[:id].is_a?(Regexp) || (@scope[:constraints] && @scope[:constraints][:id].is_a?(Regexp)) + def id_constraint? #:nodoc: + @scope[:constraints] && @scope[:constraints][:id].is_a?(Regexp) end - def id_constraint - @scope[:id] || @scope[:constraints][:id] + def id_constraint #:nodoc: + @scope[:constraints][:id] end - def canonical_action?(action, flag) - flag && CANONICAL_ACTIONS.include?(action) + def canonical_action?(action, flag) #:nodoc: + flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s) end - def shallow_scoping? + def shallow_scoping? #:nodoc: shallow? && @scope[:scope_level] == :member end - def path_for_action(action, path) + def path_for_action(action, path) #:nodoc: prefix = shallow_scoping? ? "#{@scope[:shallow_path]}/#{parent_resource.path}/:id" : @scope[:path] - if canonical_action?(action, path.blank?) - "#{prefix}(.:format)" - else - "#{prefix}/#{action_path(action, path)}(.:format)" - end - end - - def path_for_custom_action - if shallow_scoping? - "#{@scope[:shallow_path]}/#{parent_resource.path}/:id" + path = if canonical_action?(action, path.blank?) + prefix.to_s else - @scope[:path] + "#{prefix}/#{action_path(action, path)}" end end - def action_path(name, path = nil) + def action_path(name, path = nil) #:nodoc: path || @scope[:path_names][name.to_sym] || name.to_s end - def prefix_name_for_action(action, as) - if as.present? - "#{as}_" - elsif as - "" + def prefix_name_for_action(as, action) #:nodoc: + if as + as.to_s elsif !canonical_action?(action, @scope[:scope_level]) - "#{action}_" + action.to_s end end - def name_for_action(action, as=nil) - prefix = prefix_name_for_action(action, as) + def name_for_action(as, action) #:nodoc: + prefix = prefix_name_for_action(as, action) + prefix = Mapper.normalize_name(prefix) if prefix name_prefix = @scope[:as] if parent_resource + return nil if as.nil? && action.nil? + collection_name = parent_resource.collection_name member_name = parent_resource.member_name - name_prefix = "#{name_prefix}_" if name_prefix.present? end - case @scope[:scope_level] + name = case @scope[:scope_level] + when :nested + [name_prefix, prefix] when :collection - "#{prefix}#{name_prefix}#{collection_name}" + [prefix, name_prefix, collection_name] when :new - "#{prefix}new_#{name_prefix}#{member_name}" + [prefix, :new, name_prefix, member_name] + when :member + [prefix, shallow_scoping? ? @scope[:shallow_prefix] : name_prefix, member_name] + when :root + [name_prefix, collection_name, prefix] else - if shallow_scoping? - shallow_prefix = "#{@scope[:shallow_prefix]}_" if @scope[:shallow_prefix].present? - "#{prefix}#{shallow_prefix}#{member_name}" - else - "#{prefix}#{name_prefix}#{member_name}" - end + [name_prefix, member_name, prefix] end + + candidate = name.select(&:present?).join("_").presence + candidate unless as.nil? && @set.routes.find { |r| r.name == candidate } + end + end + + module Shorthand #:nodoc: + def match(*args) + if args.size == 1 && args.last.is_a?(Hash) + options = args.pop + path, to = options.find { |name, value| name.is_a?(String) } + options.merge!(:to => to).delete(path) + super(path, options) + else + super end + end + end + + def initialize(set) #:nodoc: + @set = set + @scope = { :path_names => @set.resources_path_names } end include Base include HttpHelpers + include Redirection include Scoping include Resources + include Shorthand end end end diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb index 31dba835ac..82c4fadb50 100644 --- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb +++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb @@ -17,7 +17,7 @@ module ActionDispatch # # == Usage within the framework # - # Polymorphic URL helpers are used in a number of places throughout the Rails framework: + # Polymorphic URL helpers are used in a number of places throughout the \Rails framework: # # * <tt>url_for</tt>, so you can use it with a record as the argument, e.g. # <tt>url_for(@article)</tt>; @@ -42,6 +42,18 @@ module ActionDispatch # # edit_polymorphic_path(@post) # => "/posts/1/edit" # polymorphic_path(@post, :format => :pdf) # => "/posts/1.pdf" + # + # == Using with mounted engines + # + # If you use mounted engine, there is a possibility that you will need to use + # polymorphic_url pointing at engine's routes. To do that, just pass proxy used + # to reach engine's routes as a first argument: + # + # For example: + # + # polymorphic_url([blog, @post]) # it will call blog.post_path(@post) + # form_for([blog, @post]) # => "/blog/posts/1 + # module PolymorphicRoutes # Constructs a call to a named RESTful route for the given record and returns the # resulting URL string. For example: @@ -78,19 +90,20 @@ module ActionDispatch def polymorphic_url(record_or_hash_or_array, options = {}) if record_or_hash_or_array.kind_of?(Array) record_or_hash_or_array = record_or_hash_or_array.compact + if record_or_hash_or_array.first.is_a?(ActionDispatch::Routing::RoutesProxy) + proxy = record_or_hash_or_array.shift + end record_or_hash_or_array = record_or_hash_or_array[0] if record_or_hash_or_array.size == 1 end record = extract_record(record_or_hash_or_array) record = record.to_model if record.respond_to?(:to_model) - args = case record_or_hash_or_array - when Hash; [ record_or_hash_or_array ] - when Array; record_or_hash_or_array.dup - else [ record_or_hash_or_array ] - end + args = Array === record_or_hash_or_array ? + record_or_hash_or_array.dup : + [ record_or_hash_or_array ] - inflection = if options[:action].to_s == "new" + inflection = if options[:action] && options[:action].to_s == "new" args.pop :singular elsif (record.respond_to?(:persisted?) && !record.persisted?) @@ -111,7 +124,14 @@ module ActionDispatch args.last.kind_of?(Hash) ? args.last.merge!(url_options) : args << url_options end - send(named_route, *args) + if proxy + proxy.send(named_route, *args) + else + # we need to use url_for, because polymorphic_url can be used in context of other than + # current routes (e.g. engine's routes). As named routes from engine are not included + # calling engine's named route directly would fail. + url_for _routes.url_helpers.__send__("hash_for_#{named_route}", *args) + end end # Returns the path component of a URL for the given record. It uses @@ -146,31 +166,31 @@ module ActionDispatch end def build_named_route_call(records, inflection, options = {}) - unless records.is_a?(Array) - record = extract_record(records) - route = '' - else + if records.is_a?(Array) record = records.pop - route = records.inject("") do |string, parent| + route = records.map do |parent| if parent.is_a?(Symbol) || parent.is_a?(String) - string << "#{parent}_" + parent else - string << ActiveModel::Naming.plural(parent).singularize - string << "_" + ActiveModel::Naming.route_key(parent).singularize end end + else + record = extract_record(records) + route = [] end if record.is_a?(Symbol) || record.is_a?(String) - route << "#{record}_" + route << record else - route << ActiveModel::Naming.plural(record) - route = route.singularize if inflection == :singular - route << "_" - route << "index_" if ActiveModel::Naming.uncountable?(record) && inflection == :plural + route << ActiveModel::Naming.route_key(record) + route = [route.join("_").singularize] if inflection == :singular + route << "index" if ActiveModel::Naming.uncountable?(record) && inflection == :plural end - action_prefix(options) + route + routing_type(options).to_s + route << routing_type(options) + + action_prefix(options) + route.join("_") end def extract_record(record_or_hash_or_array) diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb new file mode 100644 index 0000000000..804991ad5f --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/redirection.rb @@ -0,0 +1,110 @@ +require 'action_dispatch/http/request' + +module ActionDispatch + module Routing + module Redirection + + # Redirect any path to another path: + # + # match "/stories" => redirect("/posts") + # + # You can also use interpolation in the supplied redirect argument: + # + # match 'docs/:article', :to => redirect('/wiki/%{article}') + # + # Alternatively you can use one of the other syntaxes: + # + # The block version of redirect allows for the easy encapsulation of any logic associated with + # the redirect in question. Either the params and request are supplied as arguments, or just + # params, depending of how many arguments your block accepts. A string is required as a + # return value. + # + # match 'jokes/:number', :to => redirect do |params, request| + # path = (params[:number].to_i.even? ? "/wheres-the-beef" : "/i-love-lamp") + # "http://#{request.host_with_port}/#{path}" + # end + # + # The options version of redirect allows you to supply only the parts of the url which need + # to change, it also supports interpolation of the path similar to the first example. + # + # match 'stores/:name', :to => redirect(:subdomain => 'stores', :path => '/%{name}') + # match 'stores/:name(*all)', :to => redirect(:subdomain => 'stores', :path => '/%{name}%{all}') + # + # Finally, an object which responds to call can be supplied to redirect, allowing you to reuse + # common redirect routes. The call method must accept two arguments, params and request, and return + # a string. + # + # match 'accounts/:name' => redirect(SubdomainRedirector.new('api')) + # + def redirect(*args, &block) + options = args.last.is_a?(Hash) ? args.pop : {} + status = options.delete(:status) || 301 + + path = args.shift + + path_proc = if path.is_a?(String) + proc { |params| (params.empty? || !path.match(/%\{\w*\}/)) ? path : (path % params) } + elsif options.any? + options_proc(options) + elsif path.respond_to?(:call) + proc { |params, request| path.call(params, request) } + elsif block + block + else + raise ArgumentError, "redirection argument not supported" + end + + redirection_proc(status, path_proc) + end + + private + + def options_proc(options) + proc do |params, request| + path = if options[:path].nil? + request.path + elsif params.empty? || !options[:path].match(/%\{\w*\}/) + options.delete(:path) + else + (options.delete(:path) % params) + end + + default_options = { + :protocol => request.protocol, + :host => request.host, + :port => request.optional_port, + :path => path, + :params => request.query_parameters + } + + ActionDispatch::Http::URL.url_for(options.reverse_merge(default_options)) + end + end + + def redirection_proc(status, path_proc) + lambda do |env| + req = Request.new(env) + + params = [req.symbolized_path_parameters] + params << req if path_proc.arity > 1 + + uri = URI.parse(path_proc.call(*params)) + uri.scheme ||= req.scheme + uri.host ||= req.host + uri.port ||= req.port unless req.standard_port? + + body = %(<html><body>You are being <a href="#{ERB::Util.h(uri.to_s)}">redirected</a>.</body></html>) + + headers = { + 'Location' => uri.to_s, + 'Content-Type' => 'text/html', + 'Content-Length' => body.length.to_s + } + + [ status, headers, [body] ] + end + end + + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_dispatch/routing/route.rb b/actionpack/lib/action_dispatch/routing/route.rb index aefebf8f80..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,24 +25,22 @@ module ActionDispatch conditions[:path_info] = ::Rack::Mount::Strexp.compile(path, requirements, SEPARATORS, anchor) end - @conditions = conditions.inject({}) { |h, (k, v)| - h[k] = Rack::Mount::RegexpWithNamedGroups.new(v) - h - } + @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 - method.source.upcase - else - method.to_s.upcase - end - end + @verbs.join '|' end def segment_keys @@ -48,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 d23b580d97..b28f6c2297 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -1,7 +1,8 @@ require 'rack/mount' require 'forwardable' +require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/to_query' -require 'action_dispatch/routing/deprecated_mapper' +require 'active_support/core_ext/hash/slice' module ActionDispatch module Routing @@ -50,12 +51,13 @@ module ActionDispatch private def controller_reference(controller_param) + controller_name = "#{controller_param.camelize}Controller" + unless controller = @controllers[controller_param] - controller_name = "#{controller_param.camelize}Controller" controller = @controllers[controller_param] = - ActiveSupport::Dependencies.ref(controller_name) + ActiveSupport::Dependencies.reference(controller_name) end - controller.get + controller.get(controller_name) end def dispatch(controller, action, env) @@ -67,7 +69,7 @@ module ActionDispatch end def split_glob_param!(params) - params[@glob_param] = params[@glob_param].split('/').map { |v| URI.unescape(v) } + params[@glob_param] = params[@glob_param].split('/').map { |v| URI.parser.unescape(v) } end end @@ -158,10 +160,18 @@ module ActionDispatch # We use module_eval to avoid leaks @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1 - def #{selector}(options = nil) # def hash_for_users_url(options = nil) - options ? #{options.inspect}.merge(options) : #{options.inspect} # options ? {:only_path=>false}.merge(options) : {:only_path=>false} - end # end - protected :#{selector} # protected :hash_for_users_url + remove_method :#{selector} if method_defined?(:#{selector}) + def #{selector}(*args) + options = args.extract_options! + + if args.any? + options[:_positional_args] = args + options[:_positional_keys] = #{route.segment_keys.inspect} + end + + options ? #{options.inspect}.merge(options) : #{options.inspect} + end + protected :#{selector} END_EVAL helpers << selector end @@ -184,22 +194,16 @@ module ActionDispatch hash_access_method = hash_access_name(name, kind) @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1 + remove_method :#{selector} if method_defined?(:#{selector}) def #{selector}(*args) - options = #{hash_access_method}(args.extract_options!) - - if args.any? - options[:_positional_args] = args - options[:_positional_keys] = #{route.segment_keys.inspect} - end - - url_for(options) + url_for(#{hash_access_method}(*args)) end END_EVAL helpers << selector end end - attr_accessor :set, :routes, :named_routes + attr_accessor :set, :routes, :named_routes, :default_scope attr_accessor :disable_clear_and_finalize, :resources_path_names attr_accessor :default_url_options, :request_class, :valid_conditions @@ -211,7 +215,6 @@ module ActionDispatch self.routes = [] self.named_routes = NamedRouteCollection.new self.resources_path_names = self.class.default_resources_path_names.dup - self.controller_namespaces = Set.new self.default_url_options = {} self.request_class = request_class @@ -219,27 +222,35 @@ module ActionDispatch self.valid_conditions.delete(:id) self.valid_conditions.push(:controller, :action) + @append = [] @disable_clear_and_finalize = false clear! end def draw(&block) clear! unless @disable_clear_and_finalize + eval_block(block) + finalize! unless @disable_clear_and_finalize + nil + end + + def append(&block) + @append << block + end + + def eval_block(block) mapper = Mapper.new(self) - if block.arity == 1 - mapper.instance_exec(DeprecatedMapper.new(self), &block) + if default_scope + mapper.with_default_scope(default_scope, &block) else mapper.instance_exec(&block) end - - finalize! unless @disable_clear_and_finalize - - nil end def finalize! return if @finalized + @append.each { |blk| eval_block(blk) } @finalized = true @set.freeze end @@ -261,6 +272,31 @@ module ActionDispatch named_routes.install(destinations, regenerate_code) end + module MountedHelpers + end + + def mounted_helpers(name = :main_app) + define_mounted_helper(name) if name + MountedHelpers + end + + def define_mounted_helper(name) + return if MountedHelpers.method_defined?(name) + + routes = self + MountedHelpers.class_eval do + define_method "_#{name}" do + RoutesProxy.new(routes, self._routes_context) + end + end + + MountedHelpers.class_eval <<-RUBY + def #{name} + @#{name} ||= _#{name} + end + RUBY + end + def url_helpers @url_helpers ||= begin routes = self @@ -269,9 +305,9 @@ module ActionDispatch extend ActiveSupport::Concern include UrlFor - @routes = routes + @_routes = routes class << self - delegate :url_for, :to => '@routes' + delegate :url_for, :to => '@_routes' end extend routes.named_routes.module @@ -280,10 +316,10 @@ module ActionDispatch # Yes plz - JP included do routes.install_helpers(self) - singleton_class.send(:define_method, :_routes) { routes } + singleton_class.send(:redefine_method, :_routes) { routes } end - define_method(:_routes) { routes } + define_method(:_routes) { @_routes || routes } end helpers @@ -295,18 +331,31 @@ 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 end class Generator #:nodoc: - attr_reader :options, :recall, :set, :script_name, :named_route + PARAMETERIZE = { + :parameterize => lambda do |name, value| + if name == :controller + value + elsif value.is_a?(Array) + value.map { |v| Rack::Mount::Utils.escape_uri(v.to_param) }.join('/') + else + return nil unless param = value.to_param + param.split('/').map { |v| Rack::Mount::Utils.escape_uri(v) }.join("/") + end + end + } + + attr_reader :options, :recall, :set, :named_route def initialize(options, recall, set, extras = false) - @script_name = options.delete(:script_name) @named_route = options.delete(:use_route) @options = options.dup @recall = recall.dup @@ -392,36 +441,19 @@ module ActionDispatch end def generate - path, params = @set.set.generate(:path_info, named_route, options, recall, opts) + path, params = @set.set.generate(:path_info, named_route, options, recall, PARAMETERIZE) raise_routing_error unless path - params.reject! {|k,v| !v } - return [path, params.keys] if @extras - path << "?#{params.to_query}" if params.any? - "#{script_name}#{path}" + [path, params] rescue Rack::Mount::RoutingError raise_routing_error end - def opts - parameterize = lambda do |name, value| - if name == :controller - value - elsif value.is_a?(Array) - value.map { |v| Rack::Mount::Utils.escape_uri(v.to_param) }.join('/') - else - return nil unless param = value.to_param - param.split('/').map { |v| Rack::Mount::Utils.escape_uri(v) }.join("/") - end - end - {:parameterize => parameterize} - end - def raise_routing_error - raise ActionController::RoutingError.new("No route matches #{options.inspect}") + raise ActionController::RoutingError, "No route matches #{options.inspect}" end def different_controller? @@ -453,7 +485,12 @@ module ActionDispatch Generator.new(options, recall, self, extras).generate end - RESERVED_OPTIONS = [:anchor, :params, :only_path, :host, :protocol, :port, :trailing_slash] + RESERVED_OPTIONS = [:host, :protocol, :port, :subdomain, :domain, :tld_length, + :trailing_slash, :anchor, :params, :only_path, :script_name] + + def _generate_prefix(options = {}) + nil + end def url_for(options) finalize! @@ -461,30 +498,24 @@ module ActionDispatch handle_positional_args(options) - rewritten_url = "" - - path_segments = options.delete(:_path_segments) - - unless options[:only_path] - rewritten_url << (options[:protocol] || "http") - rewritten_url << "://" unless rewritten_url.match("://") - rewritten_url << rewrite_authentication(options) - - raise "Missing host to link to! Please provide :host parameter or set default_url_options[:host]" unless options[:host] + user, password = extract_authentication(options) + path_segments = options.delete(:_path_segments) + script_name = options.delete(:script_name) - rewritten_url << options[:host] - rewritten_url << ":#{options.delete(:port)}" if options.key?(:port) - end + path = (script_name.blank? ? _generate_prefix(options) : script_name.chomp('/')).to_s path_options = options.except(*RESERVED_OPTIONS) path_options = yield(path_options) if block_given? - path = generate(path_options, path_segments || {}) - # ROUTES TODO: This can be called directly, so script_name should probably be set in the routes - rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path) - rewritten_url << "##{Rack::Mount::Utils.escape_uri(options[:anchor].to_param.to_s)}" if options[:anchor] + path_addition, params = generate(path_options, path_segments || {}) + path << path_addition - rewritten_url + ActionDispatch::Http::URL.url_for(options.merge({ + :path => path, + :params => params, + :user => user, + :password => password + })) end def call(env) @@ -494,7 +525,7 @@ module ActionDispatch def recognize_path(path, environment = {}) method = (environment[:method] || "GET").to_s.upcase - path = Rack::Mount::Utils.normalize_path(path) + path = Rack::Mount::Utils.normalize_path(path) unless path =~ %r{://} begin env = Rack::MockRequest.env_for(path, {:method => method}) @@ -502,17 +533,19 @@ module ActionDispatch raise ActionController::RoutingError, e.message end - req = Rack::Request.new(env) + req = @request_class.new(env) @set.recognize(req) do |route, matches, params| params.each do |key, value| if value.is_a?(String) value = value.dup.force_encoding(Encoding::BINARY) if value.encoding_aware? - params[key] = URI.unescape(value) + params[key] = URI.parser.unescape(value) end end dispatcher = route.app - dispatcher = dispatcher.app while dispatcher.is_a?(Mapper::Constraints) + while dispatcher.is_a?(Mapper::Constraints) && dispatcher.matches?(env) do + dispatcher = dispatcher.app + end if dispatcher.is_a?(Dispatcher) && dispatcher.controller(params, false) dispatcher.prepare_params!(params) @@ -524,28 +557,25 @@ module ActionDispatch end private + + def extract_authentication(options) + if options[:user] && options[:password] + [options.delete(:user), options.delete(:password)] + else + nil + end + end + def handle_positional_args(options) return unless args = options.delete(:_positional_args) keys = options.delete(:_positional_keys) keys -= options.keys if args.size < keys.size - 1 # take format into account - args = args.zip(keys).inject({}) do |h, (v, k)| - h[k] = v - h - end - # Tell url_for to skip default_url_options - options.merge!(args) + options.merge!(Hash[args.zip(keys).map { |v, k| [k, v] }]) end - def rewrite_authentication(options) - if options[:user] && options[:password] - "#{Rack::Utils.escape(options.delete(:user))}:#{Rack::Utils.escape(options.delete(:password))}@" - else - "" - end - end end end end diff --git a/actionpack/lib/action_dispatch/routing/routes_proxy.rb b/actionpack/lib/action_dispatch/routing/routes_proxy.rb new file mode 100644 index 0000000000..f7d5f6397d --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/routes_proxy.rb @@ -0,0 +1,35 @@ +module ActionDispatch + module Routing + class RoutesProxy #:nodoc: + include ActionDispatch::Routing::UrlFor + + attr_accessor :scope, :routes + alias :_routes :routes + + def initialize(routes, scope) + @routes, @scope = routes, scope + end + + def url_options + scope.send(:_with_routes, routes) do + scope.url_options + end + end + + def method_missing(method, *args) + if routes.url_helpers.respond_to?(method) + self.class.class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{method}(*args) + options = args.extract_options! + args << url_options.merge((options || {}).symbolize_keys) + routes.url_helpers.#{method}(*args) + end + RUBY + send(method, *args) + else + super + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index ba93ff8630..d4db78a25a 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -1,6 +1,6 @@ module ActionDispatch module Routing - # In <b>routes.rb</b> one defines URL-to-controller mappings, but the reverse + # In <tt>config/routes.rb</tt> you define URL-to-controller mappings, but the reverse # is also possible: an URL can be generated from one of your routing definitions. # URL generation functionality is centralized in this module. # @@ -12,15 +12,14 @@ module ActionDispatch # # == URL generation from parameters # - # As you may know, some functions - such as ActionController::Base#url_for + # As you may know, some functions, such as ActionController::Base#url_for # and ActionView::Helpers::UrlHelper#link_to, can generate URLs given a set # of parameters. For example, you've probably had the chance to write code # like this in one of your views: # # <%= link_to('Click here', :controller => 'users', # :action => 'new', :message => 'Welcome!') %> - # - # # Generates a link to /users/new?message=Welcome%21 + # # => "/users/new?message=Welcome%21" # # link_to, and all other functions that require URL generation functionality, # actually use ActionController::UrlFor under the hood. And in particular, @@ -61,7 +60,7 @@ module ActionDispatch # # UrlFor also allows one to access methods that have been auto-generated from # named routes. For example, suppose that you have a 'users' resource in your - # <b>routes.rb</b>: + # <tt>config/routes.rb</tt>: # # resources :users # @@ -99,6 +98,11 @@ module ActionDispatch end end + def initialize(*) + @_routes = nil + super + end + def url_options default_url_options end @@ -111,6 +115,13 @@ module ActionDispatch # * <tt>:host</tt> - Specifies the host the link should be targeted at. # If <tt>:only_path</tt> is false, this option must be # provided either explicitly, or via +default_url_options+. + # * <tt>:subdomain</tt> - Specifies the subdomain of the link, using the +tld_length+ + # to split the domain from the host. + # * <tt>:domain</tt> - Specifies the domain of the link, using the +tld_length+ + # to split the subdomain from the host. + # * <tt>:tld_length</tt> - Number of labels the TLD id composed of, only used if + # <tt>:subdomain</tt> or <tt>:domain</tt> are supplied. Defaults to + # <tt>ActionDispatch::Http::URL.tld_length</tt>, which in turn defaults to 1. # * <tt>:port</tt> - Optionally specify the port to connect to. # * <tt>:anchor</tt> - An anchor name to be appended to the path. # * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2009/" @@ -134,6 +145,18 @@ module ActionDispatch polymorphic_url(options) end end + + protected + def _with_routes(routes) + old_routes, @_routes = @_routes, routes + yield + ensure + @_routes = old_routes + end + + def _routes_context + self + end end end end diff --git a/actionpack/lib/action_dispatch/testing/assertions/dom.rb b/actionpack/lib/action_dispatch/testing/assertions/dom.rb index 9c215de743..47c84742aa 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/dom.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/dom.rb @@ -3,7 +3,7 @@ require 'action_controller/vendor/html-scanner' module ActionDispatch module Assertions module DomAssertions - # Test two HTML strings for equivalency (e.g., identical up to reordering of attributes) + # \Test two HTML strings for equivalency (e.g., identical up to reordering of attributes) # # ==== Examples # diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb index ec5e9efe44..77a15f3e97 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/response.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb @@ -1,6 +1,6 @@ module ActionDispatch module Assertions - # A small suite of assertions that test responses from Rails applications. + # A small suite of assertions that test responses from \Rails applications. module ResponseAssertions extend ActiveSupport::Concern @@ -18,9 +18,9 @@ module ActionDispatch # * <tt>:missing</tt> - Status code was 404 # * <tt>:error</tt> - Status code was in the 500-599 range # - # You can also pass an explicit status number like assert_response(501) - # or its symbolic equivalent assert_response(:not_implemented). - # See ActionDispatch::StatusCodes for a full list. + # You can also pass an explicit status number like <tt>assert_response(501)</tt> + # or its symbolic equivalent <tt>assert_response(:not_implemented)</tt>. + # See Rack::Utils::SYMBOL_TO_STATUS_CODE for a full list. # # ==== Examples # @@ -45,8 +45,8 @@ module ActionDispatch end # Assert that the redirection options passed in match those of the redirect called in the latest action. - # This match can be partial, such that assert_redirected_to(:controller => "weblog") will also - # match the redirection of redirect_to(:controller => "weblog", :action => "show") and so on. + # This match can be partial, such that <tt>assert_redirected_to(:controller => "weblog")</tt> will also + # match the redirection of <tt>redirect_to(:controller => "weblog", :action => "show")</tt> and so on. # # ==== Examples # @@ -81,14 +81,10 @@ module ActionDispatch def normalize_argument_to_redirection(fragment) case fragment - when %r{^\w[\w\d+.-]*:.*} + when %r{^\w[A-Za-z\d+.-]*:.*} fragment when String - if fragment =~ %r{^\w[\w\d+.-]*:.*} - fragment - else - @request.protocol + @request.host_with_port + fragment - end + @request.protocol + @request.host_with_port + fragment when :back raise RedirectBackError unless refer = @request.headers["Referer"] refer diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb index 9338fa9e70..11e8c63fa0 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb @@ -1,12 +1,13 @@ +require 'uri' require 'active_support/core_ext/hash/diff' require 'active_support/core_ext/hash/indifferent_access' module ActionDispatch module Assertions - # Suite of assertions to test routes generated by Rails and the handling of requests made to them. + # Suite of assertions to test routes generated by \Rails and the handling of requests made to them. module RoutingAssertions # Asserts that the routing of the given +path+ was handled correctly and that the parsed options (given in the +expected_options+ hash) - # match +path+. Basically, it asserts that Rails recognizes the route given by +expected_options+. + # match +path+. Basically, it asserts that \Rails recognizes the route given by +expected_options+. # # Pass a hash in the second argument (+path+) to specify the request method. This is useful for routes # requiring a specific HTTP method. The hash should contain a :path with the incoming request path @@ -36,18 +37,8 @@ module ActionDispatch # # # Test a custom route # assert_recognizes({:controller => 'items', :action => 'show', :id => '1'}, 'view/item1') - # - # # Check a Simply RESTful generated route - # assert_recognizes list_items_url, 'items/list' def assert_recognizes(expected_options, path, extras={}, message=nil) - if path.is_a? Hash - request_method = path[:method] - path = path[:path] - else - request_method = nil - end - - request = recognized_request_for(path, request_method) + request = recognized_request_for(path) expected_options = expected_options.clone extras.each_key { |key| expected_options.delete key } unless extras.nil? @@ -77,7 +68,16 @@ module ActionDispatch # # Asserts that the generated route gives us our custom route # assert_generates "changesets/12", { :controller => 'scm', :action => 'show_diff', :revision => "12" } def assert_generates(expected_path, options, defaults={}, extras = {}, message=nil) - expected_path = "/#{expected_path}" unless expected_path[0] == ?/ + if expected_path =~ %r{://} + begin + uri = URI.parse(expected_path) + expected_path = uri.path.to_s.empty? ? "/" : uri.path + rescue URI::InvalidURIError => e + raise ActionController::RoutingError, e.message + end + else + expected_path = "/#{expected_path}" unless expected_path.first == '/' + end # Load routes.rb if it hasn't been loaded. generated_path, extra_keys = @routes.generate_extras(options, defaults) @@ -121,7 +121,8 @@ module ActionDispatch options[:controller] = "/#{controller}" end - assert_generates(path.is_a?(Hash) ? path[:path] : path, options, defaults, extras, message) + generate_options = options.dup.delete_if{ |k,v| defaults.key?(k) } + assert_generates(path.is_a?(Hash) ? path[:path] : path, generate_options, defaults, extras, message) end # A helper to make it easier to test different route configurations. @@ -143,16 +144,16 @@ module ActionDispatch # def with_routing old_routes, @routes = @routes, ActionDispatch::Routing::RouteSet.new - old_controller, @controller = @controller, @controller.clone if @controller - _routes = @routes - - # 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 + if defined?(@controller) && @controller + old_controller, @controller = @controller, @controller.clone + _routes = @routes + + # 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 @controller.singleton_class.send(:include, _routes.url_helpers) @controller.view_context_class = Class.new(@controller.view_context_class) do include _routes.url_helpers @@ -161,14 +162,14 @@ module ActionDispatch yield @routes ensure @routes = old_routes - if @controller + if defined?(@controller) && @controller @controller = old_controller end end # ROUTES TODO: These assertions should really work in an integration context def method_missing(selector, *args, &block) - if @controller && @routes && @routes.named_routes.helpers.include?(selector) + if defined?(@controller) && @controller && @routes && @routes.named_routes.helpers.include?(selector) @controller.send(selector, *args, &block) else super @@ -177,15 +178,35 @@ module ActionDispatch private # Recognizes the route for a given path. - def recognized_request_for(path, request_method = nil) - path = "/#{path}" unless path.first == '/' + def recognized_request_for(path) + if path.is_a?(Hash) + method = path[:method] + path = path[:path] + else + method = :get + end # Assume given controller request = ActionController::TestRequest.new - request.env["REQUEST_METHOD"] = request_method.to_s.upcase if request_method - request.path = path - params = @routes.recognize_path(path, { :method => request.method }) + if path =~ %r{://} + begin + uri = URI.parse(path) + request.env["rack.url_scheme"] = uri.scheme || "http" + request.host = uri.host if uri.host + request.port = uri.port if uri.port + request.path = uri.path.to_s.empty? ? "/" : uri.path + rescue URI::InvalidURIError => e + raise ActionController::RoutingError, e.message + end + else + path = "/#{path}" unless path.first == "/" + request.path = path + end + + request.request_method = method if method + + params = @routes.recognize_path(path, { :method => method }) request.path_parameters = params.with_indifferent_access request diff --git a/actionpack/lib/action_dispatch/testing/assertions/selector.rb b/actionpack/lib/action_dispatch/testing/assertions/selector.rb index 2fc9e2b7d6..2b862fb7d6 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/selector.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/selector.rb @@ -24,10 +24,6 @@ module ActionDispatch # # Also see HTML::Selector to learn how to use selectors. module SelectorAssertions - # :call-seq: - # css_select(selector) => array - # css_select(element, selector) => array - # # Select and return all matching elements. # # If called with a single argument, uses that argument as a selector @@ -71,7 +67,7 @@ module ActionDispatch arg = args.shift elsif arg == nil raise ArgumentError, "First argument is either selector or element to select, but nil found. Perhaps you called assert_select with an element that does not exist?" - elsif @selected + elsif defined?(@selected) && @selected matches = [] @selected.each do |selected| @@ -99,10 +95,6 @@ module ActionDispatch selector.select(root) end - # :call-seq: - # assert_select(selector, equality?, message?) - # assert_select(element, selector, equality?, message?) - # # An assertion that selects elements and makes one or more equality tests. # # If the first argument is an element, selects all matching elements @@ -195,6 +187,7 @@ module ActionDispatch def assert_select(*args, &block) # Start with optional element followed by mandatory selector. arg = args.shift + @selected ||= nil if arg.is_a?(HTML::Node) # First argument is a node (tag or text, but also HTML root), @@ -332,11 +325,6 @@ module ActionDispatch end end - # :call-seq: - # assert_select_rjs(id?) { |elements| ... } - # assert_select_rjs(statement, id?) { |elements| ... } - # assert_select_rjs(:insert, position, id?) { |elements| ... } - # # Selects content from the RJS response. # # === Narrowing down @@ -455,6 +443,7 @@ module ActionDispatch assert_block("") { true } # to count the assertion if block_given? && !([:remove, :show, :hide, :toggle].include? rjs_type) begin + @selected ||= nil in_scope, @selected = @selected, matches yield matches ensure @@ -474,9 +463,6 @@ module ActionDispatch end end - # :call-seq: - # assert_select_encoded(element?) { |elements| ... } - # # Extracts the content of an element, treats it as encoded HTML and runs # nested assertion on it. # @@ -529,8 +515,8 @@ module ActionDispatch node.content.gsub(/<!\[CDATA\[(.*)(\]\]>)?/m) { Rack::Utils.escapeHTML($1) } end - selected = elements.map do |element| - text = element.children.select{ |c| not c.tag? }.map{ |c| fix_content[c] }.join + selected = elements.map do |_element| + text = _element.children.select{ |c| not c.tag? }.map{ |c| fix_content[c] }.join root = HTML::Document.new(CGI.unescapeHTML("<encoded>#{text}</encoded>")).root css_select(root, "encoded:root", &block)[0] end @@ -543,9 +529,6 @@ module ActionDispatch end end - # :call-seq: - # assert_select_email { } - # # Extracts the body of an email and runs nested assertions on it. # # You must enable deliveries for this assertion to work, use: diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index b52795c575..5c6416a19e 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -115,8 +115,8 @@ module ActionDispatch end end - # An integration Session instance represents a set of requests and responses - # performed sequentially by some virtual user. Because you can instantiate + # An instance of this class represents a set of requests and responses + # performed sequentially by a test process. Because you can instantiate # multiple sessions and run them side-by-side, you can also mimic (to some # limited extent) multiple simultaneous users interacting with your system. # @@ -171,6 +171,7 @@ module ActionDispatch # Create and initialize a new Session instance. def initialize(app) + super() @app = app # If the app is a Rails app, make url_helpers available on the session @@ -182,6 +183,7 @@ module ActionDispatch reset! end + remove_method :default_url_options def default_url_options { :host => host, :protocol => https? ? "https" : "http" } end @@ -233,9 +235,7 @@ module ActionDispatch # Set the host name to use in the next request. # # session.host! "www.example.com" - def host!(name) - @host = name - end + alias :host! :host= private def _mock_session @@ -257,12 +257,14 @@ module ActionDispatch end end + hostname, port = host.split(':') + env = { :method => method, :params => parameters, - "SERVER_NAME" => host.split(':')[0], - "SERVER_PORT" => (https? ? "443" : "80"), + "SERVER_NAME" => hostname, + "SERVER_PORT" => port || (https? ? "443" : "80"), "HTTPS" => https? ? "on" : "off", "rack.url_scheme" => https? ? "https" : "http", @@ -305,7 +307,7 @@ module ActionDispatch include ActionDispatch::Assertions def app - @app + @app ||= nil end # Reset the current session. This is useful for testing multiple sessions @@ -317,10 +319,10 @@ module ActionDispatch %w(get post put head delete cookies assigns xml_http_request xhr get_via_redirect post_via_redirect).each do |method| define_method(method) do |*args| - reset! unless @integration_session + reset! unless integration_session # reset the html_document variable, but only for new get/post calls @html_document = nil unless %w(cookies assigns).include?(method) - @integration_session.__send__(method, *args).tap do + integration_session.__send__(method, *args).tap do copy_session_variables! end end @@ -345,7 +347,7 @@ module ActionDispatch # Copy the instance variables from the current session instance into the # test instance. def copy_session_variables! #:nodoc: - return unless @integration_session + return unless integration_session %w(controller response request).each do |var| instance_variable_set("@#{var}", @integration_session.__send__(var)) end @@ -355,35 +357,44 @@ module ActionDispatch include ActionDispatch::Routing::UrlFor def url_options - reset! unless @integration_session - @integration_session.url_options + reset! unless integration_session + integration_session.url_options + end + + def respond_to?(method, include_private = false) + integration_session.respond_to?(method, include_private) || super end # Delegate unhandled messages to the current session instance. def method_missing(sym, *args, &block) - reset! unless @integration_session - if @integration_session.respond_to?(sym) - @integration_session.__send__(sym, *args, &block).tap do + reset! unless integration_session + if integration_session.respond_to?(sym) + integration_session.__send__(sym, *args, &block).tap do copy_session_variables! end else super end end + + private + def integration_session + @integration_session ||= nil + end end end - # An IntegrationTest is one that spans multiple controllers and actions, + # An test that spans multiple controllers and actions, # tying them all together to ensure they work together as expected. It tests # more completely than either unit or functional tests do, exercising the # entire stack, from the dispatcher to the database. # - # At its simplest, you simply extend IntegrationTest and write your tests + # At its simplest, you simply extend <tt>IntegrationTest</tt> and write your tests # using the get/post methods: # # require "test_helper" # - # class ExampleTest < ActionController::IntegrationTest + # class ExampleTest < ActionDispatch::IntegrationTest # fixtures :people # # def test_login @@ -403,11 +414,11 @@ module ActionDispatch # However, you can also have multiple session instances open per test, and # even extend those instances with assertions and methods to create a very # powerful testing DSL that is specific for your application. You can even - # reference any named routes you happen to have defined! + # reference any named routes you happen to have defined. # # require "test_helper" # - # class AdvancedTest < ActionController::IntegrationTest + # class AdvancedTest < ActionDispatch::IntegrationTest # fixtures :people, :rooms # # def test_login_and_speak diff --git a/actionpack/lib/action_dispatch/testing/performance_test.rb b/actionpack/lib/action_dispatch/testing/performance_test.rb index 33a5c68b9d..e7aeb45fb3 100644 --- a/actionpack/lib/action_dispatch/testing/performance_test.rb +++ b/actionpack/lib/action_dispatch/testing/performance_test.rb @@ -1,5 +1,4 @@ require 'active_support/testing/performance' -require 'active_support/testing/default' begin module ActionDispatch @@ -11,9 +10,8 @@ begin # formats are written, so you'll have two output files per test method. class PerformanceTest < ActionDispatch::IntegrationTest include ActiveSupport::Testing::Performance - include ActiveSupport::Testing::Default end end rescue NameError $stderr.puts "Specify ruby-prof as application's dependency in Gemfile to run benchmarks." -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_dispatch/testing/test_process.rb b/actionpack/lib/action_dispatch/testing/test_process.rb index c56ebc6438..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 - @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 b3e67f6e36..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 @@ -10,9 +11,10 @@ module ActionDispatch end def initialize(env = {}) - env = Rails.application.env_defaults.merge(env) if defined?(Rails.application) + env = Rails.application.env_config.merge(env) if defined?(Rails.application) super(DEFAULT_ENV.merge(env)) + @cookies = nil self.host = 'test.host' self.remote_addr = '0.0.0.0' self.user_agent = 'Rails Testing' @@ -66,7 +68,7 @@ module ActionDispatch def accept=(mime_types) @env.delete('action_dispatch.request.accepts') - @env['HTTP_ACCEPT'] = Array(mime_types).collect { |mime_types| mime_types.to_s }.join(",") + @env['HTTP_ACCEPT'] = Array(mime_types).collect { |mime_type| mime_type.to_s }.join(",") end def cookies @@ -76,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_dispatch/testing/test_response.rb b/actionpack/lib/action_dispatch/testing/test_response.rb index 44fb1bde99..82039e72e7 100644 --- a/actionpack/lib/action_dispatch/testing/test_response.rb +++ b/actionpack/lib/action_dispatch/testing/test_response.rb @@ -14,123 +14,16 @@ module ActionDispatch end end - module DeprecatedHelpers - def template - ActiveSupport::Deprecation.warn("response.template has been deprecated. Use controller.template instead", caller) - @template - end - attr_writer :template - - def session - ActiveSupport::Deprecation.warn("response.session has been deprecated. Use request.session instead", caller) - @request.session - end - - def assigns - ActiveSupport::Deprecation.warn("response.assigns has been deprecated. Use controller.assigns instead", caller) - @template.controller.assigns - end - - def layout - ActiveSupport::Deprecation.warn("response.layout has been deprecated. Use template.layout instead", caller) - @template.layout - end - - def redirected_to - ::ActiveSupport::Deprecation.warn("response.redirected_to is deprecated. Use response.redirect_url instead", caller) - redirect_url - end - - def redirect_url_match?(pattern) - ::ActiveSupport::Deprecation.warn("response.redirect_url_match? is deprecated. Use assert_match(/foo/, response.redirect_url) instead", caller) - return false if redirect_url.nil? - p = Regexp.new(pattern) if pattern.class == String - p = pattern if pattern.class == Regexp - return false if p.nil? - p.match(redirect_url) != nil - end - - # Returns the template of the file which was used to - # render this response (or nil) - def rendered - ActiveSupport::Deprecation.warn("response.rendered has been deprecated. Use template.rendered instead", caller) - @template.instance_variable_get(:@_rendered) - end - - # A shortcut to the flash. Returns an empty hash if no session flash exists. - def flash - ActiveSupport::Deprecation.warn("response.flash has been deprecated. Use request.flash instead", caller) - request.session['flash'] || {} - end - - # Do we have a flash? - def has_flash? - ActiveSupport::Deprecation.warn("response.has_flash? has been deprecated. Use flash.any? instead", caller) - !flash.empty? - end - - # Do we have a flash that has contents? - def has_flash_with_contents? - ActiveSupport::Deprecation.warn("response.has_flash_with_contents? has been deprecated. Use flash.any? instead", caller) - !flash.empty? - end - - # Does the specified flash object exist? - def has_flash_object?(name=nil) - ActiveSupport::Deprecation.warn("response.has_flash_object? has been deprecated. Use flash[name] instead", caller) - !flash[name].nil? - end - - # Does the specified object exist in the session? - def has_session_object?(name=nil) - ActiveSupport::Deprecation.warn("response.has_session_object? has been deprecated. Use session[name] instead", caller) - !session[name].nil? - end - - # A shortcut to the template.assigns - def template_objects - ActiveSupport::Deprecation.warn("response.template_objects has been deprecated. Use template.assigns instead", caller) - @template.assigns || {} - end - - # Does the specified template object exist? - def has_template_object?(name=nil) - ActiveSupport::Deprecation.warn("response.has_template_object? has been deprecated. Use tempate.assigns[name].nil? instead", caller) - !template_objects[name].nil? - end - - # Returns binary content (downloadable file), converted to a String - def binary_content - ActiveSupport::Deprecation.warn("response.binary_content has been deprecated. Use response.body instead", caller) - body - end - end - include DeprecatedHelpers - # Was the response successful? - def success? - (200..299).include?(response_code) - end + alias_method :success?, :successful? # Was the URL not found? - def missing? - response_code == 404 - end + alias_method :missing?, :not_found? # Were we redirected? - def redirect? - (300..399).include?(response_code) - end + alias_method :redirect?, :redirection? # Was there a server-side error? - def error? - (500..599).include?(response_code) - end - alias_method :server_error?, :error? - - # Was there a client client? - def client_error? - (400..499).include?(response_code) - end + alias_method :error?, :server_error? end end diff --git a/actionpack/lib/action_pack.rb b/actionpack/lib/action_pack.rb index 1a1497385a..914b13dbfb 100644 --- a/actionpack/lib/action_pack.rb +++ b/actionpack/lib/action_pack.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2010 David Heinemeier Hansson +# Copyright (c) 2004-2011 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/actionpack/lib/action_pack/version.rb b/actionpack/lib/action_pack/version.rb index 7eaf7d0534..170ceb299a 100644 --- a/actionpack/lib/action_pack/version.rb +++ b/actionpack/lib/action_pack/version.rb @@ -1,10 +1,10 @@ module ActionPack module VERSION #:nodoc: MAJOR = 3 - MINOR = 0 + MINOR = 1 TINY = 0 - BUILD = "rc" + PRE = "beta" - STRING = [MAJOR, MINOR, TINY, BUILD].join('.') + STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end end diff --git a/actionpack/lib/action_view.rb b/actionpack/lib/action_view.rb index c0d7423682..60665387b6 100644 --- a/actionpack/lib/action_view.rb +++ b/actionpack/lib/action_view.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2010 David Heinemeier Hansson +# Copyright (c) 2004-2011 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -21,9 +21,6 @@ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #++ -activesupport_path = File.expand_path('../../../activesupport/lib', __FILE__) -$:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path) - require 'active_support/ruby/shim' require 'active_support/core_ext/class/attribute_accessors' @@ -33,35 +30,43 @@ module ActionView extend ActiveSupport::Autoload eager_autoload do + autoload :Base autoload :Context - autoload :Template autoload :Helpers + autoload :LookupContext + autoload :Partials + autoload :PathSet + autoload :Rendering + autoload :Template + autoload :TestCase - autoload_under "render" do - autoload :Layouts - autoload :Partials - autoload :Rendering + autoload_under "renderer" do + autoload :AbstractRenderer + autoload :PartialRenderer + autoload :TemplateRenderer end - autoload :Base - autoload :LookupContext - autoload :Resolver, 'action_view/template/resolver' - autoload :PathResolver, 'action_view/template/resolver' - autoload :FileSystemResolver, 'action_view/template/resolver' - autoload :PathSet, 'action_view/paths' + autoload_at "action_view/template/resolver" do + autoload :Resolver + autoload :PathResolver + autoload :FileSystemResolver + autoload :FallbackFileSystemResolver + end - autoload :MissingTemplate, 'action_view/template/error' - autoload :ActionViewError, 'action_view/template/error' - autoload :EncodingError, 'action_view/template/error' - autoload :TemplateError, 'action_view/template/error' - autoload :WrongEncodingError, 'action_view/template/error' + autoload_at "action_view/template/error" do + autoload :MissingTemplate + autoload :ActionViewError + autoload :EncodingError + autoload :TemplateError + autoload :WrongEncodingError + end - autoload :TemplateHandler, 'action_view/template' - autoload :TemplateHandlers, 'action_view/template' + autoload_at "action_view/template" do + autoload :TemplateHandler + autoload :TemplateHandlers + end end - autoload :TestCase, 'action_view/test_case' - ENCODING_FLAG = '#.*coding[:=]\s*(\S+)[ \t]*' end diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb index 20a2e7c1f0..ab8c6259c5 100644 --- a/actionpack/lib/action_view/base.rb +++ b/actionpack/lib/action_view/base.rb @@ -6,9 +6,6 @@ require 'active_support/ordered_options' require 'action_view/log_subscriber' module ActionView #:nodoc: - class NonConcattingString < ActiveSupport::SafeBuffer - end - # = Action View Base # # 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 @@ -21,7 +18,7 @@ module ActionView #:nodoc: # following loop for names: # # <b>Names of all the people</b> - # <% for person in @people %> + # <% @people.each do |person| %> # Name: <%= person.name %><br/> # <% end %> # @@ -79,8 +76,8 @@ module ActionView #:nodoc: # # === Template caching # - # By default, Rails will compile each template to a method in order to render it. When you alter a template, Rails will - # check the file's modification time and recompile it. + # By default, Rails will compile each template to a method in order to render it. When you alter a template, + # Rails will check the file's modification time and recompile it in development mode. # # == Builder # @@ -156,12 +153,9 @@ module ActionView #:nodoc: # # This refreshes the sidebar, removes a person element and highlights the user list. # - # See the ActionView::Helpers::PrototypeHelper::GeneratorMethods documentation for more details. + # See the ActionView::Helpers::PrototypeHelper::JavaScriptGenerator::GeneratorMethods documentation for more details. class Base - module Subclasses - end - - include Helpers, Rendering, Partials, Layouts, ::ERB::Util, Context + include Helpers, Rendering, Partials, ::ERB::Util, Context # Specify whether RJS responses should be wrapped in a try/catch block # that alert()s the caught exception (and then re-raises it). @@ -178,24 +172,26 @@ module ActionView #:nodoc: class << self delegate :erb_trim_mode=, :to => 'ActionView::Template::Handlers::ERB' delegate :logger, :to => 'ActionController::Base', :allow_nil => true + + def cache_template_loading + ActionView::Resolver.caching? + end + + def cache_template_loading=(value) + ActionView::Resolver.caching = value + end end - attr_accessor :base_path, :assigns, :template_extension, :lookup_context - attr_internal :captures, :request, :controller, :template, :config + attr_accessor :_template + attr_internal :request, :controller, :config, :assigns, :lookup_context - delegate :find_template, :template_exists?, :formats, :formats=, :locale, :locale=, - :view_paths, :view_paths=, :with_fallbacks, :update_details, :with_layout_format, :to => :lookup_context + delegate :formats, :formats=, :locale, :locale=, :view_paths, :view_paths=, :to => :lookup_context - delegate :request_forgery_protection_token, :template, :params, :session, :cookies, :response, :headers, + delegate :request_forgery_protection_token, :params, :session, :cookies, :response, :headers, :flash, :action_name, :controller_name, :to => :controller delegate :logger, :to => :controller, :allow_nil => true - # TODO: HACK FOR RJS - def view_context - self - end - def self.xss_safe? #:nodoc: true end @@ -206,33 +202,40 @@ module ActionView #:nodoc: end def assign(new_assigns) # :nodoc: - self.assigns = new_assigns.each { |key, value| instance_variable_set("@#{key}", value) } + @_assigns = new_assigns.each { |key, value| instance_variable_set("@#{key}", value) } end def initialize(lookup_context = nil, assigns_for_first_render = {}, controller = nil, formats = nil) #:nodoc: assign(assigns_for_first_render) - self.helpers = self.class.helpers || Module.new - - if @_controller = controller - @_request = controller.request if controller.respond_to?(:request) - end - - config = controller && controller.respond_to?(:config) ? controller.config : {} - @_config = ActiveSupport::InheritableOptions.new(config) + self.helpers = Module.new unless self.class.helpers + @_config = {} @_content_for = Hash.new { |h,k| h[k] = ActiveSupport::SafeBuffer.new } @_virtual_path = nil @output_buffer = nil - @lookup_context = lookup_context.is_a?(ActionView::LookupContext) ? + if @_controller = controller + @_request = controller.request if controller.respond_to?(:request) + @_config = controller.config.inheritable_copy if controller.respond_to?(:config) + end + + @_lookup_context = lookup_context.is_a?(ActionView::LookupContext) ? lookup_context : ActionView::LookupContext.new(lookup_context) - @lookup_context.formats = formats if formats + @_lookup_context.formats = formats if formats + end + + def store_content_for(key, value) + @_content_for[key] = value end def controller_path @controller_path ||= controller && controller.controller_path end + def controller_prefixes + @controller_prefixes ||= controller && controller._prefixes + end + ActiveSupport.run_load_hooks(:action_view, self) end end diff --git a/actionpack/lib/action_view/helpers.rb b/actionpack/lib/action_view/helpers.rb index b7ffa345cc..d338ce616a 100644 --- a/actionpack/lib/action_view/helpers.rb +++ b/actionpack/lib/action_view/helpers.rb @@ -12,14 +12,13 @@ module ActionView #:nodoc: autoload :CsrfHelper autoload :DateHelper autoload :DebugHelper - autoload :DeprecatedBlockHelpers autoload :FormHelper autoload :FormOptionsHelper autoload :FormTagHelper autoload :JavaScriptHelper, "action_view/helpers/javascript_helper" autoload :NumberHelper autoload :PrototypeHelper - autoload :RawOutputHelper + autoload :OutputSafetyHelper autoload :RecordTagHelper autoload :SanitizeHelper autoload :ScriptaculousHelper @@ -49,7 +48,7 @@ module ActionView #:nodoc: include JavaScriptHelper include NumberHelper include PrototypeHelper - include RawOutputHelper + include OutputSafetyHelper include RecordTagHelper include SanitizeHelper include ScriptaculousHelper diff --git a/actionpack/lib/action_view/helpers/active_model_helper.rb b/actionpack/lib/action_view/helpers/active_model_helper.rb index 6bb0875bc3..96c3eec337 100644 --- a/actionpack/lib/action_view/helpers/active_model_helper.rb +++ b/actionpack/lib/action_view/helpers/active_model_helper.rb @@ -7,25 +7,6 @@ module ActionView # = Active Model Helpers module Helpers module ActiveModelHelper - %w(input form error_messages_for error_message_on).each do |method| - class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def #{method}(*args) - ActiveSupport::Deprecation.warn "#{method} was removed from Rails and is now available as a plugin. " << - "Please install it with `rails plugin install git://github.com/rails/dynamic_form.git`.", caller - end - RUBY - end - end - - module ActiveModelFormBuilder - %w(error_messages error_message_on).each do |method| - class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def #{method}(*args) - ActiveSupport::Deprecation.warn "f.#{method} was removed from Rails and is now available as a plugin. " << - "Please install it with `rails plugin install git://github.com/rails/dynamic_form.git`.", caller - end - RUBY - end end module ActiveModelInstanceTag @@ -67,10 +48,6 @@ module ActionView end end - class FormBuilder - include ActiveModelFormBuilder - end - class InstanceTag include ActiveModelInstanceTag end diff --git a/actionpack/lib/action_view/helpers/asset_tag_helper.rb b/actionpack/lib/action_view/helpers/asset_tag_helper.rb index a3c43d3e93..f6b2d4f3f4 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helper.rb @@ -1,9 +1,6 @@ -require 'thread' -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' +require 'action_view/helpers/asset_tag_helpers/javascript_tag_helpers' +require 'action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers' +require 'action_view/helpers/asset_tag_helpers/asset_paths' module ActionView # = Action View Asset Tag Helpers @@ -152,7 +149,7 @@ module ActionView # # # Normally you'd calculate RELEASE_NUMBER at startup. # RELEASE_NUMBER = 12345 - # config.action_controller.asset_path_template = proc { |asset_path| + # config.action_controller.asset_path = proc { |asset_path| # "/release-#{RELEASE_NUMBER}#{asset_path}" # } # @@ -194,20 +191,8 @@ module ActionView # RewriteEngine On # RewriteRule ^/release-\d+/(images|javascripts|stylesheets)/(.*)$ /$1/$2 [L] module AssetTagHelper - mattr_reader :javascript_expansions - @@javascript_expansions = { } - - mattr_reader :stylesheet_expansions - @@stylesheet_expansions = {} - - # You can enable or disable the asset tag timestamps cache. - # With the cache enabled, the asset tag helper methods will make fewer - # expensive file system calls. However this prevents you from modifying - # any asset files while the server is running. - # - # ActionView::Helpers::AssetTagHelper.cache_asset_timestamps = false - mattr_accessor :cache_asset_timestamps - + include JavascriptTagHelpers + include StylesheetTagHelpers # Returns a link tag that browsers and news readers can use to auto-detect # an RSS or ATOM feed. The +type+ can either be <tt>:rss</tt> (default) or # <tt>:atom</tt>. Control the link options in url_for format using the @@ -241,263 +226,6 @@ module ActionView ) end - # Computes the path to a javascript asset in the public javascripts directory. - # If the +source+ filename has no extension, .js will be appended (except for explicit URIs) - # Full paths from the document root will be passed through. - # Used internally by javascript_include_tag to build the script path. - # - # ==== Examples - # javascript_path "xmlhr" # => /javascripts/xmlhr.js - # javascript_path "dir/xmlhr.js" # => /javascripts/dir/xmlhr.js - # javascript_path "/dir/xmlhr" # => /dir/xmlhr.js - # javascript_path "http://www.railsapplication.com/js/xmlhr" # => http://www.railsapplication.com/js/xmlhr - # javascript_path "http://www.railsapplication.com/js/xmlhr.js" # => http://www.railsapplication.com/js/xmlhr.js - def javascript_path(source) - compute_public_path(source, 'javascripts', 'js') - end - alias_method :path_to_javascript, :javascript_path # aliased to avoid conflicts with a javascript_path named route - - # Returns an html script tag for each of the +sources+ provided. You - # can pass in the filename (.js extension is optional) of javascript files - # that exist in your public/javascripts directory for inclusion into the - # current page or you can pass the full path relative to your document - # root. To include the Prototype and Scriptaculous javascript libraries in - # your application, pass <tt>:defaults</tt> as the source. When using - # <tt>:defaults</tt>, if an application.js file exists in your public - # javascripts directory, it will be included as well. You can modify the - # html attributes of the script tag by passing a hash as the last argument. - # - # ==== Examples - # javascript_include_tag "xmlhr" # => - # <script type="text/javascript" src="/javascripts/xmlhr.js"></script> - # - # javascript_include_tag "xmlhr.js" # => - # <script type="text/javascript" src="/javascripts/xmlhr.js"></script> - # - # javascript_include_tag "common.javascript", "/elsewhere/cools" # => - # <script type="text/javascript" src="/javascripts/common.javascript"></script> - # <script type="text/javascript" src="/elsewhere/cools.js"></script> - # - # javascript_include_tag "http://www.railsapplication.com/xmlhr" # => - # <script type="text/javascript" src="http://www.railsapplication.com/xmlhr.js"></script> - # - # javascript_include_tag "http://www.railsapplication.com/xmlhr.js" # => - # <script type="text/javascript" src="http://www.railsapplication.com/xmlhr.js"></script> - # - # javascript_include_tag :defaults # => - # <script type="text/javascript" src="/javascripts/prototype.js"></script> - # <script type="text/javascript" src="/javascripts/effects.js"></script> - # ... - # <script type="text/javascript" src="/javascripts/application.js"></script> - # - # * = The application.js file is only referenced if it exists - # - # Though it's not really recommended practice, if you need to extend the default JavaScript set for any reason - # (e.g., you're going to be using a certain .js file in every action), then take a look at the register_javascript_include_default method. - # - # You can also include all javascripts in the javascripts directory using <tt>:all</tt> as the source: - # - # javascript_include_tag :all # => - # <script type="text/javascript" src="/javascripts/prototype.js"></script> - # <script type="text/javascript" src="/javascripts/effects.js"></script> - # ... - # <script type="text/javascript" src="/javascripts/application.js"></script> - # <script type="text/javascript" src="/javascripts/shop.js"></script> - # <script type="text/javascript" src="/javascripts/checkout.js"></script> - # - # Note that the default javascript files will be included first. So Prototype and Scriptaculous are available to - # all subsequently included files. - # - # If you want Rails to search in all the subdirectories under javascripts, you should explicitly set <tt>:recursive</tt>: - # - # javascript_include_tag :all, :recursive => true - # - # == Caching multiple javascripts into one - # - # You can also cache multiple javascripts into one file, which requires less HTTP connections to download and can better be - # compressed by gzip (leading to faster transfers). Caching will only happen if config.perform_caching - # is set to <tt>true</tt> (which is the case by default for the Rails production environment, but not for the development - # environment). - # - # ==== Examples - # javascript_include_tag :all, :cache => true # when config.perform_caching is false => - # <script type="text/javascript" src="/javascripts/prototype.js"></script> - # <script type="text/javascript" src="/javascripts/effects.js"></script> - # ... - # <script type="text/javascript" src="/javascripts/application.js"></script> - # <script type="text/javascript" src="/javascripts/shop.js"></script> - # <script type="text/javascript" src="/javascripts/checkout.js"></script> - # - # javascript_include_tag :all, :cache => true # when config.perform_caching is true => - # <script type="text/javascript" src="/javascripts/all.js"></script> - # - # javascript_include_tag "prototype", "cart", "checkout", :cache => "shop" # when config.perform_caching is false => - # <script type="text/javascript" src="/javascripts/prototype.js"></script> - # <script type="text/javascript" src="/javascripts/cart.js"></script> - # <script type="text/javascript" src="/javascripts/checkout.js"></script> - # - # javascript_include_tag "prototype", "cart", "checkout", :cache => "shop" # when config.perform_caching is true => - # <script type="text/javascript" src="/javascripts/shop.js"></script> - # - # The <tt>:recursive</tt> option is also available for caching: - # - # javascript_include_tag :all, :cache => true, :recursive => true - def javascript_include_tag(*sources) - options = sources.extract_options!.stringify_keys - concat = options.delete("concat") - cache = concat || options.delete("cache") - recursive = options.delete("recursive") - - if concat || (config.perform_caching && cache) - joined_javascript_name = (cache == true ? "all" : cache) + ".js" - joined_javascript_path = File.join(joined_javascript_name[/^#{File::SEPARATOR}/] ? config.assets_dir : config.javascripts_dir, joined_javascript_name) - - unless config.perform_caching && File.exists?(joined_javascript_path) - write_asset_file_contents(joined_javascript_path, compute_javascript_paths(sources, recursive)) - end - javascript_src_tag(joined_javascript_name, options) - else - sources = expand_javascript_sources(sources, recursive) - ensure_javascript_sources!(sources) if cache - sources.collect { |source| javascript_src_tag(source, options) }.join("\n").html_safe - end - end - - # Register one or more javascript files to be included when <tt>symbol</tt> - # is passed to <tt>javascript_include_tag</tt>. This method is typically intended - # to be called from plugin initialization to register javascript files - # that the plugin installed in <tt>public/javascripts</tt>. - # - # ActionView::Helpers::AssetTagHelper.register_javascript_expansion :monkey => ["head", "body", "tail"] - # - # javascript_include_tag :monkey # => - # <script type="text/javascript" src="/javascripts/head.js"></script> - # <script type="text/javascript" src="/javascripts/body.js"></script> - # <script type="text/javascript" src="/javascripts/tail.js"></script> - def self.register_javascript_expansion(expansions) - @@javascript_expansions.merge!(expansions) - end - - # Register one or more stylesheet files to be included when <tt>symbol</tt> - # is passed to <tt>stylesheet_link_tag</tt>. This method is typically intended - # to be called from plugin initialization to register stylesheet files - # that the plugin installed in <tt>public/stylesheets</tt>. - # - # ActionView::Helpers::AssetTagHelper.register_stylesheet_expansion :monkey => ["head", "body", "tail"] - # - # stylesheet_link_tag :monkey # => - # <link href="/stylesheets/head.css" media="screen" rel="stylesheet" type="text/css" /> - # <link href="/stylesheets/body.css" media="screen" rel="stylesheet" type="text/css" /> - # <link href="/stylesheets/tail.css" media="screen" rel="stylesheet" type="text/css" /> - def self.register_stylesheet_expansion(expansions) - @@stylesheet_expansions.merge!(expansions) - end - - # Computes the path to a stylesheet asset in the public stylesheets directory. - # If the +source+ filename has no extension, <tt>.css</tt> will be appended (except for explicit URIs). - # Full paths from the document root will be passed through. - # Used internally by +stylesheet_link_tag+ to build the stylesheet path. - # - # ==== Examples - # stylesheet_path "style" # => /stylesheets/style.css - # stylesheet_path "dir/style.css" # => /stylesheets/dir/style.css - # stylesheet_path "/dir/style.css" # => /dir/style.css - # stylesheet_path "http://www.railsapplication.com/css/style" # => http://www.railsapplication.com/css/style - # stylesheet_path "http://www.railsapplication.com/css/style.css" # => http://www.railsapplication.com/css/style.css - def stylesheet_path(source) - compute_public_path(source, 'stylesheets', 'css') - end - alias_method :path_to_stylesheet, :stylesheet_path # aliased to avoid conflicts with a stylesheet_path named route - - # Returns a stylesheet link tag for the sources specified as arguments. If - # you don't specify an extension, <tt>.css</tt> will be appended automatically. - # You can modify the link attributes by passing a hash as the last argument. - # - # ==== Examples - # stylesheet_link_tag "style" # => - # <link href="/stylesheets/style.css" media="screen" rel="stylesheet" type="text/css" /> - # - # stylesheet_link_tag "style.css" # => - # <link href="/stylesheets/style.css" media="screen" rel="stylesheet" type="text/css" /> - # - # stylesheet_link_tag "http://www.railsapplication.com/style.css" # => - # <link href="http://www.railsapplication.com/style.css" media="screen" rel="stylesheet" type="text/css" /> - # - # stylesheet_link_tag "style", :media => "all" # => - # <link href="/stylesheets/style.css" media="all" rel="stylesheet" type="text/css" /> - # - # stylesheet_link_tag "style", :media => "print" # => - # <link href="/stylesheets/style.css" media="print" rel="stylesheet" type="text/css" /> - # - # stylesheet_link_tag "random.styles", "/css/stylish" # => - # <link href="/stylesheets/random.styles" media="screen" rel="stylesheet" type="text/css" /> - # <link href="/css/stylish.css" media="screen" rel="stylesheet" type="text/css" /> - # - # You can also include all styles in the stylesheets directory using <tt>:all</tt> as the source: - # - # stylesheet_link_tag :all # => - # <link href="/stylesheets/style1.css" media="screen" rel="stylesheet" type="text/css" /> - # <link href="/stylesheets/styleB.css" media="screen" rel="stylesheet" type="text/css" /> - # <link href="/stylesheets/styleX2.css" media="screen" rel="stylesheet" type="text/css" /> - # - # If you want Rails to search in all the subdirectories under stylesheets, you should explicitly set <tt>:recursive</tt>: - # - # stylesheet_link_tag :all, :recursive => true - # - # == Caching multiple stylesheets into one - # - # You can also cache multiple stylesheets into one file, which requires less HTTP connections and can better be - # compressed by gzip (leading to faster transfers). Caching will only happen if config.perform_caching - # is set to true (which is the case by default for the Rails production environment, but not for the development - # environment). Examples: - # - # ==== Examples - # stylesheet_link_tag :all, :cache => true # when config.perform_caching is false => - # <link href="/stylesheets/style1.css" media="screen" rel="stylesheet" type="text/css" /> - # <link href="/stylesheets/styleB.css" media="screen" rel="stylesheet" type="text/css" /> - # <link href="/stylesheets/styleX2.css" media="screen" rel="stylesheet" type="text/css" /> - # - # stylesheet_link_tag :all, :cache => true # when config.perform_caching is true => - # <link href="/stylesheets/all.css" media="screen" rel="stylesheet" type="text/css" /> - # - # stylesheet_link_tag "shop", "cart", "checkout", :cache => "payment" # when config.perform_caching is false => - # <link href="/stylesheets/shop.css" media="screen" rel="stylesheet" type="text/css" /> - # <link href="/stylesheets/cart.css" media="screen" rel="stylesheet" type="text/css" /> - # <link href="/stylesheets/checkout.css" media="screen" rel="stylesheet" type="text/css" /> - # - # stylesheet_link_tag "shop", "cart", "checkout", :cache => "payment" # when config.perform_caching is true => - # <link href="/stylesheets/payment.css" media="screen" rel="stylesheet" type="text/css" /> - # - # The <tt>:recursive</tt> option is also available for caching: - # - # stylesheet_link_tag :all, :cache => true, :recursive => true - # - # To force concatenation (even in development mode) set <tt>:concat</tt> to true. This is useful if - # you have too many stylesheets for IE to load. - # - # stylesheet_link_tag :all, :concat => true - # - def stylesheet_link_tag(*sources) - options = sources.extract_options!.stringify_keys - concat = options.delete("concat") - cache = concat || options.delete("cache") - recursive = options.delete("recursive") - - if concat || (config.perform_caching && cache) - joined_stylesheet_name = (cache == true ? "all" : cache) + ".css" - joined_stylesheet_path = File.join(joined_stylesheet_name[/^#{File::SEPARATOR}/] ? config.assets_dir : config.stylesheets_dir, joined_stylesheet_name) - - unless config.perform_caching && File.exists?(joined_stylesheet_path) - write_asset_file_contents(joined_stylesheet_path, compute_stylesheet_paths(sources, recursive)) - end - stylesheet_tag(joined_stylesheet_name, options) - else - sources = expand_stylesheet_sources(sources, recursive) - ensure_stylesheet_sources!(sources) if cache - sources.collect { |source| stylesheet_tag(source, options) }.join("\n").html_safe - end - end - # Web browsers cache favicons. If you just throw a <tt>favicon.ico</tt> into the document # root of your application and it changes later, clients that have it in their cache # won't see the update. Using this helper prevents that because it appends an asset ID: @@ -546,7 +274,7 @@ module ActionView # The alias +path_to_image+ is provided to avoid that. Rails uses the alias internally, and # plugin authors are encouraged to do so. def image_path(source) - compute_public_path(source, 'images') + asset_paths.compute_public_path(source, 'images') end alias_method :path_to_image, :image_path # aliased to avoid conflicts with an image_path named route @@ -561,7 +289,7 @@ module ActionView # video_path("/trailers/hd.avi") # => /trailers/hd.avi # video_path("http://www.railsapplication.com/vid/hd.avi") # => http://www.railsapplication.com/vid/hd.avi def video_path(source) - compute_public_path(source, 'videos') + asset_paths.compute_public_path(source, 'videos') end alias_method :path_to_video, :video_path # aliased to avoid conflicts with a video_path named route @@ -571,12 +299,12 @@ module ActionView # # ==== Examples # audio_path("horse") # => /audios/horse - # audio_path("horse.wav") # => /audios/horse.avi - # audio_path("sounds/horse.wav") # => /audios/sounds/horse.avi - # audio_path("/sounds/horse.wav") # => /sounds/horse.avi + # audio_path("horse.wav") # => /audios/horse.wav + # audio_path("sounds/horse.wav") # => /audios/sounds/horse.wav + # audio_path("/sounds/horse.wav") # => /sounds/horse.wav # audio_path("http://www.railsapplication.com/sounds/horse.wav") # => http://www.railsapplication.com/sounds/horse.wav def audio_path(source) - compute_public_path(source, 'audios') + asset_paths.compute_public_path(source, 'audios') end alias_method :path_to_audio, :audio_path # aliased to avoid conflicts with an audio_path named route @@ -705,202 +433,8 @@ module ActionView 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) - return source if is_uri?(source) - - source += ".#{ext}" if rewrite_extension?(source, dir, ext) - source = "/#{dir}/#{source}" unless source[0] == ?/ - source = rewrite_asset_path(source, config.asset_path) - - 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 - - source - end - - def is_uri?(path) - path =~ %r{^[-a-z]+://|^cid:} - end - - # Pick an asset host for this source. Returns +nil+ if no host is set, - # the host if no wildcard is set, the host interpolated with the - # numbers 0-3 if it contains <tt>%d</tt> (the number is the source hash mod 4), - # or the value returned from invoking the proc if it's a proc or the value from - # invoking call if it's an object responding to call. - def compute_asset_host(source) - if host = config.asset_host - if host.is_a?(Proc) || host.respond_to?(:call) - case host.is_a?(Proc) ? host.arity : host.method(:call).arity - when 2 - request = controller.respond_to?(:request) && controller.request - host.call(source, request) - else - host.call(source) - end - else - (host =~ /%d/) ? host % (source.hash % 4) : host - end - end - end - - @@asset_timestamps_cache = {} - @@asset_timestamps_cache_guard = Mutex.new - - # Use the RAILS_ASSET_ID environment variable or the source's - # modification time as its cache-busting asset id. - def rails_asset_id(source) - if asset_id = ENV["RAILS_ASSET_ID"] - asset_id - else - if @@cache_asset_timestamps && (asset_id = @@asset_timestamps_cache[source]) - asset_id - else - path = File.join(config.assets_dir, source) - asset_id = File.exist?(path) ? File.mtime(path).to_i.to_s : '' - - if @@cache_asset_timestamps - @@asset_timestamps_cache_guard.synchronize do - @@asset_timestamps_cache[source] = asset_id - end - end - - asset_id - end - end - end - - # Break out the asset path rewrite in case plugins wish to put the asset id - # someplace other than the query string. - def rewrite_asset_path(source, path = nil) - if path && path.respond_to?(:call) - return path.call(source) - elsif path && path.is_a?(String) - return path % [source] - end - - asset_id = rails_asset_id(source) - if asset_id.blank? - source - else - source + "?#{asset_id}" - end - end - - def javascript_src_tag(source, options) - content_tag("script", "", { "type" => Mime::JS, "src" => path_to_javascript(source) }.merge(options)) - end - - def stylesheet_tag(source, options) - tag("link", { "rel" => "stylesheet", "type" => Mime::CSS, "media" => "screen", "href" => html_escape(path_to_stylesheet(source)) }.merge(options), false, false) - end - - def compute_javascript_paths(*args) - expand_javascript_sources(*args).collect { |source| compute_public_path(source, 'javascripts', 'js', false) } - end - - def compute_stylesheet_paths(*args) - expand_stylesheet_sources(*args).collect { |source| compute_public_path(source, 'stylesheets', 'css', false) } - end - - def expand_javascript_sources(sources, recursive = false) - if sources.include?(:all) - all_javascript_files = collect_asset_files(config.javascripts_dir, ('**' if recursive), '*.js') - ((determine_source(:defaults, @@javascript_expansions).dup & all_javascript_files) + all_javascript_files).uniq - else - expanded_sources = sources.collect do |source| - determine_source(source, @@javascript_expansions) - end.flatten - expanded_sources << "application" if sources.include?(:defaults) && File.exist?(File.join(config.javascripts_dir, "application.js")) - expanded_sources - end - end - - def expand_stylesheet_sources(sources, recursive) - if sources.first == :all - collect_asset_files(config.stylesheets_dir, ('**' if recursive), '*.css') - else - sources.collect do |source| - determine_source(source, @@stylesheet_expansions) - end.flatten - end - end - - def determine_source(source, collection) - case source - when Symbol - collection[source] || raise(ArgumentError, "No expansion found for #{source.inspect}") - else - source - end - end - - def ensure_stylesheet_sources!(sources) - sources.each do |source| - asset_file_path!(path_to_stylesheet(source)) - end - return sources - end - - def ensure_javascript_sources!(sources) - sources.each do |source| - asset_file_path!(path_to_javascript(source)) - end - return sources - end - - def join_asset_file_contents(paths) - paths.collect { |path| File.read(asset_file_path!(path)) }.join("\n\n") - end - - def write_asset_file_contents(joined_asset_path, asset_paths) - - FileUtils.mkdir_p(File.dirname(joined_asset_path)) - File.atomic_write(joined_asset_path) { |cache| cache.write(join_asset_file_contents(asset_paths)) } - - # Set mtime to the latest of the combined files to allow for - # consistent ETag without a shared filesystem. - mt = asset_paths.map { |p| File.mtime(asset_file_path(p)) }.max - File.utime(mt, mt, joined_asset_path) - end - - def asset_file_path(path) - File.join(config.assets_dir, path.split('?').first) - end - - def asset_file_path!(path) - unless is_uri?(path) - absolute_path = asset_file_path(path) - raise(Errno::ENOENT, "Asset file not found at '#{absolute_path}'" ) unless File.exist?(absolute_path) - return absolute_path - end - end - - def collect_asset_files(*path) - dir = path.first - - Dir[File.join(*path.compact)].collect do |file| - file[-(file.size - dir.size - 1)..-1].sub(/\.\w+$/, '') - end.sort + def asset_paths + @asset_paths ||= AssetPaths.new(config, controller) end end end diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_include_tag.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_include_tag.rb new file mode 100644 index 0000000000..52eb43a1cd --- /dev/null +++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_include_tag.rb @@ -0,0 +1,146 @@ +require 'active_support/core_ext/class/attribute' +require 'active_support/core_ext/string/inflections' +require 'active_support/core_ext/file' +require 'action_view/helpers/tag_helper' + +module ActionView + module Helpers + module AssetTagHelper + + class AssetIncludeTag + attr_reader :config, :asset_paths + + class_attribute :expansions + def self.inherited(base) + base.expansions = { } + end + + def initialize(config, asset_paths) + @config = config + @asset_paths = asset_paths + end + + def asset_name + raise NotImplementedError + end + + def extension + raise NotImplementedError + end + + def custom_dir + raise NotImplementedError + end + + def asset_tag(source, options) + raise NotImplementedError + end + + def include_tag(*sources) + options = sources.extract_options!.stringify_keys + concat = options.delete("concat") + cache = concat || options.delete("cache") + recursive = options.delete("recursive") + + if concat || (config.perform_caching && cache) + joined_name = (cache == true ? "all" : cache) + ".#{extension}" + joined_path = File.join((joined_name[/^#{File::SEPARATOR}/] ? config.assets_dir : custom_dir), joined_name) + unless config.perform_caching && File.exists?(joined_path) + write_asset_file_contents(joined_path, compute_paths(sources, recursive)) + end + asset_tag(joined_name, options) + else + sources = expand_sources(sources, recursive) + ensure_sources!(sources) if cache + sources.collect { |source| asset_tag(source, options) }.join("\n").html_safe + end + end + + + private + + def path_to_asset(source, include_host = true) + asset_paths.compute_public_path(source, asset_name.to_s.pluralize, extension, include_host) + end + + def compute_paths(*args) + expand_sources(*args).collect { |source| asset_paths.compute_public_path(source, asset_name.pluralize, extension, false) } + end + + def expand_sources(sources, recursive) + if sources.first == :all + collect_asset_files(custom_dir, ('**' if recursive), "*.#{extension}") + else + sources.inject([]) do |list, source| + determined_source = determine_source(source, expansions) + update_source_list(list, determined_source) + end + end + end + + def update_source_list(list, source) + case source + when String + list.delete(source) + list << source + when Array + updated_sources = source - list + list.concat(updated_sources) + end + end + + def ensure_sources!(sources) + sources.each do |source| + asset_file_path!(path_to_asset(source, false)) + end + end + + def collect_asset_files(*path) + dir = path.first + + Dir[File.join(*path.compact)].collect do |file| + file[-(file.size - dir.size - 1)..-1].sub(/\.\w+$/, '') + end.sort + end + + def determine_source(source, collection) + case source + when Symbol + collection[source] || raise(ArgumentError, "No expansion found for #{source.inspect}") + else + source + end + end + + def join_asset_file_contents(paths) + paths.collect { |path| File.read(asset_file_path!(path, true)) }.join("\n\n") + end + + def write_asset_file_contents(joined_asset_path, asset_paths) + FileUtils.mkdir_p(File.dirname(joined_asset_path)) + File.atomic_write(joined_asset_path) { |cache| cache.write(join_asset_file_contents(asset_paths)) } + + # Set mtime to the latest of the combined files to allow for + # consistent ETag without a shared filesystem. + mt = asset_paths.map { |p| File.mtime(asset_file_path(p)) }.max + File.utime(mt, mt, joined_asset_path) + end + + def asset_file_path(path) + File.join(config.assets_dir, path.split('?').first) + end + + def asset_file_path!(path, error_if_file_is_uri = false) + if asset_paths.is_uri?(path) + raise(Errno::ENOENT, "Asset file #{path} is uri and cannot be merged into single file") if error_if_file_is_uri + else + absolute_path = asset_file_path(path) + raise(Errno::ENOENT, "Asset file not found at '#{absolute_path}'" ) unless File.exist?(absolute_path) + return absolute_path + end + end + end + + end + end +end diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb new file mode 100644 index 0000000000..014a03c54d --- /dev/null +++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb @@ -0,0 +1,153 @@ +require 'active_support/core_ext/file' + +module ActionView + module Helpers + module AssetTagHelper + + class AssetPaths + # You can enable or disable the asset tag ids cache. + # With the cache enabled, the asset tag helper methods will make fewer + # expensive file system calls (the default implementation checks the file + # system timestamp). However this prevents you from modifying any asset + # files while the server is running. + # + # ActionView::Helpers::AssetTagHelper::AssetPaths.cache_asset_ids = false + mattr_accessor :cache_asset_ids + + attr_reader :config, :controller + + def initialize(config, controller) + @config = config + @controller = controller + end + + # Add 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) + return source if is_uri?(source) + + source = rewrite_extension(source, dir, ext) if ext + source = "/#{dir}/#{source}" unless source[0] == ?/ + if controller.respond_to?(:env) && controller.env["action_dispatch.asset_path"] + source = rewrite_asset_path(source, controller.env["action_dispatch.asset_path"]) + end + source = rewrite_asset_path(source, config.asset_path) + + has_request = controller.respond_to?(:request) + source = rewrite_relative_url_root(source, controller.config.relative_url_root) if has_request && include_host + source = rewrite_host_and_protocol(source, has_request) if include_host + + source + end + + # Add or change an asset id in the asset id cache. This can be used + # for SASS on Heroku. + # :api: public + def add_to_asset_ids_cache(source, asset_id) + self.asset_ids_cache_guard.synchronize do + self.asset_ids_cache[source] = asset_id + end + end + + def is_uri?(path) + path =~ %r{^[-a-z]+://|^cid:} + end + + private + + def rewrite_extension(source, dir, ext) + source_ext = File.extname(source) + + source_with_ext = if source_ext.empty? + "#{source}.#{ext}" + elsif ext != source_ext[1..-1] + with_ext = "#{source}.#{ext}" + with_ext if File.exist?(File.join(config.assets_dir, dir, with_ext)) + end + + source_with_ext || source + end + + # Break out the asset path rewrite in case plugins wish to put the asset id + # someplace other than the query string. + def rewrite_asset_path(source, path = nil) + if path && path.respond_to?(:call) + return path.call(source) + elsif path && path.is_a?(String) + return path % [source] + end + + asset_id = rails_asset_id(source) + if asset_id.empty? + source + else + "#{source}?#{asset_id}" + end + end + + mattr_accessor :asset_ids_cache + self.asset_ids_cache = {} + + mattr_accessor :asset_ids_cache_guard + self.asset_ids_cache_guard = Mutex.new + + # Use the RAILS_ASSET_ID environment variable or the source's + # modification time as its cache-busting asset id. + def rails_asset_id(source) + if asset_id = ENV["RAILS_ASSET_ID"] + asset_id + else + if self.cache_asset_ids && (asset_id = self.asset_ids_cache[source]) + asset_id + else + path = File.join(config.assets_dir, source) + asset_id = File.exist?(path) ? File.mtime(path).to_i.to_s : '' + + if self.cache_asset_ids + add_to_asset_ids_cache(source, asset_id) + end + + asset_id + end + end + end + + def rewrite_relative_url_root(source, relative_url_root) + relative_url_root && !source.starts_with?("#{relative_url_root}/") ? "#{relative_url_root}#{source}" : source + end + + def rewrite_host_and_protocol(source, has_request) + host = compute_asset_host(source) + if has_request && host && !is_uri?(host) + host = "#{controller.request.protocol}#{host}" + end + "#{host}#{source}" + end + + # Pick an asset host for this source. Returns +nil+ if no host is set, + # the host if no wildcard is set, the host interpolated with the + # numbers 0-3 if it contains <tt>%d</tt> (the number is the source hash mod 4), + # or the value returned from invoking the proc if it's a proc or the value from + # invoking call if it's an object responding to call. + def compute_asset_host(source) + if host = config.asset_host + if host.is_a?(Proc) || host.respond_to?(:call) + case host.is_a?(Proc) ? host.arity : host.method(:call).arity + when 2 + request = controller.respond_to?(:request) && controller.request + host.call(source, request) + else + host.call(source) + end + else + (host =~ /%d/) ? host % (source.hash % 4) : host + end + end + end + end + + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb new file mode 100644 index 0000000000..82bbfcc7d2 --- /dev/null +++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb @@ -0,0 +1,184 @@ +require 'active_support/concern' +require 'active_support/core_ext/file' +require 'action_view/helpers/tag_helper' +require 'action_view/helpers/asset_tag_helpers/asset_include_tag' + +module ActionView + module Helpers + module AssetTagHelper + + class JavascriptIncludeTag < AssetIncludeTag + include TagHelper + + def asset_name + 'javascript' + end + + def extension + 'js' + end + + def asset_tag(source, options) + content_tag("script", "", { "type" => Mime::JS, "src" => path_to_asset(source) }.merge(options)) + end + + def custom_dir + config.javascripts_dir + end + + private + + def expand_sources(sources, recursive = false) + if sources.include?(:all) + all_asset_files = (collect_asset_files(custom_dir, ('**' if recursive), "*.#{extension}") - ['application']) << 'application' + ((determine_source(:defaults, expansions).dup & all_asset_files) + all_asset_files).uniq + else + expanded_sources = sources.inject([]) do |list, source| + determined_source = determine_source(source, expansions) + update_source_list(list, determined_source) + end + add_application_js(expanded_sources, sources) + expanded_sources + end + end + + def add_application_js(expanded_sources, sources) + if sources.include?(:defaults) && File.exist?(File.join(custom_dir, "application.#{extension}")) + expanded_sources.delete('application') + expanded_sources << "application" + end + end + end + + + module JavascriptTagHelpers + extend ActiveSupport::Concern + + module ClassMethods + # Register one or more javascript files to be included when <tt>symbol</tt> + # is passed to <tt>javascript_include_tag</tt>. This method is typically intended + # to be called from plugin initialization to register javascript files + # that the plugin installed in <tt>public/javascripts</tt>. + # + # ActionView::Helpers::AssetTagHelper.register_javascript_expansion :monkey => ["head", "body", "tail"] + # + # javascript_include_tag :monkey # => + # <script type="text/javascript" src="/javascripts/head.js"></script> + # <script type="text/javascript" src="/javascripts/body.js"></script> + # <script type="text/javascript" src="/javascripts/tail.js"></script> + def register_javascript_expansion(expansions) + js_expansions = JavascriptIncludeTag.expansions + expansions.each do |key, values| + js_expansions[key] = (js_expansions[key] || []) | Array(values) + end + end + end + + # Computes the path to a javascript asset in the public javascripts directory. + # If the +source+ filename has no extension, .js will be appended (except for explicit URIs) + # Full paths from the document root will be passed through. + # Used internally by javascript_include_tag to build the script path. + # + # ==== Examples + # javascript_path "xmlhr" # => /javascripts/xmlhr.js + # javascript_path "dir/xmlhr.js" # => /javascripts/dir/xmlhr.js + # javascript_path "/dir/xmlhr" # => /dir/xmlhr.js + # javascript_path "http://www.railsapplication.com/js/xmlhr" # => http://www.railsapplication.com/js/xmlhr + # javascript_path "http://www.railsapplication.com/js/xmlhr.js" # => http://www.railsapplication.com/js/xmlhr.js + def javascript_path(source) + asset_paths.compute_public_path(source, 'javascripts', 'js') + end + alias_method :path_to_javascript, :javascript_path # aliased to avoid conflicts with a javascript_path named route + + # Returns an HTML script tag for each of the +sources+ provided. You + # can pass in the filename (.js extension is optional) of JavaScript files + # that exist in your <tt>public/javascripts</tt> directory for inclusion into the + # current page or you can pass the full path relative to your document + # root. To include the Prototype and Scriptaculous JavaScript libraries in + # your application, pass <tt>:defaults</tt> as the source. When using + # <tt>:defaults</tt>, if an <tt>application.js</tt> file exists in + # <tt>public/javascripts</tt> it will be included as well. You can modify the + # HTML attributes of the script tag by passing a hash as the last argument. + # + # ==== Examples + # javascript_include_tag "xmlhr" # => + # <script type="text/javascript" src="/javascripts/xmlhr.js?1284139606"></script> + # + # javascript_include_tag "xmlhr.js" # => + # <script type="text/javascript" src="/javascripts/xmlhr.js?1284139606"></script> + # + # javascript_include_tag "common.javascript", "/elsewhere/cools" # => + # <script type="text/javascript" src="/javascripts/common.javascript?1284139606"></script> + # <script type="text/javascript" src="/elsewhere/cools.js?1423139606"></script> + # + # javascript_include_tag "http://www.railsapplication.com/xmlhr" # => + # <script type="text/javascript" src="http://www.railsapplication.com/xmlhr.js?1284139606"></script> + # + # javascript_include_tag "http://www.railsapplication.com/xmlhr.js" # => + # <script type="text/javascript" src="http://www.railsapplication.com/xmlhr.js?1284139606"></script> + # + # javascript_include_tag :defaults # => + # <script type="text/javascript" src="/javascripts/prototype.js?1284139606"></script> + # <script type="text/javascript" src="/javascripts/effects.js?1284139606"></script> + # ... + # <script type="text/javascript" src="/javascripts/application.js?1284139606"></script> + # + # * = The application.js file is only referenced if it exists + # + # You can also include all javascripts in the +javascripts+ directory using <tt>:all</tt> as the source: + # + # javascript_include_tag :all # => + # <script type="text/javascript" src="/javascripts/prototype.js?1284139606"></script> + # <script type="text/javascript" src="/javascripts/effects.js?1284139606"></script> + # ... + # <script type="text/javascript" src="/javascripts/application.js?1284139606"></script> + # <script type="text/javascript" src="/javascripts/shop.js?1284139606"></script> + # <script type="text/javascript" src="/javascripts/checkout.js?1284139606"></script> + # + # Note that the default javascript files will be included first. So Prototype and Scriptaculous are available to + # all subsequently included files. + # + # If you want Rails to search in all the subdirectories under javascripts, you should explicitly set <tt>:recursive</tt>: + # + # javascript_include_tag :all, :recursive => true + # + # == Caching multiple javascripts into one + # + # You can also cache multiple javascripts into one file, which requires less HTTP connections to download and can better be + # compressed by gzip (leading to faster transfers). Caching will only happen if config.perform_caching + # is set to <tt>true</tt> (which is the case by default for the Rails production environment, but not for the development + # environment). + # + # ==== Examples + # javascript_include_tag :all, :cache => true # when config.perform_caching is false => + # <script type="text/javascript" src="/javascripts/prototype.js?1284139606"></script> + # <script type="text/javascript" src="/javascripts/effects.js?1284139606"></script> + # ... + # <script type="text/javascript" src="/javascripts/application.js?1284139606"></script> + # <script type="text/javascript" src="/javascripts/shop.js?1284139606"></script> + # <script type="text/javascript" src="/javascripts/checkout.js?1284139606"></script> + # + # javascript_include_tag :all, :cache => true # when config.perform_caching is true => + # <script type="text/javascript" src="/javascripts/all.js?1344139789"></script> + # + # javascript_include_tag "prototype", "cart", "checkout", :cache => "shop" # when config.perform_caching is false => + # <script type="text/javascript" src="/javascripts/prototype.js?1284139606"></script> + # <script type="text/javascript" src="/javascripts/cart.js?1289139157"></script> + # <script type="text/javascript" src="/javascripts/checkout.js?1299139816"></script> + # + # javascript_include_tag "prototype", "cart", "checkout", :cache => "shop" # when config.perform_caching is true => + # <script type="text/javascript" src="/javascripts/shop.js?1299139816"></script> + # + # The <tt>:recursive</tt> option is also available for caching: + # + # javascript_include_tag :all, :cache => true, :recursive => true + def javascript_include_tag(*sources) + @javascript_include ||= JavascriptIncludeTag.new(config, asset_paths) + @javascript_include.include_tag(*sources) + end + + end + + end + end +end diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb new file mode 100644 index 0000000000..a48c87b49a --- /dev/null +++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb @@ -0,0 +1,147 @@ +require 'active_support/concern' +require 'active_support/core_ext/file' +require 'action_view/helpers/tag_helper' +require 'action_view/helpers/asset_tag_helpers/asset_include_tag' + +module ActionView + module Helpers + module AssetTagHelper + + class StylesheetIncludeTag < AssetIncludeTag + include TagHelper + + def asset_name + 'stylesheet' + end + + def extension + 'css' + end + + def asset_tag(source, options) + tag("link", { "rel" => "stylesheet", "type" => Mime::CSS, "media" => "screen", "href" => ERB::Util.html_escape(path_to_asset(source)) }.merge(options), false, false) + end + + def custom_dir + config.stylesheets_dir + end + end + + + module StylesheetTagHelpers + extend ActiveSupport::Concern + + module ClassMethods + # Register one or more stylesheet files to be included when <tt>symbol</tt> + # is passed to <tt>stylesheet_link_tag</tt>. This method is typically intended + # to be called from plugin initialization to register stylesheet files + # that the plugin installed in <tt>public/stylesheets</tt>. + # + # ActionView::Helpers::AssetTagHelper.register_stylesheet_expansion :monkey => ["head", "body", "tail"] + # + # stylesheet_link_tag :monkey # => + # <link href="/stylesheets/head.css" media="screen" rel="stylesheet" type="text/css" /> + # <link href="/stylesheets/body.css" media="screen" rel="stylesheet" type="text/css" /> + # <link href="/stylesheets/tail.css" media="screen" rel="stylesheet" type="text/css" /> + def register_stylesheet_expansion(expansions) + style_expansions = StylesheetIncludeTag.expansions + expansions.each do |key, values| + style_expansions[key] = (style_expansions[key] || []) | Array(values) + end + end + end + + # Computes the path to a stylesheet asset in the public stylesheets directory. + # If the +source+ filename has no extension, <tt>.css</tt> will be appended (except for explicit URIs). + # Full paths from the document root will be passed through. + # Used internally by +stylesheet_link_tag+ to build the stylesheet path. + # + # ==== Examples + # stylesheet_path "style" # => /stylesheets/style.css + # stylesheet_path "dir/style.css" # => /stylesheets/dir/style.css + # stylesheet_path "/dir/style.css" # => /dir/style.css + # stylesheet_path "http://www.railsapplication.com/css/style" # => http://www.railsapplication.com/css/style + # stylesheet_path "http://www.railsapplication.com/css/style.css" # => http://www.railsapplication.com/css/style.css + def stylesheet_path(source) + asset_paths.compute_public_path(source, 'stylesheets', 'css') + end + alias_method :path_to_stylesheet, :stylesheet_path # aliased to avoid conflicts with a stylesheet_path named route + + # Returns a stylesheet link tag for the sources specified as arguments. If + # you don't specify an extension, <tt>.css</tt> will be appended automatically. + # You can modify the link attributes by passing a hash as the last argument. + # + # ==== Examples + # stylesheet_link_tag "style" # => + # <link href="/stylesheets/style.css" media="screen" rel="stylesheet" type="text/css" /> + # + # stylesheet_link_tag "style.css" # => + # <link href="/stylesheets/style.css" media="screen" rel="stylesheet" type="text/css" /> + # + # stylesheet_link_tag "http://www.railsapplication.com/style.css" # => + # <link href="http://www.railsapplication.com/style.css" media="screen" rel="stylesheet" type="text/css" /> + # + # stylesheet_link_tag "style", :media => "all" # => + # <link href="/stylesheets/style.css" media="all" rel="stylesheet" type="text/css" /> + # + # stylesheet_link_tag "style", :media => "print" # => + # <link href="/stylesheets/style.css" media="print" rel="stylesheet" type="text/css" /> + # + # stylesheet_link_tag "random.styles", "/css/stylish" # => + # <link href="/stylesheets/random.styles" media="screen" rel="stylesheet" type="text/css" /> + # <link href="/css/stylish.css" media="screen" rel="stylesheet" type="text/css" /> + # + # You can also include all styles in the stylesheets directory using <tt>:all</tt> as the source: + # + # stylesheet_link_tag :all # => + # <link href="/stylesheets/style1.css" media="screen" rel="stylesheet" type="text/css" /> + # <link href="/stylesheets/styleB.css" media="screen" rel="stylesheet" type="text/css" /> + # <link href="/stylesheets/styleX2.css" media="screen" rel="stylesheet" type="text/css" /> + # + # If you want Rails to search in all the subdirectories under stylesheets, you should explicitly set <tt>:recursive</tt>: + # + # stylesheet_link_tag :all, :recursive => true + # + # == Caching multiple stylesheets into one + # + # You can also cache multiple stylesheets into one file, which requires less HTTP connections and can better be + # compressed by gzip (leading to faster transfers). Caching will only happen if config.perform_caching + # is set to true (which is the case by default for the Rails production environment, but not for the development + # environment). Examples: + # + # ==== Examples + # stylesheet_link_tag :all, :cache => true # when config.perform_caching is false => + # <link href="/stylesheets/style1.css" media="screen" rel="stylesheet" type="text/css" /> + # <link href="/stylesheets/styleB.css" media="screen" rel="stylesheet" type="text/css" /> + # <link href="/stylesheets/styleX2.css" media="screen" rel="stylesheet" type="text/css" /> + # + # stylesheet_link_tag :all, :cache => true # when config.perform_caching is true => + # <link href="/stylesheets/all.css" media="screen" rel="stylesheet" type="text/css" /> + # + # stylesheet_link_tag "shop", "cart", "checkout", :cache => "payment" # when config.perform_caching is false => + # <link href="/stylesheets/shop.css" media="screen" rel="stylesheet" type="text/css" /> + # <link href="/stylesheets/cart.css" media="screen" rel="stylesheet" type="text/css" /> + # <link href="/stylesheets/checkout.css" media="screen" rel="stylesheet" type="text/css" /> + # + # stylesheet_link_tag "shop", "cart", "checkout", :cache => "payment" # when config.perform_caching is true => + # <link href="/stylesheets/payment.css" media="screen" rel="stylesheet" type="text/css" /> + # + # The <tt>:recursive</tt> option is also available for caching: + # + # stylesheet_link_tag :all, :cache => true, :recursive => true + # + # To force concatenation (even in development mode) set <tt>:concat</tt> to true. This is useful if + # you have too many stylesheets for IE to load. + # + # stylesheet_link_tag :all, :concat => true + # + def stylesheet_link_tag(*sources) + @stylesheet_include ||= StylesheetIncludeTag.new(config, asset_paths) + @stylesheet_include.include_tag(*sources) + end + + end + + end + end +end diff --git a/actionpack/lib/action_view/helpers/atom_feed_helper.rb b/actionpack/lib/action_view/helpers/atom_feed_helper.rb index 8e7cf2e701..db9d7a08ff 100644 --- a/actionpack/lib/action_view/helpers/atom_feed_helper.rb +++ b/actionpack/lib/action_view/helpers/atom_feed_helper.rb @@ -51,7 +51,7 @@ module ActionView # * <tt>:language</tt>: Defaults to "en-US". # * <tt>:root_url</tt>: The HTML alternative that this feed is doubling for. Defaults to / on the current host. # * <tt>:url</tt>: The URL for this feed. Defaults to the current URL. - # * <tt>:id</tt>: The id for this feed. Defaults to "tag:#{request.host},#{options[:schema_date]}:#{request.request_uri.split(".")[0]}" + # * <tt>:id</tt>: The id for this feed. Defaults to "tag:#{request.host},#{options[:schema_date]}:#{request.fullpath.split(".")[0]}" # * <tt>:schema_date</tt>: The date at which the tag scheme for the feed was first used. A good default is the year you # created the feed. See http://feedvalidator.org/docs/error/InvalidTAG.html for more information. If not specified, # 2005 is used (as an "I don't care" value). diff --git a/actionpack/lib/action_view/helpers/cache_helper.rb b/actionpack/lib/action_view/helpers/cache_helper.rb index f544a9d147..385378ea29 100644 --- a/actionpack/lib/action_view/helpers/cache_helper.rb +++ b/actionpack/lib/action_view/helpers/cache_helper.rb @@ -2,31 +2,29 @@ module ActionView # = Action View Cache Helper module Helpers module CacheHelper - # This helper to exposes a method for caching of view fragments. - # See ActionController::Caching::Fragments for usage instructions. + # This helper exposes a method for caching fragments of a view + # rather than an entire action or page. This technique is useful + # caching pieces like menus, lists of newstopics, static HTML + # fragments, and so on. This method takes a block that contains + # the content you wish to cache. # - # A method for caching fragments of a view rather than an entire - # action or page. This technique is useful caching pieces like - # menus, lists of news topics, static HTML fragments, and so on. - # This method takes a block that contains the content you wish - # to cache. See ActionController::Caching::Fragments for more - # information. + # See ActionController::Caching::Fragments for usage instructions. # # ==== Examples - # If you wanted to cache a navigation menu, you could do the - # following. + # If you want to cache a navigation menu, you can do following: # # <% cache do %> # <%= render :partial => "menu" %> # <% end %> # - # You can also cache static content... + # You can also cache static content: # # <% cache do %> # <p>Hello users! Welcome to our website!</p> # <% end %> # - # ...and static content mixed with RHTML content. + # Static content with embedded ruby content can be cached as + # well: # # <% cache do %> # Topics: @@ -46,8 +44,8 @@ module ActionView private # TODO: Create an object that has caching read/write on it def fragment_for(name = {}, options = nil, &block) #:nodoc: - if controller.fragment_exist?(name, options) - controller.read_fragment(name, options) + if fragment = controller.read_fragment(name, options) + fragment else # VIEW TODO: Make #capture usable outside of ERB # This dance is needed because Builder can't use capture diff --git a/actionpack/lib/action_view/helpers/capture_helper.rb b/actionpack/lib/action_view/helpers/capture_helper.rb index 89e95e8694..c88bd1efd5 100644 --- a/actionpack/lib/action_view/helpers/capture_helper.rb +++ b/actionpack/lib/action_view/helpers/capture_helper.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/string/output_safety' module ActionView # = Action View Capture Helper @@ -38,7 +39,7 @@ module ActionView value = nil buffer = with_output_buffer { value = yield(*args) } if string = buffer.presence || value and string.is_a?(String) - NonConcattingString.new(string) + ERB::Util.html_escape string end end @@ -106,7 +107,7 @@ module ActionView # <%= javascript_include_tag :defaults %> # <% end %> # - # That will place <script> tags for Prototype, Scriptaculous, and application.js (if it exists) + # That will place <tt>script</tt> tags for Prototype, Scriptaculous, and application.js (if it exists) # on the page; this technique is useful if you'll only be using these scripts in a few views. # # Note that content_for concatenates the blocks it is given for a particular diff --git a/actionpack/lib/action_view/helpers/csrf_helper.rb b/actionpack/lib/action_view/helpers/csrf_helper.rb index 3d03f6aac6..65c8debc76 100644 --- a/actionpack/lib/action_view/helpers/csrf_helper.rb +++ b/actionpack/lib/action_view/helpers/csrf_helper.rb @@ -1,14 +1,30 @@ +require 'active_support/core_ext/string/strip' + module ActionView # = Action View CSRF Helper module Helpers module CsrfHelper - # Returns a meta tag with the cross-site request forgery protection token - # for forms to use. Place this in your head. - def csrf_meta_tag - if protect_against_forgery? - %(<meta name="csrf-param" content="#{Rack::Utils.escape_html(request_forgery_protection_token)}"/>\n<meta name="csrf-token" content="#{Rack::Utils.escape_html(form_authenticity_token)}"/>).html_safe - end + # Returns meta tags "csrf-param" and "csrf-token" with the name of the cross-site + # request forgery protection parameter and token, respectively. + # + # <head> + # <%= csrf_meta_tags %> + # </head> + # + # These are used to generate the dynamic forms that implement non-remote links with + # <tt>:method</tt>. + # + # Note that regular forms generate hidden fields, and that Ajax calls are whitelisted, + # so they do not use these tags. + def csrf_meta_tags + <<-METAS.strip_heredoc.chomp.html_safe if protect_against_forgery? + <meta name="csrf-param" content="#{Rack::Utils.escape_html(request_forgery_protection_token)}"/> + <meta name="csrf-token" content="#{Rack::Utils.escape_html(form_authenticity_token)}"/> + METAS end + + # For backwards compatibility. + alias csrf_meta_tag csrf_meta_tags end end end diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb index 8050669adb..6cd1565031 100644 --- a/actionpack/lib/action_view/helpers/date_helper.rb +++ b/actionpack/lib/action_view/helpers/date_helper.rb @@ -1,6 +1,8 @@ -require "date" +require 'date' require 'action_view/helpers/tag_helper' +require 'active_support/core_ext/date/conversions' require 'active_support/core_ext/hash/slice' +require 'active_support/core_ext/object/with_options' module ActionView module Helpers @@ -214,21 +216,10 @@ module ActionView # # Creates a time select tag that, when POSTed, will be stored in the post variable in the sunrise attribute # time_select("post", "sunrise") # - # # Creates a time select tag that, when POSTed, will be stored in the order variable in the submitted - # # attribute - # time_select("order", "submitted") - # - # # Creates a time select tag that, when POSTed, will be stored in the mail variable in the sent_at attribute - # time_select("mail", "sent_at") - # # # Creates a time select tag with a seconds field that, when POSTed, will be stored in the post variables in # # the sunrise attribute. # time_select("post", "start_time", :include_seconds => true) # - # # Creates a time select tag with a seconds field that, when POSTed, will be stored in the entry variables in - # # the submission_time attribute. - # time_select("entry", "submission_time", :include_seconds => true) - # # # You can set the :minute_step to 15 which will give you: 00, 15, 30 and 45. # time_select 'game', 'game_time', {:minute_step => 15} # @@ -576,6 +567,27 @@ module ActionView def select_year(date, options = {}, html_options = {}) DateTimeSelector.new(date, options, html_options).select_year end + + # Returns an html time tag for the given date or time. + # + # ==== Examples + # time_tag Date.today # => + # <time datetime="2010-11-04">November 04, 2010</time> + # time_tag Time.now # => + # <time datetime="2010-11-04T17:55:45+01:00">November 04, 2010 17:55</time> + # time_tag Date.yesterday, 'Yesterday' # => + # <time datetime="2010-11-03">Yesterday</time> + # time_tag Date.today, :pubdate => true # => + # <time datetime="2010-11-04" pubdate="pubdate">November 04, 2010</time> + # + def time_tag(date_or_time, *args) + options = args.extract_options! + format = options.delete(:format) || :long + content = args.first || I18n.l(date_or_time, :format => format) + datetime = date_or_time.acts_like?(:time) ? date_or_time.xmlschema : date_or_time.rfc3339 + + content_tag(:time, content, options.reverse_merge(:datetime => datetime)) + end end class DateTimeSelector #:nodoc: @@ -605,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 @@ -632,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 @@ -751,10 +763,8 @@ module ActionView # => [nil, "Jan", "Feb", "Mar", "Apr", "May", "Jun", # "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] def translated_month_names - begin - key = @options[:use_short_month] ? :'date.abbr_month_names' : :'date.month_names' - I18n.translate(key, :locale => @options[:locale]) - end + key = @options[:use_short_month] ? :'date.abbr_month_names' : :'date.month_names' + I18n.translate(key, :locale => @options[:locale]) end # Lookup month name for number @@ -781,9 +791,7 @@ module ActionView memoize :date_order def translated_date_order - begin - I18n.translate(:'date.order', :locale => @options[:locale]) || [] - end + I18n.translate(:'date.order', :locale => @options[:locale]) || [] end # Build full select tag from date type and options @@ -837,15 +845,14 @@ module ActionView # prompt_option_tag(:month, :prompt => 'Select month') # => "<option value="">Select month</option>" def prompt_option_tag(type, options) - default_options = {:year => false, :month => false, :day => false, :hour => false, :minute => false, :second => false} - - case options - when Hash - prompt = default_options.merge(options)[type.to_sym] - when String - prompt = options - else - prompt = I18n.translate(('datetime.prompts.' + type.to_s).to_sym, :locale => @options[:locale]) + prompt = case options + when Hash + default_options = {:year => false, :month => false, :day => false, :hour => false, :minute => false, :second => false} + default_options.merge!(options)[type.to_sym] + when String + options + else + I18n.translate(:"datetime.prompts.#{type}", :locale => @options[:locale]) end prompt ? content_tag(:option, prompt, :value => '') : '' @@ -897,6 +904,8 @@ module ActionView # Returns the separator for a given datetime component def separator(type) case type + when :year + @options[:discard_year] ? "" : @options[:date_separator] when :month @options[:discard_month] ? "" : @options[:date_separator] when :day @@ -927,6 +936,7 @@ module ActionView private def datetime_selector(options, html_options) datetime = value(object) || default_datetime(options) + @auto_index ||= nil options = options.dup options[:field_name] = @method_name diff --git a/actionpack/lib/action_view/helpers/debug_helper.rb b/actionpack/lib/action_view/helpers/debug_helper.rb index 1491cb073f..cd67851642 100644 --- a/actionpack/lib/action_view/helpers/debug_helper.rb +++ b/actionpack/lib/action_view/helpers/debug_helper.rb @@ -1,6 +1,6 @@ module ActionView # = Action View Debug Helper - # + # # Provides a set of methods for making it easier to debug Rails objects. module Helpers module DebugHelper diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index ebe055bebd..48abf119f1 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -2,9 +2,11 @@ require 'cgi' require 'action_view/helpers/date_helper' require 'action_view/helpers/tag_helper' require 'action_view/helpers/form_tag_helper' -require 'active_support/core_ext/class/inheritable_attributes' +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 @@ -111,6 +113,7 @@ module ActionView # <%= f.text_field :version %><br /> # <%= f.label :author, 'Author' %>: # <%= f.text_field :author %><br /> + # <%= f.submit %> # <% end %> # # There, +form_for+ is able to generate the rest of RESTful form @@ -128,6 +131,7 @@ module ActionView # Last name : <%= f.text_field :last_name %><br /> # Biography : <%= f.text_area :biography %><br /> # Admin? : <%= f.check_box :admin %><br /> + # <%= f.submit %> # <% end %> # # There, the argument is a symbol or string with the name of the @@ -159,6 +163,7 @@ module ActionView # Last name : <%= f.text_field :last_name %> # Biography : <%= text_area :person, :biography %> # Admin? : <%= check_box_tag "person[admin]", @person.company.admin? %> + # <%= f.submit %> # <% end %> # # This also works for the methods in FormOptionHelper and DateHelper that @@ -202,10 +207,16 @@ module ActionView # ... # <% end %> # + # You can also set the answer format, like this: + # + # <%= form_for(@post, :format => :json) do |f| %> + # ... + # <% end %> + # # If you have an object that needs to be represented as a different # parameter, like a Client that acts as a Person: # - # <%= form_for(@post, :as => :client do |f| %> + # <%= form_for(@post, :as => :client) do |f| %> # ... # <% end %> # @@ -222,8 +233,8 @@ module ActionView # ... # <% end %> # - # Where +@document = Document.find(params[:id])+ and - # +@comment = Comment.new+. + # Where <tt>@document = Document.find(params[:id])</tt> and + # <tt>@comment = Comment.new</tt>. # # === Unobtrusive JavaScript # @@ -252,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 @@ -262,8 +291,9 @@ module ActionView # <%= form_for @person, :url => { :action => "create" }, :builder => LabellingFormBuilder do |f| %> # <%= f.text_field :first_name %> # <%= f.text_field :last_name %> - # <%= text_area :person, :biography %> - # <%= check_box_tag "person[admin]", @person.company.admin? %> + # <%= f.text_area :biography %> + # <%= f.check_box :admin %> + # <%= f.submit %> # <% end %> # # In this case, if you use this: @@ -287,31 +317,46 @@ module ActionView # # If you don't need to attach a form to a model instance, then check out # FormTagHelper#form_tag. - def form_for(record_or_name_or_array, *args, &proc) + # + # === Form to external resources + # + # When you build forms to external resources sometimes you need to set an authenticity token or just render a form + # without it, for example when you submit data to a payment gateway number and types of fields could be limited. + # + # To set an authenticity token you need to pass an <tt>:authenticity_token</tt> parameter + # + # <%= form_for @invoice, :url => external_url, :authenticity_token => 'external_token' do |f| + # ... + # <% end %> + # + # If you don't want to an authenticity token field be rendered at all just pass <tt>false</tt>: + # + # <%= form_for @invoice, :url => external_url, :authenticity_token => false do |f| + # ... + # <% end %> + def form_for(record, options = {}, &proc) raise ArgumentError, "Missing block" unless block_given? - options = args.extract_options! + options[:html] ||= {} - case record_or_name_or_array + case record when String, Symbol - ActiveSupport::Deprecation.warn("Using form_for(:name, @resource) is deprecated. Please use form_for(@resource, :as => :name) instead.", caller) unless args.empty? - object_name = record_or_name_or_array - when Array - object = record_or_name_or_array.last - object_name = options[:as] || ActiveModel::Naming.singular(object) - apply_form_for_options!(record_or_name_or_array, options) - args.unshift object + object_name = record + object = nil else - object = record_or_name_or_array - object_name = options[:as] || ActiveModel::Naming.singular(object) - apply_form_for_options!([object], options) - args.unshift object + object = record.is_a?(Array) ? record.last : record + object_name = options[:as] || ActiveModel::Naming.param_key(object) + apply_form_for_options!(record, options) end - (options[:html] ||= {})[:remote] = true if options.delete(:remote) + options[:html][:remote] = options.delete(:remote) + options[:html][:authenticity_token] = options.delete(:authenticity_token) - output = form_tag(options.delete(:url) || {}, options.delete(:html) || {}) - output << fields_for(object_name, *(args << options), &proc) + 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 } : {} + output = form_tag(options.delete(:url) || {}, default_options.merge!(options.delete(:html))) + output << fields_for output.safe_concat('</form>') end @@ -319,21 +364,17 @@ module ActionView object = object_or_array.is_a?(Array) ? object_or_array.last : object_or_array object = convert_to_model(object) - html_options = - if object.respond_to?(:persisted?) && object.persisted? - { :class => options[:as] ? "#{options[:as]}_edit" : dom_class(object, :edit), - :id => options[:as] ? "#{options[:as]}_edit" : dom_id(object, :edit), - :method => :put } - else - { :class => options[:as] ? "#{options[:as]}_new" : dom_class(object, :new), - :id => options[:as] ? "#{options[:as]}_new" : dom_id(object), - :method => :post } - end + as = options[:as] + action, method = object.respond_to?(:persisted?) && object.persisted? ? [:edit, :put] : [:new, :post] + options[:html].reverse_merge!( + :class => as ? "#{as}_#{action}" : dom_class(object, action), + :id => as ? "#{as}_#{action}" : dom_id(object, action), + :method => method + ) - options[:html] ||= {} - options[:html].reverse_merge!(html_options) - options[:url] ||= polymorphic_path(object_or_array) + options[:url] ||= polymorphic_path(object_or_array, :format => options.delete(:format)) end + private :apply_form_for_options! # Creates a scope around a specific model object like form_for, but # doesn't create the form tags themselves. This makes fields_for suitable @@ -348,6 +389,8 @@ module ActionView # <%= fields_for @person.permission do |permission_fields| %> # Admin? : <%= permission_fields.check_box :admin %> # <% end %> + # + # <%= f.submit %> # <% end %> # # ...or if you have an object that needs to be represented as a different @@ -409,6 +452,7 @@ module ActionView # Street : <%= address_fields.text_field :street %> # Zip code: <%= address_fields.text_field :zip_code %> # <% end %> + # ... # <% end %> # # When address is already an association on a Person you can use @@ -438,6 +482,7 @@ module ActionView # ... # Delete: <%= address_fields.check_box :_destroy %> # <% end %> + # ... # <% end %> # # ==== One-to-many @@ -467,6 +512,7 @@ module ActionView # Name: <%= project_fields.text_field :name %> # <% end %> # <% end %> + # ... # <% end %> # # It's also possible to specify the instance to be used: @@ -480,6 +526,7 @@ module ActionView # <% end %> # <% end %> # <% end %> + # ... # <% end %> # # Or a collection to be used: @@ -489,6 +536,7 @@ module ActionView # <%= person_form.fields_for :projects, @active_projects do |project_fields| %> # Name: <%= project_fields.text_field :name %> # <% end %> + # ... # <% end %> # # When projects is already an association on Person you can use @@ -518,22 +566,13 @@ module ActionView # <%= person_form.fields_for :projects do |project_fields| %> # Delete: <%= project_fields.check_box :_destroy %> # <% end %> + # ... # <% end %> - def fields_for(record_or_name_or_array, *args, &block) - raise ArgumentError, "Missing block" unless block_given? - options = args.extract_options! - - case record_or_name_or_array - when String, Symbol - object_name = record_or_name_or_array - object = args.first - else - object = record_or_name_or_array - object_name = ActiveModel::Naming.singular(object) - end - - builder = options[:builder] || ActionView::Base.default_form_builder - capture(builder.new(object_name, object, self, options, block), &block) + def fields_for(record, record_object = nil, options = {}, &block) + builder = instantiate_builder(record, record_object, options, &block) + output = capture(builder, &block) + output.concat builder.hidden_field(:id) if output && options[:hidden_field_id] && !builder.emitted_hidden_id? + output end # Returns a label tag tailored for labelling an input field for a specified attribute (identified by +method+) on an object @@ -624,19 +663,19 @@ module ActionView # # ==== Examples # password_field(:login, :pass, :size => 20) - # # => <input type="text" id="login_pass" name="login[pass]" size="20" value="#{@login.pass}" /> + # # => <input type="password" id="login_pass" name="login[pass]" size="20" /> # - # password_field(:account, :secret, :class => "form_input") - # # => <input type="text" id="account_secret" name="account[secret]" value="#{@account.secret}" class="form_input" /> + # password_field(:account, :secret, :class => "form_input", :value => @account.secret) + # # => <input type="password" id="account_secret" name="account[secret]" value="#{@account.secret}" class="form_input" /> # # password_field(:user, :password, :onchange => "if $('user[password]').length > 30 { alert('Your password needs to be shorter!'); }") - # # => <input type="text" id="user_password" name="user[password]" value="#{@user.password}" onchange = "if $('user[password]').length > 30 { alert('Your password needs to be shorter!'); }"/> + # # => <input type="password" id="user_password" name="user[password]" onchange = "if $('user[password]').length > 30 { alert('Your password needs to be shorter!'); }"/> # # password_field(:account, :pin, :size => 20, :class => 'form_input') - # # => <input type="text" id="account_pin" name="account[pin]" size="20" value="#{@account.pin}" class="form_input" /> + # # => <input type="password" id="account_pin" name="account[pin]" size="20" class="form_input" /> # def password_field(object_name, method, options = {}) - InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("password", options) + InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("password", { :value => nil }.merge!(options)) end # Returns a hidden input tag tailored for accessing a specified attribute (identified by +method+) on an object @@ -657,11 +696,13 @@ module ActionView InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("hidden", options) end - # Returns an file upload input tag tailored for accessing a specified attribute (identified by +method+) on an object + # Returns a file upload input tag tailored for accessing a specified attribute (identified by +method+) on an object # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example # shown. # + # Using this method inside a +form_for+ block will set the enclosing form's encoding to <tt>multipart/form-data</tt>. + # # ==== Examples # file_field(:user, :avatar) # # => <input type="file" id="user_avatar" name="user[avatar]" /> @@ -803,7 +844,7 @@ module ActionView options["incremental"] = true unless options.has_key?("incremental") end - InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("search", options) + InstanceTag.new(object_name, method, self, options.delete("object")).to_input_field_tag("search", options) end # Returns a text_field of type "tel". @@ -837,29 +878,42 @@ module ActionView def range_field(object_name, method, options = {}) InstanceTag.new(object_name, method, self, options.delete(:object)).to_number_field_tag("range", options) end + + private + + def instantiate_builder(record, *args, &block) + options = args.extract_options! + record_object = args.shift + + case record + when String, Symbol + object = record_object + object_name = record + else + object = record + object_name = ActiveModel::Naming.param_key(object) + end + + builder = options[:builder] || ActionView::Base.default_form_builder + builder.new(object_name, object, self, options, block) + end end - module InstanceTagMethods #:nodoc: - extend ActiveSupport::Concern + class InstanceTag include Helpers::CaptureHelper, Context, Helpers::TagHelper, Helpers::FormTagHelper - attr_reader :method_name, :object_name + attr_reader :object, :method_name, :object_name - DEFAULT_FIELD_OPTIONS = { "size" => 30 }.freeze - DEFAULT_RADIO_OPTIONS = { }.freeze - DEFAULT_TEXT_AREA_OPTIONS = { "cols" => 40, "rows" => 20 }.freeze + DEFAULT_FIELD_OPTIONS = { "size" => 30 } + DEFAULT_RADIO_OPTIONS = { } + DEFAULT_TEXT_AREA_OPTIONS = { "cols" => 40, "rows" => 20 } def initialize(object_name, method_name, template_object, object = nil) @object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup @template_object = template_object - @object = object - if @object_name.sub!(/\[\]$/,"") || @object_name.sub!(/\[\]\]$/,"]") - if (object ||= @template_object.instance_variable_get("@#{Regexp.last_match.pre_match}")) && object.respond_to?(:to_param) - @auto_index = object.to_param - else - raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}" - end - end + @object_name.sub!(/\[\]$/,"") || @object_name.sub!(/\[\]\]$/,"]") + @object = retrieve_object(object) + @auto_index = retrieve_autoindex(Regexp.last_match.pre_match) if Regexp.last_match end def to_label_tag(text = nil, options = {}, &block) @@ -905,7 +959,7 @@ module ActionView end options["type"] ||= field_type options["value"] = options.fetch("value"){ value_before_type_cast(object) } unless field_type == "file" - options["value"] &&= html_escape(options["value"]) + options["value"] &&= ERB::Util.html_escape(options["value"]) add_default_name_and_id(options) tag("input", options) end @@ -941,7 +995,7 @@ module ActionView options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split) end - content_tag("textarea", html_escape(options.delete('value') || value_before_type_cast(object)), options) + content_tag("textarea", ERB::Util.html_escape(options.delete('value') || value_before_type_cast(object)), options) end def to_check_box_tag(options = {}, checked_value = "1", unchecked_value = "0") @@ -983,14 +1037,26 @@ module ActionView content_tag(tag_name, value(object), options) end - def object - @object || @template_object.instance_variable_get("@#{@object_name}") + def retrieve_object(object) + if object + object + elsif @template_object.instance_variable_defined?("@#{@object_name}") + @template_object.instance_variable_get("@#{@object_name}") + end rescue NameError - # As @object_name may contain the nested syntax (item[subobject]) we - # need to fallback to nil. + # As @object_name may contain the nested syntax (item[subobject]) we need to fallback to nil. nil end + def retrieve_autoindex(pre_match) + object = self.object || @template_object.instance_variable_get("@#{pre_match}") + if object && object.respond_to?(:to_param) + object.to_param + else + raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}" + end + end + def value(object) self.class.value(object, @method_name) end @@ -999,16 +1065,16 @@ module ActionView self.class.value_before_type_cast(object, @method_name) end - module ClassMethods + class << self def value(object, method_name) - object.send method_name unless object.nil? + object.send method_name if object end def value_before_type_cast(object, method_name) unless object.nil? - object.respond_to?(method_name) ? - object.send(method_name) : - object.send(method_name + "_before_type_cast") + object.respond_to?(method_name + "_before_type_cast") ? + object.send(method_name + "_before_type_cast") : + object.send(method_name) end end @@ -1085,17 +1151,21 @@ module ActionView end end - class InstanceTag - include InstanceTagMethods - end - - class FormBuilder #:nodoc: + class FormBuilder # The methods which wrap a form helper call. - class_inheritable_accessor :field_helpers + class_attribute :field_helpers self.field_helpers = (FormHelper.instance_method_names - ['form_for']) attr_accessor :object_name, :object, :options + attr_reader :multipart, :parent_builder + alias :multipart? :multipart + + def multipart=(multipart) + @multipart = multipart + parent_builder.multipart = multipart if parent_builder + end + def self.model_name @model_name ||= Struct.new(:partial_path).new(name.demodulize.underscore.sub!(/_builder$/, '')) end @@ -1107,6 +1177,7 @@ module ActionView def initialize(object_name, object, template, options, proc) @nested_child_index = {} @object_name, @object, @template, @options, @proc = object_name, object, template, options, proc + @parent_builder = options[:parent_builder] @default_options = @options ? @options.slice(:index) : {} if @object_name.to_s.match(/\[\]$/) if object ||= @template.instance_variable_get("@#{Regexp.last_match.pre_match}") and object.respond_to?(:to_param) @@ -1115,9 +1186,10 @@ module ActionView raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}" end end + @multipart = nil end - (field_helpers - %w(label check_box radio_button fields_for hidden_field)).each do |selector| + (field_helpers - %w(label check_box radio_button fields_for hidden_field file_field)).each do |selector| class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 def #{selector}(method, options = {}) # def text_field(method, options = {}) @template.send( # @template.send( @@ -1139,27 +1211,23 @@ module ActionView index = "" end - if options[:builder] - args << {} unless args.last.is_a?(Hash) - args.last[:builder] ||= options[:builder] - end + args << {} unless args.last.is_a?(Hash) + args.last[:builder] ||= options[:builder] + args.last[:parent_builder] = self case record_or_name_or_array when String, Symbol if nested_attributes_association?(record_or_name_or_array) return fields_for_with_nested_attributes(record_or_name_or_array, args, block) else - name = "#{object_name}#{index}[#{record_or_name_or_array}]" + name = record_or_name_or_array end - when Array - object = record_or_name_or_array.last - name = "#{object_name}#{index}[#{ActiveModel::Naming.singular(object)}]" - args.unshift(object) else - object = record_or_name_or_array - name = "#{object_name}#{index}[#{ActiveModel::Naming.singular(object)}]" + object = record_or_name_or_array.is_a?(Array) ? record_or_name_or_array.last : record_or_name_or_array + name = ActiveModel::Naming.param_key(object) args.unshift(object) end + name = "#{object_name}#{index}[#{name}]" @template.fields_for(name, *args, &block) end @@ -1181,6 +1249,11 @@ module ActionView @template.hidden_field(@object_name, method, objectify_options(options)) end + def file_field(method, options = {}) + self.multipart = true + @template.file_field(@object_name, method, objectify_options(options)) + end + # Add the submit button for the given form. When no value is given, it checks # if the object is a new resource or not to create the proper label: # @@ -1211,11 +1284,11 @@ module ActionView def submit(value=nil, options={}) value, options = nil, value if value.is_a?(Hash) value ||= submit_default_value - @template.submit_tag(value, options.reverse_merge(:id => "#{object_name}_submit")) + @template.submit_tag(value, options) end def emitted_hidden_id? - @emitted_hidden_id + @emitted_hidden_id ||= nil end private @@ -1224,7 +1297,7 @@ module ActionView end def submit_default_value - object = @object.respond_to?(:to_model) ? @object.to_model : @object + object = convert_to_model(@object) key = object ? (object.persisted? ? :update : :create) : :submit model = if object.class.respond_to?(:model_name) @@ -1249,15 +1322,15 @@ module ActionView name = "#{object_name}[#{association_name}_attributes]" options = args.extract_options! association = args.shift - association = association.to_model if association.respond_to?(:to_model) + association = convert_to_model(association) if association.respond_to?(:persisted?) association = [association] if @object.send(association_name).is_a?(Array) - elsif !association.is_a?(Array) + elsif !association.respond_to?(:to_ary) association = @object.send(association_name) end - if association.is_a?(Array) + if association.respond_to?(:to_ary) explicit_child_index = options[:child_index] output = ActiveSupport::SafeBuffer.new association.each do |child| @@ -1270,22 +1343,22 @@ module ActionView end def fields_for_nested_model(name, object, options, block) - object = object.to_model if object.respond_to?(:to_model) + object = convert_to_model(object) - if object.persisted? - @template.fields_for(name, object, options) do |builder| - block.call(builder) - @template.concat builder.hidden_field(:id) unless builder.emitted_hidden_id? - end - else - @template.fields_for(name, object, options, &block) - end + 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 def nested_child_index(name) @nested_child_index[name] ||= -1 @nested_child_index[name] += 1 end + + def convert_to_model(object) + object.respond_to?(:to_model) ? object.to_model : object + end end end diff --git a/actionpack/lib/action_view/helpers/form_options_helper.rb b/actionpack/lib/action_view/helpers/form_options_helper.rb index ee34452769..7698602022 100644 --- a/actionpack/lib/action_view/helpers/form_options_helper.rb +++ b/actionpack/lib/action_view/helpers/form_options_helper.rb @@ -2,6 +2,7 @@ require 'cgi' require 'erb' require 'action_view/helpers/form_helper' require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/string/output_safety' module ActionView # = Action View Form Option Helpers @@ -53,16 +54,16 @@ module ActionView # <option value="2">Sam</option> # <option value="3">Tobias</option> # </select> - # - # Like the other form helpers, +select+ can accept an <tt>:index</tt> option to manually set the ID used in the resulting output. Unlike other helpers, +select+ expects this + # + # Like the other form helpers, +select+ can accept an <tt>:index</tt> option to manually set the ID used in the resulting output. Unlike other helpers, +select+ expects this # option to be in the +html_options+ parameter. - # - # Example: - # + # + # Example: + # # select("album[]", "genre", %w[rap rock country], {}, { :index => nil }) - # + # # becomes: - # + # # <select name="album[][genre]" id="album__genre"> # <option value="rap">rap</option> # <option value="rock">rock</option> @@ -100,7 +101,6 @@ module ActionView # module FormOptionsHelper # ERB::Util can mask some helpers like textilize. Make sure to include them. - include ERB::Util include TextHelper # Create a select tag and a series of contained option tags for the provided object and method. @@ -140,7 +140,7 @@ module ActionView # The <tt>:value_method</tt> and <tt>:text_method</tt> parameters are methods to be called on each member # of +collection+. The return values are used as the +value+ attribute and contents of each # <tt><option></tt> tag, respectively. - # + # # Example object structure for use with this method: # class Post < ActiveRecord::Base # belongs_to :author @@ -247,7 +247,7 @@ module ActionView # # time_zone_select( "user", 'time_zone', /Australia/) # - # time_zone_select( "user", "time_zone", ActiveSupport::Timezone.all.sort, :model => ActiveSupport::Timezone) + # time_zone_select( "user", "time_zone", ActiveSupport::TimeZone.all.sort, :model => ActiveSupport::TimeZone) def time_zone_select(object, method, priority_zones = nil, options = {}, html_options = {}) InstanceTag.new(object, method, self, options.delete(:object)).to_time_zone_select_tag(priority_zones, options, html_options) end @@ -297,18 +297,18 @@ module ActionView def options_for_select(container, selected = nil) return container if String === container - container = container.to_a if Hash === container - selected, disabled = extract_selected_and_disabled(selected) + selected, disabled = extract_selected_and_disabled(selected).map do | r | + Array.wrap(r).map(&:to_s) + end - options_for_select = container.inject([]) do |options, element| + container.map do |element| html_attributes = option_html_attributes(element) - text, value = option_text_and_value(element) + text, value = option_text_and_value(element).map(&:to_s) selected_attribute = ' selected="selected"' if option_value_selected?(value, selected) disabled_attribute = ' disabled="disabled"' if disabled && option_value_selected?(value, disabled) - options << %(<option value="#{html_escape(value.to_s)}"#{selected_attribute}#{disabled_attribute}#{html_attributes}>#{html_escape(text.to_s)}</option>) - end + %(<option value="#{ERB::Util.html_escape(value)}"#{selected_attribute}#{disabled_attribute}#{html_attributes}>#{ERB::Util.html_escape(text)}</option>) + end.join("\n").html_safe - options_for_select.join("\n").html_safe end # Returns a string of option tags that have been compiled by iterating over the +collection+ and assigning the @@ -394,12 +394,12 @@ module ActionView # <b>Note:</b> Only the <tt><optgroup></tt> and <tt><option></tt> tags are returned, so you still have to # wrap the output in an appropriate <tt><select></tt> tag. def option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, selected_key = nil) - collection.inject("") do |options_for_select, group| + collection.map do |group| group_label_string = eval("group.#{group_label_method}") - options_for_select += "<optgroup label=\"#{html_escape(group_label_string)}\">" - options_for_select += options_from_collection_for_select(eval("group.#{group_method}"), option_key_method, option_value_method, selected_key) - options_for_select += '</optgroup>' - end.html_safe + "<optgroup label=\"#{ERB::Util.html_escape(group_label_string)}\">" + + options_from_collection_for_select(eval("group.#{group_method}"), option_key_method, option_value_method, selected_key) + + '</optgroup>' + end.join.html_safe end # Returns a string of <tt><option></tt> tags, like <tt>options_for_select</tt>, but @@ -493,7 +493,7 @@ module ActionView end zone_options += options_for_select(convert_zones[zones], selected) - zone_options + zone_options.html_safe end private @@ -501,7 +501,7 @@ module ActionView return "" unless Array === element html_attributes = [] element.select { |e| Hash === e }.reduce({}, :merge).each do |k, v| - html_attributes << " #{k}=\"#{html_escape(v.to_s)}\"" + html_attributes << " #{k}=\"#{ERB::Util.html_escape(v.to_s)}\"" end html_attributes.join end @@ -528,10 +528,12 @@ module ActionView end def extract_selected_and_disabled(selected) - if selected.is_a?(Hash) - [selected[:selected], selected[:disabled]] + if selected.is_a?(Proc) + [ selected, nil ] else - [selected, nil] + selected = Array.wrap(selected) + options = selected.extract_options!.symbolize_keys + [ options.include?(:selected) ? options[:selected] : selected, options[:disabled] ] end end @@ -593,11 +595,11 @@ module ActionView private def add_options(option_tags, options, value = nil) if options[:include_blank] - option_tags = "<option value=\"\">#{html_escape(options[:include_blank]) if options[:include_blank].kind_of?(String)}</option>\n" + option_tags + option_tags = "<option value=\"\">#{ERB::Util.html_escape(options[:include_blank]) if options[:include_blank].kind_of?(String)}</option>\n" + option_tags end if value.blank? && options[:prompt] prompt = options[:prompt].kind_of?(String) ? options[:prompt] : I18n.translate('helpers.select.prompt', :default => 'Please select') - option_tags = "<option value=\"\">#{html_escape(prompt)}</option>\n" + option_tags + option_tags = "<option value=\"\">#{ERB::Util.html_escape(prompt)}</option>\n" + option_tags end option_tags.html_safe end diff --git a/actionpack/lib/action_view/helpers/form_tag_helper.rb b/actionpack/lib/action_view/helpers/form_tag_helper.rb index 1ea870426a..49aa434020 100644 --- a/actionpack/lib/action_view/helpers/form_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/form_tag_helper.rb @@ -1,6 +1,7 @@ require 'cgi' require 'action_view/helpers/tag_helper' require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/string/output_safety' module ActionView # = Action View Form Tag Helpers @@ -24,8 +25,11 @@ module ActionView # * <tt>:method</tt> - The method to use when submitting the form, usually either "get" or "post". # If "put", "delete", or another verb is used, a hidden input with name <tt>_method</tt> # is added to simulate the verb over post. + # * <tt>:authenticity_token</tt> - Authenticity token to use in the form. Use only if you need to + # pass custom authenticity token string, or to not add authenticity_token field at all + # (by passing <tt>false</tt>). # * A list of parameters to feed to the URL the form will be posted to. - # * <tt>:remote</tt> - If set to true, will allow the Unobtrusive JavaScript drivers to control the + # * <tt>:remote</tt> - If set to true, will allow the Unobtrusive JavaScript drivers to control the # submit behaviour. By default this behaviour is an ajax submit. # # ==== Examples @@ -38,14 +42,20 @@ module ActionView # form_tag('/upload', :multipart => true) # # => <form action="/upload" method="post" enctype="multipart/form-data"> # - # <%= form_tag('/posts')do -%> + # <%= form_tag('/posts') do -%> # <div><%= submit_tag 'Save' %></div> # <% end -%> # # => <form action="/posts" method="post"><div><input type="submit" name="submit" value="Save" /></div></form> - # + # # <%= form_tag('/posts', :remote => true) %> # # => <form action="/posts" method="post" data-remote="true"> - # + # + # form_tag('http://far.away.com/form', :authenticity_token => false) + # # form without authenticity token + # + # form_tag('http://far.away.com/form', :authenticity_token => "cf50faa3fe97702ca1ae") + # # form with custom authenticity token + # def form_tag(url_for_options = {}, options = {}, *parameters_for_url, &block) html_options = html_options_for_form(url_for_options, options, *parameters_for_url) if block_given? @@ -67,7 +77,7 @@ module ActionView # * Any other key creates standard HTML attributes for the tag. # # ==== Examples - # select_tag "people", options_from_collection_for_select(@people, "name", "id") + # select_tag "people", options_from_collection_for_select(@people, "id", "name") # # <select id="people" name="people"><option value="1">David</option></select> # # select_tag "people", "<option>David</option>" @@ -93,10 +103,6 @@ module ActionView # # => <select disabled="disabled" id="destination" name="destination"><option>NYC</option> # # <option>Paris</option><option>Rome</option></select> def select_tag(name, option_tags = nil, options = {}) - if Array === option_tags - ActiveSupport::Deprecation.warn 'Passing an array of option_tags to select_tag implicitly joins them without marking them as HTML-safe. Pass option_tags.join.html_safe instead.', caller - end - html_name = (options[:multiple] == true && !name.to_s.ends_with?("[]")) ? "#{name}[]" : name if blank = options.delete(:include_blank) if blank.kind_of?(String) @@ -115,6 +121,7 @@ module ActionView # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input. # * <tt>:size</tt> - The number of visible characters that will fit in the input. # * <tt>:maxlength</tt> - The maximum number of characters that the browser will allow the user to enter. + # * <tt>:placeholder</tt> - The text contained in the field by default which is removed when the field receives focus. # * Any other key creates standard HTML attributes for the tag. # # ==== Examples @@ -124,6 +131,9 @@ module ActionView # text_field_tag 'query', 'Enter your search query here' # # => <input id="query" name="query" type="text" value="Enter your search query here" /> # + # text_field_tag 'search', nil, :placeholder => 'Enter search term...' + # # => <input id="search" name="search" placeholder="Enter search term..." type="text" /> + # # text_field_tag 'request', nil, :class => 'special_input' # # => <input class="special_input" id="request" name="request" type="text" /> # @@ -291,7 +301,7 @@ module ActionView end escape = options.key?("escape") ? options.delete("escape") : true - content = html_escape(content) if escape + content = ERB::Util.html_escape(content) if escape content_tag :textarea, content.to_s.html_safe, { "name" => name, "id" => sanitize_to_id(name) }.update(options) end @@ -351,12 +361,12 @@ module ActionView # Creates a submit button with the text <tt>value</tt> as the caption. # # ==== Options - # * <tt>:confirm => 'question?'</tt> - If present the unobtrusive JavaScript - # drivers will provide a prompt with the question specified. If the user accepts, + # * <tt>:confirm => 'question?'</tt> - If present the unobtrusive JavaScript + # drivers will provide a prompt with the question specified. If the user accepts, # the form is processed normally, otherwise no action is taken. # * <tt>:disabled</tt> - If true, the user will not be able to use this input. - # * <tt>:disable_with</tt> - Value of this parameter will be used as the value for a - # disabled version of the submit button when the form is submitted. This feature is + # * <tt>:disable_with</tt> - Value of this parameter will be used as the value for a + # disabled version of the submit button when the form is submitted. This feature is # provided by the unobtrusive JavaScript driver. # * Any other key creates standard HTML options for the tag. # @@ -383,7 +393,7 @@ module ActionView # # name="commit" type="submit" value="Edit" /> # # submit_tag "Save", :confirm => "Are you sure?" - # # => <input name='commit' type='submit' value='Save' + # # => <input name='commit' type='submit' value='Save' # data-confirm="Are you sure?" /> # def submit_tag(value = "Save changes", options = {}) @@ -394,12 +404,65 @@ module ActionView end if confirm = options.delete("confirm") - add_confirm_to_attributes!(options, confirm) + options["data-confirm"] = confirm end tag :input, { "type" => "submit", "name" => "commit", "value" => value }.update(options.stringify_keys) end + # Creates a button element that defines a <tt>submit</tt> button, + # <tt>reset</tt>button or a generic button which can be used in + # JavaScript, for example. You can use the button tag as a regular + # submit tag but it isn't supported in legacy browsers. However, + # the button tag allows richer labels such as images and emphasis, + # so this helper will also accept a block. + # + # ==== Options + # * <tt>:confirm => 'question?'</tt> - If present, the + # unobtrusive JavaScript drivers will provide a prompt with + # the question specified. If the user accepts, the form is + # processed normally, otherwise no action is taken. + # * <tt>:disabled</tt> - If true, the user will not be able to + # use this input. + # * <tt>:disable_with</tt> - Value of this parameter will be + # used as the value for a disabled version of the submit + # button when the form is submitted. This feature is provided + # by the unobtrusive JavaScript driver. + # * Any other key creates standard HTML options for the tag. + # + # ==== Examples + # button_tag + # # => <button name="button" type="submit">Button</button> + # + # button_tag(:type => 'button') do + # content_tag(:strong, 'Ask me!') + # end + # # => <button name="button" type="button"> + # <strong>Ask me!</strong> + # </button> + # + # button_tag "Checkout", :disable_with => "Please wait..." + # # => <button data-disable-with="Please wait..." name="button" + # type="submit">Checkout</button> + # + def button_tag(content_or_options = nil, options = nil, &block) + options = content_or_options if block_given? && content_or_options.is_a?(Hash) + options ||= {} + options.stringify_keys! + + if disable_with = options.delete("disable_with") + options["data-disable-with"] = disable_with + end + + if confirm = options.delete("confirm") + options["data-confirm"] = confirm + end + + options.reverse_merge! 'name' => 'button', 'type' => 'submit' + + content_tag :button, content_or_options || 'Button', options, &block + end + # Displays an image which when clicked will submit the form. # # <tt>source</tt> is passed to AssetTagHelper#path_to_image @@ -427,7 +490,7 @@ module ActionView options.stringify_keys! if confirm = options.delete("confirm") - add_confirm_to_attributes!(options, confirm) + options["data-confirm"] = confirm end tag :input, { "type" => "image", "src" => path_to_image(source) }.update(options.stringify_keys) @@ -529,17 +592,19 @@ 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") + html_options["authenticity_token"] = html_options.delete("authenticity_token") if html_options.has_key?("authenticity_token") end end def extra_tags_for_form(html_options) snowman_tag = tag(:input, :type => "hidden", - :name => "_e", :value => "☃".html_safe) + :name => "utf8", :value => "✓".html_safe) + authenticity_token = html_options.delete("authenticity_token") method = html_options.delete("method").to_s method_tag = case method @@ -548,10 +613,10 @@ module ActionView '' when /^post$/i, "", nil html_options["method"] = "post" - token_tag + token_tag(authenticity_token) else html_options["method"] = "post" - tag(:input, :type => "hidden", :name => "_method", :value => method) + token_tag + tag(:input, :type => "hidden", :name => "_method", :value => method) + token_tag(authenticity_token) end tags = snowman_tag << method_tag @@ -571,11 +636,12 @@ module ActionView output.safe_concat("</form>") end - def token_tag - unless protect_against_forgery? + def token_tag(token) + if token == false || !protect_against_forgery? '' else - tag(:input, :type => "hidden", :name => request_forgery_protection_token.to_s, :value => form_authenticity_token) + token = form_authenticity_token if token.nil? + tag(:input, :type => "hidden", :name => request_forgery_protection_token.to_s, :value => token) end end diff --git a/actionpack/lib/action_view/helpers/javascript_helper.rb b/actionpack/lib/action_view/helpers/javascript_helper.rb index 84f53b842c..cd3a3eac80 100644 --- a/actionpack/lib/action_view/helpers/javascript_helper.rb +++ b/actionpack/lib/action_view/helpers/javascript_helper.rb @@ -65,7 +65,8 @@ module ActionView # //]]> # </script> # - # +html_options+ may be a hash of attributes for the <script> tag. Example: + # +html_options+ may be a hash of attributes for the <tt>\<script></tt> + # tag. Example: # javascript_tag "alert('All is good')", :defer => 'defer' # # => <script defer="defer" type="text/javascript">alert('All is good')</script> # diff --git a/actionpack/lib/action_view/helpers/number_helper.rb b/actionpack/lib/action_view/helpers/number_helper.rb index f11027bc93..05a9c5b4f1 100644 --- a/actionpack/lib/action_view/helpers/number_helper.rb +++ b/actionpack/lib/action_view/helpers/number_helper.rb @@ -1,6 +1,7 @@ require 'active_support/core_ext/big_decimal/conversions' require 'active_support/core_ext/float/rounding' require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/string/output_safety' module ActionView # = Action View Number Helpers @@ -14,7 +15,7 @@ module ActionView # unchanged if can't be converted into a valid number. module NumberHelper - DEFAULT_CURRENCY_VALUES = { :format => "%u%n", :unit => "$", :separator => ".", :delimiter => ",", + DEFAULT_CURRENCY_VALUES = { :format => "%u%n", :negative_format => "-%u%n", :unit => "$", :separator => ".", :delimiter => ",", :precision => 2, :significant => false, :strip_insignificant_zeros => false } # Raised when argument +number+ param given to the helpers is invalid and @@ -47,51 +48,51 @@ module ActionView # number_to_phone(1235551234, :country_code => 1, :extension => 1343, :delimiter => ".") # => +1.123.555.1234 x 1343 def number_to_phone(number, options = {}) - return nil if number.nil? + return unless number 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 + raise InvalidNumberError, number + end if options[:raise] number = number.to_s.strip options = options.symbolize_keys - area_code = options[:area_code] || nil + area_code = options[:area_code] delimiter = options[:delimiter] || "-" - extension = options[:extension].to_s.strip || nil - country_code = options[:country_code] || nil + extension = options[:extension] + country_code = options[:country_code] - str = "" - str << "+#{country_code}#{delimiter}" unless country_code.blank? - str << if area_code - number.gsub!(/([0-9]{1,3})([0-9]{3})([0-9]{4}$)/,"(\\1) \\2#{delimiter}\\3") + if area_code + number.gsub!(/(\d{1,3})(\d{3})(\d{4}$)/,"(\\1) \\2#{delimiter}\\3") else - number.gsub!(/([0-9]{0,3})([0-9]{3})([0-9]{4})$/,"\\1#{delimiter}\\2#{delimiter}\\3") - number.starts_with?('-') ? number.slice!(1..-1) : number + number.gsub!(/(\d{0,3})(\d{3})(\d{4})$/,"\\1#{delimiter}\\2#{delimiter}\\3") + number.slice!(0, 1) if number.starts_with?('-') end + + str = [] + str << "+#{country_code}#{delimiter}" unless country_code.blank? + str << number str << " x #{extension}" unless extension.blank? - is_number_html_safe ? str.html_safe : str + ERB::Util.html_escape(str.join) end # Formats a +number+ into a currency string (e.g., $13.65). You can customize the format # in the +options+ hash. # # ==== Options - # * <tt>:locale</tt> - Sets the locale to be used for formatting (defaults to current locale). - # * <tt>:precision</tt> - Sets the level of precision (defaults to 2). - # * <tt>:unit</tt> - Sets the denomination of the currency (defaults to "$"). - # * <tt>:separator</tt> - Sets the separator between the units (defaults to "."). - # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to ","). - # * <tt>:format</tt> - Sets the format of the output string (defaults to "%u%n"). The field types are: - # - # %u The currency unit - # %n The number + # * <tt>:locale</tt> - Sets the locale to be used for formatting (defaults to current locale). + # * <tt>:precision</tt> - Sets the level of precision (defaults to 2). + # * <tt>:unit</tt> - Sets the denomination of the currency (defaults to "$"). + # * <tt>:separator</tt> - Sets the separator between the units (defaults to "."). + # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to ","). + # * <tt>:format</tt> - Sets the format for non-negative numbers (defaults to "%u%n"). + # Fields are <tt>%u</tt> for the currency, and <tt>%n</tt> + # for the number. + # * <tt>:negative_format</tt> - Sets the format for negative numbers (defaults to prepending + # an hyphen to the formatted number given by <tt>:format</tt>). + # Accepts the same fields than <tt>:format</tt>, except + # <tt>%n</tt> is here the absolute value of the number. # # ==== Examples # number_to_currency(1234567890.50) # => $1,234,567,890.50 @@ -99,12 +100,14 @@ module ActionView # number_to_currency(1234567890.506, :precision => 3) # => $1,234,567,890.506 # number_to_currency(1234567890.506, :locale => :fr) # => 1 234 567 890,506 € # + # number_to_currency(1234567890.50, :negative_format => "(%u%n)") + # # => ($1,234,567,890.51) # number_to_currency(1234567890.50, :unit => "£", :separator => ",", :delimiter => "") # # => £1234567890,50 # number_to_currency(1234567890.50, :unit => "£", :separator => ",", :delimiter => "", :format => "%n %u") # # => 1234567890,50 £ def number_to_currency(number, options = {}) - return nil if number.nil? + return unless number options.symbolize_keys! @@ -112,11 +115,17 @@ module ActionView currency = I18n.translate(:'number.currency.format', :locale => options[:locale], :default => {}) defaults = DEFAULT_CURRENCY_VALUES.merge(defaults).merge!(currency) + defaults[:negative_format] = "-" + options[:format] if options[:format] options = defaults.merge!(options) unit = options.delete(:unit) format = options.delete(:format) + if number.to_f < 0 + format = options.delete(:negative_format) + number = number.respond_to?("abs") ? number.abs : number.sub(/^-/, '') + end + begin value = number_with_precision(number, options.merge(:raise => true)) format.gsub(/%n/, value).gsub(/%u/, unit).html_safe @@ -149,7 +158,7 @@ module ActionView # number_to_percentage(302.24398923423, :precision => 5) # => 302.24399% # number_to_percentage(1000, :locale => :fr) # => 1 000,000% def number_to_percentage(number, options = {}) - return nil if number.nil? + return unless number options.symbolize_keys! @@ -260,13 +269,14 @@ module ActionView if number == 0 digits, rounded_number = 1, 0 else - digits = (Math.log10(number) + 1).floor - rounded_number = BigDecimal.new((number / 10 ** (digits - precision)).to_s).round.to_f * 10 ** (digits - precision) + digits = (Math.log10(number.abs) + 1).floor + rounded_number = (BigDecimal.new(number.to_s) / BigDecimal.new((10 ** (digits - precision)).to_f.to_s)).round.to_f * 10 ** (digits - precision) + digits = (Math.log10(rounded_number.abs) + 1).floor # After rounding, the number of digits may have changed end - precision = precision - digits + 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 + rounded_number = BigDecimal.new(number.to_s).round(precision).to_f end formatted_number = number_with_delimiter("%01.#{precision}f" % rounded_number, options) if strip_insignificant_zeros @@ -325,7 +335,7 @@ module ActionView 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) @@ -359,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 @@ -415,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" @@ -447,6 +457,8 @@ module ActionView #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) + inverted_du = DECIMAL_UNITS.invert + units = options.delete :units unit_exponents = case units when Hash @@ -457,10 +469,10 @@ module ActionView 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} + end.keys.map{|e_name| inverted_du[e_name] }.sort_by{|e| -e} - number_exponent = Math.log10(number).floor - display_exponent = unit_exponents.find{|e| number_exponent >= e } + number_exponent = number != 0 ? Math.log10(number.abs).floor : 0 + 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/output_safety_helper.rb b/actionpack/lib/action_view/helpers/output_safety_helper.rb new file mode 100644 index 0000000000..a035dd70ad --- /dev/null +++ b/actionpack/lib/action_view/helpers/output_safety_helper.rb @@ -0,0 +1,38 @@ +require 'active_support/core_ext/string/output_safety' + +module ActionView #:nodoc: + # = Action View Raw Output Helper + module Helpers #:nodoc: + module OutputSafetyHelper + # This method outputs without escaping a string. Since escaping tags is + # now default, this can be used when you don't want Rails to automatically + # escape tags. This is not recommended if the data is coming from the user's + # input. + # + # For example: + # + # <%=raw @user.name %> + def raw(stringish) + stringish.to_s.html_safe + end + + # This method returns a html safe string similar to what <tt>Array#join</tt> + # would return. All items in the array, including the supplied separator, are + # html escaped unless they are html safe, and the returned string is marked + # as html safe. + # + # safe_join(["<p>foo</p>".html_safe, "<p>bar</p>"], "<br />") + # # => "<p>foo</p><br /><p>bar</p>" + # + # safe_join(["<p>foo</p>".html_safe, "<p>bar</p>".html_safe], "<br />".html_safe) + # # => "<p>foo</p><br /><p>bar</p>" + # + def safe_join(array, sep=$,) + sep ||= "".html_safe + sep = ERB::Util.html_escape(sep) + + array.map { |i| ERB::Util.html_escape(i) }.join(sep).html_safe + end + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_view/helpers/prototype_helper.rb b/actionpack/lib/action_view/helpers/prototype_helper.rb index 99f9363a9a..18e303778c 100644 --- a/actionpack/lib/action_view/helpers/prototype_helper.rb +++ b/actionpack/lib/action_view/helpers/prototype_helper.rb @@ -1,6 +1,7 @@ require 'set' require 'active_support/json' require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/string/output_safety' module ActionView # = Action View Prototype Helpers @@ -102,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', @@ -130,8 +131,7 @@ module ActionView "new Ajax.Updater(#{update}, " url_options = options[:url] - url_options = url_options.merge(:escape => false) if url_options.is_a?(Hash) - function << "'#{html_escape(escape_javascript(url_for(url_options)))}'" + function << "'#{ERB::Util.html_escape(escape_javascript(url_for(url_options)))}'" function << ", #{javascript_options})" function = "#{options[:before]}; #{function}" if options[:before] @@ -161,8 +161,8 @@ module ActionView # JavaScriptGenerator generates blocks of JavaScript code that allow you # to change the content and presentation of multiple DOM elements. Use - # this in your Ajax response bodies, either in a <script> tag or as plain - # JavaScript sent with a Content-type of "text/javascript". + # this in your Ajax response bodies, either in a <tt>\<script></tt> tag + # or as plain JavaScript sent with a Content-type of "text/javascript". # # Create new instances with PrototypeHelper#update_page or with # ActionController::Base#render, then call +insert_html+, +replace_html+, @@ -224,7 +224,7 @@ module ActionView # # You can also use PrototypeHelper#update_page_tag instead of # PrototypeHelper#update_page to wrap the generated JavaScript in a - # <script> tag. + # <tt>\<script></tt> tag. module GeneratorMethods def to_s #:nodoc: (@lines * $/).tap do |javascript| @@ -546,7 +546,7 @@ module ActionView end def with_formats(*args) - @context ? @context.update_details(:formats => args) { yield } : yield + @context ? @context.lookup_context.update_details(:formats => args) { yield } : yield end def javascript_object_for(object) @@ -579,15 +579,15 @@ module ActionView # page.hide 'spinner' # end def update_page(&block) - JavaScriptGenerator.new(view_context, &block).to_s.html_safe + JavaScriptGenerator.new(self, &block).to_s.html_safe end - # Works like update_page but wraps the generated JavaScript in a <script> - # tag. Use this to include generated JavaScript in an ERb template. - # See JavaScriptGenerator for more information. + # Works like update_page but wraps the generated JavaScript in a + # <tt>\<script></tt> tag. Use this to include generated JavaScript in an + # ERb template. See JavaScriptGenerator for more information. # - # +html_options+ may be a hash of <script> attributes to be passed - # to ActionView::Helpers::JavaScriptHelper#javascript_tag. + # +html_options+ may be a hash of <tt>\<script></tt> attributes to be + # passed to ActionView::Helpers::JavaScriptHelper#javascript_tag. def update_page_tag(html_options = {}, &block) javascript_tag update_page(&block), html_options end diff --git a/actionpack/lib/action_view/helpers/raw_output_helper.rb b/actionpack/lib/action_view/helpers/raw_output_helper.rb deleted file mode 100644 index da7599fa8f..0000000000 --- a/actionpack/lib/action_view/helpers/raw_output_helper.rb +++ /dev/null @@ -1,18 +0,0 @@ -module ActionView #:nodoc: - # = Action View Raw Output Helper - module Helpers #:nodoc: - module RawOutputHelper - # This method outputs without escaping a string. Since escaping tags is - # now default, this can be used when you don't want Rails to automatically - # escape tags. This is not recommended if the data is coming from the user's - # input. - # - # For example: - # - # <%=raw @user.name %> - def raw(stringish) - stringish.to_s.html_safe - end - end - end -end
\ No newline at end of file diff --git a/actionpack/lib/action_view/helpers/record_tag_helper.rb b/actionpack/lib/action_view/helpers/record_tag_helper.rb index e4a9210cde..4d300a1469 100644 --- a/actionpack/lib/action_view/helpers/record_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/record_tag_helper.rb @@ -52,9 +52,9 @@ module ActionView # # <li id="person_123" class="person bar">... # - def content_tag_for(tag_name, record, *args, &block) - prefix = args.first.is_a?(Hash) ? nil : args.shift - options = args.extract_options! + def content_tag_for(tag_name, record, prefix = nil, options = nil, &block) + options, prefix = prefix, nil if prefix.is_a?(Hash) + options ||= {} options.merge!({ :class => "#{dom_class(record, prefix)} #{options[:class]}".strip, :id => dom_id(record, prefix) }) content_tag(tag_name, options, &block) end diff --git a/actionpack/lib/action_view/helpers/sanitize_helper.rb b/actionpack/lib/action_view/helpers/sanitize_helper.rb index d82005fa24..0fee34f8a4 100644 --- a/actionpack/lib/action_view/helpers/sanitize_helper.rb +++ b/actionpack/lib/action_view/helpers/sanitize_helper.rb @@ -9,7 +9,7 @@ module ActionView # These helper methods extend Action View making them callable within your template files. module SanitizeHelper extend ActiveSupport::Concern - # This +sanitize+ helper will html encode all tags and strip all attributes that + # This +sanitize+ helper will html encode all tags and strip all attributes that # aren't specifically allowed. # # It also strips href/src tags with invalid protocols, like javascript: especially. @@ -21,7 +21,7 @@ module ActionView # # You can add or remove tags/attributes if you want to customize it a bit. # See ActionView::Base for full docs on the available options. You can add - # tags/attributes for single uses of +sanitize+ by passing either the + # tags/attributes for single uses of +sanitize+ by passing either the # <tt>:attributes</tt> or <tt>:tags</tt> options: # # Normal Use diff --git a/actionpack/lib/action_view/helpers/tag_helper.rb b/actionpack/lib/action_view/helpers/tag_helper.rb index 5d032b32a7..786af5ca58 100644 --- a/actionpack/lib/action_view/helpers/tag_helper.rb +++ b/actionpack/lib/action_view/helpers/tag_helper.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/string/output_safety' require 'set' module ActionView @@ -7,16 +8,14 @@ module ActionView # Provides methods to generate HTML tags programmatically when you can't use # a Builder. By default, they output XHTML compliant tags. module TagHelper - include ERB::Util - extend ActiveSupport::Concern include CaptureHelper BOOLEAN_ATTRIBUTES = %w(disabled readonly multiple checked autobuffer autoplay controls loop selected hidden scoped async defer reversed ismap seemless muted required - autofocus novalidate formnovalidate open).to_set - BOOLEAN_ATTRIBUTES.merge(BOOLEAN_ATTRIBUTES.map {|attr| attr.to_sym }) + autofocus novalidate formnovalidate open pubdate).to_set + BOOLEAN_ATTRIBUTES.merge(BOOLEAN_ATTRIBUTES.map {|attribute| attribute.to_sym }) # Returns an empty HTML tag of type +name+ which by default is XHTML # compliant. Set +open+ to true to create an open tag compatible @@ -25,9 +24,21 @@ module ActionView # escaping. # # ==== Options - # The +options+ hash is used with attributes with no value like (<tt>disabled</tt> and - # <tt>readonly</tt>), which you can give a value of true in the +options+ hash. You can use - # symbols or strings for the attribute names. + # You can use symbols or strings for the attribute names. + # + # Use +true+ with boolean attributes that can render with no value, like + # +disabled+ and +readonly+. + # + # HTML5 <tt>data-*</tt> attributes can be set with a single +data+ key + # pointing to a hash of sub-attributes. + # + # To play nicely with JavaScript conventions sub-attributes are dasherized. + # For example, a key +user_id+ would render as <tt>data-user-id</tt> and + # thus accessed as <tt>dataset.userId</tt>. + # + # Values are encoded to JSON, with the exception of strings and symbols. + # This may come in handy when using jQuery's HTML5-aware <tt>.data()<tt> + # from 1.4.3. # # ==== Examples # tag("br") @@ -36,14 +47,17 @@ module ActionView # tag("br", nil, true) # # => <br> # - # tag("input", { :type => 'text', :disabled => true }) + # tag("input", :type => 'text', :disabled => true) # # => <input type="text" disabled="disabled" /> # - # tag("img", { :src => "open & shut.png" }) + # tag("img", :src => "open & shut.png") # # => <img src="open & shut.png" /> # - # tag("img", { :src => "open & shut.png" }, false, false) + # tag("img", {:src => "open & shut.png"}, false, false) # # => <img src="open & shut.png" /> + # + # tag("div", :data => {:name => 'Stephen', :city_state => %w(Chicago IL)}) + # # => <div data-name="Stephen" data-city-state="["Chicago","IL"]" /> def tag(name, options = nil, open = false, escape = true) "<#{name}#{tag_options(options, escape) if options}#{open ? ">" : " />"}".html_safe end @@ -118,11 +132,19 @@ module ActionView unless options.blank? attrs = [] options.each_pair do |key, value| - if BOOLEAN_ATTRIBUTES.include?(key) + if key.to_s == 'data' && value.is_a?(Hash) + value.each do |k, v| + if !v.is_a?(String) && !v.is_a?(Symbol) + v = v.to_json + end + v = ERB::Util.html_escape(v) if escape + attrs << %(data-#{k.to_s.dasherize}="#{v}") + end + elsif BOOLEAN_ATTRIBUTES.include?(key) attrs << %(#{key}="#{key}") if value elsif !value.nil? final_value = value.is_a?(Array) ? value.join(" ") : value - final_value = html_escape(final_value) if escape + final_value = ERB::Util.html_escape(final_value) if escape attrs << %(#{key}="#{final_value}") end end diff --git a/actionpack/lib/action_view/helpers/text_helper.rb b/actionpack/lib/action_view/helpers/text_helper.rb index c1de5c8cb3..2d3c5fe7e7 100644 --- a/actionpack/lib/action_view/helpers/text_helper.rb +++ b/actionpack/lib/action_view/helpers/text_helper.rb @@ -9,6 +9,24 @@ module ActionView # and transforming strings, which can reduce the amount of inline Ruby code in # your views. These helper methods extend Action View making them callable # within your template files. + # + # ==== Sanitization + # + # Most text helpers by default sanitize the given content, but do not escape it. + # This means HTML tags will appear in the page but all malicious code will be removed. + # Let's look at some examples using the +simple_format+ method: + # + # simple_format('<a href="http://example.com/">Example</a>') + # # => "<p><a href=\"http://example.com/\">Example</a></p>" + # + # simple_format('<a href="javascript:alert('no!')">Example</a>') + # # => "<p><a>Example</a></p>" + # + # If you want to escape all content, you should invoke the +h+ method before + # calling the text helper. + # + # simple_format h('<a href="http://example.com/">Example</a>') + # # => "<p><a href=\"http://example.com/\">Example</a></p>" module TextHelper extend ActiveSupport::Concern @@ -134,6 +152,8 @@ module ActionView # excerpt('This is an example', 'an', 5) # => ...s is an exam... # excerpt('This is also an example', 'an', 8, '<chop> ') # => <chop> is also an example def excerpt(text, phrase, *args) + return unless text && phrase + options = args.extract_options! unless args.empty? options[:radius] = args[0] || 100 @@ -141,21 +161,16 @@ module ActionView end options.reverse_merge!(:radius => 100, :omission => "...") - if text && phrase - phrase = Regexp.escape(phrase) + phrase = Regexp.escape(phrase) + return unless found_pos = text.mb_chars =~ /(#{phrase})/i - if found_pos = text.mb_chars =~ /(#{phrase})/i - start_pos = [ found_pos - options[:radius], 0 ].max - end_pos = [ [ found_pos + phrase.mb_chars.length + options[:radius] - 1, 0].max, text.mb_chars.length ].min + start_pos = [ found_pos - options[:radius], 0 ].max + end_pos = [ [ found_pos + phrase.mb_chars.length + options[:radius] - 1, 0].max, text.mb_chars.length ].min - prefix = start_pos > 0 ? options[:omission] : "" - postfix = end_pos < text.mb_chars.length - 1 ? options[:omission] : "" + prefix = start_pos > 0 ? options[:omission] : "" + postfix = end_pos < text.mb_chars.length - 1 ? options[:omission] : "" - prefix + text.mb_chars[start_pos..end_pos].strip + postfix - else - nil - end - end + prefix + text.mb_chars[start_pos..end_pos].strip + postfix end # Attempts to pluralize the +singular+ word unless +count+ is 1. If @@ -219,6 +234,10 @@ module ActionView # # You can pass any HTML attributes into <tt>html_options</tt>. These # will be added to all created paragraphs. + # + # ==== Options + # * <tt>:sanitize</tt> - If +false+, does not sanitize +text+. + # # ==== Examples # my_text = "Here is some basic text...\n...with a line break." # @@ -232,6 +251,9 @@ module ActionView # # simple_format("Look ma! A class!", :class => 'description') # # => "<p class='description'>Look ma! A class!</p>" + # + # simple_format("<span>I'm allowed!</span> It's true.", {}, :sanitize => false) + # # => "<p><span>I'm allowed!</span> It's true.</p>" def simple_format(text, html_options={}, options={}) text = ''.html_safe if text.nil? start_tag = tag('p', html_options, true) @@ -263,7 +285,7 @@ module ActionView # # post_body = "Welcome to my new blog at http://www.myblog.com/. Please e-mail me at me@email.com." # auto_link(post_body, :html => { :target => '_blank' }) do |text| - # truncate(text, 15) + # truncate(text, :length => 15) # end # # => "Welcome to my new blog at <a href=\"http://www.myblog.com/\" target=\"_blank\">http://www.m...</a>. # Please e-mail me at <a href=\"mailto:me@email.com\">me@email.com</a>." @@ -345,10 +367,10 @@ module ActionView values.unshift(first_value) cycle = get_cycle(name) - if (cycle.nil? || cycle.values != values) + unless cycle && cycle.values == values cycle = set_cycle(name, Cycle.new(*values)) end - return cycle.to_s + cycle.to_s end # Returns the current cycle string after a cycle has been started. Useful @@ -365,7 +387,7 @@ module ActionView # <% end %> def current_cycle(name = "default") cycle = get_cycle(name) - cycle.current_value unless cycle.nil? + cycle.current_value if cycle end # Resets a cycle so that it starts from the first element the next time @@ -389,7 +411,7 @@ module ActionView # </table> def reset_cycle(name = "default") cycle = get_cycle(name) - cycle.reset unless cycle.nil? + cycle.reset if cycle end class Cycle #:nodoc: @@ -444,7 +466,7 @@ module ActionView end AUTO_LINK_RE = %r{ - (?: ([\w+.:-]+:)// | www\. ) + (?: ([0-9A-Za-z+.:-]+:)// | www\. ) [^\s<]+ }x @@ -462,7 +484,7 @@ module ActionView text.gsub(AUTO_LINK_RE) do scheme, href = $1, $& punctuation = [] - + if auto_linked?($`, $') # do not change string; URL is already linked href @@ -507,7 +529,7 @@ module ActionView end end end - + # Detects already linked context or position in the middle of a tag def auto_linked?(left, right) (left =~ AUTO_LINK_CRE[0] and right =~ AUTO_LINK_CRE[1]) or diff --git a/actionpack/lib/action_view/helpers/translation_helper.rb b/actionpack/lib/action_view/helpers/translation_helper.rb index dac9c28ab7..59e6ce878f 100644 --- a/actionpack/lib/action_view/helpers/translation_helper.rb +++ b/actionpack/lib/action_view/helpers/translation_helper.rb @@ -1,38 +1,56 @@ require 'action_view/helpers/tag_helper' +require 'i18n/exceptions' + +module I18n + class ExceptionHandler + include Module.new { + def call(exception, locale, key, options) + exception.is_a?(MissingTranslationData) ? super.html_safe : super + end + } + end +end module ActionView # = Action View Translation Helpers module Helpers module TranslationHelper # Delegates to I18n#translate but also performs three additional functions. - # First, it'll catch MissingTranslationData exceptions and turn them into - # inline spans that contains the missing key, such that you can see in a - # view what is missing where. # - # Second, it'll scope the key by the current partial if the key starts - # with a period. So if you call <tt>translate(".foo")</tt> from the - # <tt>people/index.html.erb</tt> template, you'll actually be calling + # First, it'll pass the :rescue_format => :html option to I18n so that any caught + # MissingTranslationData exceptions will be turned into inline spans that + # + # * have a "translation-missing" class set, + # * contain the missing key as a title attribute and + # * a titleized version of the last key segment as a text. + # + # 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 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 + # with a period. So if you call <tt>translate(".foo")</tt> from the + # <tt>people/index.html.erb</tt> template, you'll actually be calling # <tt>I18n.translate("people.index.foo")</tt>. This makes it less repetitive # to translate many keys within the same partials and gives you a simple framework - # for scoping them consistently. If you don't prepend the key with a period, + # for scoping them consistently. If you don't prepend the key with a period, # nothing is converted. # - # Third, it'll mark the translation as safe HTML if the key has the suffix - # "_html" or the last element of the key is the word "html". For example, - # calling translate("footer_html") or translate("footer.html") will return + # Third, it'll mark the translation as safe HTML if the key has the suffix + # "_html" or the last element of the key is the word "html". For example, + # calling translate("footer_html") or translate("footer.html") will return # a safe HTML string that won't be escaped by other HTML helper methods. This # naming convention helps to identify translations that include HTML tags so that # you know what kind of output to expect when you call translate in a template. def translate(key, options = {}) - translation = I18n.translate(scope_key_by_partial(key), options.merge!(:raise => true)) + options.merge!(:rescue_format => :html) unless options.key?(:rescue_format) + translation = I18n.translate(scope_key_by_partial(key), options) if html_safe_translation_key?(key) && translation.respond_to?(:html_safe) translation.html_safe else translation end - rescue I18n::MissingTranslationData => e - keys = I18n.normalize_keys(e.locale, e.key, e.options[:scope]) - content_tag('span', keys.join(', '), :class => 'translation_missing') end alias :t :translate @@ -45,8 +63,8 @@ module ActionView private def scope_key_by_partial(key) if key.to_s.first == "." - if @_virtual_path - @_virtual_path.gsub(%r{/_?}, ".") + key.to_s + if (path = @_template && @_template.virtual_path) + path.gsub(%r{/_?}, ".") + key.to_s 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 a5c6718c58..2cd2dca711 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 'active_support/core_ext/string/output_safety' require 'action_dispatch' module ActionView @@ -13,7 +14,7 @@ module ActionView module UrlHelper # This helper may be included in any class that includes the # URL helpers of a routes (routes.url_helpers). Some methods - # provided here will only work in the4 context of a request + # provided here will only work in the context of a request # (link_to_unless_current, for instance), which must be provided # as a method called #request on the context. @@ -22,6 +23,10 @@ module ActionView include ActionDispatch::Routing::UrlFor include TagHelper + def _routes_context + controller + end + # Need to map default url options to controller one. # def default_url_options(*args) #:nodoc: # controller.send(:default_url_options, *args) @@ -91,7 +96,7 @@ module ActionView # # => javascript:history.back() def url_for(options = {}) options ||= {} - url = case options + case options when String options when Hash @@ -102,8 +107,6 @@ module ActionView else polymorphic_path(options) end - - url end # Creates a link tag of the given +name+ using a URL created by the set @@ -235,16 +238,11 @@ module ActionView html_options = convert_options_to_data_attributes(options, html_options) url = url_for(options) - if html_options - html_options = html_options.stringify_keys - href = html_options['href'] - tag_options = tag_options(html_options) - else - tag_options = nil - end + href = html_options['href'] + tag_options = tag_options(html_options) - href_attr = "href=\"#{html_escape(url)}\"" unless href - "<a #{href_attr}#{tag_options}>#{html_escape(name || url)}</a>".html_safe + href_attr = "href=\"#{ERB::Util.html_escape(url)}\"" unless href + "<a #{href_attr}#{tag_options}>#{ERB::Util.html_escape(name || url)}</a>".html_safe end end @@ -255,8 +253,9 @@ module ActionView # using the +link_to+ method with the <tt>:method</tt> modifier as described in # the +link_to+ documentation. # - # The generated form element has a class name of <tt>button_to</tt> - # to allow styling of the form itself and its children. You can control + # By default, the generated form element has a class name of <tt>button_to</tt> + # to allow styling of the form itself and its children. This can be changed + # using the <tt>:form_class</tt> modifier within +html_options+. You can control # the form submission and input element behavior using +html_options+. # This method accepts the <tt>:method</tt> and <tt>:confirm</tt> modifiers # described in the +link_to+ documentation. If no <tt>:method</tt> modifier @@ -269,13 +268,16 @@ module ActionView # The +options+ hash accepts the same options as url_for. # # There are a few special +html_options+: - # * <tt>:method</tt> - Specifies the anchor name to be appended to the path. - # * <tt>:disabled</tt> - Specifies the anchor name to be appended to the path. + # * <tt>:method</tt> - Symbol of HTTP verb. Supported verbs are <tt>:post</tt>, <tt>:get</tt>, + # <tt>:delete</tt> and <tt>:put</tt>. By default it will be <tt>:post</tt>. + # * <tt>:disabled</tt> - If set to true, it will generate a disabled button. # * <tt>:confirm</tt> - This will use the unobtrusive JavaScript driver to # prompt with the question specified. If the user accepts, the link is # processed normally, otherwise no action is taken. # * <tt>:remote</tt> - If set to true, will allow the Unobtrusive JavaScript drivers to control the # submit behaviour. By default this behaviour is an ajax submit. + # * <tt>:form_class</tt> - This controls the class of the form within which the submit button will + # be placed # # ==== Examples # <%= button_to "New", :action => "new" %> @@ -284,6 +286,12 @@ module ActionView # # </form>" # # + # <%= button_to "New", :action => "new", :form_class => "new-thing" %> + # # => "<form method="post" action="/controller/new" class="new-thing"> + # # <div><input value="New" type="submit" /></div> + # # </form>" + # + # # <%= button_to "Delete Image", { :action => "delete", :id => @image.id }, # :confirm => "Are you sure?", :method => :delete %> # # => "<form method="post" action="/images/delete/1" class="button_to"> @@ -313,6 +321,7 @@ module ActionView end form_method = method.to_s == 'get' ? 'get' : 'post' + form_class = html_options.delete('form_class') || 'button_to' remote = html_options.delete('remote') @@ -328,7 +337,7 @@ module ActionView html_options.merge!("type" => "submit", "value" => name) - ("<form method=\"#{form_method}\" action=\"#{html_escape(url)}\" #{"data-remote=\"true\"" if remote} class=\"button_to\"><div>" + + ("<form method=\"#{form_method}\" action=\"#{ERB::Util.html_escape(url)}\" #{"data-remote=\"true\"" if remote} class=\"#{ERB::Util.html_escape(form_class)}\"><div>" + method_tag + tag("input", html_options) + request_token_tag + "</div></form>").html_safe end @@ -367,8 +376,8 @@ module ActionView # "Go Back" link instead of a link to the comments page, we could do something like this... # # <%= - # link_to_unless_current("Comment", { :controller => 'comments', :action => 'new}) do - # link_to("Go back", { :controller => 'posts', :action => 'index' }) + # link_to_unless_current("Comment", { :controller => "comments", :action => "new" }) do + # link_to("Go back", { :controller => "posts", :action => "index" }) # end # %> def link_to_unless_current(name, options = {}, html_options = {}, &block) @@ -474,43 +483,41 @@ module ActionView # :subject => "This is an example email" # # => <a href="mailto:me@domain.com?cc=ccaddress@domain.com&subject=This%20is%20an%20example%20email">My email</a> def mail_to(email_address, name = nil, html_options = {}) - email_address = html_escape(email_address) + email_address = ERB::Util.html_escape(email_address) html_options = html_options.stringify_keys encode = html_options.delete("encode").to_s - cc, bcc, subject, body = html_options.delete("cc"), html_options.delete("bcc"), html_options.delete("subject"), html_options.delete("body") - extras = [] - extras << "cc=#{Rack::Utils.escape(cc).gsub("+", "%20")}" unless cc.nil? - extras << "bcc=#{Rack::Utils.escape(bcc).gsub("+", "%20")}" unless bcc.nil? - extras << "body=#{Rack::Utils.escape(body).gsub("+", "%20")}" unless body.nil? - extras << "subject=#{Rack::Utils.escape(subject).gsub("+", "%20")}" unless subject.nil? - extras = extras.empty? ? '' : '?' + html_escape(extras.join('&')) + extras = %w{ cc bcc body subject }.map { |item| + option = html_options.delete(item) || next + "#{item}=#{Rack::Utils.escape(option).gsub("+", "%20")}" + }.compact + extras = extras.empty? ? '' : '?' + ERB::Util.html_escape(extras.join('&')) email_address_obfuscated = email_address.dup - email_address_obfuscated.gsub!(/@/, html_options.delete("replace_at")) if html_options.has_key?("replace_at") - email_address_obfuscated.gsub!(/\./, html_options.delete("replace_dot")) if html_options.has_key?("replace_dot") - - string = '' - - if encode == "javascript" - "document.write('#{content_tag("a", name || email_address_obfuscated.html_safe, html_options.merge("href" => "mailto:#{email_address}#{extras}".html_safe))}');".each_byte do |c| + email_address_obfuscated.gsub!(/@/, html_options.delete("replace_at")) if html_options.key?("replace_at") + email_address_obfuscated.gsub!(/\./, html_options.delete("replace_dot")) if html_options.key?("replace_dot") + case encode + when "javascript" + string = '' + html = content_tag("a", name || email_address_obfuscated.html_safe, html_options.merge("href" => "mailto:#{email_address}#{extras}".html_safe)) + html = escape_javascript(html) + "document.write('#{html}');".each_byte do |c| string << sprintf("%%%x", c) end "<script type=\"#{Mime::JS}\">eval(decodeURIComponent('#{string}'))</script>".html_safe - elsif encode == "hex" - email_address_encoded = '' - email_address_obfuscated.each_byte do |c| - email_address_encoded << sprintf("&#%d;", c) - end - - protocol = 'mailto:' - protocol.each_byte { |c| string << sprintf("&#%d;", c) } - - email_address.each_byte do |c| + when "hex" + email_address_encoded = email_address_obfuscated.unpack('C*').map {|c| + sprintf("&#%d;", c) + }.join + + string = 'mailto:'.unpack('C*').map { |c| + sprintf("&#%d;", c) + }.join + email_address.unpack('C*').map { |c| char = c.chr - string << (char =~ /\w/ ? sprintf("%%%x", c) : char) - end + char =~ /\w/ ? sprintf("%%%x", c) : char + }.join + content_tag "a", name || email_address_encoded.html_safe, html_options.merge("href" => "#{string}#{extras}".html_safe) else content_tag "a", name || email_address_obfuscated.html_safe, html_options.merge("href" => "mailto:#{email_address}#{extras}".html_safe) @@ -586,34 +593,31 @@ module ActionView private def convert_options_to_data_attributes(options, html_options) - html_options = {} if html_options.nil? - html_options = html_options.stringify_keys + if html_options.nil? + link_to_remote_options?(options) ? {'data-remote' => 'true'} : {} + else + html_options = html_options.stringify_keys + html_options['data-remote'] = 'true' if link_to_remote_options?(options) || link_to_remote_options?(html_options) - if (options.is_a?(Hash) && options.key?('remote') && options.delete('remote')) || (html_options.is_a?(Hash) && html_options.key?('remote') && html_options.delete('remote')) - html_options['data-remote'] = 'true' - end + disable_with = html_options.delete("disable_with") + confirm = html_options.delete('confirm') + method = html_options.delete('method') - confirm = html_options.delete("confirm") + html_options["data-disable-with"] = disable_with if disable_with + html_options["data-confirm"] = confirm if confirm + add_method_to_attributes!(html_options, method) if method - if html_options.key?("popup") - ActiveSupport::Deprecation.warn(":popup has been deprecated", caller) + html_options end - - method, href = html_options.delete("method"), html_options['href'] - - add_confirm_to_attributes!(html_options, confirm) if confirm - add_method_to_attributes!(html_options, method) if method - - html_options end - def add_confirm_to_attributes!(html_options, confirm) - html_options["data-confirm"] = confirm if confirm + def link_to_remote_options?(options) + options.is_a?(Hash) && options.key?('remote') && options.delete('remote') end def add_method_to_attributes!(html_options, method) - html_options["rel"] = "nofollow" if method && method.to_s.downcase != "get" - html_options["data-method"] = method if method + html_options["rel"] = "nofollow" if method.to_s.downcase != "get" + html_options["data-method"] = method end def options_for_javascript(options) 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/log_subscriber.rb b/actionpack/lib/action_view/log_subscriber.rb index 443a0eafd1..29ffbd6fdd 100644 --- a/actionpack/lib/action_view/log_subscriber.rb +++ b/actionpack/lib/action_view/log_subscriber.rb @@ -7,7 +7,7 @@ module ActionView message = "Rendered #{from_rails_root(event.payload[:identifier])}" message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout] message << (" (%.1fms)" % event.duration) - info(message) + info(message) end alias :render_partial :render_template alias :render_collection :render_template diff --git a/actionpack/lib/action_view/lookup_context.rb b/actionpack/lib/action_view/lookup_context.rb index 3ea8b86af1..06c607931d 100644 --- a/actionpack/lib/action_view/lookup_context.rb +++ b/actionpack/lib/action_view/lookup_context.rb @@ -10,7 +10,7 @@ module ActionView # this key is generated just once during the request, it speeds up all cache accesses. class LookupContext #:nodoc: mattr_accessor :fallbacks - @@fallbacks = [FileSystemResolver.new(""), FileSystemResolver.new("/")] + @@fallbacks = FallbackFileSystemResolver.instances mattr_accessor :registered_details self.registered_details = [] @@ -61,6 +61,7 @@ module ActionView def initialize(view_paths, details = {}) @details, @details_key = { :handlers => default_handlers }, nil @frozen_formats, @skip_default_locale = false, false + @cache = true self.view_paths = view_paths self.registered_detail_setters.each do |key, setter| @@ -77,17 +78,17 @@ module ActionView @view_paths = ActionView::Base.process_view_paths(paths) end - def find(name, prefix = nil, partial = false) - @view_paths.find(*args_for_lookup(name, prefix, partial)) + def find(name, prefixes = [], partial = false, keys = []) + @view_paths.find(*args_for_lookup(name, prefixes, partial, keys)) end alias :find_template :find - def find_all(name, prefix = nil, partial = false) - @view_paths.find_all(*args_for_lookup(name, prefix, partial)) + def find_all(name, prefixes = [], partial = false, keys = []) + @view_paths.find_all(*args_for_lookup(name, prefixes, partial, keys)) end - def exists?(name, prefix = nil, partial = false) - @view_paths.exists?(*args_for_lookup(name, prefix, partial)) + def exists?(name, prefixes = [], partial = false, keys = []) + @view_paths.exists?(*args_for_lookup(name, prefixes, partial, keys)) end alias :template_exists? :exists? @@ -106,18 +107,26 @@ module ActionView protected - def args_for_lookup(name, prefix, partial) #:nodoc: - name, prefix = normalize_name(name, prefix) - [name, prefix, partial || false, @details, details_key] + def args_for_lookup(name, prefixes, partial, keys) #:nodoc: + name, prefixes = normalize_name(name, prefixes) + [name, prefixes, partial || false, @details, details_key, keys] 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) #:nodoc: + def normalize_name(name, prefixes) #:nodoc: name = name.to_s.gsub(handlers_regexp, '') parts = name.split('/') - return parts.pop, [prefix, *parts].compact.join("/") + name = parts.pop + + prefixes = if prefixes.blank? + [parts.join('/')] + else + prefixes.map { |prefix| [prefix, *parts].compact.join('/') } + end + + return name, prefixes end def default_handlers #:nodoc: @@ -130,10 +139,20 @@ module ActionView end module Details + attr_accessor :cache + # 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) + @details_key ||= DetailsKey.get(@details) if @cache + end + + # Temporary skip passing the details_key forward. + def disable_cache + old_value, @cache = @cache, false + yield + ensure + @cache = old_value end # Freeze the current formats in the lookup context. By freezing them, you are guaranteeing @@ -145,11 +164,11 @@ module ActionView @frozen_formats = true end - # Overload formats= to reject [:"*/*"] values. + # Overload formats= to reject ["*/*"] values. def formats=(values) if values && values.size == 1 value = values.first - values = nil if value == :"*/*" + values = nil if value == "*/*" values << :html if value == :js end super(values) @@ -167,11 +186,11 @@ module ActionView 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 + # to original_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) if value - config = I18n.config.respond_to?(:i18n_config) ? I18n.config.i18n_config : I18n.config + config = I18n.config.respond_to?(:original_config) ? I18n.config.original_config : I18n.config config.locale = value end @@ -226,4 +245,4 @@ module ActionView include Details include ViewPaths end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_view/render/partials.rb b/actionpack/lib/action_view/partials.rb index 459aae94a2..c181689e62 100644 --- a/actionpack/lib/action_view/render/partials.rb +++ b/actionpack/lib/action_view/partials.rb @@ -12,19 +12,48 @@ module ActionView # # <%= render :partial => "account" %> # - # This would render "advertiser/_account.erb" and pass the instance variable @account in as a local variable + # This would render "advertiser/_account.html.erb" and pass the instance variable @account in as a local variable # +account+ to the template for display. # # In another template for Advertiser#buy, we could have: # # <%= render :partial => "account", :locals => { :account => @buyer } %> # - # <% for ad in @advertisements %> + # <% @advertisements.each do |ad| %> # <%= render :partial => "ad", :locals => { :ad => ad } %> # <% end %> # - # This would first render "advertiser/_account.erb" with @buyer passed in as the local variable +account+, then - # render "advertiser/_ad.erb" and pass the local variable +ad+ to the template for display. + # This would first render "advertiser/_account.html.erb" with @buyer passed in as the local variable +account+, then + # render "advertiser/_ad.html.erb" and pass the local variable +ad+ to the template for display. + # + # == The :as and :object options + # + # By default <tt>ActionView::Partials::PartialRenderer</tt> has its object in a local variable with the same + # name as the template. So, given + # + # <%= render :partial => "contract" %> + # + # within contract we'll get <tt>@contract</tt> in the local variable +contract+, as if we had written + # + # <%= render :partial => "contract", :locals => { :contract => @contract } %> + # + # With the <tt>:as</tt> option we can specify a different name for said local variable. For example, if we + # wanted it to be +agreement+ instead of +contract+ we'd do: + # + # <%= render :partial => "contract", :as => 'agreement' %> + # + # The <tt>:object</tt> option can be used to directly specify which object is rendered into the partial; + # useful when the template's object is elsewhere, in a different ivar or in a local variable for instance. + # + # Revisiting a previous example we could have written this code: + # + # <%= render :partial => "account", :object => @buyer %> + # + # <% @advertisements.each do |ad| %> + # <%= render :partial => "ad", :object => ad %> + # <% end %> + # + # The <tt>:object</tt> and <tt>:as</tt> options can be used together. # # == Rendering a collection of partials # @@ -35,10 +64,22 @@ module ActionView # # <%= render :partial => "ad", :collection => @advertisements %> # - # This will render "advertiser/_ad.erb" and pass the local variable +ad+ to the template for display. An + # This will render "advertiser/_ad.html.erb" and pass the local variable +ad+ to the template for display. An # iteration counter will automatically be made available to the template with a name of the form # +partial_name_counter+. In the case of the example above, the template would be fed +ad_counter+. # + # The <tt>:as</tt> option may be used when rendering partials. + # + # You can specify a partial to be rendered between elements via the <tt>:spacer_template</tt> option. + # The following example will render <tt>advertiser/_ad_divider.html.erb</tt> between each ad partial: + # + # <%= render :partial => "ad", :collection => @advertisements, :spacer_template => "ad_divider" %> + # + # If the given <tt>:collection</tt> is nil or empty, <tt>render</tt> will return nil. This will allow you + # to specify a text which will displayed instead by using this form: + # + # <%= render(:partial => "ad", :collection => @advertisements) || "There's no ad to be displayed" %> + # # NOTE: Due to backwards compatibility concerns, the collection can't be one of hashes. Normally you'd also # just keep domain objects, like Active Records, in there. # @@ -48,7 +89,7 @@ module ActionView # # <%= render :partial => "advertisement/ad", :locals => { :ad => @advertisement } %> # - # This will render the partial "advertisement/_ad.erb" regardless of which controller this is being called from. + # This will render the partial "advertisement/_ad.html.erb" regardless of which controller this is being called from. # # == Rendering objects with the RecordIdentifier # @@ -56,7 +97,7 @@ module ActionView # you're following its conventions for RecordIdentifier#partial_path. Examples: # # # @account is an Account instance, so it uses the RecordIdentifier to replace - # # <%= render :partial => "accounts/account", :locals => { :account => @buyer } %> + # # <%= render :partial => "accounts/account", :locals => { :account => @account} %> # <%= render :partial => @account %> # # # @posts is an array of Post instances, so it uses the RecordIdentifier to replace @@ -174,165 +215,12 @@ module ActionView # <%- end -%> # <% end %> module Partials - extend ActiveSupport::Concern - - class PartialRenderer - PARTIAL_NAMES = Hash.new {|h,k| h[k] = {} } - - def initialize(view_context, options, block) - @view = view_context - @partial_names = PARTIAL_NAMES[@view.controller.class.name] - - setup(options, block) - end - - def setup(options, block) - partial = options[:partial] - - @options = options - @locals = options[:locals] || {} - @block = block - - if String === partial - @object = options[:object] - @path = partial - @collection = collection - else - @object = partial - - if @collection = collection - paths = @collection_paths = @collection.map { |o| partial_path(o) } - @path = paths.uniq.size == 1 ? paths.first : nil - else - @path = partial_path - end - end - end - - def render - identifier = ((@template = find_template) ? @template.identifier : @path) - - if @collection - ActiveSupport::Notifications.instrument("render_collection.action_view", - :identifier => identifier || "collection", :count => @collection.size) do - render_collection - end - else - content = ActiveSupport::Notifications.instrument("render_partial.action_view", - :identifier => identifier) do - render_partial - end - - if !@block && (layout = @options[:layout]) - content = @view._render_layout(find_template(layout), @locals){ content } - end - - content - end - end - - def render_collection - return nil if @collection.blank? - - if @options.key?(:spacer_template) - spacer = find_template(@options[:spacer_template]).render(@view, @locals) - end - - result = @template ? collection_with_template : collection_without_template - result.join(spacer).html_safe - end - - def collection_with_template(template = @template) - segments, locals, template = [], @locals, @template - - if @options[:as] - as = @options[:as] - counter = "#{as}_counter".to_sym - else - as = template.variable_name - counter = template.counter_name - end - - locals[counter] = -1 - - @collection.each do |object| - locals[counter] += 1 - locals[as] = object - segments << template.render(@view, locals) - end - - segments - end - - def collection_without_template(collection_paths = @collection_paths) - segments, locals = [], @locals - index, template = -1, nil - - if @options[:as] - as = @options[:as] - counter = "#{as}_counter" - end - - @collection.each_with_index do |object, i| - template = find_template(collection_paths[i]) - locals[as || template.variable_name] = object - locals[counter || template.counter_name] = (index += 1) - - segments << template.render(@view, locals) - end - - @template = template - segments - end - - def render_partial(object = @object) - locals, view, template = @locals, @view, @template - - object ||= locals[template.variable_name] - locals[@options[:as] || template.variable_name] = object - - template.render(view, locals) do |*name| - view._layout_for(*name, &@block) - end - end - - private - - def collection - if @object.respond_to?(:to_ary) - @object - elsif @options.key?(:collection) - @options[:collection] || [] - end - end - - def find_template(path=@path) - return path unless path.is_a?(String) - prefix = @view.controller_path unless path.include?(?/) - @view.find_template(path, prefix, true) - end - - def partial_path(object = @object) - @partial_names[object.class.name] ||= begin - object = object.to_model if object.respond_to?(:to_model) - - object.class.model_name.partial_path.dup.tap do |partial| - path = @view.controller_path - partial.insert(0, "#{File.dirname(path)}/") if partial.include?(?/) && path.include?(?/) - end - end - end - end - def _render_partial(options, &block) #:nodoc: - if defined?(@renderer) - @renderer.setup(options, block) - else - @renderer = PartialRenderer.new(self, options, block) - end - - @renderer.render + _partial_renderer.setup(options, block).render end + def _partial_renderer #:nodoc: + @_partial_renderer ||= PartialRenderer.new(self) + end end end diff --git a/actionpack/lib/action_view/paths.rb b/actionpack/lib/action_view/path_set.rb index 9857d688d2..e3de3e1eac 100644 --- a/actionpack/lib/action_view/paths.rb +++ b/actionpack/lib/action_view/path_set.rb @@ -10,16 +10,16 @@ module ActionView #:nodoc: METHOD end - def find(path, prefix = nil, partial = false, details = {}, key = nil) - template = find_all(path, prefix, partial, details, key).first - raise MissingTemplate.new(self, "#{prefix}/#{path}", details, partial) unless template - template + def find(*args) + find_all(*args).first || raise(MissingTemplate.new(self, *args)) end - def find_all(*args) - each do |resolver| - templates = resolver.find_all(*args) - return templates unless templates.empty? + def find_all(path, prefixes = [], *args) + prefixes.each do |prefix| + each do |resolver| + templates = resolver.find_all(path, prefix, *args) + return templates unless templates.empty? + end end [] end diff --git a/actionpack/lib/action_view/railtie.rb b/actionpack/lib/action_view/railtie.rb index 33dfcbb803..501ec07b09 100644 --- a/actionpack/lib/action_view/railtie.rb +++ b/actionpack/lib/action_view/railtie.rb @@ -8,10 +8,10 @@ module ActionView config.action_view.stylesheet_expansions = {} config.action_view.javascript_expansions = { :defaults => ['prototype', 'effects', 'dragdrop', 'controls', 'rails'] } - initializer "action_view.cache_asset_timestamps" do |app| + initializer "action_view.cache_asset_ids" do |app| unless app.config.cache_classes ActiveSupport.on_load(:action_view) do - ActionView::Helpers::AssetTagHelper.cache_asset_timestamps = false + ActionView::Helpers::AssetTagHelper::AssetPaths.cache_asset_ids = false end end end @@ -35,5 +35,13 @@ module ActionView end end end + + initializer "action_view.caching" do |app| + ActiveSupport.on_load(:action_view) do + if app.config.action_view.cache_template_loading.nil? + ActionView::Resolver.caching = app.config.cache_classes + end + end + end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_view/render/layouts.rb b/actionpack/lib/action_view/render/layouts.rb deleted file mode 100644 index a474783a20..0000000000 --- a/actionpack/lib/action_view/render/layouts.rb +++ /dev/null @@ -1,79 +0,0 @@ -module ActionView - # = Action View Layouts - module Layouts - # Returns the contents that are yielded to a layout, given a name or a block. - # - # You can think of a layout as a method that is called with a block. If the user calls - # <tt>yield :some_name</tt>, the block, by default, returns <tt>content_for(:some_name)</tt>. - # If the user calls simply +yield+, the default block returns <tt>content_for(:layout)</tt>. - # - # The user can override this default by passing a block to the layout: - # - # # The template - # <%= render :layout => "my_layout" do %> - # Content - # <% end %> - # - # # The layout - # <html> - # <%= yield %> - # </html> - # - # In this case, instead of the default block, which would return <tt>content_for(:layout)</tt>, - # this method returns the block that was passed in to <tt>render :layout</tt>, and the response - # would be - # - # <html> - # Content - # </html> - # - # Finally, the block can take block arguments, which can be passed in by +yield+: - # - # # The template - # <%= render :layout => "my_layout" do |customer| %> - # Hello <%= customer.name %> - # <% end %> - # - # # The layout - # <html> - # <%= yield Struct.new(:name).new("David") %> - # </html> - # - # In this case, the layout would receive the block passed into <tt>render :layout</tt>, - # and the struct specified would be passed into the block as an argument. The result - # would be - # - # <html> - # Hello David - # </html> - # - def _layout_for(name = nil, &block) #:nodoc: - if !block || name - @_content_for[name || :layout].html_safe - else - capture(&block) - end - end - - # This is the method which actually finds the layout using details in the lookup - # context object. If no layout is found, it checks if at least a layout with - # the given name exists across all details before raising the error. - def find_layout(layout) - begin - with_layout_format do - layout =~ /^\// ? - with_fallbacks { find_template(layout) } : find_template(layout) - end - rescue ActionView::MissingTemplate => e - update_details(:formats => nil) do - raise unless template_exists?(layout) - end - end - 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) } - end - end -end diff --git a/actionpack/lib/action_view/render/rendering.rb b/actionpack/lib/action_view/render/rendering.rb deleted file mode 100644 index 5320245173..0000000000 --- a/actionpack/lib/action_view/render/rendering.rb +++ /dev/null @@ -1,67 +0,0 @@ -require 'active_support/core_ext/object/try' - -module ActionView - # = Action View Rendering - module Rendering - # Returns the result of a render that's dictated by the options hash. The primary options are: - # - # * <tt>:partial</tt> - See ActionView::Partials. - # * <tt>:update</tt> - Calls update_page with the block given. - # * <tt>:file</tt> - Renders an explicit template file (this used to be the old default), add :locals to pass in those. - # * <tt>:inline</tt> - Renders an inline template similar to how it's done in the controller. - # * <tt>:text</tt> - Renders the text passed in out. - # - # If no options hash is passed or :update specified, the default is to render a partial and use the second parameter - # as the locals hash. - def render(options = {}, locals = {}, &block) - case options - when Hash - if block_given? - _render_partial(options.merge(:partial => options[:layout]), &block) - elsif options.key?(:partial) - _render_partial(options) - else - template = _determine_template(options) - lookup_context.freeze_formats(template.formats, true) - _render_template(template, options[:layout], options) - end - when :update - update_page(&block) - else - _render_partial(:partial => options, :locals => locals) - end - end - - # Determine the template to be rendered using the given options. - def _determine_template(options) #:nodoc: - if options.key?(:inline) - handler = Template.handler_class_for_extension(options[:type] || "erb") - Template.new(options[:inline], "inline template", handler, {}) - elsif options.key?(:text) - Template::Text.new(options[:text], formats.try(:first)) - elsif options.key?(:file) - with_fallbacks { find_template(options[:file], options[:prefix]) } - elsif options.key?(:template) - options[:template].respond_to?(:render) ? - options[:template] : find_template(options[:template], options[:prefix]) - end - end - - # Renders the given template. An string representing the layout can be - # supplied as well. - def _render_template(template, layout = nil, options = {}) #:nodoc: - locals = options[:locals] || {} - layout = find_layout(layout) if layout - - ActiveSupport::Notifications.instrument("render_template.action_view", - :identifier => template.identifier, :layout => layout.try(:virtual_path)) do - - content = template.render(self, locals) { |*name| _layout_for(*name) } - @_content_for[:layout] = content if layout - - content = _render_layout(layout, locals) if layout - content - end - end - end -end diff --git a/actionpack/lib/action_view/renderer/abstract_renderer.rb b/actionpack/lib/action_view/renderer/abstract_renderer.rb new file mode 100644 index 0000000000..4a52b3172e --- /dev/null +++ b/actionpack/lib/action_view/renderer/abstract_renderer.rb @@ -0,0 +1,37 @@ +module ActionView + class AbstractRenderer #:nodoc: + delegate :find_template, :template_exists?, :with_fallbacks, :update_details, + :with_layout_format, :formats, :freeze_formats, :to => :@lookup_context + + def initialize(view) + @view = view + @lookup_context = view.lookup_context + end + + def render + raise NotImplementedError + end + + # Checks if the given path contains a format and if so, change + # the lookup context to take this new format into account. + def wrap_formats(value) + return yield unless value.is_a?(String) + + if value.sub!(formats_regexp, "") + update_details(:formats => [$1.to_sym]){ yield } + else + yield + end + end + + def formats_regexp + @@formats_regexp ||= /\.(#{Mime::SET.symbols.join('|')})$/ + end + + protected + + def instrument(name, options={}) + ActiveSupport::Notifications.instrument("render_#{name}.action_view", options){ yield } + end + end +end diff --git a/actionpack/lib/action_view/renderer/partial_renderer.rb b/actionpack/lib/action_view/renderer/partial_renderer.rb new file mode 100644 index 0000000000..94c0a8a8fb --- /dev/null +++ b/actionpack/lib/action_view/renderer/partial_renderer.rb @@ -0,0 +1,167 @@ +require 'action_view/renderer/abstract_renderer' + +module ActionView + class PartialRenderer < AbstractRenderer #:nodoc: + PARTIAL_NAMES = Hash.new {|h,k| h[k] = {} } + + def initialize(view) + super + @partial_names = PARTIAL_NAMES[@view.controller.class.name] + end + + def setup(options, block) + partial = options[:partial] + + @options = options + @locals = options[:locals] || {} + @block = block + + if String === partial + @object = options[:object] + @path = partial + @collection = collection + else + @object = partial + + if @collection = collection_from_object || collection + paths = @collection_data = @collection.map { |o| partial_path(o) } + @path = paths.uniq.size == 1 ? paths.first : nil + else + @path = partial_path + end + end + + if @path + @variable, @variable_counter = retrieve_variable(@path) + else + paths.map! { |path| retrieve_variable(path).unshift(path) } + end + + self + end + + def render + wrap_formats(@path) do + identifier = ((@template = find_partial) ? @template.identifier : @path) + + if @collection + instrument(:collection, :identifier => identifier || "collection", :count => @collection.size) do + render_collection + end + else + instrument(:partial, :identifier => identifier) do + render_partial + end + end + end + end + + def render_collection + return nil if @collection.blank? + + if @options.key?(:spacer_template) + spacer = find_template(@options[:spacer_template]).render(@view, @locals) + end + + result = @template ? collection_with_template : collection_without_template + result.join(spacer).html_safe + end + + def render_partial + locals, view, block = @locals, @view, @block + object, as = @object, @variable + + if !block && (layout = @options[:layout]) + layout = find_template(layout) + end + + object ||= locals[as] + locals[as] = object + + content = @template.render(view, locals) do |*name| + view._layout_for(*name, &block) + end + + content = layout.render(view, locals){ content } if layout + content + end + + private + + def collection + if @options.key?(:collection) + collection = @options[:collection] + collection.respond_to?(:to_ary) ? collection.to_ary : [] + end + end + + def collection_from_object + if @object.respond_to?(:to_ary) + @object.to_ary + end + end + + def find_partial + if path = @path + locals = @locals.keys + locals << @variable + locals << @variable_counter if @collection + find_template(path, locals) + end + end + + def find_template(path=@path, locals=@locals.keys) + prefixes = path.include?(?/) ? [] : @view.controller_prefixes + @lookup_context.find_template(path, prefixes, true, locals) + end + + def collection_with_template + segments, locals, template = [], @locals, @template + as, counter = @variable, @variable_counter + + locals[counter] = -1 + + @collection.each do |object| + locals[counter] += 1 + locals[as] = object + segments << template.render(@view, locals) + end + + segments + end + + def collection_without_template + segments, locals, collection_data = [], @locals, @collection_data + index, template, cache = -1, nil, {} + keys = @locals.keys + + @collection.each_with_index do |object, i| + path, *data = collection_data[i] + template = (cache[path] ||= find_template(path, keys + data)) + locals[data[0]] = object + locals[data[1]] = (index += 1) + segments << template.render(@view, locals) + end + + @template = template + segments + end + + def partial_path(object = @object) + @partial_names[object.class.name] ||= begin + object = object.to_model if object.respond_to?(:to_model) + + object.class.model_name.partial_path.dup.tap do |partial| + path = @view.controller_prefixes.first + partial.insert(0, "#{File.dirname(path)}/") if partial.include?(?/) && path.include?(?/) + end + end + end + + def retrieve_variable(path) + variable = @options[:as].try(:to_sym) || path[%r'_?(\w+)(\.\w+)*$', 1].to_sym + variable_counter = :"#{variable}_counter" if @collection + [variable, variable_counter] + end + end +end diff --git a/actionpack/lib/action_view/renderer/template_renderer.rb b/actionpack/lib/action_view/renderer/template_renderer.rb new file mode 100644 index 0000000000..9ae1636131 --- /dev/null +++ b/actionpack/lib/action_view/renderer/template_renderer.rb @@ -0,0 +1,98 @@ +require 'set' +require 'active_support/core_ext/object/try' +require 'active_support/core_ext/array/wrap' +require 'action_view/renderer/abstract_renderer' + +module ActionView + class TemplateRenderer < AbstractRenderer #:nodoc: + attr_reader :rendered + + def initialize(view) + super + @rendered = Set.new + end + + def render(options) + wrap_formats(options[:template] || options[:file]) do + template = determine_template(options) + render_template(template, options[:layout], options[:locals]) + end + end + + def render_once(options) + paths, locals = options[:once], options[:locals] || {} + layout, keys = options[:layout], locals.keys + prefixes = options.fetch(:prefixes, @view.controller_prefixes) + + raise "render :once expects a String or an Array to be given" unless paths + + render_with_layout(layout, locals) do + contents = [] + Array.wrap(paths).each do |path| + template = find_template(path, prefixes, false, keys) + contents << render_template(template, nil, locals) if @rendered.add?(template) + end + contents.join("\n") + end + end + + # Determine the template to be rendered using the given options. + def determine_template(options) #:nodoc: + keys = options[:locals].try(:keys) || [] + + if options.key?(:text) + Template::Text.new(options[:text], formats.try(:first)) + elsif options.key?(:file) + with_fallbacks { find_template(options[:file], options[:prefixes], false, keys) } + elsif options.key?(:inline) + handler = Template.handler_for_extension(options[:type] || "erb") + Template.new(options[:inline], "inline template", handler, :locals => keys) + elsif options.key?(:template) + options[:template].respond_to?(:render) ? + options[:template] : find_template(options[:template], options[:prefixes], false, keys) + end + end + + # Renders the given template. An string representing the layout can be + # supplied as well. + def render_template(template, layout_name = nil, locals = {}) #:nodoc: + freeze_formats(template.formats, true) + view, locals = @view, locals || {} + + render_with_layout(layout_name, locals) do |layout| + instrument(:template, :identifier => template.identifier, :layout => layout.try(:virtual_path)) do + template.render(view, locals) { |*name| view._layout_for(*name) } + end + end + end + + def render_with_layout(path, locals) #:nodoc: + layout = path && find_layout(path, locals.keys) + content = yield(layout) + + if layout + view = @view + view.store_content_for(:layout, content) + layout.render(view, locals){ |*name| view._layout_for(*name) } + else + content + end + end + + # This is the method which actually finds the layout using details in the lookup + # context object. If no layout is found, it checks if at least a layout with + # the given name exists across all details before raising the error. + def find_layout(layout, keys) + begin + with_layout_format do + layout =~ /^\// ? + with_fallbacks { find_template(layout, nil, false, keys) } : find_template(layout, nil, false, keys) + end + rescue ActionView::MissingTemplate => e + update_details(:formats => nil) do + raise unless template_exists?(layout) + end + end + end + end +end diff --git a/actionpack/lib/action_view/rendering.rb b/actionpack/lib/action_view/rendering.rb new file mode 100644 index 0000000000..baa5d2c3fd --- /dev/null +++ b/actionpack/lib/action_view/rendering.rb @@ -0,0 +1,106 @@ +require 'active_support/core_ext/object/try' + +module ActionView + # = Action View Rendering + module Rendering + # Returns the result of a render that's dictated by the options hash. The primary options are: + # + # * <tt>:partial</tt> - See ActionView::Partials. + # * <tt>:update</tt> - Calls update_page with the block given. + # * <tt>:file</tt> - Renders an explicit template file (this used to be the old default), add :locals to pass in those. + # * <tt>:inline</tt> - Renders an inline template similar to how it's done in the controller. + # * <tt>:text</tt> - Renders the text passed in out. + # * <tt>:once</tt> - Accepts a string or an array of strings and Rails will ensure they each of them are rendered just once. + # + # If no options hash is passed or :update specified, the default is to render a partial and use the second parameter + # as the locals hash. + def render(options = {}, locals = {}, &block) + case options + when Hash + if block_given? + _render_partial(options.merge(:partial => options[:layout]), &block) + elsif options.key?(:partial) + _render_partial(options) + elsif options.key?(:once) + _render_once(options) + else + _render_template(options) + end + when :update + update_page(&block) + else + _render_partial(:partial => options, :locals => locals) + end + end + + # Returns the contents that are yielded to a layout, given a name or a block. + # + # You can think of a layout as a method that is called with a block. If the user calls + # <tt>yield :some_name</tt>, the block, by default, returns <tt>content_for(:some_name)</tt>. + # If the user calls simply +yield+, the default block returns <tt>content_for(:layout)</tt>. + # + # The user can override this default by passing a block to the layout: + # + # # The template + # <%= render :layout => "my_layout" do %> + # Content + # <% end %> + # + # # The layout + # <html> + # <%= yield %> + # </html> + # + # In this case, instead of the default block, which would return <tt>content_for(:layout)</tt>, + # this method returns the block that was passed in to <tt>render :layout</tt>, and the response + # would be + # + # <html> + # Content + # </html> + # + # Finally, the block can take block arguments, which can be passed in by +yield+: + # + # # The template + # <%= render :layout => "my_layout" do |customer| %> + # Hello <%= customer.name %> + # <% end %> + # + # # The layout + # <html> + # <%= yield Struct.new(:name).new("David") %> + # </html> + # + # In this case, the layout would receive the block passed into <tt>render :layout</tt>, + # and the struct specified would be passed into the block as an argument. The result + # would be + # + # <html> + # Hello David + # </html> + # + def _layout_for(*args, &block) + name = args.first + + if name.is_a?(Symbol) + @_content_for[name].html_safe + elsif block + capture(*args, &block) + else + @_content_for[:layout].html_safe + end + end + + def _render_once(options) #:nodoc: + _template_renderer.render_once(options) + end + + def _render_template(options) #:nodoc: + _template_renderer.render(options) + end + + def _template_renderer #:nodoc: + @_template_renderer ||= TemplateRenderer.new(self) + end + end +end diff --git a/actionpack/lib/action_view/template.rb b/actionpack/lib/action_view/template.rb index 40ff1f2182..96d506fac5 100644 --- a/actionpack/lib/action_view/template.rb +++ b/actionpack/lib/action_view/template.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/object/try' require 'active_support/core_ext/kernel/singleton_class' module ActionView @@ -97,9 +98,12 @@ module ActionView extend Template::Handlers - attr_reader :source, :identifier, :handler, :virtual_path, :formats, - :original_encoding + attr_accessor :locals, :formats, :virtual_path + attr_reader :source, :identifier, :handler, :original_encoding, :updated_at + + # This finalizer is needed (and exactly with a proc inside another proc) + # otherwise templates leak in development. Finalizer = proc do |method_name, mod| proc do mod.module_eval do @@ -109,50 +113,80 @@ module ActionView end def initialize(source, identifier, handler, details) - @source = source - @identifier = identifier - @handler = handler - @original_encoding = nil - - @virtual_path = details[:virtual_path] - @method_names = {} + format = details[:format] || (handler.default_format if handler.respond_to?(:default_format)) - format = details[:format] || :html - @formats = Array.wrap(format).map(&:to_sym) + @source = source + @identifier = identifier + @handler = handler + @compiled = false + @original_encoding = nil + @locals = details[:locals] || [] + @virtual_path = details[:virtual_path] + @updated_at = details[:updated_at] || Time.now + @formats = Array.wrap(format).map { |f| f.is_a?(Mime::Type) ? f.ref : f } end + # Render a template. If the template was not compiled yet, it is done + # exactly before rendering. + # + # This method is instrumented as "!render_template.action_view". 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. def render(view, 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. + old_template, view._template = view._template, self ActiveSupport::Notifications.instrument("!render_template.action_view", :virtual_path => @virtual_path) do - if view.is_a?(ActionView::CompiledTemplates) - mod = ActionView::CompiledTemplates - else - mod = view.singleton_class - end - - method_name = compile(locals, view, mod) + compile!(view) view.send(method_name, locals, &block) end rescue Exception => e - if e.is_a?(Template::Error) - e.sub_template_of(self) - raise e - else - raise Template::Error.new(self, view.respond_to?(:assigns) ? view.assigns : {}, e) - end + handle_render_error(view, e) + ensure + view._template = old_template end def mime_type @mime_type ||= Mime::Type.lookup_by_extension(@formats.first.to_s) if @formats.first end - def variable_name - @variable_name ||= @virtual_path[%r'_?(\w+)(\.\w+)*$', 1].to_sym + # Receives a view object and return a template similar to self by using @virtual_path. + # + # This method is useful if you have a template object but it does not contain its source + # anymore since it was already compiled. In such cases, all you need to do is to call + # refresh passing in the view object. + # + # Notice this method raises an error if the template to be refreshed does not have a + # virtual path set (true just for inline templates). + def refresh(view) + raise "A template needs to have a virtual path in order to be refreshed" unless @virtual_path + lookup = view.lookup_context + pieces = @virtual_path.split("/") + name = pieces.pop + partial = !!name.sub!(/^_/, "") + lookup.disable_cache do + lookup.find_template(name, [ pieces.join('/') ], partial, @locals) + end + end + + # Expires this template by setting his updated_at date to Jan 1st, 1970. + def expire! + @updated_at = Time.utc(1970) + end + + # Receives a view context and renders a template exactly like self by using + # the @virtual_path. It raises an error if no @virtual_path was given. + def rerender(view) + raise "A template needs to have a virtual path in order to be rerendered" unless @virtual_path + name = @virtual_path.dup + if name.sub!(/(^|\/)_([^\/]*)$/, '\1\2') + view.render :partial => name + else + view.render :template => @virtual_path + end end - def counter_name - @counter_name ||= "#{variable_name}_counter".to_sym + # Used to store template data by template handlers. + def data + @data ||= {} end def inspect @@ -164,7 +198,27 @@ module ActionView end end - private + protected + + # Compile a template. This method ensures a template is compiled + # just once and removes the source after it is compiled. + def compile!(view) #:nodoc: + return if @compiled + + if view.is_a?(ActionView::CompiledTemplates) + mod = ActionView::CompiledTemplates + else + mod = view.singleton_class + end + + compile(view, mod) + + # Just discard the source if we have a virtual path. This + # means we can get the template back. + @source = nil if @virtual_path + @compiled = true + end + # Among other things, this method is responsible for properly setting # the encoding of the source. Until this point, we assume that the # source is BINARY data. If no additional information is supplied, @@ -185,11 +239,8 @@ module ActionView # encode the source into Encoding.default_internal. In general, # this means that templates will be UTF-8 inside of Rails, # regardless of the original source encoding. - def compile(locals, view, mod) - method_name = build_method_name(locals) - return method_name if view.respond_to?(method_name) - - locals_code = locals.keys.map! { |key| "#{key} = local_assigns[:#{key}];" }.join + def compile(view, mod) #:nodoc: + method_name = self.method_name if source.encoding_aware? # Look for # encoding: *. If we find one, we'll encode the @@ -223,15 +274,16 @@ module ActionView end end - code = @handler.call(self) + arity = @handler.respond_to?(:arity) ? @handler.arity : @handler.method(:call).arity + code = arity.abs == 1 ? @handler.call(self) : @handler.call(self, view) # Make sure that the resulting String to be evalled is in the # encoding of the code source = <<-end_src def #{method_name}(local_assigns) - _old_virtual_path, @_virtual_path = @_virtual_path, #{@virtual_path.inspect};_old_output_buffer = @output_buffer;#{locals_code};#{code} + _old_output_buffer = @output_buffer;#{locals_code};#{code} ensure - @_virtual_path, @output_buffer = _old_virtual_path, _old_output_buffer + @output_buffer = _old_output_buffer end end_src @@ -254,8 +306,6 @@ module ActionView begin mod.module_eval(source, identifier, 0) ObjectSpace.define_finalizer(self, Finalizer[method_name, mod]) - - method_name rescue Exception => e # errors from template code if logger = (view && view.logger) logger.debug "ERROR: compiling #{method_name} RAISED #{e}" @@ -267,12 +317,27 @@ module ActionView end end - def build_method_name(locals) - @method_names[locals.keys.hash] ||= "_#{identifier_method_name}__#{@identifier.hash}_#{__id__}_#{locals.keys.hash}".gsub('-', "_") + def handle_render_error(view, e) #:nodoc: + if e.is_a?(Template::Error) + e.sub_template_of(self) + raise e + else + assigns = view.respond_to?(:assigns) ? view.assigns : {} + template = @virtual_path ? refresh(view) : self + raise Template::Error.new(template, assigns, e) + end + end + + def locals_code #:nodoc: + @locals.map { |key| "#{key} = local_assigns[:#{key}];" }.join + end + + def method_name #:nodoc: + @method_name ||= "_#{identifier_method_name}__#{@identifier.hash}_#{__id__}".gsub('-', "_") end - def identifier_method_name - @identifier_method_name ||= inspect.gsub(/[^a-z_]/, '_') + def identifier_method_name #:nodoc: + inspect.gsub(/[^a-z_]/, '_') end end end diff --git a/actionpack/lib/action_view/template/error.rb b/actionpack/lib/action_view/template/error.rb index b1839b65e5..e246646963 100644 --- a/actionpack/lib/action_view/template/error.rb +++ b/actionpack/lib/action_view/template/error.rb @@ -27,7 +27,7 @@ module ActionView class MissingTemplate < ActionViewError #:nodoc: attr_reader :path - def initialize(paths, path, details, partial) + def initialize(paths, path, prefixes, partial, details, *) @path = path display_paths = paths.compact.map{ |p| p.to_s.inspect }.join(", ") template_type = if partial @@ -38,20 +38,27 @@ module ActionView 'template' end - super("Missing #{template_type} #{path} with #{details.inspect} in view paths #{display_paths}") + searched_paths = prefixes.map { |prefix| [prefix, path].join("/") } + + out = "Missing #{template_type} #{searched_paths.join(", ")} with #{details.inspect}. Searched in:\n" + out += paths.compact.map { |p| " * #{p.to_s.inspect}\n" }.join + super out 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. + # The Template::Error exception is raised when the compilation or rendering of the template + # fails. This exception then gathers a bunch of intimate details and uses it to report a + # precise exception message. class Error < ActionViewError #:nodoc: SOURCE_CODE_RADIUS = 3 attr_reader :original_exception, :backtrace def initialize(template, assigns, original_exception) + super(original_exception.message) @template, @assigns, @original_exception = template, assigns.dup, original_exception + @sub_templates = nil @backtrace = original_exception.backtrace end @@ -59,10 +66,6 @@ module ActionView @template.identifier end - def message - ActiveSupport::Deprecation.silence { original_exception.message } - end - def sub_template_message if @sub_templates "Trace of template inclusion: " + diff --git a/actionpack/lib/action_view/template/handler.rb b/actionpack/lib/action_view/template/handler.rb index 8ecc911519..636f3ebbad 100644 --- a/actionpack/lib/action_view/template/handler.rb +++ b/actionpack/lib/action_view/template/handler.rb @@ -1,4 +1,4 @@ -require "action_dispatch/http/mime_type" +require 'action_dispatch/http/mime_type' require 'active_support/core_ext/class/attribute' # Legacy TemplateHandler stub @@ -7,6 +7,8 @@ module ActionView module Handlers #:nodoc: module Compilable def self.included(base) + ActiveSupport::Deprecation.warn "Including Compilable in your template handler is deprecated. " << + "Since Rails 3, all the API your template handler needs to implement is to respond to #call." base.extend(ClassMethods) end @@ -26,6 +28,12 @@ module ActionView class_attribute :default_format self.default_format = Mime::HTML + def self.inherited(base) + ActiveSupport::Deprecation.warn "Inheriting from ActionView::Template::Handler is deprecated. " << + "Since Rails 3, all the API your template handler needs to implement is to respond to #call." + super + end + def self.call(template) raise "Need to implement #{self.class.name}#call(template)" end @@ -35,7 +43,7 @@ module ActionView end end end - + TemplateHandlers = Template::Handlers TemplateHandler = Template::Handler end diff --git a/actionpack/lib/action_view/template/handlers.rb b/actionpack/lib/action_view/template/handlers.rb index 84d6474dd1..4438199497 100644 --- a/actionpack/lib/action_view/template/handlers.rb +++ b/actionpack/lib/action_view/template/handlers.rb @@ -7,18 +7,14 @@ module ActionView #:nodoc: autoload :Builder, 'action_view/template/handlers/builder' def self.extended(base) - base.register_default_template_handler :erb, ERB - base.register_template_handler :rjs, RJS - base.register_template_handler :builder, Builder - - # TODO: Depreciate old template extensions - base.register_template_handler :rhtml, ERB - base.register_template_handler :rxml, Builder + base.register_default_template_handler :erb, ERB.new + base.register_template_handler :rjs, RJS.new + base.register_template_handler :builder, Builder.new end @@template_handlers = {} @@default_template_handlers = nil - + def self.extensions @@template_extensions ||= @@template_handlers.keys end @@ -48,7 +44,13 @@ module ActionView #:nodoc: end def handler_class_for_extension(extension) - (extension && registered_template_handler(extension.to_sym)) || @@default_template_handlers + ActiveSupport::Deprecation.warn "handler_class_for_extension is deprecated. " << + "Please use handler_for_extension instead", caller + handler_for_extension(extension) + end + + def handler_for_extension(extension) + registered_template_handler(extension) || @@default_template_handlers end end end diff --git a/actionpack/lib/action_view/template/handlers/builder.rb b/actionpack/lib/action_view/template/handlers/builder.rb index a93cfca8aa..2c52cfd90e 100644 --- a/actionpack/lib/action_view/template/handlers/builder.rb +++ b/actionpack/lib/action_view/template/handlers/builder.rb @@ -1,11 +1,11 @@ module ActionView module Template::Handlers - class Builder < Template::Handler - include Compilable - + class Builder + # Default format used by Builder. + class_attribute :default_format self.default_format = Mime::XML - def compile(template) + def call(template) require 'builder' "xml = ::Builder::XmlMarkup.new(:indent => 2);" + "self.output_buffer = xml.target!;" + diff --git a/actionpack/lib/action_view/template/handlers/erb.rb b/actionpack/lib/action_view/template/handlers/erb.rb index ce609e01af..a36837afc8 100644 --- a/actionpack/lib/action_view/template/handlers/erb.rb +++ b/actionpack/lib/action_view/template/handlers/erb.rb @@ -1,6 +1,7 @@ require 'active_support/core_ext/class/attribute_accessors' require 'active_support/core_ext/string/output_safety' -require "action_view/template" +require 'action_view/template' +require 'action_view/template/handler' require 'erubis' module ActionView @@ -14,13 +15,7 @@ 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 + alias :safe_append= :safe_concat end class Template @@ -45,45 +40,44 @@ module ActionView end end - def add_stmt(src, code) + def add_expr_escaped(src, code) if code =~ BLOCK_EXPR - src << '@output_buffer.append_if_string= ' << code + src << "@output_buffer.safe_append= " << code else - super + src << "@output_buffer.safe_concat((" << code << ").to_s);" end end - def add_expr_escaped(src, code) - src << '@output_buffer.append= ' << escaped_expr(code) << ';' - end - def add_postamble(src) src << '@output_buffer.to_s' end end - class ERB < Handler - include Compilable - - ## - # :singleton-method: + class ERB # Specify trim mode for the ERB compiler. Defaults to '-'. # See ERb documentation for suitable values. - cattr_accessor :erb_trim_mode + class_attribute :erb_trim_mode self.erb_trim_mode = '-' + # Default format used by ERB. + class_attribute :default_format self.default_format = Mime::HTML - cattr_accessor :erb_implementation + # Default implementation used. + class_attribute :erb_implementation self.erb_implementation = Erubis ENCODING_TAG = Regexp.new("\\A(<%#{ENCODING_FLAG}-?%>)[ \\t]*") - def self.handles_encoding? + def self.call(template) + new.call(template) + end + + def handles_encoding? true end - def compile(template) + def call(template) if template.source.encoding_aware? # First, convert to BINARY, so in case the encoding is # wrong, we can still find an encoding tag @@ -109,6 +103,7 @@ module ActionView end private + def valid_encoding(string, encoding) # If a magic encoding comment was found, tag the # String with this encoding. This is for a case diff --git a/actionpack/lib/action_view/template/handlers/rjs.rb b/actionpack/lib/action_view/template/handlers/rjs.rb index 128be5077c..9d71059134 100644 --- a/actionpack/lib/action_view/template/handlers/rjs.rb +++ b/actionpack/lib/action_view/template/handlers/rjs.rb @@ -1,17 +1,13 @@ module ActionView module Template::Handlers - class RJS < Template::Handler - include Compilable - + class RJS + # Default format used by RJS. + class_attribute :default_format self.default_format = Mime::JS - def compile(template) + def call(template) "update_page do |page|;#{template.source}\nend" end - - def default_format - Mime::JS - end end end end diff --git a/actionpack/lib/action_view/template/resolver.rb b/actionpack/lib/action_view/template/resolver.rb index c9e20ca14e..6c1063592f 100644 --- a/actionpack/lib/action_view/template/resolver.rb +++ b/actionpack/lib/action_view/template/resolver.rb @@ -5,9 +5,35 @@ 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 + + class << self + alias :caching? :caching + end + def initialize - @cached = Hash.new { |h1,k1| h1[k1] = - Hash.new { |h2,k2| h2[k2] = Hash.new { |h3, k3| h3[k3] = {} } } } + @cached = Hash.new { |h1,k1| h1[k1] = Hash.new { |h2,k2| + h2[k2] = Hash.new { |h3,k3| h3[k3] = Hash.new { |h4,k4| h4[k4] = {} } } } } end def clear_cache @@ -15,68 +41,131 @@ module ActionView end # Normalizes the arguments and passes it on to find_template. - def find_all(name, prefix=nil, partial=false, details={}, key=nil) - cached(key, prefix, name, partial) do + def find_all(name, prefix=nil, partial=false, details={}, key=nil, locals=[]) + cached(key, [name, prefix, partial], details, locals) do find_templates(name, prefix, partial, details) end end private - def caching? - @caching ||= !defined?(Rails.application) || Rails.application.config.cache_classes - end + delegate :caching?, :to => "self.class" # This is what child classes implement. No defaults are needed # 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 - def cached(key, prefix, name, partial) - return yield unless key && caching? - @cached[key][prefix][name][partial] ||= yield + # Helpers that builds a path. Useful for building virtual paths. + def build_path(name, prefix, partial) + Path.new(name, prefix, partial) + end + + # Handles templates caching. If a key is given and caching is on + # always check the cache before hitting the resolver. Otherwise, + # it always hits the resolver but check if the resolver is fresher + # before returning it. + def cached(key, path_info, details, locals) #:nodoc: + name, prefix, partial = path_info + locals = sort_locals(locals) + + if key && caching? + @cached[key][name][prefix][partial][locals] ||= decorate(yield, path_info, details, locals) + else + fresh = decorate(yield, path_info, details, locals) + return fresh unless key + + scope = @cached[key][name][prefix][partial] + cache = scope[locals] + mtime = cache && cache.map(&:updated_at).max + + if !mtime || fresh.empty? || fresh.any? { |t| t.updated_at > mtime } + scope[locals] = fresh + else + cache + end + end + end + + # Ensures all the resolver information is set in the template. + def decorate(templates, path_info, details, locals) #:nodoc: + cached = nil + templates.each do |t| + t.locals = locals + t.formats = details[:formats] || [:html] if t.formats.empty? + t.virtual_path ||= (cached ||= build_path(*path_info)) + end + end + + if :symbol.respond_to?("<=>") + def sort_locals(locals) #:nodoc: + locals.sort.freeze + end + else + def sort_locals(locals) #:nodoc: + locals = locals.map{ |l| l.to_s } + locals.sort! + locals.freeze + end end end class PathResolver < Resolver - EXTENSION_ORDER = [:locale, :formats, :handlers] + EXTENSIONS = [:locale, :formats, :handlers] + DEFAULT_PATTERN = ":prefix/:action{.:locale,}{.:formats,}{.:handlers,}" - def to_s - @path.to_s + def initialize(pattern=nil) + @pattern = pattern || DEFAULT_PATTERN + super() end - alias :to_path :to_s - private + private def find_templates(name, prefix, partial, details) - path = build_path(name, prefix, partial, details) - query(path, EXTENSION_ORDER.map { |ext| details[ext] }, details[:formats]) - end - - def build_path(name, prefix, partial, details) - path = "" - path << "#{prefix}/" unless prefix.empty? - path << (partial ? "_#{name}" : name) - path + path = build_path(name, prefix, partial) + 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 = build_query(path, exts) + templates = [] + sanitizer = Hash.new { |h,k| h[k] = Dir["#{File.dirname(k)}/*"] } - exts.each do |ext| - query << '{' << ext.map {|e| e && ".#{e}" }.join(',') << ',}' - end + Dir[query].each do |p| + next if File.directory?(p) || !sanitizer[p].include?(p) - Dir[query].reject { |p| File.directory?(p) }.map do |p| handler, format = extract_handler_and_format(p, formats) - contents = File.open(p, "rb") {|io| io.read } - Template.new(contents, File.expand_path(p), handler, - :virtual_path => path, :format => format) + templates << Template.new(contents, File.expand_path(p), handler, + :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 end # Extract handler and formats from path. If a format cannot be a found neither @@ -85,26 +174,76 @@ module ActionView 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 - format ||= handler.default_format if handler.respond_to?(:default_format) - format ||= default_formats - + handler = Template.handler_for_extension(pieces.pop) + format = pieces.last && Mime[pieces.last] [handler, format] end end + # 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 + def to_s + @path.to_s + end + alias :to_path :to_s + def eql?(resolver) self.class.equal?(resolver.class) && to_path == resolver.to_path end alias :== :eql? end + + # The same as FileSystemResolver but does not allow templates to store + # a virtual path since it is invalid for such resolvers. + class FallbackFileSystemResolver < FileSystemResolver + def self.instances + [new(""), new("/")] + end + + def decorate(*) + super.each { |t| t.virtual_path = nil } + end + end end diff --git a/actionpack/lib/action_view/template/text.rb b/actionpack/lib/action_view/template/text.rb index 51be831dfb..4261c3b5e2 100644 --- a/actionpack/lib/action_view/template/text.rb +++ b/actionpack/lib/action_view/template/text.rb @@ -25,10 +25,6 @@ module ActionView #:nodoc: def formats [@mime_type.to_sym] end - - def partial? - false - end end end end diff --git a/actionpack/lib/action_view/test_case.rb b/actionpack/lib/action_view/test_case.rb index 137281e5e9..3e2ddffa16 100644 --- a/actionpack/lib/action_view/test_case.rb +++ b/actionpack/lib/action_view/test_case.rb @@ -20,12 +20,12 @@ module ActionView end def initialize + super self.class.controller_path = "" @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new @request.env.delete('PATH_INFO') - @params = {} end end @@ -44,7 +44,7 @@ module ActionView include ActionView::Helpers attr_accessor :controller, :output_buffer, :rendered - + module ClassMethods def tests(helper_class) self.helper_class = helper_class @@ -74,6 +74,11 @@ module ActionView @helper_class ||= determine_default_helper_class(name) end + def new(*) + include_helper_modules! + super + end + private def include_helper_modules! @@ -89,7 +94,6 @@ module ActionView @output_buffer = ActiveSupport::SafeBuffer.new @rendered = '' - self.class.send(:include_helper_modules!) make_test_case_available_to_view! say_no_to_protect_against_forgery! end @@ -99,7 +103,7 @@ module ActionView end def render(options = {}, local_assigns = {}, &block) - view.assign(_assigns) + view.assign(view_assigns) @rendered << output = view.render(options, local_assigns, &block) output end @@ -123,6 +127,7 @@ module ActionView def say_no_to_protect_against_forgery! _helpers.module_eval do + remove_method :protect_against_forgery? if method_defined?(:protect_against_forgery?) def protect_against_forgery? false end @@ -132,8 +137,10 @@ module ActionView def make_test_case_available_to_view! test_case_instance = self _helpers.module_eval do - define_method(:_test_case) { test_case_instance } - private :_test_case + unless private_method_defined?(:_test_case) + define_method(:_test_case) { test_case_instance } + private :_test_case + end end end @@ -149,47 +156,59 @@ module ActionView # The instance of ActionView::Base that is used by +render+. def view @view ||= begin - view = ActionView::Base.new(ActionController::Base.view_paths, {}, @controller) - view.singleton_class.send :include, _helpers - view.singleton_class.send :include, @controller._routes.url_helpers - view.singleton_class.send :delegate, :alert, :notice, :to => "request.flash" - view.extend(Locals) - view.locals = self.locals - view.output_buffer = self.output_buffer - view - end + view = @controller.view_context + view.singleton_class.send :include, _helpers + view.extend(Locals) + view.locals = self.locals + view.output_buffer = self.output_buffer + view + end end alias_method :_view, :view - EXCLUDE_IVARS = %w{ + INTERNAL_IVARS = %w{ + @__name__ + @__io__ @_assertion_wrapped + @_assertions @_result + @_routes @controller @layouts @locals @method_name @output_buffer @partials + @passed @rendered @request @routes @templates + @options @test_passed @view @view_context_class } - def _instance_variables - instance_variables.map(&:to_s) - EXCLUDE_IVARS + def _user_defined_ivars + instance_variables.map(&:to_s) - INTERNAL_IVARS + end + + # Returns a Hash of instance variables and their values, as defined by + # the user in the test case, which are then assigned to the view being + # rendered. This is generally intended for internal use and extension + # frameworks. + def view_assigns + Hash[_user_defined_ivars.map do |var| + [var[1, var.length].to_sym, instance_variable_get(var)] + end] end def _assigns - _instance_variables.inject({}) do |hash, var| - name = var[1..-1].to_sym - hash[name] = instance_variable_get(var) - hash - end + ActiveSupport::Deprecation.warn "ActionView::TestCase#_assigns is deprecated and will be removed in future versions. " << + "Please use view_assigns instead." + view_assigns end def _routes @@ -198,7 +217,7 @@ module ActionView def method_missing(selector, *args) if @controller.respond_to?(:_routes) && - @controller._routes.named_routes.helpers.include?(selector) + @controller._routes.named_routes.helpers.include?(selector) @controller.__send__(selector, *args) else super diff --git a/actionpack/lib/action_view/testing/resolvers.rb b/actionpack/lib/action_view/testing/resolvers.rb index 578c56c6c4..773dfcbb1d 100644 --- a/actionpack/lib/action_view/testing/resolvers.rb +++ b/actionpack/lib/action_view/testing/resolvers.rb @@ -8,36 +8,43 @@ module ActionView #:nodoc: class FixtureResolver < PathResolver attr_reader :hash - def initialize(hash = {}) - super() + def initialize(hash = {}, pattern=nil) + super(pattern) @hash = hash end + def to_s + @hash.keys.join(', ') + end + private def query(path, exts, formats) - query = Regexp.escape(path) - exts.each do |ext| - query << '(' << ext.map {|e| e && Regexp.escape(".#{e}") }.join('|') << '|)' + query = "" + EXTENSIONS.each do |ext| + query << '(' << exts[ext].map {|e| e && Regexp.escape(".#{e}") }.join('|') << '|)' end + query = /^(#{Regexp.escape(path)})#{query}$/ templates = [] - @hash.select { |k,v| k =~ /^#{query}$/ }.each do |path, source| - handler, format = extract_handler_and_format(path, formats) - templates << Template.new(source, path, handler, - :virtual_path => path, :format => format) + @hash.each do |_path, array| + source, updated_at = array + next unless _path =~ query + handler, format = extract_handler_and_format(_path, formats) + templates << Template.new(source, _path, handler, + :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 - class NullResolver < ActionView::PathResolver + class NullResolver < PathResolver def query(path, exts, formats) handler, format = extract_handler_and_format(path, formats) [ActionView::Template.new("Template generated by Null Resolver", path, handler, :virtual_path => path, :format => format)] end end - + end diff --git a/actionpack/test/abstract/abstract_controller_test.rb b/actionpack/test/abstract/abstract_controller_test.rb index 19855490b4..981c023d38 100644 --- a/actionpack/test/abstract/abstract_controller_test.rb +++ b/actionpack/test/abstract/abstract_controller_test.rb @@ -30,14 +30,14 @@ module AbstractController class RenderingController < AbstractController::Base include ::AbstractController::Rendering - def _prefix() end + def _prefixes + [] + end def render(options = {}) if options.is_a?(String) options = {:_template_name => options} end - - options[:_prefix] = _prefix super end @@ -116,8 +116,8 @@ module AbstractController name.underscore end - def _prefix - self.class.prefix + def _prefixes + [self.class.prefix] end end @@ -157,10 +157,10 @@ module AbstractController private def self.layout(formats) begin - find_template(name.underscore, {:formats => formats}, :_prefix => "layouts") + find_template(name.underscore, {:formats => formats}, :_prefixes => ["layouts"]) rescue ActionView::MissingTemplate begin - find_template("application", {:formats => formats}, :_prefix => "layouts") + find_template("application", {:formats => formats}, :_prefixes => ["layouts"]) rescue ActionView::MissingTemplate end end diff --git a/actionpack/test/abstract/callbacks_test.rb b/actionpack/test/abstract/callbacks_test.rb index 232a1679e0..3bdde86291 100644 --- a/actionpack/test/abstract/callbacks_test.rb +++ b/actionpack/test/abstract/callbacks_test.rb @@ -2,27 +2,27 @@ require 'abstract_unit' module AbstractController module Testing - + class ControllerWithCallbacks < AbstractController::Base include AbstractController::Callbacks end - + class Callback1 < ControllerWithCallbacks set_callback :process_action, :before, :first - + def first @text = "Hello world" end - + def index self.response_body = @text end end - + class TestCallbacks1 < ActiveSupport::TestCase test "basic callbacks work" do controller = Callback1.new - result = controller.process(:index) + controller.process(:index) assert_equal "Hello world", controller.response_body end end @@ -31,22 +31,23 @@ module AbstractController before_filter :first after_filter :second around_filter :aroundz - + def first @text = "Hello world" end - + def second @second = "Goodbye" end - + def aroundz @aroundz = "FIRST" yield @aroundz << "SECOND" end - + def index + @text ||= nil self.response_body = @text.to_s end end @@ -54,22 +55,22 @@ module AbstractController class Callback2Overwrite < Callback2 before_filter :first, :except => :index end - + class TestCallbacks2 < ActiveSupport::TestCase def setup @controller = Callback2.new end test "before_filter works" do - result = @controller.process(:index) + @controller.process(:index) assert_equal "Hello world", @controller.response_body end - + test "after_filter works" do @controller.process(:index) assert_equal "Goodbye", @controller.instance_variable_get("@second") end - + test "around_filter works" do @controller.process(:index) assert_equal "FIRSTSECOND", @controller.instance_variable_get("@aroundz") @@ -77,20 +78,20 @@ module AbstractController test "before_filter with overwritten condition" do @controller = Callback2Overwrite.new - result = @controller.process(:index) + @controller.process(:index) assert_equal "", @controller.response_body end end - + class Callback3 < ControllerWithCallbacks before_filter do |c| c.instance_variable_set("@text", "Hello world") end - + after_filter do |c| c.instance_variable_set("@second", "Goodbye") end - + def index self.response_body = @text end @@ -100,41 +101,41 @@ module AbstractController def setup @controller = Callback3.new end - + test "before_filter works with procs" do - result = @controller.process(:index) + @controller.process(:index) assert_equal "Hello world", @controller.response_body end - + test "after_filter works with procs" do - result = @controller.process(:index) + @controller.process(:index) assert_equal "Goodbye", @controller.instance_variable_get("@second") - end + end end - + class CallbacksWithConditions < ControllerWithCallbacks before_filter :list, :only => :index before_filter :authenticate, :except => :index - + def index self.response_body = @list.join(", ") end - + def sekrit_data self.response_body = (@list + [@authenticated]).join(", ") end - + private def list @list = ["Hello", "World"] end - + def authenticate @list = [] @authenticated = "true" end end - + class TestCallbacksWithConditions < ActiveSupport::TestCase def setup @controller = CallbacksWithConditions.new @@ -144,98 +145,99 @@ module AbstractController @controller.process(:index) assert_equal "Hello, World", @controller.response_body end - + test "when :only is specified, a before filter is not triggered on other actions" do @controller.process(:sekrit_data) assert_equal "true", @controller.response_body end - + test "when :except is specified, an after filter is not triggered on that action" do - result = @controller.process(:index) - assert_nil @controller.instance_variable_get("@authenticated") + @controller.process(:index) + assert !@controller.instance_variable_defined?("@authenticated") end end - + class CallbacksWithArrayConditions < ControllerWithCallbacks before_filter :list, :only => [:index, :listy] before_filter :authenticate, :except => [:index, :listy] - + def index self.response_body = @list.join(", ") end - + def sekrit_data self.response_body = (@list + [@authenticated]).join(", ") end - + private def list @list = ["Hello", "World"] end - + def authenticate @list = [] @authenticated = "true" - end + end end - + class TestCallbacksWithArrayConditions < ActiveSupport::TestCase def setup @controller = CallbacksWithArrayConditions.new end test "when :only is specified with an array, a before filter is triggered on that action" do - result = @controller.process(:index) + @controller.process(:index) assert_equal "Hello, World", @controller.response_body end - + test "when :only is specified with an array, a before filter is not triggered on other actions" do - result = @controller.process(:sekrit_data) + @controller.process(:sekrit_data) assert_equal "true", @controller.response_body end - + test "when :except is specified with an array, an after filter is not triggered on that action" do - result = @controller.process(:index) - assert_nil @controller.instance_variable_get("@authenticated") + @controller.process(:index) + assert !@controller.instance_variable_defined?("@authenticated") end - end - + end + class ChangedConditions < Callback2 before_filter :first, :only => :index - + def not_index + @text ||= nil self.response_body = @text.to_s end end - + class TestCallbacksWithChangedConditions < ActiveSupport::TestCase def setup @controller = ChangedConditions.new end - + test "when a callback is modified in a child with :only, it works for the :only action" do - result = @controller.process(:index) + @controller.process(:index) assert_equal "Hello world", @controller.response_body end - + test "when a callback is modified in a child with :only, it does not work for other actions" do - result = @controller.process(:not_index) + @controller.process(:not_index) assert_equal "", @controller.response_body end end - + class SetsResponseBody < ControllerWithCallbacks before_filter :set_body - + def index self.response_body = "Fail" end - + def set_body self.response_body = "Success" end end - + class TestHalting < ActiveSupport::TestCase test "when a callback sets the response body, the action should not be invoked" do controller = SetsResponseBody.new @@ -243,6 +245,27 @@ module AbstractController assert_equal "Success", controller.response_body end end - + + class CallbacksWithArgs < ControllerWithCallbacks + set_callback :process_action, :before, :first + + def first + @text = "Hello world" + end + + def index(text) + self.response_body = @text + text + end + end + + class TestCallbacksWithArgs < ActiveSupport::TestCase + test "callbacks still work when invoking process with multiple args" do + controller = CallbacksWithArgs.new + controller.process(:index, " Howdy!") + assert_equal "Hello world Howdy!", controller.response_body + end + end + + end end diff --git a/actionpack/test/abstract/helper_test.rb b/actionpack/test/abstract/helper_test.rb index 15c27501f2..b28a5b5afb 100644 --- a/actionpack/test/abstract/helper_test.rb +++ b/actionpack/test/abstract/helper_test.rb @@ -4,7 +4,7 @@ ActionController::Base.helpers_path = File.expand_path('../../fixtures/helpers', module AbstractController module Testing - + class ControllerWithHelpers < AbstractController::Base include AbstractController::Rendering include Helpers @@ -13,13 +13,13 @@ module AbstractController render :inline => "Module <%= included_method %>" end end - + module HelperyTest def included_method "Included" end end - + class AbstractHelpers < ControllerWithHelpers helper(HelperyTest) do def helpery_test @@ -38,6 +38,10 @@ module AbstractController end end + class ::HelperyTestController < AbstractHelpers + clear_helpers + end + class AbstractHelpersBlock < ControllerWithHelpers helper do include ::AbstractController::Testing::HelperyTest @@ -45,7 +49,6 @@ module AbstractController end class TestHelpers < ActiveSupport::TestCase - def setup @controller = AbstractHelpers.new end @@ -74,8 +77,22 @@ module AbstractController @controller.process(:with_module) assert_equal "Module Included", @controller.response_body end + end + class ClearHelpersTest < ActiveSupport::TestCase + def setup + @controller = HelperyTestController.new + end + + def test_clears_up_previous_helpers + @controller.process(:with_symbol) + assert_equal "I respond to bare_a: false", @controller.response_body + end + + def test_includes_controller_default_helper + @controller.process(:with_block) + assert_equal "Hello Default", @controller.response_body + end end - end end diff --git a/actionpack/test/abstract/layouts_test.rb b/actionpack/test/abstract/layouts_test.rb index f580ad40f7..5ed6aa68b5 100644 --- a/actionpack/test/abstract/layouts_test.rb +++ b/actionpack/test/abstract/layouts_test.rb @@ -225,7 +225,7 @@ module AbstractControllerTests end test "when the layout is specified as a symbol and the method doesn't exist, raise an exception" do - assert_raises(NoMethodError) { WithSymbolAndNoMethod.new.process(:index) } + assert_raises(NameError) { WithSymbolAndNoMethod.new.process(:index) } end test "when the layout is specified as a symbol and the method returns something besides a string/false/nil, raise an exception" do diff --git a/actionpack/test/abstract/render_test.rb b/actionpack/test/abstract/render_test.rb index 25dc8bd804..b9293d1241 100644 --- a/actionpack/test/abstract/render_test.rb +++ b/actionpack/test/abstract/render_test.rb @@ -6,8 +6,8 @@ module AbstractController class ControllerRenderer < AbstractController::Base include AbstractController::Rendering - def _prefix - "renderer" + def _prefixes + %w[renderer] end self.view_paths = [ActionView::FixtureResolver.new( diff --git a/actionpack/test/abstract/translation_test.rb b/actionpack/test/abstract/translation_test.rb index 09ebfab85e..8ec50fd57a 100644 --- a/actionpack/test/abstract/translation_test.rb +++ b/actionpack/test/abstract/translation_test.rb @@ -7,19 +7,19 @@ class TranslationControllerTest < Test::Unit::TestCase def setup @controller = ActionController::Base.new end - + def test_action_controller_base_responds_to_translate assert_respond_to @controller, :translate end - + def test_action_controller_base_responds_to_t assert_respond_to @controller, :t end - + def test_action_controller_base_responds_to_localize assert_respond_to @controller, :localize end - + def test_action_controller_base_responds_to_l assert_respond_to @controller, :l end diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index 5be47f7c96..60534a9746 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -12,8 +12,16 @@ $:.unshift(File.dirname(__FILE__) + '/fixtures/alternate_helpers') ENV['TMPDIR'] = File.join(File.dirname(__FILE__), 'tmp') -if defined?(Encoding.default_internal) - Encoding.default_internal = "UTF-8" +require 'active_support/core_ext/kernel/reporting' + +require 'active_support/core_ext/string/encoding' +if "ruby".encoding_aware? + # These are the normal settings that will be set up by Railties + # TODO: Have these tests support other combinations of these values + silence_warnings do + Encoding.default_internal = "UTF-8" + Encoding.default_external = "UTF-8" + end end require 'test/unit' @@ -28,25 +36,14 @@ require 'active_record' require 'action_controller/caching' require 'action_controller/caching/sweeping' -begin - require 'ruby-debug' - Debugger.settings[:autoeval] = true - Debugger.start -rescue LoadError - # Debugging disabled. `gem install ruby-debug` to enable. -end - require 'pp' # require 'pp' early to prevent hidden_methods from not picking up the pretty-print methods until too late module Rails -end - -# Monkey patch the old routes initialization to be silenced. -class ActionDispatch::Routing::DeprecatedMapper - def initialize_with_silencer(*args) - ActiveSupport::Deprecation.silence { initialize_without_silencer(*args) } + class << self + def env + @_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "test") + end end - alias_method_chain :initialize, :silencer end ActiveSupport::Dependencies.hook! @@ -120,14 +117,12 @@ module ActiveSupport # Hold off drawing routes until all the possible controller classes # have been loaded. setup_once do - SharedTestRoutes.draw do |map| - # FIXME: match ':controller(/:action(/:id))' - map.connect ':controller/:action/:id' + SharedTestRoutes.draw do + match ':controller(/:action)' end - ActionController::IntegrationTest.app.routes.draw do |map| - # FIXME: match ':controller(/:action(/:id))' - map.connect ':controller/:action/:id' + ActionDispatch::IntegrationTest.app.routes.draw do + match ':controller(/:action)' end end end @@ -165,9 +160,7 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase setup do @routes = SharedTestRoutes end -end -class ActionController::IntegrationTest < ActiveSupport::TestCase def self.build_app(routes = nil) RoutedRackApp.new(routes || ActionDispatch::Routing::RouteSet.new) do |middleware| middleware.use "ActionDispatch::ShowExceptions" @@ -218,7 +211,7 @@ class ActionController::IntegrationTest < ActiveSupport::TestCase end def with_autoload_path(path) - path = File.join(File.dirname(__FILE__), "fixtures", path) + path = File.join(File.dirname(__FILE__), "fixtures", path) if ActiveSupport::Dependencies.autoload_paths.include?(path) yield else @@ -234,7 +227,7 @@ class ActionController::IntegrationTest < ActiveSupport::TestCase end # Temporary base class -class Rack::TestCase < ActionController::IntegrationTest +class Rack::TestCase < ActionDispatch::IntegrationTest def self.testing(klass = nil) if klass @testing = "/#{klass.name.underscore}".sub!(/_controller$/, '') @@ -276,11 +269,27 @@ class Rack::TestCase < ActionController::IntegrationTest end end -class ActionController::Base - def self.test_routes(&block) - routes = ActionDispatch::Routing::RouteSet.new - routes.draw(&block) - include routes.url_helpers +module ActionController + class Base + include ActionController::Testing + # This stub emulates the Railtie including the URL helpers from a Rails application + include SharedTestRoutes.url_helpers + + self.view_paths = FIXTURE_LOAD_PATH + + def self.test_routes(&block) + routes = ActionDispatch::Routing::RouteSet.new + routes.draw(&block) + include routes.url_helpers + end + end + + class TestCase + include ActionDispatch::TestProcess + + setup do + @routes = SharedTestRoutes + end end end @@ -297,25 +306,37 @@ module ActionView end end -module ActionController - class Base - include ActionController::Testing - end +class Workshop + extend ActiveModel::Naming + include ActiveModel::Conversion + attr_accessor :id - Base.view_paths = FIXTURE_LOAD_PATH + def initialize(id) + @id = id + end - class TestCase - include ActionDispatch::TestProcess + def persisted? + id.present? + end - setup do - @routes = SharedTestRoutes - end + def to_s + id.to_s end end -# This stub emulates the Railtie including the URL helpers from a Rails application -module ActionController - class Base - include SharedTestRoutes.url_helpers +module ActionDispatch + class ShowExceptions + private + remove_method :public_path + def public_path + "#{FIXTURE_LOAD_PATH}/public" + end + + remove_method :logger + # Silence logger + def logger + nil + end end end + diff --git a/actionpack/test/action_dispatch/routing/mapper_test.rb b/actionpack/test/action_dispatch/routing/mapper_test.rb new file mode 100644 index 0000000000..e21b271907 --- /dev/null +++ b/actionpack/test/action_dispatch/routing/mapper_test.rb @@ -0,0 +1,58 @@ +require 'abstract_unit' + +module ActionDispatch + module Routing + class MapperTest < ActiveSupport::TestCase + class FakeSet + attr_reader :routes + + def initialize + @routes = [] + end + + def resources_path_names + {} + end + + def request_class + ActionDispatch::Request + end + + def add_route(*args) + routes << args + end + + def conditions + routes.map { |x| x[1] } + end + end + + def test_initialize + Mapper.new FakeSet.new + end + + def test_map_slash + fakeset = FakeSet.new + mapper = Mapper.new fakeset + mapper.match '/', :to => 'posts#index', :as => :main + assert_equal '/', fakeset.conditions.first[:path_info] + end + + def test_map_more_slashes + fakeset = FakeSet.new + mapper = Mapper.new fakeset + + # FIXME: is this a desired behavior? + mapper.match '/one/two/', :to => 'posts#index', :as => :main + assert_equal '/one/two(.:format)', fakeset.conditions.first[:path_info] + end + + def test_map_wildcard + fakeset = FakeSet.new + mapper = Mapper.new fakeset + mapper.match '/*path', :to => 'pages#show', :as => :page + assert_equal '/*path', fakeset.conditions.first[:path_info] + end + end + end +end diff --git a/actionpack/test/activerecord/active_record_store_test.rb b/actionpack/test/activerecord/active_record_store_test.rb index bdd1a0a15c..f0fb113860 100644 --- a/actionpack/test/activerecord/active_record_store_test.rb +++ b/actionpack/test/activerecord/active_record_store_test.rb @@ -1,6 +1,6 @@ require 'active_record_unit' -class ActiveRecordStoreTest < ActionController::IntegrationTest +class ActiveRecordStoreTest < ActionDispatch::IntegrationTest class TestController < ActionController::Base def no_session_access head :ok @@ -23,6 +23,13 @@ class ActiveRecordStoreTest < ActionController::IntegrationTest def call_reset_session session[:foo] reset_session + reset_session if params[:twice] + session[:foo] = "baz" + head :ok + end + + def renew + env["rack.session.options"][:renew] = true session[:foo] = "baz" head :ok end @@ -64,6 +71,20 @@ class ActiveRecordStoreTest < ActionController::IntegrationTest end end end + + define_method("test_renewing_with_#{class_name}_store") do + with_store class_name do + with_test_route_set do + get '/set_session_value' + assert_response :success + assert cookies['_session_id'] + + get '/renew' + assert_response :success + assert_not_equal [], headers['Set-Cookie'] + end + end + end end def test_getting_nil_session_value @@ -74,6 +95,17 @@ class ActiveRecordStoreTest < ActionController::IntegrationTest end end + def test_calling_reset_session_twice_does_not_raise_errors + with_test_route_set do + get '/call_reset_session', :twice => "true" + assert_response :success + + get '/get_session_value' + assert_response :success + assert_equal 'foo: "baz"', response.body + end + end + def test_setting_session_value_after_session_reset with_test_route_set do get '/set_session_value' @@ -198,7 +230,7 @@ class ActiveRecordStoreTest < ActionController::IntegrationTest def with_test_route_set(options = {}) with_routing do |set| - set.draw do |map| + set.draw do match ':action', :to => 'active_record_store_test/test' end diff --git a/actionpack/test/activerecord/controller_runtime_test.rb b/actionpack/test/activerecord/controller_runtime_test.rb index cfd86d704d..7931da3741 100644 --- a/actionpack/test/activerecord/controller_runtime_test.rb +++ b/actionpack/test/activerecord/controller_runtime_test.rb @@ -37,6 +37,6 @@ class ControllerRuntimeLogSubscriberTest < ActionController::TestCase wait assert_equal 2, @logger.logged(:info).size - assert_match /\(Views: [\d\.]+ms | ActiveRecord: [\d\.]+ms\)/, @logger.logged(:info)[1] + assert_match(/\(Views: [\d.]+ms | ActiveRecord: [\d.]+ms\)/, @logger.logged(:info)[1]) end -end
\ No newline at end of file +end diff --git a/actionpack/test/activerecord/polymorphic_routes_test.rb b/actionpack/test/activerecord/polymorphic_routes_test.rb index 90a1ef982c..f9e47d5118 100644 --- a/actionpack/test/activerecord/polymorphic_routes_test.rb +++ b/actionpack/test/activerecord/polymorphic_routes_test.rb @@ -25,6 +25,22 @@ class Series < ActiveRecord::Base set_table_name 'projects' end +module Blog + class Post < ActiveRecord::Base + set_table_name 'projects' + end + + class Blog < ActiveRecord::Base + set_table_name 'projects' + end + + def self._railtie + o = Object.new + def o.railtie_name; "blog" end + o + end +end + class PolymorphicRoutesTest < ActionController::TestCase include SharedTestRoutes.url_helpers self.default_url_options[:host] = 'example.com' @@ -37,6 +53,38 @@ class PolymorphicRoutesTest < ActionController::TestCase @tax = Tax.new @fax = Fax.new @series = Series.new + @blog_post = Blog::Post.new + @blog_blog = Blog::Blog.new + end + + def test_passing_routes_proxy + with_namespaced_routes(:blog) do + proxy = ActionDispatch::Routing::RoutesProxy.new(_routes, self) + @blog_post.save + assert_equal "http://example.com/posts/#{@blog_post.id}", polymorphic_url([proxy, @blog_post]) + end + end + + def test_namespaced_model + with_namespaced_routes(:blog) do + @blog_post.save + assert_equal "http://example.com/posts/#{@blog_post.id}", polymorphic_url(@blog_post) + end + end + + def test_namespaced_model_with_name_the_same_as_namespace + with_namespaced_routes(:blog) do + @blog_blog.save + assert_equal "http://example.com/blogs/#{@blog_blog.id}", polymorphic_url(@blog_blog) + end + end + + def test_namespaced_model_with_nested_resources + with_namespaced_routes(:blog) do + @blog_post.save + @blog_blog.save + assert_equal "http://example.com/blogs/#{@blog_blog.id}/posts/#{@blog_post.id}", polymorphic_url([@blog_blog, @blog_post]) + end end def test_with_record @@ -385,20 +433,36 @@ class PolymorphicRoutesTest < ActionController::TestCase end end + def with_namespaced_routes(name) + with_routing do |set| + set.draw do + scope(:module => name) do + resources :blogs do + resources :posts + end + resources :posts + end + end + + self.class.send(:include, @routes.url_helpers) + yield + end + end + def with_test_routes(options = {}) with_routing do |set| - set.draw do |map| - map.resources :projects do |projects| - projects.resources :tasks - projects.resource :bid do |bid| - bid.resources :tasks + set.draw do + resources :projects do + resources :tasks + resource :bid do + resources :tasks end end - map.resources :taxes do |taxes| - taxes.resources :faxes - taxes.resource :bid + resources :taxes do + resources :faxes + resource :bid end - map.resources :series + resources :series end self.class.send(:include, @routes.url_helpers) diff --git a/actionpack/test/activerecord/render_partial_with_record_identification_test.rb b/actionpack/test/activerecord/render_partial_with_record_identification_test.rb index df50c3dc6f..99f09286ff 100644 --- a/actionpack/test/activerecord/render_partial_with_record_identification_test.rb +++ b/actionpack/test/activerecord/render_partial_with_record_identification_test.rb @@ -11,7 +11,7 @@ class RenderPartialWithRecordIdentificationController < ActionController::Base render :partial => @topic.replies end - def render_with_named_scope + def render_with_scope render :partial => Reply.base end @@ -62,8 +62,8 @@ class RenderPartialWithRecordIdentificationTest < ActiveRecordTestCase assert_equal 'Birdman is better!', @response.body end - def test_rendering_partial_with_named_scope - get :render_with_named_scope + def test_rendering_partial_with_scope + get :render_with_scope assert_template 'replies/_reply' assert_equal 'Birdman is better!Nuh uh!', @response.body end @@ -93,38 +93,6 @@ class RenderPartialWithRecordIdentificationTest < ActiveRecordTestCase end end -class RenderPartialWithRecordIdentificationController < ActionController::Base - def render_with_has_many_and_belongs_to_association - @developer = Developer.find(1) - render :partial => @developer.projects - end - - def render_with_has_many_association - @topic = Topic.find(1) - render :partial => @topic.replies - end - - def render_with_has_many_through_association - @developer = Developer.find(:first) - render :partial => @developer.topics - end - - def render_with_belongs_to_association - @reply = Reply.find(1) - render :partial => @reply.topic - end - - def render_with_record - @developer = Developer.find(:first) - render :partial => @developer - end - - def render_with_record_collection - @developers = Developer.find(:all) - render :partial => @developers - end -end - class Game < Struct.new(:name, :id) extend ActiveModel::Naming include ActiveModel::Conversion diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb index 53cdd358b4..7f3d943bba 100644 --- a/actionpack/test/controller/action_pack_assertions_test.rb +++ b/actionpack/test/controller/action_pack_assertions_test.rb @@ -2,34 +2,26 @@ require 'abstract_unit' require 'action_controller/vendor/html-scanner' require 'controller/fake_controllers' -# a controller class to facilitate the tests class ActionPackAssertionsController < ActionController::Base - # this does absolutely nothing def nothing() head :ok end - # a standard template def hello_world() render :template => "test/hello_world"; end - # a standard template def hello_xml_world() render :template => "test/hello_xml_world"; end - # a standard template rendering PDF def hello_xml_world_pdf self.content_type = "application/pdf" render :template => "test/hello_xml_world" end - # a standard template rendering PDF def hello_xml_world_pdf_header response.headers["Content-Type"] = "application/pdf; charset=utf-8" render :template => "test/hello_xml_world" end - # a standard partial def partial() render :partial => 'test/partial'; end - # a redirect to an internal location def redirect_internal() redirect_to "/nothing"; end def redirect_to_action() redirect_to :action => "flash_me", :id => 1, :params => { "panda" => "fun" }; end @@ -40,33 +32,28 @@ class ActionPackAssertionsController < ActionController::Base def redirect_to_path() redirect_to '/some/path' end + def redirect_invalid_external_route() redirect_to 'ht_tp://www.rubyonrails.org' end + def redirect_to_named_route() redirect_to route_one_url end - # a redirect to an external location def redirect_external() redirect_to "http://www.rubyonrails.org"; end - # a 404 def response404() head '404 AWOL' end - # a 500 def response500() head '500 Sorry' end - # a fictional 599 def response599() head '599 Whoah!' end - # putting stuff in the flash def flash_me flash['hello'] = 'my name is inigo montoya...' render :text => "Inconceivable!" end - # we have a flash, but nothing is in it def flash_me_naked flash.clear render :text => "wow!" end - # assign some template instance variables def assign_this @howdy = "ho" render :inline => "Mr. Henke" @@ -84,61 +71,20 @@ class ActionPackAssertionsController < ActionController::Base render :text => "Hello!", :content_type => Mime::RSS end - # puts something in the session def session_stuffing session['xmas'] = 'turkey' render :text => "ho ho ho" end - # raises exception on get requests - def raise_on_get + def raise_exception_on_get raise "get" if request.get? render :text => "request method: #{request.env['REQUEST_METHOD']}" end - # raises exception on post requests - def raise_on_post + def raise_exception_on_post raise "post" if request.post? render :text => "request method: #{request.env['REQUEST_METHOD']}" end - - def get_valid_record - @record = Class.new do - def valid? - true - end - - def errors - Class.new do - def full_messages; []; end - end.new - end - - end.new - - render :nothing => true - end - - - def get_invalid_record - @record = Class.new do - - def valid? - false - end - - def errors - Class.new do - def full_messages; ['...stuff...']; end - end.new - end - end.new - - render :nothing => true - end - - # 911 - def rescue_action(e) raise; end end # Used to test that assert_response includes the exception message @@ -181,48 +127,39 @@ module Admin end end -# require "action_dispatch/test_process" - -# a test case to exercise the new capabilities TestRequest & TestResponse class ActionPackAssertionsControllerTest < ActionController::TestCase - # -- assertion-based testing ------------------------------------------------ def test_assert_tag_and_url_for get :render_url assert_tag :content => "/action_pack_assertions/flash_me" end - # test the get method, make sure the request really was a get - def test_get - assert_raise(RuntimeError) { get :raise_on_get } - get :raise_on_post + def test_get_request + assert_raise(RuntimeError) { get :raise_exception_on_get } + get :raise_exception_on_post assert_equal @response.body, 'request method: GET' end - # test the get method, make sure the request really was a get - def test_post - assert_raise(RuntimeError) { post :raise_on_post } - post :raise_on_get + def test_post_request + assert_raise(RuntimeError) { post :raise_exception_on_post } + post :raise_exception_on_get + assert_equal @response.body, 'request method: POST' + end + + def test_get_post_request_switch + post :raise_exception_on_get + assert_equal @response.body, 'request method: POST' + get :raise_exception_on_post + assert_equal @response.body, 'request method: GET' + post :raise_exception_on_get assert_equal @response.body, 'request method: POST' + get :raise_exception_on_post + assert_equal @response.body, 'request method: GET' end -# the following test fails because the request_method is now cached on the request instance -# test the get/post switch within one test action -# def test_get_post_switch -# post :raise_on_get -# assert_equal @response.body, 'request method: POST' -# get :raise_on_post -# assert_equal @response.body, 'request method: GET' -# post :raise_on_get -# assert_equal @response.body, 'request method: POST' -# get :raise_on_post -# assert_equal @response.body, 'request method: GET' -# end - - # test the redirection to a named route - def test_assert_redirect_to_named_route + def test_redirect_to_named_route with_routing do |set| - set.draw do |map| + set.draw do match 'route_one', :to => 'action_pack_assertions#nothing', :as => :route_one match ':controller/:action' end @@ -234,9 +171,17 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase end end + def test_string_constraint + with_routing do |set| + set.draw do + match "photos", :to => 'action_pack_assertions#nothing', :constraints => {:subdomain => "admin"} + end + end + end + def test_assert_redirect_to_named_route_failure with_routing do |set| - set.draw do |map| + set.draw do match 'route_one', :to => 'action_pack_assertions#nothing', :as => :route_one match 'route_two', :to => 'action_pack_assertions#nothing', :id => 'two', :as => :route_two match ':controller/:action' @@ -258,10 +203,9 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase @controller = Admin::InnerModuleController.new with_routing do |set| - set.draw do |map| + set.draw do match 'admin/inner_module', :to => 'admin/inner_module#index', :as => :admin_inner_module - # match ':controller/:action' - map.connect ':controller/:action/:id' + match ':controller/:action' end process :redirect_to_index # redirection is <{"action"=>"index", "controller"=>"admin/admin/inner_module"}> @@ -273,10 +217,9 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase @controller = Admin::InnerModuleController.new with_routing do |set| - set.draw do |map| + set.draw do match '/action_pack_assertions/:id', :to => 'action_pack_assertions#index', :as => :top_level - # match ':controller/:action' - map.connect ':controller/:action/:id' + match ':controller/:action' end process :redirect_to_top_level_named_route # assert_redirected_to "http://test.host/action_pack_assertions/foo" would pass because of exact match early return @@ -288,11 +231,10 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase @controller = Admin::InnerModuleController.new with_routing do |set| - set.draw do |map| + set.draw do # this controller exists in the admin namespace as well which is the only difference from previous test match '/user/:id', :to => 'user#index', :as => :top_level - # match ':controller/:action' - map.connect ':controller/:action/:id' + match ':controller/:action' end process :redirect_to_top_level_named_route # assert_redirected_to top_level_url('foo') would pass because of exact match early return @@ -300,53 +242,44 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase end end - # -- standard request/response object testing -------------------------------- - - # make sure that the template objects exist - def test_template_objects_alive + def test_template_objects_exist process :assign_this - assert !@controller.instance_variable_get(:"@hi") + assert !@controller.instance_variable_defined?(:"@hi") assert @controller.instance_variable_get(:"@howdy") end - # make sure we don't have template objects when we shouldn't - def test_template_object_missing + def test_template_objects_missing process :nothing - assert_nil @controller.instance_variable_get(:@howdy) + assert !@controller.instance_variable_defined?(:@howdy) end - # check the empty flashing - def test_flash_me_naked + def test_empty_flash process :flash_me_naked - assert_deprecated do - assert !@response.has_flash? - assert !@response.has_flash_with_contents? - end + assert flash.empty? end - # check if we have flash objects - def test_flash_haves + def test_flash_exist process :flash_me - assert_deprecated do - assert @response.has_flash? - assert @response.has_flash_with_contents? - assert @response.has_flash_object?('hello') - end + assert flash.any? + assert_present flash['hello'] end - # ensure we don't have flash objects - def test_flash_have_nots + def test_flash_does_not_exist process :nothing - assert_deprecated do - assert !@response.has_flash? - assert !@response.has_flash_with_contents? - assert_nil @response.flash['hello'] - end + assert flash.empty? end + def test_session_exist + process :session_stuffing + assert_equal session['xmas'], 'turkey' + end - # check if we were rendered by a file-based template? - def test_rendered_action + def session_does_not_exist + process :nothing + assert session.empty? + end + + def test_render_template_action process :nothing assert_template nil @@ -354,7 +287,6 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase assert_template 'hello_world' end - # check the redirection location def test_redirection_location process :redirect_internal assert_equal 'http://test.host/nothing', @response.redirect_url @@ -368,7 +300,6 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase assert_nil @response.redirect_url end - # check server errors def test_server_error_response_code process :response500 assert @response.server_error? @@ -380,31 +311,23 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase assert !@response.server_error? end - # check a 404 response code def test_missing_response_code process :response404 assert @response.missing? end - # check client errors def test_client_error_response_code process :response404 assert @response.client_error? end - # check to see if our redirection matches a pattern def test_redirect_url_match process :redirect_external assert @response.redirect? - assert_deprecated do - assert @response.redirect_url_match?("rubyonrails") - assert @response.redirect_url_match?(/rubyonrails/) - assert !@response.redirect_url_match?("phpoffrails") - assert !@response.redirect_url_match?(/perloffrails/) - end + assert_match(/rubyonrails/, @response.redirect_url) + assert !/perloffrails/.match(@response.redirect_url) end - # check for a redirection def test_redirection process :redirect_internal assert @response.redirect? @@ -416,14 +339,12 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase assert !@response.redirect? end - # check a successful response code def test_successful_response_code process :nothing assert @response.success? end - # a basic check to make sure we have a TestResponse object - def test_has_response + def test_response_object process :nothing assert_kind_of ActionController::TestResponse, @response end @@ -457,6 +378,11 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase end end + def test_redirect_invalid_external_route + process :redirect_invalid_external_route + assert_redirected_to "http://test.hostht_tp://www.rubyonrails.org" + end + def test_redirected_to_url_full_url process :redirect_to_path assert_redirected_to 'http://test.host/some/path' @@ -547,6 +473,14 @@ class AssertTemplateTest < ActionController::TestCase assert_template :hello_planet end end + + def test_assert_template_reset_between_requests + get :hello_world + assert_template 'test/hello_world' + + get :nothing + assert_template nil + end end class ActionPackHeaderTest < ActionController::TestCase diff --git a/actionpack/test/controller/assert_select_test.rb b/actionpack/test/controller/assert_select_test.rb index 4f8ad23174..f63321c78b 100644 --- a/actionpack/test/controller/assert_select_test.rb +++ b/actionpack/test/controller/assert_select_test.rb @@ -15,10 +15,8 @@ class AssertSelectTest < ActionController::TestCase class AssertSelectMailer < ActionMailer::Base def test(html) - recipients "test <test@test.host>" - from "test@test.host" - subject "Test e-mail" - part :content_type=>"text/html", :body=>html + mail :body => html, :content_type => "text/html", + :subject => "Test e-mail", :from => "test@test.host", :to => "test <test@test.host>" end end @@ -257,7 +255,7 @@ class AssertSelectTest < ActionController::TestCase end assert_raise(Assertion) {assert_select_rjs :insert, :top, "test2"} end - + def test_assert_select_rjs_for_redirect_to render_rjs do |page| page.redirect_to '/' @@ -461,8 +459,8 @@ class AssertSelectTest < ActionController::TestCase assert_select_rjs :remove, "test1" - rescue Assertion - assert_equal "No RJS statement that removes 'test1' was rendered.", $!.message + rescue Assertion => e + assert_equal "No RJS statement that removes 'test1' was rendered.", e.message end def test_assert_select_rjs_for_remove_ignores_block @@ -493,8 +491,8 @@ class AssertSelectTest < ActionController::TestCase assert_select_rjs :show, "test1" - rescue Assertion - assert_equal "No RJS statement that shows 'test1' was rendered.", $!.message + rescue Assertion => e + assert_equal "No RJS statement that shows 'test1' was rendered.", e.message end def test_assert_select_rjs_for_show_ignores_block @@ -525,8 +523,8 @@ class AssertSelectTest < ActionController::TestCase assert_select_rjs :hide, "test1" - rescue Assertion - assert_equal "No RJS statement that hides 'test1' was rendered.", $!.message + rescue Assertion => e + assert_equal "No RJS statement that hides 'test1' was rendered.", e.message end def test_assert_select_rjs_for_hide_ignores_block @@ -557,8 +555,8 @@ class AssertSelectTest < ActionController::TestCase assert_select_rjs :toggle, "test1" - rescue Assertion - assert_equal "No RJS statement that toggles 'test1' was rendered.", $!.message + rescue Assertion => e + assert_equal "No RJS statement that toggles 'test1' was rendered.", e.message end def test_assert_select_rjs_for_toggle_ignores_block @@ -731,7 +729,7 @@ EOF end def render_rjs(&block) - @controller.response_with &block + @controller.response_with(&block) get :rjs end diff --git a/actionpack/test/controller/base_test.rb b/actionpack/test/controller/base_test.rb index ae270b751e..9c22a4e7e0 100644 --- a/actionpack/test/controller/base_test.rb +++ b/actionpack/test/controller/base_test.rb @@ -87,7 +87,7 @@ class RecordIdentifierController < ActionController::Base end class ControllerClassTests < ActiveSupport::TestCase - + def test_controller_path assert_equal 'empty', EmptyController.controller_path assert_equal EmptyController.controller_path, EmptyController.new.controller_path @@ -99,20 +99,6 @@ class ControllerClassTests < ActiveSupport::TestCase assert_equal 'empty', EmptyController.controller_name assert_equal 'contained_empty', Submodule::ContainedEmptyController.controller_name end - - def test_filter_parameter_logging - parameters = [] - config = mock(:config => mock(:filter_parameters => parameters)) - Rails.expects(:application).returns(config) - - assert_deprecated do - Class.new(ActionController::Base) do - filter_parameter_logging :password - end - end - - assert_equal [:password], parameters - end def test_record_identifier assert_respond_to RecordIdentifierController.new, :dom_id @@ -164,15 +150,15 @@ class PerformActionTest < ActionController::TestCase rescue_action_in_public! end - + def test_process_should_be_precise use_controller EmptyController exception = assert_raise AbstractController::ActionNotFound do get :non_existent end - assert_equal exception.message, "The action 'non_existent' could not be found for EmptyController" + assert_equal exception.message, "The action 'non_existent' could not be found for EmptyController" end - + def test_get_on_priv_should_show_selector use_controller MethodMissingController get :shouldnt_be_called @@ -224,7 +210,7 @@ class UrlOptionsTest < ActionController::TestCase assert_equal 'http://www.override.com/from_view?locale=en', @controller.send(:from_view_url) assert_equal 'http://www.override.com/default_url_options/new?locale=en', @controller.url_for(:controller => 'default_url_options') end - end + end def test_url_helpers_does_not_become_actions with_routing do |set| @@ -308,7 +294,7 @@ class EmptyUrlOptionsTest < ActionController::TestCase @controller.request = @request with_routing do |set| - set.draw do |map| + set.draw do resources :things end diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index c161bea945..01f3e8f2b6 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -16,6 +16,7 @@ end class PageCachingTestController < CachingController caches_page :ok, :no_content, :if => Proc.new { |c| !c.request.format.json? } caches_page :found, :not_found + caches_page :about_me def ok @@ -47,6 +48,14 @@ class PageCachingTestController < CachingController def trailing_slash render :text => "Sneak attack" end + + def about_me + respond_to do |format| + format.html {render :text => 'I am html'} + format.xml {render :text => 'I am xml'} + end + end + end class PageCachingTest < ActionController::TestCase @@ -76,7 +85,7 @@ class PageCachingTest < ActionController::TestCase def test_page_caching_resources_saves_to_correct_path_with_extension_even_if_default_route with_routing do |set| - set.draw do |map| + set.draw do match 'posts.:format', :to => 'posts#index', :as => :formatted_posts match '/', :to => 'posts#index', :as => :main end @@ -111,6 +120,13 @@ class PageCachingTest < ActionController::TestCase assert File.exist?("#{FILE_STORE_PATH}/page_caching_test/trailing_slash.html") end + def test_should_obey_http_accept_attribute + @request.env['HTTP_ACCEPT'] = 'text/xml' + get :about_me + assert File.exist?("#{FILE_STORE_PATH}/page_caching_test/about_me.xml") + assert_equal 'I am xml', @response.body + end + def test_should_cache_with_trailing_slash_on_url @controller.class.cache_page 'cached content', '/page_caching_test/trailing_slash/' assert File.exist?("#{FILE_STORE_PATH}/page_caching_test/trailing_slash.html") @@ -140,6 +156,17 @@ class PageCachingTest < ActionController::TestCase assert_page_not_cached :ok end + def test_page_caching_directory_set_as_pathname + begin + ActionController::Base.page_cache_directory = Pathname.new(FILE_STORE_PATH) + get :ok + assert_response :ok + assert_page_cached :ok + ensure + ActionController::Base.page_cache_directory = FILE_STORE_PATH + end + end + private def assert_page_cached(action, message = "#{action} should have been cached") assert page_cached?(action), message @@ -185,6 +212,7 @@ class ActionCachingTestController < CachingController def with_layout @cache_this = MockTime.now.to_f.to_s + @title = nil render :text => @cache_this, :layout => true end @@ -240,7 +268,6 @@ class ActionCachingMockController end def request - mocked_path = @mock_path Object.new.instance_eval(<<-EVAL) def path; '#{@mock_path}' end def format; 'all' end @@ -399,7 +426,6 @@ class ActionCacheTest < ActionController::TestCase get :index assert_response :success - new_cached_time = content_to_cache assert_not_equal cached_time, @response.body end @@ -452,7 +478,7 @@ class ActionCacheTest < ActionController::TestCase def test_xml_version_of_resource_is_treated_as_different_cache with_routing do |set| - set.draw do |map| + set.draw do match ':controller(/:action(.:format))' end @@ -533,6 +559,11 @@ class ActionCacheTest < ActionController::TestCase assert_response 404 end + def test_four_oh_four_renders_content + get :four_oh_four + assert_equal "404'd!", @response.body + end + def test_simple_runtime_error_returns_500_for_multiple_requests get :simple_runtime_error assert_response 500 @@ -637,7 +668,7 @@ class FragmentCachingTest < ActionController::TestCase @store.write('views/another_name', 'another_value') @store.write('views/primalgrasp', 'will not expire ;-)') - @controller.expire_fragment /name/ + @controller.expire_fragment(/name/) assert_nil @store.read('views/name') assert_nil @store.read('views/another_name') @@ -727,23 +758,23 @@ CACHED def test_fragment_caching_in_partials get :html_fragment_cached_with_partial assert_response :success - assert_match /Old fragment caching in a partial/, @response.body - assert_match "Old fragment caching in a partial", @store.read('views/test.host/functional_caching/html_fragment_cached_with_partial') + assert_match(/Old fragment caching in a partial/, @response.body) + assert_match("Old fragment caching in a partial", @store.read('views/test.host/functional_caching/html_fragment_cached_with_partial')) end def test_render_inline_before_fragment_caching get :inline_fragment_cached assert_response :success - assert_match /Some inline content/, @response.body - assert_match /Some cached content/, @response.body - assert_match "Some cached content", @store.read('views/test.host/functional_caching/inline_fragment_cached') + assert_match(/Some inline content/, @response.body) + assert_match(/Some cached content/, @response.body) + assert_match("Some cached content", @store.read('views/test.host/functional_caching/inline_fragment_cached')) end def test_fragment_caching_in_rjs_partials xhr :get, :js_fragment_cached_with_partial assert_response :success - assert_match /Old fragment caching in a partial/, @response.body - assert_match "Old fragment caching in a partial", @store.read('views/test.host/functional_caching/js_fragment_cached_with_partial') + assert_match(/Old fragment caching in a partial/, @response.body) + assert_match("Old fragment caching in a partial", @store.read('views/test.host/functional_caching/js_fragment_cached_with_partial')) end def test_html_formatted_fragment_caching diff --git a/actionpack/test/controller/capture_test.rb b/actionpack/test/controller/capture_test.rb index 47253f22b8..d78acb8ce8 100644 --- a/actionpack/test/controller/capture_test.rb +++ b/actionpack/test/controller/capture_test.rb @@ -6,21 +6,29 @@ class CaptureController < ActionController::Base def self.controller_path; "test"; end def content_for + @title = nil render :layout => "talk_from_action" end def content_for_with_parameter + @title = nil render :layout => "talk_from_action" end def content_for_concatenated + @title = nil render :layout => "talk_from_action" end def non_erb_block_content_for + @title = nil render :layout => "talk_from_action" end + def proper_block_detection + @todo = "some todo" + end + def rescue_action(e) raise end end @@ -62,8 +70,8 @@ class CaptureTest < ActionController::TestCase end def test_proper_block_detection - @todo = "some todo" get :proper_block_detection + assert_equal "some todo", @response.body end private diff --git a/actionpack/test/controller/content_type_test.rb b/actionpack/test/controller/content_type_test.rb index 967107853b..9500c25a32 100644 --- a/actionpack/test/controller/content_type_test.rb +++ b/actionpack/test/controller/content_type_test.rb @@ -29,18 +29,18 @@ class OldContentTypeController < ActionController::Base render :text => "hello world!" end - def render_default_for_rhtml + def render_default_for_erb end - def render_default_for_rxml + def render_default_for_builder end def render_default_for_rjs end - def render_change_for_rxml + def render_change_for_builder response.content_type = Mime::HTML - render :action => "render_default_for_rxml" + render :action => "render_default_for_builder" end def render_default_content_types_for_respond_to @@ -108,23 +108,23 @@ class ContentTypeTest < ActionController::TestCase assert_equal "utf-8", @response.charset, @response.headers.inspect end - def test_nil_default_for_rhtml + def test_nil_default_for_erb OldContentTypeController.default_charset = nil - get :render_default_for_rhtml + get :render_default_for_erb assert_equal Mime::HTML, @response.content_type assert_nil @response.charset, @response.headers.inspect ensure OldContentTypeController.default_charset = "utf-8" end - def test_default_for_rhtml - get :render_default_for_rhtml + def test_default_for_erb + get :render_default_for_erb assert_equal Mime::HTML, @response.content_type assert_equal "utf-8", @response.charset end - def test_default_for_rxml - get :render_default_for_rxml + def test_default_for_builder + get :render_default_for_builder assert_equal Mime::XML, @response.content_type assert_equal "utf-8", @response.charset end @@ -135,8 +135,8 @@ class ContentTypeTest < ActionController::TestCase assert_equal "utf-8", @response.charset end - def test_change_for_rxml - get :render_change_for_rxml + def test_change_for_builder + get :render_change_for_builder assert_equal Mime::HTML, @response.content_type assert_equal "utf-8", @response.charset end diff --git a/actionpack/test/controller/dispatcher_test.rb b/actionpack/test/controller/dispatcher_test.rb deleted file mode 100644 index 7e19bce3b7..0000000000 --- a/actionpack/test/controller/dispatcher_test.rb +++ /dev/null @@ -1,59 +0,0 @@ -require 'abstract_unit' - -# Ensure deprecated dispatcher works -class DeprecatedDispatcherTest < ActiveSupport::TestCase - class DummyApp - def call(env) - [200, {}, 'response'] - end - end - - def setup - ActionDispatch::Callbacks.reset_callbacks(:prepare) - ActionDispatch::Callbacks.reset_callbacks(:call) - end - - def test_assert_deprecated_to_prepare - a = nil - - assert_deprecated do - ActionController::Dispatcher.to_prepare { a = 1 } - end - - assert_nil a - dispatch - assert_equal 1, a - end - - def test_assert_deprecated_before_dispatch - a = nil - - assert_deprecated do - ActionController::Dispatcher.before_dispatch { a = 1 } - end - - assert_nil a - dispatch - assert_equal 1, a - end - - def test_assert_deprecated_after_dispatch - a = nil - - assert_deprecated do - ActionController::Dispatcher.after_dispatch { a = 1 } - end - - assert_nil a - dispatch - assert_equal 1, a - end - - private - - def dispatch(cache_classes = true) - @dispatcher ||= ActionDispatch::Callbacks.new(DummyApp.new, !cache_classes) - @dispatcher.call({'rack.input' => StringIO.new('')}) - end - -end diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb index 14f1bd797a..9e44e8e088 100644 --- a/actionpack/test/controller/filters_test.rb +++ b/actionpack/test/controller/filters_test.rb @@ -78,7 +78,8 @@ class FilterTest < ActionController::TestCase end class RenderingController < ActionController::Base - before_filter :render_something_else + before_filter :before_filter_rendering + after_filter :unreached_after_filter def show @ran_action = true @@ -86,9 +87,59 @@ class FilterTest < ActionController::TestCase end private - def render_something_else + def before_filter_rendering + @ran_filter ||= [] + @ran_filter << "before_filter_rendering" render :inline => "something else" end + + def unreached_after_filter + @ran_filter << "unreached_after_filter_after_render" + end + end + + class RenderingForPrependAfterFilterController < RenderingController + prepend_after_filter :unreached_prepend_after_filter + + private + def unreached_prepend_after_filter + @ran_filter << "unreached_preprend_after_filter_after_render" + end + end + + class BeforeFilterRedirectionController < ActionController::Base + before_filter :before_filter_redirects + after_filter :unreached_after_filter + + def show + @ran_action = true + render :inline => "ran show action" + end + + def target_of_redirection + @ran_target_of_redirection = true + render :inline => "ran target_of_redirection action" + end + + private + def before_filter_redirects + @ran_filter ||= [] + @ran_filter << "before_filter_redirects" + redirect_to(:action => 'target_of_redirection') + end + + def unreached_after_filter + @ran_filter << "unreached_after_filter_after_redirection" + end + end + + class BeforeFilterRedirectionForPrependAfterFilterController < BeforeFilterRedirectionController + prepend_after_filter :unreached_prepend_after_filter_after_redirection + + private + def unreached_prepend_after_filter_after_redirection + @ran_filter << "unreached_prepend_after_filter_after_redirection" + end end class ConditionalFilterController < ActionController::Base @@ -314,6 +365,7 @@ class FilterTest < ActionController::TestCase def initialize @@execution_log = "" + super() end before_filter { |c| c.class.execution_log << " before procfilter " } @@ -447,18 +499,34 @@ class FilterTest < ActionController::TestCase class ::AppSweeper < ActionController::Caching::Sweeper; end class SweeperTestController < ActionController::Base - cache_sweeper :app_sweeper + cache_sweeper :app_sweeper def show render :text => 'hello world' end end + + class ImplicitActionsController < ActionController::Base + before_filter :find_only, :only => :edit + before_filter :find_except, :except => :edit + + private + + def find_only + @only = 'Only' + end + + def find_except + @except = 'Except' + end + end + def test_sweeper_should_not_block_rendering response = test_process(SweeperTestController) assert_equal 'hello world', response.body end def test_before_method_of_sweeper_should_always_return_true - sweeper = ActionController::Caching::Sweeper.send(:new) + sweeper = ActionController::Caching::Sweeper.send(:new) assert sweeper.before(TestController.new) end @@ -611,7 +679,7 @@ class FilterTest < ActionController::TestCase end def test_prepending_and_appending_around_filter - controller = test_process(MixedFilterController) + test_process(MixedFilterController) assert_equal " before aroundfilter before procfilter before appended aroundfilter " + " after appended aroundfilter after procfilter after aroundfilter ", MixedFilterController.execution_log @@ -623,6 +691,32 @@ class FilterTest < ActionController::TestCase assert !assigns["ran_action"] end + def test_before_filter_rendering_breaks_filtering_chain_for_after_filter + test_process(RenderingController) + assert_equal %w( before_filter_rendering ), assigns["ran_filter"] + assert !assigns["ran_action"] + end + + def test_before_filter_redirects_breaks_filtering_chain_for_after_filter + test_process(BeforeFilterRedirectionController) + assert_response :redirect + assert_equal "http://test.host/filter_test/before_filter_redirection/target_of_redirection", redirect_to_url + assert_equal %w( before_filter_redirects ), assigns["ran_filter"] + end + + def test_before_filter_rendering_breaks_filtering_chain_for_preprend_after_filter + test_process(RenderingForPrependAfterFilterController) + assert_equal %w( before_filter_rendering ), assigns["ran_filter"] + assert !assigns["ran_action"] + end + + def test_before_filter_redirects_breaks_filtering_chain_for_preprend_after_filter + test_process(BeforeFilterRedirectionForPrependAfterFilterController) + assert_response :redirect + assert_equal "http://test.host/filter_test/before_filter_redirection_for_prepend_after_filter/target_of_redirection", redirect_to_url + assert_equal %w( before_filter_redirects ), assigns["ran_filter"] + end + def test_filters_with_mixed_specialization_run_in_order assert_nothing_raised do response = test_process(MixedSpecializationController, 'bar') @@ -668,7 +762,7 @@ class FilterTest < ActionController::TestCase assert_equal %w( ensure_login find_user ), assigns["ran_filter"] test_process(ConditionalSkippingController, "login") - assert_nil @controller.instance_variable_get("@ran_after_filter") + assert !@controller.instance_variable_defined?("@ran_after_filter") test_process(ConditionalSkippingController, "change_password") assert_equal %w( clean_up ), @controller.instance_variable_get("@ran_after_filter") end @@ -704,6 +798,18 @@ class FilterTest < ActionController::TestCase assert_equal("I rescued this: #<FilterTest::ErrorToRescue: Something made the bad noise.>", response.body) end + def test_filters_obey_only_and_except_for_implicit_actions + test_process(ImplicitActionsController, 'show') + assert_equal 'Except', assigns(:except) + assert_nil assigns(:only) + assert_equal 'show', response.body + + test_process(ImplicitActionsController, 'edit') + assert_equal 'Only', assigns(:only) + assert_nil assigns(:except) + assert_equal 'edit', response.body + end + private def test_process(controller, action = "show") @controller = controller.is_a?(Class) ? controller.new : controller @@ -756,12 +862,12 @@ class ControllerWithSymbolAsFilter < PostsController def without_exception # Do stuff... - 1 + 1 + wtf = 1 + 1 yield # Do stuff... - 1 + 1 + wtf += 1 end end diff --git a/actionpack/test/controller/flash_test.rb b/actionpack/test/controller/flash_test.rb index 4be09f8c83..3569a2f213 100644 --- a/actionpack/test/controller/flash_test.rb +++ b/actionpack/test/controller/flash_test.rb @@ -209,7 +209,7 @@ class FlashTest < ActionController::TestCase end end -class FlashIntegrationTest < ActionController::IntegrationTest +class FlashIntegrationTest < ActionDispatch::IntegrationTest SessionKey = '_myapp_session' SessionSecret = 'b3c631c314c0bbca50c1b2843150fe33' @@ -255,7 +255,7 @@ class FlashIntegrationTest < ActionController::IntegrationTest def with_test_route_set with_routing do |set| - set.draw do |map| + set.draw do match ':action', :to => FlashIntegrationTest::TestController end diff --git a/actionpack/test/controller/helper_test.rb b/actionpack/test/controller/helper_test.rb index 9b9657929f..9f0670ffdf 100644 --- a/actionpack/test/controller/helper_test.rb +++ b/actionpack/test/controller/helper_test.rb @@ -25,6 +25,32 @@ class AllHelpersController < ActionController::Base helper :all end +module ImpressiveLibrary + extend ActiveSupport::Concern + included do + helper_method :useful_function + end + + def useful_function() end +end + +ActionController::Base.send :include, ImpressiveLibrary + +class JustMeController < ActionController::Base + clear_helpers + + def flash + render :inline => "<h1><%= notice %></h1>" + end + + def lib + render :inline => '<%= useful_function %>' + end +end + +class MeTooController < JustMeController +end + module LocalAbcHelper def a() end def b() end @@ -50,7 +76,7 @@ class HelperTest < ActiveSupport::TestCase # Set default test helper. self.test_helper = LocalAbcHelper end - + def test_deprecated_helper assert_equal expected_helper_methods, missing_methods assert_nothing_raised { @controller_class.helper TestHelper } @@ -70,28 +96,45 @@ class HelperTest < ActiveSupport::TestCase def call_controller(klass, action) request = ActionController::TestRequest.new - klass.action(action).call(request.env) + klass.action(action).call(request.env) end def test_helper_for_nested_controller - assert_equal 'hello: Iz guuut!', + assert_equal 'hello: Iz guuut!', call_controller(Fun::GamesController, "render_hello_world").last.body # request = ActionController::TestRequest.new - # + # # resp = Fun::GamesController.action(:render_hello_world).call(request.env) # assert_equal 'hello: Iz guuut!', resp.last.body end def test_helper_for_acronym_controller assert_equal "test: baz", call_controller(Fun::PdfController, "test").last.body - # + # # request = ActionController::TestRequest.new # response = ActionController::TestResponse.new # request.action = 'test' - # + # # assert_equal 'test: baz', Fun::PdfController.process(request, response).body end + def test_default_helpers_only + assert_equal [JustMeHelper], JustMeController._helpers.ancestors.reject(&:anonymous?) + assert_equal [MeTooHelper, JustMeHelper], MeTooController._helpers.ancestors.reject(&:anonymous?) + end + + def test_base_helper_methods_after_clear_helpers + assert_nothing_raised do + call_controller(JustMeController, "flash") + end + end + + def test_lib_helper_methods_after_clear_helpers + assert_nothing_raised do + call_controller(JustMeController, "lib") + end + end + def test_all_helpers methods = AllHelpersController._helpers.instance_methods.map {|m| m.to_s} @@ -135,17 +178,6 @@ class HelperTest < ActiveSupport::TestCase assert methods.include?('foobar') end - def test_deprecation - assert_deprecated do - ActionController::Base.helpers_dir = "some/foo/bar" - end - assert_deprecated do - assert_equal ["some/foo/bar"], ActionController::Base.helpers_dir - end - ensure - ActionController::Base.helpers_path = File.expand_path('../../fixtures/helpers', __FILE__) - end - private def expected_helper_methods TestHelper.instance_methods.map {|m| m.to_s } @@ -192,7 +224,7 @@ class IsolatedHelpersTest < ActiveSupport::TestCase def call_controller(klass, action) request = ActionController::TestRequest.new - klass.action(action).call(request.env) + klass.action(action).call(request.env) end def setup diff --git a/actionpack/test/controller/http_basic_authentication_test.rb b/actionpack/test/controller/http_basic_authentication_test.rb index 23688ca584..01c650a494 100644 --- a/actionpack/test/controller/http_basic_authentication_test.rb +++ b/actionpack/test/controller/http_basic_authentication_test.rb @@ -13,7 +13,7 @@ class HttpBasicAuthenticationTest < ActionController::TestCase def display render :text => 'Definitely Maybe' end - + def show render :text => 'Only for loooooong credentials' end @@ -33,7 +33,7 @@ class HttpBasicAuthenticationTest < ActionController::TestCase request_http_basic_authentication("SuperSecret") end end - + def authenticate_long_credentials authenticate_or_request_with_http_basic do |username, password| username == '1234567890123456789012345678901234567890' && password == '1234567890123456789012345678901234567890' @@ -56,7 +56,7 @@ class HttpBasicAuthenticationTest < ActionController::TestCase test "successful authentication with #{header.downcase} and long credentials" do @request.env[header] = encode_credentials('1234567890123456789012345678901234567890', '1234567890123456789012345678901234567890') get :show - + assert_response :success assert_equal 'Only for loooooong credentials', @response.body, "Authentication failed for request header #{header} and long credentials" end diff --git a/actionpack/test/controller/http_token_authentication_test.rb b/actionpack/test/controller/http_token_authentication_test.rb index 3dfccae3db..3054c1684c 100644 --- a/actionpack/test/controller/http_token_authentication_test.rb +++ b/actionpack/test/controller/http_token_authentication_test.rb @@ -13,7 +13,7 @@ class HttpTokenAuthenticationTest < ActionController::TestCase def display render :text => 'Definitely Maybe' end - + def show render :text => 'Only for loooooong credentials' end @@ -33,7 +33,7 @@ class HttpTokenAuthenticationTest < ActionController::TestCase request_http_token_authentication("SuperSecret") end end - + def authenticate_long_credentials authenticate_or_request_with_http_token do |token, options| token == '1234567890123456789012345678901234567890' && options[:algorithm] == 'test' @@ -56,7 +56,7 @@ class HttpTokenAuthenticationTest < ActionController::TestCase test "successful authentication with #{header.downcase} and long credentials" do @request.env[header] = encode_credentials('1234567890123456789012345678901234567890', :algorithm => 'test') get :show - + assert_response :success assert_equal 'Only for loooooong credentials', @response.body, "Authentication failed for request header #{header} and long credentials" end diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index 5ee8e2b6ae..f0d62b0b13 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -8,7 +8,7 @@ class SessionTest < Test::Unit::TestCase } def setup - @session = ActionController::Integration::Session.new(StubApp) + @session = ActionDispatch::Integration::Session.new(StubApp) end def test_https_bang_works_and_sets_truth_by_default @@ -167,7 +167,7 @@ end class IntegrationTestTest < Test::Unit::TestCase def setup - @test = ::ActionController::IntegrationTest.new(:default_test) + @test = ::ActionDispatch::IntegrationTest.new(:app) @test.class.stubs(:fixture_table_names).returns([]) @session = @test.open_session end @@ -202,7 +202,7 @@ end # Tests that integration tests don't call Controller test methods for processing. # Integration tests have their own setup and teardown. -class IntegrationTestUsesCorrectClass < ActionController::IntegrationTest +class IntegrationTestUsesCorrectClass < ActionDispatch::IntegrationTest def self.fixture_table_names [] end @@ -218,7 +218,7 @@ class IntegrationTestUsesCorrectClass < ActionController::IntegrationTest end end -class IntegrationProcessTest < ActionController::IntegrationTest +class IntegrationProcessTest < ActionDispatch::IntegrationTest class IntegrationController < ActionController::Base def get respond_to do |format| @@ -427,7 +427,7 @@ class IntegrationProcessTest < ActionController::IntegrationTest include set.url_helpers end - set.draw do |map| + set.draw do match ':action', :to => controller get 'get/:action', :to => controller end @@ -439,7 +439,7 @@ class IntegrationProcessTest < ActionController::IntegrationTest end end -class MetalIntegrationTest < ActionController::IntegrationTest +class MetalIntegrationTest < ActionDispatch::IntegrationTest include SharedTestRoutes.url_helpers class Poller @@ -476,7 +476,7 @@ class MetalIntegrationTest < ActionController::IntegrationTest end end -class ApplicationIntegrationTest < ActionController::IntegrationTest +class ApplicationIntegrationTest < ActionDispatch::IntegrationTest class TestController < ActionController::Base def index render :text => "index" diff --git a/actionpack/test/controller/layout_test.rb b/actionpack/test/controller/layout_test.rb index 165c61ffad..cafe2b9320 100644 --- a/actionpack/test/controller/layout_test.rb +++ b/actionpack/test/controller/layout_test.rb @@ -46,13 +46,13 @@ class LayoutAutoDiscoveryTest < ActionController::TestCase def test_application_layout_is_default_when_no_controller_match @controller = ProductController.new get :hello - assert_equal 'layout_test.rhtml hello.rhtml', @response.body + assert_equal 'layout_test.erb hello.erb', @response.body end def test_controller_name_layout_name_match @controller = ItemController.new get :hello - assert_equal 'item.rhtml hello.rhtml', @response.body + assert_equal 'item.erb hello.erb', @response.body end def test_third_party_template_library_auto_discovers_layout @@ -65,13 +65,13 @@ class LayoutAutoDiscoveryTest < ActionController::TestCase def test_namespaced_controllers_auto_detect_layouts1 @controller = ControllerNameSpace::NestedController.new get :hello - assert_equal 'controller_name_space/nested.rhtml hello.rhtml', @response.body + assert_equal 'controller_name_space/nested.erb hello.erb', @response.body end def test_namespaced_controllers_auto_detect_layouts2 @controller = MultipleExtensions.new get :hello - assert_equal 'multiple_extensions.html.erb hello.rhtml', @response.body.strip + assert_equal 'multiple_extensions.html.erb hello.erb', @response.body.strip end end @@ -79,7 +79,7 @@ class DefaultLayoutController < LayoutTest end class AbsolutePathLayoutController < LayoutTest - layout File.expand_path(File.expand_path(__FILE__) + '/../../fixtures/layout_tests/layouts/layout_test.rhtml') + layout File.expand_path(File.expand_path(__FILE__) + '/../../fixtures/layout_tests/layouts/layout_test.erb') end class HasOwnLayoutController < LayoutTest @@ -115,7 +115,7 @@ end class LayoutSetInResponseTest < ActionController::TestCase include ActionView::Template::Handlers - + def test_layout_set_when_using_default_layout @controller = DefaultLayoutController.new get :hello @@ -127,7 +127,7 @@ class LayoutSetInResponseTest < ActionController::TestCase get :hello assert_template :layout => "layouts/item" end - + def test_layout_only_exception_when_included @controller = OnlyLayoutController.new get :hello @@ -137,7 +137,7 @@ class LayoutSetInResponseTest < ActionController::TestCase def test_layout_only_exception_when_excepted @controller = OnlyLayoutController.new get :goodbye - assert !@response.body.include?("item.rhtml"), "#{@response.body.inspect} included 'item.rhtml'" + assert !@response.body.include?("item.erb"), "#{@response.body.inspect} included 'item.erb'" end def test_layout_except_exception_when_included @@ -149,7 +149,7 @@ class LayoutSetInResponseTest < ActionController::TestCase def test_layout_except_exception_when_excepted @controller = ExceptLayoutController.new get :goodbye - assert !@response.body.include?("item.rhtml"), "#{@response.body.inspect} included 'item.rhtml'" + assert !@response.body.include?("item.erb"), "#{@response.body.inspect} included 'item.erb'" end def test_layout_set_when_using_render @@ -173,7 +173,7 @@ class LayoutSetInResponseTest < ActionController::TestCase def test_absolute_pathed_layout @controller = AbsolutePathLayoutController.new get :hello - assert_equal "layout_test.rhtml hello.rhtml", @response.body.strip + assert_equal "layout_test.erb hello.erb", @response.body.strip end end @@ -184,7 +184,7 @@ class RenderWithTemplateOptionController < LayoutTest end class SetsNonExistentLayoutFile < LayoutTest - layout "nofile.rhtml" + layout "nofile.erb" end class LayoutExceptionRaised < ActionController::TestCase @@ -208,7 +208,7 @@ class LayoutStatusIsRenderedTest < ActionController::TestCase end end -unless Config::CONFIG['host_os'] =~ /mswin|mingw/ +unless RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ class LayoutSymlinkedTest < LayoutTest layout "symlinked/symlinked_layout" end diff --git a/actionpack/test/controller/log_subscriber_test.rb b/actionpack/test/controller/log_subscriber_test.rb index 0a18741f0c..ddfa3df552 100644 --- a/actionpack/test/controller/log_subscriber_test.rb +++ b/actionpack/test/controller/log_subscriber_test.rb @@ -16,10 +16,6 @@ module Another send_data "cool data", :filename => "file.txt" end - def xfile_sender - send_file File.expand_path("company.rb", FIXTURE_LOAD_PATH), :x_sendfile => true - end - def file_sender send_file File.expand_path("company.rb", FIXTURE_LOAD_PATH) end @@ -28,10 +24,19 @@ module Another render :inline => "<%= cache('foo'){ 'bar' } %>" end + def with_fragment_cache_and_percent_in_key + render :inline => "<%= cache('foo%bar'){ 'Contains % sign in key' } %>" + end + def with_page_cache cache_page("Super soaker", "/index.html") render :nothing => true end + + def with_exception + raise Exception + end + end end @@ -72,8 +77,8 @@ class ACLogSubscriberTest < ActionController::TestCase get :show wait assert_equal 2, logs.size - assert_match /Completed/, logs.last - assert_match /200 OK/, logs.last + assert_match(/Completed/, logs.last) + assert_match(/200 OK/, logs.last) end def test_process_action_without_parameters @@ -93,7 +98,7 @@ class ACLogSubscriberTest < ActionController::TestCase def test_process_action_with_view_runtime get :show wait - assert_match /\(Views: [\d\.]+ms\)/, logs[1] + assert_match(/\(Views: [\d.]+ms\)/, logs[1]) end def test_process_action_with_filter_parameters @@ -103,9 +108,9 @@ class ACLogSubscriberTest < ActionController::TestCase wait params = logs[1] - assert_match /"amount"=>"\[FILTERED\]"/, params - assert_match /"lifo"=>"\[FILTERED\]"/, params - assert_match /"step"=>"1"/, params + assert_match(/"amount"=>"\[FILTERED\]"/, params) + assert_match(/"lifo"=>"\[FILTERED\]"/, params) + assert_match(/"step"=>"1"/, params) end def test_redirect_to @@ -121,7 +126,7 @@ class ACLogSubscriberTest < ActionController::TestCase wait assert_equal 3, logs.size - assert_match /Sent data file\.txt/, logs[1] + assert_match(/Sent data file\.txt/, logs[1]) end def test_send_file @@ -129,27 +134,30 @@ class ACLogSubscriberTest < ActionController::TestCase wait assert_equal 3, logs.size - assert_match /Sent file/, logs[1] - assert_match /test\/fixtures\/company\.rb/, logs[1] + assert_match(/Sent file/, logs[1]) + assert_match(/test\/fixtures\/company\.rb/, logs[1]) end - def test_send_xfile - assert_deprecated { get :xfile_sender } + def test_with_fragment_cache + @controller.config.perform_caching = true + get :with_fragment_cache wait - assert_equal 3, logs.size - assert_match /Sent file/, logs[1] - assert_match /test\/fixtures\/company\.rb/, logs[1] + assert_equal 4, logs.size + assert_match(/Read fragment views\/foo/, logs[1]) + assert_match(/Write fragment views\/foo/, logs[2]) + ensure + @controller.config.perform_caching = true end - def test_with_fragment_cache + def test_with_fragment_cache_and_percent_in_key @controller.config.perform_caching = true - get :with_fragment_cache + get :with_fragment_cache_and_percent_in_key wait assert_equal 4, logs.size - assert_match /Exist fragment\? views\/foo/, logs[1] - assert_match /Write fragment views\/foo/, logs[2] + assert_match(/Read fragment views\/foo/, logs[1]) + assert_match(/Write fragment views\/foo/, logs[2]) ensure @controller.config.perform_caching = true end @@ -160,12 +168,22 @@ class ACLogSubscriberTest < ActionController::TestCase wait assert_equal 3, logs.size - assert_match /Write page/, logs[1] - assert_match /\/index\.html/, logs[1] + assert_match(/Write page/, logs[1]) + assert_match(/\/index\.html/, logs[1]) ensure @controller.config.perform_caching = true end + def test_process_action_with_exception_includes_http_status_code + begin + get :with_exception + wait + rescue Exception + end + assert_equal 2, logs.size + assert_match(/Completed 500/, logs.last) + end + def logs @logs ||= @logger.logged(:info) end diff --git a/actionpack/test/controller/mime_responds_test.rb b/actionpack/test/controller/mime_responds_test.rb index b5ce391b61..5debf96232 100644 --- a/actionpack/test/controller/mime_responds_test.rb +++ b/actionpack/test/controller/mime_responds_test.rb @@ -2,6 +2,14 @@ require 'abstract_unit' require 'controller/fake_models' require 'active_support/core_ext/hash/conversions' +class StarStarMimeController < ActionController::Base + layout nil + + def index + render + end +end + class RespondToController < ActionController::Base layout :set_layout @@ -36,7 +44,7 @@ class RespondToController < ActionController::Base type.all { render :text => "Nothing" } end end - + def json_xml_or_html respond_to do |type| type.json { render :text => 'JSON' } @@ -44,7 +52,7 @@ class RespondToController < ActionController::Base type.html { render :text => 'HTML' } end end - + def forced_xml request.format = :xml @@ -89,7 +97,6 @@ class RespondToController < ActionController::Base end end - Mime::Type.register("text/x-mobile", :mobile) def custom_constant_handling respond_to do |type| @@ -126,7 +133,6 @@ class RespondToController < ActionController::Base end end - Mime::Type.register_alias("text/html", :iphone) def iphone_with_html_response_type request.format = :iphone if request.env["HTTP_ACCEPT"] == "text/iphone" @@ -160,16 +166,43 @@ class RespondToController < ActionController::Base end end +class StarStarMimeControllerTest < ActionController::TestCase + tests StarStarMimeController + + def test_javascript_with_format + @request.accept = "text/javascript" + get :index, :format => 'js' + assert_match "function addition(a,b){ return a+b; }", @response.body + end + + def test_javascript_with_no_format + @request.accept = "text/javascript" + get :index + assert_match "function addition(a,b){ return a+b; }", @response.body + end + + def test_javascript_with_no_format_only_star_star + @request.accept = "*/*" + get :index + assert_match "function addition(a,b){ return a+b; }", @response.body + end + +end + class RespondToControllerTest < ActionController::TestCase tests RespondToController def setup super @request.host = "www.example.com" + Mime::Type.register_alias("text/html", :iphone) + Mime::Type.register("text/x-mobile", :mobile) end def teardown super + Mime::Type.unregister(:iphone) + Mime::Type.unregister(:mobile) end def test_html @@ -216,6 +249,16 @@ class RespondToControllerTest < ActionController::TestCase assert_response 406 end + def test_json_or_yaml_with_leading_star_star + @request.accept = "*/*, application/json" + get :json_xml_or_html + assert_equal 'HTML', @response.body + + @request.accept = "*/* , application/json" + get :json_xml_or_html + assert_equal 'HTML', @response.body + end + def test_json_or_yaml xhr :get, :json_or_yaml assert_equal 'JSON', @response.body @@ -373,17 +416,17 @@ class RespondToControllerTest < ActionController::TestCase get :handle_any_any assert_equal 'Whatever you ask for, I got it', @response.body end - + def test_browser_check_with_any_any @request.accept = "application/json, application/xml" get :json_xml_or_html assert_equal 'JSON', @response.body - + @request.accept = "application/json, application/xml, */*" get :json_xml_or_html assert_equal 'HTML', @response.body end - + def test_rjs_type_skips_layout @request.accept = "text/javascript" @@ -487,6 +530,10 @@ class RespondWithController < ActionController::Base respond_with(resource) end + def using_hash_resource + respond_with({:result => resource}) + end + def using_resource_with_block respond_with(resource) do |format| format.csv { render :text => "CSV" } @@ -518,7 +565,7 @@ class RespondWithController < ActionController::Base def using_resource_with_action respond_with(resource, :action => :foo) do |format| - format.html { raise ActionView::MissingTemplate.new([], "foo/bar", {}, false) } + format.html { raise ActionView::MissingTemplate.new([], "bar", ["foo"], {}, false) } end end @@ -552,6 +599,17 @@ class InheritedRespondWithController < RespondWithController end end +class RenderJsonRespondWithController < RespondWithController + clear_respond_to + respond_to :json + + def index + respond_with(resource) do |format| + format.json { render :json => RenderJsonTestException.new('boom') } + end + end +end + class EmptyRespondWithController < ActionController::Base def index respond_with(Customer.new("david", 13)) @@ -564,10 +622,14 @@ class RespondWithControllerTest < ActionController::TestCase def setup super @request.host = "www.example.com" + Mime::Type.register_alias('text/html', :iphone) + Mime::Type.register('text/x-mobile', :mobile) end def teardown super + Mime::Type.unregister(:iphone) + Mime::Type.unregister(:mobile) end def test_using_resource @@ -587,6 +649,18 @@ class RespondWithControllerTest < ActionController::TestCase end end + def test_using_hash_resource + @request.accept = "application/xml" + get :using_hash_resource + assert_equal "application/xml", @response.content_type + assert_equal "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<hash>\n <name>david</name>\n</hash>\n", @response.body + + @request.accept = "application/json" + get :using_hash_resource + assert_equal "application/json", @response.content_type + assert_equal %Q[{"result":{"name":"david","id":13}}], @response.body + end + def test_using_resource_with_block @request.accept = "*/*" get :using_resource_with_block @@ -709,6 +783,15 @@ class RespondWithControllerTest < ActionController::TestCase assert_equal " ", @response.body end + def test_using_resource_for_put_with_json_yields_ok_on_success + Customer.any_instance.stubs(:to_json).returns('{"name": "David"}') + @request.accept = "application/json" + put :using_resource + assert_equal "application/json", @response.content_type + assert_equal 200, @response.status + assert_equal "{}", @response.body + end + def test_using_resource_for_put_with_xml_yields_unprocessable_entity_on_failure @request.accept = "application/xml" errors = { :name => :invalid } @@ -739,6 +822,16 @@ class RespondWithControllerTest < ActionController::TestCase assert_equal " ", @response.body end + def test_using_resource_for_delete_with_json_yields_ok_on_success + Customer.any_instance.stubs(:to_json).returns('{"name": "David"}') + Customer.any_instance.stubs(:destroyed?).returns(true) + @request.accept = "application/json" + delete :using_resource + assert_equal "application/json", @response.content_type + assert_equal 200, @response.status + assert_equal "{}", @response.body + end + def test_using_resource_for_delete_with_html_redirects_on_failure with_test_route_set do errors = { :name => :invalid } @@ -784,8 +877,8 @@ class RespondWithControllerTest < ActionController::TestCase get :using_resource_with_collection assert_equal "application/xml", @response.content_type assert_equal 200, @response.status - assert_match /<name>david<\/name>/, @response.body - assert_match /<name>jamis<\/name>/, @response.body + assert_match(/<name>david<\/name>/, @response.body) + assert_match(/<name>jamis<\/name>/, @response.body) end def test_using_resource_with_action @@ -834,6 +927,14 @@ class RespondWithControllerTest < ActionController::TestCase assert_equal "JSON", @response.body end + def test_render_json_object_responds_to_str_still_produce_json + @controller = RenderJsonRespondWithController.new + @request.accept = "application/json" + get :index, :format => :json + assert_match(/"message":"boom"/, @response.body) + assert_match(/"error":"RenderJsonTestException"/, @response.body) + end + def test_no_double_render_is_raised @request.accept = "text/html" assert_raise ActionView::MissingTemplate do @@ -876,7 +977,7 @@ class RespondWithControllerTest < ActionController::TestCase private def with_test_route_set with_routing do |set| - set.draw do |map| + set.draw do resources :customers resources :quiz_stores do resources :customers @@ -917,6 +1018,12 @@ class MimeControllerLayoutsTest < ActionController::TestCase def setup super @request.host = "www.example.com" + Mime::Type.register_alias("text/html", :iphone) + end + + def teardown + super + Mime::Type.unregister(:iphone) end def test_missing_layout_renders_properly diff --git a/actionpack/test/controller/new_base/bare_metal_test.rb b/actionpack/test/controller/new_base/bare_metal_test.rb index 8a06e8d2a0..3ca29f1bcf 100644 --- a/actionpack/test/controller/new_base/bare_metal_test.rb +++ b/actionpack/test/controller/new_base/bare_metal_test.rb @@ -24,4 +24,26 @@ module BareMetalTest assert_equal "Hello world", string end end + + class HeadController < ActionController::Metal + include ActionController::Head + + def index + head :not_found + end + end + + class HeadTest < ActiveSupport::TestCase + test "head works on its own" do + status = HeadController.action(:index).call(Rack::MockRequest.env_for("/")).first + assert_equal 404, status + end + end + + class BareControllerTest < ActionController::TestCase + test "GET index" do + get :index + assert_equal "Hello world", @response.body + end + end end diff --git a/actionpack/test/controller/new_base/content_negotiation_test.rb b/actionpack/test/controller/new_base/content_negotiation_test.rb index b98a22dfcc..5fd5946619 100644 --- a/actionpack/test/controller/new_base/content_negotiation_test.rb +++ b/actionpack/test/controller/new_base/content_negotiation_test.rb @@ -7,6 +7,10 @@ module ContentNegotiation self.view_paths = [ActionView::FixtureResolver.new( "content_negotiation/basic/hello.html.erb" => "Hello world <%= request.formats.first.to_s %>!" )] + + def all + render :text => self.formats.inspect + end end class TestContentNegotiation < Rack::TestCase @@ -14,5 +18,10 @@ module ContentNegotiation get "/content_negotiation/basic/hello", {}, "HTTP_ACCEPT" => "*/*" assert_body "Hello world */*!" end + + test "Not all mimes are converted to symbol" do + get "/content_negotiation/basic/all", {}, "HTTP_ACCEPT" => "text/plain, mime/another" + assert_body '[:text, "mime/another"]' + end end end diff --git a/actionpack/test/controller/new_base/content_type_test.rb b/actionpack/test/controller/new_base/content_type_test.rb index 33c2e442f0..8ba30944f5 100644 --- a/actionpack/test/controller/new_base/content_type_test.rb +++ b/actionpack/test/controller/new_base/content_type_test.rb @@ -42,10 +42,16 @@ module ContentType class ExplicitContentTypeTest < Rack::TestCase test "default response is HTML and UTF8" do - get "/content_type/base" + with_routing do |set| + set.draw do + match ':controller', :action => 'index' + end - assert_body "Hello world!" - assert_header "Content-Type", "text/html; charset=utf-8" + get "/content_type/base" + + assert_body "Hello world!" + assert_header "Content-Type", "text/html; charset=utf-8" + end end test "setting the content type of the response directly on the response object" do diff --git a/actionpack/test/controller/new_base/etag_test.rb b/actionpack/test/controller/new_base/etag_test.rb deleted file mode 100644 index 51bfb2278a..0000000000 --- a/actionpack/test/controller/new_base/etag_test.rb +++ /dev/null @@ -1,46 +0,0 @@ -require 'abstract_unit' - -module Etags - class BasicController < ActionController::Base - self.view_paths = [ActionView::FixtureResolver.new( - "etags/basic/base.html.erb" => "Hello from without_layout.html.erb", - "layouts/etags.html.erb" => "teh <%= yield %> tagz" - )] - - def without_layout - render :action => "base" - end - - def with_layout - render :action => "base", :layout => "etags" - end - end - - class EtagTest < Rack::TestCase - describe "Rendering without any special etag options returns an etag that is an MD5 hash of its text" - - test "an action without a layout" do - get "/etags/basic/without_layout" - - body = "Hello from without_layout.html.erb" - assert_body body - assert_header "Etag", etag_for(body) - assert_status 200 - end - - test "an action with a layout" do - get "/etags/basic/with_layout" - - body = "teh Hello from without_layout.html.erb tagz" - assert_body body - assert_header "Etag", etag_for(body) - assert_status 200 - end - - private - - def etag_for(text) - %("#{Digest::MD5.hexdigest(text)}") - end - end -end diff --git a/actionpack/test/controller/new_base/middleware_test.rb b/actionpack/test/controller/new_base/middleware_test.rb index 26a66c91a6..ccef060863 100644 --- a/actionpack/test/controller/new_base/middleware_test.rb +++ b/actionpack/test/controller/new_base/middleware_test.rb @@ -25,8 +25,25 @@ module MiddlewareTest result end end + + class BlockMiddleware + attr_accessor :configurable_message + def initialize(app, &block) + @app = app + yield(self) if block_given? + end + + def call(env) + result = @app.call(env) + result[1]["Configurable-Message"] = configurable_message + result + end + end class MyController < ActionController::Metal + use BlockMiddleware do |config| + config.configurable_message = "Configured by block." + end use MyMiddleware middleware.insert_before MyMiddleware, ExclaimerMiddleware @@ -67,6 +84,11 @@ module MiddlewareTest assert_equal "First!", result[1]["Middleware-Order"] end + test "middleware stack accepts block arguments" do + result = @app.call(env_for("/")) + assert_equal "Configured by block.", result[1]["Configurable-Message"] + end + test "middleware stack accepts only and except as options" do result = ActionsController.action(:show).call(env_for("/")) assert_equal "First!", result[1]["Middleware-Order"] diff --git a/actionpack/test/controller/new_base/render_action_test.rb b/actionpack/test/controller/new_base/render_action_test.rb index d92e9c4bf2..aa44e0b282 100644 --- a/actionpack/test/controller/new_base/render_action_test.rb +++ b/actionpack/test/controller/new_base/render_action_test.rb @@ -83,6 +83,9 @@ module RenderAction end class RenderLayoutTest < Rack::TestCase + def setup + end + describe "Both <controller_path>.html.erb and application.html.erb are missing" test "rendering with layout => true" do diff --git a/actionpack/test/controller/new_base/render_implicit_action_test.rb b/actionpack/test/controller/new_base/render_implicit_action_test.rb index 90cc7933ff..9f69d20329 100644 --- a/actionpack/test/controller/new_base/render_implicit_action_test.rb +++ b/actionpack/test/controller/new_base/render_implicit_action_test.rb @@ -6,10 +6,10 @@ module RenderImplicitAction "render_implicit_action/simple/hello_world.html.erb" => "Hello world!", "render_implicit_action/simple/hyphen-ated.html.erb" => "Hello hyphen-ated!" )] - + def hello_world() end end - + class RenderImplicitActionTest < Rack::TestCase test "render a simple action with new explicit call to render" do get "/render_implicit_action/simple/hello_world" diff --git a/actionpack/test/controller/new_base/render_layout_test.rb b/actionpack/test/controller/new_base/render_layout_test.rb index 372fb66b88..bb2a953536 100644 --- a/actionpack/test/controller/new_base/render_layout_test.rb +++ b/actionpack/test/controller/new_base/render_layout_test.rb @@ -71,7 +71,7 @@ module ControllerLayouts self.view_paths = [ActionView::FixtureResolver.new( "layouts/application.html.erb" => "<html><%= yield %></html>", "controller_layouts/mismatch_format/index.js.rjs" => "page[:test].ext", - "controller_layouts/mismatch_format/implicit.rjs" => "page[:test].ext" + "controller_layouts/mismatch_format/implicit.rjs" => "page[:test].ext" )] def explicit diff --git a/actionpack/test/controller/new_base/render_once_test.rb b/actionpack/test/controller/new_base/render_once_test.rb new file mode 100644 index 0000000000..175abf8a7e --- /dev/null +++ b/actionpack/test/controller/new_base/render_once_test.rb @@ -0,0 +1,86 @@ +require 'abstract_unit' + +module RenderTemplate + class RenderOnceController < ActionController::Base + layout false + + RESOLVER = ActionView::FixtureResolver.new( + "test/a.html.erb" => "a", + "test/b.html.erb" => "<>", + "test/c.html.erb" => "c", + "test/one.html.erb" => "<%= render :once => 'result' %>", + "test/two.html.erb" => "<%= render :once => 'result' %>", + "test/three.html.erb" => "<%= render :once => 'result' %>", + "test/result.html.erb" => "YES!", + "other/result.html.erb" => "NO!", + "layouts/test.html.erb" => "l<%= yield %>l" + ) + + self.view_paths = [RESOLVER] + + def _prefixes + %w(test) + end + + def multiple + render :once => %w(a b c) + end + + def once + render :once => %w(one two three) + end + + def duplicate + render :once => %w(a a a) + end + + def with_layout + render :once => %w(a b c), :layout => "test" + end + + def with_prefix + render :once => "result", :prefixes => %w(other) + end + + def with_nil_prefix + render :once => "test/result", :prefixes => [] + end + end + + module Tests + def test_mutliple_arguments_get_all_rendered + get :multiple + assert_response "a\n<>\nc" + end + + def test_referenced_templates_get_rendered_once + get :once + assert_response "YES!\n\n" + end + + def test_duplicated_templates_get_rendered_once + get :duplicate + assert_response "a" + end + + def test_layout_wraps_all_rendered_templates + get :with_layout + assert_response "la\n<>\ncl" + end + + def test_with_prefix_option + get :with_prefix + assert_response "NO!" + end + + def test_with_nil_prefix_option + get :with_nil_prefix + assert_response "YES!" + end + end + + class TestRenderOnce < Rack::TestCase + testing RenderTemplate::RenderOnceController + include Tests + end +end diff --git a/actionpack/test/controller/new_base/render_partial_test.rb b/actionpack/test/controller/new_base/render_partial_test.rb index 1a1b36a65e..83b0d039ad 100644 --- a/actionpack/test/controller/new_base/render_partial_test.rb +++ b/actionpack/test/controller/new_base/render_partial_test.rb @@ -1,27 +1,63 @@ require 'abstract_unit' module RenderPartial - + class BasicController < ActionController::Base - + self.view_paths = [ActionView::FixtureResolver.new( - "render_partial/basic/_basic.html.erb" => "BasicPartial!", - "render_partial/basic/basic.html.erb" => "<%= @test_unchanged = 'goodbye' %><%= render :partial => 'basic' %><%= @test_unchanged %>" + "render_partial/basic/_basic.html.erb" => "BasicPartial!", + "render_partial/basic/basic.html.erb" => "<%= @test_unchanged = 'goodbye' %><%= render :partial => 'basic' %><%= @test_unchanged %>", + "render_partial/basic/with_json.html.erb" => "<%= render 'with_json.json' %>", + "render_partial/basic/_with_json.json.erb" => "<%= render 'final' %>", + "render_partial/basic/_final.json.erb" => "{ final: json }", + "render_partial/basic/overriden.html.erb" => "<%= @test_unchanged = 'goodbye' %><%= render :partial => 'overriden' %><%= @test_unchanged %>", + "render_partial/basic/_overriden.html.erb" => "ParentPartial!", + "render_partial/child/_overriden.html.erb" => "OverridenPartial!" )] - + + def html_with_json_inside_json + render :action => "with_json" + end + def changing @test_unchanged = 'hello' render :action => "basic" - end + end + + def overriden + @test_unchanged = 'hello' + end end + class ChildController < BasicController; end + class TestPartial < Rack::TestCase testing BasicController - + test "rendering a partial in ActionView doesn't pull the ivars again from the controller" do get :changing assert_response("goodbyeBasicPartial!goodbye") end + + test "rendering a template with renders another partial with other format that renders other partial in the same format" do + get :html_with_json_inside_json + assert_content_type "text/html; charset=utf-8" + assert_response "{ final: json }" + end end - + + class TestInheritedPartial < Rack::TestCase + testing ChildController + + test "partial from parent controller gets picked if missing in child one" do + get :changing + assert_response("goodbyeBasicPartial!goodbye") + end + + test "partial from child controller gets picked" do + get :overriden + assert_response("goodbyeOverridenPartial!goodbye") + end + end + end diff --git a/actionpack/test/controller/new_base/render_template_test.rb b/actionpack/test/controller/new_base/render_template_test.rb index 70cebbfd89..584f2d772c 100644 --- a/actionpack/test/controller/new_base/render_template_test.rb +++ b/actionpack/test/controller/new_base/render_template_test.rb @@ -4,17 +4,26 @@ module RenderTemplate class WithoutLayoutController < ActionController::Base self.view_paths = [ActionView::FixtureResolver.new( - "test/basic.html.erb" => "Hello from basic.html.erb", - "shared.html.erb" => "Elastica", - "locals.html.erb" => "The secret is <%= secret %>", - "xml_template.xml.builder" => "xml.html do\n xml.p 'Hello'\nend", - "with_raw.html.erb" => "Hello <%=raw '<strong>this is raw</strong>' %>" + "test/basic.html.erb" => "Hello from basic.html.erb", + "shared.html.erb" => "Elastica", + "locals.html.erb" => "The secret is <%= secret %>", + "xml_template.xml.builder" => "xml.html do\n xml.p 'Hello'\nend", + "with_raw.html.erb" => "Hello <%=raw '<strong>this is raw</strong>' %>", + "with_implicit_raw.html.erb" => "Hello <%== '<strong>this is also raw</strong>' %>", + "test/with_json.html.erb" => "<%= render :template => 'test/with_json.json' %>", + "test/with_json.json.erb" => "<%= render :template => 'test/final' %>", + "test/final.json.erb" => "{ final: json }", + "test/with_error.html.erb" => "<%= idontexist %>" )] def index render :template => "test/basic" end + def html_with_json_inside_json + render :template => "test/with_json" + end + def index_without_key render "test/basic" end @@ -42,6 +51,14 @@ module RenderTemplate def with_raw render :template => "with_raw" end + + def with_implicit_raw + render :template => "with_implicit_raw" + end + + def with_error + render :template => "test/with_error" + end end class TestWithoutLayout < Rack::TestCase @@ -87,6 +104,23 @@ module RenderTemplate assert_body "Hello <strong>this is raw</strong>" assert_status 200 + + get :with_implicit_raw + + assert_body "Hello <strong>this is also raw</strong>" + assert_status 200 + end + + test "rendering a template with renders another template with other format that renders other template in the same format" do + get :html_with_json_inside_json + assert_content_type "text/html; charset=utf-8" + assert_response "{ final: json }" + end + + test "rendering a template with error properly exceprts the code" do + get :with_error + assert_status 500 + assert_match "undefined local variable or method `idontexist'", response.body end end @@ -123,10 +157,14 @@ module RenderTemplate describe "Rendering with :template using implicit or explicit layout" test "rendering with implicit layout" do - get "/render_template/with_layout" + with_routing do |set| + set.draw { match ':controller', :action => :index } - assert_body "Hello from basic.html.erb, I'm here!" - assert_status 200 + get "/render_template/with_layout" + + assert_body "Hello from basic.html.erb, I'm here!" + assert_status 200 + end end test "rendering with layout => :true" do diff --git a/actionpack/test/controller/new_base/render_test.rb b/actionpack/test/controller/new_base/render_test.rb index d985d9f9ad..d6062bfa8c 100644 --- a/actionpack/test/controller/new_base/render_test.rb +++ b/actionpack/test/controller/new_base/render_test.rb @@ -6,7 +6,11 @@ module Render "render/blank_render/index.html.erb" => "Hello world!", "render/blank_render/access_request.html.erb" => "The request: <%= request.method.to_s.upcase %>", "render/blank_render/access_action_name.html.erb" => "Action Name: <%= action_name %>", - "render/blank_render/access_controller_name.html.erb" => "Controller Name: <%= controller_name %>" + "render/blank_render/access_controller_name.html.erb" => "Controller Name: <%= controller_name %>", + "render/blank_render/overriden_with_own_view_paths_appended.html.erb" => "parent content", + "render/blank_render/overriden_with_own_view_paths_prepended.html.erb" => "parent content", + "render/blank_render/overriden.html.erb" => "parent content", + "render/child_render/overriden.html.erb" => "child content" )] def index @@ -21,6 +25,15 @@ module Render render :action => "access_action_name" end + def overriden_with_own_view_paths_appended + end + + def overriden_with_own_view_paths_prepended + end + + def overriden + end + private def secretz @@ -35,17 +48,34 @@ module Render end end + class ChildRenderController < BlankRenderController + append_view_path ActionView::FixtureResolver.new("render/child_render/overriden_with_own_view_paths_appended.html.erb" => "child content") + prepend_view_path ActionView::FixtureResolver.new("render/child_render/overriden_with_own_view_paths_prepended.html.erb" => "child content") + end + class RenderTest < Rack::TestCase test "render with blank" do - get "/render/blank_render" + with_routing do |set| + set.draw do + match ":controller", :action => 'index' + end - assert_body "Hello world!" - assert_status 200 + get "/render/blank_render" + + assert_body "Hello world!" + assert_status 200 + end end test "rendering more than once raises an exception" do - assert_raises(AbstractController::DoubleRenderError) do - get "/render/double_render", {}, "action_dispatch.show_exceptions" => false + with_routing do |set| + set.draw do + match ":controller", :action => 'index' + end + + assert_raises(AbstractController::DoubleRenderError) do + get "/render/double_render", {}, "action_dispatch.show_exceptions" => false + end end end end @@ -65,7 +95,7 @@ module Render end end end - + class TestVariousObjectsAvailableInView < Rack::TestCase test "The request object is accessible in the view" do get "/render/blank_render/access_request" @@ -82,4 +112,26 @@ module Render assert_body "Controller Name: blank_render" end end + + class TestViewInheritance < Rack::TestCase + test "Template from child controller gets picked over parent one" do + get "/render/child_render/overriden" + assert_body "child content" + end + + test "Template from child controller with custom view_paths prepended gets picked over parent one" do + get "/render/child_render/overriden_with_own_view_paths_prepended" + assert_body "child content" + end + + test "Template from child controller with custom view_paths appended gets picked over parent one" do + get "/render/child_render/overriden_with_own_view_paths_appended" + assert_body "child content" + end + + test "Template from parent controller gets picked if missing in child controller" do + get "/render/child_render/index" + assert_body "Hello world!" + end + end end diff --git a/actionpack/test/controller/new_base/render_text_test.rb b/actionpack/test/controller/new_base/render_text_test.rb index 0e6f51c998..06d500cca7 100644 --- a/actionpack/test/controller/new_base/render_text_test.rb +++ b/actionpack/test/controller/new_base/render_text_test.rb @@ -3,7 +3,7 @@ require 'abstract_unit' module RenderText class SimpleController < ActionController::Base self.view_paths = [ActionView::FixtureResolver.new] - + def index render :text => "hello david" end @@ -14,24 +14,24 @@ module RenderText "layouts/application.html.erb" => "<%= yield %>, I'm here!", "layouts/greetings.html.erb" => "<%= yield %>, I wish thee well.", "layouts/ivar.html.erb" => "<%= yield %>, <%= @ivar %>" - )] - + )] + def index render :text => "hello david" end - + def custom_code render :text => "hello world", :status => 404 end - + def with_custom_code_as_string render :text => "hello world", :status => "404 Not Found" end - + def with_nil render :text => nil end - + def with_nil_and_status render :text => nil, :status => 403 end @@ -39,23 +39,23 @@ module RenderText def with_false render :text => false end - + def with_layout_true render :text => "hello world", :layout => true end - + def with_layout_false render :text => "hello world", :layout => false end - + def with_layout_nil render :text => "hello world", :layout => nil end - + def with_custom_layout render :text => "hello world", :layout => "greetings" end - + def with_ivar_in_layout @ivar = "hello world" render :text => "hello world", :layout => "ivar" @@ -66,16 +66,24 @@ module RenderText describe "Rendering text using render :text" test "rendering text from a action with default options renders the text with the layout" do - get "/render_text/simple" - assert_body "hello david" - assert_status 200 + with_routing do |set| + set.draw { match ':controller', :action => 'index' } + + get "/render_text/simple" + assert_body "hello david" + assert_status 200 + end end test "rendering text from a action with default options renders the text without the layout" do - get "/render_text/with_layout" + with_routing do |set| + set.draw { match ':controller', :action => 'index' } - assert_body "hello david" - assert_status 200 + get "/render_text/with_layout" + + assert_body "hello david" + assert_status 200 + end end test "rendering text, while also providing a custom status code" do diff --git a/actionpack/test/controller/new_base/render_xml_test.rb b/actionpack/test/controller/new_base/render_xml_test.rb index d044738a78..b8527a943d 100644 --- a/actionpack/test/controller/new_base/render_xml_test.rb +++ b/actionpack/test/controller/new_base/render_xml_test.rb @@ -1,7 +1,7 @@ require 'abstract_unit' module RenderXml - + # This has no layout and it works class BasicController < ActionController::Base self.view_paths = [ActionView::FixtureResolver.new( diff --git a/actionpack/test/controller/output_escaping_test.rb b/actionpack/test/controller/output_escaping_test.rb index 43a8c05cda..f6913a2138 100644 --- a/actionpack/test/controller/output_escaping_test.rb +++ b/actionpack/test/controller/output_escaping_test.rb @@ -3,7 +3,7 @@ require 'abstract_unit' class OutputEscapingTest < ActiveSupport::TestCase test "escape_html shouldn't die when passed nil" do - assert ERB::Util.h(nil).blank? + assert_blank ERB::Util.h(nil) end test "escapeHTML should escape strings" do diff --git a/actionpack/test/controller/record_identifier_test.rb b/actionpack/test/controller/record_identifier_test.rb index 835a0e970b..f3e5ff8a47 100644 --- a/actionpack/test/controller/record_identifier_test.rb +++ b/actionpack/test/controller/record_identifier_test.rb @@ -1,30 +1,5 @@ require 'abstract_unit' - -class Comment - extend ActiveModel::Naming - include ActiveModel::Conversion - - attr_reader :id - def to_key; id ? [id] : nil end - def save; @id = 1 end - def new_record?; @id.nil? end - def name - @id.nil? ? 'new comment' : "comment ##{@id}" - end -end - -class Sheep - extend ActiveModel::Naming - include ActiveModel::Conversion - - attr_reader :id - def to_key; id ? [id] : nil end - def save; @id = 1 end - def new_record?; @id.nil? end - def name - @id.nil? ? 'new sheep' : "sheep ##{@id}" - end -end +require 'controller/fake_models' class RecordIdentifierTest < Test::Unit::TestCase include ActionController::RecordIdentifier diff --git a/actionpack/test/controller/redirect_test.rb b/actionpack/test/controller/redirect_test.rb index 441bc47908..92d4a6d98b 100644 --- a/actionpack/test/controller/redirect_test.rb +++ b/actionpack/test/controller/redirect_test.rb @@ -3,24 +3,6 @@ require 'abstract_unit' class WorkshopsController < ActionController::Base end -class Workshop - extend ActiveModel::Naming - include ActiveModel::Conversion - attr_accessor :id - - def initialize(id) - @id = id - end - - def persisted? - id.present? - end - - def to_s - id.to_s - end -end - class RedirectController < ActionController::Base def simple_redirect redirect_to :action => "hello_world" @@ -99,6 +81,19 @@ class RedirectController < ActionController::Base redirect_to nil end + def redirect_to_with_block + redirect_to proc { "http://www.rubyonrails.org/" } + end + + def redirect_to_with_block_and_assigns + @url = "http://www.rubyonrails.org/" + redirect_to proc { @url } + end + + def redirect_to_with_block_and_options + redirect_to proc { {:action => "hello_world"} } + end + def rescue_errors(e) raise e end def rescue_action(e) raise end @@ -232,7 +227,7 @@ class RedirectTest < ActionController::TestCase def test_redirect_to_record with_routing do |set| - set.draw do |map| + set.draw do resources :workshops match ':controller/:action' end @@ -252,6 +247,31 @@ class RedirectTest < ActionController::TestCase get :redirect_to_nil end end + + def test_redirect_to_with_block + get :redirect_to_with_block + assert_response :redirect + assert_redirected_to "http://www.rubyonrails.org/" + end + + def test_redirect_to_with_block_and_assigns + get :redirect_to_with_block_and_assigns + assert_response :redirect + assert_redirected_to "http://www.rubyonrails.org/" + end + + def test_redirect_to_with_block_and_accepted_options + with_routing do |set| + set.draw do + match ':controller/:action' + end + + get :redirect_to_with_block_and_options + + assert_response :redirect + assert_redirected_to "http://test.host/redirect/hello_world" + end + end end module ModuleTest diff --git a/actionpack/test/controller/render_json_test.rb b/actionpack/test/controller/render_json_test.rb index 5958b18d80..fc604a2db3 100644 --- a/actionpack/test/controller/render_json_test.rb +++ b/actionpack/test/controller/render_json_test.rb @@ -9,6 +9,10 @@ class RenderJsonTest < ActionController::TestCase hash.except!(*options[:except]) if options[:except] hash end + + def to_json(options = {}) + super :except => [:c, :e] + end end class TestController < ActionController::Base @@ -22,6 +26,10 @@ class RenderJsonTest < ActionController::TestCase render :json => nil end + def render_json_render_to_string + render :text => render_to_string(:json => '[]') + end + def render_json_hello_world render :json => ActiveSupport::JSON.encode(:hello => 'world') end @@ -49,6 +57,10 @@ class RenderJsonTest < ActionController::TestCase def render_json_with_extra_options render :json => JsonRenderable.new, :except => [:c, :e] end + + def render_json_without_options + render :json => JsonRenderable.new + end end tests TestController @@ -68,6 +80,12 @@ class RenderJsonTest < ActionController::TestCase assert_equal 'application/json', @response.content_type end + def test_render_json_render_to_string + get :render_json_render_to_string + assert_equal '[]', @response.body + end + + def test_render_json get :render_json_hello_world assert_equal '{"hello":"world"}', @response.body @@ -109,4 +127,9 @@ class RenderJsonTest < ActionController::TestCase assert_equal '{"a":"b"}', @response.body assert_equal 'application/json', @response.content_type end + + def test_render_json_calls_to_json_from_object + get :render_json_without_options + assert_equal '{"a":"b"}', @response.body + end end diff --git a/actionpack/test/controller/render_other_test.rb b/actionpack/test/controller/render_other_test.rb index dfc4f2db8c..eda777e7a7 100644 --- a/actionpack/test/controller/render_other_test.rb +++ b/actionpack/test/controller/render_other_test.rb @@ -120,6 +120,7 @@ class RenderOtherTest < ActionController::TestCase private def default_render + @alternate_default_render ||= nil if @alternate_default_render @alternate_default_render.call else @@ -224,15 +225,15 @@ class RenderOtherTest < ActionController::TestCase get :update_page_with_instance_variables assert_template nil assert_equal 'text/javascript; charset=utf-8', @response.headers["Content-Type"] - assert_match /balance/, @response.body - assert_match /\$37/, @response.body + assert_match(/balance/, @response.body) + assert_match(/\$37/, @response.body) end def test_update_page_with_view_method get :update_page_with_view_method assert_template nil assert_equal 'text/javascript; charset=utf-8', @response.headers["Content-Type"] - assert_match /2 people/, @response.body + assert_match(/2 people/, @response.body) end def test_should_render_html_formatted_partial_with_rjs diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index a57a12f271..be492152f2 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -7,7 +7,7 @@ module Fun # :ported: def hello_world end - + def nested_partial_with_form_builder render :partial => ActionView::Helpers::FormBuilder.new(:post, nil, view_context, {}, Proc.new {}) end @@ -99,11 +99,6 @@ class TestController < ActionController::Base render :template => "test/hello_world" end - def render_hello_world_with_etag_set - response.etag = "hello_world" - render :template => "test/hello_world" - end - # :ported: compatibility def render_hello_world_with_forward_slash render :template => "/test/hello_world" @@ -130,6 +125,10 @@ class TestController < ActionController::Base render :action => "hello_world" end + def render_action_upcased_hello_world + render :action => "Hello_world" + end + def render_action_hello_world_as_string render "hello_world" end @@ -276,6 +275,7 @@ class TestController < ActionController::Base # :ported: def builder_layout_test + @name = nil render :action => "hello", :layout => "layouts/builder" end @@ -327,6 +327,7 @@ class TestController < ActionController::Base end def default_render + @alternate_default_render ||= nil if @alternate_default_render @alternate_default_render.call else @@ -339,14 +340,17 @@ class TestController < ActionController::Base end def layout_test_with_different_layout + @variable_for_layout = nil render :action => "hello_world", :layout => "standard" end def layout_test_with_different_layout_and_string_action + @variable_for_layout = nil render "hello_world", :layout => "standard" end def layout_test_with_different_layout_and_symbol_action + @variable_for_layout = nil render :hello_world, :layout => "standard" end @@ -355,6 +359,7 @@ class TestController < ActionController::Base end def layout_overriding_layout + @variable_for_layout = nil render :action => "hello_world", :layout => "standard" end @@ -488,6 +493,10 @@ class TestController < ActionController::Base head :x_custom_header => "something" end + def head_with_www_authenticate_header + head 'WWW-Authenticate' => 'something' + end + def head_with_status_code_first head :forbidden, :x_custom_header => "something" end @@ -639,6 +648,7 @@ class TestController < ActionController::Base private def determine_layout + @variable_for_layout ||= nil case action_name when "hello_world", "layout_test", "rendering_without_layout", "rendering_nothing_on_layout", "render_text_hello_world", @@ -736,6 +746,12 @@ class RenderTest < ActionController::TestCase assert_template "test/hello_world" end + def test_render_action_upcased + assert_raise ActionView::MissingTemplate do + get :render_action_upcased_hello_world + end + end + # :ported: def test_render_action_hello_world_as_string get :render_action_hello_world_as_string @@ -1016,7 +1032,7 @@ class RenderTest < ActionController::TestCase assert_equal " ", @response.body end - def test_render_to_string + def test_render_to_string_not_deprecated assert_not_deprecated { get :hello_in_a_string } assert_equal "How's there? goodbyeHello: davidHello: marygoodbye\n", @response.body end @@ -1113,20 +1129,20 @@ class RenderTest < ActionController::TestCase def test_head_with_location_header get :head_with_location_header - assert @response.body.blank? + assert_blank @response.body assert_equal "/foo", @response.headers["Location"] assert_response :ok end def test_head_with_location_object with_routing do |set| - set.draw do |map| + set.draw do resources :customers match ':controller/:action' end get :head_with_location_object - assert @response.body.blank? + assert_blank @response.body assert_equal "http://www.nextangle.com/customers/1", @response.headers["Location"] assert_response :ok end @@ -1134,11 +1150,18 @@ class RenderTest < ActionController::TestCase def test_head_with_custom_header get :head_with_custom_header - assert @response.body.blank? + assert_blank @response.body assert_equal "something", @response.headers["X-Custom-Header"] assert_response :ok end + def test_head_with_www_authenticate_header + get :head_with_www_authenticate_header + assert_blank @response.body + assert_equal "something", @response.headers["WWW-Authenticate"] + assert_response :ok + end + def test_head_with_symbolic_status get :head_with_symbolic_status, :status => "ok" assert_equal 200, @response.status @@ -1234,7 +1257,7 @@ class RenderTest < ActionController::TestCase assert_match(/<label/, @response.body) assert_template('test/_labelling_form') end - + def test_nested_partial_with_form_builder @controller = Fun::GamesController.new get :nested_partial_with_form_builder @@ -1368,119 +1391,6 @@ class ExpiresInRenderTest < ActionController::TestCase end end - -class EtagRenderTest < ActionController::TestCase - tests TestController - - def setup - super - @request.host = "www.nextangle.com" - @expected_bang_etag = etag_for(expand_key([:foo, 123])) - end - - def test_render_blank_body_shouldnt_set_etag - get :blank_response - assert !@response.etag? - end - - def test_render_200_should_set_etag - get :render_hello_world_from_variable - assert_equal etag_for("hello david"), @response.headers['ETag'] - assert_equal "max-age=0, private, must-revalidate", @response.headers['Cache-Control'] - end - - def test_render_against_etag_request_should_304_when_match - @request.if_none_match = etag_for("hello david") - get :render_hello_world_from_variable - assert_equal 304, @response.status.to_i - assert @response.body.empty? - end - - def test_render_against_etag_request_should_have_no_content_length_when_match - @request.if_none_match = etag_for("hello david") - get :render_hello_world_from_variable - assert !@response.headers.has_key?("Content-Length") - end - - def test_render_against_etag_request_should_200_when_no_match - @request.if_none_match = etag_for("hello somewhere else") - get :render_hello_world_from_variable - assert_equal 200, @response.status.to_i - assert !@response.body.empty? - end - - def test_render_should_not_set_etag_when_last_modified_has_been_specified - get :render_hello_world_with_last_modified_set - assert_equal 200, @response.status.to_i - assert_not_nil @response.last_modified - assert_nil @response.etag - assert @response.body.present? - end - - def test_render_with_etag - get :render_hello_world_from_variable - expected_etag = etag_for('hello david') - assert_equal expected_etag, @response.headers['ETag'] - @response = ActionController::TestResponse.new - - @request.if_none_match = expected_etag - get :render_hello_world_from_variable - assert_equal 304, @response.status.to_i - - @response = ActionController::TestResponse.new - @request.if_none_match = "\"diftag\"" - get :render_hello_world_from_variable - assert_equal 200, @response.status.to_i - end - - def render_with_404_shouldnt_have_etag - get :render_custom_code - assert_nil @response.headers['ETag'] - end - - def test_etag_should_not_be_changed_when_already_set - get :render_hello_world_with_etag_set - assert_equal etag_for("hello_world"), @response.headers['ETag'] - end - - def test_etag_should_govern_renders_with_layouts_too - get :builder_layout_test - assert_equal "<wrapper>\n<html>\n <p>Hello </p>\n<p>This is grand!</p>\n</html>\n</wrapper>\n", @response.body - assert_equal etag_for("<wrapper>\n<html>\n <p>Hello </p>\n<p>This is grand!</p>\n</html>\n</wrapper>\n"), @response.headers['ETag'] - end - - def test_etag_with_bang_should_set_etag - get :conditional_hello_with_bangs - assert_equal @expected_bang_etag, @response.headers["ETag"] - assert_response :success - end - - def test_etag_with_bang_should_obey_if_none_match - @request.if_none_match = @expected_bang_etag - get :conditional_hello_with_bangs - assert_response :not_modified - end - - def test_etag_with_public_true_should_set_header - get :conditional_hello_with_public_header - assert_equal "public", @response.headers['Cache-Control'] - end - - def test_etag_with_public_true_should_set_header_and_retain_other_headers - get :conditional_hello_with_public_header_and_expires_at - assert_equal "max-age=60, public", @response.headers['Cache-Control'] - end - - protected - def etag_for(text) - %("#{Digest::MD5.hexdigest(text)}") - end - - def expand_key(args) - ActiveSupport::Cache.expand_cache_key(args) - end -end - class LastModifiedRenderTest < ActionController::TestCase tests TestController @@ -1499,7 +1409,7 @@ class LastModifiedRenderTest < ActionController::TestCase @request.if_modified_since = @last_modified get :conditional_hello assert_equal 304, @response.status.to_i - assert @response.body.blank?, @response.body + assert_blank @response.body assert_equal @last_modified, @response.headers['Last-Modified'] end @@ -1514,7 +1424,7 @@ class LastModifiedRenderTest < ActionController::TestCase @request.if_modified_since = 'Thu, 16 Jul 2008 00:00:00 GMT' get :conditional_hello assert_equal 200, @response.status.to_i - assert !@response.body.blank? + assert_present @response.body assert_equal @last_modified, @response.headers['Last-Modified'] end diff --git a/actionpack/test/controller/render_xml_test.rb b/actionpack/test/controller/render_xml_test.rb index 4bf867fa41..ec4dc848ff 100644 --- a/actionpack/test/controller/render_xml_test.rb +++ b/actionpack/test/controller/render_xml_test.rb @@ -70,10 +70,9 @@ class RenderXmlTest < ActionController::TestCase def test_rendering_with_object_location_should_set_header_with_url_for with_routing do |set| - set.draw do |map| + set.draw do resources :customers - # match ':controller/:action' - map.connect ':controller/:action/:id' + match ':controller/:action' end get :render_with_object_location diff --git a/actionpack/test/controller/request/test_request_test.rb b/actionpack/test/controller/request/test_request_test.rb index 0a39feb7fe..e624f11773 100644 --- a/actionpack/test/controller/request/test_request_test.rb +++ b/actionpack/test/controller/request/test_request_test.rb @@ -29,8 +29,7 @@ class ActionController::TestRequestTest < ActiveSupport::TestCase end def test_session_id_different_on_each_call - prev_id = assert_not_equal(@request.session_options[:id], ActionController::TestRequest.new.session_options[:id]) end -end
\ No newline at end of file +end diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb index be05ef6167..d520b5e512 100644 --- a/actionpack/test/controller/request_forgery_protection_test.rb +++ b/actionpack/test/controller/request_forgery_protection_test.rb @@ -1,5 +1,6 @@ require 'abstract_unit' require 'digest/sha1' +require 'active_support/core_ext/string/strip' # common controller actions module RequestForgeryProtectionActions @@ -11,12 +12,28 @@ module RequestForgeryProtectionActions render :inline => "<%= button_to('New', '/') {} %>" end + def external_form + render :inline => "<%= form_tag('http://farfar.away/form', :authenticity_token => 'external_token') {} %>" + end + + def external_form_without_protection + render :inline => "<%= form_tag('http://farfar.away/form', :authenticity_token => false) {} %>" + end + def unsafe render :text => 'pwn' end def meta - render :inline => "<%= csrf_meta_tag %>" + render :inline => "<%= csrf_meta_tags %>" + end + + def external_form_for + render :inline => "<%= form_for(:some_resource, :authenticity_token => 'external_token') {} %>" + end + + def form_for_without_protection + render :inline => "<%= form_for(:some_resource, :authenticity_token => false ) {} %>" end def rescue_action(e) raise e end @@ -28,6 +45,16 @@ class RequestForgeryProtectionController < ActionController::Base protect_from_forgery :only => %w(index meta) end +class RequestForgeryProtectionControllerUsingOldBehaviour < ActionController::Base + include RequestForgeryProtectionActions + protect_from_forgery :only => %w(index meta) + + def handle_unverified_request + raise(ActionController::InvalidAuthenticityToken) + end +end + + class FreeCookieController < RequestForgeryProtectionController self.allow_forgery_protection = false @@ -50,153 +77,92 @@ end # common test methods module RequestForgeryProtectionTests - def teardown - ActionController::Base.request_forgery_protection_token = nil - end - - - def test_should_render_form_with_token_tag - get :index - assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token - end - - def test_should_render_button_to_with_token_tag - get :show_button - assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token - end - - def test_should_allow_get - get :index - assert_response :success - end - - def test_should_allow_post_without_token_on_unsafe_action - post :unsafe - assert_response :success - end - - def test_should_not_allow_html_post_without_token - @request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s - assert_raise(ActionController::InvalidAuthenticityToken) { post :index, :format => :html } - end - - def test_should_not_allow_html_put_without_token - @request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s - assert_raise(ActionController::InvalidAuthenticityToken) { put :index, :format => :html } - end - - def test_should_not_allow_html_delete_without_token - @request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s - assert_raise(ActionController::InvalidAuthenticityToken) { delete :index, :format => :html } - end + def setup + @token = "cf50faa3fe97702ca1ae" - def test_should_allow_api_formatted_post_without_token - assert_nothing_raised do - post :index, :format => 'xml' - end + ActiveSupport::SecureRandom.stubs(:base64).returns(@token) + ActionController::Base.request_forgery_protection_token = :authenticity_token end - def test_should_not_allow_api_formatted_put_without_token - assert_nothing_raised do - put :index, :format => 'xml' - end - end - def test_should_allow_api_formatted_delete_without_token - assert_nothing_raised do - delete :index, :format => 'xml' + def test_should_render_form_with_token_tag + assert_not_blocked do + get :index end + assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token end - def test_should_not_allow_api_formatted_post_sent_as_url_encoded_form_without_token - assert_raise(ActionController::InvalidAuthenticityToken) do - @request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s - post :index, :format => 'xml' + def test_should_render_button_to_with_token_tag + assert_not_blocked do + get :show_button end + assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token end - def test_should_not_allow_api_formatted_put_sent_as_url_encoded_form_without_token - assert_raise(ActionController::InvalidAuthenticityToken) do - @request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s - put :index, :format => 'xml' - end + def test_should_allow_get + assert_not_blocked { get :index } end - def test_should_not_allow_api_formatted_delete_sent_as_url_encoded_form_without_token - assert_raise(ActionController::InvalidAuthenticityToken) do - @request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s - delete :index, :format => 'xml' - end + def test_should_allow_post_without_token_on_unsafe_action + assert_not_blocked { post :unsafe } end - def test_should_not_allow_api_formatted_post_sent_as_multipart_form_without_token - assert_raise(ActionController::InvalidAuthenticityToken) do - @request.env['CONTENT_TYPE'] = Mime::MULTIPART_FORM.to_s - post :index, :format => 'xml' - end + def test_should_not_allow_post_without_token + assert_blocked { post :index } end - def test_should_not_allow_api_formatted_put_sent_as_multipart_form_without_token - assert_raise(ActionController::InvalidAuthenticityToken) do - @request.env['CONTENT_TYPE'] = Mime::MULTIPART_FORM.to_s - put :index, :format => 'xml' - end + def test_should_not_allow_post_without_token_irrespective_of_format + assert_blocked { post :index, :format=>'xml' } end - def test_should_not_allow_api_formatted_delete_sent_as_multipart_form_without_token - assert_raise(ActionController::InvalidAuthenticityToken) do - @request.env['CONTENT_TYPE'] = Mime::MULTIPART_FORM.to_s - delete :index, :format => 'xml' - end + def test_should_not_allow_put_without_token + assert_blocked { put :index } end - def test_should_allow_xhr_post_without_token - assert_nothing_raised { xhr :post, :index } + def test_should_not_allow_delete_without_token + assert_blocked { delete :index } end - def test_should_allow_xhr_put_without_token - assert_nothing_raised { xhr :put, :index } + def test_should_not_allow_xhr_post_without_token + assert_blocked { xhr :post, :index } end - def test_should_allow_xhr_delete_without_token - assert_nothing_raised { xhr :delete, :index } + def test_should_allow_post_with_token + assert_not_blocked { post :index, :authenticity_token => @token } end - def test_should_allow_xhr_post_with_encoded_form_content_type_without_token - @request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s - assert_nothing_raised { xhr :post, :index } + def test_should_allow_put_with_token + assert_not_blocked { put :index, :authenticity_token => @token } end - def test_should_allow_post_with_token - post :index, :authenticity_token => @token - assert_response :success + def test_should_allow_delete_with_token + assert_not_blocked { delete :index, :authenticity_token => @token } end - def test_should_allow_put_with_token - put :index, :authenticity_token => @token - assert_response :success + def test_should_allow_post_with_token_in_header + @request.env['HTTP_X_CSRF_TOKEN'] = @token + assert_not_blocked { post :index } end - def test_should_allow_delete_with_token - delete :index, :authenticity_token => @token - assert_response :success + def test_should_allow_delete_with_token_in_header + @request.env['HTTP_X_CSRF_TOKEN'] = @token + assert_not_blocked { delete :index } end - def test_should_allow_post_with_xml - @request.env['CONTENT_TYPE'] = Mime::XML.to_s - post :index, :format => 'xml' - assert_response :success + def test_should_allow_put_with_token_in_header + @request.env['HTTP_X_CSRF_TOKEN'] = @token + assert_not_blocked { put :index } end - def test_should_allow_put_with_xml - @request.env['CONTENT_TYPE'] = Mime::XML.to_s - put :index, :format => 'xml' + def assert_blocked + session[:something_like_user_id] = 1 + yield + assert_nil session[:something_like_user_id], "session values are still present" assert_response :success end - def test_should_allow_delete_with_xml - @request.env['CONTENT_TYPE'] = Mime::XML.to_s - delete :index, :format => 'xml' + def assert_not_blocked + assert_nothing_raised { yield } assert_response :success end end @@ -205,21 +171,23 @@ end class RequestForgeryProtectionControllerTest < ActionController::TestCase include RequestForgeryProtectionTests - def setup - @controller = RequestForgeryProtectionController.new - @request = ActionController::TestRequest.new - @request.format = :html - @response = ActionController::TestResponse.new - @token = "cf50faa3fe97702ca1ae" - - ActiveSupport::SecureRandom.stubs(:base64).returns(@token) - ActionController::Base.request_forgery_protection_token = :authenticity_token - end test 'should emit a csrf-token meta tag' do ActiveSupport::SecureRandom.stubs(:base64).returns(@token + '<=?') get :meta - assert_equal %(<meta name="csrf-param" content="authenticity_token"/>\n<meta name="csrf-token" content="cf50faa3fe97702ca1ae<=?"/>), @response.body + assert_equal <<-METAS.strip_heredoc.chomp, @response.body + <meta name="csrf-param" content="authenticity_token"/> + <meta name="csrf-token" content="cf50faa3fe97702ca1ae<=?"/> + METAS + end +end + +class RequestForgeryProtectionControllerUsingOldBehaviourTest < ActionController::TestCase + include RequestForgeryProtectionTests + def assert_blocked + assert_raises(ActionController::InvalidAuthenticityToken) do + yield + end end end @@ -251,17 +219,27 @@ class FreeCookieControllerTest < ActionController::TestCase test 'should not emit a csrf-token meta tag' do get :meta - assert @response.body.blank? + assert_blank @response.body end end + + + + class CustomAuthenticityParamControllerTest < ActionController::TestCase def setup + ActionController::Base.request_forgery_protection_token = :custom_token_name + super + end + + def teardown ActionController::Base.request_forgery_protection_token = :authenticity_token + super end def test_should_allow_custom_token - post :index, :authenticity_token => 'foobar' + post :index, :custom_token_name => 'foobar' assert_response :ok end end diff --git a/actionpack/test/controller/rescue_test.rb b/actionpack/test/controller/rescue_test.rb index a24de62b19..c445285538 100644 --- a/actionpack/test/controller/rescue_test.rb +++ b/actionpack/test/controller/rescue_test.rb @@ -1,19 +1,5 @@ require 'abstract_unit' -module ActionDispatch - class ShowExceptions - private - def public_path - "#{FIXTURE_LOAD_PATH}/public" - end - - # Silence logger - def logger - nil - end - end -end - class RescueController < ActionController::Base class NotAuthorized < StandardError end @@ -149,7 +135,7 @@ class RescueController < ActionController::Base def missing_template end - + def io_error_in_view raise ActionView::TemplateError.new(nil, {}, IOError.new('this is io error')) end @@ -311,7 +297,7 @@ class RescueControllerTest < ActionController::TestCase end end -class RescueTest < ActionController::IntegrationTest +class RescueTest < ActionDispatch::IntegrationTest class TestController < ActionController::Base class RecordInvalid < StandardError def message @@ -371,7 +357,7 @@ class RescueTest < ActionController::IntegrationTest private def with_test_routing with_routing do |set| - set.draw do |map| + set.draw do match 'foo', :to => ::RescueTest::TestController.action(:foo) match 'invalid', :to => ::RescueTest::TestController.action(:invalid) match 'b00m', :to => ::RescueTest::TestController.action(:b00m) diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb index 6c8f470fba..acb4617a60 100644 --- a/actionpack/test/controller/resources_test.rb +++ b/actionpack/test/controller/resources_test.rb @@ -1,5 +1,6 @@ -require 'active_support/core_ext/object/try' require 'abstract_unit' +require 'active_support/core_ext/object/try' +require 'active_support/core_ext/object/with_options' class ResourcesController < ActionController::Base def index() render :nothing => true end @@ -32,36 +33,6 @@ module Backoffice end class ResourcesTest < ActionController::TestCase - def test_should_arrange_actions - resource = ActionDispatch::Routing::DeprecatedMapper::Resource.new(:messages, { - :collection => { :rss => :get, :reorder => :post, :csv => :post }, - :member => { :rss => :get, :atom => :get, :upload => :post, :fix => :post }, - :new => { :preview => :get, :draft => :get }}, {}) - - assert_resource_methods [:rss], resource, :collection, :get - assert_resource_methods [:csv, :reorder], resource, :collection, :post - assert_resource_methods [:edit, :rss, :atom], resource, :member, :get - assert_resource_methods [:upload, :fix], resource, :member, :post - assert_resource_methods [:new, :preview, :draft], resource, :new, :get - end - - def test_should_resource_controller_name_equal_resource_name_by_default - resource = ActionDispatch::Routing::DeprecatedMapper::Resource.new(:messages, {}, {}) - assert_equal 'messages', resource.controller - end - - def test_should_resource_controller_name_equal_controller_option - resource = ActionDispatch::Routing::DeprecatedMapper::Resource.new(:messages, {:controller => 'posts'}, {}) - assert_equal 'posts', resource.controller - end - - def test_should_all_singleton_paths_be_the_same - [ :path, :nesting_path_prefix, :member_path ].each do |method| - resource = ActionDispatch::Routing::DeprecatedMapper::SingletonResource.new(:messages, {:path_prefix => 'admin'}, {}) - assert_equal 'admin/messages', resource.send(method) - end - end - def test_default_restful_routes with_restful_routing :messages do assert_simply_restful_for :messages @@ -69,9 +40,9 @@ class ResourcesTest < ActionController::TestCase end def test_override_paths_for_member_and_collection_methods - collection_methods = { 'rss' => :get, 'reorder' => :post, 'csv' => :post } - member_methods = { 'rss' => :get, :atom => :get, :upload => :post, :fix => :post } - path_names = {:new => 'nuevo', 'rss' => 'canal', :fix => 'corrigir' } + collection_methods = { :rss => :get, :reorder => :post, :csv => :post } + member_methods = { :rss => :get, :atom => :get, :upload => :post, :fix => :post } + path_names = {:new => 'nuevo', :rss => 'canal', :fix => 'corrigir' } with_restful_routing :messages, :collection => collection_methods, @@ -89,7 +60,7 @@ class ResourcesTest < ActionController::TestCase end collection_methods.each do |action, method| - assert_recognizes(options.merge(:action => action), + assert_recognizes(options.merge(:action => action.to_s), :path => "/messages/#{path_names[action] || action}", :method => method) end @@ -112,12 +83,6 @@ class ResourcesTest < ActionController::TestCase end end - def test_override_paths_for_default_restful_actions - resource = ActionDispatch::Routing::DeprecatedMapper::Resource.new(:messages, { - :path_names => {:new => 'nuevo', :edit => 'editar'}}, {}) - assert_equal resource.new_path, "#{resource.path}/nuevo" - end - def test_multiple_default_restful_routes with_restful_routing :messages, :comments do assert_simply_restful_for :messages @@ -131,7 +96,7 @@ class ResourcesTest < ActionController::TestCase end end - def test_irregular_id_with_no_requirements_should_raise_error + def test_irregular_id_with_no_constraints_should_raise_error expected_options = {:controller => 'messages', :action => 'show', :id => '1.1.1'} with_restful_routing :messages do @@ -141,25 +106,25 @@ class ResourcesTest < ActionController::TestCase end end - def test_irregular_id_with_requirements_should_pass + def test_irregular_id_with_constraints_should_pass expected_options = {:controller => 'messages', :action => 'show', :id => '1.1.1'} - with_restful_routing(:messages, :requirements => {:id => /[0-9]\.[0-9]\.[0-9]/}) do + with_restful_routing(:messages, :constraints => {:id => /[0-9]\.[0-9]\.[0-9]/}) do assert_recognizes(expected_options, :path => 'messages/1.1.1', :method => :get) end end - def test_with_path_prefix_requirements + def test_with_path_prefix_constraints expected_options = {:controller => 'messages', :action => 'show', :thread_id => '1.1.1', :id => '1'} - with_restful_routing :messages, :path_prefix => '/thread/:thread_id', :requirements => {:thread_id => /[0-9]\.[0-9]\.[0-9]/} do + with_restful_routing :messages, :path_prefix => '/thread/:thread_id', :constraints => {:thread_id => /[0-9]\.[0-9]\.[0-9]/} do assert_recognizes(expected_options, :path => 'thread/1.1.1/messages/1', :method => :get) end end - def test_irregular_id_requirements_should_get_passed_to_member_actions + def test_irregular_id_constraints_should_get_passed_to_member_actions expected_options = {:controller => 'messages', :action => 'custom', :id => '1.1.1'} - with_restful_routing(:messages, :member => {:custom => :get}, :requirements => {:id => /[0-9]\.[0-9]\.[0-9]/}) do + with_restful_routing(:messages, :member => {:custom => :get}, :constraints => {:id => /[0-9]\.[0-9]\.[0-9]/}) do assert_recognizes(expected_options, :path => 'messages/1.1.1/custom', :method => :get) end end @@ -178,7 +143,7 @@ class ResourcesTest < ActionController::TestCase end def test_with_name_prefix - with_restful_routing :messages, :name_prefix => 'post_' do + with_restful_routing :messages, :as => 'post_messages' do assert_simply_restful_for :messages, :name_prefix => 'post_' end end @@ -186,7 +151,16 @@ class ResourcesTest < ActionController::TestCase def test_with_collection_actions actions = { 'a' => :get, 'b' => :put, 'c' => :post, 'd' => :delete } - with_restful_routing :messages, :collection => actions do + with_routing do |set| + set.draw do + resources :messages do + get :a, :on => :collection + put :b, :on => :collection + post :c, :on => :collection + delete :d, :on => :collection + end + end + assert_restful_routes_for :messages do |options| actions.each do |action, method| assert_recognizes(options.merge(:action => action), :path => "/messages/#{action}", :method => method) @@ -204,7 +178,18 @@ class ResourcesTest < ActionController::TestCase def test_with_collection_actions_and_name_prefix actions = { 'a' => :get, 'b' => :put, 'c' => :post, 'd' => :delete } - with_restful_routing :messages, :path_prefix => '/threads/:thread_id', :name_prefix => "thread_", :collection => actions do + with_routing do |set| + set.draw do + scope '/threads/:thread_id' do + resources :messages, :as => 'thread_messages' do + get :a, :on => :collection + put :b, :on => :collection + post :c, :on => :collection + delete :d, :on => :collection + end + end + end + assert_restful_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do |options| actions.each do |action, method| assert_recognizes(options.merge(:action => action), :path => "/threads/1/messages/#{action}", :method => method) @@ -222,7 +207,16 @@ class ResourcesTest < ActionController::TestCase def test_with_collection_actions_and_name_prefix_and_member_action_with_same_name actions = { 'a' => :get } - with_restful_routing :messages, :path_prefix => '/threads/:thread_id', :name_prefix => "thread_", :collection => actions, :member => actions do + with_routing do |set| + set.draw do + scope '/threads/:thread_id' do + resources :messages, :as => 'thread_messages' do + get :a, :on => :collection + get :a, :on => :member + end + end + end + assert_restful_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do |options| actions.each do |action, method| assert_recognizes(options.merge(:action => action), :path => "/threads/1/messages/#{action}", :method => method) @@ -240,7 +234,18 @@ class ResourcesTest < ActionController::TestCase def test_with_collection_action_and_name_prefix_and_formatted actions = { 'a' => :get, 'b' => :put, 'c' => :post, 'd' => :delete } - with_restful_routing :messages, :path_prefix => '/threads/:thread_id', :name_prefix => "thread_", :collection => actions do + with_routing do |set| + set.draw do + scope '/threads/:thread_id' do + resources :messages, :as => 'thread_messages' do + get :a, :on => :collection + put :b, :on => :collection + post :c, :on => :collection + delete :d, :on => :collection + end + end + end + assert_restful_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do |options| actions.each do |action, method| assert_recognizes(options.merge(:action => action, :format => 'xml'), :path => "/threads/1/messages/#{action}.xml", :method => method) @@ -274,7 +279,7 @@ class ResourcesTest < ActionController::TestCase def test_with_member_action_and_requirement expected_options = {:controller => 'messages', :action => 'mark', :id => '1.1.1'} - with_restful_routing(:messages, :requirements => {:id => /[0-9]\.[0-9]\.[0-9]/}, :member => { :mark => :get }) do + with_restful_routing(:messages, :constraints => {:id => /[0-9]\.[0-9]\.[0-9]/}, :member => { :mark => :get }) do assert_recognizes(expected_options, :path => 'messages/1.1.1/mark', :method => :get) end end @@ -296,31 +301,18 @@ class ResourcesTest < ActionController::TestCase end end - # def test_member_when_changed_default_restful_actions_and_path_names_not_specified - # default_path_names = ActionController::Base.resources_path_names - # ActionController::Base.resources_path_names = {:new => 'nuevo', :edit => 'editar'} - # - # with_restful_routing :messages do - # new_options = { :action => 'new', :controller => 'messages' } - # new_path = "/messages/nuevo" - # edit_options = { :action => 'edit', :id => '1', :controller => 'messages' } - # edit_path = "/messages/1/editar" - # - # assert_restful_routes_for :messages do |options| - # assert_recognizes(options.merge(new_options), :path => new_path, :method => :get) - # end - # - # assert_restful_routes_for :messages do |options| - # assert_recognizes(options.merge(edit_options), :path => edit_path, :method => :get) - # end - # end - # ensure - # ActionController::Base.resources_path_names = default_path_names - # end - def test_with_two_member_actions_with_same_method [:put, :post].each do |method| - with_restful_routing :messages, :member => { :mark => method, :unmark => method } do + with_routing do |set| + set.draw do + resources :messages do + member do + match :mark , :via => method + match :unmark, :via => method + end + end + end + %w(mark unmark).each do |action| action_options = {:action => action, :id => '1'} action_path = "/messages/1/#{action}" @@ -337,7 +329,19 @@ class ResourcesTest < ActionController::TestCase end def test_array_as_collection_or_member_method_value - with_restful_routing :messages, :collection => { :search => [:get, :post] }, :member => { :toggle => [:get, :post] } do + with_routing do |set| + set.draw do + resources :messages do + collection do + match :search, :via => [:post, :get] + end + + member do + match :toggle, :via => [:post, :get] + end + end + end + assert_restful_routes_for :messages do |options| [:get, :post].each do |method| assert_recognizes(options.merge(:action => 'search'), :path => "/messages/search", :method => method) @@ -350,7 +354,13 @@ class ResourcesTest < ActionController::TestCase end def test_with_new_action - with_restful_routing :messages, :new => { :preview => :post } do + with_routing do |set| + set.draw do + resources :messages do + post :preview, :on => :new + end + end + preview_options = {:action => 'preview'} preview_path = "/messages/new/preview" assert_restful_routes_for :messages do |options| @@ -364,7 +374,15 @@ class ResourcesTest < ActionController::TestCase end def test_with_new_action_with_name_prefix - with_restful_routing :messages, :new => { :preview => :post }, :path_prefix => '/threads/:thread_id', :name_prefix => 'thread_' do + with_routing do |set| + set.draw do + scope('/threads/:thread_id') do + resources :messages, :as => "thread_messages" do + post :preview, :on => :new + end + end + end + preview_options = {:action => 'preview', :thread_id => '1'} preview_path = "/threads/1/messages/new/preview" assert_restful_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do |options| @@ -378,7 +396,15 @@ class ResourcesTest < ActionController::TestCase end def test_with_formatted_new_action_with_name_prefix - with_restful_routing :messages, :new => { :preview => :post }, :path_prefix => '/threads/:thread_id', :name_prefix => 'thread_' do + with_routing do |set| + set.draw do + scope('/threads/:thread_id') do + resources :messages, :as => "thread_messages" do + post :preview, :on => :new + end + end + end + preview_options = {:action => 'preview', :thread_id => '1', :format => 'xml'} preview_path = "/threads/1/messages/new/preview.xml" assert_restful_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do |options| @@ -401,7 +427,13 @@ class ResourcesTest < ActionController::TestCase end end - with_restful_routing :messages, :new => { :new => :any } do + with_routing do |set| + set.draw do + resources :messages do + match :new, :via => [:post, :get], :on => :new + end + end + assert_restful_routes_for :messages do |options| assert_recognizes(options.merge(:action => "new"), :path => "/messages/new", :method => :post) assert_recognizes(options.merge(:action => "new"), :path => "/messages/new", :method => :get) @@ -411,10 +443,10 @@ class ResourcesTest < ActionController::TestCase def test_nested_restful_routes with_routing do |set| - set.draw do |map| - map.resources :threads do |map| - map.resources :messages do |map| - map.resources :comments + set.draw do + resources :threads do + resources :messages do + resources :comments end end end @@ -431,32 +463,12 @@ class ResourcesTest < ActionController::TestCase end end - def test_nested_restful_routes_with_overwritten_defaults - with_routing do |set| - set.draw do |map| - map.resources :threads do |map| - map.resources :messages, :name_prefix => nil do |map| - map.resources :comments, :name_prefix => nil - end - end - end - - assert_simply_restful_for :threads - assert_simply_restful_for :messages, - :path_prefix => 'threads/1/', - :options => { :thread_id => '1' } - assert_simply_restful_for :comments, - :path_prefix => 'threads/1/messages/2/', - :options => { :thread_id => '1', :message_id => '2' } - end - end - def test_shallow_nested_restful_routes with_routing do |set| - set.draw do |map| - map.resources :threads, :shallow => true do |map| - map.resources :messages do |map| - map.resources :comments + set.draw do + resources :threads, :shallow => true do + resources :messages do + resources :comments end end end @@ -478,11 +490,11 @@ class ResourcesTest < ActionController::TestCase def test_shallow_nested_restful_routes_with_namespaces with_routing do |set| - set.draw do |map| - map.namespace :backoffice do |map| - map.namespace :admin do |map| - map.resources :products, :shallow => true do |map| - map.resources :images + set.draw do + namespace :backoffice do + namespace :admin do + resources :products, :shallow => true do + resources :images end end end @@ -531,9 +543,9 @@ class ResourcesTest < ActionController::TestCase def test_should_create_nested_singleton_resource_routes with_routing do |set| - set.draw do |map| - map.resource :admin, :controller => 'admin' do |admin| - admin.resource :account + set.draw do + resource :admin, :controller => 'admin' do + resource :account end end @@ -542,69 +554,15 @@ class ResourcesTest < ActionController::TestCase end end - def test_resource_has_many_should_become_nested_resources - with_routing do |set| - set.draw do |map| - map.resources :messages, :has_many => [ :comments, :authors ] - end - - assert_simply_restful_for :messages - assert_simply_restful_for :comments, :name_prefix => "message_", :path_prefix => 'messages/1/', :options => { :message_id => '1' } - assert_simply_restful_for :authors, :name_prefix => "message_", :path_prefix => 'messages/1/', :options => { :message_id => '1' } - end - end - - def test_resources_has_many_hash_should_become_nested_resources - with_routing do |set| - set.draw do |map| - map.resources :threads, :has_many => { :messages => [ :comments, { :authors => :threads } ] } - end - - assert_simply_restful_for :threads - assert_simply_restful_for :messages, :name_prefix => "thread_", :path_prefix => 'threads/1/', :options => { :thread_id => '1' } - assert_simply_restful_for :comments, :name_prefix => "thread_message_", :path_prefix => 'threads/1/messages/1/', :options => { :thread_id => '1', :message_id => '1' } - assert_simply_restful_for :authors, :name_prefix => "thread_message_", :path_prefix => 'threads/1/messages/1/', :options => { :thread_id => '1', :message_id => '1' } - assert_simply_restful_for :threads, :name_prefix => "thread_message_author_", :path_prefix => 'threads/1/messages/1/authors/1/', :options => { :thread_id => '1', :message_id => '1', :author_id => '1' } - end - end - - def test_shallow_resource_has_many_should_become_shallow_nested_resources - with_routing do |set| - set.draw do |map| - map.resources :messages, :has_many => [ :comments, :authors ], :shallow => true - end - - assert_simply_restful_for :messages, :shallow => true - assert_simply_restful_for :comments, :name_prefix => "message_", :path_prefix => 'messages/1/', :shallow => true, :options => { :message_id => '1' } - assert_simply_restful_for :authors, :name_prefix => "message_", :path_prefix => 'messages/1/', :shallow => true, :options => { :message_id => '1' } - end - end - - def test_resource_has_one_should_become_nested_resources - with_routing do |set| - set.draw do |map| - map.resources :messages, :has_one => :logo - end - - assert_simply_restful_for :messages - assert_singleton_restful_for :logo, :name_prefix => 'message_', :path_prefix => 'messages/1/', :options => { :message_id => '1' } - end - end - - def test_shallow_resource_has_one_should_become_shallow_nested_resources - with_routing do |set| - set.draw do |map| - map.resources :messages, :has_one => :logo, :shallow => true - end - - assert_simply_restful_for :messages, :shallow => true - assert_singleton_restful_for :logo, :name_prefix => 'message_', :path_prefix => 'messages/1/', :shallow => true, :options => { :message_id => '1' } - end - end - def test_singleton_resource_with_member_action [:put, :post].each do |method| - with_singleton_resources :account, :member => { :reset => method } do + with_routing do |set| + set.draw do + resource :account do + match :reset, :on => :member, :via => method + end + end + reset_options = {:action => 'reset'} reset_path = "/account/reset" assert_singleton_routes_for :account do |options| @@ -620,7 +578,14 @@ class ResourcesTest < ActionController::TestCase def test_singleton_resource_with_two_member_actions_with_same_method [:put, :post].each do |method| - with_singleton_resources :account, :member => { :reset => method, :disable => method } do + with_routing do |set| + set.draw do + resource :account do + match :reset, :on => :member, :via => method + match :disable, :on => :member, :via => method + end + end + %w(reset disable).each do |action| action_options = {:action => action} action_path = "/account/#{action}" @@ -638,9 +603,9 @@ class ResourcesTest < ActionController::TestCase def test_should_nest_resources_in_singleton_resource with_routing do |set| - set.draw do |map| - map.resource :account do |account| - account.resources :messages + set.draw do + resource :account do + resources :messages end end @@ -649,11 +614,13 @@ class ResourcesTest < ActionController::TestCase end end - def test_should_nest_resources_in_singleton_resource_with_path_prefix + def test_should_nest_resources_in_singleton_resource_with_path_scope with_routing do |set| - set.draw do |map| - map.resource(:account, :path_prefix => ':site_id') do |account| - account.resources :messages + set.draw do + scope ':site_id' do + resource(:account) do + resources :messages + end end end @@ -664,9 +631,9 @@ class ResourcesTest < ActionController::TestCase def test_should_nest_singleton_resource_in_resources with_routing do |set| - set.draw do |map| - map.resources :threads do |thread| - thread.resource :admin, :controller => 'admin' + set.draw do + resources :threads do + resource :admin, :controller => 'admin' end end @@ -691,52 +658,18 @@ class ResourcesTest < ActionController::TestCase end end - def test_should_not_allow_invalid_head_method_for_member_routes - with_routing do |set| - assert_raise(ArgumentError) do - set.draw do |map| - map.resources :messages, :member => {:something => :head} - end - end - end - end - - def test_should_not_allow_invalid_http_methods_for_member_routes + def test_new_style_named_routes_for_resource with_routing do |set| - assert_raise(ArgumentError) do - set.draw do |map| - map.resources :messages, :member => {:something => :invalid} + set.draw do + scope '/threads/:thread_id' do + resources :messages, :as => 'thread_messages' do + get :search, :on => :collection + match :preview, :on => :new + end end end - end - end - - def test_resource_action_separator - with_routing do |set| - set.draw do |map| - map.resources :messages, :collection => {:search => :get}, :new => {:preview => :any}, :name_prefix => 'thread_', :path_prefix => '/threads/:thread_id' - map.resource :account, :member => {:login => :get}, :new => {:preview => :any}, :name_prefix => 'admin_', :path_prefix => '/admin' - end - - action_separator = ActionController::Base.resource_action_separator assert_simply_restful_for :messages, :name_prefix => 'thread_', :path_prefix => 'threads/1/', :options => { :thread_id => '1' } - assert_named_route "/threads/1/messages#{action_separator}search", "search_thread_messages_path", {} - assert_named_route "/threads/1/messages/new", "new_thread_message_path", {} - assert_named_route "/threads/1/messages/new#{action_separator}preview", "preview_new_thread_message_path", {} - assert_singleton_restful_for :account, :name_prefix => 'admin_', :path_prefix => 'admin/' - assert_named_route "/admin/account#{action_separator}login", "login_admin_account_path", {} - assert_named_route "/admin/account/new", "new_admin_account_path", {} - assert_named_route "/admin/account/new#{action_separator}preview", "preview_new_admin_account_path", {} - end - end - - def test_new_style_named_routes_for_resource - with_routing do |set| - set.draw do |map| - map.resources :messages, :collection => {:search => :get}, :new => {:preview => :any}, :name_prefix => 'thread_', :path_prefix => '/threads/:thread_id' - end - assert_simply_restful_for :messages, :name_prefix => 'thread_', :path_prefix => 'threads/1/', :options => { :thread_id => '1' } assert_named_route "/threads/1/messages/search", "search_thread_messages_path", {} assert_named_route "/threads/1/messages/new", "new_thread_message_path", {} assert_named_route "/threads/1/messages/new/preview", "preview_new_thread_message_path", {} @@ -745,8 +678,13 @@ class ResourcesTest < ActionController::TestCase def test_new_style_named_routes_for_singleton_resource with_routing do |set| - set.draw do |map| - map.resource :account, :member => {:login => :get}, :new => {:preview => :any}, :name_prefix => 'admin_', :path_prefix => '/admin' + set.draw do + scope '/admin' do + resource :account, :as => :admin_account do + get :login, :on => :member + match :preview, :on => :new + end + end end assert_singleton_restful_for :account, :name_prefix => 'admin_', :path_prefix => 'admin/' assert_named_route "/admin/account/login", "login_admin_account_path", {} @@ -757,48 +695,22 @@ class ResourcesTest < ActionController::TestCase def test_resources_in_namespace with_routing do |set| - set.draw do |map| - map.namespace :backoffice do |backoffice| - backoffice.resources :products - end - end - - assert_simply_restful_for :products, :controller => "backoffice/products", :name_prefix => 'backoffice_', :path_prefix => 'backoffice/' - end - end - - def test_resource_has_many_in_namespace - with_routing do |set| - set.draw do |map| - map.namespace :backoffice do |backoffice| - backoffice.resources :products, :has_many => :tags - end - end - - assert_simply_restful_for :products, :controller => "backoffice/products", :name_prefix => 'backoffice_', :path_prefix => 'backoffice/' - assert_simply_restful_for :tags, :controller => "backoffice/tags", :name_prefix => "backoffice_product_", :path_prefix => 'backoffice/products/1/', :options => { :product_id => '1' } - end - end - - def test_resource_has_one_in_namespace - with_routing do |set| - set.draw do |map| - map.namespace :backoffice do |backoffice| - backoffice.resources :products, :has_one => :manufacturer + set.draw do + namespace :backoffice do + resources :products end end assert_simply_restful_for :products, :controller => "backoffice/products", :name_prefix => 'backoffice_', :path_prefix => 'backoffice/' - assert_singleton_restful_for :manufacturer, :controller => "backoffice/manufacturers", :name_prefix => 'backoffice_product_', :path_prefix => 'backoffice/products/1/', :options => { :product_id => '1' } end end def test_resources_in_nested_namespace with_routing do |set| - set.draw do |map| - map.namespace :backoffice do |backoffice| - backoffice.namespace :admin do |admin| - admin.resources :products + set.draw do + namespace :backoffice do + namespace :admin do + resources :products end end end @@ -809,8 +721,10 @@ class ResourcesTest < ActionController::TestCase def test_resources_using_namespace with_routing do |set| - set.draw do |map| - map.resources :products, :namespace => "backoffice/" + set.draw do + namespace :backoffice, :path => nil, :as => nil do + resources :products + end end assert_simply_restful_for :products, :controller => "backoffice/products" @@ -819,10 +733,10 @@ class ResourcesTest < ActionController::TestCase def test_nested_resources_using_namespace with_routing do |set| - set.draw do |map| - map.namespace :backoffice do |backoffice| - backoffice.resources :products do |products| - products.resources :images + set.draw do + namespace :backoffice do + resources :products do + resources :images end end end @@ -833,11 +747,11 @@ class ResourcesTest < ActionController::TestCase def test_nested_resources_in_nested_namespace with_routing do |set| - set.draw do |map| - map.namespace :backoffice do |backoffice| - backoffice.namespace :admin do |admin| - admin.resources :products do |products| - products.resources :images + set.draw do + namespace :backoffice do + namespace :admin do + resources :products do + resources :images end end end @@ -854,21 +768,24 @@ class ResourcesTest < ActionController::TestCase assert_recognizes({:controller => "messages", :action => "index"}, "/messages/") end - with_restful_routing :messages, :as => 'reviews' do - assert_simply_restful_for :messages, :as => 'reviews' - assert_recognizes({:controller => "messages", :action => "index"}, "/reviews") - assert_recognizes({:controller => "messages", :action => "index"}, "/reviews/") + with_routing do |set| + set.draw do + resources :messages, :path => 'reviews' + end + assert_simply_restful_for :messages, :as => 'reviews' + assert_recognizes({:controller => "messages", :action => "index"}, "/reviews") + assert_recognizes({:controller => "messages", :action => "index"}, "/reviews/") end end def test_multiple_with_path_segment_and_controller with_routing do |set| - set.draw do |map| - map.resources :products do |products| - products.resources :product_reviews, :as => 'reviews', :controller => 'messages' + set.draw do + resources :products do + resources :product_reviews, :path => 'reviews', :controller => 'messages' end - map.resources :tutors do |tutors| - tutors.resources :tutor_reviews, :as => 'reviews', :controller => 'comments' + resources :tutors do + resources :tutor_reviews, :path => 'reviews', :controller => 'comments' end end @@ -877,17 +794,22 @@ class ResourcesTest < ActionController::TestCase end end - def test_with_path_segment_path_prefix_requirements + def test_with_path_segment_path_prefix_constraints expected_options = {:controller => 'messages', :action => 'show', :thread_id => '1.1.1', :id => '1'} - with_restful_routing :messages, :as => 'comments',:path_prefix => '/thread/:thread_id', :requirements => { :thread_id => /[0-9]\.[0-9]\.[0-9]/ } do + with_routing do |set| + set.draw do + scope '/thread/:thread_id', :constraints => { :thread_id => /[0-9]\.[0-9]\.[0-9]/ } do + resources :messages, :path => 'comments' + end + end assert_recognizes(expected_options, :path => 'thread/1.1.1/comments/1', :method => :get) end end def test_resource_has_only_show_action with_routing do |set| - set.draw do |map| - map.resources :products, :only => :show + set.draw do + resources :products, :only => :show end assert_resource_allowed_routes('products', {}, { :id => '1' }, :show, [:index, :new, :create, :edit, :update, :destroy]) @@ -897,8 +819,8 @@ class ResourcesTest < ActionController::TestCase def test_singleton_resource_has_only_show_action with_routing do |set| - set.draw do |map| - map.resource :account, :only => :show + set.draw do + resource :account, :only => :show end assert_singleton_resource_allowed_routes('accounts', {}, :show, [:index, :new, :create, :edit, :update, :destroy]) @@ -908,8 +830,8 @@ class ResourcesTest < ActionController::TestCase def test_resource_does_not_have_destroy_action with_routing do |set| - set.draw do |map| - map.resources :products, :except => :destroy + set.draw do + resources :products, :except => :destroy end assert_resource_allowed_routes('products', {}, { :id => '1' }, [:index, :new, :create, :show, :edit, :update], :destroy) @@ -919,8 +841,8 @@ class ResourcesTest < ActionController::TestCase def test_singleton_resource_does_not_have_destroy_action with_routing do |set| - set.draw do |map| - map.resource :account, :except => :destroy + set.draw do + resource :account, :except => :destroy end assert_singleton_resource_allowed_routes('accounts', {}, [:new, :create, :show, :edit, :update], :destroy) @@ -930,8 +852,8 @@ class ResourcesTest < ActionController::TestCase def test_resource_has_only_create_action_and_named_route with_routing do |set| - set.draw do |map| - map.resources :products, :only => :create + set.draw do + resources :products, :only => :create end assert_resource_allowed_routes('products', {}, { :id => '1' }, :create, [:index, :new, :show, :edit, :update, :destroy]) @@ -943,8 +865,8 @@ class ResourcesTest < ActionController::TestCase def test_resource_has_only_update_action_and_named_route with_routing do |set| - set.draw do |map| - map.resources :products, :only => :update + set.draw do + resources :products, :only => :update end assert_resource_allowed_routes('products', {}, { :id => '1' }, :update, [:index, :new, :create, :show, :edit, :destroy]) @@ -956,8 +878,8 @@ class ResourcesTest < ActionController::TestCase def test_resource_has_only_destroy_action_and_named_route with_routing do |set| - set.draw do |map| - map.resources :products, :only => :destroy + set.draw do + resources :products, :only => :destroy end assert_resource_allowed_routes('products', {}, { :id => '1' }, :destroy, [:index, :new, :create, :show, :edit, :update]) @@ -969,8 +891,8 @@ class ResourcesTest < ActionController::TestCase def test_singleton_resource_has_only_create_action_and_named_route with_routing do |set| - set.draw do |map| - map.resource :account, :only => :create + set.draw do + resource :account, :only => :create end assert_singleton_resource_allowed_routes('accounts', {}, :create, [:new, :show, :edit, :update, :destroy]) @@ -982,8 +904,8 @@ class ResourcesTest < ActionController::TestCase def test_singleton_resource_has_only_update_action_and_named_route with_routing do |set| - set.draw do |map| - map.resource :account, :only => :update + set.draw do + resource :account, :only => :update end assert_singleton_resource_allowed_routes('accounts', {}, :update, [:new, :create, :show, :edit, :destroy]) @@ -995,8 +917,8 @@ class ResourcesTest < ActionController::TestCase def test_singleton_resource_has_only_destroy_action_and_named_route with_routing do |set| - set.draw do |map| - map.resource :account, :only => :destroy + set.draw do + resource :account, :only => :destroy end assert_singleton_resource_allowed_routes('accounts', {}, :destroy, [:new, :create, :show, :edit, :update]) @@ -1008,8 +930,10 @@ class ResourcesTest < ActionController::TestCase def test_resource_has_only_collection_action with_routing do |set| - set.draw do |map| - map.resources :products, :except => :all, :collection => { :sale => :get } + set.draw do + resources :products, :only => [] do + get :sale, :on => :collection + end end assert_resource_allowed_routes('products', {}, { :id => '1' }, [], [:index, :new, :create, :show, :edit, :update, :destroy]) @@ -1022,8 +946,10 @@ class ResourcesTest < ActionController::TestCase def test_resource_has_only_member_action with_routing do |set| - set.draw do |map| - map.resources :products, :except => :all, :member => { :preview => :get } + set.draw do + resources :products, :only => [] do + get :preview, :on => :member + end end assert_resource_allowed_routes('products', {}, { :id => '1' }, [], [:index, :new, :create, :show, :edit, :update, :destroy]) @@ -1036,8 +962,12 @@ class ResourcesTest < ActionController::TestCase def test_singleton_resource_has_only_member_action with_routing do |set| - set.draw do |map| - map.resource :account, :except => :all, :member => { :signup => :get } + set.draw do + resource :account, :only => [] do + member do + get :signup + end + end end assert_singleton_resource_allowed_routes('accounts', {}, [], [:new, :create, :show, :edit, :update, :destroy]) @@ -1050,9 +980,11 @@ class ResourcesTest < ActionController::TestCase def test_nested_resource_has_only_show_and_member_action with_routing do |set| - set.draw do |map| - map.resources :products, :only => [:index, :show] do |product| - product.resources :images, :member => { :thumbnail => :get }, :only => :show + set.draw do + resources :products, :only => [:index, :show] do + resources :images, :only => :show do + get :thumbnail, :on => :member + end end end @@ -1066,9 +998,9 @@ class ResourcesTest < ActionController::TestCase def test_nested_resource_does_not_inherit_only_option with_routing do |set| - set.draw do |map| - map.resources :products, :only => :show do |product| - product.resources :images, :except => :destroy + set.draw do + resources :products, :only => :show do + resources :images, :except => :destroy end end @@ -1079,9 +1011,9 @@ class ResourcesTest < ActionController::TestCase def test_nested_resource_does_not_inherit_only_option_by_default with_routing do |set| - set.draw do |map| - map.resources :products, :only => :show do |product| - product.resources :images + set.draw do + resources :products, :only => :show do + resources :images end end @@ -1092,9 +1024,9 @@ class ResourcesTest < ActionController::TestCase def test_nested_resource_does_not_inherit_except_option with_routing do |set| - set.draw do |map| - map.resources :products, :except => :show do |product| - product.resources :images, :only => :destroy + set.draw do + resources :products, :except => :show do + resources :images, :only => :destroy end end @@ -1105,9 +1037,9 @@ class ResourcesTest < ActionController::TestCase def test_nested_resource_does_not_inherit_except_option_by_default with_routing do |set| - set.draw do |map| - map.resources :products, :except => :show do |product| - product.resources :images + set.draw do + resources :products, :except => :show do + resources :images end end @@ -1118,8 +1050,8 @@ class ResourcesTest < ActionController::TestCase def test_default_singleton_restful_route_uses_get with_routing do |set| - set.draw do |map| - map.resource :product + set.draw do + resource :product end assert_routing '/product', :controller => 'products', :action => 'show' @@ -1135,15 +1067,41 @@ class ResourcesTest < ActionController::TestCase protected def with_restful_routing(*args) + options = args.extract_options! + collection_methods = options.delete(:collection) + member_methods = options.delete(:member) + path_prefix = options.delete(:path_prefix) + args.push(options) + with_routing do |set| - set.draw { |map| map.resources(*args) } + set.draw do + scope(path_prefix || '') do + resources(*args) do + if collection_methods + collection do + collection_methods.each do |name, method| + send(method, name) + end + end + end + + if member_methods + member do + member_methods.each do |name, method| + send(method, name) + end + end + end + end + end + end yield end end def with_singleton_resources(*args) with_routing do |set| - set.draw { |map| map.resource(*args) } + set.draw {resource(*args) } yield end end @@ -1385,7 +1343,7 @@ class ResourcesTest < ActionController::TestCase end def distinct_routes? (r1, r2) - if r1.conditions == r2.conditions and r1.requirements == r2.requirements then + if r1.conditions == r2.conditions and r1.constraints == r2.constraints then if r1.segments.collect(&:to_s) == r2.segments.collect(&:to_s) then return false end diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index fc85b01394..18cf944f46 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -2,6 +2,7 @@ require 'abstract_unit' require 'controller/fake_controllers' require 'active_support/dependencies' +require 'active_support/core_ext/object/with_options' class MilestonesController < ActionController::Base def index() head :ok end @@ -9,14 +10,22 @@ class MilestonesController < ActionController::Base def rescue_action(e) raise e end end -ROUTING = ActionController::Routing +ROUTING = ActionDispatch::Routing + +module RoutingTestHelpers + def url_for(set, options, recall = nil) + set.send(:url_for, options.merge(:only_path => true, :_path_segments => recall)) + end +end # See RFC 3986, section 3.3 for allowed path characters. class UriReservedCharactersRoutingTest < Test::Unit::TestCase + include RoutingTestHelpers + def setup - @set = ActionController::Routing::RouteSet.new - @set.draw do |map| - map.connect ':controller/:action/:variable/*additional' + @set = ActionDispatch::Routing::RouteSet.new + @set.draw do + match ':controller/:action/:variable/*additional' end safe, unsafe = %w(: @ & = + $ , ;), %w(^ ? # [ ]) @@ -27,27 +36,31 @@ class UriReservedCharactersRoutingTest < Test::Unit::TestCase end def test_route_generation_escapes_unsafe_path_characters - @set.generate(:controller => "content", :action => "act#{@segment}ion", :variable => "variable", :additional => "foo") assert_equal "/content/act#{@escaped}ion/var#{@escaped}iable/add#{@escaped}itional-1/add#{@escaped}itional-2", - @set.generate(:controller => "content", - :action => "act#{@segment}ion", - :variable => "var#{@segment}iable", - :additional => ["add#{@segment}itional-1", "add#{@segment}itional-2"]) + url_for(@set, { + :controller => "content", + :action => "act#{@segment}ion", + :variable => "var#{@segment}iable", + :additional => ["add#{@segment}itional-1", "add#{@segment}itional-2"] + }) end def test_route_recognition_unescapes_path_components options = { :controller => "content", :action => "act#{@segment}ion", :variable => "var#{@segment}iable", - :additional => ["add#{@segment}itional-1", "add#{@segment}itional-2"] } + :additional => "add#{@segment}itional-1/add#{@segment}itional-2" } assert_equal options, @set.recognize_path("/content/act#{@escaped}ion/var#{@escaped}iable/add#{@escaped}itional-1/add#{@escaped}itional-2") end def test_route_generation_allows_passing_non_string_values_to_generated_helper - assert_equal "/content/action/variable/1/2", @set.generate(:controller => "content", - :action => "action", - :variable => "variable", - :additional => [1, 2]) + assert_equal "/content/action/variable/1/2", + url_for(@set, { + :controller => "content", + :action => "action", + :variable => "variable", + :additional => [1, 2] + }) end end @@ -67,10 +80,12 @@ class MockController end class LegacyRouteSetTests < Test::Unit::TestCase + include RoutingTestHelpers + attr_reader :rs def setup - @rs = ::ActionController::Routing::RouteSet.new + @rs = ::ActionDispatch::Routing::RouteSet.new end def teardown @@ -78,80 +93,83 @@ class LegacyRouteSetTests < Test::Unit::TestCase end def test_default_setup - @rs.draw {|m| m.connect ':controller/:action/:id' } + @rs.draw { match '/:controller(/:action(/:id))' } assert_equal({:controller => "content", :action => 'index'}, rs.recognize_path("/content")) - assert_equal({:controller => "content", :action => 'list'}, rs.recognize_path("/content/list")) + assert_equal({:controller => "content", :action => 'list'}, rs.recognize_path("/content/list")) assert_equal({:controller => "content", :action => 'show', :id => '10'}, rs.recognize_path("/content/show/10")) assert_equal({:controller => "admin/user", :action => 'show', :id => '10'}, rs.recognize_path("/admin/user/show/10")) - assert_equal '/admin/user/show/10', rs.generate(:controller => 'admin/user', :action => 'show', :id => 10) + assert_equal '/admin/user/show/10', url_for(rs, { :controller => 'admin/user', :action => 'show', :id => 10 }) - assert_equal '/admin/user/show', rs.generate({:action => 'show'}, {:controller => 'admin/user', :action => 'list', :id => '10'}) - assert_equal '/admin/user/list/10', rs.generate({}, {:controller => 'admin/user', :action => 'list', :id => '10'}) + assert_equal '/admin/user/show', url_for(rs, { :action => 'show' }, { :controller => 'admin/user', :action => 'list', :id => '10' }) + assert_equal '/admin/user/list/10', url_for(rs, {}, { :controller => 'admin/user', :action => 'list', :id => '10' }) - assert_equal '/admin/stuff', rs.generate({:controller => 'stuff'}, {:controller => 'admin/user', :action => 'list', :id => '10'}) - assert_equal '/stuff', rs.generate({:controller => '/stuff'}, {:controller => 'admin/user', :action => 'list', :id => '10'}) + assert_equal '/admin/stuff', url_for(rs, { :controller => 'stuff' }, { :controller => 'admin/user', :action => 'list', :id => '10' }) + assert_equal '/stuff', url_for(rs, { :controller => '/stuff' }, { :controller => 'admin/user', :action => 'list', :id => '10' }) end def test_ignores_leading_slash @rs.clear! - @rs.draw {|m| m.connect '/:controller/:action/:id'} + @rs.draw { match '/:controller(/:action(/:id))'} test_default_setup end def test_time_recognition # We create many routes to make situation more realistic - @rs = ::ActionController::Routing::RouteSet.new - @rs.draw { |map| - map.frontpage '', :controller => 'search', :action => 'new' - map.resources :videos do |video| - video.resources :comments - video.resource :file, :controller => 'video_file' - video.resource :share, :controller => 'video_shares' - video.resource :abuse, :controller => 'video_abuses' + @rs = ::ActionDispatch::Routing::RouteSet.new + @rs.draw { + root :to => "search#new", :as => "frontpage" + resources :videos do + resources :comments + resource :file, :controller => 'video_file' + resource :share, :controller => 'video_shares' + resource :abuse, :controller => 'video_abuses' end - map.resources :abuses, :controller => 'video_abuses' - map.resources :video_uploads - map.resources :video_visits + resources :abuses, :controller => 'video_abuses' + resources :video_uploads + resources :video_visits - map.resources :users do |user| - user.resource :settings - user.resources :videos + resources :users do + resource :settings + resources :videos end - map.resources :channels do |channel| - channel.resources :videos, :controller => 'channel_videos' + resources :channels do + resources :videos, :controller => 'channel_videos' end - map.resource :session - map.resource :lost_password - map.search 'search', :controller => 'search' - map.resources :pages - map.connect ':controller/:action/:id' + resource :session + resource :lost_password + match 'search' => 'search#index', :as => 'search' + resources :pages + match ':controller/:action/:id' } end def test_route_with_colon_first - rs.draw do |map| - map.connect '/:controller/:action/:id', :action => 'index', :id => nil - map.connect ':url', :controller => 'tiny_url', :action => 'translate' + rs.draw do + match '/:controller/:action/:id', :action => 'index', :id => nil + match ':url', :controller => 'tiny_url', :action => 'translate' end end def test_route_with_regexp_for_controller - rs.draw do |map| - map.connect ':controller/:admintoken/:action/:id', :controller => /admin\/.+/ - map.connect ':controller/:action/:id' + rs.draw do + match ':controller/:admintoken(/:action(/:id))', :controller => /admin\/.+/ + match '/:controller(/:action(/:id))' end + assert_equal({:controller => "admin/user", :admintoken => "foo", :action => "index"}, rs.recognize_path("/admin/user/foo")) - assert_equal({:controller => "content", :action => "foo"}, rs.recognize_path("/content/foo")) - assert_equal '/admin/user/foo', rs.generate(:controller => "admin/user", :admintoken => "foo", :action => "index") - assert_equal '/content/foo', rs.generate(:controller => "content", :action => "foo") + assert_equal({:controller => "content", :action => "foo"}, + rs.recognize_path("/content/foo")) + + assert_equal '/admin/user/foo', url_for(rs, { :controller => "admin/user", :admintoken => "foo", :action => "index" }) + assert_equal '/content/foo', url_for(rs, { :controller => "content", :action => "foo" }) end def test_route_with_regexp_and_captures_for_controller - rs.draw do |map| - map.connect ':controller/:action/:id', :controller => /admin\/(accounts|users)/ + rs.draw do + match '/:controller(/:action(/:id))', :controller => /admin\/(accounts|users)/ end assert_equal({:controller => "admin/accounts", :action => "index"}, rs.recognize_path("/admin/accounts")) assert_equal({:controller => "admin/users", :action => "index"}, rs.recognize_path("/admin/users")) @@ -159,100 +177,97 @@ class LegacyRouteSetTests < Test::Unit::TestCase end def test_route_with_regexp_and_dot - rs.draw do |map| - map.connect ':controller/:action/:file', - :controller => /admin|user/, - :action => /upload|download/, - :defaults => {:file => nil}, - :requirements => {:file => %r{[^/]+(\.[^/]+)?}} + rs.draw do + match ':controller/:action/:file', + :controller => /admin|user/, + :action => /upload|download/, + :defaults => {:file => nil}, + :constraints => {:file => %r{[^/]+(\.[^/]+)?}} end # Without a file extension assert_equal '/user/download/file', - rs.generate(:controller => "user", :action => "download", :file => "file") - assert_equal( - {:controller => "user", :action => "download", :file => "file"}, + url_for(rs, { :controller => "user", :action => "download", :file => "file" }) + + assert_equal({:controller => "user", :action => "download", :file => "file"}, rs.recognize_path("/user/download/file")) # Now, let's try a file with an extension, really a dot (.) assert_equal '/user/download/file.jpg', - rs.generate( - :controller => "user", :action => "download", :file => "file.jpg") - assert_equal( - {:controller => "user", :action => "download", :file => "file.jpg"}, + url_for(rs, { :controller => "user", :action => "download", :file => "file.jpg" }) + + assert_equal({:controller => "user", :action => "download", :file => "file.jpg"}, rs.recognize_path("/user/download/file.jpg")) end def test_basic_named_route - rs.draw do |map| - map.home '', :controller => 'content', :action => 'list' + rs.draw do + root :to => 'content#list', :as => 'home' end - x = setup_for_named_route - assert_equal("http://test.host/", - x.send(:home_url)) + assert_equal("http://test.host/", setup_for_named_route.send(:home_url)) end def test_named_route_with_option - rs.draw do |map| - map.page 'page/:title', :controller => 'content', :action => 'show_page' + rs.draw do + match 'page/:title' => 'content#show_page', :as => 'page' end - x = setup_for_named_route + assert_equal("http://test.host/page/new%20stuff", - x.send(:page_url, :title => 'new stuff')) + setup_for_named_route.send(:page_url, :title => 'new stuff')) end def test_named_route_with_default - rs.draw do |map| - map.page 'page/:title', :controller => 'content', :action => 'show_page', :title => 'AboutPage' + rs.draw do + match 'page/:title' => 'content#show_page', :title => 'AboutPage', :as => 'page' end - x = setup_for_named_route - assert_equal("http://test.host/page/AboutRails", - x.send(:page_url, :title => "AboutRails")) - end - - def test_named_route_with_name_prefix - rs.draw do |map| - map.page 'page', :controller => 'content', :action => 'show_page', :name_prefix => 'my_' - end - x = setup_for_named_route - assert_equal("http://test.host/page", - x.send(:my_page_url)) + assert_equal("http://test.host/page/AboutRails", + setup_for_named_route.send(:page_url, :title => "AboutRails")) end def test_named_route_with_path_prefix - rs.draw do |map| - map.page 'page', :controller => 'content', :action => 'show_page', :path_prefix => 'my' + rs.draw do + scope "my" do + match 'page' => 'content#show_page', :as => 'page' + end end - x = setup_for_named_route + assert_equal("http://test.host/my/page", - x.send(:page_url)) + setup_for_named_route.send(:page_url)) end def test_named_route_with_blank_path_prefix - rs.draw do |map| - map.page 'page', :controller => 'content', :action => 'show_page', :path_prefix => '' + rs.draw do + scope "" do + match 'page' => 'content#show_page', :as => 'page' + end end - x = setup_for_named_route + assert_equal("http://test.host/page", - x.send(:page_url)) + setup_for_named_route.send(:page_url)) end def test_named_route_with_nested_controller - rs.draw do |map| - map.users 'admin/user', :controller => 'admin/user', :action => 'index' + rs.draw do + match 'admin/user' => 'admin/user#index', :as => "users" end - x = setup_for_named_route + assert_equal("http://test.host/admin/user", - x.send(:users_url)) + setup_for_named_route.send(:users_url)) end def test_optimised_named_route_with_host - rs.draw do |map| - map.pages 'pages', :controller => 'content', :action => 'show_page', :host => 'foo.com' + rs.draw do + match 'page' => 'content#show_page', :as => 'pages', :host => 'foo.com' end - x = setup_for_named_route - x.expects(:url_for).with(:host => 'foo.com', :only_path => false, :controller => 'content', :action => 'show_page', :use_route => :pages).once - x.send(:pages_url) + routes = setup_for_named_route + routes.expects(:url_for).with({ + :host => 'foo.com', + :only_path => false, + :controller => 'content', + :action => 'show_page', + :use_route => 'pages' + }).once + routes.send(:pages_url) end def setup_for_named_route @@ -260,263 +275,229 @@ class LegacyRouteSetTests < Test::Unit::TestCase end def test_named_route_without_hash - rs.draw do |map| - map.normal ':controller/:action/:id' + rs.draw do + match ':controller/:action/:id', :as => 'normal' end end def test_named_route_root - rs.draw do |map| - map.root :controller => "hello" + rs.draw do + root :to => "hello#index" end - x = setup_for_named_route - assert_equal("http://test.host/", x.send(:root_url)) - assert_equal("/", x.send(:root_path)) + routes = setup_for_named_route + assert_equal("http://test.host/", routes.send(:root_url)) + assert_equal("/", routes.send(:root_path)) end def test_named_route_with_regexps - rs.draw do |map| - map.article 'page/:year/:month/:day/:title', :controller => 'page', :action => 'show', + rs.draw do + match 'page/:year/:month/:day/:title' => 'page#show', :as => 'article', :year => /\d+/, :month => /\d+/, :day => /\d+/ - map.connect ':controller/:action/:id' + match ':controller/:action/:id' end - x = setup_for_named_route - # assert_equal( - # {:controller => 'page', :action => 'show', :title => 'hi', :use_route => :article, :only_path => false}, - # x.send(:article_url, :title => 'hi') - # ) - assert_equal( - "http://test.host/page/2005/6/10/hi", - x.send(:article_url, :title => 'hi', :day => 10, :year => 2005, :month => 6) - ) + + routes = setup_for_named_route + + assert_equal "http://test.host/page/2005/6/10/hi", + routes.send(:article_url, :title => 'hi', :day => 10, :year => 2005, :month => 6) end def test_changing_controller - @rs.draw {|m| m.connect ':controller/:action/:id' } + @rs.draw { match ':controller/:action/:id' } - assert_equal '/admin/stuff/show/10', rs.generate( - {:controller => 'stuff', :action => 'show', :id => 10}, - {:controller => 'admin/user', :action => 'index'} - ) + assert_equal '/admin/stuff/show/10', + url_for(rs, {:controller => 'stuff', :action => 'show', :id => 10}, + {:controller => 'admin/user', :action => 'index'}) end def test_paths_escaped - rs.draw do |map| - map.path 'file/*path', :controller => 'content', :action => 'show_file' - map.connect ':controller/:action/:id' + rs.draw do + match 'file/*path' => 'content#show_file', :as => 'path' + match ':controller/:action/:id' end # No + to space in URI escaping, only for query params. results = rs.recognize_path "/file/hello+world/how+are+you%3F" assert results, "Recognition should have succeeded" - assert_equal ['hello+world', 'how+are+you?'], results[:path] + assert_equal 'hello+world/how+are+you?', results[:path] # Use %20 for space instead. results = rs.recognize_path "/file/hello%20world/how%20are%20you%3F" assert results, "Recognition should have succeeded" - assert_equal ['hello world', 'how are you?'], results[:path] + assert_equal 'hello world/how are you?', results[:path] end def test_paths_slashes_unescaped_with_ordered_parameters - rs.draw do |map| - map.path '/file/*path', :controller => 'content' + rs.draw do + match '/file/*path' => 'content#index', :as => 'path' end # No / to %2F in URI, only for query params. - x = setup_for_named_route - assert_equal("/file/hello/world", x.send(:path_path, ['hello', 'world'])) + assert_equal("/file/hello/world", setup_for_named_route.send(:path_path, ['hello', 'world'])) end def test_non_controllers_cannot_be_matched - rs.draw do |map| - map.connect ':controller/:action/:id' + rs.draw do + match ':controller/:action/:id' end assert_raise(ActionController::RoutingError) { rs.recognize_path("/not_a/show/10") } end - def test_paths_do_not_accept_defaults - assert_raise(ActionController::RoutingError) do - rs.draw do |map| - map.path 'file/*path', :controller => 'content', :action => 'show_file', :path => %w(fake default) - map.connect ':controller/:action/:id' - end - end - - rs.draw do |map| - map.path 'file/*path', :controller => 'content', :action => 'show_file', :path => [] - map.connect ':controller/:action/:id' + def test_should_list_options_diff_when_routing_constraints_dont_match + rs.draw do + match 'post/:id' => 'post#show', :constraints => { :id => /\d+/ }, :as => 'post' end - end - - def test_should_list_options_diff_when_routing_requirements_dont_match - rs.draw do |map| - map.post 'post/:id', :controller=> 'post', :action=> 'show', :requirements => {:id => /\d+/} + assert_raise(ActionController::RoutingError) do + url_for(rs, { :controller => 'post', :action => 'show', :bad_param => "foo", :use_route => "post" }) end - assert_raise(ActionController::RoutingError) { rs.generate(:controller => 'post', :action => 'show', :bad_param => "foo", :use_route => "post") } end def test_dynamic_path_allowed - rs.draw do |map| - map.connect '*path', :controller => 'content', :action => 'show_file' + rs.draw do + match '*path' => 'content#show_file' end - assert_equal '/pages/boo', rs.generate(:controller => 'content', :action => 'show_file', :path => %w(pages boo)) + assert_equal '/pages/boo', + url_for(rs, { :controller => 'content', :action => 'show_file', :path => %w(pages boo) }) end def test_dynamic_recall_paths_allowed - rs.draw do |map| - map.connect '*path', :controller => 'content', :action => 'show_file' + rs.draw do + match '*path' => 'content#show_file' end - assert_equal '/pages/boo', rs.generate({}, :controller => 'content', :action => 'show_file', :path => %w(pages boo)) + assert_equal '/pages/boo', + url_for(rs, {}, { :controller => 'content', :action => 'show_file', :path => %w(pages boo) }) end def test_backwards - rs.draw do |map| - map.connect 'page/:id/:action', :controller => 'pages', :action => 'show' - map.connect ':controller/:action/:id' + rs.draw do + match 'page/:id(/:action)' => 'pages#show' + match ':controller(/:action(/:id))' end - assert_equal '/page/20', rs.generate({:id => 20}, {:controller => 'pages', :action => 'show'}) - assert_equal '/page/20', rs.generate(:controller => 'pages', :id => 20, :action => 'show') - assert_equal '/pages/boo', rs.generate(:controller => 'pages', :action => 'boo') + assert_equal '/page/20', url_for(rs, { :id => 20 }, { :controller => 'pages', :action => 'show' }) + assert_equal '/page/20', url_for(rs, { :controller => 'pages', :id => 20, :action => 'show' }) + assert_equal '/pages/boo', url_for(rs, { :controller => 'pages', :action => 'boo' }) end def test_route_with_fixnum_default - rs.draw do |map| - map.connect 'page/:id', :controller => 'content', :action => 'show_page', :id => 1 - map.connect ':controller/:action/:id' + rs.draw do + match 'page(/:id)' => 'content#show_page', :id => 1 + match ':controller/:action/:id' end - assert_equal '/page', rs.generate(:controller => 'content', :action => 'show_page') - assert_equal '/page', rs.generate(:controller => 'content', :action => 'show_page', :id => 1) - assert_equal '/page', rs.generate(:controller => 'content', :action => 'show_page', :id => '1') - assert_equal '/page/10', rs.generate(:controller => 'content', :action => 'show_page', :id => 10) + assert_equal '/page', url_for(rs, { :controller => 'content', :action => 'show_page' }) + assert_equal '/page', url_for(rs, { :controller => 'content', :action => 'show_page', :id => 1 }) + assert_equal '/page', url_for(rs, { :controller => 'content', :action => 'show_page', :id => '1' }) + assert_equal '/page/10', url_for(rs, { :controller => 'content', :action => 'show_page', :id => 10 }) - assert_equal({:controller => "content", :action => 'show_page', :id => '1'}, rs.recognize_path("/page")) + assert_equal({:controller => "content", :action => 'show_page', :id => 1 }, rs.recognize_path("/page")) assert_equal({:controller => "content", :action => 'show_page', :id => '1'}, rs.recognize_path("/page/1")) assert_equal({:controller => "content", :action => 'show_page', :id => '10'}, rs.recognize_path("/page/10")) end # For newer revision def test_route_with_text_default - rs.draw do |map| - map.connect 'page/:id', :controller => 'content', :action => 'show_page', :id => 1 - map.connect ':controller/:action/:id' + rs.draw do + match 'page/:id' => 'content#show_page', :id => 1 + match ':controller/:action/:id' end - assert_equal '/page/foo', rs.generate(:controller => 'content', :action => 'show_page', :id => 'foo') - assert_equal({:controller => "content", :action => 'show_page', :id => 'foo'}, rs.recognize_path("/page/foo")) + assert_equal '/page/foo', url_for(rs, { :controller => 'content', :action => 'show_page', :id => 'foo' }) + assert_equal({ :controller => "content", :action => 'show_page', :id => 'foo' }, rs.recognize_path("/page/foo")) - token = "\321\202\320\265\320\272\321\201\321\202" # 'text' in russian + token = "\321\202\320\265\320\272\321\201\321\202" # 'text' in Russian token.force_encoding(Encoding::BINARY) if token.respond_to?(:force_encoding) escaped_token = CGI::escape(token) - assert_equal '/page/' + escaped_token, rs.generate(:controller => 'content', :action => 'show_page', :id => token) - assert_equal({:controller => "content", :action => 'show_page', :id => token}, rs.recognize_path("/page/#{escaped_token}")) + assert_equal '/page/' + escaped_token, url_for(rs, { :controller => 'content', :action => 'show_page', :id => token }) + assert_equal({ :controller => "content", :action => 'show_page', :id => token }, rs.recognize_path("/page/#{escaped_token}")) end def test_action_expiry - @rs.draw {|m| m.connect ':controller/:action/:id' } - assert_equal '/content', rs.generate({:controller => 'content'}, {:controller => 'content', :action => 'show'}) + @rs.draw { match ':controller(/:action(/:id))' } + assert_equal '/content', url_for(rs, { :controller => 'content' }, { :controller => 'content', :action => 'show' }) end def test_requirement_should_prevent_optional_id - rs.draw do |map| - map.post 'post/:id', :controller=> 'post', :action=> 'show', :requirements => {:id => /\d+/} + rs.draw do + match 'post/:id' => 'post#show', :constraints => {:id => /\d+/}, :as => 'post' end - assert_equal '/post/10', rs.generate(:controller => 'post', :action => 'show', :id => 10) + assert_equal '/post/10', url_for(rs, { :controller => 'post', :action => 'show', :id => 10 }) assert_raise ActionController::RoutingError do - rs.generate(:controller => 'post', :action => 'show') + url_for(rs, { :controller => 'post', :action => 'show' }) end end def test_both_requirement_and_optional - rs.draw do |map| - map.blog('test/:year', :controller => 'post', :action => 'show', + rs.draw do + match('test(/:year)' => 'post#show', :as => 'blog', :defaults => { :year => nil }, - :requirements => { :year => /\d{4}/ } + :constraints => { :year => /\d{4}/ } ) - map.connect ':controller/:action/:id' + match ':controller/:action/:id' end - assert_equal '/test', rs.generate(:controller => 'post', :action => 'show') - assert_equal '/test', rs.generate(:controller => 'post', :action => 'show', :year => nil) + assert_equal '/test', url_for(rs, { :controller => 'post', :action => 'show' }) + assert_equal '/test', url_for(rs, { :controller => 'post', :action => 'show', :year => nil }) - x = setup_for_named_route - assert_equal("http://test.host/test", - x.send(:blog_url)) + assert_equal("http://test.host/test", setup_for_named_route.send(:blog_url)) end def test_set_to_nil_forgets - rs.draw do |map| - map.connect 'pages/:year/:month/:day', :controller => 'content', :action => 'list_pages', :month => nil, :day => nil - map.connect ':controller/:action/:id' + rs.draw do + match 'pages(/:year(/:month(/:day)))' => 'content#list_pages', :month => nil, :day => nil + match ':controller/:action/:id' end assert_equal '/pages/2005', - rs.generate(:controller => 'content', :action => 'list_pages', :year => 2005) + url_for(rs, { :controller => 'content', :action => 'list_pages', :year => 2005 }) assert_equal '/pages/2005/6', - rs.generate(:controller => 'content', :action => 'list_pages', :year => 2005, :month => 6) + url_for(rs, { :controller => 'content', :action => 'list_pages', :year => 2005, :month => 6 }) assert_equal '/pages/2005/6/12', - rs.generate(:controller => 'content', :action => 'list_pages', :year => 2005, :month => 6, :day => 12) + url_for(rs, { :controller => 'content', :action => 'list_pages', :year => 2005, :month => 6, :day => 12 }) assert_equal '/pages/2005/6/4', - rs.generate({:day => 4}, {:controller => 'content', :action => 'list_pages', :year => '2005', :month => '6', :day => '12'}) + url_for(rs, { :day => 4 }, { :controller => 'content', :action => 'list_pages', :year => '2005', :month => '6', :day => '12' }) assert_equal '/pages/2005/6', - rs.generate({:day => nil}, {:controller => 'content', :action => 'list_pages', :year => '2005', :month => '6', :day => '12'}) + url_for(rs, { :day => nil }, { :controller => 'content', :action => 'list_pages', :year => '2005', :month => '6', :day => '12' }) assert_equal '/pages/2005', - rs.generate({:day => nil, :month => nil}, {:controller => 'content', :action => 'list_pages', :year => '2005', :month => '6', :day => '12'}) + url_for(rs, { :day => nil, :month => nil }, { :controller => 'content', :action => 'list_pages', :year => '2005', :month => '6', :day => '12' }) end - def test_url_with_no_action_specified - rs.draw do |map| - map.connect '', :controller => 'content' - map.connect ':controller/:action/:id' + def test_root_url_generation_with_controller_and_action + rs.draw do + root :to => "content#index" end - assert_equal '/', rs.generate(:controller => 'content', :action => 'index') - assert_equal '/', rs.generate(:controller => 'content') + assert_equal '/', url_for(rs, { :controller => 'content', :action => 'index' }) + assert_equal '/', url_for(rs, { :controller => 'content' }) end - def test_named_url_with_no_action_specified - rs.draw do |map| - map.home '', :controller => 'content' - map.connect ':controller/:action/:id' + def test_named_root_url_generation_with_controller_and_action + rs.draw do + root :to => "content#index", :as => 'home' end - assert_equal '/', rs.generate(:controller => 'content', :action => 'index') - assert_equal '/', rs.generate(:controller => 'content') + assert_equal '/', url_for(rs, { :controller => 'content', :action => 'index' }) + assert_equal '/', url_for(rs, { :controller => 'content' }) - x = setup_for_named_route - assert_equal("http://test.host/", - x.send(:home_url)) - end - - def test_url_generated_when_forgetting_action - [{:controller => 'content', :action => 'index'}, {:controller => 'content'}].each do |hash| - rs.draw do |map| - map.home '', hash - map.connect ':controller/:action/:id' - end - assert_equal '/', rs.generate({:action => nil}, {:controller => 'content', :action => 'hello'}) - assert_equal '/', rs.generate({:controller => 'content'}) - assert_equal '/content/hi', rs.generate({:controller => 'content', :action => 'hi'}) - end + assert_equal("http://test.host/", setup_for_named_route.send(:home_url)) end def test_named_route_method - rs.draw do |map| - map.categories 'categories', :controller => 'content', :action => 'categories' - map.connect ':controller/:action/:id' + rs.draw do + match 'categories' => 'content#categories', :as => 'categories' + match ':controller(/:action(/:id))' end - assert_equal '/categories', rs.generate(:controller => 'content', :action => 'categories') - assert_equal '/content/hi', rs.generate({:controller => 'content', :action => 'hi'}) + assert_equal '/categories', url_for(rs, { :controller => 'content', :action => 'categories' }) + assert_equal '/content/hi', url_for(rs, { :controller => 'content', :action => 'hi' }) end def test_named_routes_array @@ -525,23 +506,26 @@ class LegacyRouteSetTests < Test::Unit::TestCase end def test_nil_defaults - rs.draw do |map| - map.connect 'journal', - :controller => 'content', - :action => 'list_journal', + rs.draw do + match 'journal' => 'content#list_journal', :date => nil, :user_id => nil - map.connect ':controller/:action/:id' + match ':controller/:action/:id' end - assert_equal '/journal', rs.generate(:controller => 'content', :action => 'list_journal', :date => nil, :user_id => nil) + assert_equal '/journal', url_for(rs, { + :controller => 'content', + :action => 'list_journal', + :date => nil, + :user_id => nil + }) end def setup_request_method_routes_for(method) - rs.draw do |r| - r.connect '/match', :controller => 'books', :action => 'get', :conditions => { :method => :get } - r.connect '/match', :controller => 'books', :action => 'post', :conditions => { :method => :post } - r.connect '/match', :controller => 'books', :action => 'put', :conditions => { :method => :put } - r.connect '/match', :controller => 'books', :action => 'delete', :conditions => { :method => :delete } + rs.draw do + match '/match' => 'books#get', :via => :get + match '/match' => 'books#post', :via => :post + match '/match' => 'books#put', :via => :put + match '/match' => 'books#delete', :via => :delete end end @@ -554,9 +538,9 @@ class LegacyRouteSetTests < Test::Unit::TestCase end def test_recognize_array_of_methods - rs.draw do |r| - r.connect '/match', :controller => 'books', :action => 'get_or_post', :conditions => { :method => [:get, :post] } - r.connect '/match', :controller => 'books', :action => 'not_get_or_post' + rs.draw do + match '/match' => 'books#get_or_post', :via => [:get, :post] + match '/match' => 'books#not_get_or_post' end params = rs.recognize_path("/match", :method => :post) @@ -567,11 +551,11 @@ class LegacyRouteSetTests < Test::Unit::TestCase end def test_subpath_recognized - rs.draw do |r| - r.connect '/books/:id/edit', :controller => 'subpath_books', :action => 'edit' - r.connect '/items/:id/:action', :controller => 'subpath_books' - r.connect '/posts/new/:action', :controller => 'subpath_books' - r.connect '/posts/:id', :controller => 'subpath_books', :action => "show" + rs.draw do + match '/books/:id/edit' => 'subpath_books#edit' + match '/items/:id/:action' => 'subpath_books' + match '/posts/new/:action' => 'subpath_books' + match '/posts/:id' => 'subpath_books#show' end hash = rs.recognize_path "/books/17/edit" @@ -592,36 +576,35 @@ class LegacyRouteSetTests < Test::Unit::TestCase end def test_subpath_generated - rs.draw do |r| - r.connect '/books/:id/edit', :controller => 'subpath_books', :action => 'edit' - r.connect '/items/:id/:action', :controller => 'subpath_books' - r.connect '/posts/new/:action', :controller => 'subpath_books' + rs.draw do + match '/books/:id/edit' => 'subpath_books#edit' + match '/items/:id/:action' => 'subpath_books' + match '/posts/new/:action' => 'subpath_books' end - assert_equal "/books/7/edit", rs.generate(:controller => "subpath_books", :id => 7, :action => "edit") - assert_equal "/items/15/complete", rs.generate(:controller => "subpath_books", :id => 15, :action => "complete") - assert_equal "/posts/new/preview", rs.generate(:controller => "subpath_books", :action => "preview") + assert_equal "/books/7/edit", url_for(rs, { :controller => "subpath_books", :id => 7, :action => "edit" }) + assert_equal "/items/15/complete", url_for(rs, { :controller => "subpath_books", :id => 15, :action => "complete" }) + assert_equal "/posts/new/preview", url_for(rs, { :controller => "subpath_books", :action => "preview" }) end - def test_failed_requirements_raises_exception_with_violated_requirements - rs.draw do |r| - r.foo_with_requirement 'foos/:id', :controller=>'foos', :requirements=>{:id=>/\d+/} + def test_failed_constraints_raises_exception_with_violated_constraints + rs.draw do + match 'foos/:id' => 'foos#show', :as => 'foo_with_requirement', :constraints => { :id => /\d+/ } end - x = setup_for_named_route assert_raise(ActionController::RoutingError) do - x.send(:foo_with_requirement_url, "I am Against the requirements") + setup_for_named_route.send(:foo_with_requirement_url, "I am Against the constraints") end end def test_routes_changed_correctly_after_clear - rs = ::ActionController::Routing::RouteSet.new - rs.draw do |r| - r.connect 'ca', :controller => 'ca', :action => "aa" - r.connect 'cb', :controller => 'cb', :action => "ab" - r.connect 'cc', :controller => 'cc', :action => "ac" - r.connect ':controller/:action/:id' - r.connect ':controller/:action/:id.:format' + rs = ::ActionDispatch::Routing::RouteSet.new + rs.draw do + match 'ca' => 'ca#aa' + match 'cb' => 'cb#ab' + match 'cc' => 'cc#ac' + match ':controller/:action/:id' + match ':controller/:action/:id.:format' end hash = rs.recognize_path "/cc" @@ -629,22 +612,23 @@ class LegacyRouteSetTests < Test::Unit::TestCase assert_not_nil hash assert_equal %w(cc ac), [hash[:controller], hash[:action]] - rs.draw do |r| - r.connect 'cb', :controller => 'cb', :action => "ab" - r.connect 'cc', :controller => 'cc', :action => "ac" - r.connect ':controller/:action/:id' - r.connect ':controller/:action/:id.:format' + rs.draw do + match 'cb' => 'cb#ab' + match 'cc' => 'cc#ac' + match ':controller/:action/:id' + match ':controller/:action/:id.:format' end hash = rs.recognize_path "/cc" assert_not_nil hash assert_equal %w(cc ac), [hash[:controller], hash[:action]] - end end class RouteSetTest < ActiveSupport::TestCase + include RoutingTestHelpers + def set @set ||= ROUTING::RouteSet.new end @@ -656,30 +640,30 @@ class RouteSetTest < ActiveSupport::TestCase def default_route_set @default_route_set ||= begin set = ROUTING::RouteSet.new - set.draw do |map| - map.connect '/:controller/:action/:id/' + set.draw do + match '/:controller(/:action(/:id))' end set end end def test_generate_extras - set.draw { |m| m.connect ':controller/:action/:id' } + set.draw { match ':controller/(:action(/:id))' } path, extras = set.generate_extras(:controller => "foo", :action => "bar", :id => 15, :this => "hello", :that => "world") assert_equal "/foo/bar/15", path assert_equal %w(that this), extras.map { |e| e.to_s }.sort end def test_extra_keys - set.draw { |m| m.connect ':controller/:action/:id' } + set.draw { match ':controller/:action/:id' } extras = set.extra_keys(:controller => "foo", :action => "bar", :id => 15, :this => "hello", :that => "world") assert_equal %w(that this), extras.map { |e| e.to_s }.sort end def test_generate_extras_not_first - set.draw do |map| - map.connect ':controller/:action/:id.:format' - map.connect ':controller/:action/:id' + set.draw do + match ':controller/:action/:id.:format' + match ':controller/:action/:id' end path, extras = set.generate_extras(:controller => "foo", :action => "bar", :id => 15, :this => "hello", :that => "world") assert_equal "/foo/bar/15", path @@ -687,17 +671,18 @@ class RouteSetTest < ActiveSupport::TestCase end def test_generate_not_first - set.draw do |map| - map.connect ':controller/:action/:id.:format' - map.connect ':controller/:action/:id' + set.draw do + match ':controller/:action/:id.:format' + match ':controller/:action/:id' end - assert_equal "/foo/bar/15?this=hello", set.generate(:controller => "foo", :action => "bar", :id => 15, :this => "hello") + assert_equal "/foo/bar/15?this=hello", + url_for(set, { :controller => "foo", :action => "bar", :id => 15, :this => "hello" }) end def test_extra_keys_not_first - set.draw do |map| - map.connect ':controller/:action/:id.:format' - map.connect ':controller/:action/:id' + set.draw do + match ':controller/:action/:id.:format' + match ':controller/:action/:id' end extras = set.extra_keys(:controller => "foo", :action => "bar", :id => 15, :this => "hello", :that => "world") assert_equal %w(that this), extras.map { |e| e.to_s }.sort @@ -705,44 +690,44 @@ class RouteSetTest < ActiveSupport::TestCase def test_draw assert_equal 0, set.routes.size - set.draw do |map| - map.connect '/hello/world', :controller => 'a', :action => 'b' + set.draw do + match '/hello/world' => 'a#b' end assert_equal 1, set.routes.size end def test_draw_symbol_controller_name assert_equal 0, set.routes.size - set.draw do |map| - map.connect '/users/index', :controller => :users, :action => :index + set.draw do + match '/users/index' => 'users#index' end - params = set.recognize_path('/users/index', :method => :get) + set.recognize_path('/users/index', :method => :get) assert_equal 1, set.routes.size end def test_named_draw assert_equal 0, set.routes.size - set.draw do |map| - map.hello '/hello/world', :controller => 'a', :action => 'b' + set.draw do + match '/hello/world' => 'a#b', :as => 'hello' end assert_equal 1, set.routes.size assert_equal set.routes.first, set.named_routes[:hello] end def test_later_named_routes_take_precedence - set.draw do |map| - map.hello '/hello/world', :controller => 'a', :action => 'b' - map.hello '/hello', :controller => 'a', :action => 'b' + set.draw do + match '/hello/world' => 'a#b', :as => 'hello' + match '/hello' => 'a#b', :as => 'hello' end assert_equal set.routes.last, set.named_routes[:hello] end def setup_named_route_test - set.draw do |map| - map.show '/people/:id', :controller => 'people', :action => 'show' - map.index '/people', :controller => 'people', :action => 'index' - map.multi '/people/go/:foo/:bar/joe/:id', :controller => 'people', :action => 'multi' - map.users '/admin/users', :controller => 'admin/users', :action => 'index' + set.draw do + match '/people(/:id)' => 'people#show', :as => 'show' + match '/people' => 'people#index', :as => 'index' + match '/people/go/:foo/:bar/joe(/:id)' => 'people#multi', :as => 'multi' + match '/admin/users' => 'admin/users#index', :as => "users" end MockController.build(set.url_helpers).new @@ -752,15 +737,15 @@ class RouteSetTest < ActiveSupport::TestCase controller = setup_named_route_test assert_equal( - { :controller => 'people', :action => 'show', :id => 5, :use_route => :show, :only_path => false }, + { :controller => 'people', :action => 'show', :id => 5, :use_route => "show", :only_path => false }, controller.send(:hash_for_show_url, :id => 5)) assert_equal( - { :controller => 'people', :action => 'index', :use_route => :index, :only_path => false }, + { :controller => 'people', :action => 'index', :use_route => "index", :only_path => false }, controller.send(:hash_for_index_url)) assert_equal( - { :controller => 'people', :action => 'show', :id => 5, :use_route => :show, :only_path => true }, + { :controller => 'people', :action => 'show', :id => 5, :use_route => "show", :only_path => true }, controller.send(:hash_for_show_path, :id => 5) ) end @@ -776,7 +761,7 @@ class RouteSetTest < ActiveSupport::TestCase assert_equal "http://test.host/admin/users", controller.send(:users_url) assert_equal '/admin/users', controller.send(:users_path) - assert_equal '/admin/users', set.generate(controller.send(:hash_for_users_url), {:controller => 'users', :action => 'index'}) + assert_equal '/admin/users', url_for(set, controller.send(:hash_for_users_url), { :controller => 'users', :action => 'index' }) end def test_named_route_url_method_with_anchor @@ -841,30 +826,23 @@ class RouteSetTest < ActiveSupport::TestCase end def test_draw_default_route - set.draw do |map| - map.connect '/:controller/:action/:id' + set.draw do + match '/:controller/:action/:id' end assert_equal 1, set.routes.size - assert_equal '/users/show/10', set.generate(:controller => 'users', :action => 'show', :id => 10) - assert_equal '/users/index/10', set.generate(:controller => 'users', :id => 10) + assert_equal '/users/show/10', url_for(set, { :controller => 'users', :action => 'show', :id => 10 }) + assert_equal '/users/index/10', url_for(set, { :controller => 'users', :id => 10 }) assert_equal({:controller => 'users', :action => 'index', :id => '10'}, set.recognize_path('/users/index/10')) assert_equal({:controller => 'users', :action => 'index', :id => '10'}, set.recognize_path('/users/index/10/')) end - def test_draw_default_route_with_default_controller - set.draw do |map| - map.connect '/:controller/:action/:id', :controller => 'users' - end - assert_equal({:controller => 'users', :action => 'index'}, set.recognize_path('/')) - end - def test_route_with_parameter_shell - set.draw do |map| - map.connect 'page/:id', :controller => 'pages', :action => 'show', :id => /\d+/ - map.connect '/:controller/:action/:id' + set.draw do + match 'page/:id' => 'pages#show', :id => /\d+/ + match '/:controller(/:action(/:id))' end assert_equal({:controller => 'pages', :action => 'index'}, set.recognize_path('/pages')) @@ -875,76 +853,66 @@ class RouteSetTest < ActiveSupport::TestCase assert_equal({:controller => 'pages', :action => 'show', :id => '10'}, set.recognize_path('/page/10')) end - def test_route_requirements_with_anchor_chars_are_invalid - assert_raise ArgumentError do - set.draw do |map| - map.connect 'page/:id', :controller => 'pages', :action => 'show', :id => /^\d+/ + def test_route_constraints_on_request_object_with_anchors_are_valid + assert_nothing_raised do + set.draw do + match 'page/:id' => 'pages#show', :constraints => { :host => /^foo$/ } end end + end + + def test_route_constraints_with_anchor_chars_are_invalid assert_raise ArgumentError do - set.draw do |map| - map.connect 'page/:id', :controller => 'pages', :action => 'show', :id => /\A\d+/ + set.draw do + match 'page/:id' => 'pages#show', :id => /^\d+/ end end assert_raise ArgumentError do - set.draw do |map| - map.connect 'page/:id', :controller => 'pages', :action => 'show', :id => /\d+$/ + set.draw do + match 'page/:id' => 'pages#show', :id => /\A\d+/ end end assert_raise ArgumentError do - set.draw do |map| - map.connect 'page/:id', :controller => 'pages', :action => 'show', :id => /\d+\Z/ + set.draw do + match 'page/:id' => 'pages#show', :id => /\d+$/ end end assert_raise ArgumentError do - set.draw do |map| - map.connect 'page/:id', :controller => 'pages', :action => 'show', :id => /\d+\z/ + set.draw do + match 'page/:id' => 'pages#show', :id => /\d+\Z/ end end - end - - def test_route_requirements_with_invalid_http_method_is_invalid assert_raise ArgumentError do - set.draw do |map| - map.connect 'valid/route', :controller => 'pages', :action => 'show', :conditions => {:method => :invalid} + set.draw do + match 'page/:id' => 'pages#show', :id => /\d+\z/ end end end - def test_route_requirements_with_options_method_condition_is_valid + def test_route_constraints_with_options_method_condition_is_valid assert_nothing_raised do - set.draw do |map| - map.connect 'valid/route', :controller => 'pages', :action => 'show', :conditions => {:method => :options} - end - end - end - - def test_route_requirements_with_head_method_condition_is_invalid - assert_raise ArgumentError do - set.draw do |map| - map.connect 'valid/route', :controller => 'pages', :action => 'show', :conditions => {:method => :head} + set.draw do + match 'valid/route' => 'pages#show', :via => :options end end end def test_recognize_with_encoded_id_and_regex - set.draw do |map| - map.connect 'page/:id', :controller => 'pages', :action => 'show', :id => /[a-zA-Z0-9\+]+/ + set.draw do + match 'page/:id' => 'pages#show', :id => /[a-zA-Z0-9\+]+/ end assert_equal({:controller => 'pages', :action => 'show', :id => '10'}, set.recognize_path('/page/10')) assert_equal({:controller => 'pages', :action => 'show', :id => 'hello+world'}, set.recognize_path('/page/hello+world')) end - def test_recognize_with_conditions - set.draw do |map| - map.with_options(:controller => "people") do |people| - people.people "/people", :action => "index", :conditions => { :method => :get } - people.connect "/people", :action => "create", :conditions => { :method => :post } - people.person "/people/:id", :action => "show", :conditions => { :method => :get } - people.connect "/people/:id", :action => "update", :conditions => { :method => :put } - people.connect "/people/:id", :action => "destroy", :conditions => { :method => :delete } - end + def test_recognize_with_http_methods + set.draw do + get "/people" => "people#index", :as => "people" + post "/people" => "people#create" + get "/people/:id" => "people#show", :as => "person" + put "/people/:id" => "people#update" + delete "/people/:id" => "people#destroy" end params = set.recognize_path("/people", :method => :get) @@ -953,10 +921,10 @@ class RouteSetTest < ActiveSupport::TestCase params = set.recognize_path("/people", :method => :post) assert_equal("create", params[:action]) - params = set.recognize_path("/people", :method => :put) + params = set.recognize_path("/people/5", :method => :put) assert_equal("update", params[:action]) - assert_raise(ActionController::RoutingError) { + assert_raise(ActionController::UnknownHttpMethod) { set.recognize_path("/people", :method => :bacon) } @@ -978,10 +946,9 @@ class RouteSetTest < ActiveSupport::TestCase end def test_recognize_with_alias_in_conditions - set.draw do |map| - map.people "/people", :controller => 'people', :action => "index", - :conditions => { :method => :get } - map.root :people + set.draw do + match "/people" => 'people#index', :as => 'people', :via => :get + root :to => "people#index" end params = set.recognize_path("/people", :method => :get) @@ -994,9 +961,8 @@ class RouteSetTest < ActiveSupport::TestCase end def test_typo_recognition - set.draw do |map| - map.connect 'articles/:year/:month/:day/:title', - :controller => 'articles', :action => 'permalink', + set.draw do + match 'articles/:year/:month/:day/:title' => 'articles#permalink', :year => /\d{4}/, :day => /\d{1,2}/, :month => /\d{1,2}/ end @@ -1010,22 +976,20 @@ class RouteSetTest < ActiveSupport::TestCase def test_routing_traversal_does_not_load_extra_classes assert !Object.const_defined?("Profiler__"), "Profiler should not be loaded" - set.draw do |map| - map.connect '/profile', :controller => 'profile' + set.draw do + match '/profile' => 'profile#index' end - params = set.recognize_path("/profile") rescue nil + set.recognize_path("/profile") rescue nil assert !Object.const_defined?("Profiler__"), "Profiler should not be loaded" end def test_recognize_with_conditions_and_format - set.draw do |map| - map.with_options(:controller => "people") do |people| - people.person "/people/:id", :action => "show", :conditions => { :method => :get } - people.connect "/people/:id", :action => "update", :conditions => { :method => :put } - people.connect "/people/:id.:_format", :action => "show", :conditions => { :method => :get } - end + set.draw do + get "people/:id" => "people#show", :as => "person" + put "people/:id" => "people#update" + get "people/:id(.:format)" => "people#show" end params = set.recognize_path("/people/5", :method => :get) @@ -1038,21 +1002,21 @@ class RouteSetTest < ActiveSupport::TestCase params = set.recognize_path("/people/5.png", :method => :get) assert_equal("show", params[:action]) assert_equal("5", params[:id]) - assert_equal("png", params[:_format]) + assert_equal("png", params[:format]) end def test_generate_with_default_action - set.draw do |map| - map.connect "/people", :controller => "people" - map.connect "/people/list", :controller => "people", :action => "list" + set.draw do + match "/people", :controller => "people", :action => "index" + match "/people/list", :controller => "people", :action => "list" end - url = set.generate(:controller => "people", :action => "list") + url = url_for(set, { :controller => "people", :action => "list" }) assert_equal "/people/list", url end def test_root_map - set.draw { |map| map.root :controller => "people" } + set.draw { root :to => 'people#index' } params = set.recognize_path("", :method => :get) assert_equal("people", params[:controller]) @@ -1060,10 +1024,10 @@ class RouteSetTest < ActiveSupport::TestCase end def test_namespace - set.draw do |map| + set.draw do - map.namespace 'api' do |api| - api.route 'inventory', :controller => "products", :action => 'inventory' + namespace 'api' do + match 'inventory' => 'products#inventory' end end @@ -1074,12 +1038,10 @@ class RouteSetTest < ActiveSupport::TestCase end def test_namespaced_root_map - set.draw do |map| - - map.namespace 'api' do |api| - api.root :controller => "products" + set.draw do + namespace 'api' do + root :to => 'products#index' end - end params = set.recognize_path("/api", :method => :get) @@ -1088,9 +1050,9 @@ class RouteSetTest < ActiveSupport::TestCase end def test_namespace_with_path_prefix - set.draw do |map| - map.namespace 'api', :path_prefix => 'prefix' do |api| - api.route 'inventory', :controller => "products", :action => 'inventory' + set.draw do + scope :module => "api", :path => "prefix" do + match 'inventory' => 'products#inventory' end end @@ -1100,9 +1062,9 @@ class RouteSetTest < ActiveSupport::TestCase end def test_namespace_with_blank_path_prefix - set.draw do |map| - map.namespace 'api', :path_prefix => '' do |api| - api.route 'inventory', :controller => "products", :action => 'inventory' + set.draw do + scope :module => "api", :path => "" do + match 'inventory' => 'products#inventory' end end @@ -1112,128 +1074,129 @@ class RouteSetTest < ActiveSupport::TestCase end def test_generate_changes_controller_module - set.draw { |map| map.connect ':controller/:action/:id' } + set.draw { match ':controller/:action/:id' } current = { :controller => "bling/bloop", :action => "bap", :id => 9 } - url = set.generate({:controller => "foo/bar", :action => "baz", :id => 7}, current) - assert_equal "/foo/bar/baz/7", url - end - # def test_id_is_not_impossibly_sticky - # set.draw do |map| - # map.connect 'foo/:number', :controller => "people", :action => "index" - # map.connect ':controller/:action/:id' - # end - # - # url = set.generate({:controller => "people", :action => "index", :number => 3}, - # {:controller => "people", :action => "index", :id => "21"}) - # assert_equal "/foo/3", url - # end + assert_equal "/foo/bar/baz/7", + url_for(set, { :controller => "foo/bar", :action => "baz", :id => 7 }, current) + end def test_id_is_sticky_when_it_ought_to_be - set.draw do |map| - map.connect ':controller/:id/:action' + set.draw do + match ':controller/:id/:action' end - url = set.generate({:action => "destroy"}, {:controller => "people", :action => "show", :id => "7"}) + url = url_for(set, { :action => "destroy" }, { :controller => "people", :action => "show", :id => "7" }) assert_equal "/people/7/destroy", url end def test_use_static_path_when_possible - set.draw do |map| - map.connect 'about', :controller => "welcome", :action => "about" - map.connect ':controller/:action/:id' + set.draw do + match 'about' => "welcome#about" + match ':controller/:action/:id' end - url = set.generate({:controller => "welcome", :action => "about"}, - {:controller => "welcome", :action => "get", :id => "7"}) + url = url_for(set, { :controller => "welcome", :action => "about" }, + { :controller => "welcome", :action => "get", :id => "7" }) + assert_equal "/about", url end def test_generate - set.draw { |map| map.connect ':controller/:action/:id' } + set.draw { match ':controller/:action/:id' } args = { :controller => "foo", :action => "bar", :id => "7", :x => "y" } - assert_equal "/foo/bar/7?x=y", set.generate(args) + assert_equal "/foo/bar/7?x=y", url_for(set, args) assert_equal ["/foo/bar/7", [:x]], set.generate_extras(args) assert_equal [:x], set.extra_keys(args) end def test_generate_with_path_prefix - set.draw { |map| map.connect ':controller/:action/:id', :path_prefix => 'my' } + set.draw do + scope "my" do + match ':controller(/:action(/:id))' + end + end args = { :controller => "foo", :action => "bar", :id => "7", :x => "y" } - assert_equal "/my/foo/bar/7?x=y", set.generate(args) + assert_equal "/my/foo/bar/7?x=y", url_for(set, args) end def test_generate_with_blank_path_prefix - set.draw { |map| map.connect ':controller/:action/:id', :path_prefix => '' } + set.draw do + scope "" do + match ':controller(/:action(/:id))' + end + end args = { :controller => "foo", :action => "bar", :id => "7", :x => "y" } - assert_equal "/foo/bar/7?x=y", set.generate(args) + assert_equal "/foo/bar/7?x=y", url_for(set, args) end def test_named_routes_are_never_relative_to_modules - set.draw do |map| - map.connect "/connection/manage/:action", :controller => 'connection/manage' - map.connect "/connection/connection", :controller => "connection/connection" - map.family_connection "/connection", :controller => "connection" + set.draw do + match "/connection/manage(/:action)" => 'connection/manage#index' + match "/connection/connection" => "connection/connection#index" + match '/connection' => 'connection#index', :as => 'family_connection' end - url = set.generate({:controller => "connection"}, {:controller => 'connection/manage'}) + url = url_for(set, { :controller => "connection" }, { :controller => 'connection/manage' }) assert_equal "/connection/connection", url - url = set.generate({:use_route => :family_connection, :controller => "connection"}, {:controller => 'connection/manage'}) + url = url_for(set, { :use_route => :family_connection, :controller => "connection" }, { :controller => 'connection/manage' }) assert_equal "/connection", url end def test_action_left_off_when_id_is_recalled - set.draw do |map| - map.connect ':controller/:action/:id' + set.draw do + match ':controller(/:action(/:id))' end - assert_equal '/books', set.generate( + assert_equal '/books', url_for(set, {:controller => 'books', :action => 'index'}, {:controller => 'books', :action => 'show', :id => '10'} ) end def test_query_params_will_be_shown_when_recalled - set.draw do |map| - map.connect 'show_weblog/:parameter', :controller => 'weblog', :action => 'show' - map.connect ':controller/:action/:id' + set.draw do + match 'show_weblog/:parameter' => 'weblog#show' + match ':controller(/:action(/:id))' end - assert_equal '/weblog/edit?parameter=1', set.generate( + assert_equal '/weblog/edit?parameter=1', url_for(set, {:action => 'edit', :parameter => 1}, {:controller => 'weblog', :action => 'show', :parameter => 1} ) end def test_format_is_not_inherit - set.draw do |map| - map.connect '/posts.:format', :controller => 'posts' + set.draw do + match '/posts(.:format)' => 'posts#index' end - assert_equal '/posts', set.generate( + assert_equal '/posts', url_for(set, {:controller => 'posts'}, {:controller => 'posts', :action => 'index', :format => 'xml'} ) - assert_equal '/posts.xml', set.generate( + assert_equal '/posts.xml', url_for(set, {:controller => 'posts', :format => 'xml'}, {:controller => 'posts', :action => 'index', :format => 'xml'} ) end def test_expiry_determination_should_consider_values_with_to_param - set.draw { |map| map.connect 'projects/:project_id/:controller/:action' } - assert_equal '/projects/1/weblog/show', set.generate( - {:action => 'show', :project_id => 1}, - {:controller => 'weblog', :action => 'show', :project_id => '1'}) + set.draw { match 'projects/:project_id/:controller/:action' } + assert_equal '/projects/1/weblog/show', url_for(set, + { :action => 'show', :project_id => 1 }, + { :controller => 'weblog', :action => 'show', :project_id => '1' }) end def test_named_route_in_nested_resource - set.draw do |map| - map.resources :projects do |project| - project.milestones 'milestones', :controller => 'milestones', :action => 'index' + set.draw do + resources :projects do + member do + match 'milestones' => 'milestones#index', :as => 'milestones' + end end end @@ -1244,9 +1207,9 @@ class RouteSetTest < ActiveSupport::TestCase def test_setting_root_in_namespace_using_symbol assert_nothing_raised do - set.draw do |map| - map.namespace :admin do |admin| - admin.root :controller => 'home' + set.draw do + namespace :admin do + root :to => "home#index" end end end @@ -1254,51 +1217,47 @@ class RouteSetTest < ActiveSupport::TestCase def test_setting_root_in_namespace_using_string assert_nothing_raised do - set.draw do |map| - map.namespace 'admin' do |admin| - admin.root :controller => 'home' + set.draw do + namespace 'admin' do + root :to => "home#index" end end end end - def test_route_requirements_with_unsupported_regexp_options_must_error + def test_route_constraints_with_unsupported_regexp_options_must_error assert_raise ArgumentError do - set.draw do |map| - map.connect 'page/:name', :controller => 'pages', - :action => 'show', - :requirements => {:name => /(david|jamis)/m} + set.draw do + match 'page/:name' => 'pages#show', + :constraints => { :name => /(david|jamis)/m } end end end - def test_route_requirements_with_supported_options_must_not_error + def test_route_constraints_with_supported_options_must_not_error assert_nothing_raised do - set.draw do |map| - map.connect 'page/:name', :controller => 'pages', - :action => 'show', - :requirements => {:name => /(david|jamis)/i} + set.draw do + match 'page/:name' => 'pages#show', + :constraints => { :name => /(david|jamis)/i } end end assert_nothing_raised do - set.draw do |map| - map.connect 'page/:name', :controller => 'pages', - :action => 'show', - :requirements => {:name => / # Desperately overcommented regexp + set.draw do + match 'page/:name' => 'pages#show', + :constraints => { :name => / # Desperately overcommented regexp ( #Either david #The Creator | #Or jamis #The Deployer - )/x} + )/x } end end end def test_route_requirement_recognize_with_ignore_case - set.draw do |map| - map.connect 'page/:name', :controller => 'pages', - :action => 'show', - :requirements => {:name => /(david|jamis)/i} + set.draw do + match 'page/:name' => 'pages#show', + :constraints => {:name => /(david|jamis)/i} end assert_equal({:controller => 'pages', :action => 'show', :name => 'jamis'}, set.recognize_path('/page/jamis')) assert_raise ActionController::RoutingError do @@ -1308,26 +1267,24 @@ class RouteSetTest < ActiveSupport::TestCase end def test_route_requirement_generate_with_ignore_case - set.draw do |map| - map.connect 'page/:name', :controller => 'pages', - :action => 'show', - :requirements => {:name => /(david|jamis)/i} + set.draw do + match 'page/:name' => 'pages#show', + :constraints => {:name => /(david|jamis)/i} end - url = set.generate({:controller => 'pages', :action => 'show', :name => 'david'}) + url = url_for(set, { :controller => 'pages', :action => 'show', :name => 'david' }) assert_equal "/page/david", url assert_raise ActionController::RoutingError do - url = set.generate({:controller => 'pages', :action => 'show', :name => 'davidjamis'}) + url_for(set, { :controller => 'pages', :action => 'show', :name => 'davidjamis' }) end - url = set.generate({:controller => 'pages', :action => 'show', :name => 'JAMIS'}) + url = url_for(set, { :controller => 'pages', :action => 'show', :name => 'JAMIS' }) assert_equal "/page/JAMIS", url end def test_route_requirement_recognize_with_extended_syntax - set.draw do |map| - map.connect 'page/:name', :controller => 'pages', - :action => 'show', - :requirements => {:name => / # Desperately overcommented regexp + set.draw do + match 'page/:name' => 'pages#show', + :constraints => {:name => / # Desperately overcommented regexp ( #Either david #The Creator | #Or @@ -1344,33 +1301,10 @@ class RouteSetTest < ActiveSupport::TestCase end end - def test_route_requirement_generate_with_extended_syntax - set.draw do |map| - map.connect 'page/:name', :controller => 'pages', - :action => 'show', - :requirements => {:name => / # Desperately overcommented regexp - ( #Either - david #The Creator - | #Or - jamis #The Deployer - )/x} - end - - url = set.generate({:controller => 'pages', :action => 'show', :name => 'david'}) - assert_equal "/page/david", url - assert_raise ActionController::RoutingError do - url = set.generate({:controller => 'pages', :action => 'show', :name => 'davidjamis'}) - end - assert_raise ActionController::RoutingError do - url = set.generate({:controller => 'pages', :action => 'show', :name => 'JAMIS'}) - end - end - - def test_route_requirement_generate_with_xi_modifiers - set.draw do |map| - map.connect 'page/:name', :controller => 'pages', - :action => 'show', - :requirements => {:name => / # Desperately overcommented regexp + def test_route_requirement_with_xi_modifiers + set.draw do + match 'page/:name' => 'pages#show', + :constraints => {:name => / # Desperately overcommented regexp ( #Either david #The Creator | #Or @@ -1378,98 +1312,83 @@ class RouteSetTest < ActiveSupport::TestCase )/xi} end - url = set.generate({:controller => 'pages', :action => 'show', :name => 'JAMIS'}) - assert_equal "/page/JAMIS", url - end + assert_equal({:controller => 'pages', :action => 'show', :name => 'JAMIS'}, + set.recognize_path('/page/JAMIS')) - def test_route_requirement_recognize_with_xi_modifiers - set.draw do |map| - map.connect 'page/:name', :controller => 'pages', - :action => 'show', - :requirements => {:name => / # Desperately overcommented regexp - ( #Either - david #The Creator - | #Or - jamis #The Deployer - )/xi} - end - assert_equal({:controller => 'pages', :action => 'show', :name => 'JAMIS'}, set.recognize_path('/page/JAMIS')) + assert_equal "/page/JAMIS", + url_for(set, { :controller => 'pages', :action => 'show', :name => 'JAMIS' }) end def test_routes_with_symbols - set.draw do |map| - map.connect 'unnamed', :controller => :pages, :action => :show, :name => :as_symbol - map.named 'named', :controller => :pages, :action => :show, :name => :as_symbol + set.draw do + match 'unnamed', :controller => :pages, :action => :show, :name => :as_symbol + match 'named' , :controller => :pages, :action => :show, :name => :as_symbol, :as => :named end assert_equal({:controller => 'pages', :action => 'show', :name => :as_symbol}, set.recognize_path('/unnamed')) assert_equal({:controller => 'pages', :action => 'show', :name => :as_symbol}, set.recognize_path('/named')) end def test_regexp_chunk_should_add_question_mark_for_optionals - set.draw do |map| - map.connect '/', :controller => 'foo' - map.connect '/hello', :controller => 'bar' + set.draw do + match '/' => 'foo#index' + match '/hello' => 'bar#index' end - assert_equal '/', set.generate(:controller => 'foo') - assert_equal '/hello', set.generate(:controller => 'bar') + assert_equal '/', url_for(set, { :controller => 'foo' }) + assert_equal '/hello', url_for(set, { :controller => 'bar' }) assert_equal({:controller => "foo", :action => "index"}, set.recognize_path('/')) assert_equal({:controller => "bar", :action => "index"}, set.recognize_path('/hello')) end def test_assign_route_options_with_anchor_chars - set.draw do |map| - map.connect '/cars/:action/:person/:car/', :controller => 'cars' + set.draw do + match '/cars/:action/:person/:car/', :controller => 'cars' end - assert_equal '/cars/buy/1/2', set.generate(:controller => 'cars', :action => 'buy', :person => '1', :car => '2') + assert_equal '/cars/buy/1/2', url_for(set, { :controller => 'cars', :action => 'buy', :person => '1', :car => '2' }) assert_equal({:controller => "cars", :action => "buy", :person => "1", :car => "2"}, set.recognize_path('/cars/buy/1/2')) end def test_segmentation_of_dot_path - set.draw do |map| - map.connect '/books/:action.rss', :controller => 'books' + set.draw do + match '/books/:action.rss', :controller => 'books' end - assert_equal '/books/list.rss', set.generate(:controller => 'books', :action => 'list') + assert_equal '/books/list.rss', url_for(set, { :controller => 'books', :action => 'list' }) assert_equal({:controller => "books", :action => "list"}, set.recognize_path('/books/list.rss')) end def test_segmentation_of_dynamic_dot_path - set.draw do |map| - map.connect '/books/:action.:format', :controller => 'books' + set.draw do + match '/books(/:action(.:format))', :controller => 'books' end - assert_equal '/books/list.rss', set.generate(:controller => 'books', :action => 'list', :format => 'rss') - assert_equal '/books/list.xml', set.generate(:controller => 'books', :action => 'list', :format => 'xml') - assert_equal '/books/list', set.generate(:controller => 'books', :action => 'list') - assert_equal '/books', set.generate(:controller => 'books', :action => 'index') + assert_equal '/books/list.rss', url_for(set, { :controller => 'books', :action => 'list', :format => 'rss' }) + assert_equal '/books/list.xml', url_for(set, { :controller => 'books', :action => 'list', :format => 'xml' }) + assert_equal '/books/list', url_for(set, { :controller => 'books', :action => 'list' }) + assert_equal '/books', url_for(set, { :controller => 'books', :action => 'index' }) assert_equal({:controller => "books", :action => "list", :format => "rss"}, set.recognize_path('/books/list.rss')) assert_equal({:controller => "books", :action => "list", :format => "xml"}, set.recognize_path('/books/list.xml')) - assert_equal({:controller => "books", :action => "list"}, set.recognize_path('/books/list')) + assert_equal({:controller => "books", :action => "list"}, set.recognize_path('/books/list')) assert_equal({:controller => "books", :action => "index"}, set.recognize_path('/books')) end def test_slashes_are_implied - ['/:controller/:action/:id/', '/:controller/:action/:id', - ':controller/:action/:id', '/:controller/:action/:id/' - ].each do |path| - @set = nil - set.draw { |map| map.connect(path) } + @set = nil + set.draw { match("/:controller(/:action(/:id))") } - assert_equal '/content', set.generate(:controller => 'content', :action => 'index') - assert_equal '/content/list', set.generate(:controller => 'content', :action => 'list') - assert_equal '/content/show/1', set.generate(:controller => 'content', :action => 'show', :id => '1') + assert_equal '/content', url_for(set, { :controller => 'content', :action => 'index' }) + assert_equal '/content/list', url_for(set, { :controller => 'content', :action => 'list' }) + assert_equal '/content/show/1', url_for(set, { :controller => 'content', :action => 'show', :id => '1' }) - assert_equal({:controller => "content", :action => "index"}, set.recognize_path('/content')) - assert_equal({:controller => "content", :action => "index"}, set.recognize_path('/content/index')) - assert_equal({:controller => "content", :action => "list"}, set.recognize_path('/content/list')) - assert_equal({:controller => "content", :action => "show", :id => "1"}, set.recognize_path('/content/show/1')) - end + assert_equal({:controller => "content", :action => "index"}, set.recognize_path('/content')) + assert_equal({:controller => "content", :action => "index"}, set.recognize_path('/content/index')) + assert_equal({:controller => "content", :action => "list"}, set.recognize_path('/content/list')) + assert_equal({:controller => "content", :action => "show", :id => "1"}, set.recognize_path('/content/show/1')) end def test_default_route_recognition @@ -1493,102 +1412,102 @@ class RouteSetTest < ActiveSupport::TestCase end def test_default_route_should_omit_default_action - assert_equal '/accounts', default_route_set.generate({:controller => 'accounts', :action => 'index'}) + assert_equal '/accounts', url_for(default_route_set, { :controller => 'accounts', :action => 'index' }) end def test_default_route_should_include_default_action_when_id_present - assert_equal '/accounts/index/20', default_route_set.generate({:controller => 'accounts', :action => 'index', :id => '20'}) + assert_equal '/accounts/index/20', url_for(default_route_set, { :controller => 'accounts', :action => 'index', :id => '20' }) end def test_default_route_should_work_with_action_but_no_id - assert_equal '/accounts/list_all', default_route_set.generate({:controller => 'accounts', :action => 'list_all'}) + assert_equal '/accounts/list_all', url_for(default_route_set, { :controller => 'accounts', :action => 'list_all' }) end def test_default_route_should_uri_escape_pluses expected = { :controller => 'pages', :action => 'show', :id => 'hello world' } assert_equal expected, default_route_set.recognize_path('/pages/show/hello%20world') - assert_equal '/pages/show/hello%20world', default_route_set.generate(expected, expected) + assert_equal '/pages/show/hello%20world', url_for(default_route_set, expected) expected[:id] = 'hello+world' assert_equal expected, default_route_set.recognize_path('/pages/show/hello+world') assert_equal expected, default_route_set.recognize_path('/pages/show/hello%2Bworld') - assert_equal '/pages/show/hello+world', default_route_set.generate(expected, expected) + assert_equal '/pages/show/hello+world', url_for(default_route_set, expected) end def test_build_empty_query_string - assert_uri_equal '/foo', default_route_set.generate({:controller => 'foo'}) + assert_uri_equal '/foo', url_for(default_route_set, { :controller => 'foo' }) end def test_build_query_string_with_nil_value - assert_uri_equal '/foo', default_route_set.generate({:controller => 'foo', :x => nil}) + assert_uri_equal '/foo', url_for(default_route_set, { :controller => 'foo', :x => nil }) end def test_simple_build_query_string - assert_uri_equal '/foo?x=1&y=2', default_route_set.generate({:controller => 'foo', :x => '1', :y => '2'}) + assert_uri_equal '/foo?x=1&y=2', url_for(default_route_set, { :controller => 'foo', :x => '1', :y => '2' }) end def test_convert_ints_build_query_string - assert_uri_equal '/foo?x=1&y=2', default_route_set.generate({:controller => 'foo', :x => 1, :y => 2}) + assert_uri_equal '/foo?x=1&y=2', url_for(default_route_set, { :controller => 'foo', :x => 1, :y => 2 }) end def test_escape_spaces_build_query_string - assert_uri_equal '/foo?x=hello+world&y=goodbye+world', default_route_set.generate({:controller => 'foo', :x => 'hello world', :y => 'goodbye world'}) + assert_uri_equal '/foo?x=hello+world&y=goodbye+world', url_for(default_route_set, { :controller => 'foo', :x => 'hello world', :y => 'goodbye world' }) end def test_expand_array_build_query_string - assert_uri_equal '/foo?x[]=1&x[]=2', default_route_set.generate({:controller => 'foo', :x => [1, 2]}) + assert_uri_equal '/foo?x%5B%5D=1&x%5B%5D=2', url_for(default_route_set, { :controller => 'foo', :x => [1, 2] }) end def test_escape_spaces_build_query_string_selected_keys - assert_uri_equal '/foo?x=hello+world', default_route_set.generate({:controller => 'foo', :x => 'hello world'}) + assert_uri_equal '/foo?x=hello+world', url_for(default_route_set, { :controller => 'foo', :x => 'hello world' }) end def test_generate_with_default_params - set.draw do |map| - map.connect 'dummy/page/:page', :controller => 'dummy' - map.connect 'dummy/dots/page.:page', :controller => 'dummy', :action => 'dots' - map.connect 'ibocorp/:page', :controller => 'ibocorp', - :requirements => { :page => /\d+/ }, - :defaults => { :page => 1 } + set.draw do + match 'dummy/page/:page' => 'dummy#show' + match 'dummy/dots/page.:page' => 'dummy#dots' + match 'ibocorp(/:page)' => 'ibocorp#show', + :constraints => { :page => /\d+/ }, + :defaults => { :page => 1 } - map.connect ':controller/:action/:id' + match ':controller/:action/:id' end - assert_equal '/ibocorp', set.generate({:controller => 'ibocorp', :page => 1}) + assert_equal '/ibocorp', url_for(set, { :controller => 'ibocorp', :action => "show", :page => 1 }) end def test_generate_with_optional_params_recalls_last_request - set.draw do |map| - map.connect "blog/", :controller => "blog", :action => "index" + set.draw do + match "blog/", :controller => "blog", :action => "index" - map.connect "blog/:year/:month/:day", - :controller => "blog", - :action => "show_date", - :requirements => { :year => /(19|20)\d\d/, :month => /[01]?\d/, :day => /[0-3]?\d/ }, - :day => nil, :month => nil + match "blog(/:year(/:month(/:day)))", + :controller => "blog", + :action => "show_date", + :constraints => { :year => /(19|20)\d\d/, :month => /[01]?\d/, :day => /[0-3]?\d/ }, + :day => nil, :month => nil - map.connect "blog/show/:id", :controller => "blog", :action => "show", :id => /\d+/ - map.connect "blog/:controller/:action/:id" - map.connect "*anything", :controller => "blog", :action => "unknown_request" + match "blog/show/:id", :controller => "blog", :action => "show", :id => /\d+/ + match "blog/:controller/:action(/:id)" + match "*anything", :controller => "blog", :action => "unknown_request" end assert_equal({:controller => "blog", :action => "index"}, set.recognize_path("/blog")) assert_equal({:controller => "blog", :action => "show", :id => "123"}, set.recognize_path("/blog/show/123")) - assert_equal({:controller => "blog", :action => "show_date", :year => "2004"}, set.recognize_path("/blog/2004")) - assert_equal({:controller => "blog", :action => "show_date", :year => "2004", :month => "12"}, set.recognize_path("/blog/2004/12")) + assert_equal({:controller => "blog", :action => "show_date", :year => "2004", :day => nil, :month => nil }, set.recognize_path("/blog/2004")) + assert_equal({:controller => "blog", :action => "show_date", :year => "2004", :month => "12", :day => nil }, set.recognize_path("/blog/2004/12")) assert_equal({:controller => "blog", :action => "show_date", :year => "2004", :month => "12", :day => "25"}, set.recognize_path("/blog/2004/12/25")) assert_equal({:controller => "articles", :action => "edit", :id => "123"}, set.recognize_path("/blog/articles/edit/123")) assert_equal({:controller => "articles", :action => "show_stats"}, set.recognize_path("/blog/articles/show_stats")) - assert_equal({:controller => "blog", :action => "unknown_request", :anything => ["blog", "wibble"]}, set.recognize_path("/blog/wibble")) - assert_equal({:controller => "blog", :action => "unknown_request", :anything => ["junk"]}, set.recognize_path("/junk")) + assert_equal({:controller => "blog", :action => "unknown_request", :anything => "blog/wibble"}, set.recognize_path("/blog/wibble")) + assert_equal({:controller => "blog", :action => "unknown_request", :anything => "junk"}, set.recognize_path("/junk")) last_request = set.recognize_path("/blog/2006/07/28").freeze assert_equal({:controller => "blog", :action => "show_date", :year => "2006", :month => "07", :day => "28"}, last_request) - assert_equal("/blog/2006/07/25", set.generate({:day => 25}, last_request)) - assert_equal("/blog/2005", set.generate({:year => 2005}, last_request)) - assert_equal("/blog/show/123", set.generate({:action => "show" , :id => 123}, last_request)) - assert_equal("/blog/2006", set.generate({:year => 2006}, last_request)) - assert_equal("/blog/2006", set.generate({:year => 2006, :month => nil}, last_request)) + assert_equal("/blog/2006/07/25", url_for(set, { :day => 25 }, last_request)) + assert_equal("/blog/2005", url_for(set, { :year => 2005 }, last_request)) + assert_equal("/blog/show/123", url_for(set, { :action => "show" , :id => 123 }, last_request)) + assert_equal("/blog/2006", url_for(set, { :year => 2006 }, last_request)) + assert_equal("/blog/2006", url_for(set, { :year => 2006, :month => nil }, last_request)) end private @@ -1604,76 +1523,67 @@ class RouteSetTest < ActiveSupport::TestCase end class RackMountIntegrationTests < ActiveSupport::TestCase + include RoutingTestHelpers + Model = Struct.new(:to_param) - Mapping = lambda { |map| - map.namespace :admin do |admin| - admin.resources :users + Mapping = lambda { + namespace :admin do + resources :users, :posts end - map.namespace 'api' do |api| - api.root :controller => 'users' + namespace 'api' do + root :to => 'users#index' end - map.connect 'blog/:year/:month/:day', - :controller => 'posts', - :action => 'show_date', - :requirements => { :year => /(19|20)\d\d/, :month => /[01]?\d/, :day => /[0-3]?\d/}, - :day => nil, - :month => nil + match '/blog(/:year(/:month(/:day)))' => 'posts#show_date', + :constraints => { + :year => /(19|20)\d\d/, + :month => /[01]?\d/, + :day => /[0-3]?\d/ + }, + :day => nil, + :month => nil - map.blog('archive/:year', :controller => 'archive', :action => 'index', + match 'archive/:year', :controller => 'archive', :action => 'index', :defaults => { :year => nil }, - :requirements => { :year => /\d{4}/ } - ) + :constraints => { :year => /\d{4}/ }, + :as => "blog" - map.resources :people - map.connect 'legacy/people', :controller => 'people', :action => 'index', :legacy => 'true' + resources :people + match 'legacy/people' => "people#index", :legacy => "true" - map.connect 'symbols', :controller => :symbols, :action => :show, :name => :as_symbol - map.connect 'id_default/:id', :controller => 'foo', :action => 'id_default', :id => 1 - map.connect 'get_or_post', :controller => 'foo', :action => 'get_or_post', :conditions => { :method => [:get, :post] } - map.connect 'optional/:optional', :controller => 'posts', :action => 'index' - map.project 'projects/:project_id', :controller => 'project' - map.connect 'clients', :controller => 'projects', :action => 'index' + match 'symbols', :controller => :symbols, :action => :show, :name => :as_symbol + match 'id_default(/:id)' => "foo#id_default", :id => 1 + match 'get_or_post' => "foo#get_or_post", :via => [:get, :post] + match 'optional/:optional' => "posts#index" + match 'projects/:project_id' => "project#index", :as => "project" + match 'clients' => "projects#index" - map.connect 'ignorecase/geocode/:postalcode', :controller => 'geocode', - :action => 'show', :postalcode => /hx\d\d-\d[a-z]{2}/i - map.geocode 'extended/geocode/:postalcode', :controller => 'geocode', - :action => 'show',:requirements => { + match 'ignorecase/geocode/:postalcode' => 'geocode#show', :postalcode => /hx\d\d-\d[a-z]{2}/i + match 'extended/geocode/:postalcode' => 'geocode#show',:constraints => { :postalcode => /# Postcode format \d{5} #Prefix (-\d{4})? #Suffix /x - } + }, :as => "geocode" - map.connect '', :controller => 'news', :format => nil - map.connect 'news.:format', :controller => 'news' + match 'news(.:format)' => "news#index" - map.connect 'comment/:id/:action', :controller => 'comments', :action => 'show' - map.connect 'ws/:controller/:action/:id', :ws => true - map.connect 'account/:action', :controller => :account, :action => :subscription - map.connect 'pages/:page_id/:controller/:action/:id' - map.connect ':controller/ping', :action => 'ping' - map.connect ':controller/:action/:id' + match 'comment/:id(/:action)' => "comments#show" + match 'ws/:controller(/:action(/:id))', :ws => true + match 'account(/:action)' => "account#subscription" + match 'pages/:page_id/:controller(/:action(/:id))' + match ':controller/ping', :action => 'ping' + match ':controller(/:action(/:id))(.:format)' + root :to => "news#index" } def setup - @routes = ActionController::Routing::RouteSet.new + @routes = ActionDispatch::Routing::RouteSet.new @routes.draw(&Mapping) end - def test_add_route - @routes.clear! - - assert_raise(ActionController::RoutingError) do - @routes.draw do |map| - map.path 'file/*path', :controller => 'content', :action => 'show_file', :path => %w(fake default) - map.connect ':controller/:action/:id' - end - end - end - def test_recognize_path assert_equal({:controller => 'admin/users', :action => 'index'}, @routes.recognize_path('/admin/users', :method => :get)) assert_equal({:controller => 'admin/users', :action => 'create'}, @routes.recognize_path('/admin/users', :method => :post)) @@ -1689,8 +1599,8 @@ class RackMountIntegrationTests < ActiveSupport::TestCase assert_equal({:controller => 'api/users', :action => 'index'}, @routes.recognize_path('/api', :method => :get)) assert_equal({:controller => 'api/users', :action => 'index'}, @routes.recognize_path('/api/', :method => :get)) - assert_equal({:controller => 'posts', :action => 'show_date', :year => '2009'}, @routes.recognize_path('/blog/2009', :method => :get)) - assert_equal({:controller => 'posts', :action => 'show_date', :year => '2009', :month => '01'}, @routes.recognize_path('/blog/2009/01', :method => :get)) + assert_equal({:controller => 'posts', :action => 'show_date', :year => '2009', :month => nil, :day => nil }, @routes.recognize_path('/blog/2009', :method => :get)) + assert_equal({:controller => 'posts', :action => 'show_date', :year => '2009', :month => '01', :day => nil }, @routes.recognize_path('/blog/2009/01', :method => :get)) assert_equal({:controller => 'posts', :action => 'show_date', :year => '2009', :month => '01', :day => '01'}, @routes.recognize_path('/blog/2009/01/01', :method => :get)) assert_equal({:controller => 'archive', :action => 'index', :year => '2010'}, @routes.recognize_path('/archive/2010')) @@ -1710,7 +1620,7 @@ class RackMountIntegrationTests < ActiveSupport::TestCase assert_equal({:controller => 'symbols', :action => 'show', :name => :as_symbol}, @routes.recognize_path('/symbols')) assert_equal({:controller => 'foo', :action => 'id_default', :id => '1'}, @routes.recognize_path('/id_default/1')) assert_equal({:controller => 'foo', :action => 'id_default', :id => '2'}, @routes.recognize_path('/id_default/2')) - assert_equal({:controller => 'foo', :action => 'id_default', :id => '1'}, @routes.recognize_path('/id_default')) + assert_equal({:controller => 'foo', :action => 'id_default', :id => 1 }, @routes.recognize_path('/id_default')) assert_equal({:controller => 'foo', :action => 'get_or_post'}, @routes.recognize_path('/get_or_post', :method => :get)) assert_equal({:controller => 'foo', :action => 'get_or_post'}, @routes.recognize_path('/get_or_post', :method => :post)) assert_raise(ActionController::ActionControllerError) { @routes.recognize_path('/get_or_post', :method => :put) } @@ -1743,126 +1653,118 @@ class RackMountIntegrationTests < ActiveSupport::TestCase assert_equal({:controller => 'geocode', :action => 'show', :postalcode => '12345-1234'}, @routes.recognize_path('/extended/geocode/12345-1234')) assert_equal({:controller => 'geocode', :action => 'show', :postalcode => '12345'}, @routes.recognize_path('/extended/geocode/12345')) - assert_equal({:controller => 'news', :action => 'index', :format => nil}, @routes.recognize_path('/', :method => :get)) + assert_equal({:controller => 'news', :action => 'index' }, @routes.recognize_path('/', :method => :get)) assert_equal({:controller => 'news', :action => 'index', :format => 'rss'}, @routes.recognize_path('/news.rss', :method => :get)) assert_raise(ActionController::RoutingError) { @routes.recognize_path('/none', :method => :get) } end def test_generate - assert_equal '/admin/users', @routes.generate(:use_route => 'admin_users') - assert_equal '/admin/users', @routes.generate(:controller => 'admin/users') - assert_equal '/admin/users', @routes.generate(:controller => 'admin/users', :action => 'index') - assert_equal '/admin/users', @routes.generate({:action => 'index'}, {:controller => 'admin/users'}) - assert_equal '/admin/users', @routes.generate({:controller => 'users', :action => 'index'}, {:controller => 'admin/accounts'}) - assert_equal '/people', @routes.generate({:controller => '/people', :action => 'index'}, {:controller => 'admin/accounts'}) - - assert_equal '/admin/posts', @routes.generate({:controller => 'admin/posts'}) - assert_equal '/admin/posts/new', @routes.generate({:controller => 'admin/posts', :action => 'new'}) - - assert_equal '/blog/2009', @routes.generate(:controller => 'posts', :action => 'show_date', :year => 2009) - assert_equal '/blog/2009/1', @routes.generate(:controller => 'posts', :action => 'show_date', :year => 2009, :month => 1) - assert_equal '/blog/2009/1/1', @routes.generate(:controller => 'posts', :action => 'show_date', :year => 2009, :month => 1, :day => 1) - - assert_equal '/archive/2010', @routes.generate(:controller => 'archive', :action => 'index', :year => '2010') - assert_equal '/archive', @routes.generate(:controller => 'archive', :action => 'index') - assert_equal '/archive?year=january', @routes.generate(:controller => 'archive', :action => 'index', :year => 'january') - - assert_equal '/people', @routes.generate(:use_route => 'people') - assert_equal '/people', @routes.generate(:use_route => 'people', :controller => 'people', :action => 'index') - assert_equal '/people.xml', @routes.generate(:use_route => 'people', :controller => 'people', :action => 'index', :format => 'xml') - assert_equal '/people', @routes.generate({:use_route => 'people', :controller => 'people', :action => 'index'}, {:controller => 'people', :action => 'index'}) - assert_equal '/people', @routes.generate(:controller => 'people') - assert_equal '/people', @routes.generate(:controller => 'people', :action => 'index') - assert_equal '/people', @routes.generate({:action => 'index'}, {:controller => 'people'}) - assert_equal '/people', @routes.generate({:action => 'index'}, {:controller => 'people', :action => 'show', :id => '1'}) - assert_equal '/people', @routes.generate({:controller => 'people', :action => 'index'}, {:controller => 'people', :action => 'show', :id => '1'}) - assert_equal '/people', @routes.generate({}, {:controller => 'people', :action => 'index'}) - assert_equal '/people/1', @routes.generate({:controller => 'people', :action => 'show'}, {:controller => 'people', :action => 'show', :id => '1'}) - assert_equal '/people/new', @routes.generate(:use_route => 'new_person') - assert_equal '/people/new', @routes.generate(:controller => 'people', :action => 'new') - assert_equal '/people/1', @routes.generate(:use_route => 'person', :id => '1') - assert_equal '/people/1', @routes.generate(:controller => 'people', :action => 'show', :id => '1') - assert_equal '/people/1.xml', @routes.generate(:controller => 'people', :action => 'show', :id => '1', :format => 'xml') - assert_equal '/people/1', @routes.generate(:controller => 'people', :action => 'show', :id => 1) - assert_equal '/people/1', @routes.generate(:controller => 'people', :action => 'show', :id => Model.new('1')) - assert_equal '/people/1', @routes.generate({:action => 'show', :id => '1'}, {:controller => 'people', :action => 'index'}) - assert_equal '/people/1', @routes.generate({:action => 'show', :id => 1}, {:controller => 'people', :action => 'show', :id => '1'}) - # assert_equal '/people', @routes.generate({:controller => 'people', :action => 'index'}, {:controller => 'people', :action => 'index', :id => '1'}) - assert_equal '/people', @routes.generate({:controller => 'people', :action => 'index'}, {:controller => 'people', :action => 'show', :id => '1'}) - assert_equal '/people/1', @routes.generate({}, {:controller => 'people', :action => 'show', :id => '1'}) - assert_equal '/people/1', @routes.generate({:controller => 'people', :action => 'show'}, {:controller => 'people', :action => 'index', :id => '1'}) - assert_equal '/people/1/edit', @routes.generate(:controller => 'people', :action => 'edit', :id => '1') - assert_equal '/people/1/edit.xml', @routes.generate(:controller => 'people', :action => 'edit', :id => '1', :format => 'xml') - assert_equal '/people/1/edit', @routes.generate(:use_route => 'edit_person', :id => '1') - assert_equal '/people/1?legacy=true', @routes.generate(:controller => 'people', :action => 'show', :id => '1', :legacy => 'true') - assert_equal '/people?legacy=true', @routes.generate(:controller => 'people', :action => 'index', :legacy => 'true') - - assert_equal '/id_default/2', @routes.generate(:controller => 'foo', :action => 'id_default', :id => '2') - assert_equal '/id_default', @routes.generate(:controller => 'foo', :action => 'id_default', :id => '1') - assert_equal '/id_default', @routes.generate(:controller => 'foo', :action => 'id_default', :id => 1) - assert_equal '/id_default', @routes.generate(:controller => 'foo', :action => 'id_default') - assert_equal '/optional/bar', @routes.generate(:controller => 'posts', :action => 'index', :optional => 'bar') - assert_equal '/posts', @routes.generate(:controller => 'posts', :action => 'index') - - assert_equal '/project', @routes.generate({:controller => 'project', :action => 'index'}) - assert_equal '/projects/1', @routes.generate({:controller => 'project', :action => 'index', :project_id => '1'}) - assert_equal '/projects/1', @routes.generate({:controller => 'project', :action => 'index'}, {:project_id => '1'}) - assert_raise(ActionController::RoutingError) { @routes.generate({:use_route => 'project', :controller => 'project', :action => 'index'}) } - assert_equal '/projects/1', @routes.generate({:use_route => 'project', :controller => 'project', :action => 'index', :project_id => '1'}) - assert_equal '/projects/1', @routes.generate({:use_route => 'project', :controller => 'project', :action => 'index'}, {:project_id => '1'}) - - assert_equal '/clients', @routes.generate(:controller => 'projects', :action => 'index') - assert_equal '/clients?project_id=1', @routes.generate(:controller => 'projects', :action => 'index', :project_id => '1') - assert_equal '/clients', @routes.generate({:controller => 'projects', :action => 'index'}, {:project_id => '1'}) - assert_equal '/clients', @routes.generate({:action => 'index'}, {:controller => 'projects', :action => 'index', :project_id => '1'}) - - assert_equal '/comment/20', @routes.generate({:id => 20}, {:controller => 'comments', :action => 'show'}) - assert_equal '/comment/20', @routes.generate(:controller => 'comments', :id => 20, :action => 'show') - assert_equal '/comments/boo', @routes.generate(:controller => 'comments', :action => 'boo') - - assert_equal '/ws/posts/show/1', @routes.generate(:controller => 'posts', :action => 'show', :id => '1', :ws => true) - assert_equal '/ws/posts', @routes.generate(:controller => 'posts', :action => 'index', :ws => true) - - assert_equal '/account', @routes.generate(:controller => 'account', :action => 'subscription') - assert_equal '/account/billing', @routes.generate(:controller => 'account', :action => 'billing') - - assert_equal '/pages/1/notes/show/1', @routes.generate(:page_id => '1', :controller => 'notes', :action => 'show', :id => '1') - assert_equal '/pages/1/notes/list', @routes.generate(:page_id => '1', :controller => 'notes', :action => 'list') - assert_equal '/pages/1/notes', @routes.generate(:page_id => '1', :controller => 'notes', :action => 'index') - assert_equal '/pages/1/notes', @routes.generate(:page_id => '1', :controller => 'notes') - assert_equal '/notes', @routes.generate(:page_id => nil, :controller => 'notes') - assert_equal '/notes', @routes.generate(:controller => 'notes') - assert_equal '/notes/print', @routes.generate(:controller => 'notes', :action => 'print') - assert_equal '/notes/print', @routes.generate({}, {:controller => 'notes', :action => 'print'}) - - assert_equal '/notes/index/1', @routes.generate({:controller => 'notes'}, {:controller => 'notes', :id => '1'}) - assert_equal '/notes/index/1', @routes.generate({:controller => 'notes'}, {:controller => 'notes', :id => '1', :foo => 'bar'}) - assert_equal '/notes/index/1', @routes.generate({:controller => 'notes'}, {:controller => 'notes', :id => '1'}) - assert_equal '/notes/index/1', @routes.generate({:action => 'index'}, {:controller => 'notes', :id => '1'}) - assert_equal '/notes/index/1', @routes.generate({}, {:controller => 'notes', :id => '1'}) - assert_equal '/notes/show/1', @routes.generate({}, {:controller => 'notes', :action => 'show', :id => '1'}) - assert_equal '/notes/index/1', @routes.generate({:controller => 'notes', :id => '1'}, {:foo => 'bar'}) - assert_equal '/posts', @routes.generate({:controller => 'posts'}, {:controller => 'notes', :action => 'show', :id => '1'}) - assert_equal '/notes/list', @routes.generate({:action => 'list'}, {:controller => 'notes', :action => 'show', :id => '1'}) - - assert_equal '/posts/ping', @routes.generate(:controller => 'posts', :action => 'ping') - assert_equal '/posts/show/1', @routes.generate(:controller => 'posts', :action => 'show', :id => '1') - assert_equal '/posts', @routes.generate(:controller => 'posts') - assert_equal '/posts', @routes.generate(:controller => 'posts', :action => 'index') - assert_equal '/posts', @routes.generate({:controller => 'posts'}, {:controller => 'posts', :action => 'index'}) - assert_equal '/posts/create', @routes.generate({:action => 'create'}, {:controller => 'posts'}) - assert_equal '/posts?foo=bar', @routes.generate(:controller => 'posts', :foo => 'bar') - assert_equal '/posts?foo[]=bar&foo[]=baz', @routes.generate(:controller => 'posts', :foo => ['bar', 'baz']) - assert_equal '/posts?page=2', @routes.generate(:controller => 'posts', :page => 2) - assert_equal '/posts?q[foo][a]=b', @routes.generate(:controller => 'posts', :q => { :foo => { :a => 'b'}}) - - assert_equal '/', @routes.generate(:controller => 'news', :action => 'index') - assert_equal '/', @routes.generate(:controller => 'news', :action => 'index', :format => nil) - assert_equal '/news.rss', @routes.generate(:controller => 'news', :action => 'index', :format => 'rss') - - - assert_raise(ActionController::RoutingError) { @routes.generate({:action => 'index'}) } + assert_equal '/admin/users', url_for(@routes, { :use_route => 'admin_users' }) + assert_equal '/admin/users', url_for(@routes, { :controller => 'admin/users' }) + assert_equal '/admin/users', url_for(@routes, { :controller => 'admin/users', :action => 'index' }) + assert_equal '/admin/users', url_for(@routes, { :action => 'index' }, { :controller => 'admin/users' }) + assert_equal '/admin/users', url_for(@routes, { :controller => 'users', :action => 'index' }, { :controller => 'admin/accounts' }) + assert_equal '/people', url_for(@routes, { :controller => '/people', :action => 'index' }, { :controller => 'admin/accounts' }) + + assert_equal '/admin/posts', url_for(@routes, { :controller => 'admin/posts' }) + assert_equal '/admin/posts/new', url_for(@routes, { :controller => 'admin/posts', :action => 'new' }) + + assert_equal '/blog/2009', url_for(@routes, { :controller => 'posts', :action => 'show_date', :year => 2009 }) + assert_equal '/blog/2009/1', url_for(@routes, { :controller => 'posts', :action => 'show_date', :year => 2009, :month => 1 }) + assert_equal '/blog/2009/1/1', url_for(@routes, { :controller => 'posts', :action => 'show_date', :year => 2009, :month => 1, :day => 1 }) + + assert_equal '/archive/2010', url_for(@routes, { :controller => 'archive', :action => 'index', :year => '2010' }) + assert_equal '/archive', url_for(@routes, { :controller => 'archive', :action => 'index' }) + assert_equal '/archive?year=january', url_for(@routes, { :controller => 'archive', :action => 'index', :year => 'january' }) + + assert_equal '/people', url_for(@routes, { :controller => 'people', :action => 'index' }) + assert_equal '/people', url_for(@routes, { :action => 'index' }, { :controller => 'people' }) + assert_equal '/people', url_for(@routes, { :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' }) + assert_equal '/people', url_for(@routes, { :controller => 'people', :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' }) + assert_equal '/people', url_for(@routes, {}, { :controller => 'people', :action => 'index' }) + assert_equal '/people/1', url_for(@routes, { :controller => 'people', :action => 'show' }, { :controller => 'people', :action => 'show', :id => '1' }) + assert_equal '/people/new', url_for(@routes, { :use_route => 'new_person' }) + assert_equal '/people/new', url_for(@routes, { :controller => 'people', :action => 'new' }) + assert_equal '/people/1', url_for(@routes, { :use_route => 'person', :id => '1' }) + assert_equal '/people/1', url_for(@routes, { :controller => 'people', :action => 'show', :id => '1' }) + assert_equal '/people/1.xml', url_for(@routes, { :controller => 'people', :action => 'show', :id => '1', :format => 'xml' }) + assert_equal '/people/1', url_for(@routes, { :controller => 'people', :action => 'show', :id => 1 }) + assert_equal '/people/1', url_for(@routes, { :controller => 'people', :action => 'show', :id => Model.new('1') }) + assert_equal '/people/1', url_for(@routes, { :action => 'show', :id => '1' }, { :controller => 'people', :action => 'index' }) + assert_equal '/people/1', url_for(@routes, { :action => 'show', :id => 1 }, { :controller => 'people', :action => 'show', :id => '1' }) + assert_equal '/people', url_for(@routes, { :controller => 'people', :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' }) + assert_equal '/people/1', url_for(@routes, {}, { :controller => 'people', :action => 'show', :id => '1' }) + assert_equal '/people/1', url_for(@routes, { :controller => 'people', :action => 'show' }, { :controller => 'people', :action => 'index', :id => '1' }) + assert_equal '/people/1/edit', url_for(@routes, { :controller => 'people', :action => 'edit', :id => '1' }) + assert_equal '/people/1/edit.xml', url_for(@routes, { :controller => 'people', :action => 'edit', :id => '1', :format => 'xml' }) + assert_equal '/people/1/edit', url_for(@routes, { :use_route => 'edit_person', :id => '1' }) + assert_equal '/people/1?legacy=true', url_for(@routes, { :controller => 'people', :action => 'show', :id => '1', :legacy => 'true' }) + assert_equal '/people?legacy=true', url_for(@routes, { :controller => 'people', :action => 'index', :legacy => 'true' }) + + assert_equal '/id_default/2', url_for(@routes, { :controller => 'foo', :action => 'id_default', :id => '2' }) + assert_equal '/id_default', url_for(@routes, { :controller => 'foo', :action => 'id_default', :id => '1' }) + assert_equal '/id_default', url_for(@routes, { :controller => 'foo', :action => 'id_default', :id => 1 }) + assert_equal '/id_default', url_for(@routes, { :controller => 'foo', :action => 'id_default' }) + assert_equal '/optional/bar', url_for(@routes, { :controller => 'posts', :action => 'index', :optional => 'bar' }) + assert_equal '/posts', url_for(@routes, { :controller => 'posts', :action => 'index' }) + + assert_equal '/project', url_for(@routes, { :controller => 'project', :action => 'index' }) + assert_equal '/projects/1', url_for(@routes, { :controller => 'project', :action => 'index', :project_id => '1' }) + assert_equal '/projects/1', url_for(@routes, { :controller => 'project', :action => 'index'}, {:project_id => '1' }) + assert_raise(ActionController::RoutingError) { url_for(@routes, { :use_route => 'project', :controller => 'project', :action => 'index' }) } + assert_equal '/projects/1', url_for(@routes, { :use_route => 'project', :controller => 'project', :action => 'index', :project_id => '1' }) + assert_equal '/projects/1', url_for(@routes, { :use_route => 'project', :controller => 'project', :action => 'index' }, { :project_id => '1' }) + + assert_equal '/clients', url_for(@routes, { :controller => 'projects', :action => 'index' }) + assert_equal '/clients?project_id=1', url_for(@routes, { :controller => 'projects', :action => 'index', :project_id => '1' }) + assert_equal '/clients', url_for(@routes, { :controller => 'projects', :action => 'index' }, { :project_id => '1' }) + assert_equal '/clients', url_for(@routes, { :action => 'index' }, { :controller => 'projects', :action => 'index', :project_id => '1' }) + + assert_equal '/comment/20', url_for(@routes, { :id => 20 }, { :controller => 'comments', :action => 'show' }) + assert_equal '/comment/20', url_for(@routes, { :controller => 'comments', :id => 20, :action => 'show' }) + assert_equal '/comments/boo', url_for(@routes, { :controller => 'comments', :action => 'boo' }) + + assert_equal '/ws/posts/show/1', url_for(@routes, { :controller => 'posts', :action => 'show', :id => '1', :ws => true }) + assert_equal '/ws/posts', url_for(@routes, { :controller => 'posts', :action => 'index', :ws => true }) + + assert_equal '/account', url_for(@routes, { :controller => 'account', :action => 'subscription' }) + assert_equal '/account/billing', url_for(@routes, { :controller => 'account', :action => 'billing' }) + + assert_equal '/pages/1/notes/show/1', url_for(@routes, { :page_id => '1', :controller => 'notes', :action => 'show', :id => '1' }) + assert_equal '/pages/1/notes/list', url_for(@routes, { :page_id => '1', :controller => 'notes', :action => 'list' }) + assert_equal '/pages/1/notes', url_for(@routes, { :page_id => '1', :controller => 'notes', :action => 'index' }) + assert_equal '/pages/1/notes', url_for(@routes, { :page_id => '1', :controller => 'notes' }) + assert_equal '/notes', url_for(@routes, { :page_id => nil, :controller => 'notes' }) + assert_equal '/notes', url_for(@routes, { :controller => 'notes' }) + assert_equal '/notes/print', url_for(@routes, { :controller => 'notes', :action => 'print' }) + assert_equal '/notes/print', url_for(@routes, {}, { :controller => 'notes', :action => 'print' }) + + assert_equal '/notes/index/1', url_for(@routes, { :controller => 'notes' }, { :controller => 'notes', :id => '1' }) + assert_equal '/notes/index/1', url_for(@routes, { :controller => 'notes' }, { :controller => 'notes', :id => '1', :foo => 'bar' }) + assert_equal '/notes/index/1', url_for(@routes, { :controller => 'notes' }, { :controller => 'notes', :id => '1' }) + assert_equal '/notes/index/1', url_for(@routes, { :action => 'index' }, { :controller => 'notes', :id => '1' }) + assert_equal '/notes/index/1', url_for(@routes, {}, { :controller => 'notes', :id => '1' }) + assert_equal '/notes/show/1', url_for(@routes, {}, { :controller => 'notes', :action => 'show', :id => '1' }) + assert_equal '/notes/index/1', url_for(@routes, { :controller => 'notes', :id => '1' }, { :foo => 'bar' }) + assert_equal '/posts', url_for(@routes, { :controller => 'posts' }, { :controller => 'notes', :action => 'show', :id => '1' }) + assert_equal '/notes/list', url_for(@routes, { :action => 'list' }, { :controller => 'notes', :action => 'show', :id => '1' }) + + assert_equal '/posts/ping', url_for(@routes, { :controller => 'posts', :action => 'ping' }) + assert_equal '/posts/show/1', url_for(@routes, { :controller => 'posts', :action => 'show', :id => '1' }) + assert_equal '/posts', url_for(@routes, { :controller => 'posts' }) + assert_equal '/posts', url_for(@routes, { :controller => 'posts', :action => 'index' }) + assert_equal '/posts', url_for(@routes, { :controller => 'posts' }, { :controller => 'posts', :action => 'index' }) + assert_equal '/posts/create', url_for(@routes, { :action => 'create' }, { :controller => 'posts' }) + assert_equal '/posts?foo=bar', url_for(@routes, { :controller => 'posts', :foo => 'bar' }) + assert_equal '/posts?foo%5B%5D=bar&foo%5B%5D=baz', url_for(@routes, { :controller => 'posts', :foo => ['bar', 'baz'] }) + assert_equal '/posts?page=2', url_for(@routes, { :controller => 'posts', :page => 2 }) + assert_equal '/posts?q%5Bfoo%5D%5Ba%5D=b', url_for(@routes, { :controller => 'posts', :q => { :foo => { :a => 'b'}} }) + + assert_equal '/news.rss', url_for(@routes, { :controller => 'news', :action => 'index', :format => 'rss' }) + + + assert_raise(ActionController::RoutingError) { url_for(@routes, { :action => 'index' }) } end def test_generate_extras diff --git a/actionpack/test/controller/runner_test.rb b/actionpack/test/controller/runner_test.rb new file mode 100644 index 0000000000..24c220dcd5 --- /dev/null +++ b/actionpack/test/controller/runner_test.rb @@ -0,0 +1,22 @@ +require 'abstract_unit' +require 'action_dispatch/testing/integration' + +module ActionDispatch + class RunnerTest < Test::Unit::TestCase + class MyRunner + include Integration::Runner + + def initialize(session) + @integration_session = session + end + + def hi; end + end + + def test_respond_to? + runner = MyRunner.new(Class.new { def x; end }.new) + assert runner.respond_to?(:hi) + assert runner.respond_to?(:x) + end + end +end diff --git a/actionpack/test/controller/selector_test.rb b/actionpack/test/controller/selector_test.rb index 5a5dc840b5..8ce9e43402 100644 --- a/actionpack/test/controller/selector_test.rb +++ b/actionpack/test/controller/selector_test.rb @@ -437,7 +437,7 @@ class SelectorTest < Test::Unit::TestCase assert_equal "4", @matches[1].attributes["id"] end - + def test_first_and_last parse(%Q{<table><thead></thead><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>}) # First child. @@ -471,7 +471,7 @@ class SelectorTest < Test::Unit::TestCase end - def test_first_and_last + def test_only_child_and_only_type_first_and_last # Only child. parse(%Q{<table><tr></tr></table>}) select("table:only-child") @@ -503,7 +503,7 @@ class SelectorTest < Test::Unit::TestCase assert_equal 1, @matches.size end - + def test_content parse(%Q{<div> </div>}) select("div:content()") @@ -582,7 +582,7 @@ class SelectorTest < Test::Unit::TestCase assert_equal "foo", @matches[0].attributes["title"] end - + def test_pseudo_class_negation parse(%Q{<div><p id="1"></p><p id="2"></p></div>}) select("p") @@ -594,7 +594,7 @@ class SelectorTest < Test::Unit::TestCase assert_equal 1, @matches.size assert_equal "1", @matches[0].attributes["id"] end - + def test_negation_details parse(%Q{<p id="1"></p><p id="2"></p><p id="3"></p>}) diff --git a/actionpack/test/controller/test_test.rb b/actionpack/test/controller/test_test.rb index 13c9d9ee38..edda0d0a30 100644 --- a/actionpack/test/controller/test_test.rb +++ b/actionpack/test/controller/test_test.rb @@ -135,6 +135,11 @@ XML @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new @request.env['PATH_INFO'] = nil + @routes = ActionDispatch::Routing::RouteSet.new.tap do |r| + r.draw do + match ':controller(/:action(/:id))' + end + end end def test_raw_post_handling @@ -454,18 +459,26 @@ XML def test_assert_routing_with_method with_routing do |set| - set.draw { |map| map.resources(:content) } + set.draw { resources(:content) } assert_routing({ :method => 'post', :path => 'content' }, { :controller => 'content', :action => 'create' }) end end def test_assert_routing_in_module - assert_routing 'admin/user', :controller => 'admin/user', :action => 'index' + with_routing do |set| + set.draw do + namespace :admin do + match 'user' => 'user#index' + end + end + + assert_routing 'admin/user', :controller => 'admin/user', :action => 'index' + end end - + def test_assert_routing_with_glob with_routing do |set| - set.draw { |map| match('*path' => "pages#show") } + set.draw { match('*path' => "pages#show") } assert_routing('/company/about', { :controller => 'pages', :action => 'show', :path => 'company/about' }) end end @@ -487,7 +500,7 @@ XML def test_array_path_parameter_handled_properly with_routing do |set| - set.draw do |map| + set.draw do match 'file/*path', :to => 'test_test/test#test_params' match ':controller/:action' end @@ -546,7 +559,15 @@ XML assert_equal "bar", @request.params[:foo] @request.recycle! post :no_op - assert @request.params[:foo].blank? + assert_blank @request.params[:foo] + end + + def test_symbolized_path_params_reset_after_request + get :test_params, :id => "foo" + assert_equal "foo", @request.symbolized_path_parameters[:id] + @request.recycle! + get :test_params + assert_nil @request.symbolized_path_parameters[:id] end def test_should_have_knowledge_of_client_side_cookie_state_even_if_they_are_not_set @@ -570,7 +591,7 @@ XML assert false, "expected RuntimeError, got nothing" rescue RuntimeError => error assert true - assert_match %r{@#{variable} is nil}, error.message + assert_match(%r{@#{variable} is nil}, error.message) rescue => error assert false, "expected RuntimeError, got #{error.class}" end @@ -653,13 +674,6 @@ XML assert_redirected_to 'created resource' end end - - def test_binary_content_works_with_send_file - get :test_send_file - assert_deprecated do - assert_nothing_raised(NoMethodError) { @response.binary_content } - end - end end class InferringClassNameTest < ActionController::TestCase @@ -694,7 +708,7 @@ class NamedRoutesControllerTest < ActionController::TestCase def test_should_be_able_to_use_named_routes_before_a_request_is_done with_routing do |set| - set.draw { |map| resources :contents } + set.draw { resources :contents } assert_equal 'http://test.host/contents/new', new_content_url assert_equal 'http://test.host/contents/1', content_url(:id => 1) end diff --git a/actionpack/test/controller/url_for_test.rb b/actionpack/test/controller/url_for_test.rb index 71a4a43477..3f3d6dcc2f 100644 --- a/actionpack/test/controller/url_for_test.rb +++ b/actionpack/test/controller/url_for_test.rb @@ -5,7 +5,7 @@ module AbstractController class UrlForTests < ActionController::TestCase class W - include SharedTestRoutes.url_helpers + include ActionDispatch::Routing::RouteSet.new.tap { |r| r.draw { match ':controller(/:action(/:id(.:format)))' } }.url_helpers end def teardown @@ -17,7 +17,7 @@ module AbstractController end def test_exception_is_thrown_without_host - assert_raise RuntimeError do + assert_raise ArgumentError do W.new.url_for :controller => 'c', :action => 'a', :id => 'i' end end @@ -60,6 +60,27 @@ module AbstractController ) end + def test_subdomain_may_be_changed + add_host! + assert_equal('http://api.basecamphq.com/c/a/i', + W.new.url_for(:subdomain => 'api', :controller => 'c', :action => 'a', :id => 'i') + ) + end + + def test_domain_may_be_changed + add_host! + assert_equal('http://www.37signals.com/c/a/i', + W.new.url_for(:domain => '37signals.com', :controller => 'c', :action => 'a', :id => 'i') + ) + end + + def test_tld_length_may_be_changed + add_host! + assert_equal('http://mobile.www.basecamphq.com/c/a/i', + W.new.url_for(:subdomain => 'mobile', :tld_length => 2, :controller => 'c', :action => 'a', :id => 'i') + ) + end + def test_port add_host! assert_equal('http://www.basecamphq.com:3000/c/a/i', @@ -74,16 +95,29 @@ module AbstractController ) end - def test_protocol_with_and_without_separator + def test_protocol_with_and_without_separators add_host! assert_equal('https://www.basecamphq.com/c/a/i', W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => 'https') ) assert_equal('https://www.basecamphq.com/c/a/i', + W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => 'https:') + ) + assert_equal('https://www.basecamphq.com/c/a/i', W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => 'https://') ) end + def test_without_protocol + add_host! + assert_equal('//www.basecamphq.com/c/a/i', + W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => '//') + ) + assert_equal('//www.basecamphq.com/c/a/i', + W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => false) + ) + end + def test_trailing_slash add_host! options = {:controller => 'foo', :trailing_slash => true, :action => 'bar', :id => '33'} @@ -130,7 +164,7 @@ module AbstractController def test_named_routes with_routing do |set| - set.draw do |map| + set.draw do match 'this/is/verbose', :to => 'home#index', :as => :no_args match 'home/sweet/home/:user', :to => 'home#index', :as => :home end @@ -151,7 +185,7 @@ module AbstractController def test_relative_url_root_is_respected_for_named_routes with_routing do |set| - set.draw do |map| + set.draw do match '/home/sweet/home/:user', :to => 'home#index', :as => :home end @@ -165,7 +199,7 @@ module AbstractController def test_only_path with_routing do |set| - set.draw do |map| + set.draw do match 'home/sweet/home/:user', :to => 'home#index', :as => :home match ':controller/:action/:id' end @@ -219,7 +253,7 @@ module AbstractController def test_hash_recursive_and_array_parameters url = W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :id => 101, :query => {:person => {:name => 'Bob', :position => ['prof', 'art director']}, :hobby => 'piercing'}) - assert_match %r(^/c/a/101), url + assert_match(%r(^/c/a/101), url) params = extract_params(url) assert_equal params[0], { 'query[hobby]' => 'piercing' }.to_query assert_equal params[1], { 'query[person][name]' => 'Bob' }.to_query @@ -233,7 +267,7 @@ module AbstractController def test_named_routes_with_nil_keys with_routing do |set| - set.draw do |map| + set.draw do match 'posts.:format', :to => 'posts#index', :as => :posts match '/', :to => 'posts#index', :as => :main end @@ -277,6 +311,14 @@ module AbstractController assert_equal("/c/a", W.new.url_for(HashWithIndifferentAccess.new('controller' => 'c', 'action' => 'a', 'only_path' => true))) end + def test_url_params_with_nil_to_param_are_not_in_url + assert_equal("/c/a", W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :id => Struct.new(:to_param).new(nil))) + end + + def test_false_url_params_are_included_in_query + assert_equal("/c/a?show=false", W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :show => false)) + end + private def extract_params(url) url.split('?', 2).last.split('&').sort diff --git a/actionpack/test/controller/url_rewriter_test.rb b/actionpack/test/controller/url_rewriter_test.rb index a8d7b75372..89de4c1da4 100644 --- a/actionpack/test/controller/url_rewriter_test.rb +++ b/actionpack/test/controller/url_rewriter_test.rb @@ -19,6 +19,11 @@ class UrlRewriterTests < ActionController::TestCase @request = ActionController::TestRequest.new @params = {} @rewriter = Rewriter.new(@request) #.new(@request, @params) + @routes = ActionDispatch::Routing::RouteSet.new.tap do |r| + r.draw do + match ':controller(/:action(/:id))' + end + end end def test_port diff --git a/actionpack/test/controller/view_paths_test.rb b/actionpack/test/controller/view_paths_test.rb index b8972b04b6..edfcb5cc4d 100644 --- a/actionpack/test/controller/view_paths_test.rb +++ b/actionpack/test/controller/view_paths_test.rb @@ -20,7 +20,7 @@ class ViewLoadPathsTest < ActionController::TestCase layout 'test/sub' def hello_world; render(:template => 'test/hello_world'); end end - + def setup # TestController.view_paths = nil @@ -64,7 +64,7 @@ class ViewLoadPathsTest < ActionController::TestCase @controller.append_view_path(%w(bar baz)) assert_paths(FIXTURE_LOAD_PATH, "foo", "bar", "baz") - + @controller.append_view_path(FIXTURE_LOAD_PATH) assert_paths(FIXTURE_LOAD_PATH, "foo", "bar", "baz", FIXTURE_LOAD_PATH) end diff --git a/actionpack/test/controller/webservice_test.rb b/actionpack/test/controller/webservice_test.rb index 5942950b15..621fb79915 100644 --- a/actionpack/test/controller/webservice_test.rb +++ b/actionpack/test/controller/webservice_test.rb @@ -1,6 +1,6 @@ require 'abstract_unit' -class WebServiceTest < ActionController::IntegrationTest +class WebServiceTest < ActionDispatch::IntegrationTest class TestController < ActionController::Base def assign_parameters if params[:full] @@ -24,12 +24,13 @@ class WebServiceTest < ActionController::IntegrationTest def setup @controller = TestController.new + @integration_session = nil end def test_check_parameters with_test_route_set do get "/" - assert @controller.response.body.blank? + assert_blank @controller.response.body end end @@ -161,7 +162,7 @@ class WebServiceTest < ActionController::IntegrationTest def test_use_xml_ximple_with_empty_request with_test_route_set do assert_nothing_raised { post "/", "", {'CONTENT_TYPE' => 'application/xml'} } - assert @controller.response.body.blank? + assert_blank @controller.response.body end end @@ -215,7 +216,7 @@ class WebServiceTest < ActionController::IntegrationTest def test_typecast_as_yaml with_test_route_set do with_params_parsers Mime::YAML => :yaml do - yaml = <<-YAML + yaml = (<<-YAML).strip --- data: a: 15 @@ -254,7 +255,7 @@ class WebServiceTest < ActionController::IntegrationTest def with_test_route_set with_routing do |set| - set.draw do |map| + set.draw do match '/', :to => 'web_service_test/test#assign_parameters' end yield diff --git a/actionpack/test/dispatch/callbacks_test.rb b/actionpack/test/dispatch/callbacks_test.rb index 9df882ce75..eed2eca2ab 100644 --- a/actionpack/test/dispatch/callbacks_test.rb +++ b/actionpack/test/dispatch/callbacks_test.rb @@ -1,6 +1,6 @@ require 'abstract_unit' -class DispatcherTest < Test::Unit::TestCase +class DispatcherTest < ActiveSupport::TestCase class Foo cattr_accessor :a, :b end @@ -9,69 +9,13 @@ class DispatcherTest < Test::Unit::TestCase def call(env) [200, {}, 'response'] end - end + end def setup Foo.a, Foo.b = 0, 0 - ActionDispatch::Callbacks.reset_callbacks(:prepare) ActionDispatch::Callbacks.reset_callbacks(:call) end - def test_prepare_callbacks_with_cache_classes - a = b = c = nil - ActionDispatch::Callbacks.to_prepare { |*args| a = b = c = 1 } - ActionDispatch::Callbacks.to_prepare { |*args| b = c = 2 } - ActionDispatch::Callbacks.to_prepare { |*args| c = 3 } - - # Ensure to_prepare callbacks are not run when defined - assert_nil a || b || c - - # Run callbacks - dispatch - - assert_equal 1, a - assert_equal 2, b - assert_equal 3, c - - # Make sure they are only run once - a = b = c = nil - dispatch - assert_nil a || b || c - end - - def test_prepare_callbacks_without_cache_classes - a = b = c = nil - ActionDispatch::Callbacks.to_prepare { |*args| a = b = c = 1 } - ActionDispatch::Callbacks.to_prepare { |*args| b = c = 2 } - ActionDispatch::Callbacks.to_prepare { |*args| c = 3 } - - # Ensure to_prepare callbacks are not run when defined - assert_nil a || b || c - - # Run callbacks - dispatch(false) - - assert_equal 1, a - assert_equal 2, b - assert_equal 3, c - - # Make sure they are run again - a = b = c = nil - dispatch(false) - assert_equal 1, a - assert_equal 2, b - assert_equal 3, c - end - - def test_to_prepare_with_identifier_replaces - ActionDispatch::Callbacks.to_prepare(:unique_id) { |*args| Foo.a, Foo.b = 1, 1 } - ActionDispatch::Callbacks.to_prepare(:unique_id) { |*args| Foo.a = 2 } - - dispatch - assert_equal 2, Foo.a - assert_equal 0, Foo.b - end - def test_before_and_after_callbacks ActionDispatch::Callbacks.before { |*args| Foo.a += 1; Foo.b += 1 } ActionDispatch::Callbacks.after { |*args| Foo.a += 1; Foo.b += 1 } @@ -85,10 +29,22 @@ class DispatcherTest < Test::Unit::TestCase assert_equal 4, Foo.b end + def test_to_prepare_and_cleanup_delegation + prepared = cleaned = false + ActionDispatch::Callbacks.to_prepare { prepared = true } + ActionDispatch::Callbacks.to_prepare { cleaned = true } + + ActionDispatch::Reloader.prepare! + assert prepared + + ActionDispatch::Reloader.cleanup! + assert cleaned + end + private - def dispatch(cache_classes = true, &block) - @dispatcher ||= ActionDispatch::Callbacks.new(block || DummyApp.new, !cache_classes) + def dispatch(&block) + @dispatcher ||= ActionDispatch::Callbacks.new(block || DummyApp.new) @dispatcher.call({'rack.input' => StringIO.new('')}) end diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb index b04c1a42c0..39159fd629 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -48,6 +48,11 @@ class CookiesTest < ActionController::TestCase head :ok end + def authenticate_with_secure + cookies["user_name"] = { :value => "david", :secure => true } + head :ok + end + def set_permanent_cookie cookies.permanent[:user_name] = "Jamie" head :ok @@ -89,6 +94,50 @@ class CookiesTest < ActionController::TestCase cookies.delete(:user_name, :domain => :all) head :ok end + + def set_cookie_with_domain_and_tld + cookies[:user_name] = {:value => "rizwanreza", :domain => :all, :tld_length => 2} + head :ok + end + + def delete_cookie_with_domain_and_tld + cookies.delete(:user_name, :domain => :all, :tld_length => 2) + head :ok + end + + def set_cookie_with_domains + cookies[:user_name] = {:value => "rizwanreza", :domain => %w(example1.com example2.com .example3.com)} + head :ok + end + + def delete_cookie_with_domains + cookies.delete(:user_name, :domain => %w(example1.com example2.com .example3.com)) + head :ok + end + + def symbol_key + cookies[:user_name] = "david" + head :ok + end + + def string_key + cookies['user_name'] = "david" + head :ok + end + + def symbol_key_mock + cookies[:user_name] = "david" if cookies[:user_name] == "andrew" + head :ok + end + + def string_key_mock + cookies['user_name'] = "david" if cookies['user_name'] == "andrew" + head :ok + end + + def noop + head :ok + end end tests TestController @@ -129,6 +178,26 @@ class CookiesTest < ActionController::TestCase assert_equal({"user_name" => "david"}, @response.cookies) end + def test_setting_cookie_with_secure + @request.env["HTTPS"] = "on" + get :authenticate_with_secure + assert_cookie_header "user_name=david; path=/; secure" + assert_equal({"user_name" => "david"}, @response.cookies) + end + + def test_setting_cookie_with_secure_in_development + Rails.env.stubs(:development?).returns(true) + get :authenticate_with_secure + assert_cookie_header "user_name=david; path=/; secure" + assert_equal({"user_name" => "david"}, @response.cookies) + end + + def test_not_setting_cookie_with_secure + get :authenticate_with_secure + assert_not_cookie_header "user_name=david; path=/; secure" + assert_not_equal({"user_name" => "david"}, @response.cookies) + end + def test_multiple_cookies get :set_multiple_cookies assert_equal 2, @response.cookies.size @@ -158,8 +227,8 @@ class CookiesTest < ActionController::TestCase def test_permanent_cookie get :set_permanent_cookie - assert_match /Jamie/, @response.headers["Set-Cookie"] - assert_match %r(#{20.years.from_now.utc.year}), @response.headers["Set-Cookie"] + assert_match(/Jamie/, @response.headers["Set-Cookie"]) + assert_match(%r(#{20.years.from_now.utc.year}), @response.headers["Set-Cookie"]) end def test_signed_cookie @@ -174,7 +243,7 @@ class CookiesTest < ActionController::TestCase def test_permanent_signed_cookie get :set_permanent_signed_cookie - assert_match %r(#{20.years.from_now.utc.year}), @response.headers["Set-Cookie"] + assert_match(%r(#{20.years.from_now.utc.year}), @response.headers["Set-Cookie"]) assert_equal 100, @controller.send(:cookies).signed[:remember_me] end @@ -232,12 +301,181 @@ class CookiesTest < ActionController::TestCase assert_cookie_header "user_name=rizwanreza; domain=.nextangle.com; path=/" end + def test_cookie_with_all_domain_option_using_a_non_standard_tld + @request.host = "two.subdomains.nextangle.local" + get :set_cookie_with_domain + assert_response :success + assert_cookie_header "user_name=rizwanreza; domain=.nextangle.local; path=/" + end + + def test_cookie_with_all_domain_option_using_australian_style_tld + @request.host = "nextangle.com.au" + get :set_cookie_with_domain + assert_response :success + assert_cookie_header "user_name=rizwanreza; domain=.nextangle.com.au; path=/" + end + + def test_cookie_with_all_domain_option_using_uk_style_tld + @request.host = "nextangle.co.uk" + get :set_cookie_with_domain + assert_response :success + assert_cookie_header "user_name=rizwanreza; domain=.nextangle.co.uk; path=/" + end + + def test_cookie_with_all_domain_option_using_host_with_port + @request.host = "nextangle.local:3000" + get :set_cookie_with_domain + assert_response :success + assert_cookie_header "user_name=rizwanreza; domain=.nextangle.local; path=/" + end + + def test_cookie_with_all_domain_option_using_localhost + @request.host = "localhost" + get :set_cookie_with_domain + assert_response :success + assert_cookie_header "user_name=rizwanreza; path=/" + end + + def test_cookie_with_all_domain_option_using_ipv4_address + @request.host = "192.168.1.1" + get :set_cookie_with_domain + assert_response :success + assert_cookie_header "user_name=rizwanreza; path=/" + end + + def test_cookie_with_all_domain_option_using_ipv6_address + @request.host = "2001:0db8:85a3:0000:0000:8a2e:0370:7334" + get :set_cookie_with_domain + assert_response :success + assert_cookie_header "user_name=rizwanreza; path=/" + end + def test_deleting_cookie_with_all_domain_option get :delete_cookie_with_domain assert_response :success assert_cookie_header "user_name=; domain=.nextangle.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT" end + def test_cookie_with_all_domain_option_and_tld_length + get :set_cookie_with_domain_and_tld + assert_response :success + assert_cookie_header "user_name=rizwanreza; domain=.nextangle.com; path=/" + end + + def test_cookie_with_all_domain_option_using_a_non_standard_tld_and_tld_length + @request.host = "two.subdomains.nextangle.local" + get :set_cookie_with_domain_and_tld + assert_response :success + assert_cookie_header "user_name=rizwanreza; domain=.nextangle.local; path=/" + end + + def test_cookie_with_all_domain_option_using_host_with_port_and_tld_length + @request.host = "nextangle.local:3000" + get :set_cookie_with_domain_and_tld + assert_response :success + assert_cookie_header "user_name=rizwanreza; domain=.nextangle.local; path=/" + end + + def test_deleting_cookie_with_all_domain_option_and_tld_length + get :delete_cookie_with_domain_and_tld + assert_response :success + assert_cookie_header "user_name=; domain=.nextangle.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT" + end + + def test_cookie_with_several_preset_domains_using_one_of_these_domains + @request.host = "example1.com" + get :set_cookie_with_domains + assert_response :success + assert_cookie_header "user_name=rizwanreza; domain=example1.com; path=/" + end + + def test_cookie_with_several_preset_domains_using_other_domain + @request.host = "other-domain.com" + get :set_cookie_with_domains + assert_response :success + assert_cookie_header "user_name=rizwanreza; path=/" + end + + def test_cookie_with_several_preset_domains_using_shared_domain + @request.host = "example3.com" + get :set_cookie_with_domains + assert_response :success + assert_cookie_header "user_name=rizwanreza; domain=.example3.com; path=/" + end + + def test_deletings_cookie_with_several_preset_domains_using_one_of_these_domains + @request.host = "example2.com" + get :delete_cookie_with_domains + assert_response :success + assert_cookie_header "user_name=; domain=example2.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT" + end + + def test_deletings_cookie_with_several_preset_domains_using_other_domain + @request.host = "other-domain.com" + get :delete_cookie_with_domains + assert_response :success + assert_cookie_header "user_name=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT" + end + + def test_cookies_hash_is_indifferent_access + [:symbol_key, :string_key].each do |cookie_key| + get cookie_key + assert_equal "david", cookies[:user_name] + assert_equal "david", cookies['user_name'] + end + end + + def test_setting_request_cookies_is_indifferent_access + @request.cookies.clear + @request.cookies[:user_name] = "andrew" + get :string_key_mock + assert_equal "david", cookies[:user_name] + + @request.cookies.clear + @request.cookies['user_name'] = "andrew" + get :symbol_key_mock + assert_equal "david", cookies['user_name'] + end + + def test_cookies_retained_across_requests + get :symbol_key + assert_equal "user_name=david; path=/", @response.headers["Set-Cookie"] + assert_equal "david", cookies[:user_name] + + get :noop + assert_nil @response.headers["Set-Cookie"] + assert_equal "user_name=david", @request.env['HTTP_COOKIE'] + assert_equal "david", cookies[:user_name] + + get :noop + assert_nil @response.headers["Set-Cookie"] + assert_equal "user_name=david", @request.env['HTTP_COOKIE'] + assert_equal "david", cookies[:user_name] + end + + def test_cookies_can_be_cleared + get :symbol_key + assert_equal "user_name=david; path=/", @response.headers["Set-Cookie"] + assert_equal "david", cookies[:user_name] + + @request.cookies.clear + get :noop + assert_nil @response.headers["Set-Cookie"] + assert_nil @request.env['HTTP_COOKIE'] + assert_nil cookies[:user_name] + + get :symbol_key + assert_equal "user_name=david; path=/", @response.headers["Set-Cookie"] + assert_equal "david", cookies[:user_name] + end + + def test_cookies_are_escaped + @request.cookies[:user_ids] = '1;2' + get :noop + assert_equal "user_ids=1%3B2", @request.env['HTTP_COOKIE'] + assert_equal "1;2", cookies[:user_ids] + end + private def assert_cookie_header(expected) header = @response.headers["Set-Cookie"] @@ -247,4 +485,13 @@ class CookiesTest < ActionController::TestCase assert_equal expected.split("\n"), header end end -end
\ No newline at end of file + + def assert_not_cookie_header(expected) + header = @response.headers["Set-Cookie"] + if header.respond_to?(:to_str) + assert_not_equal expected.split("\n").sort, header.split("\n").sort + else + assert_not_equal expected.split("\n"), header + end + end +end diff --git a/actionpack/test/dispatch/middleware_stack/middleware_test.rb b/actionpack/test/dispatch/middleware_stack/middleware_test.rb new file mode 100644 index 0000000000..9607f026db --- /dev/null +++ b/actionpack/test/dispatch/middleware_stack/middleware_test.rb @@ -0,0 +1,77 @@ +require 'abstract_unit' +require 'action_dispatch/middleware/stack' + +module ActionDispatch + class MiddlewareStack + class MiddlewareTest < ActiveSupport::TestCase + class Omg; end + + { + 'concrete' => Omg, + 'anonymous' => Class.new + }.each do |name, klass| + + define_method("test_#{name}_klass") do + mw = Middleware.new klass + assert_equal klass, mw.klass + end + + define_method("test_#{name}_==") do + mw1 = Middleware.new klass + mw2 = Middleware.new klass + assert_equal mw1, mw2 + end + + end + + def test_string_class + mw = Middleware.new Omg.name + assert_equal Omg, mw.klass + end + + def test_double_equal_works_with_classes + k = Class.new + mw = Middleware.new k + assert_operator mw, :==, k + + result = mw != Class.new + assert result, 'middleware should not equal other anon class' + end + + def test_double_equal_works_with_strings + mw = Middleware.new Omg + assert_operator mw, :==, Omg.name + end + + def test_double_equal_normalizes_strings + mw = Middleware.new Omg + assert_operator mw, :==, "::#{Omg.name}" + end + + def test_middleware_loads_classnames_from_cache + mw = Class.new(Middleware) { + attr_accessor :classcache + }.new(Omg.name) + + fake_cache = { mw.name => Omg } + mw.classcache = fake_cache + + assert_equal Omg, mw.klass + + fake_cache[mw.name] = Middleware + assert_equal Middleware, mw.klass + end + + def test_middleware_always_returns_class + mw = Class.new(Middleware) { + attr_accessor :classcache + }.new(Omg) + + fake_cache = { mw.name => Middleware } + mw.classcache = fake_cache + + assert_equal Omg, mw.klass + end + end + end +end diff --git a/actionpack/test/dispatch/middleware_stack_test.rb b/actionpack/test/dispatch/middleware_stack_test.rb index 6a1a4f556f..831f3db3e2 100644 --- a/actionpack/test/dispatch/middleware_stack_test.rb +++ b/actionpack/test/dispatch/middleware_stack_test.rb @@ -4,6 +4,12 @@ class MiddlewareStackTest < ActiveSupport::TestCase class FooMiddleware; end class BarMiddleware; end class BazMiddleware; end + class BlockMiddleware + attr_reader :block + def initialize(&block) + @block = block + end + end def setup @stack = ActionDispatch::MiddlewareStack.new @@ -39,7 +45,16 @@ class MiddlewareStackTest < ActiveSupport::TestCase assert_equal BazMiddleware, @stack.last.klass assert_equal([true, {:foo => "bar"}], @stack.last.args) end - + + test "use should push middleware class with block arguments onto the stack" do + proc = Proc.new {} + assert_difference "@stack.size" do + @stack.use(BlockMiddleware, &proc) + end + assert_equal BlockMiddleware, @stack.last.klass + assert_equal proc, @stack.last.block + end + test "insert inserts middleware at the integer index" do @stack.insert(1, BazMiddleware) assert_equal BazMiddleware, @stack[1].klass diff --git a/actionpack/test/dispatch/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb index 369212e2d0..11cf68fdb3 100644 --- a/actionpack/test/dispatch/mime_type_test.rb +++ b/actionpack/test/dispatch/mime_type_test.rb @@ -6,10 +6,57 @@ class MimeTypeTest < ActiveSupport::TestCase test "parse single" do Mime::LOOKUP.keys.each do |mime_type| - assert_equal [Mime::Type.lookup(mime_type)], Mime::Type.parse(mime_type) + unless mime_type == 'image/*' + assert_equal [Mime::Type.lookup(mime_type)], Mime::Type.parse(mime_type) + end end end + test "unregister" do + begin + Mime::Type.register("text/x-mobile", :mobile) + assert defined?(Mime::MOBILE) + assert_equal Mime::MOBILE, Mime::LOOKUP['text/x-mobile'] + assert_equal Mime::MOBILE, Mime::EXTENSION_LOOKUP['mobile'] + + Mime::Type.unregister(:mobile) + assert !defined?(Mime::MOBILE), "Mime::MOBILE should not be defined" + assert !Mime::LOOKUP.has_key?('text/x-mobile'), "Mime::LOOKUP should not have key ['text/x-mobile]" + assert !Mime::EXTENSION_LOOKUP.has_key?('mobile'), "Mime::EXTENSION_LOOKUP should not have key ['mobile]" + ensure + Mime.module_eval { remove_const :MOBILE if const_defined?(:MOBILE) } + Mime::LOOKUP.reject!{|key,_| key == 'text/x-mobile'} + end + end + + test "parse text with trailing star at the beginning" do + accept = "text/*, text/html, application/json, multipart/form-data" + expect = [Mime::HTML, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::XML, Mime::YAML, Mime::JSON, Mime::MULTIPART_FORM] + parsed = Mime::Type.parse(accept) + assert_equal expect, parsed + end + + test "parse text with trailing star in the end" do + accept = "text/html, application/json, multipart/form-data, text/*" + expect = [Mime::HTML, Mime::JSON, Mime::MULTIPART_FORM, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::XML, Mime::YAML] + parsed = Mime::Type.parse(accept) + assert_equal expect, parsed + end + + test "parse text with trailing star" do + accept = "text/*" + expect = [Mime::HTML, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::XML, Mime::YAML, Mime::JSON] + parsed = Mime::Type.parse(accept) + assert_equal expect, parsed + end + + test "parse application with trailing star" do + accept = "application/*" + expect = [Mime::HTML, Mime::JS, Mime::XML, Mime::RSS, Mime::ATOM, Mime::YAML, Mime::URL_ENCODED_FORM, Mime::JSON, Mime::PDF] + parsed = Mime::Type.parse(accept) + assert_equal expect, parsed + end + test "parse without q" do accept = "text/xml,application/xhtml+xml,text/yaml,application/xml,text/html,image/png,text/plain,application/pdf,*/*" expect = [Mime::HTML, Mime::XML, Mime::YAML, Mime::PNG, Mime::TEXT, Mime::PDF, Mime::ALL] @@ -41,11 +88,10 @@ class MimeTypeTest < ActiveSupport::TestCase begin Mime::Type.register("image/gif", :gif) assert_nothing_raised do - Mime::GIF assert_equal Mime::GIF, Mime::SET.last end ensure - Mime.module_eval { remove_const :GIF if const_defined?(:GIF) } + Mime::Type.unregister(:gif) end end @@ -85,6 +131,13 @@ class MimeTypeTest < ActiveSupport::TestCase assert unverified.each { |type| assert !Mime.const_get(type.to_s.upcase).verify_request?, "Nonverifiable Mime Type is verified: #{type.inspect}" } end + test "references gives preference to symbols before strings" do + assert_equal :html, Mime::HTML.ref + another = Mime::Type.lookup("foo/bar") + assert_nil another.to_sym + assert_equal "foo/bar", another.ref + end + test "regexp matcher" do assert Mime::JS =~ "text/javascript" assert Mime::JS =~ "application/javascript" diff --git a/actionpack/test/dispatch/mount_test.rb b/actionpack/test/dispatch/mount_test.rb index 00ca5ec9dc..1a032539b9 100644 --- a/actionpack/test/dispatch/mount_test.rb +++ b/actionpack/test/dispatch/mount_test.rb @@ -2,6 +2,17 @@ require 'abstract_unit' class TestRoutingMount < ActionDispatch::IntegrationTest Router = ActionDispatch::Routing::RouteSet.new + + class FakeEngine + def self.routes + Object.new + end + + def self.call(env) + [200, {"Content-Type" => "text/html"}, ["OK"]] + end + end + Router.draw do SprocketsApp = lambda { |env| [200, {"Content-Type" => "text/html"}, ["#{env["SCRIPT_NAME"]} -- #{env["PATH_INFO"]}"]] @@ -10,6 +21,8 @@ class TestRoutingMount < ActionDispatch::IntegrationTest mount SprocketsApp, :at => "/sprockets" mount SprocketsApp => "/shorthand" + mount FakeEngine, :at => "/fakeengine" + scope "/its_a" do mount SprocketsApp, :at => "/sprocket" end @@ -28,9 +41,14 @@ class TestRoutingMount < ActionDispatch::IntegrationTest get "/its_a/sprocket/omg" assert_equal "/its_a/sprocket -- /omg", response.body end - + def test_mounting_with_shorthand get "/shorthand/omg" assert_equal "/shorthand -- /omg", response.body end + + def test_with_fake_engine_does_not_call_invalid_method + get "/fakeengine" + assert_equal "OK", response.body + end end
\ No newline at end of file diff --git a/actionpack/test/dispatch/prefix_generation_test.rb b/actionpack/test/dispatch/prefix_generation_test.rb new file mode 100644 index 0000000000..18f28deee4 --- /dev/null +++ b/actionpack/test/dispatch/prefix_generation_test.rb @@ -0,0 +1,317 @@ +require 'abstract_unit' +require 'rack/test' + +module TestGenerationPrefix + class Post + extend ActiveModel::Naming + + def to_param + "1" + end + + def self.model_name + klass = "Post" + def klass.name; self end + + ActiveModel::Name.new(klass) + end + end + + class WithMountedEngine < ActionDispatch::IntegrationTest + include Rack::Test::Methods + + class BlogEngine + def self.routes + @routes ||= begin + routes = ActionDispatch::Routing::RouteSet.new + routes.draw do + match "/posts/:id", :to => "inside_engine_generating#show", :as => :post + match "/posts", :to => "inside_engine_generating#index", :as => :posts + match "/url_to_application", :to => "inside_engine_generating#url_to_application" + match "/polymorphic_path_for_engine", :to => "inside_engine_generating#polymorphic_path_for_engine" + match "/conflicting_url", :to => "inside_engine_generating#conflicting" + end + + routes + end + end + + def self.call(env) + env['action_dispatch.routes'] = routes + routes.call(env) + end + end + + class RailsApplication + def self.routes + @routes ||= begin + routes = ActionDispatch::Routing::RouteSet.new + routes.draw do + scope "/:omg", :omg => "awesome" do + mount BlogEngine => "/blog", :as => "blog_engine" + end + match "/generate", :to => "outside_engine_generating#index" + match "/polymorphic_path_for_engine", :to => "outside_engine_generating#polymorphic_path_for_engine" + match "/polymorphic_with_url_for", :to => "outside_engine_generating#polymorphic_with_url_for" + match "/conflicting_url", :to => "outside_engine_generating#conflicting" + root :to => "outside_engine_generating#index" + end + + routes + end + end + + def self.call(env) + env['action_dispatch.routes'] = routes + routes.call(env) + end + end + + # force draw + RailsApplication.routes + + class ::InsideEngineGeneratingController < ActionController::Base + include BlogEngine.routes.url_helpers + include RailsApplication.routes.mounted_helpers + + def index + render :text => posts_path + end + + def show + render :text => post_path(:id => params[:id]) + end + + def url_to_application + path = main_app.url_for(:controller => "outside_engine_generating", + :action => "index", + :only_path => true) + render :text => path + end + + def polymorphic_path_for_engine + render :text => polymorphic_path(Post.new) + end + + def conflicting + render :text => "engine" + end + end + + class ::OutsideEngineGeneratingController < ActionController::Base + include BlogEngine.routes.mounted_helpers + + def index + render :text => blog_engine.post_path(:id => 1) + end + + def polymorphic_path_for_engine + render :text => blog_engine.polymorphic_path(Post.new) + end + + def polymorphic_with_url_for + render :text => blog_engine.url_for(Post.new) + end + + def conflicting + render :text => "application" + end + end + + class EngineObject + include ActionDispatch::Routing::UrlFor + include BlogEngine.routes.url_helpers + end + + class AppObject + include ActionDispatch::Routing::UrlFor + include RailsApplication.routes.url_helpers + end + + def app + RailsApplication + end + + def engine_object + @engine_object ||= EngineObject.new + end + + def app_object + @app_object ||= AppObject.new + end + + def setup + RailsApplication.routes.default_url_options = {} + end + + # Inside Engine + test "[ENGINE] generating engine's url use SCRIPT_NAME from request" do + get "/pure-awesomeness/blog/posts/1" + assert_equal "/pure-awesomeness/blog/posts/1", last_response.body + end + + test "[ENGINE] generating application's url never uses SCRIPT_NAME from request" do + get "/pure-awesomeness/blog/url_to_application" + assert_equal "/generate", last_response.body + end + + test "[ENGINE] generating application's url includes default_url_options[:script_name]" do + RailsApplication.routes.default_url_options = {:script_name => "/something"} + get "/pure-awesomeness/blog/url_to_application" + assert_equal "/something/generate", last_response.body + end + + test "[ENGINE] generating application's url should give higher priority to default_url_options[:script_name]" do + RailsApplication.routes.default_url_options = {:script_name => "/something"} + get "/pure-awesomeness/blog/url_to_application", {}, 'SCRIPT_NAME' => '/foo' + assert_equal "/something/generate", last_response.body + end + + test "[ENGINE] generating engine's url with polymorphic path" do + get "/pure-awesomeness/blog/polymorphic_path_for_engine" + assert_equal "/pure-awesomeness/blog/posts/1", last_response.body + end + + test "[ENGINE] url_helpers from engine have higher priotity than application's url_helpers" do + get "/awesome/blog/conflicting_url" + assert_equal "engine", last_response.body + end + + # Inside Application + test "[APP] generating engine's route includes prefix" do + get "/generate" + assert_equal "/awesome/blog/posts/1", last_response.body + end + + test "[APP] generating engine's route includes default_url_options[:script_name]" do + RailsApplication.routes.default_url_options = {:script_name => "/something"} + get "/generate" + assert_equal "/something/awesome/blog/posts/1", last_response.body + end + + test "[APP] generating engine's route should give higher priority to default_url_options[:script_name]" do + RailsApplication.routes.default_url_options = {:script_name => "/something"} + get "/generate", {}, 'SCRIPT_NAME' => "/foo" + assert_equal "/something/awesome/blog/posts/1", last_response.body + end + + test "[APP] generating engine's url with polymorphic path" do + get "/polymorphic_path_for_engine" + assert_equal "/awesome/blog/posts/1", last_response.body + end + + test "[APP] generating engine's url with url_for(@post)" do + get "/polymorphic_with_url_for" + assert_equal "http://example.org/awesome/blog/posts/1", last_response.body + end + + # Inside any Object + test "[OBJECT] generating engine's route includes prefix" do + assert_equal "/awesome/blog/posts/1", engine_object.post_path(:id => 1) + end + + test "[OBJECT] generating engine's route includes dynamic prefix" do + assert_equal "/pure-awesomeness/blog/posts/3", engine_object.post_path(:id => 3, :omg => "pure-awesomeness") + end + + test "[OBJECT] generating engine's route includes default_url_options[:script_name]" do + RailsApplication.routes.default_url_options = {:script_name => "/something"} + assert_equal "/something/pure-awesomeness/blog/posts/3", engine_object.post_path(:id => 3, :omg => "pure-awesomeness") + end + + test "[OBJECT] generating application's route" do + assert_equal "/", app_object.root_path + end + + test "[OBJECT] generating application's route includes default_url_options[:script_name]" do + RailsApplication.routes.default_url_options = {:script_name => "/something"} + assert_equal "/something/", app_object.root_path + end + + test "[OBJECT] generating engine's route with url_for" do + path = engine_object.url_for(:controller => "inside_engine_generating", + :action => "show", + :only_path => true, + :omg => "omg", + :id => 1) + assert_equal "/omg/blog/posts/1", path + end + + test "[OBJECT] generating engine's route with named helpers" do + path = engine_object.posts_path + assert_equal "/awesome/blog/posts", path + + path = engine_object.posts_url(:host => "example.com") + assert_equal "http://example.com/awesome/blog/posts", path + end + + test "[OBJECT] generating engine's route with polymorphic_url" do + path = engine_object.polymorphic_path(Post.new) + assert_equal "/awesome/blog/posts/1", path + + path = engine_object.polymorphic_url(Post.new, :host => "www.example.com") + assert_equal "http://www.example.com/awesome/blog/posts/1", path + end + end + + class EngineMountedAtRoot < ActionDispatch::IntegrationTest + include Rack::Test::Methods + + class BlogEngine + def self.routes + @routes ||= begin + routes = ActionDispatch::Routing::RouteSet.new + routes.draw do + match "/posts/:id", :to => "posts#show", :as => :post + end + + routes + end + end + + def self.call(env) + env['action_dispatch.routes'] = routes + routes.call(env) + end + end + + class RailsApplication + def self.routes + @routes ||= begin + routes = ActionDispatch::Routing::RouteSet.new + routes.draw do + mount BlogEngine => "/" + end + + routes + end + end + + def self.call(env) + env['action_dispatch.routes'] = routes + routes.call(env) + end + end + + # force draw + RailsApplication.routes + + class ::PostsController < ActionController::Base + include BlogEngine.routes.url_helpers + include RailsApplication.routes.mounted_helpers + + def show + render :text => post_path(:id => params[:id]) + end + end + + def app + RailsApplication + end + + test "generating path inside engine" do + get "/posts/1" + assert_equal "/posts/1", last_response.body + end + end +end diff --git a/actionpack/test/dispatch/reloader_test.rb b/actionpack/test/dispatch/reloader_test.rb new file mode 100644 index 0000000000..eaabc1feb3 --- /dev/null +++ b/actionpack/test/dispatch/reloader_test.rb @@ -0,0 +1,138 @@ +require 'abstract_unit' + +class ReloaderTest < Test::Unit::TestCase + Reloader = ActionDispatch::Reloader + + def test_prepare_callbacks + a = b = c = nil + Reloader.to_prepare { |*args| a = b = c = 1 } + Reloader.to_prepare { |*args| b = c = 2 } + Reloader.to_prepare { |*args| c = 3 } + + # Ensure to_prepare callbacks are not run when defined + assert_nil a || b || c + + # Run callbacks + call_and_return_body + + assert_equal 1, a + assert_equal 2, b + assert_equal 3, c + end + + class MyBody < Array + def initialize(&block) + @on_close = block + end + + def foo + "foo" + end + + def bar + "bar" + end + + def close + @on_close.call if @on_close + end + end + + def test_returned_body_object_always_responds_to_close + body = call_and_return_body + assert_respond_to body, :close + end + + def test_returned_body_object_behaves_like_underlying_object + body = call_and_return_body do + b = MyBody.new + b << "hello" + b << "world" + [200, { "Content-Type" => "text/html" }, b] + end + assert_equal 2, body.size + assert_equal "hello", body[0] + assert_equal "world", body[1] + assert_equal "foo", body.foo + assert_equal "bar", body.bar + end + + def test_it_calls_close_on_underlying_object_when_close_is_called_on_body + close_called = false + body = call_and_return_body do + b = MyBody.new do + close_called = true + end + [200, { "Content-Type" => "text/html" }, b] + end + body.close + assert close_called + end + + def test_returned_body_object_responds_to_all_methods_supported_by_underlying_object + body = call_and_return_body do + [200, { "Content-Type" => "text/html" }, MyBody.new] + end + assert_respond_to body, :size + assert_respond_to body, :each + assert_respond_to body, :foo + assert_respond_to body, :bar + end + + def test_cleanup_callbacks_are_called_when_body_is_closed + cleaned = false + Reloader.to_cleanup { cleaned = true } + + body = call_and_return_body + assert !cleaned + + body.close + assert cleaned + end + + def test_prepare_callbacks_arent_called_when_body_is_closed + prepared = false + Reloader.to_prepare { prepared = true } + + body = call_and_return_body + prepared = false + + body.close + assert !prepared + end + + def test_manual_reloading + prepared = cleaned = false + Reloader.to_prepare { prepared = true } + Reloader.to_cleanup { cleaned = true } + + Reloader.prepare! + assert prepared + assert !cleaned + + prepared = cleaned = false + Reloader.cleanup! + assert !prepared + assert cleaned + end + + def test_cleanup_callbacks_are_called_on_exceptions + cleaned = false + Reloader.to_cleanup { cleaned = true } + + begin + call_and_return_body do + raise "error" + end + rescue + end + + assert cleaned + end + + private + def call_and_return_body(&block) + @reloader ||= Reloader.new(block || proc {[200, {}, 'response']}) + @reloader.call({'rack.input' => StringIO.new('')})[2] + end +end diff --git a/actionpack/test/dispatch/request/json_params_parsing_test.rb b/actionpack/test/dispatch/request/json_params_parsing_test.rb index 0faa99a912..34db7a4c66 100644 --- a/actionpack/test/dispatch/request/json_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/json_params_parsing_test.rb @@ -1,6 +1,6 @@ require 'abstract_unit' -class JsonParamsParsingTest < ActionController::IntegrationTest +class JsonParamsParsingTest < ActionDispatch::IntegrationTest class TestController < ActionController::Base class << self attr_accessor :last_request_parameters @@ -56,7 +56,7 @@ class JsonParamsParsingTest < ActionController::IntegrationTest def with_test_routing with_routing do |set| - set.draw do |map| + set.draw do match ':action', :to => ::JsonParamsParsingTest::TestController end yield diff --git a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb index e3ec5cf182..3ff558ec5a 100644 --- a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb @@ -1,6 +1,6 @@ require 'abstract_unit' -class MultipartParamsParsingTest < ActionController::IntegrationTest +class MultipartParamsParsingTest < ActionDispatch::IntegrationTest class TestController < ActionController::Base class << self attr_accessor :last_request_parameters @@ -36,7 +36,6 @@ class MultipartParamsParsingTest < ActionController::IntegrationTest assert_equal 'bar', params['foo'] file = params['file'] - assert_kind_of Tempfile, file assert_equal 'file.txt', file.original_filename assert_equal "text/plain", file.content_type assert_equal 'contents', file.read @@ -49,8 +48,6 @@ class MultipartParamsParsingTest < ActionController::IntegrationTest file = params['file'] foo = params['foo'] - assert_kind_of Tempfile, file - assert_equal 'file.txt', file.original_filename assert_equal "text/plain", file.content_type @@ -64,11 +61,9 @@ class MultipartParamsParsingTest < ActionController::IntegrationTest file = params['file'] - assert_kind_of Tempfile, file - assert_equal 'file.txt', file.original_filename assert_equal "text/plain", file.content_type - assert ('a' * 20480) == file.read + assert_equal(('a' * 20480), file.read) end test "parses binary file" do @@ -77,13 +72,11 @@ class MultipartParamsParsingTest < ActionController::IntegrationTest assert_equal 'bar', params['foo'] file = params['file'] - assert_kind_of Tempfile, file assert_equal 'file.csv', file.original_filename assert_nil file.content_type assert_equal 'contents', file.read file = params['flowers'] - assert_kind_of Tempfile, file assert_equal 'flowers.jpg', file.original_filename assert_equal "image/jpeg", file.content_type assert_equal 19512, file.size @@ -156,7 +149,7 @@ class MultipartParamsParsingTest < ActionController::IntegrationTest def with_test_routing with_routing do |set| - set.draw do |map| + set.draw do match ':action', :to => 'multipart_params_parsing_test/test' end yield diff --git a/actionpack/test/dispatch/request/query_string_parsing_test.rb b/actionpack/test/dispatch/request/query_string_parsing_test.rb index 071d80c5b0..f6a1475d04 100644 --- a/actionpack/test/dispatch/request/query_string_parsing_test.rb +++ b/actionpack/test/dispatch/request/query_string_parsing_test.rb @@ -1,6 +1,6 @@ require 'abstract_unit' -class QueryStringParsingTest < ActionController::IntegrationTest +class QueryStringParsingTest < ActionDispatch::IntegrationTest class TestController < ActionController::Base class << self attr_accessor :last_query_parameters @@ -108,7 +108,7 @@ class QueryStringParsingTest < ActionController::IntegrationTest private def assert_parses(expected, actual) with_routing do |set| - set.draw do |map| + set.draw do match ':action', :to => ::QueryStringParsingTest::TestController end diff --git a/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb b/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb index 0bcef81534..04a0fb6f34 100644 --- a/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb @@ -1,6 +1,6 @@ require 'abstract_unit' -class UrlEncodedParamsParsingTest < ActionController::IntegrationTest +class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest class TestController < ActionController::Base class << self attr_accessor :last_request_parameters, :last_request_type @@ -129,7 +129,7 @@ class UrlEncodedParamsParsingTest < ActionController::IntegrationTest private def with_test_routing with_routing do |set| - set.draw do |map| + set.draw do match ':action', :to => ::UrlEncodedParamsParsingTest::TestController end yield diff --git a/actionpack/test/dispatch/request/xml_params_parsing_test.rb b/actionpack/test/dispatch/request/xml_params_parsing_test.rb index d44c642420..ad9de02eb4 100644 --- a/actionpack/test/dispatch/request/xml_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/xml_params_parsing_test.rb @@ -1,6 +1,6 @@ require 'abstract_unit' -class XmlParamsParsingTest < ActionController::IntegrationTest +class XmlParamsParsingTest < ActionDispatch::IntegrationTest class TestController < ActionController::Base class << self attr_accessor :last_request_parameters @@ -18,6 +18,7 @@ class XmlParamsParsingTest < ActionController::IntegrationTest test "parses a strict rack.input" do class Linted + undef call if method_defined?(:call) def call(env) bar = env['action_dispatch.request.request_parameters']['foo'] result = "<ok>#{bar}</ok>" @@ -96,7 +97,7 @@ class XmlParamsParsingTest < ActionController::IntegrationTest private def with_test_routing with_routing do |set| - set.draw do |map| + set.draw do match ':action', :to => ::XmlParamsParsingTest::TestController end yield diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index c8947aac80..f03ae7f2b3 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -1,6 +1,31 @@ require 'abstract_unit' class RequestTest < ActiveSupport::TestCase + + def url_for(options = {}) + options.reverse_merge!(:host => 'www.example.com') + ActionDispatch::Http::URL.url_for(options) + end + + test "url_for class method" do + e = assert_raise(ArgumentError) { url_for(:host => nil) } + assert_match(/Please provide the :host parameter/, e.message) + + assert_equal '/books', url_for(:only_path => true, :path => '/books') + + assert_equal 'http://www.example.com', url_for + assert_equal 'http://api.example.com', url_for(:subdomain => 'api') + assert_equal 'http://www.ror.com', url_for(:domain => 'ror.com') + assert_equal 'http://api.ror.co.uk', url_for(:host => 'www.ror.co.uk', :subdomain => 'api', :tld_length => 2) + assert_equal 'http://www.example.com:8080', url_for(:port => 8080) + assert_equal 'https://www.example.com', url_for(:protocol => 'https') + assert_equal 'http://www.example.com/docs', url_for(:path => '/docs') + assert_equal 'http://www.example.com#signup', url_for(:anchor => 'signup') + assert_equal 'http://www.example.com/', url_for(:trailing_slash => true) + assert_equal 'http://dhh:supersecret@www.example.com', url_for(:user => 'dhh', :password => 'supersecret') + assert_equal 'http://www.example.com?search=books', url_for(:params => { :search => 'books' }) + end + test "remote ip" do request = stub_request 'REMOTE_ADDR' => '1.2.3.4' assert_equal '1.2.3.4', request.remote_ip @@ -45,9 +70,9 @@ class RequestTest < ActiveSupport::TestCase e = assert_raise(ActionDispatch::RemoteIp::IpSpoofAttackError) { request.remote_ip } - assert_match /IP spoofing attack/, e.message - assert_match /HTTP_X_FORWARDED_FOR="1.1.1.1"/, e.message - assert_match /HTTP_CLIENT_IP="2.2.2.2"/, e.message + assert_match(/IP spoofing attack/, e.message) + assert_match(/HTTP_X_FORWARDED_FOR="1.1.1.1"/, e.message) + assert_match(/HTTP_CLIENT_IP="2.2.2.2"/, e.message) # turn IP Spoofing detection off. # This is useful for sites that are aimed at non-IP clients. The typical @@ -96,6 +121,9 @@ class RequestTest < ActiveSupport::TestCase request = stub_request 'HTTP_HOST' => "www.rubyonrails.co.uk" assert_equal "rubyonrails.co.uk", request.domain(2) + request = stub_request 'HTTP_HOST' => "www.rubyonrails.co.uk", :tld_length => 2 + assert_equal "rubyonrails.co.uk", request.domain + request = stub_request 'HTTP_HOST' => "192.168.1.200" assert_nil request.domain @@ -116,6 +144,9 @@ class RequestTest < ActiveSupport::TestCase request = stub_request 'HTTP_HOST' => "dev.www.rubyonrails.co.uk" assert_equal %w( dev www ), request.subdomains(2) + request = stub_request 'HTTP_HOST' => "dev.www.rubyonrails.co.uk", :tld_length => 2 + assert_equal %w( dev www ), request.subdomains + request = stub_request 'HTTP_HOST' => "foobar.foobar.com" assert_equal %w( foobar ), request.subdomains @@ -132,12 +163,46 @@ class RequestTest < ActiveSupport::TestCase assert_equal [], request.subdomains end + test "standard_port" do + request = stub_request + assert_equal 80, request.standard_port + + request = stub_request 'HTTPS' => 'on' + assert_equal 443, request.standard_port + end + + test "standard_port?" do + request = stub_request + assert !request.ssl? + assert request.standard_port? + + request = stub_request 'HTTPS' => 'on' + assert request.ssl? + assert request.standard_port? + + request = stub_request 'HTTP_HOST' => 'www.example.org:8080' + assert !request.ssl? + assert !request.standard_port? + + request = stub_request 'HTTP_HOST' => 'www.example.org:8443', 'HTTPS' => 'on' + assert request.ssl? + assert !request.standard_port? + end + + test "optional port" do + request = stub_request 'HTTP_HOST' => 'www.example.org:80' + assert_equal nil, request.optional_port + + request = stub_request 'HTTP_HOST' => 'www.example.org:8080' + assert_equal 8080, request.optional_port + end + test "port string" do request = stub_request 'HTTP_HOST' => 'www.example.org:80' - assert_equal "", request.port_string + assert_equal '', request.port_string request = stub_request 'HTTP_HOST' => 'www.example.org:8080' - assert_equal ":8080", request.port_string + assert_equal ':8080', request.port_string end test "full path" do @@ -223,6 +288,16 @@ class RequestTest < ActiveSupport::TestCase assert request.ssl? end + test "scheme returns https when proxied" do + request = stub_request 'rack.url_scheme' => 'http' + assert !request.ssl? + assert_equal 'http', request.scheme + + request = stub_request 'rack.url_scheme' => 'http', 'HTTP_X_FORWARDED_PROTO' => 'https' + assert request.ssl? + assert_equal 'https', request.scheme + end + test "String request methods" do [:get, :post, :put, :delete].each do |method| request = stub_request 'REQUEST_METHOD' => method.to_s.upcase @@ -346,6 +421,18 @@ class RequestTest < ActiveSupport::TestCase assert_equal({"bar" => 2}, request.query_parameters) end + test "parameters still accessible after rack parse error" do + mock_rack_env = { "QUERY_STRING" => "x[y]=1&x[y][][w]=2", "rack.input" => "foo" } + request = nil + begin + request = stub_request(mock_rack_env) + request.parameters + rescue TypeError + # rack will raise a TypeError when parsing this query string + end + assert_equal({}, request.parameters) + end + test "formats with accept header" do request = stub_request 'HTTP_ACCEPT' => 'text/html' request.expects(:parameters).at_least_once.returns({}) @@ -428,15 +515,56 @@ class RequestTest < ActiveSupport::TestCase assert_equal "[FILTERED]", request.raw_post assert_equal "[FILTERED]", request.params["amount"] - assert_equal "1", request.params["step"] + assert_equal "1", request.params["step"] + end + + test "filtered_path returns path with filtered query string" do + %w(; &).each do |sep| + request = stub_request('QUERY_STRING' => %w(username=sikachu secret=bd4f21f api_key=b1bc3b3cd352f68d79d7).join(sep), + 'PATH_INFO' => '/authenticate', + 'action_dispatch.parameter_filter' => [:secret, :api_key]) + + path = request.filtered_path + assert_equal %w(/authenticate?username=sikachu secret=[FILTERED] api_key=[FILTERED]).join(sep), path + end + end + + test "filtered_path should not unescape a genuine '[FILTERED]' value" do + request = stub_request('QUERY_STRING' => "secret=bd4f21f&genuine=%5BFILTERED%5D", + 'PATH_INFO' => '/authenticate', + 'action_dispatch.parameter_filter' => [:secret]) + + path = request.filtered_path + assert_equal "/authenticate?secret=[FILTERED]&genuine=%5BFILTERED%5D", path + end + + test "filtered_path should preserve duplication of keys in query string" do + request = stub_request('QUERY_STRING' => "username=sikachu&secret=bd4f21f&username=fxn", + 'PATH_INFO' => '/authenticate', + 'action_dispatch.parameter_filter' => [:secret]) + + path = request.filtered_path + assert_equal "/authenticate?username=sikachu&secret=[FILTERED]&username=fxn", path + end + + test "filtered_path should ignore searchparts" do + request = stub_request('QUERY_STRING' => "secret", + 'PATH_INFO' => '/authenticate', + 'action_dispatch.parameter_filter' => [:secret]) + + path = request.filtered_path + assert_equal "/authenticate?secret", path end protected def stub_request(env = {}) ip_spoofing_check = env.key?(:ip_spoofing_check) ? env.delete(:ip_spoofing_check) : true + @trusted_proxies ||= nil ip_app = ActionDispatch::RemoteIp.new(Proc.new { }, ip_spoofing_check, @trusted_proxies) + tld_length = env.key?(:tld_length) ? env.delete(:tld_length) : 1 ip_app.call(env) + ActionDispatch::Http::URL.tld_length = tld_length ActionDispatch::Request.new(env) end diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb index c20fa10f63..6f38714b2e 100644 --- a/actionpack/test/dispatch/response_test.rb +++ b/actionpack/test/dispatch/response_test.rb @@ -11,9 +11,7 @@ class ResponseTest < ActiveSupport::TestCase status, headers, body = @response.to_a assert_equal 200, status assert_equal({ - "Content-Type" => "text/html; charset=utf-8", - "Cache-Control" => "max-age=0, private, must-revalidate", - "ETag" => '"65a8e27d8879283831b664bd8b7f0ad4"' + "Content-Type" => "text/html; charset=utf-8" }, headers) parts = [] @@ -21,15 +19,17 @@ class ResponseTest < ActiveSupport::TestCase assert_equal ["Hello, World!"], parts end + test "status handled properly in initialize" do + assert_equal 200, ActionDispatch::Response.new('200 OK').status + end + test "utf8 output" do @response.body = [1090, 1077, 1089, 1090].pack("U*") - status, headers, body = @response.to_a + status, headers, _ = @response.to_a assert_equal 200, status assert_equal({ - "Content-Type" => "text/html; charset=utf-8", - "Cache-Control" => "max-age=0, private, must-revalidate", - "ETag" => '"ebb5e89e8a94e9dd22abf5d915d112b2"' + "Content-Type" => "text/html; charset=utf-8" }, headers) end @@ -41,8 +41,7 @@ class ResponseTest < ActiveSupport::TestCase status, headers, body = @response.to_a assert_equal 200, status assert_equal({ - "Content-Type" => "text/html; charset=utf-8", - "Cache-Control" => "no-cache" + "Content-Type" => "text/html; charset=utf-8" }, headers) parts = [] @@ -53,20 +52,20 @@ class ResponseTest < ActiveSupport::TestCase test "content type" do [204, 304].each do |c| @response.status = c.to_s - status, headers, body = @response.to_a + _, headers, _ = @response.to_a assert !headers.has_key?("Content-Type"), "#{c} should not have Content-Type header" end [200, 302, 404, 500].each do |c| @response.status = c.to_s - status, headers, body = @response.to_a + _, headers, _ = @response.to_a assert headers.has_key?("Content-Type"), "#{c} did not have Content-Type header" end end test "does not include Status header" do @response.status = "200 OK" - status, headers, body = @response.to_a + _, headers, _ = @response.to_a assert !headers.has_key?('Status') end @@ -120,10 +119,10 @@ class ResponseTest < ActiveSupport::TestCase end test "read cache control" do - resp = ActionDispatch::Response.new.tap { |resp| - resp.cache_control[:public] = true - resp.etag = '123' - resp.body = 'Hello' + resp = ActionDispatch::Response.new.tap { |response| + response.cache_control[:public] = true + response.etag = '123' + response.body = 'Hello' } resp.to_a @@ -135,10 +134,10 @@ class ResponseTest < ActiveSupport::TestCase end test "read charset and content type" do - resp = ActionDispatch::Response.new.tap { |resp| - resp.charset = 'utf-16' - resp.content_type = Mime::XML - resp.body = 'Hello' + resp = ActionDispatch::Response.new.tap { |response| + response.charset = 'utf-16' + response.content_type = Mime::XML + response.body = 'Hello' } resp.to_a diff --git a/actionpack/test/dispatch/routing_assertions_test.rb b/actionpack/test/dispatch/routing_assertions_test.rb new file mode 100644 index 0000000000..9f95d82129 --- /dev/null +++ b/actionpack/test/dispatch/routing_assertions_test.rb @@ -0,0 +1,103 @@ +require 'abstract_unit' +require 'controller/fake_controllers' + +class SecureArticlesController < ArticlesController; end +class BlockArticlesController < ArticlesController; end + +class RoutingAssertionsTest < ActionController::TestCase + + def setup + @routes = ActionDispatch::Routing::RouteSet.new + @routes.draw do + resources :articles + + scope 'secure', :constraints => { :protocol => 'https://' } do + resources :articles, :controller => 'secure_articles' + end + + scope 'block', :constraints => lambda { |r| r.ssl? } do + resources :articles, :controller => 'block_articles' + end + end + end + + def test_assert_generates + assert_generates('/articles', { :controller => 'articles', :action => 'index' }) + assert_generates('/articles/1', { :controller => 'articles', :action => 'show', :id => '1' }) + end + + def test_assert_generates_with_defaults + assert_generates('/articles/1/edit', { :controller => 'articles', :action => 'edit' }, { :id => '1' }) + end + + def test_assert_generates_with_extras + assert_generates('/articles', { :controller => 'articles', :action => 'index', :page => '1' }, {}, { :page => '1' }) + end + + def test_assert_recognizes + assert_recognizes({ :controller => 'articles', :action => 'index' }, '/articles') + assert_recognizes({ :controller => 'articles', :action => 'show', :id => '1' }, '/articles/1') + end + + def test_assert_recognizes_with_extras + assert_recognizes({ :controller => 'articles', :action => 'index', :page => '1' }, '/articles', { :page => '1' }) + end + + def test_assert_recognizes_with_method + assert_recognizes({ :controller => 'articles', :action => 'create' }, { :path => '/articles', :method => :post }) + assert_recognizes({ :controller => 'articles', :action => 'update', :id => '1' }, { :path => '/articles/1', :method => :put }) + end + + def test_assert_recognizes_with_hash_constraint + assert_raise(ActionController::RoutingError) do + assert_recognizes({ :controller => 'secure_articles', :action => 'index' }, 'http://test.host/secure/articles') + end + assert_recognizes({ :controller => 'secure_articles', :action => 'index' }, 'https://test.host/secure/articles') + end + + def test_assert_recognizes_with_block_constraint + assert_raise(ActionController::RoutingError) do + assert_recognizes({ :controller => 'block_articles', :action => 'index' }, 'http://test.host/block/articles') + end + assert_recognizes({ :controller => 'block_articles', :action => 'index' }, 'https://test.host/block/articles') + end + + def test_assert_routing + assert_routing('/articles', :controller => 'articles', :action => 'index') + end + + def test_assert_routing_with_defaults + assert_routing('/articles/1/edit', { :controller => 'articles', :action => 'edit', :id => '1' }, { :id => '1' }) + end + + def test_assert_routing_with_extras + assert_routing('/articles', { :controller => 'articles', :action => 'index', :page => '1' }, { }, { :page => '1' }) + end + + def test_assert_routing_with_hash_constraint + assert_raise(ActionController::RoutingError) do + assert_routing('http://test.host/secure/articles', { :controller => 'secure_articles', :action => 'index' }) + end + assert_routing('https://test.host/secure/articles', { :controller => 'secure_articles', :action => 'index' }) + end + + def test_assert_routing_with_block_constraint + assert_raise(ActionController::RoutingError) do + assert_routing('http://test.host/block/articles', { :controller => 'block_articles', :action => 'index' }) + end + assert_routing('https://test.host/block/articles', { :controller => 'block_articles', :action => 'index' }) + end + + def test_with_routing + with_routing do |routes| + routes.draw do + resources :articles, :path => 'artikel' + end + + assert_routing('/artikel', :controller => 'articles', :action => 'index') + assert_raise(ActionController::RoutingError) do + assert_routing('/articles', { :controller => 'articles', :action => 'index' }) + end + end + end +end diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 3f090b7254..5e5758a60e 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -13,6 +13,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + class YoutubeFavoritesRedirector + def self.call(params, request) + "http://www.youtube.com/watch?v=#{params[:youtube_id]}" + end + end + stub_controllers do |routes| Routes = routes Routes.draw do @@ -40,11 +46,29 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest get :new, :path => "build" post :create, :path => "create", :as => "" put :update + get :remove, :action => :destroy, :as => :remove + end + + scope "pagemark", :controller => "pagemarks", :as => :pagemark do + get "new", :path => "build" + post "create", :as => "" + put "update" get "remove", :action => :destroy, :as => :remove end match 'account/logout' => redirect("/logout"), :as => :logout_redirect match 'account/login', :to => redirect("/login") + match 'secure', :to => redirect("/secure/login") + + match 'mobile', :to => redirect(:subdomain => 'mobile') + match 'documentation', :to => redirect(:domain => 'example-documentation.com', :path => '') + match 'new_documentation', :to => redirect(:path => '/documentation/new') + match 'super_new_documentation', :to => redirect(:host => 'super-docs.com') + + match 'stores/:name', :to => redirect(:subdomain => 'stores', :path => '/%{name}') + match 'stores/:name(*rest)', :to => redirect(:subdomain => 'stores', :path => '/%{name}%{rest}') + + match 'youtube_favorites/:youtube_id/:name', :to => redirect(YoutubeFavoritesRedirector) constraints(lambda { |req| true }) do match 'account/overview' @@ -128,7 +152,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end member do - get :some_path_with_name + get 'some_path_with_name' put :accessible_projects post :resend, :generate_new_password end @@ -147,6 +171,11 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end resources :replies do + collection do + get 'page/:page' => 'replies#index', :page => %r{\d+} + get ':page' => 'replies#index', :page => %r{\d+} + end + new do post :preview end @@ -158,7 +187,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end resources :posts, :only => [:index, :show] do - resources :comments, :except => :destroy + namespace :admin do + root :to => "index#index" + end + resources :comments, :except => :destroy do + get "views" => "comments#views", :as => :views + end end resource :past, :only => :destroy @@ -176,6 +210,14 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + scope '/hello' do + shallow do + resources :notes do + resources :trackbacks + end + end + end + resources :threads, :shallow => true do resource :owner resources :messages do @@ -187,7 +229,9 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end - resources :sheep + resources :sheep do + get "_it", :on => :member + end resources :clients do namespace :google do @@ -200,22 +244,28 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end resources :customers do - get "recent" => "customers#recent", :as => :recent, :on => :collection - get "profile" => "customers#profile", :as => :profile, :on => :member - post "preview" => "customers#preview", :as => :preview, :on => :new + get :recent, :on => :collection + get "profile", :on => :member + get "secret/profile" => "customers#secret", :on => :member + post "preview" => "customers#preview", :as => :another_preview, :on => :new resource :avatar do - get "thumbnail(.:format)" => "avatars#thumbnail", :as => :thumbnail, :on => :member + get "thumbnail" => "avatars#thumbnail", :as => :thumbnail, :on => :member end resources :invoices do - get "outstanding" => "invoices#outstanding", :as => :outstanding, :on => :collection + get "outstanding" => "invoices#outstanding", :on => :collection get "overdue", :to => :overdue, :on => :collection get "print" => "invoices#print", :as => :print, :on => :member post "preview" => "invoices#preview", :as => :preview, :on => :new + get "aged/:months", :on => :collection, :action => :aged, :as => :aged end resources :notes, :shallow => true do get "preview" => "notes#preview", :as => :preview, :on => :new get "print" => "notes#print", :as => :print, :on => :member end + get "inactive", :on => :collection + post "deactivate", :on => :member + get "old", :on => :collection, :as => :stale + get "export" end namespace :api do @@ -337,6 +387,14 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest resources :content + namespace :transport do + resources :taxis + end + + namespace :medical do + resource :taxis + end + scope :constraints => { :id => /\d+/ } do get '/tickets', :to => 'tickets#index', :as => :tickets end @@ -371,6 +429,15 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + namespace :wiki do + resources :articles, :id => /[^\/]+/ do + resources :comments, :only => [:create, :new] + end + end + + resources :wiki_pages, :path => :pages + resource :wiki_account, :path => :my_account + scope :only => :show do namespace :only do resources :sectors, :only => :index do @@ -405,11 +472,41 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + resources :sections, :id => /.+/ do + get :preview, :on => :member + end + + scope :as => "routes" do + get "/c/:id", :as => :collision, :to => "collision#show" + get "/collision", :to => "collision#show" + get "/no_collision", :to => "collision#show", :as => nil + + get "/fc/:id", :as => :forced_collision, :to => "forced_collision#show" + get "/forced_collision", :as => :forced_collision, :to => "forced_collision#show" + end + + match '/purchases/:token/:filename', + :to => 'purchases#fetch', + :token => /[[:alnum:]]{10}/, + :filename => /(.+)/, + :as => :purchase + + resources :lists, :id => /([A-Za-z0-9]{25})|default/ do + resources :todos, :id => /\d+/ + end + + scope '/countries/:country', :constraints => lambda { |params, req| %[all France].include?(params[:country]) } do + match '/', :to => 'countries#index' + match '/cities', :to => 'countries#cities' + end + + match '/countries/:country/(*other)', :to => redirect{ |params, req| params[:other] ? "/countries/all/#{params[:other]}" : '/countries/all' } + match '/:locale/*file.:format', :to => 'files#show', :file => /path\/to\/existing\/file/ end end - class TestAltApp < ActionController::IntegrationTest + class TestAltApp < ActionDispatch::IntegrationTest class AltRequest def initialize(env) @env = env @@ -599,6 +696,55 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + def test_redirect_hash_with_subdomain + with_test_routes do + get '/mobile' + verify_redirect 'http://mobile.example.com/mobile' + end + end + + def test_redirect_hash_with_domain_and_path + with_test_routes do + get '/documentation' + verify_redirect 'http://www.example-documentation.com' + end + end + + def test_redirect_hash_with_path + with_test_routes do + get '/new_documentation' + verify_redirect 'http://www.example.com/documentation/new' + end + end + + def test_redirect_hash_with_host + with_test_routes do + get '/super_new_documentation?section=top' + verify_redirect 'http://super-docs.com/super_new_documentation?section=top' + end + end + + def test_redirect_hash_path_substitution + with_test_routes do + get '/stores/iernest' + verify_redirect 'http://stores.example.com/iernest' + end + end + + def test_redirect_hash_path_substitution_with_catch_all + with_test_routes do + get '/stores/iernest/products' + verify_redirect 'http://stores.example.com/iernest/products' + end + end + + def test_redirect_class + with_test_routes do + get '/youtube_favorites/oHg5SJYRHA0/rick-rolld' + verify_redirect 'http://www.youtube.com/watch?v=oHg5SJYRHA0' + end + end + def test_openid with_test_routes do get '/openid/login' @@ -613,15 +759,15 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest with_test_routes do get '/bookmark/build' assert_equal 'bookmarks#new', @response.body - assert_equal '/bookmark/build', new_bookmark_path + assert_equal '/bookmark/build', bookmark_new_path post '/bookmark/create' assert_equal 'bookmarks#create', @response.body assert_equal '/bookmark/create', bookmark_path - put '/bookmark' + put '/bookmark/update' assert_equal 'bookmarks#update', @response.body - assert_equal '/bookmark', update_bookmark_path + assert_equal '/bookmark/update', bookmark_update_path get '/bookmark/remove' assert_equal 'bookmarks#destroy', @response.body @@ -629,6 +775,26 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + def test_pagemarks + with_test_routes do + get '/pagemark/build' + assert_equal 'pagemarks#new', @response.body + assert_equal '/pagemark/build', pagemark_new_path + + post '/pagemark/create' + assert_equal 'pagemarks#create', @response.body + assert_equal '/pagemark/create', pagemark_path + + put '/pagemark/update' + assert_equal 'pagemarks#update', @response.body + assert_equal '/pagemark/update', pagemark_update_path + + get '/pagemark/remove' + assert_equal 'pagemarks#destroy', @response.body + assert_equal '/pagemark/remove', pagemark_remove_path + end + end + def test_admin with_test_routes do get '/admin', {}, {'REMOTE_ADDR' => '192.168.1.100'} @@ -963,6 +1129,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal '/sheep/1', sheep_path(1) assert_equal '/sheep/new', new_sheep_path assert_equal '/sheep/1/edit', edit_sheep_path(1) + assert_equal '/sheep/1/_it', _it_sheep_path(1) end end @@ -1141,14 +1308,6 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end - def test_index - with_test_routes do - assert_equal '/info', info_path - get '/info' - assert_equal 'projects#info', @response.body - end - end - def test_match_shorthand_with_no_scope with_test_routes do assert_equal '/account/overview', account_overview_path @@ -1165,6 +1324,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + def test_dynamically_generated_helpers_on_collection_do_not_clobber_resources_url_helper + with_test_routes do + assert_equal '/replies', replies_path + end + end + def test_scoped_controller_with_namespace_and_action with_test_routes do assert_equal '/account/twitter/callback', account_callback_path("twitter") @@ -1524,11 +1689,66 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + def test_shallow_nested_resources_within_scope + with_test_routes do + + get '/hello/notes/1/trackbacks' + assert_equal 'trackbacks#index', @response.body + assert_equal '/hello/notes/1/trackbacks', note_trackbacks_path(:note_id => 1) + + get '/hello/notes/1/edit' + assert_equal 'notes#edit', @response.body + assert_equal '/hello/notes/1/edit', edit_note_path(:id => '1') + + get '/hello/notes/1/trackbacks/new' + assert_equal 'trackbacks#new', @response.body + assert_equal '/hello/notes/1/trackbacks/new', new_note_trackback_path(:note_id => 1) + + get '/hello/trackbacks/1' + assert_equal 'trackbacks#show', @response.body + assert_equal '/hello/trackbacks/1', trackback_path(:id => '1') + + get '/hello/trackbacks/1/edit' + assert_equal 'trackbacks#edit', @response.body + assert_equal '/hello/trackbacks/1/edit', edit_trackback_path(:id => '1') + + put '/hello/trackbacks/1' + assert_equal 'trackbacks#update', @response.body + + post '/hello/notes/1/trackbacks' + assert_equal 'trackbacks#create', @response.body + + delete '/hello/trackbacks/1' + assert_equal 'trackbacks#destroy', @response.body + + get '/hello/notes' + assert_equal 'notes#index', @response.body + + post '/hello/notes' + assert_equal 'notes#create', @response.body + + get '/hello/notes/new' + assert_equal 'notes#new', @response.body + assert_equal '/hello/notes/new', new_note_path + + get '/hello/notes/1' + assert_equal 'notes#show', @response.body + assert_equal '/hello/notes/1', note_path(:id => 1) + + put '/hello/notes/1' + assert_equal 'notes#update', @response.body + + delete '/hello/notes/1' + assert_equal 'notes#destroy', @response.body + end + end + def test_custom_resource_routes_are_scoped with_test_routes do assert_equal '/customers/recent', recent_customers_path assert_equal '/customers/1/profile', profile_customer_path(:id => '1') - assert_equal '/customers/new/preview', preview_new_customer_path + assert_equal '/customers/1/secret/profile', secret_profile_customer_path(:id => '1') + assert_equal '/customers/new/preview', another_preview_new_customer_path assert_equal '/customers/1/avatar/thumbnail.jpg', thumbnail_customer_avatar_path(:customer_id => '1', :format => :jpg) assert_equal '/customers/1/invoices/outstanding', outstanding_customer_invoices_path(:customer_id => '1') assert_equal '/customers/1/invoices/2/print', print_customer_invoice_path(:customer_id => '1', :id => '2') @@ -1541,6 +1761,9 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest get '/customers/1/invoices/overdue' assert_equal 'invoices#overdue', @response.body + + get '/customers/1/secret/profile' + assert_equal 'customers#secret', @response.body end end @@ -1884,11 +2107,269 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + def test_resources_are_not_pluralized + with_test_routes do + get '/transport/taxis' + assert_equal 'transport/taxis#index', @response.body + assert_equal '/transport/taxis', transport_taxis_path + + get '/transport/taxis/new' + assert_equal 'transport/taxis#new', @response.body + assert_equal '/transport/taxis/new', new_transport_taxi_path + + post '/transport/taxis' + assert_equal 'transport/taxis#create', @response.body + + get '/transport/taxis/1' + assert_equal 'transport/taxis#show', @response.body + assert_equal '/transport/taxis/1', transport_taxi_path(:id => '1') + + get '/transport/taxis/1/edit' + assert_equal 'transport/taxis#edit', @response.body + assert_equal '/transport/taxis/1/edit', edit_transport_taxi_path(:id => '1') + + put '/transport/taxis/1' + assert_equal 'transport/taxis#update', @response.body + + delete '/transport/taxis/1' + assert_equal 'transport/taxis#destroy', @response.body + end + end + + def test_singleton_resources_are_not_singularized + with_test_routes do + get '/medical/taxis/new' + assert_equal 'medical/taxes#new', @response.body + assert_equal '/medical/taxis/new', new_medical_taxis_path + + post '/medical/taxis' + assert_equal 'medical/taxes#create', @response.body + + get '/medical/taxis' + assert_equal 'medical/taxes#show', @response.body + assert_equal '/medical/taxis', medical_taxis_path + + get '/medical/taxis/edit' + assert_equal 'medical/taxes#edit', @response.body + assert_equal '/medical/taxis/edit', edit_medical_taxis_path + + put '/medical/taxis' + assert_equal 'medical/taxes#update', @response.body + + delete '/medical/taxis' + assert_equal 'medical/taxes#destroy', @response.body + end + end + + def test_greedy_resource_id_regexp_doesnt_match_edit_and_custom_action + with_test_routes do + get '/sections/1/edit' + assert_equal 'sections#edit', @response.body + assert_equal '/sections/1/edit', edit_section_path(:id => '1') + + get '/sections/1/preview' + assert_equal 'sections#preview', @response.body + assert_equal '/sections/1/preview', preview_section_path(:id => '1') + end + end + + def test_resource_constraints_are_pushed_to_scope + with_test_routes do + get '/wiki/articles/Ruby_on_Rails_3.0' + assert_equal 'wiki/articles#show', @response.body + assert_equal '/wiki/articles/Ruby_on_Rails_3.0', wiki_article_path(:id => 'Ruby_on_Rails_3.0') + + get '/wiki/articles/Ruby_on_Rails_3.0/comments/new' + assert_equal 'wiki/comments#new', @response.body + assert_equal '/wiki/articles/Ruby_on_Rails_3.0/comments/new', new_wiki_article_comment_path(:article_id => 'Ruby_on_Rails_3.0') + + post '/wiki/articles/Ruby_on_Rails_3.0/comments' + assert_equal 'wiki/comments#create', @response.body + assert_equal '/wiki/articles/Ruby_on_Rails_3.0/comments', wiki_article_comments_path(:article_id => 'Ruby_on_Rails_3.0') + end + end + + def test_resources_path_can_be_a_symbol + with_test_routes do + get '/pages' + assert_equal 'wiki_pages#index', @response.body + assert_equal '/pages', wiki_pages_path + + get '/pages/Ruby_on_Rails' + assert_equal 'wiki_pages#show', @response.body + assert_equal '/pages/Ruby_on_Rails', wiki_page_path(:id => 'Ruby_on_Rails') + + get '/my_account' + assert_equal 'wiki_accounts#show', @response.body + assert_equal '/my_account', wiki_account_path + end + end + + def test_redirect_https + with_test_routes do + with_https do + get '/secure' + verify_redirect 'https://www.example.com/secure/login' + end + end + end + + def test_symbolized_path_parameters_is_not_stale + get '/countries/France' + assert_equal 'countries#index', @response.body + + get '/countries/France/cities' + assert_equal 'countries#cities', @response.body + + get '/countries/UK' + verify_redirect 'http://www.example.com/countries/all' + + get '/countries/UK/cities' + verify_redirect 'http://www.example.com/countries/all/cities' + end + + def test_custom_resource_actions_defined_using_string + get '/customers/inactive' + assert_equal 'customers#inactive', @response.body + assert_equal '/customers/inactive', inactive_customers_path + + post '/customers/1/deactivate' + assert_equal 'customers#deactivate', @response.body + assert_equal '/customers/1/deactivate', deactivate_customer_path(:id => '1') + + get '/customers/old' + assert_equal 'customers#old', @response.body + assert_equal '/customers/old', stale_customers_path + + get '/customers/1/invoices/aged/3' + assert_equal 'invoices#aged', @response.body + assert_equal '/customers/1/invoices/aged/3', aged_customer_invoices_path(:customer_id => '1', :months => '3') + end + + def test_route_defined_in_resources_scope_level + get '/customers/1/export' + assert_equal 'customers#export', @response.body + assert_equal '/customers/1/export', customer_export_path(:customer_id => '1') + end + + def test_named_character_classes_in_regexp_constraints + get '/purchases/315004be7e/Ruby_on_Rails_3.pdf' + assert_equal 'purchases#fetch', @response.body + assert_equal '/purchases/315004be7e/Ruby_on_Rails_3.pdf', purchase_path(:token => '315004be7e', :filename => 'Ruby_on_Rails_3.pdf') + end + + def test_nested_resource_constraints + get '/lists/01234012340123401234fffff' + assert_equal 'lists#show', @response.body + assert_equal '/lists/01234012340123401234fffff', list_path(:id => '01234012340123401234fffff') + + get '/lists/01234012340123401234fffff/todos/1' + assert_equal 'todos#show', @response.body + assert_equal '/lists/01234012340123401234fffff/todos/1', list_todo_path(:list_id => '01234012340123401234fffff', :id => '1') + + get '/lists/2/todos/1' + assert_equal 'Not Found', @response.body + assert_raises(ActionController::RoutingError){ list_todo_path(:list_id => '2', :id => '1') } + end + + def test_named_routes_collision_is_avoided_unless_explicitly_given_as + assert_equal "/c/1", routes_collision_path(1) + assert_equal "/forced_collision", routes_forced_collision_path + end + + def test_explicitly_avoiding_the_named_route + assert !respond_to?(:routes_no_collision_path) + end + + def test_controller_name_with_leading_slash_raise_error + assert_raise(ArgumentError) do + self.class.stub_controllers do |routes| + routes.draw { get '/feeds/:service', :to => '/feeds#show' } + end + end + + assert_raise(ArgumentError) do + self.class.stub_controllers do |routes| + routes.draw { get '/feeds/:service', :controller => '/feeds', :action => 'show' } + end + end + + assert_raise(ArgumentError) do + self.class.stub_controllers do |routes| + routes.draw { get '/api/feeds/:service', :to => '/api/feeds#show' } + end + end + + assert_raise(ArgumentError) do + self.class.stub_controllers do |routes| + routes.draw { controller("/feeds") { get '/feeds/:service', :to => :show } } + end + end + + assert_raise(ArgumentError) do + self.class.stub_controllers do |routes| + routes.draw { resources :feeds, :controller => '/feeds' } + end + end + end + + def test_invalid_route_name_raises_error + assert_raise(ArgumentError) do + self.class.stub_controllers do |routes| + routes.draw { get '/products', :to => 'products#index', :as => 'products ' } + end + end + + assert_raise(ArgumentError) do + self.class.stub_controllers do |routes| + routes.draw { get '/products', :to => 'products#index', :as => ' products' } + end + end + + assert_raise(ArgumentError) do + self.class.stub_controllers do |routes| + routes.draw { get '/products', :to => 'products#index', :as => 'products!' } + end + end + + assert_raise(ArgumentError) do + self.class.stub_controllers do |routes| + routes.draw { get '/products', :to => 'products#index', :as => 'products index' } + end + end + + assert_raise(ArgumentError) do + self.class.stub_controllers do |routes| + routes.draw { get '/products', :to => 'products#index', :as => '1products' } + end + end + end + + def test_nested_route_in_nested_resource + get "/posts/1/comments/2/views" + assert_equal "comments#views", @response.body + assert_equal "/posts/1/comments/2/views", post_comment_views_path(:post_id => '1', :comment_id => '2') + end + + def test_root_in_deeply_nested_scope + get "/posts/1/admin" + assert_equal "admin/index#index", @response.body + assert_equal "/posts/1/admin", post_admin_root_path(:post_id => '1') + end + private def with_test_routes yield end + def with_https + old_https = https? + https! + yield + ensure + https!(old_https) + end + def verify_redirect(url, status=301) assert_equal status, @response.status assert_equal url, @response.headers['Location'] @@ -1899,3 +2380,96 @@ private %(<html><body>You are being <a href="#{ERB::Util.h(url)}">redirected</a>.</body></html>) end end + +class TestAppendingRoutes < ActionDispatch::IntegrationTest + def simple_app(resp) + lambda { |e| [ 200, { 'Content-Type' => 'text/plain' }, [resp] ] } + end + + setup do + s = self + @app = ActionDispatch::Routing::RouteSet.new + @app.append do + match '/hello' => s.simple_app('fail') + match '/goodbye' => s.simple_app('goodbye') + end + + @app.draw do + match '/hello' => s.simple_app('hello') + end + end + + def test_goodbye_should_be_available + get '/goodbye' + assert_equal 'goodbye', @response.body + end + + def test_hello_should_not_be_overwritten + get '/hello' + assert_equal 'hello', @response.body + end + + def test_missing_routes_are_still_missing + get '/random' + assert_equal 404, @response.status + end +end + +class TestDefaultScope < ActionDispatch::IntegrationTest + module ::Blog + class PostsController < ActionController::Base + def index + render :text => "blog/posts#index" + end + end + end + + DefaultScopeRoutes = ActionDispatch::Routing::RouteSet.new + DefaultScopeRoutes.default_scope = {:module => :blog} + DefaultScopeRoutes.draw do + resources :posts + end + + def app + DefaultScopeRoutes + end + + include DefaultScopeRoutes.url_helpers + + def test_default_scope + get '/posts' + assert_equal "blog/posts#index", @response.body + end +end + +class TestHttpMethods < ActionDispatch::IntegrationTest + RFC2616 = %w(OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT) + RFC2518 = %w(PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK) + RFC3253 = %w(VERSION-CONTROL REPORT CHECKOUT CHECKIN UNCHECKOUT MKWORKSPACE UPDATE LABEL MERGE BASELINE-CONTROL MKACTIVITY) + RFC3648 = %w(ORDERPATCH) + RFC3744 = %w(ACL) + RFC5323 = %w(SEARCH) + RFC5789 = %w(PATCH) + + def simple_app(response) + lambda { |env| [ 200, { 'Content-Type' => 'text/plain' }, [response] ] } + end + + setup do + s = self + @app = ActionDispatch::Routing::RouteSet.new + + @app.draw do + (RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC5789).each do |method| + match '/' => s.simple_app(method), :via => method.underscore.to_sym + end + end + end + + (RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC5789).each do |method| + test "request method #{method.underscore} can be matched" do + get '/', nil, 'REQUEST_METHOD' => method + assert_equal method, @response.body + end + end +end diff --git a/actionpack/test/dispatch/session/cookie_store_test.rb b/actionpack/test/dispatch/session/cookie_store_test.rb index 3864821ef0..27f55fd7ab 100644 --- a/actionpack/test/dispatch/session/cookie_store_test.rb +++ b/actionpack/test/dispatch/session/cookie_store_test.rb @@ -1,7 +1,7 @@ require 'abstract_unit' require 'stringio' -class CookieStoreTest < ActionController::IntegrationTest +class CookieStoreTest < ActionDispatch::IntegrationTest SessionKey = '_myapp_session' SessionSecret = 'b3c631c314c0bbca50c1b2843150fe33' @@ -53,18 +53,6 @@ class CookieStoreTest < ActionController::IntegrationTest def rescue_action(e) raise end end - def test_raises_argument_error_if_missing_session_key - assert_raise(ArgumentError, nil.inspect) { - ActionDispatch::Session::CookieStore.new(nil, - :key => nil, :secret => SessionSecret) - } - - assert_raise(ArgumentError, ''.inspect) { - ActionDispatch::Session::CookieStore.new(nil, - :key => '', :secret => SessionSecret) - } - end - def test_setting_session_value with_test_route_set do get '/set_session_value' @@ -106,6 +94,23 @@ class CookieStoreTest < ActionController::IntegrationTest end end + def test_does_not_set_secure_cookies_over_http + with_test_route_set(:secure => true) do + get '/set_session_value' + assert_response :success + assert_equal nil, headers['Set-Cookie'] + end + end + + def test_does_set_secure_cookies_over_https + with_test_route_set(:secure => true) do + get '/set_session_value', nil, 'HTTPS' => 'on' + assert_response :success + assert_equal "_myapp_session=#{response.body}; path=/; secure; HttpOnly", + headers['Set-Cookie'] + end + end + # {:foo=>#<SessionAutoloadTest::Foo bar:"baz">, :session_id=>"ce8b0752a6ab7c7af3cdb8a80e6b9e46"} SignedSerializedCookie = "BAh7BzoIZm9vbzodU2Vzc2lvbkF1dG9sb2FkVGVzdDo6Rm9vBjoJQGJhciIIYmF6Og9zZXNzaW9uX2lkIiVjZThiMDc1MmE2YWI3YzdhZjNjZGI4YTgwZTZiOWU0Ng==--2bf3af1ae8bd4e52b9ac2099258ace0c380e601c" @@ -118,11 +123,11 @@ class CookieStoreTest < ActionController::IntegrationTest assert_equal 'id: ce8b0752a6ab7c7af3cdb8a80e6b9e46', response.body, "should auto-load unloaded class" end end - end - + end + def test_deserializes_unloaded_classes_on_get_value with_test_route_set do - with_autoload_path "session_autoload_test" do + with_autoload_path "session_autoload_test" do cookies[SessionKey] = SignedSerializedCookie get '/get_session_value' assert_response :success @@ -189,7 +194,6 @@ class CookieStoreTest < ActionController::IntegrationTest with_test_route_set do get '/set_session_value' assert_response :success - session_payload = response.body assert_equal "_myapp_session=#{response.body}; path=/; HttpOnly", headers['Set-Cookie'] @@ -262,12 +266,12 @@ class CookieStoreTest < ActionController::IntegrationTest def test_session_store_with_explicit_domain with_test_route_set(:domain => "example.es") do get '/set_session_value' - assert_match /domain=example\.es/, headers['Set-Cookie'] + assert_match(/domain=example\.es/, headers['Set-Cookie']) headers['Set-Cookie'] end end - def test_session_store_without_domain + def test_session_store_without_domain with_test_route_set do get '/set_session_value' assert_no_match(/domain\=/, headers['Set-Cookie']) @@ -298,7 +302,7 @@ class CookieStoreTest < ActionController::IntegrationTest def with_test_route_set(options = {}) with_routing do |set| - set.draw do |map| + set.draw do match ':action', :to => ::CookieStoreTest::TestController end diff --git a/actionpack/test/dispatch/session/mem_cache_store_test.rb b/actionpack/test/dispatch/session/mem_cache_store_test.rb index 9bd6f9b8c4..8502bc547b 100644 --- a/actionpack/test/dispatch/session/mem_cache_store_test.rb +++ b/actionpack/test/dispatch/session/mem_cache_store_test.rb @@ -1,7 +1,7 @@ require 'abstract_unit' # You need to start a memcached server inorder to run these tests -class MemCacheStoreTest < ActionController::IntegrationTest +class MemCacheStoreTest < ActionDispatch::IntegrationTest class TestController < ActionController::Base def no_session_access head :ok @@ -11,7 +11,7 @@ class MemCacheStoreTest < ActionController::IntegrationTest session[:foo] = "bar" head :ok end - + def set_serialized_session_value session[:foo] = SessionAutoloadTest::Foo.new head :ok @@ -174,7 +174,7 @@ class MemCacheStoreTest < ActionController::IntegrationTest private def with_test_route_set with_routing do |set| - set.draw do |map| + set.draw do match ':action', :to => ::MemCacheStoreTest::TestController end diff --git a/actionpack/test/dispatch/show_exceptions_test.rb b/actionpack/test/dispatch/show_exceptions_test.rb index 4966527f4d..2a478c214f 100644 --- a/actionpack/test/dispatch/show_exceptions_test.rb +++ b/actionpack/test/dispatch/show_exceptions_test.rb @@ -1,20 +1,7 @@ require 'abstract_unit' -module ActionDispatch - class ShowExceptions - private - def public_path - "#{FIXTURE_LOAD_PATH}/public" - end - - # Silence logger - def logger - nil - end - end -end +class ShowExceptionsTest < ActionDispatch::IntegrationTest -class ShowExceptionsTest < ActionController::IntegrationTest Boomer = lambda do |env| req = ActionDispatch::Request.new(env) case req.path @@ -26,6 +13,8 @@ class ShowExceptionsTest < ActionController::IntegrationTest raise ActionController::NotImplemented when "/unprocessable_entity" raise ActionController::InvalidAuthenticityToken + when "/not_found_original_exception" + raise ActionView::Template::Error.new('template', {}, AbstractController::ActionNotFound.new) else raise "puke!" end @@ -58,15 +47,15 @@ class ShowExceptionsTest < ActionController::IntegrationTest get "/", {}, {'action_dispatch.show_exceptions' => true} assert_response 500 - assert_match /puke/, body + assert_match(/puke/, body) get "/not_found", {}, {'action_dispatch.show_exceptions' => true} assert_response 404 - assert_match /#{ActionController::UnknownAction.name}/, body + assert_match(/#{ActionController::UnknownAction.name}/, body) get "/method_not_allowed", {}, {'action_dispatch.show_exceptions' => true} assert_response 405 - assert_match /ActionController::MethodNotAllowed/, body + assert_match(/ActionController::MethodNotAllowed/, body) end end @@ -96,15 +85,15 @@ class ShowExceptionsTest < ActionController::IntegrationTest get "/", {}, {'action_dispatch.show_exceptions' => true} assert_response 500 - assert_match /puke/, body + assert_match(/puke/, body) get "/not_found", {}, {'action_dispatch.show_exceptions' => true} assert_response 404 - assert_match /#{ActionController::UnknownAction.name}/, body + assert_match(/#{ActionController::UnknownAction.name}/, body) get "/method_not_allowed", {}, {'action_dispatch.show_exceptions' => true} assert_response 405 - assert_match /ActionController::MethodNotAllowed/, body + assert_match(/ActionController::MethodNotAllowed/, body) end test "does not show filtered parameters" do @@ -113,6 +102,23 @@ class ShowExceptionsTest < ActionController::IntegrationTest get "/", {"foo"=>"bar"}, {'action_dispatch.show_exceptions' => true, 'action_dispatch.parameter_filter' => [:foo]} assert_response 500 - assert_match ""foo"=>"[FILTERED]"", body + assert_match(""foo"=>"[FILTERED]"", body) + end + + test "show registered original exception for wrapped exceptions when consider_all_requests_local is false" do + @app = ProductionApp + self.remote_addr = '208.77.188.166' + + get "/not_found_original_exception", {}, {'action_dispatch.show_exceptions' => true} + assert_response 404 + assert_match(/404 error/, body) + end + + test "show registered original exception for wrapped exceptions when consider_all_requests_local is true" do + @app = DevelopmentApp + + get "/not_found_original_exception", {}, {'action_dispatch.show_exceptions' => true} + assert_response 404 + assert_match(/AbstractController::ActionNotFound/, body) end end diff --git a/actionpack/test/dispatch/static_test.rb b/actionpack/test/dispatch/static_test.rb index e6957bb0ea..655745a848 100644 --- a/actionpack/test/dispatch/static_test.rb +++ b/actionpack/test/dispatch/static_test.rb @@ -1,35 +1,81 @@ require 'abstract_unit' +module StaticTests + def test_serves_dynamic_content + assert_equal "Hello, World!", get("/nofile").body + end + + def test_serves_static_index_at_root + assert_html "/index.html", get("/index.html") + assert_html "/index.html", get("/index") + assert_html "/index.html", get("/") + assert_html "/index.html", get("") + end + + def test_serves_static_file_in_directory + assert_html "/foo/bar.html", get("/foo/bar.html") + assert_html "/foo/bar.html", get("/foo/bar/") + assert_html "/foo/bar.html", get("/foo/bar") + end + + def test_serves_static_index_file_in_directory + assert_html "/foo/index.html", get("/foo/index.html") + assert_html "/foo/index.html", get("/foo/") + assert_html "/foo/index.html", get("/foo") + end + + private + + def assert_html(body, response) + assert_equal body, response.body + assert_equal "text/html", response.headers["Content-Type"] + end + + def get(path) + Rack::MockRequest.new(@app).request("GET", path) + end +end + class StaticTest < ActiveSupport::TestCase DummyApp = lambda { |env| [200, {"Content-Type" => "text/plain"}, ["Hello, World!"]] } App = ActionDispatch::Static.new(DummyApp, "#{FIXTURE_LOAD_PATH}/public") - test "serves dynamic content" do - assert_equal "Hello, World!", get("/nofile") + def setup + @app = App end - test "serves static index at root" do - assert_equal "/index.html", get("/index.html") - assert_equal "/index.html", get("/index") - assert_equal "/index.html", get("/") - end + include StaticTests +end - test "serves static file in directory" do - assert_equal "/foo/bar.html", get("/foo/bar.html") - assert_equal "/foo/bar.html", get("/foo/bar/") - assert_equal "/foo/bar.html", get("/foo/bar") - end +class MultipleDirectorisStaticTest < ActiveSupport::TestCase + DummyApp = lambda { |env| + [200, {"Content-Type" => "text/plain"}, ["Hello, World!"]] + } + App = ActionDispatch::Static.new(DummyApp, + { "/" => "#{FIXTURE_LOAD_PATH}/public", + "/blog" => "#{FIXTURE_LOAD_PATH}/blog_public", + "/foo" => "#{FIXTURE_LOAD_PATH}/non_existing_dir" + }) - test "serves static index file in directory" do - assert_equal "/foo/index.html", get("/foo/index.html") - assert_equal "/foo/index.html", get("/foo/") - assert_equal "/foo/index.html", get("/foo") + def setup + @app = App end - private - def get(path) - Rack::MockRequest.new(App).request("GET", path).body - end + include StaticTests + + test "serves files from other mounted directories" do + assert_html "/blog/index.html", get("/blog/index.html") + assert_html "/blog/index.html", get("/blog/index") + assert_html "/blog/index.html", get("/blog/") + + assert_html "/blog/blog.html", get("/blog/blog/") + assert_html "/blog/blog.html", get("/blog/blog.html") + assert_html "/blog/blog.html", get("/blog/blog") + + assert_html "/blog/subdir/index.html", get("/blog/subdir/index.html") + assert_html "/blog/subdir/index.html", get("/blog/subdir/") + assert_html "/blog/subdir/index.html", get("/blog/subdir") + end end diff --git a/actionpack/test/dispatch/test_request_test.rb b/actionpack/test/dispatch/test_request_test.rb index e42ade73d1..81a8c24525 100644 --- a/actionpack/test/dispatch/test_request_test.rb +++ b/actionpack/test/dispatch/test_request_test.rb @@ -36,10 +36,10 @@ class TestRequestTest < ActiveSupport::TestCase req.cookies["user_name"] = "david" assert_equal({"user_name" => "david"}, req.cookies) - assert_equal "user_name=david;", req.env["HTTP_COOKIE"] + assert_equal "user_name=david", req.env["HTTP_COOKIE"] req.cookies["login"] = "XJ-122" assert_equal({"user_name" => "david", "login" => "XJ-122"}, req.cookies) - assert_equal %w(login=XJ-122 user_name=david), req.env["HTTP_COOKIE"].split(/; ?/).sort + assert_equal %w(login=XJ-122 user_name=david), req.env["HTTP_COOKIE"].split(/; /).sort end end diff --git a/actionpack/test/dispatch/test_response_test.rb b/actionpack/test/dispatch/test_response_test.rb new file mode 100644 index 0000000000..dc17668def --- /dev/null +++ b/actionpack/test/dispatch/test_response_test.rb @@ -0,0 +1,21 @@ +require 'abstract_unit' + +class TestResponseTest < ActiveSupport::TestCase + def assert_response_code_range(range, predicate) + response = ActionDispatch::TestResponse.new + (0..599).each do |status| + response.status = status + assert_equal range.include?(status), response.send(predicate), + "ActionDispatch::TestResponse.new(#{status}).#{predicate}" + end + end + + test "helpers" do + assert_response_code_range 200..299, :success? + assert_response_code_range [404], :missing? + assert_response_code_range 300..399, :redirect? + assert_response_code_range 500..599, :error? + assert_response_code_range 500..599, :server_error? + assert_response_code_range 400..499, :client_error? + end +end diff --git a/actionpack/test/dispatch/uploaded_file_test.rb b/actionpack/test/dispatch/uploaded_file_test.rb new file mode 100644 index 0000000000..e2a7f1bad7 --- /dev/null +++ b/actionpack/test/dispatch/uploaded_file_test.rb @@ -0,0 +1,70 @@ +require 'abstract_unit' + +module ActionDispatch + class UploadedFileTest < ActiveSupport::TestCase + def test_constructor_with_argument_error + assert_raises(ArgumentError) do + Http::UploadedFile.new({}) + end + end + + def test_original_filename + uf = Http::UploadedFile.new(:filename => 'foo', :tempfile => Object.new) + assert_equal 'foo', uf.original_filename + end + + def test_content_type + uf = Http::UploadedFile.new(:type => 'foo', :tempfile => Object.new) + assert_equal 'foo', uf.content_type + end + + def test_headers + uf = Http::UploadedFile.new(:head => 'foo', :tempfile => Object.new) + assert_equal 'foo', uf.headers + end + + def test_tempfile + uf = Http::UploadedFile.new(:tempfile => 'foo') + assert_equal 'foo', uf.tempfile + end + + def test_delegates_path_to_tempfile + tf = Class.new { def path; 'thunderhorse' end } + uf = Http::UploadedFile.new(:tempfile => tf.new) + assert_equal 'thunderhorse', uf.path + end + + def test_delegates_open_to_tempfile + tf = Class.new { def open; 'thunderhorse' end } + uf = Http::UploadedFile.new(:tempfile => tf.new) + assert_equal 'thunderhorse', uf.open + end + + def test_delegates_to_tempfile + tf = Class.new { def read; 'thunderhorse' end } + uf = Http::UploadedFile.new(:tempfile => tf.new) + assert_equal 'thunderhorse', uf.read + end + + def test_delegates_to_tempfile_with_params + tf = Class.new { def read *args; args end } + uf = Http::UploadedFile.new(:tempfile => tf.new) + assert_equal %w{ thunder horse }, uf.read(*%w{ thunder horse }) + end + + def test_delegate_respects_respond_to? + tf = Class.new { def read; yield end; private :read } + uf = Http::UploadedFile.new(:tempfile => tf.new) + assert_raises(NoMethodError) do + uf.read + end + end + + def test_respond_to? + tf = Class.new { def read; yield end } + uf = Http::UploadedFile.new(:tempfile => tf.new) + assert uf.respond_to?(:headers), 'responds to headers' + assert uf.respond_to?(:read), 'responds to read' + end + end +end diff --git a/actionpack/test/dispatch/url_generation_test.rb b/actionpack/test/dispatch/url_generation_test.rb index f83651d583..2b54bc62b0 100644 --- a/actionpack/test/dispatch/url_generation_test.rb +++ b/actionpack/test/dispatch/url_generation_test.rb @@ -31,7 +31,7 @@ module TestUrlGeneration end test "the request's SCRIPT_NAME takes precedence over the routes'" do - get "/foo", {}, 'SCRIPT_NAME' => "/new" + get "/foo", {}, 'SCRIPT_NAME' => "/new", 'action_dispatch.routes' => Routes assert_equal "/new/foo", response.body end @@ -41,3 +41,4 @@ module TestUrlGeneration end end end + diff --git a/actionpack/test/fixtures/alternate_helpers/foo_helper.rb b/actionpack/test/fixtures/alternate_helpers/foo_helper.rb index a956fce6fa..2528584473 100644 --- a/actionpack/test/fixtures/alternate_helpers/foo_helper.rb +++ b/actionpack/test/fixtures/alternate_helpers/foo_helper.rb @@ -1,3 +1,3 @@ module FooHelper - def baz() end + redefine_method(:baz) {} end diff --git a/actionpack/test/fixtures/blog_public/.gitignore b/actionpack/test/fixtures/blog_public/.gitignore new file mode 100644 index 0000000000..312e635ee6 --- /dev/null +++ b/actionpack/test/fixtures/blog_public/.gitignore @@ -0,0 +1 @@ +absolute/* diff --git a/actionpack/test/fixtures/blog_public/blog.html b/actionpack/test/fixtures/blog_public/blog.html new file mode 100644 index 0000000000..79ad44c010 --- /dev/null +++ b/actionpack/test/fixtures/blog_public/blog.html @@ -0,0 +1 @@ +/blog/blog.html
\ No newline at end of file diff --git a/actionpack/test/fixtures/blog_public/index.html b/actionpack/test/fixtures/blog_public/index.html new file mode 100644 index 0000000000..2de3825481 --- /dev/null +++ b/actionpack/test/fixtures/blog_public/index.html @@ -0,0 +1 @@ +/blog/index.html
\ No newline at end of file diff --git a/actionpack/test/fixtures/blog_public/subdir/index.html b/actionpack/test/fixtures/blog_public/subdir/index.html new file mode 100644 index 0000000000..517bded335 --- /dev/null +++ b/actionpack/test/fixtures/blog_public/subdir/index.html @@ -0,0 +1 @@ +/blog/subdir/index.html
\ No newline at end of file diff --git a/actionpack/test/fixtures/companies.yml b/actionpack/test/fixtures/companies.yml index 707f72abc6..ed2992e0b1 100644 --- a/actionpack/test/fixtures/companies.yml +++ b/actionpack/test/fixtures/companies.yml @@ -17,7 +17,7 @@ Google: id: 4 name: Google rating: 4 - + Ionist: id: 5 name: Ioni.st diff --git a/actionpack/test/fixtures/company.rb b/actionpack/test/fixtures/company.rb index cbbd0edb14..cd39ea7898 100644 --- a/actionpack/test/fixtures/company.rb +++ b/actionpack/test/fixtures/company.rb @@ -6,5 +6,5 @@ class Company < ActiveRecord::Base validates_presence_of :name def validate errors.add('rating', 'rating should not be 2') if rating == 2 - end + end end
\ No newline at end of file diff --git a/actionpack/test/fixtures/custom_pattern/another.html.erb b/actionpack/test/fixtures/custom_pattern/another.html.erb new file mode 100644 index 0000000000..6d7f3bafbb --- /dev/null +++ b/actionpack/test/fixtures/custom_pattern/another.html.erb @@ -0,0 +1 @@ +Hello custom patterns!
\ No newline at end of file diff --git a/actionpack/test/fixtures/custom_pattern/html/another.erb b/actionpack/test/fixtures/custom_pattern/html/another.erb new file mode 100644 index 0000000000..dbd7e96ab6 --- /dev/null +++ b/actionpack/test/fixtures/custom_pattern/html/another.erb @@ -0,0 +1 @@ +Another template!
\ No newline at end of file diff --git a/actionpack/test/fixtures/custom_pattern/html/path.erb b/actionpack/test/fixtures/custom_pattern/html/path.erb new file mode 100644 index 0000000000..6d7f3bafbb --- /dev/null +++ b/actionpack/test/fixtures/custom_pattern/html/path.erb @@ -0,0 +1 @@ +Hello custom patterns!
\ No newline at end of file diff --git a/actionpack/test/fixtures/db_definitions/sqlite.sql b/actionpack/test/fixtures/db_definitions/sqlite.sql index 8e1947d14a..99df4b3e61 100644 --- a/actionpack/test/fixtures/db_definitions/sqlite.sql +++ b/actionpack/test/fixtures/db_definitions/sqlite.sql @@ -5,20 +5,20 @@ CREATE TABLE 'companies' ( ); CREATE TABLE 'replies' ( - 'id' INTEGER PRIMARY KEY NOT NULL, - 'content' text, - 'created_at' datetime, - 'updated_at' datetime, + 'id' INTEGER PRIMARY KEY NOT NULL, + 'content' text, + 'created_at' datetime, + 'updated_at' datetime, 'topic_id' integer, 'developer_id' integer ); CREATE TABLE 'topics' ( - 'id' INTEGER PRIMARY KEY NOT NULL, - 'title' varchar(255), - 'subtitle' varchar(255), - 'content' text, - 'created_at' datetime, + 'id' INTEGER PRIMARY KEY NOT NULL, + 'title' varchar(255), + 'subtitle' varchar(255), + 'content' text, + 'created_at' datetime, 'updated_at' datetime ); @@ -43,7 +43,7 @@ CREATE TABLE 'developers_projects' ( ); CREATE TABLE 'mascots' ( - 'id' INTEGER PRIMARY KEY NOT NULL, + 'id' INTEGER PRIMARY KEY NOT NULL, 'company_id' INTEGER NOT NULL, 'name' TEXT DEFAULT NULL );
\ No newline at end of file diff --git a/actionpack/test/fixtures/filter_test/implicit_actions/edit.html.erb b/actionpack/test/fixtures/filter_test/implicit_actions/edit.html.erb new file mode 100644 index 0000000000..8491ab9f80 --- /dev/null +++ b/actionpack/test/fixtures/filter_test/implicit_actions/edit.html.erb @@ -0,0 +1 @@ +edit
\ No newline at end of file diff --git a/actionpack/test/fixtures/filter_test/implicit_actions/show.html.erb b/actionpack/test/fixtures/filter_test/implicit_actions/show.html.erb new file mode 100644 index 0000000000..0a89cecf05 --- /dev/null +++ b/actionpack/test/fixtures/filter_test/implicit_actions/show.html.erb @@ -0,0 +1 @@ +show
\ No newline at end of file diff --git a/actionpack/test/fixtures/helpers/helpery_test_helper.rb b/actionpack/test/fixtures/helpers/helpery_test_helper.rb new file mode 100644 index 0000000000..a4f2951efa --- /dev/null +++ b/actionpack/test/fixtures/helpers/helpery_test_helper.rb @@ -0,0 +1,5 @@ +module HelperyTestHelper + def helpery_test + "Default" + end +end diff --git a/actionpack/test/fixtures/helpers/just_me_helper.rb b/actionpack/test/fixtures/helpers/just_me_helper.rb new file mode 100644 index 0000000000..b140a7b9b4 --- /dev/null +++ b/actionpack/test/fixtures/helpers/just_me_helper.rb @@ -0,0 +1,3 @@ +module JustMeHelper + def me() "mine!" end +end
\ No newline at end of file diff --git a/actionpack/test/fixtures/helpers/me_too_helper.rb b/actionpack/test/fixtures/helpers/me_too_helper.rb new file mode 100644 index 0000000000..ce56042143 --- /dev/null +++ b/actionpack/test/fixtures/helpers/me_too_helper.rb @@ -0,0 +1,3 @@ +module MeTooHelper + def me() "me too!" end +end
\ No newline at end of file diff --git a/actionpack/test/fixtures/layout_tests/alt/hello.erb b/actionpack/test/fixtures/layout_tests/alt/hello.erb new file mode 100644 index 0000000000..1055c36659 --- /dev/null +++ b/actionpack/test/fixtures/layout_tests/alt/hello.erb @@ -0,0 +1 @@ +alt/hello.erb diff --git a/actionpack/test/fixtures/layout_tests/alt/hello.rhtml b/actionpack/test/fixtures/layout_tests/alt/hello.rhtml deleted file mode 100644 index fcda6cf97a..0000000000 --- a/actionpack/test/fixtures/layout_tests/alt/hello.rhtml +++ /dev/null @@ -1 +0,0 @@ -alt/hello.rhtml diff --git a/actionpack/test/fixtures/layout_tests/alt/layouts/alt.rhtml b/actionpack/test/fixtures/layout_tests/alt/layouts/alt.erb index e69de29bb2..e69de29bb2 100644 --- a/actionpack/test/fixtures/layout_tests/alt/layouts/alt.rhtml +++ b/actionpack/test/fixtures/layout_tests/alt/layouts/alt.erb diff --git a/actionpack/test/fixtures/layout_tests/layouts/controller_name_space/nested.erb b/actionpack/test/fixtures/layout_tests/layouts/controller_name_space/nested.erb new file mode 100644 index 0000000000..121bc079a1 --- /dev/null +++ b/actionpack/test/fixtures/layout_tests/layouts/controller_name_space/nested.erb @@ -0,0 +1 @@ +controller_name_space/nested.erb <%= yield %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/layout_tests/layouts/controller_name_space/nested.rhtml b/actionpack/test/fixtures/layout_tests/layouts/controller_name_space/nested.rhtml deleted file mode 100644 index 5f86a7de4d..0000000000 --- a/actionpack/test/fixtures/layout_tests/layouts/controller_name_space/nested.rhtml +++ /dev/null @@ -1 +0,0 @@ -controller_name_space/nested.rhtml <%= yield %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/layout_tests/layouts/item.erb b/actionpack/test/fixtures/layout_tests/layouts/item.erb new file mode 100644 index 0000000000..60f04d77d5 --- /dev/null +++ b/actionpack/test/fixtures/layout_tests/layouts/item.erb @@ -0,0 +1 @@ +item.erb <%= yield %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/layout_tests/layouts/item.rhtml b/actionpack/test/fixtures/layout_tests/layouts/item.rhtml deleted file mode 100644 index 1bc7cbda06..0000000000 --- a/actionpack/test/fixtures/layout_tests/layouts/item.rhtml +++ /dev/null @@ -1 +0,0 @@ -item.rhtml <%= yield %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/layout_tests/layouts/layout_test.erb b/actionpack/test/fixtures/layout_tests/layouts/layout_test.erb new file mode 100644 index 0000000000..b74ac0840d --- /dev/null +++ b/actionpack/test/fixtures/layout_tests/layouts/layout_test.erb @@ -0,0 +1 @@ +layout_test.erb <%= yield %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/layout_tests/layouts/layout_test.rhtml b/actionpack/test/fixtures/layout_tests/layouts/layout_test.rhtml deleted file mode 100644 index c0f2642b40..0000000000 --- a/actionpack/test/fixtures/layout_tests/layouts/layout_test.rhtml +++ /dev/null @@ -1 +0,0 @@ -layout_test.rhtml <%= yield %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/layout_tests/views/goodbye.erb b/actionpack/test/fixtures/layout_tests/views/goodbye.erb new file mode 100644 index 0000000000..4ee911188e --- /dev/null +++ b/actionpack/test/fixtures/layout_tests/views/goodbye.erb @@ -0,0 +1 @@ +hello.erb
\ No newline at end of file diff --git a/actionpack/test/fixtures/layout_tests/views/goodbye.rhtml b/actionpack/test/fixtures/layout_tests/views/goodbye.rhtml deleted file mode 100644 index bbccf0913e..0000000000 --- a/actionpack/test/fixtures/layout_tests/views/goodbye.rhtml +++ /dev/null @@ -1 +0,0 @@ -hello.rhtml
\ No newline at end of file diff --git a/actionpack/test/fixtures/layout_tests/views/hello.erb b/actionpack/test/fixtures/layout_tests/views/hello.erb new file mode 100644 index 0000000000..4ee911188e --- /dev/null +++ b/actionpack/test/fixtures/layout_tests/views/hello.erb @@ -0,0 +1 @@ +hello.erb
\ No newline at end of file diff --git a/actionpack/test/fixtures/layout_tests/views/hello.rhtml b/actionpack/test/fixtures/layout_tests/views/hello.rhtml deleted file mode 100644 index bbccf0913e..0000000000 --- a/actionpack/test/fixtures/layout_tests/views/hello.rhtml +++ /dev/null @@ -1 +0,0 @@ -hello.rhtml
\ No newline at end of file diff --git a/actionpack/test/fixtures/layouts/_customers.erb b/actionpack/test/fixtures/layouts/_customers.erb new file mode 100644 index 0000000000..ae63f13cd3 --- /dev/null +++ b/actionpack/test/fixtures/layouts/_customers.erb @@ -0,0 +1 @@ +<title><%= yield Struct.new(:name).new("David") %></title>
\ No newline at end of file diff --git a/actionpack/test/fixtures/layouts/_partial_and_yield.erb b/actionpack/test/fixtures/layouts/_partial_and_yield.erb new file mode 100644 index 0000000000..74cc428ffa --- /dev/null +++ b/actionpack/test/fixtures/layouts/_partial_and_yield.erb @@ -0,0 +1,2 @@ +<%= render :partial => 'test/partial' %> +<%= yield %> diff --git a/actionpack/test/fixtures/layouts/_yield_only.erb b/actionpack/test/fixtures/layouts/_yield_only.erb new file mode 100644 index 0000000000..37f0bddbd7 --- /dev/null +++ b/actionpack/test/fixtures/layouts/_yield_only.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/actionpack/test/fixtures/layouts/_yield_with_params.erb b/actionpack/test/fixtures/layouts/_yield_with_params.erb new file mode 100644 index 0000000000..68e6557fb8 --- /dev/null +++ b/actionpack/test/fixtures/layouts/_yield_with_params.erb @@ -0,0 +1 @@ +<%= yield 'Yield!' %> diff --git a/actionpack/test/fixtures/layouts/yield_with_render_partial_inside.erb b/actionpack/test/fixtures/layouts/yield_with_render_partial_inside.erb new file mode 100644 index 0000000000..74cc428ffa --- /dev/null +++ b/actionpack/test/fixtures/layouts/yield_with_render_partial_inside.erb @@ -0,0 +1,2 @@ +<%= render :partial => 'test/partial' %> +<%= yield %> diff --git a/actionpack/test/fixtures/old_content_type/render_default_for_rxml.rxml b/actionpack/test/fixtures/old_content_type/render_default_for_builder.builder index 598d62e2fc..598d62e2fc 100644 --- a/actionpack/test/fixtures/old_content_type/render_default_for_rxml.rxml +++ b/actionpack/test/fixtures/old_content_type/render_default_for_builder.builder diff --git a/actionpack/test/fixtures/old_content_type/render_default_for_rhtml.rhtml b/actionpack/test/fixtures/old_content_type/render_default_for_erb.erb index c7926d48bb..c7926d48bb 100644 --- a/actionpack/test/fixtures/old_content_type/render_default_for_rhtml.rhtml +++ b/actionpack/test/fixtures/old_content_type/render_default_for_erb.erb diff --git a/actionpack/test/fixtures/replies.yml b/actionpack/test/fixtures/replies.yml index 66020b706a..2a3454b8bf 100644 --- a/actionpack/test/fixtures/replies.yml +++ b/actionpack/test/fixtures/replies.yml @@ -5,7 +5,7 @@ witty_retort: content: Birdman is better! created_at: <%= 6.hours.ago.to_s(:db) %> updated_at: nil - + another: id: 2 topic_id: 2 diff --git a/actionpack/test/fixtures/star_star_mime/index.js.erb b/actionpack/test/fixtures/star_star_mime/index.js.erb new file mode 100644 index 0000000000..4da4181f56 --- /dev/null +++ b/actionpack/test/fixtures/star_star_mime/index.js.erb @@ -0,0 +1 @@ +function addition(a,b){ return a+b; } diff --git a/actionpack/test/fixtures/test/_layout_with_partial_and_yield.html.erb b/actionpack/test/fixtures/test/_layout_with_partial_and_yield.html.erb new file mode 100644 index 0000000000..5db0822f07 --- /dev/null +++ b/actionpack/test/fixtures/test/_layout_with_partial_and_yield.html.erb @@ -0,0 +1,4 @@ +Before +<%= render :partial => "test/partial.html.erb" %> +<%= yield %> +After diff --git a/actionpack/test/fixtures/test/_object_inspector.erb b/actionpack/test/fixtures/test/_object_inspector.erb new file mode 100644 index 0000000000..53af593821 --- /dev/null +++ b/actionpack/test/fixtures/test/_object_inspector.erb @@ -0,0 +1 @@ +<%= object_inspector.inspect -%>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_partial_with_layout.erb b/actionpack/test/fixtures/test/_partial_with_layout.erb new file mode 100644 index 0000000000..2a50c834fe --- /dev/null +++ b/actionpack/test/fixtures/test/_partial_with_layout.erb @@ -0,0 +1,2 @@ +<%= render :partial => 'test/partial', :layout => 'test/layout_for_partial', :locals => { :name => 'Bar!' } %> +partial with layout diff --git a/actionpack/test/fixtures/test/_partial_with_layout_block_content.erb b/actionpack/test/fixtures/test/_partial_with_layout_block_content.erb new file mode 100644 index 0000000000..65dafd93a8 --- /dev/null +++ b/actionpack/test/fixtures/test/_partial_with_layout_block_content.erb @@ -0,0 +1,4 @@ +<%= render :layout => 'test/layout_for_partial', :locals => { :name => 'Bar!' } do %> + Content from inside layout! +<% end %> +partial with layout diff --git a/actionpack/test/fixtures/test/_partial_with_layout_block_partial.erb b/actionpack/test/fixtures/test/_partial_with_layout_block_partial.erb new file mode 100644 index 0000000000..444197a7d0 --- /dev/null +++ b/actionpack/test/fixtures/test/_partial_with_layout_block_partial.erb @@ -0,0 +1,4 @@ +<%= render :layout => 'test/layout_for_partial', :locals => { :name => 'Bar!' } do %> + <%= render 'test/partial' %> +<% end %> +partial with layout diff --git a/actionpack/test/fixtures/test/_partial_with_partial.erb b/actionpack/test/fixtures/test/_partial_with_partial.erb new file mode 100644 index 0000000000..ee0d5037b6 --- /dev/null +++ b/actionpack/test/fixtures/test/_partial_with_partial.erb @@ -0,0 +1,2 @@ +<%= render 'test/partial' %> +partial with partial diff --git a/actionpack/test/fixtures/test/hello_world_from_rxml.builder b/actionpack/test/fixtures/test/hello_world_from_rxml.builder index 8455b11edc..619a97ba96 100644 --- a/actionpack/test/fixtures/test/hello_world_from_rxml.builder +++ b/actionpack/test/fixtures/test/hello_world_from_rxml.builder @@ -1,4 +1,3 @@ xml.html do xml.p "Hello" end -"String return value" diff --git a/actionpack/test/fixtures/test/hello_xml_world.builder b/actionpack/test/fixtures/test/hello_xml_world.builder index 02b14fe87c..e7081b89fe 100644 --- a/actionpack/test/fixtures/test/hello_xml_world.builder +++ b/actionpack/test/fixtures/test/hello_xml_world.builder @@ -2,7 +2,7 @@ xml.html do xml.head do xml.title "Hello World" end - + xml.body do xml.p "abes" xml.p "monks" diff --git a/actionpack/test/fixtures/test/layout_render_object.erb b/actionpack/test/fixtures/test/layout_render_object.erb new file mode 100644 index 0000000000..acc4453c08 --- /dev/null +++ b/actionpack/test/fixtures/test/layout_render_object.erb @@ -0,0 +1 @@ +<%= render :layout => "layouts/customers" do |customer| %><%= customer.name %><% end %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/proper_block_detection.erb b/actionpack/test/fixtures/test/proper_block_detection.erb index 23564dbcee..b55efbb25d 100644 --- a/actionpack/test/fixtures/test/proper_block_detection.erb +++ b/actionpack/test/fixtures/test/proper_block_detection.erb @@ -1 +1 @@ -<%= @todo %> +<%= @todo %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/scoped_translation.erb b/actionpack/test/fixtures/test/scoped_translation.erb deleted file mode 100644 index 3be63ab3cc..0000000000 --- a/actionpack/test/fixtures/test/scoped_translation.erb +++ /dev/null @@ -1 +0,0 @@ -<%= t('.foo.bar').join %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/translation.erb b/actionpack/test/fixtures/test/translation.erb deleted file mode 100644 index 81a837d1ff..0000000000 --- a/actionpack/test/fixtures/test/translation.erb +++ /dev/null @@ -1 +0,0 @@ -<%= t('.helper') %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/topics.yml b/actionpack/test/fixtures/topics.yml index 61ea02d76f..7fdd49d54e 100644 --- a/actionpack/test/fixtures/topics.yml +++ b/actionpack/test/fixtures/topics.yml @@ -5,7 +5,7 @@ futurama: content: I like futurama created_at: <%= 1.day.ago.to_s(:db) %> updated_at: - + harvey_birdman: id: 2 title: Harvey Birdman is the king of all men diff --git a/actionpack/test/fixtures/translations/templates/array.erb b/actionpack/test/fixtures/translations/templates/array.erb new file mode 100644 index 0000000000..d86045a172 --- /dev/null +++ b/actionpack/test/fixtures/translations/templates/array.erb @@ -0,0 +1 @@ +<%= t('.foo.bar') %> diff --git a/actionpack/test/fixtures/translations/templates/found.erb b/actionpack/test/fixtures/translations/templates/found.erb new file mode 100644 index 0000000000..080c9c0aee --- /dev/null +++ b/actionpack/test/fixtures/translations/templates/found.erb @@ -0,0 +1 @@ +<%= t('.foo') %> diff --git a/actionpack/test/fixtures/translations/templates/missing.erb b/actionpack/test/fixtures/translations/templates/missing.erb new file mode 100644 index 0000000000..0f3f17f8ef --- /dev/null +++ b/actionpack/test/fixtures/translations/templates/missing.erb @@ -0,0 +1 @@ +<%= t('.missing') %> diff --git a/actionpack/test/lib/controller/fake_models.rb b/actionpack/test/lib/controller/fake_models.rb index bf3e175f1f..67baf369e2 100644 --- a/actionpack/test/lib/controller/fake_models.rb +++ b/actionpack/test/lib/controller/fake_models.rb @@ -55,6 +55,11 @@ class Post < Struct.new(:title, :author_name, :body, :secret, :written_on, :cost alias_method :secret?, :secret + def initialize(*args) + super + @persisted = false + end + def persisted=(boolean) @persisted = boolean end @@ -83,7 +88,7 @@ class Comment def to_key; id ? [id] : nil end def save; @id = 1; @post_id = 1 end def persisted?; @id.present? end - def to_param; @id; end + def to_param; @id.to_s; end def name @id.nil? ? "new #{self.class.name.downcase}" : "#{self.class.name.downcase} ##{@id}" end @@ -91,6 +96,7 @@ class Comment attr_accessor :relevances def relevances_attributes=(attributes); end + attr_accessor :body end class Tag @@ -129,6 +135,20 @@ class CommentRelevance end end +class Sheep + extend ActiveModel::Naming + include ActiveModel::Conversion + + attr_reader :id + def to_key; id ? [id] : nil end + def save; @id = 1 end + def new_record?; @id.nil? end + def name + @id.nil? ? 'new sheep' : "sheep ##{@id}" + end +end + + class TagRelevance extend ActiveModel::Naming include ActiveModel::Conversion @@ -149,3 +169,34 @@ class Author < Comment attr_accessor :post def post_attributes=(attributes); end end + +module Blog + def self._railtie + self + end + + class Post < Struct.new(:title, :id) + extend ActiveModel::Naming + include ActiveModel::Conversion + + def persisted? + id.present? + end + end +end + +class ArelLike + def to_ary + true + end + def each + a = Array.new(2) { |id| Comment.new(id + 1) } + a.each { |i| yield i } + end +end + +class RenderJsonTestException < Exception + def to_json(options = nil) + return { :error => self.class.name, :message => self.to_s }.to_json + end +end diff --git a/actionpack/test/template/active_model_helper_test.rb b/actionpack/test/template/active_model_helper_test.rb index 6ab244d178..8530a72a82 100644 --- a/actionpack/test/template/active_model_helper_test.rb +++ b/actionpack/test/template/active_model_helper_test.rb @@ -59,12 +59,4 @@ class ActiveModelHelperTest < ActionView::TestCase ensure ActionView::Base.field_error_proc = old_proc if old_proc end - - def test_deprecations - %w(input form error_messages_for error_message_on).each do |method| - assert_deprecated do - send(method, "post") - end - end - end end diff --git a/actionpack/test/template/asset_tag_helper_test.rb b/actionpack/test/template/asset_tag_helper_test.rb index 6d5e4893c4..1bf748af14 100644 --- a/actionpack/test/template/asset_tag_helper_test.rb +++ b/actionpack/test/template/asset_tag_helper_test.rb @@ -54,6 +54,9 @@ class AssetTagHelperTest < ActionView::TestCase def teardown config.perform_caching = false ENV.delete('RAILS_ASSET_ID') + + JavascriptIncludeTag.expansions.clear + StylesheetIncludeTag.expansions.clear end AutoDiscoveryToTag = { @@ -89,9 +92,10 @@ class AssetTagHelperTest < ActionView::TestCase %(javascript_include_tag("bank", :lang => "vbscript")) => %(<script lang="vbscript" src="/javascripts/bank.js" type="text/javascript"></script>), %(javascript_include_tag("common.javascript", "/elsewhere/cools")) => %(<script src="/javascripts/common.javascript" type="text/javascript"></script>\n<script src="/elsewhere/cools.js" type="text/javascript"></script>), %(javascript_include_tag(:defaults)) => %(<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/rails.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>), - %(javascript_include_tag(:all)) => %(<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>\n<script src="/javascripts/bank.js" type="text/javascript"></script>\n<script src="/javascripts/robber.js" type="text/javascript"></script>\n<script src="/javascripts/version.1.0.js" type="text/javascript"></script>), - %(javascript_include_tag(:all, :recursive => true)) => %(<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>\n<script src="/javascripts/bank.js" type="text/javascript"></script>\n<script src="/javascripts/robber.js" type="text/javascript"></script>\n<script src="/javascripts/subdir/subdir.js" type="text/javascript"></script>\n<script src="/javascripts/version.1.0.js" type="text/javascript"></script>), + %(javascript_include_tag(:all)) => %(<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/bank.js" type="text/javascript"></script>\n<script src="/javascripts/robber.js" type="text/javascript"></script>\n<script src="/javascripts/version.1.0.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>), + %(javascript_include_tag(:all, :recursive => true)) => %(<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/bank.js" type="text/javascript"></script>\n<script src="/javascripts/robber.js" type="text/javascript"></script>\n<script src="/javascripts/subdir/subdir.js" type="text/javascript"></script>\n<script src="/javascripts/version.1.0.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>), %(javascript_include_tag(:defaults, "bank")) => %(<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/rails.js" type="text/javascript"></script>\n<script src="/javascripts/bank.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>), + %(javascript_include_tag(:defaults, "application")) => %(<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/rails.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>), %(javascript_include_tag("bank", :defaults)) => %(<script src="/javascripts/bank.js" type="text/javascript"></script>\n<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/rails.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>), %(javascript_include_tag("http://example.com/all")) => %(<script src="http://example.com/all" type="text/javascript"></script>), @@ -262,19 +266,66 @@ class AssetTagHelperTest < ActionView::TestCase assert_dom_equal %(<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/bank.js" type="text/javascript"></script>\n<script src="/javascripts/robber.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>), javascript_include_tag('controls', :robbery, 'effects') end + def test_custom_javascript_expansions_return_unique_set + ENV["RAILS_ASSET_ID"] = "" + ActionView::Helpers::AssetTagHelper::register_javascript_expansion :defaults => %w(prototype effects dragdrop controls rails application) + assert_dom_equal %(<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/rails.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>), javascript_include_tag(:defaults) + end + def test_custom_javascript_expansions_and_defaults_puts_application_js_at_the_end ENV["RAILS_ASSET_ID"] = "" ActionView::Helpers::AssetTagHelper::register_javascript_expansion :robbery => ["bank", "robber"] - assert_dom_equal %(<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/rails.js" type="text/javascript"></script>\n<script src="/javascripts/bank.js" type="text/javascript"></script>\n<script src="/javascripts/robber.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>), javascript_include_tag('controls',:defaults, :robbery, 'effects') + assert_dom_equal %(<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/rails.js" type="text/javascript"></script>\n<script src="/javascripts/bank.js" type="text/javascript"></script>\n<script src="/javascripts/robber.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>), javascript_include_tag('controls',:defaults, :robbery, 'effects') + end + + def test_javascript_include_tag_should_not_output_the_same_asset_twice + ENV["RAILS_ASSET_ID"] = "" + assert_dom_equal %(<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/rails.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>), javascript_include_tag('prototype', 'effects', :defaults) + end + + def test_javascript_include_tag_should_not_output_the_same_expansion_twice + ENV["RAILS_ASSET_ID"] = "" + assert_dom_equal %(<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/rails.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>), javascript_include_tag(:defaults, :defaults) + end + + def test_single_javascript_asset_keys_should_take_precedence_over_expansions + ENV["RAILS_ASSET_ID"] = "" + assert_dom_equal %(<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/rails.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>), javascript_include_tag('controls', :defaults, 'effects') + assert_dom_equal %(<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/rails.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>), javascript_include_tag('controls', 'effects', :defaults) + end + + def test_registering_javascript_expansions_merges_with_existing_expansions + ENV["RAILS_ASSET_ID"] = "" + ActionView::Helpers::AssetTagHelper::register_javascript_expansion :can_merge => ['bank'] + ActionView::Helpers::AssetTagHelper::register_javascript_expansion :can_merge => ['robber'] + ActionView::Helpers::AssetTagHelper::register_javascript_expansion :can_merge => ['bank'] + assert_dom_equal %(<script src="/javascripts/bank.js" type="text/javascript"></script>\n<script src="/javascripts/robber.js" type="text/javascript"></script>), javascript_include_tag(:can_merge) end def test_custom_javascript_expansions_with_undefined_symbol + assert_raise(ArgumentError) { javascript_include_tag('first', :unknown, 'last') } + end + + def test_custom_javascript_expansions_with_nil_value ActionView::Helpers::AssetTagHelper::register_javascript_expansion :monkey => nil - assert_raise(ArgumentError) { javascript_include_tag('first', :monkey, 'last') } + assert_dom_equal %(<script src="/javascripts/first.js" type="text/javascript"></script>\n<script src="/javascripts/last.js" type="text/javascript"></script>), javascript_include_tag('first', :monkey, 'last') + end + + def test_custom_javascript_expansions_with_empty_array_value + ActionView::Helpers::AssetTagHelper::register_javascript_expansion :monkey => [] + assert_dom_equal %(<script src="/javascripts/first.js" type="text/javascript"></script>\n<script src="/javascripts/last.js" type="text/javascript"></script>), javascript_include_tag('first', :monkey, 'last') + end + + def test_custom_javascript_and_stylesheet_expansion_with_same_name + ENV["RAILS_ASSET_ID"] = "" + ActionView::Helpers::AssetTagHelper::register_javascript_expansion :robbery => ["bank", "robber"] + ActionView::Helpers::AssetTagHelper::register_stylesheet_expansion :robbery => ["money", "security"] + assert_dom_equal %(<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/bank.js" type="text/javascript"></script>\n<script src="/javascripts/robber.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>), javascript_include_tag('controls', :robbery, 'effects') + assert_dom_equal %(<link href="/stylesheets/style.css" rel="stylesheet" type="text/css" media="screen" />\n<link href="/stylesheets/money.css" rel="stylesheet" type="text/css" media="screen" />\n<link href="/stylesheets/security.css" rel="stylesheet" type="text/css" media="screen" />\n<link href="/stylesheets/print.css" rel="stylesheet" type="text/css" media="screen" />), stylesheet_link_tag('style', :robbery, 'print') end def test_reset_javascript_expansions - ActionView::Helpers::AssetTagHelper.javascript_expansions.clear + JavascriptIncludeTag.expansions.clear assert_raise(ArgumentError) { javascript_include_tag(:defaults) } end @@ -306,7 +357,6 @@ class AssetTagHelperTest < ActionView::TestCase ENV["RAILS_ASSET_ID"] = "" assert stylesheet_link_tag('dir/file').html_safe? assert stylesheet_link_tag('dir/other/file', 'dir/file2').html_safe? - assert stylesheet_tag('dir/file', {}).html_safe? end def test_custom_stylesheet_expansions @@ -315,9 +365,49 @@ class AssetTagHelperTest < ActionView::TestCase assert_dom_equal %(<link href="/stylesheets/version.1.0.css" media="screen" rel="stylesheet" type="text/css" />\n<link href="/stylesheets/bank.css" media="screen" rel="stylesheet" type="text/css" />\n<link href="/stylesheets/robber.css" media="screen" rel="stylesheet" type="text/css" />\n<link href="/stylesheets/subdir/subdir.css" media="screen" rel="stylesheet" type="text/css" />), stylesheet_link_tag('version.1.0', :robbery, 'subdir/subdir') end - def test_custom_stylesheet_expansions_with_undefined_symbol + def test_custom_stylesheet_expansions_return_unique_set + ENV["RAILS_ASSET_ID"] = "" + ActionView::Helpers::AssetTagHelper::register_stylesheet_expansion :cities => %w(wellington amsterdam london) + assert_dom_equal %(<link href="/stylesheets/wellington.css" media="screen" rel="stylesheet" type="text/css" />\n<link href="/stylesheets/amsterdam.css" media="screen" rel="stylesheet" type="text/css" />\n<link href="/stylesheets/london.css" media="screen" rel="stylesheet" type="text/css" />), stylesheet_link_tag(:cities) + end + + def test_stylesheet_link_tag_should_not_output_the_same_asset_twice + ENV["RAILS_ASSET_ID"] = "" + assert_dom_equal %(<link href="/stylesheets/wellington.css" media="screen" rel="stylesheet" type="text/css" />\n<link href="/stylesheets/amsterdam.css" media="screen" rel="stylesheet" type="text/css" />), stylesheet_link_tag('wellington', 'wellington', 'amsterdam') + end + + def test_stylesheet_link_tag_should_not_output_the_same_expansion_twice + ENV["RAILS_ASSET_ID"] = "" + ActionView::Helpers::AssetTagHelper::register_stylesheet_expansion :cities => %w(wellington amsterdam london) + assert_dom_equal %(<link href="/stylesheets/wellington.css" media="screen" rel="stylesheet" type="text/css" />\n<link href="/stylesheets/amsterdam.css" media="screen" rel="stylesheet" type="text/css" />\n<link href="/stylesheets/london.css" media="screen" rel="stylesheet" type="text/css" />), stylesheet_link_tag(:cities, :cities) + end + + def test_single_stylesheet_asset_keys_should_take_precedence_over_expansions + ENV["RAILS_ASSET_ID"] = "" + ActionView::Helpers::AssetTagHelper::register_stylesheet_expansion :cities => %w(wellington amsterdam london) + assert_dom_equal %(<link href="/stylesheets/london.css" media="screen" rel="stylesheet" type="text/css" />\n<link href="/stylesheets/wellington.css" media="screen" rel="stylesheet" type="text/css" />\n<link href="/stylesheets/amsterdam.css" media="screen" rel="stylesheet" type="text/css" />), stylesheet_link_tag('london', :cities) + end + + def test_custom_stylesheet_expansions_with_unknown_symbol + assert_raise(ArgumentError) { stylesheet_link_tag('first', :unknown, 'last') } + end + + def test_custom_stylesheet_expansions_with_nil_value ActionView::Helpers::AssetTagHelper::register_stylesheet_expansion :monkey => nil - assert_raise(ArgumentError) { stylesheet_link_tag('first', :monkey, 'last') } + assert_dom_equal %(<link href="/stylesheets/first.css" rel="stylesheet" type="text/css" media="screen" />\n<link href="/stylesheets/last.css" rel="stylesheet" type="text/css" media="screen" />), stylesheet_link_tag('first', :monkey, 'last') + end + + def test_custom_stylesheet_expansions_with_empty_array_value + ActionView::Helpers::AssetTagHelper::register_stylesheet_expansion :monkey => [] + assert_dom_equal %(<link href="/stylesheets/first.css" rel="stylesheet" type="text/css" media="screen" />\n<link href="/stylesheets/last.css" rel="stylesheet" type="text/css" media="screen" />), stylesheet_link_tag('first', :monkey, 'last') + end + + def test_registering_stylesheet_expansions_merges_with_existing_expansions + ENV["RAILS_ASSET_ID"] = "" + ActionView::Helpers::AssetTagHelper::register_stylesheet_expansion :can_merge => ['bank'] + ActionView::Helpers::AssetTagHelper::register_stylesheet_expansion :can_merge => ['robber'] + ActionView::Helpers::AssetTagHelper::register_stylesheet_expansion :can_merge => ['bank'] + assert_dom_equal %(<link href="/stylesheets/bank.css" media="screen" rel="stylesheet" type="text/css" />\n<link href="/stylesheets/robber.css" media="screen" rel="stylesheet" type="text/css" />), stylesheet_link_tag(:can_merge) end def test_image_path @@ -387,6 +477,15 @@ class AssetTagHelperTest < ActionView::TestCase assert_equal %(<img alt="Rails" src="#{expected_path}" />), image_tag("rails.png") end + def test_env_asset_path + @controller.config.asset_path = "/assets%s" + def @controller.env; @_env ||= {} end + @controller.env["action_dispatch.asset_path"] = "/omg%s" + + expected_path = "/assets/omg/images/rails.png" + assert_equal %(<img alt="Rails" src="#{expected_path}" />), image_tag("rails.png") + end + def test_proc_asset_id @controller.config.asset_path = Proc.new do |asset_path| "/assets.v12345#{asset_path}" @@ -396,6 +495,20 @@ class AssetTagHelperTest < ActionView::TestCase assert_equal %(<img alt="Rails" src="#{expected_path}" />), image_tag("rails.png") end + def test_env_proc_asset_path + @controller.config.asset_path = Proc.new do |asset_path| + "/assets.v12345#{asset_path}" + end + + def @controller.env; @_env ||= {} end + @controller.env["action_dispatch.asset_path"] = Proc.new do |asset_path| + "/omg#{asset_path}" + end + + expected_path = "/assets.v12345/omg/images/rails.png" + assert_equal %(<img alt="Rails" src="#{expected_path}" />), image_tag("rails.png") + end + def test_image_tag_interpreting_email_cid_correctly # An inline image has no need for an alt tag to be automatically generated from the cid: assert_equal '<img src="cid:thi%25%25sis@acontentid" />', image_tag("cid:thi%25%25sis@acontentid") @@ -605,7 +718,7 @@ class AssetTagHelperTest < ActionView::TestCase assert File.exist?(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'combined.js')) assert_equal( - %(// prototype js\n\n// effects js\n\n// dragdrop js\n\n// controls js\n\n// application js\n\n// bank js\n\n// robber js\n\n// subdir js\n\n\n// version.1.0 js), + %(// prototype js\n\n// effects js\n\n// dragdrop js\n\n// controls js\n\n// bank js\n\n// robber js\n\n// subdir js\n\n\n// version.1.0 js\n\n// application js), IO.read(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'combined.js')) ) @@ -626,7 +739,7 @@ class AssetTagHelperTest < ActionView::TestCase assert File.exist?(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'combined.js')) assert_equal( - %(// prototype js\n\n// effects js\n\n// dragdrop js\n\n// controls js\n\n// application js\n\n// bank js\n\n// robber js\n\n// version.1.0 js), + %(// prototype js\n\n// effects js\n\n// dragdrop js\n\n// controls js\n\n// bank js\n\n// robber js\n\n// version.1.0 js\n\n// application js), IO.read(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'combined.js')) ) @@ -658,29 +771,49 @@ class AssetTagHelperTest < ActionView::TestCase FileUtils.rm_f(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'money.js')) end + def test_caching_javascript_include_tag_with_named_paths_and_relative_url_root_when_caching_off + ENV["RAILS_ASSET_ID"] = "" + @controller.config.relative_url_root = "/collaboration/hieraki" + config.perform_caching = false + + assert_dom_equal( + %(<script src="/collaboration/hieraki/javascripts/robber.js" type="text/javascript"></script>), + javascript_include_tag('robber', :cache => true) + ) + + assert !File.exist?(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'all.js')) + + assert_dom_equal( + %(<script src="/collaboration/hieraki/javascripts/robber.js" type="text/javascript"></script>), + javascript_include_tag('robber', :cache => "money", :recursive => true) + ) + + assert !File.exist?(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'money.js')) + end + def test_caching_javascript_include_tag_when_caching_off ENV["RAILS_ASSET_ID"] = "" config.perform_caching = false assert_dom_equal( - %(<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>\n<script src="/javascripts/bank.js" type="text/javascript"></script>\n<script src="/javascripts/robber.js" type="text/javascript"></script>\n<script src="/javascripts/version.1.0.js" type="text/javascript"></script>), + %(<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/bank.js" type="text/javascript"></script>\n<script src="/javascripts/robber.js" type="text/javascript"></script>\n<script src="/javascripts/version.1.0.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>), javascript_include_tag(:all, :cache => true) ) assert_dom_equal( - %(<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>\n<script src="/javascripts/bank.js" type="text/javascript"></script>\n<script src="/javascripts/robber.js" type="text/javascript"></script>\n<script src="/javascripts/subdir/subdir.js" type="text/javascript"></script>\n<script src="/javascripts/version.1.0.js" type="text/javascript"></script>), + %(<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/bank.js" type="text/javascript"></script>\n<script src="/javascripts/robber.js" type="text/javascript"></script>\n<script src="/javascripts/subdir/subdir.js" type="text/javascript"></script>\n<script src="/javascripts/version.1.0.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>), javascript_include_tag(:all, :cache => true, :recursive => true) ) assert !File.exist?(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'all.js')) assert_dom_equal( - %(<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>\n<script src="/javascripts/bank.js" type="text/javascript"></script>\n<script src="/javascripts/robber.js" type="text/javascript"></script>\n<script src="/javascripts/version.1.0.js" type="text/javascript"></script>), + %(<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/bank.js" type="text/javascript"></script>\n<script src="/javascripts/robber.js" type="text/javascript"></script>\n<script src="/javascripts/version.1.0.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>), javascript_include_tag(:all, :cache => "money") ) assert_dom_equal( - %(<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>\n<script src="/javascripts/bank.js" type="text/javascript"></script>\n<script src="/javascripts/robber.js" type="text/javascript"></script>\n<script src="/javascripts/subdir/subdir.js" type="text/javascript"></script>\n<script src="/javascripts/version.1.0.js" type="text/javascript"></script>), + %(<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/bank.js" type="text/javascript"></script>\n<script src="/javascripts/robber.js" type="text/javascript"></script>\n<script src="/javascripts/subdir/subdir.js" type="text/javascript"></script>\n<script src="/javascripts/version.1.0.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>), javascript_include_tag(:all, :cache => "money", :recursive => true) ) @@ -704,6 +837,17 @@ class AssetTagHelperTest < ActionView::TestCase assert !File.exist?(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'money.js')) end + def test_caching_javascript_include_tag_when_caching_on_and_javascript_file_is_uri + ENV["RAILS_ASSET_ID"] = "" + config.perform_caching = true + + assert_raise(Errno::ENOENT) { + javascript_include_tag('bank', 'robber', 'https://ajax.googleapis.com/ajax/libs/jquery/1.4.3/jquery.js', :cache => true) + } + + assert !File.exist?(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'all.js')) + end + def test_caching_javascript_include_tag_when_caching_off_and_missing_javascript_file ENV["RAILS_ASSET_ID"] = "" config.perform_caching = false @@ -874,6 +1018,30 @@ class AssetTagHelperTest < ActionView::TestCase FileUtils.rm_f(File.join(ActionView::Helpers::AssetTagHelper::STYLESHEETS_DIR, 'money.css')) end + + def test_caching_stylesheet_link_tag_with_named_paths_and_relative_url_root_when_caching_off + ENV["RAILS_ASSET_ID"] = "" + @controller.config.relative_url_root = "/collaboration/hieraki" + config.perform_caching = false + + assert_dom_equal( + %(<link href="/collaboration/hieraki/stylesheets/robber.css" media="screen" rel="stylesheet" type="text/css" />), + stylesheet_link_tag('robber', :cache => true) + ) + + assert !File.exist?(File.join(ActionView::Helpers::AssetTagHelper::STYLESHEETS_DIR, 'all.css')) + + assert_dom_equal( + %(<link href="/collaboration/hieraki/stylesheets/robber.css" media="screen" rel="stylesheet" type="text/css" />), + stylesheet_link_tag('robber', :cache => "money") + ) + + assert !File.exist?(File.join(ActionView::Helpers::AssetTagHelper::STYLESHEETS_DIR, 'money.css')) + end + + + + def test_caching_stylesheet_include_tag_when_caching_off ENV["RAILS_ASSET_ID"] = "" config.perform_caching = false @@ -915,7 +1083,7 @@ class AssetTagHelperNonVhostTest < ActionView::TestCase @request = Struct.new(:protocol).new("gopher://") @controller.request = @request - ActionView::Helpers::AssetTagHelper.javascript_expansions.clear + JavascriptIncludeTag.expansions.clear end def url_for(options) @@ -952,7 +1120,7 @@ class AssetTagHelperNonVhostTest < ActionView::TestCase def test_should_wildcard_asset_host_between_zero_and_four @controller.config.asset_host = 'http://a%d.example.com' - assert_match %r(http://a[0123].example.com/collaboration/hieraki/images/xml.png), image_path('xml.png') + assert_match(%r(http://a[0123].example.com/collaboration/hieraki/images/xml.png), image_path('xml.png')) end def test_asset_host_without_protocol_should_use_request_protocol diff --git a/actionpack/test/template/atom_feed_helper_test.rb b/actionpack/test/template/atom_feed_helper_test.rb index 869ea22469..36102bbc4f 100644 --- a/actionpack/test/template/atom_feed_helper_test.rb +++ b/actionpack/test/template/atom_feed_helper_test.rb @@ -203,7 +203,7 @@ class AtomFeedTest < ActionController::TestCase def test_feed_should_use_default_language_if_none_is_given with_restful_routing(:scrolls) do get :index, :id => "defaults" - assert_match %r{xml:lang="en-US"}, @response.body + assert_match(%r{xml:lang="en-US"}, @response.body) end end @@ -226,7 +226,7 @@ class AtomFeedTest < ActionController::TestCase get :index, :id=>"provide_builder" # because we pass in the non-default builder, the content generated by the # helper should go 'nowhere'. Leaving the response body blank. - assert @response.body.blank? + assert_blank @response.body end end @@ -314,7 +314,7 @@ class AtomFeedTest < ActionController::TestCase private def with_restful_routing(resources) with_routing do |set| - set.draw do |map| + set.draw do resources(resources) end yield diff --git a/actionpack/test/template/capture_helper_test.rb b/actionpack/test/template/capture_helper_test.rb index f7c42c7f22..03050485fa 100644 --- a/actionpack/test/template/capture_helper_test.rb +++ b/actionpack/test/template/capture_helper_test.rb @@ -15,7 +15,6 @@ class CaptureHelperTest < ActionView::TestCase end assert_nil @av.output_buffer assert_equal 'foobar', string - assert_kind_of ActionView::NonConcattingString, string end def test_capture_captures_the_value_returned_by_the_block_if_the_temporary_buffer_is_blank @@ -23,13 +22,22 @@ class CaptureHelperTest < ActionView::TestCase a + b end assert_equal 'foobar', string - assert_kind_of ActionView::NonConcattingString, string end def test_capture_returns_nil_if_the_returned_value_is_not_a_string assert_nil @av.capture { 1 } end + def test_capture_escapes_html + string = @av.capture { '<em>bar</em>' } + assert_equal '<em>bar</em>', string + end + + def test_capture_doesnt_escape_twice + string = @av.capture { '<em>bar</em>'.html_safe } + assert_equal '<em>bar</em>', string + end + def test_content_for assert ! content_for?(:title) content_for :title, 'title' diff --git a/actionpack/test/template/compiled_templates_test.rb b/actionpack/test/template/compiled_templates_test.rb index 2c719757e4..3f31edd5ce 100644 --- a/actionpack/test/template/compiled_templates_test.rb +++ b/actionpack/test/template/compiled_templates_test.rb @@ -8,7 +8,7 @@ class CompiledTemplatesTest < Test::Unit::TestCase @compiled_templates.send(:remove_method, m) if m =~ /^_render_template_/ end end - + def test_template_gets_recompiled_when_using_different_keys_in_local_assigns assert_equal "one", render(:file => "test/render_file_with_locals_and_default.erb") assert_equal "two", render(:file => "test/render_file_with_locals_and_default.erb", :locals => { :secret => "two" }) diff --git a/actionpack/test/template/date_helper_i18n_test.rb b/actionpack/test/template/date_helper_i18n_test.rb index b69a449617..d45215acfd 100644 --- a/actionpack/test/template/date_helper_i18n_test.rb +++ b/actionpack/test/template/date_helper_i18n_test.rb @@ -5,7 +5,7 @@ class DateHelperDistanceOfTimeInWordsI18nTests < Test::Unit::TestCase attr_reader :request def setup - @from = Time.mktime(2004, 6, 6, 21, 45, 0) + @from = Time.utc(2004, 6, 6, 21, 45, 0) end # distance_of_time_in_words @@ -62,7 +62,7 @@ class DateHelperDistanceOfTimeInWordsI18nTests < Test::Unit::TestCase [:'about_x_years', 1] => 'about 1 year', [:'about_x_years', 2] => 'about 2 years', [:'over_x_years', 1] => 'over 1 year', - [:'over_x_years', 2] => 'over 2 years' + [:'over_x_years', 2] => 'over 2 years' }.each do |args, expected| key, count = *args diff --git a/actionpack/test/template/date_helper_test.rb b/actionpack/test/template/date_helper_test.rb index a1db49d4d0..aca2fef170 100644 --- a/actionpack/test/template/date_helper_test.rb +++ b/actionpack/test/template/date_helper_test.rb @@ -99,7 +99,7 @@ class DateHelperTest < ActionView::TestCase end def test_distance_in_words - from = Time.mktime(2004, 6, 6, 21, 45, 0) + from = Time.utc(2004, 6, 6, 21, 45, 0) assert_distance_of_time_in_words(from) end @@ -673,7 +673,7 @@ class DateHelperTest < ActionView::TestCase expected = %(<input id="date_first_year" name="date[first][year]" type="hidden" value="2003" />\n) expected << %(<input id="date_first_month" name="date[first][month]" type="hidden" value="8" />\n) expected << %(<input id="date_first_day" name="date[first][day]" type="hidden" value="16" />\n) - + assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005, :prefix => "date[first]", :order => [:day]) end @@ -897,7 +897,7 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { :date_separator => " / ", :discard_day => true, :start_year => 2003, :end_year => 2005, :prefix => "date[first]"}) end - + def test_select_date_with_separator_discard_month_and_day expected = %(<select id="date_first_year" name="date[first][year]">\n) expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) @@ -978,7 +978,7 @@ class DateHelperTest < ActionView::TestCase expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n) expected << "</select>\n" - expected << " — " + expected << " — " expected << %(<select id="date_first_hour" name="date[first][hour]">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n) @@ -1007,7 +1007,7 @@ class DateHelperTest < ActionView::TestCase expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n) expected << "</select>\n" - expected << " — " + expected << " — " expected << %(<select id="date_first_hour" name="date[first][hour]" class="selector">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n) @@ -1121,7 +1121,7 @@ class DateHelperTest < ActionView::TestCase expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n) expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n) - + expected << %(<select id="date_hour" name="date[hour]">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n) expected << "</select>\n" @@ -1158,7 +1158,7 @@ class DateHelperTest < ActionView::TestCase expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n) expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n) - + expected << %(<select id="date_hour" name="date[hour]">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n) expected << "</select>\n" @@ -1182,7 +1182,7 @@ class DateHelperTest < ActionView::TestCase expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n) expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n) - + expected << %(<select id="date_hour" name="date[hour]">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n) expected << "</select>\n" @@ -1206,7 +1206,7 @@ class DateHelperTest < ActionView::TestCase expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n) expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n) - + expected << %(<select id="date_hour" name="date[hour]" class="selector">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n) expected << "</select>\n" @@ -1229,11 +1229,11 @@ class DateHelperTest < ActionView::TestCase expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n) expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n) - + expected << %(<select id="date_hour" name="date[hour]">\n) expected << %(<option value="">Hour</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n) expected << "</select>\n" - + expected << " : " expected << %(<select id="date_minute" name="date[minute]">\n) @@ -1253,7 +1253,7 @@ class DateHelperTest < ActionView::TestCase expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n) expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n) - + expected << %(<select id="date_hour" name="date[hour]">\n) expected << %(<option value="">Choose hour</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n) expected << "</select>\n" @@ -1497,26 +1497,6 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, date_select("post", "written_on", :order=>[:year, :month], :include_blank=>true) end - def test_date_select_with_nil_and_blank_and_order - @post = Post.new - - start_year = Time.now.year-5 - end_year = Time.now.year+5 - - expected = '<input name="post[written_on(3i)]" type="hidden" id="post_written_on_3i"/>' + "\n" - expected << %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} - expected << "<option value=\"\"></option>\n" - start_year.upto(end_year) { |i| expected << %(<option value="#{i}">#{i}</option>\n) } - expected << "</select>\n" - - expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n} - expected << "<option value=\"\"></option>\n" - 1.upto(12) { |i| expected << %(<option value="#{i}">#{Date::MONTHNAMES[i]}</option>\n) } - expected << "</select>\n" - - assert_dom_equal expected, date_select("post", "written_on", :order=>[:year, :month], :include_blank=>true) - end - def test_date_select_cant_override_discard_hour @post = Post.new @post.written_on = Date.new(2004, 6, 15) @@ -1604,6 +1584,47 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, date_select("post", "written_on", { :date_separator => " / " }) end + def test_date_select_with_separator_and_order + @post = Post.new + @post.written_on = Date.new(2004, 6, 15) + + expected = %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n} + expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n} + expected << "</select>\n" + + expected << " / " + + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n} + expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n} + expected << "</select>\n" + + expected << " / " + + expected << %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} + expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} + expected << "</select>\n" + + assert_dom_equal expected, date_select("post", "written_on", { :order => [:day, :month, :year], :date_separator => " / " }) + end + + def test_date_select_with_separator_and_order_and_year_discarded + @post = Post.new + @post.written_on = Date.new(2004, 6, 15) + + expected = %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n} + expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n} + expected << "</select>\n" + + expected << " / " + + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n} + expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n} + expected << "</select>\n" + expected << %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n} + + assert_dom_equal expected, date_select("post", "written_on", { :order => [:day, :month, :year], :discard_year => true, :date_separator => " / " }) + end + def test_date_select_with_default_prompt @post = Post.new @post.written_on = Date.new(2004, 6, 15) @@ -1861,10 +1882,17 @@ class DateHelperTest < ActionView::TestCase end def test_datetime_select_defaults_to_time_zone_now_when_config_time_zone_is_set - time = stub(:year => 2004, :month => 6, :day => 15, :hour => 16, :min => 35, :sec => 0) - time_zone = mock() - time_zone.expects(:now).returns time - Time.zone_default = time_zone + # The love zone is UTC+0 + mytz = Class.new(ActiveSupport::TimeZone) { + attr_accessor :now + }.create('tenderlove', 0) + + now = Time.mktime(2004, 6, 15, 16, 35, 0) + mytz.now = now + Time.zone = mytz + + assert_equal mytz, Time.zone + @post = Post.new expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n} @@ -1891,7 +1919,7 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, datetime_select("post", "updated_at") ensure - Time.zone_default = nil + Time.zone = nil end def test_datetime_select_with_html_options_within_fields_for @@ -2670,13 +2698,39 @@ class DateHelperTest < ActionView::TestCase assert select_time(Time.mktime(2003, 8, 16, 8, 4, 18), {}, :class => 'selector').html_safe? assert select_date(Time.mktime(2003, 8, 16), :date_separator => " / ", :start_year => 2003, :end_year => 2005, :prefix => "date[first]").html_safe? end - + def test_object_select_html_safety @post = Post.new @post.written_on = Date.new(2004, 6, 15) - assert date_select("post", "written_on", :default => Time.local(2006, 9, 19, 15, 16, 35), :include_blank => true).html_safe? - assert time_select("post", "written_on", :ignore_date => true).html_safe? + assert date_select("post", "written_on", :default => Time.local(2006, 9, 19, 15, 16, 35), :include_blank => true).html_safe? + assert time_select("post", "written_on", :ignore_date => true).html_safe? + end + + def test_time_tag_with_date + date = Date.today + expected = "<time datetime=\"#{date.rfc3339}\">#{I18n.l(date, :format => :long)}</time>" + assert_equal expected, time_tag(date) + end + + def test_time_tag_with_time + time = Time.now + expected = "<time datetime=\"#{time.xmlschema}\">#{I18n.l(time, :format => :long)}</time>" + assert_equal expected, time_tag(time) + end + + def test_time_tag_pubdate_option + assert_match /<time.*pubdate="pubdate">.*<\/time>/, time_tag(Time.now, :pubdate => true) + end + + def test_time_tag_with_given_text + assert_match /<time.*>Right now<\/time>/, time_tag(Time.now, 'Right now') + end + + def test_time_tag_with_different_format + time = Time.now + expected = "<time datetime=\"#{time.xmlschema}\">#{I18n.l(time, :format => :short)}</time>" + assert_equal expected, time_tag(time, :format => :short) end protected diff --git a/actionpack/test/template/erb/tag_helper_test.rb b/actionpack/test/template/erb/tag_helper_test.rb index d073100986..a384e94766 100644 --- a/actionpack/test/template/erb/tag_helper_test.rb +++ b/actionpack/test/template/erb/tag_helper_test.rb @@ -2,59 +2,32 @@ require "abstract_unit" require "template/erb/helper" module ERBTest - module SharedTagHelpers - extend ActiveSupport::Testing::Declarative + class TagHelperTest < BlockTestCase - def maybe_deprecated - if @deprecated - assert_deprecated { yield } - else - yield - end - end + extend ActiveSupport::Testing::Declarative test "percent equals works for content_tag and does not require parenthesis on method call" do - maybe_deprecated { assert_equal "<div>Hello world</div>", render_content("content_tag :div", "Hello world") } + assert_equal "<div>Hello world</div>", render_content("content_tag :div", "Hello world") end test "percent equals works for javascript_tag" do expected_output = "<script type=\"text/javascript\">\n//<![CDATA[\nalert('Hello')\n//]]>\n</script>" - maybe_deprecated { assert_equal expected_output, render_content("javascript_tag", "alert('Hello')") } + assert_equal expected_output, render_content("javascript_tag", "alert('Hello')") end test "percent equals works for javascript_tag with options" do expected_output = "<script id=\"the_js_tag\" type=\"text/javascript\">\n//<![CDATA[\nalert('Hello')\n//]]>\n</script>" - maybe_deprecated { assert_equal expected_output, render_content("javascript_tag(:id => 'the_js_tag')", "alert('Hello')") } + assert_equal expected_output, render_content("javascript_tag(:id => 'the_js_tag')", "alert('Hello')") end test "percent equals works with form tags" do expected_output = %r{<form.*action="foo".*method="post">.*hello*</form>} - maybe_deprecated { assert_match expected_output, render_content("form_tag('foo')", "<%= 'hello' %>") } + assert_match expected_output, render_content("form_tag('foo')", "<%= 'hello' %>") end test "percent equals works with fieldset tags" do expected_output = "<fieldset><legend>foo</legend>hello</fieldset>" - maybe_deprecated { assert_equal expected_output, render_content("field_set_tag('foo')", "<%= 'hello' %>") } - end - end - - class TagHelperTest < BlockTestCase - def block_helper(str, rest) - "<%= #{str} do %>#{rest}<% end %>" + assert_equal expected_output, render_content("field_set_tag('foo')", "<%= 'hello' %>") end - - include SharedTagHelpers - end - - class DeprecatedTagHelperTest < BlockTestCase - def block_helper(str, rest) - "<% __in_erb_template=true %><% #{str} do %>#{rest}<% end %>" - end - - def setup - @deprecated = true - end - - include SharedTagHelpers end end
\ No newline at end of file diff --git a/actionpack/test/template/erb_util_test.rb b/actionpack/test/template/erb_util_test.rb index d3129d0e1a..d1891094e8 100644 --- a/actionpack/test/template/erb_util_test.rb +++ b/actionpack/test/template/erb_util_test.rb @@ -14,7 +14,7 @@ class ErbUtilTest < Test::Unit::TestCase end end end - + def test_html_escape_is_html_safe escaped = h("<p>") assert_equal "<p>", escaped diff --git a/actionpack/test/template/form_helper_test.rb b/actionpack/test/template/form_helper_test.rb index be66710ae5..359b078466 100644 --- a/actionpack/test/template/form_helper_test.rb +++ b/actionpack/test/template/form_helper_test.rb @@ -4,16 +4,6 @@ require 'controller/fake_models' class FormHelperTest < ActionView::TestCase tests ActionView::Helpers::FormHelper - class Developer - def name_before_type_cast - "David" - end - - def name - "Santiago" - end - end - def form_for(*) @output_buffer = super end @@ -73,15 +63,38 @@ class FormHelperTest < ActionView::TestCase @post.body = "Back to the hill and over it again!" @post.secret = 1 @post.written_on = Date.new(2004, 6, 15) + + @blog_post = Blog::Post.new("And his name will be forty and four.", 44) + end + + Routes = ActionDispatch::Routing::RouteSet.new + Routes.draw do + resources :posts do + resources :comments + end + + namespace :admin do + resources :posts do + resources :comments + end + end + + match "/foo", :to => "controller#action" + root :to => "main#index" end + def _routes + Routes + end + + include Routes.url_helpers + def url_for(object) @url_for_options = object - if object.is_a?(Hash) - "http://www.example.com" - else - super + if object.is_a?(Hash) && object[:use_route].blank? && object[:controller].blank? + object.merge!(:controller => "main", :action => "index") end + super end def test_label @@ -165,7 +178,10 @@ class FormHelperTest < ActionView::TestCase '<input id="post_title" name="post[title]" size="30" type="text" value="Hello World" />', text_field("post", "title") ) assert_dom_equal( - '<input id="post_title" name="post[title]" size="30" type="password" value="Hello World" />', password_field("post", "title") + '<input id="post_title" name="post[title]" size="30" type="password" />', password_field("post", "title") + ) + assert_dom_equal( + '<input id="post_title" name="post[title]" size="30" type="password" value="Hello World" />', password_field("post", "title", :value => @post.title) ) assert_dom_equal( '<input id="person_name" name="person[name]" size="30" type="password" />', password_field("person", "name") @@ -240,7 +256,7 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, hidden_field("post", "title", :value => nil) end - def test_text_field_with_options + def test_hidden_field_with_options assert_dom_equal '<input id="post_title" name="post[title]" type="hidden" value="Something Else" />', hidden_field("post", "title", :value => "Something Else") end @@ -250,13 +266,6 @@ class FormHelperTest < ActionView::TestCase text_field("user", "email", :type => "email") end - def test_text_field_from_a_user_defined_method - @developer = Developer.new - assert_dom_equal( - '<input id="developer_name" name="developer[name]" size="30" type="text" value="Santiago" />', text_field("developer", "name") - ) - end - def test_check_box assert_dom_equal( '<input name="post[secret]" type="hidden" value="0" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="1" />', @@ -600,31 +609,91 @@ class FormHelperTest < ActionView::TestCase ) end + def test_form_for_requires_block + assert_raises(ArgumentError) do + form_for(:post, @post, :html => { :id => 'create-post' }) + end + end + def test_form_for - assert_deprecated do - form_for(:post, @post, :html => { :id => 'create-post' }) do |f| - concat f.label(:title) { "The Title" } - concat f.text_field(:title) - concat f.text_area(:body) - concat f.check_box(:secret) - concat f.submit('Create post') - end + form_for(@post, :html => { :id => 'create-post' }) do |f| + concat f.label(:title) { "The Title" } + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + concat f.submit('Create post') end - expected = - "<form accept-charset='UTF-8' action='http://www.example.com' id='create-post' method='post'>" + - snowman + + expected = whole_form("/posts/123", "create-post" , "edit_post", :method => "put") do "<label for='post_title'>The Title</label>" + "<input name='post[title]' size='30' type='text' id='post_title' value='Hello World' />" + "<textarea name='post[body]' id='post_body' rows='20' cols='40'>Back to the hill and over it again!</textarea>" + "<input name='post[secret]' type='hidden' value='0' />" + "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />" + - "<input name='commit' id='post_submit' type='submit' value='Create post' />" + - "</form>" + "<input name='commit' type='submit' value='Create post' />" + end assert_dom_equal expected, output_buffer end + def test_form_for_with_file_field_generate_multipart + Post.send :attr_accessor, :file + + form_for(@post, :html => { :id => 'create-post' }) do |f| + concat f.file_field(:file) + end + + expected = whole_form("/posts/123", "create-post" , "edit_post", :method => "put", :multipart => true) do + "<input name='post[file]' type='file' id='post_file' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_fields_for_with_file_field_generate_multipart + Comment.send :attr_accessor, :file + + form_for(@post) do |f| + concat f.fields_for(:comment, @post) { |c| + concat c.file_field(:file) + } + end + + expected = whole_form("/posts/123", "edit_post_123" , "edit_post", :method => "put", :multipart => true) do + "<input name='post[comment][file]' type='file' id='post_comment_file' />" + end + + assert_dom_equal expected, output_buffer + end + + + def test_form_for_with_format + form_for(@post, :format => :json, :html => { :id => "edit_post_123", :class => "edit_post" }) do |f| + concat f.label(:title) + end + + expected = whole_form("/posts/123.json", "edit_post_123" , "edit_post", :method => "put") do + "<label for='post_title'>Title</label>" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_for_with_isolated_namespaced_model + form_for(@blog_post) do |f| + concat f.text_field :title + concat f.submit('Edit post') + end + + expected = + "<form accept-charset='UTF-8' action='/posts/44' method='post'>" + + snowman + + "<label for='post_title'>The Title</label>" + + "<input name='post[title]' size='30' type='text' id='post_title' value='And his name will be forty and four.' />" + + "<input name='commit' id='post_submit' type='submit' value='Edit post' />" + + "</form>" + end + def test_form_for_with_symbol_object_name form_for(@post, :as => "other_name", :html => { :id => 'create-post' }) do |f| concat f.label(:title, :class => 'post_title') @@ -640,22 +709,20 @@ class FormHelperTest < ActionView::TestCase "<textarea name='other_name[body]' id='other_name_body' rows='20' cols='40'>Back to the hill and over it again!</textarea>" + "<input name='other_name[secret]' value='0' type='hidden' />" + "<input name='other_name[secret]' checked='checked' id='other_name_secret' value='1' type='checkbox' />" + - "<input name='commit' id='other_name_submit' value='Create post' type='submit' />" + "<input name='commit' value='Create post' type='submit' />" end assert_dom_equal expected, output_buffer end def test_form_for_with_method - assert_deprecated do - form_for(:post, @post, :html => { :id => 'create-post', :method => :put }) do |f| - concat f.text_field(:title) - concat f.text_area(:body) - concat f.check_box(:secret) - end + form_for(@post, :url => '/', :html => { :id => 'create-post', :method => :put }) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) end - expected = whole_form("http://www.example.com", "create-post", nil, "put") do + expected = whole_form("/", "create-post", "edit_post", "put") do "<input name='post[title]' size='30' type='text' id='post_title' value='Hello World' />" + "<textarea name='post[body]' id='post_body' rows='20' cols='40'>Back to the hill and over it again!</textarea>" + "<input name='post[secret]' type='hidden' value='0' />" + @@ -665,16 +732,28 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end + def test_form_for_with_search_field + # Test case for bug which would emit an "object" attribute + # when used with form_for using a search_field form helper + form_for(Post.new, :url => "/search", :html => { :id => 'search-post', :method => :get}) do |f| + concat f.search_field(:title) + end + + expected = whole_form("/search", "search-post", "new_post", "get") do + "<input name='post[title]' size='30' type='search' id='post_title' />" + end + + assert_dom_equal expected, output_buffer + end + def test_form_for_with_remote - assert_deprecated do - form_for(:post, @post, :remote => true, :html => { :id => 'create-post', :method => :put }) do |f| - concat f.text_field(:title) - concat f.text_area(:body) - concat f.check_box(:secret) - end + form_for(@post, :url => '/', :remote => true, :html => { :id => 'create-post', :method => :put }) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) end - expected = whole_form("http://www.example.com", "create-post", nil, :method => "put", :remote => true) do + expected = whole_form("/", "create-post", "edit_post", :method => "put", :remote => true) do "<input name='post[title]' size='30' type='text' id='post_title' value='Hello World' />" + "<textarea name='post[body]' id='post_body' rows='20' cols='40'>Back to the hill and over it again!</textarea>" + "<input name='post[secret]' type='hidden' value='0' />" + @@ -685,15 +764,14 @@ class FormHelperTest < ActionView::TestCase end def test_form_for_with_remote_without_html - assert_deprecated do - form_for(:post, @post, :remote => true) do |f| - concat f.text_field(:title) - concat f.text_area(:body) - concat f.check_box(:secret) - end + @post.persisted = false + form_for(@post, :remote => true) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) end - expected = whole_form("http://www.example.com", nil, nil, :remote => true) do + expected = whole_form("/posts", 'new_post', 'new_post', :remote => true) do "<input name='post[title]' size='30' type='text' id='post_title' value='Hello World' />" + "<textarea name='post[body]' id='post_body' rows='20' cols='40'>Back to the hill and over it again!</textarea>" + "<input name='post[secret]' type='hidden' value='0' />" + @@ -710,7 +788,7 @@ class FormHelperTest < ActionView::TestCase concat f.check_box(:secret) end - expected = whole_form("http://www.example.com", "create-post") do + expected = whole_form("/", "create-post") do "<input name='post[title]' size='30' type='text' id='post_title' value='Hello World' />" + "<textarea name='post[body]' id='post_body' rows='20' cols='40'>Back to the hill and over it again!</textarea>" + "<input name='post[secret]' type='hidden' value='0' />" + @@ -721,16 +799,14 @@ class FormHelperTest < ActionView::TestCase end def test_form_for_with_index - assert_deprecated do - form_for("post[]", @post) do |f| - concat f.label(:title) - concat f.text_field(:title) - concat f.text_area(:body) - concat f.check_box(:secret) - end + form_for(@post, :as => "post[]") do |f| + concat f.label(:title) + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) end - expected = whole_form do + expected = whole_form('/posts/123', 'post[]_edit', 'post[]_edit', 'put') do "<label for='post_123_title'>Title</label>" + "<input name='post[123][title]' size='30' type='text' id='post_123_title' value='Hello World' />" + "<textarea name='post[123][body]' id='post_123_body' rows='20' cols='40'>Back to the hill and over it again!</textarea>" + @@ -742,15 +818,13 @@ class FormHelperTest < ActionView::TestCase end def test_form_for_with_nil_index_option_override - assert_deprecated do - form_for("post[]", @post, :index => nil) do |f| - concat f.text_field(:title) - concat f.text_area(:body) - concat f.check_box(:secret) - end + form_for(@post, :as => "post[]", :index => nil) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) end - expected = whole_form do + expected = whole_form('/posts/123', 'post[]_edit', 'post[]_edit', 'put') do "<input name='post[][title]' size='30' type='text' id='post__title' value='Hello World' />" + "<textarea name='post[][body]' id='post__body' rows='20' cols='40'>Back to the hill and over it again!</textarea>" + "<input name='post[][secret]' type='hidden' value='0' />" + @@ -764,15 +838,13 @@ class FormHelperTest < ActionView::TestCase old_locale, I18n.locale = I18n.locale, :submit @post.persisted = false - assert_deprecated do - form_for(:post, @post) do |f| - concat f.submit - end + form_for(@post) do |f| + concat f.submit end - expected = whole_form do - "<input name='commit' id='post_submit' type='submit' value='Create Post' />" - end + expected = whole_form('/posts', 'new_post', 'new_post') do + "<input name='commit' type='submit' value='Create Post' />" + end assert_dom_equal expected, output_buffer ensure @@ -782,15 +854,13 @@ class FormHelperTest < ActionView::TestCase def test_submit_with_object_as_existing_record_and_locale_strings old_locale, I18n.locale = I18n.locale, :submit - assert_deprecated do - form_for(:post, @post) do |f| - concat f.submit - end + form_for(@post) do |f| + concat f.submit end - expected = whole_form do - "<input name='commit' id='post_submit' type='submit' value='Confirm Post changes' />" - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + "<input name='commit' type='submit' value='Confirm Post changes' />" + end assert_dom_equal expected, output_buffer ensure @@ -804,9 +874,9 @@ class FormHelperTest < ActionView::TestCase concat f.submit :class => "extra" end - expected = whole_form do - "<input name='commit' class='extra' id='post_submit' type='submit' value='Save changes' />" - end + expected = whole_form do + "<input name='commit' class='extra' type='submit' value='Save changes' />" + end assert_dom_equal expected, output_buffer ensure @@ -816,15 +886,13 @@ class FormHelperTest < ActionView::TestCase def test_submit_with_object_and_nested_lookup old_locale, I18n.locale = I18n.locale, :submit - assert_deprecated do - form_for(:another_post, @post) do |f| - concat f.submit - end + form_for(@post, :as => :another_post) do |f| + concat f.submit end - expected = whole_form do - "<input name='commit' id='another_post_submit' type='submit' value='Update your Post' />" - end + expected = whole_form('/posts/123', 'another_post_edit', 'another_post_edit', :method => 'put') do + "<input name='commit' type='submit' value='Update your Post' />" + end assert_dom_equal expected, output_buffer ensure @@ -832,188 +900,167 @@ class FormHelperTest < ActionView::TestCase end def test_nested_fields_for - assert_deprecated do - form_for(:post, @post) do |f| - concat f.fields_for(:comment, @post) { |c| - concat c.text_field(:title) - } - end + @comment.body = 'Hello World' + form_for(@post) do |f| + concat f.fields_for(@comment) { |c| + concat c.text_field(:body) + } end - expected = whole_form do - "<input name='post[comment][title]' size='30' type='text' id='post_comment_title' value='Hello World' />" - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + "<input name='post[comment][body]' size='30' type='text' id='post_comment_body' value='Hello World' />" + end assert_dom_equal expected, output_buffer end def test_nested_fields_for_with_nested_collections - assert_deprecated do - form_for('post[]', @post) do |f| - concat f.text_field(:title) - concat f.fields_for('comment[]', @comment) { |c| - concat c.text_field(:name) - } - end + form_for(@post, :as => 'post[]') do |f| + concat f.text_field(:title) + concat f.fields_for('comment[]', @comment) { |c| + concat c.text_field(:name) + } end - expected = whole_form do - "<input name='post[123][title]' size='30' type='text' id='post_123_title' value='Hello World' />" + - "<input name='post[123][comment][][name]' size='30' type='text' id='post_123_comment__name' value='new comment' />" - end + expected = whole_form('/posts/123', 'post[]_edit', 'post[]_edit', 'put') do + "<input name='post[123][title]' size='30' type='text' id='post_123_title' value='Hello World' />" + + "<input name='post[123][comment][][name]' size='30' type='text' id='post_123_comment__name' value='new comment' />" + end assert_dom_equal expected, output_buffer end def test_nested_fields_for_with_index_and_parent_fields - assert_deprecated do - form_for('post', @post, :index => 1) do |c| - concat c.text_field(:title) - concat c.fields_for('comment', @comment, :index => 1) { |r| - concat r.text_field(:name) - } - end + form_for(@post, :index => 1) do |c| + concat c.text_field(:title) + concat c.fields_for('comment', @comment, :index => 1) { |r| + concat r.text_field(:name) + } end - expected = whole_form do - "<input name='post[1][title]' size='30' type='text' id='post_1_title' value='Hello World' />" + - "<input name='post[1][comment][1][name]' size='30' type='text' id='post_1_comment_1_name' value='new comment' />" - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', 'put') do + "<input name='post[1][title]' size='30' type='text' id='post_1_title' value='Hello World' />" + + "<input name='post[1][comment][1][name]' size='30' type='text' id='post_1_comment_1_name' value='new comment' />" + end assert_dom_equal expected, output_buffer end def test_form_for_with_index_and_nested_fields_for - assert_deprecated do - output_buffer = form_for(:post, @post, :index => 1) do |f| - concat f.fields_for(:comment, @post) { |c| - concat c.text_field(:title) - } - end + output_buffer = form_for(@post, :index => 1) do |f| + concat f.fields_for(:comment, @post) { |c| + concat c.text_field(:title) + } end - expected = whole_form do - "<input name='post[1][comment][title]' size='30' type='text' id='post_1_comment_title' value='Hello World' />" - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', 'put') do + "<input name='post[1][comment][title]' size='30' type='text' id='post_1_comment_title' value='Hello World' />" + end assert_dom_equal expected, output_buffer end def test_nested_fields_for_with_index_on_both - assert_deprecated do - form_for(:post, @post, :index => 1) do |f| - concat f.fields_for(:comment, @post, :index => 5) { |c| - concat c.text_field(:title) - } - end + form_for(@post, :index => 1) do |f| + concat f.fields_for(:comment, @post, :index => 5) { |c| + concat c.text_field(:title) + } end - expected = whole_form do - "<input name='post[1][comment][5][title]' size='30' type='text' id='post_1_comment_5_title' value='Hello World' />" - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', 'put') do + "<input name='post[1][comment][5][title]' size='30' type='text' id='post_1_comment_5_title' value='Hello World' />" + end assert_dom_equal expected, output_buffer end def test_nested_fields_for_with_auto_index - assert_deprecated do - form_for("post[]", @post) do |f| - concat f.fields_for(:comment, @post) { |c| - concat c.text_field(:title) - } - end + form_for(@post, :as => "post[]") do |f| + concat f.fields_for(:comment, @post) { |c| + concat c.text_field(:title) + } end - expected = whole_form do - "<input name='post[123][comment][title]' size='30' type='text' id='post_123_comment_title' value='Hello World' />" - end + expected = whole_form('/posts/123', 'post[]_edit', 'post[]_edit', 'put') do + "<input name='post[123][comment][title]' size='30' type='text' id='post_123_comment_title' value='Hello World' />" + end assert_dom_equal expected, output_buffer end def test_nested_fields_for_with_index_radio_button - assert_deprecated do - form_for(:post, @post) do |f| - concat f.fields_for(:comment, @post, :index => 5) { |c| - concat c.radio_button(:title, "hello") - } - end + form_for(@post) do |f| + concat f.fields_for(:comment, @post, :index => 5) { |c| + concat c.radio_button(:title, "hello") + } end - expected = whole_form do - "<input name='post[comment][5][title]' type='radio' id='post_comment_5_title_hello' value='hello' />" - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', 'put') do + "<input name='post[comment][5][title]' type='radio' id='post_comment_5_title_hello' value='hello' />" + end assert_dom_equal expected, output_buffer end def test_nested_fields_for_with_auto_index_on_both - assert_deprecated do - form_for("post[]", @post) do |f| - concat f.fields_for("comment[]", @post) { |c| - concat c.text_field(:title) - } - end + form_for(@post, :as => "post[]") do |f| + concat f.fields_for("comment[]", @post) { |c| + concat c.text_field(:title) + } end - expected = whole_form do - "<input name='post[123][comment][123][title]' size='30' type='text' id='post_123_comment_123_title' value='Hello World' />" - end + expected = whole_form('/posts/123', 'post[]_edit', 'post[]_edit', 'put') do + "<input name='post[123][comment][123][title]' size='30' type='text' id='post_123_comment_123_title' value='Hello World' />" + end assert_dom_equal expected, output_buffer end def test_nested_fields_for_with_index_and_auto_index - assert_deprecated do - output_buffer = form_for("post[]", @post) do |f| - concat f.fields_for(:comment, @post, :index => 5) { |c| - concat c.text_field(:title) - } - end - - output_buffer << form_for(:post, @post, :index => 1) do |f| - concat f.fields_for("comment[]", @post) { |c| - concat c.text_field(:title) - } - end + output_buffer = form_for(@post, :as => "post[]") do |f| + concat f.fields_for(:comment, @post, :index => 5) { |c| + concat c.text_field(:title) + } + end - expected = whole_form do - "<input name='post[123][comment][5][title]' size='30' type='text' id='post_123_comment_5_title' value='Hello World' />" - end + whole_form do - "<input name='post[1][comment][123][title]' size='30' type='text' id='post_1_comment_123_title' value='Hello World' />" - end + output_buffer << form_for(@post, :as => :post, :index => 1) do |f| + concat f.fields_for("comment[]", @post) { |c| + concat c.text_field(:title) + } + end - assert_dom_equal expected, output_buffer + expected = whole_form('/posts/123', 'post[]_edit', 'post[]_edit', 'put') do + "<input name='post[123][comment][5][title]' size='30' type='text' id='post_123_comment_5_title' value='Hello World' />" + end + whole_form('/posts/123', 'post_edit', 'post_edit', 'put') do + "<input name='post[1][comment][123][title]' size='30' type='text' id='post_1_comment_123_title' value='Hello World' />" end + + assert_dom_equal expected, output_buffer end def test_nested_fields_for_with_a_new_record_on_a_nested_attributes_one_to_one_association @post.author = Author.new - assert_deprecated do - form_for(:post, @post) do |f| - concat f.text_field(:title) - concat f.fields_for(:author) { |af| - concat af.text_field(:name) - } - end + form_for(@post) do |f| + concat f.text_field(:title) + concat f.fields_for(:author) { |af| + concat af.text_field(:name) + } end - expected = whole_form do - '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + - '<input id="post_author_attributes_name" name="post[author_attributes][name]" size="30" type="text" value="new author" />' - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + + '<input id="post_author_attributes_name" name="post[author_attributes][name]" size="30" type="text" value="new author" />' + end assert_dom_equal expected, output_buffer end def test_nested_fields_for_with_explicitly_passed_object_on_a_nested_attributes_one_to_one_association - assert_deprecated do - form_for(:post, @post) do |f| - f.fields_for(:author, Author.new(123)) do |af| - assert_not_nil af.object - assert_equal 123, af.object.id - end + form_for(@post) do |f| + f.fields_for(:author, Author.new(123)) do |af| + assert_not_nil af.object + assert_equal 123, af.object.id end end end @@ -1021,67 +1068,238 @@ class FormHelperTest < ActionView::TestCase def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association @post.author = Author.new(321) - assert_deprecated do - form_for(:post, @post) do |f| - concat f.text_field(:title) - concat f.fields_for(:author) { |af| - concat af.text_field(:name) + form_for(@post) do |f| + concat f.text_field(:title) + concat f.fields_for(:author) { |af| + concat af.text_field(:name) + } + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + + '<input id="post_author_attributes_name" name="post[author_attributes][name]" size="30" type="text" value="author #321" />' + + '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association_using_erb_and_inline_block + @post.author = Author.new(321) + + form_for(@post) do |f| + concat f.text_field(:title) + concat f.fields_for(:author) { |af| + af.text_field(:name) + } + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + + '<input id="post_author_attributes_name" name="post[author_attributes][name]" size="30" type="text" value="author #321" />' + + '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id + @post.author = Author.new(321) + + form_for(@post) do |f| + concat f.text_field(:title) + concat f.fields_for(:author, :include_id => false) { |af| + af.text_field(:name) + } + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + + '<input id="post_author_attributes_name" name="post[author_attributes][name]" size="30" type="text" value="author #321" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id_inherited + @post.author = Author.new(321) + + form_for(@post, :include_id => false) do |f| + concat f.text_field(:title) + concat f.fields_for(:author) { |af| + af.text_field(:name) + } + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + + '<input id="post_author_attributes_name" name="post[author_attributes][name]" size="30" type="text" value="author #321" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id_override + @post.author = Author.new(321) + + form_for(@post, :include_id => false) do |f| + concat f.text_field(:title) + concat f.fields_for(:author, :include_id => true) { |af| + af.text_field(:name) + } + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + + '<input id="post_author_attributes_name" name="post[author_attributes][name]" size="30" type="text" value="author #321" />' + + '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_existing_records_on_a_nested_attributes_one_to_one_association_with_explicit_hidden_field_placement + @post.author = Author.new(321) + + form_for(@post) do |f| + concat f.text_field(:title) + concat f.fields_for(:author) { |af| + concat af.hidden_field(:id) + concat af.text_field(:name) + } + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + + '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' + + '<input id="post_author_attributes_name" name="post[author_attributes][name]" size="30" type="text" value="author #321" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association + @post.comments = Array.new(2) { |id| Comment.new(id + 1) } + + form_for(@post) do |f| + concat f.text_field(:title) + @post.comments.each do |comment| + concat f.fields_for(:comments, comment) { |cf| + concat cf.text_field(:name) } end end - expected = whole_form do - '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + - '<input id="post_author_attributes_name" name="post[author_attributes][name]" size="30" type="text" value="author #321" />' + - '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #1" />' + + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="comment #2" />' + + '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' + end assert_dom_equal expected, output_buffer end - def test_nested_fields_for_with_existing_records_on_a_nested_attributes_one_to_one_association_with_explicit_hidden_field_placement + def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id + @post.comments = Array.new(2) { |id| Comment.new(id + 1) } @post.author = Author.new(321) - assert_deprecated do - form_for(:post, @post) do |f| - concat f.text_field(:title) - concat f.fields_for(:author) { |af| - concat af.hidden_field(:id) - concat af.text_field(:name) + form_for(@post) do |f| + concat f.text_field(:title) + concat f.fields_for(:author) { |af| + concat af.text_field(:name) + } + @post.comments.each do |comment| + concat f.fields_for(:comments, comment, :include_id => false) { |cf| + concat cf.text_field(:name) } end end - expected = whole_form do - '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + - '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' + - '<input id="post_author_attributes_name" name="post[author_attributes][name]" size="30" type="text" value="author #321" />' - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + + '<input id="post_author_attributes_name" name="post[author_attributes][name]" size="30" type="text" value="author #321" />' + + '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' + + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #1" />' + + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="comment #2" />' + end assert_dom_equal expected, output_buffer end - def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association + def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id_inherited + @post.comments = Array.new(2) { |id| Comment.new(id + 1) } + @post.author = Author.new(321) + + form_for(@post, :include_id => false) do |f| + concat f.text_field(:title) + concat f.fields_for(:author) { |af| + concat af.text_field(:name) + } + @post.comments.each do |comment| + concat f.fields_for(:comments, comment) { |cf| + concat cf.text_field(:name) + } + end + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + + '<input id="post_author_attributes_name" name="post[author_attributes][name]" size="30" type="text" value="author #321" />' + + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #1" />' + + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="comment #2" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id_override @post.comments = Array.new(2) { |id| Comment.new(id + 1) } + @post.author = Author.new(321) - assert_deprecated do - form_for(:post, @post) do |f| - concat f.text_field(:title) - @post.comments.each do |comment| - concat f.fields_for(:comments, comment) { |cf| - concat cf.text_field(:name) - } - end + form_for(@post, :include_id => false) do |f| + concat f.text_field(:title) + concat f.fields_for(:author, :include_id => true) { |af| + concat af.text_field(:name) + } + @post.comments.each do |comment| + concat f.fields_for(:comments, comment) { |cf| + concat cf.text_field(:name) + } end end - expected = whole_form do - '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #1" />' + - '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + - '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="comment #2" />' + - '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + + '<input id="post_author_attributes_name" name="post[author_attributes][name]" size="30" type="text" value="author #321" />' + + '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' + + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #1" />' + + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="comment #2" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_using_erb_and_inline_block + @post.comments = Array.new(2) { |id| Comment.new(id + 1) } + + form_for(@post) do |f| + concat f.text_field(:title) + @post.comments.each do |comment| + concat f.fields_for(:comments, comment) { |cf| + cf.text_field(:name) + } + end + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #1" />' + + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="comment #2" />' + + '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' + end assert_dom_equal expected, output_buffer end @@ -1089,25 +1307,23 @@ class FormHelperTest < ActionView::TestCase def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_with_explicit_hidden_field_placement @post.comments = Array.new(2) { |id| Comment.new(id + 1) } - assert_deprecated do - form_for(:post, @post) do |f| - concat f.text_field(:title) - @post.comments.each do |comment| - concat f.fields_for(:comments, comment) { |cf| - concat cf.hidden_field(:id) - concat cf.text_field(:name) - } - end + form_for(@post) do |f| + concat f.text_field(:title) + @post.comments.each do |comment| + concat f.fields_for(:comments, comment) { |cf| + concat cf.hidden_field(:id) + concat cf.text_field(:name) + } end end - expected = whole_form do - '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + - '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #1" />' + - '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' + - '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="comment #2" />' - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #1" />' + + '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' + + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="comment #2" />' + end assert_dom_equal expected, output_buffer end @@ -1115,22 +1331,20 @@ class FormHelperTest < ActionView::TestCase def test_nested_fields_for_with_new_records_on_a_nested_attributes_collection_association @post.comments = [Comment.new, Comment.new] - assert_deprecated do - form_for(:post, @post) do |f| - concat f.text_field(:title) - @post.comments.each do |comment| - concat f.fields_for(:comments, comment) { |cf| - concat cf.text_field(:name) - } - end + form_for(@post) do |f| + concat f.text_field(:title) + @post.comments.each do |comment| + concat f.fields_for(:comments, comment) { |cf| + concat cf.text_field(:name) + } end end - expected = whole_form do - '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="new comment" />' + - '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="new comment" />' - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="new comment" />' + + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="new comment" />' + end assert_dom_equal expected, output_buffer end @@ -1138,40 +1352,36 @@ class FormHelperTest < ActionView::TestCase def test_nested_fields_for_with_existing_and_new_records_on_a_nested_attributes_collection_association @post.comments = [Comment.new(321), Comment.new] - assert_deprecated do - form_for(:post, @post) do |f| - concat f.text_field(:title) - @post.comments.each do |comment| - concat f.fields_for(:comments, comment) { |cf| - concat cf.text_field(:name) - } - end + form_for(@post) do |f| + concat f.text_field(:title) + @post.comments.each do |comment| + concat f.fields_for(:comments, comment) { |cf| + concat cf.text_field(:name) + } end end - expected = whole_form do - '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #321" />' + - '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' + - '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="new comment" />' - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #321" />' + + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' + + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="new comment" />' + end assert_dom_equal expected, output_buffer end def test_nested_fields_for_with_an_empty_supplied_attributes_collection - assert_deprecated do - form_for(:post, @post) do |f| - concat f.text_field(:title) - f.fields_for(:comments, []) do |cf| - concat cf.text_field(:name) - end + form_for(@post) do |f| + concat f.text_field(:title) + f.fields_for(:comments, []) do |cf| + concat cf.text_field(:name) end end - expected = whole_form do - '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + end assert_dom_equal expected, output_buffer end @@ -1179,22 +1389,41 @@ class FormHelperTest < ActionView::TestCase def test_nested_fields_for_with_existing_records_on_a_supplied_nested_attributes_collection @post.comments = Array.new(2) { |id| Comment.new(id + 1) } - assert_deprecated do - form_for(:post, @post) do |f| - concat f.text_field(:title) - concat f.fields_for(:comments, @post.comments) { |cf| - concat cf.text_field(:name) - } - end + form_for(@post) do |f| + concat f.text_field(:title) + concat f.fields_for(:comments, @post.comments) { |cf| + concat cf.text_field(:name) + } end - expected = whole_form do - '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #1" />' + - '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + - '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="comment #2" />' + - '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #1" />' + + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="comment #2" />' + + '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_arel_like + @post.comments = ArelLike.new + + form_for(@post) do |f| + concat f.text_field(:title) + concat f.fields_for(:comments, @post.comments) { |cf| + concat cf.text_field(:name) + } + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #1" />' + + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="comment #2" />' + + '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' + end assert_dom_equal expected, output_buffer end @@ -1203,22 +1432,20 @@ class FormHelperTest < ActionView::TestCase comments = Array.new(2) { |id| Comment.new(id + 1) } @post.comments = [] - assert_deprecated do - form_for(:post, @post) do |f| - concat f.text_field(:title) - concat f.fields_for(:comments, comments) { |cf| - concat cf.text_field(:name) - } - end + form_for(@post) do |f| + concat f.text_field(:title) + concat f.fields_for(:comments, comments) { |cf| + concat cf.text_field(:name) + } end - expected = whole_form do - '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #1" />' + - '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + - '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="comment #2" />' + - '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #1" />' + + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="comment #2" />' + + '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' + end assert_dom_equal expected, output_buffer end @@ -1227,22 +1454,20 @@ class FormHelperTest < ActionView::TestCase @post.comments = [Comment.new(321), Comment.new] yielded_comments = [] - assert_deprecated do - form_for(:post, @post) do |f| - concat f.text_field(:title) - concat f.fields_for(:comments) { |cf| - concat cf.text_field(:name) - yielded_comments << cf.object - } - end + form_for(@post) do |f| + concat f.text_field(:title) + concat f.fields_for(:comments) { |cf| + concat cf.text_field(:name) + yielded_comments << cf.object + } end - expected = whole_form do - '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #321" />' + - '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' + - '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="new comment" />' - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' + + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #321" />' + + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' + + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="new comment" />' + end assert_dom_equal expected, output_buffer assert_equal yielded_comments, @post.comments @@ -1251,18 +1476,16 @@ class FormHelperTest < ActionView::TestCase def test_nested_fields_for_with_child_index_option_override_on_a_nested_attributes_collection_association @post.comments = [] - assert_deprecated do - form_for(:post, @post) do |f| - concat f.fields_for(:comments, Comment.new(321), :child_index => 'abc') { |cf| - concat cf.text_field(:name) - } - end + form_for(@post) do |f| + concat f.fields_for(:comments, Comment.new(321), :child_index => 'abc') { |cf| + concat cf.text_field(:name) + } end - expected = whole_form do - '<input id="post_comments_attributes_abc_name" name="post[comments_attributes][abc][name]" size="30" type="text" value="comment #321" />' + - '<input id="post_comments_attributes_abc_id" name="post[comments_attributes][abc][id]" type="hidden" value="321" />' - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '<input id="post_comments_attributes_abc_name" name="post[comments_attributes][abc][name]" size="30" type="text" value="comment #321" />' + + '<input id="post_comments_attributes_abc_id" name="post[comments_attributes][abc][id]" type="hidden" value="321" />' + end assert_dom_equal expected, output_buffer end @@ -1274,43 +1497,41 @@ class FormHelperTest < ActionView::TestCase @post.tags[0].relevances = [] @post.tags[1].relevances = [] - assert_deprecated do - form_for(:post, @post) do |f| - concat f.fields_for(:comments, @post.comments[0]) { |cf| - concat cf.text_field(:name) - concat cf.fields_for(:relevances, CommentRelevance.new(314)) { |crf| - concat crf.text_field(:value) - } + form_for(@post) do |f| + concat f.fields_for(:comments, @post.comments[0]) { |cf| + concat cf.text_field(:name) + concat cf.fields_for(:relevances, CommentRelevance.new(314)) { |crf| + concat crf.text_field(:value) } - concat f.fields_for(:tags, @post.tags[0]) { |tf| - concat tf.text_field(:value) - concat tf.fields_for(:relevances, TagRelevance.new(3141)) { |trf| - concat trf.text_field(:value) - } + } + concat f.fields_for(:tags, @post.tags[0]) { |tf| + concat tf.text_field(:value) + concat tf.fields_for(:relevances, TagRelevance.new(3141)) { |trf| + concat trf.text_field(:value) } - concat f.fields_for('tags', @post.tags[1]) { |tf| - concat tf.text_field(:value) - concat tf.fields_for(:relevances, TagRelevance.new(31415)) { |trf| - concat trf.text_field(:value) - } + } + concat f.fields_for('tags', @post.tags[1]) { |tf| + concat tf.text_field(:value) + concat tf.fields_for(:relevances, TagRelevance.new(31415)) { |trf| + concat trf.text_field(:value) } - end + } end - expected = whole_form do - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #321" />' + - '<input id="post_comments_attributes_0_relevances_attributes_0_value" name="post[comments_attributes][0][relevances_attributes][0][value]" size="30" type="text" value="commentrelevance #314" />' + - '<input id="post_comments_attributes_0_relevances_attributes_0_id" name="post[comments_attributes][0][relevances_attributes][0][id]" type="hidden" value="314" />' + - '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' + - '<input id="post_tags_attributes_0_value" name="post[tags_attributes][0][value]" size="30" type="text" value="tag #123" />' + - '<input id="post_tags_attributes_0_relevances_attributes_0_value" name="post[tags_attributes][0][relevances_attributes][0][value]" size="30" type="text" value="tagrelevance #3141" />' + - '<input id="post_tags_attributes_0_relevances_attributes_0_id" name="post[tags_attributes][0][relevances_attributes][0][id]" type="hidden" value="3141" />' + - '<input id="post_tags_attributes_0_id" name="post[tags_attributes][0][id]" type="hidden" value="123" />' + - '<input id="post_tags_attributes_1_value" name="post[tags_attributes][1][value]" size="30" type="text" value="tag #456" />' + - '<input id="post_tags_attributes_1_relevances_attributes_0_value" name="post[tags_attributes][1][relevances_attributes][0][value]" size="30" type="text" value="tagrelevance #31415" />' + - '<input id="post_tags_attributes_1_relevances_attributes_0_id" name="post[tags_attributes][1][relevances_attributes][0][id]" type="hidden" value="31415" />' + - '<input id="post_tags_attributes_1_id" name="post[tags_attributes][1][id]" type="hidden" value="456" />' - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #321" />' + + '<input id="post_comments_attributes_0_relevances_attributes_0_value" name="post[comments_attributes][0][relevances_attributes][0][value]" size="30" type="text" value="commentrelevance #314" />' + + '<input id="post_comments_attributes_0_relevances_attributes_0_id" name="post[comments_attributes][0][relevances_attributes][0][id]" type="hidden" value="314" />' + + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' + + '<input id="post_tags_attributes_0_value" name="post[tags_attributes][0][value]" size="30" type="text" value="tag #123" />' + + '<input id="post_tags_attributes_0_relevances_attributes_0_value" name="post[tags_attributes][0][relevances_attributes][0][value]" size="30" type="text" value="tagrelevance #3141" />' + + '<input id="post_tags_attributes_0_relevances_attributes_0_id" name="post[tags_attributes][0][relevances_attributes][0][id]" type="hidden" value="3141" />' + + '<input id="post_tags_attributes_0_id" name="post[tags_attributes][0][id]" type="hidden" value="123" />' + + '<input id="post_tags_attributes_1_value" name="post[tags_attributes][1][value]" size="30" type="text" value="tag #456" />' + + '<input id="post_tags_attributes_1_relevances_attributes_0_value" name="post[tags_attributes][1][relevances_attributes][0][value]" size="30" type="text" value="tagrelevance #31415" />' + + '<input id="post_tags_attributes_1_relevances_attributes_0_id" name="post[tags_attributes][1][relevances_attributes][0][id]" type="hidden" value="31415" />' + + '<input id="post_tags_attributes_1_id" name="post[tags_attributes][1][id]" type="hidden" value="456" />' + end assert_dom_equal expected, output_buffer end @@ -1438,47 +1659,40 @@ class FormHelperTest < ActionView::TestCase end def test_form_for_and_fields_for - assert_deprecated do - form_for(:post, @post, :html => { :id => 'create-post' }) do |post_form| - concat post_form.text_field(:title) - concat post_form.text_area(:body) + form_for(@post, :as => :post, :html => { :id => 'create-post' }) do |post_form| + concat post_form.text_field(:title) + concat post_form.text_area(:body) - concat fields_for(:parent_post, @post) { |parent_fields| - concat parent_fields.check_box(:secret) - } - end + concat fields_for(:parent_post, @post) { |parent_fields| + concat parent_fields.check_box(:secret) + } end - expected = - "<form accept-charset='UTF-8' action='http://www.example.com' id='create-post' method='post'>" + - snowman + + expected = whole_form('/posts/123', 'create-post', 'post_edit', :method => 'put') do "<input name='post[title]' size='30' type='text' id='post_title' value='Hello World' />" + "<textarea name='post[body]' id='post_body' rows='20' cols='40'>Back to the hill and over it again!</textarea>" + "<input name='parent_post[secret]' type='hidden' value='0' />" + - "<input name='parent_post[secret]' checked='checked' type='checkbox' id='parent_post_secret' value='1' />" + - "</form>" + "<input name='parent_post[secret]' checked='checked' type='checkbox' id='parent_post_secret' value='1' />" + end assert_dom_equal expected, output_buffer end def test_form_for_and_fields_for_with_object - assert_deprecated do - form_for(:post, @post, :html => { :id => 'create-post' }) do |post_form| - concat post_form.text_field(:title) - concat post_form.text_area(:body) + form_for(@post, :as => :post, :html => { :id => 'create-post' }) do |post_form| + concat post_form.text_field(:title) + concat post_form.text_area(:body) - concat post_form.fields_for(@comment) { |comment_fields| - concat comment_fields.text_field(:name) - } - end + concat post_form.fields_for(@comment) { |comment_fields| + concat comment_fields.text_field(:name) + } end - expected = - whole_form("http://www.example.com", "create-post") do - "<input name='post[title]' size='30' type='text' id='post_title' value='Hello World' />" + - "<textarea name='post[body]' id='post_body' rows='20' cols='40'>Back to the hill and over it again!</textarea>" + - "<input name='post[comment][name]' type='text' id='post_comment_name' value='new comment' size='30' />" - end + expected = whole_form('/posts/123', 'create-post', 'post_edit', :method => 'put') do + "<input name='post[title]' size='30' type='text' id='post_title' value='Hello World' />" + + "<textarea name='post[body]' id='post_body' rows='20' cols='40'>Back to the hill and over it again!</textarea>" + + "<input name='post[comment][name]' type='text' id='post_comment_name' value='new comment' size='30' />" + end assert_dom_equal expected, output_buffer end @@ -1494,63 +1708,63 @@ class FormHelperTest < ActionView::TestCase end def test_form_for_with_labelled_builder - assert_deprecated do - form_for(:post, @post, :builder => LabelledFormBuilder) do |f| - concat f.text_field(:title) - concat f.text_area(:body) - concat f.check_box(:secret) - end + form_for(@post, :builder => LabelledFormBuilder) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) end - expected = whole_form do - "<label for='title'>Title:</label> <input name='post[title]' size='30' type='text' id='post_title' value='Hello World' /><br/>" + - "<label for='body'>Body:</label> <textarea name='post[body]' id='post_body' rows='20' cols='40'>Back to the hill and over it again!</textarea><br/>" + - "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' /><br/>" - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + "<label for='title'>Title:</label> <input name='post[title]' size='30' type='text' id='post_title' value='Hello World' /><br/>" + + "<label for='body'>Body:</label> <textarea name='post[body]' id='post_body' rows='20' cols='40'>Back to the hill and over it again!</textarea><br/>" + + "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' /><br/>" + end assert_dom_equal expected, output_buffer end def snowman(method = nil) txt = %{<div style="margin:0;padding:0;display:inline">} - txt << %{<input name="_e" type="hidden" value="☃" />} - txt << %{<input name="_method" type="hidden" value="#{method}" />} if method + txt << %{<input name="utf8" type="hidden" value="✓" />} + if (method && !['get','post'].include?(method.to_s)) + txt << %{<input name="_method" type="hidden" value="#{method}" />} + end txt << %{</div>} end - def form_text(action = "http://www.example.com", id = nil, html_class = nil, remote = nil) + def form_text(action = "/", id = nil, html_class = nil, remote = nil, multipart = nil, method = nil) txt = %{<form accept-charset="UTF-8" action="#{action}"} + txt << %{ enctype="multipart/form-data"} if multipart txt << %{ data-remote="true"} if remote txt << %{ class="#{html_class}"} if html_class txt << %{ id="#{id}"} if id - txt << %{ method="post">} + method = method.to_s == "get" ? "get" : "post" + txt << %{ method="#{method}">} end - def whole_form(action = "http://www.example.com", id = nil, html_class = nil, options = nil) + def whole_form(action = "/", id = nil, html_class = nil, options = nil) contents = block_given? ? yield : "" if options.is_a?(Hash) - method, remote = options.values_at(:method, :remote) + method, remote, multipart = options.values_at(:method, :remote, :multipart) else method = options end - form_text(action, id, html_class, remote) + snowman(method) + contents + "</form>" + form_text(action, id, html_class, remote, multipart, method) + snowman(method) + contents + "</form>" end def test_default_form_builder old_default_form_builder, ActionView::Base.default_form_builder = ActionView::Base.default_form_builder, LabelledFormBuilder - assert_deprecated do - form_for(:post, @post) do |f| - concat f.text_field(:title) - concat f.text_area(:body) - concat f.check_box(:secret) - end + form_for(@post) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) end - expected = whole_form do + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do "<label for='title'>Title:</label> <input name='post[title]' size='30' type='text' id='post_title' value='Hello World' /><br/>" + "<label for='body'>Body:</label> <textarea name='post[body]' id='post_body' rows='20' cols='40'>Back to the hill and over it again!</textarea><br/>" + "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' /><br/>" @@ -1579,12 +1793,10 @@ class FormHelperTest < ActionView::TestCase def test_form_for_with_labelled_builder_with_nested_fields_for_without_options_hash klass = nil - assert_deprecated do - form_for(:post, @post, :builder => LabelledFormBuilder) do |f| - f.fields_for(:comments, Comment.new) do |nested_fields| - klass = nested_fields.class - '' - end + form_for(@post, :builder => LabelledFormBuilder) do |f| + f.fields_for(:comments, Comment.new) do |nested_fields| + klass = nested_fields.class + '' end end @@ -1594,12 +1806,10 @@ class FormHelperTest < ActionView::TestCase def test_form_for_with_labelled_builder_with_nested_fields_for_with_options_hash klass = nil - assert_deprecated do - form_for(:post, @post, :builder => LabelledFormBuilder) do |f| - f.fields_for(:comments, Comment.new, :index => 'foo') do |nested_fields| - klass = nested_fields.class - '' - end + form_for(@post, :builder => LabelledFormBuilder) do |f| + f.fields_for(:comments, Comment.new, :index => 'foo') do |nested_fields| + klass = nested_fields.class + '' end end @@ -1611,12 +1821,10 @@ class FormHelperTest < ActionView::TestCase def test_form_for_with_labelled_builder_with_nested_fields_for_with_custom_builder klass = nil - assert_deprecated do - form_for(:post, @post, :builder => LabelledFormBuilder) do |f| - f.fields_for(:comments, Comment.new, :builder => LabelledFormBuilderSubclass) do |nested_fields| - klass = nested_fields.class - '' - end + form_for(@post, :builder => LabelledFormBuilder) do |f| + f.fields_for(:comments, Comment.new, :builder => LabelledFormBuilderSubclass) do |nested_fields| + klass = nested_fields.class + '' end end @@ -1624,39 +1832,29 @@ class FormHelperTest < ActionView::TestCase end def test_form_for_with_html_options_adds_options_to_form_tag - assert_deprecated do - form_for(:post, @post, :html => {:id => 'some_form', :class => 'some_class'}) do |f| end - end - expected = whole_form("http://www.example.com", "some_form", "some_class") + form_for(@post, :html => {:id => 'some_form', :class => 'some_class'}) do |f| end + expected = whole_form("/posts/123", "some_form", "some_class", 'put') assert_dom_equal expected, output_buffer end def test_form_for_with_string_url_option - assert_deprecated do - form_for(:post, @post, :url => 'http://www.otherdomain.com') do |f| end - end + form_for(@post, :url => 'http://www.otherdomain.com') do |f| end - assert_equal whole_form("http://www.otherdomain.com"), output_buffer - # assert_equal '<form action="http://www.otherdomain.com" method="post"></form>', output_buffer + assert_equal whole_form("http://www.otherdomain.com", 'edit_post_123', 'edit_post', 'put'), output_buffer end def test_form_for_with_hash_url_option - assert_deprecated do - form_for(:post, @post, :url => {:controller => 'controller', :action => 'action'}) do |f| end - end + form_for(@post, :url => {:controller => 'controller', :action => 'action'}) do |f| end assert_equal 'controller', @url_for_options[:controller] assert_equal 'action', @url_for_options[:action] end def test_form_for_with_record_url_option - assert_deprecated do - form_for(:post, @post, :url => @post) do |f| end - end + form_for(@post, :url => @post) do |f| end - expected = whole_form("/posts/123") - # expected = "<form action=\"/posts/123\" method=\"post\"></form>" + expected = whole_form("/posts/123", 'edit_post_123', 'edit_post', 'put') assert_equal expected, output_buffer end @@ -1682,14 +1880,14 @@ class FormHelperTest < ActionView::TestCase @comment.save form_for([@post, @comment]) {} - expected = whole_form(comment_path(@post, @comment), "edit_comment_1", "edit_comment", "put") + expected = whole_form(post_comment_path(@post, @comment), "edit_comment_1", "edit_comment", "put") assert_dom_equal expected, output_buffer end def test_form_for_with_new_object_in_list form_for([@post, @comment]) {} - expected = whole_form(comments_path(@post), "new_comment", "new_comment") + expected = whole_form(post_comments_path(@post), "new_comment", "new_comment") assert_dom_equal expected, output_buffer end @@ -1697,14 +1895,14 @@ class FormHelperTest < ActionView::TestCase @comment.save form_for([:admin, @post, @comment]) {} - expected = whole_form(admin_comment_path(@post, @comment), "edit_comment_1", "edit_comment", "put") + expected = whole_form(admin_post_comment_path(@post, @comment), "edit_comment_1", "edit_comment", "put") assert_dom_equal expected, output_buffer end def test_form_for_with_new_object_and_namespace_in_list form_for([:admin, @post, @comment]) {} - expected = whole_form(admin_comments_path(@post), "new_comment", "new_comment") + expected = whole_form(admin_post_comments_path(@post), "new_comment", "new_comment") assert_dom_equal expected, output_buffer end @@ -1721,35 +1919,8 @@ class FormHelperTest < ActionView::TestCase end protected - def comments_path(post) - "/posts/#{post.id}/comments" - end - alias_method :post_comments_path, :comments_path - - def comment_path(post, comment) - "/posts/#{post.id}/comments/#{comment.id}" - end - alias_method :post_comment_path, :comment_path - - def admin_comments_path(post) - "/admin/posts/#{post.id}/comments" - end - alias_method :admin_post_comments_path, :admin_comments_path - - def admin_comment_path(post, comment) - "/admin/posts/#{post.id}/comments/#{comment.id}" - end - alias_method :admin_post_comment_path, :admin_comment_path - - def posts_path - "/posts" - end - - def post_path(post) - "/posts/#{post.id}" - end - def protect_against_forgery? false end + end diff --git a/actionpack/test/template/form_options_helper_test.rb b/actionpack/test/template/form_options_helper_test.rb index d14e5020c7..93ff7ba0fd 100644 --- a/actionpack/test/template/form_options_helper_test.rb +++ b/actionpack/test/template/form_options_helper_test.rb @@ -1,13 +1,19 @@ require 'abstract_unit' require 'tzinfo' +class Map < Hash + def category + "<mus>" + end +end + TZInfo::Timezone.cattr_reader :loaded_zones class FormOptionsHelperTest < ActionView::TestCase tests ActionView::Helpers::FormOptionsHelper silence_warnings do - Post = Struct.new('Post', :title, :author_name, :body, :secret, :written_on, :category, :origin) + Post = Struct.new('Post', :title, :author_name, :body, :secret, :written_on, :category, :origin, :allow_comments) Continent = Struct.new('Continent', :continent_name, :countries) Country = Struct.new('Country', :country_id, :country_name) Firm = Struct.new('Firm', :time_zone) @@ -130,6 +136,13 @@ class FormOptionsHelperTest < ActionView::TestCase ) end + def test_boolean_array_options_for_select_with_selection_and_disabled_value + assert_dom_equal( + "<option value=\"true\">true</option>\n<option value=\"false\" selected=\"selected\">false</option>", + options_for_select([ true, false ], :selected => false, :disabled => nil) + ) + end + def test_array_options_for_string_include_in_other_string_bug_fix assert_dom_equal( "<option value=\"ruby\">ruby</option>\n<option value=\"rubyonrails\" selected=\"selected\">rubyonrails</option>", @@ -176,6 +189,68 @@ class FormOptionsHelperTest < ActionView::TestCase ) end + def test_collection_options_with_preselected_value_as_string_and_option_value_is_integer + albums = [ Album.new(1, "first","rap"), Album.new(2, "second","pop")] + assert_dom_equal( + %(<option selected="selected" value="1">rap</option>\n<option value="2">pop</option>), + options_from_collection_for_select(albums, "id", "genre", :selected => "1") + ) + end + + def test_collection_options_with_preselected_value_as_integer_and_option_value_is_string + albums = [ Album.new("1", "first","rap"), Album.new("2", "second","pop")] + + assert_dom_equal( + %(<option selected="selected" value="1">rap</option>\n<option value="2">pop</option>), + options_from_collection_for_select(albums, "id", "genre", :selected => 1) + ) + end + + def test_collection_options_with_preselected_value_as_string_and_option_value_is_float + albums = [ Album.new(1.0, "first","rap"), Album.new(2.0, "second","pop")] + + assert_dom_equal( + %(<option value="1.0">rap</option>\n<option value="2.0" selected="selected">pop</option>), + options_from_collection_for_select(albums, "id", "genre", :selected => "2.0") + ) + end + + def test_collection_options_with_preselected_value_as_nil + albums = [ Album.new(1.0, "first","rap"), Album.new(2.0, "second","pop")] + + assert_dom_equal( + %(<option value="1.0">rap</option>\n<option value="2.0">pop</option>), + options_from_collection_for_select(albums, "id", "genre", :selected => nil) + ) + end + + def test_collection_options_with_disabled_value_as_nil + albums = [ Album.new(1.0, "first","rap"), Album.new(2.0, "second","pop")] + + assert_dom_equal( + %(<option value="1.0">rap</option>\n<option value="2.0">pop</option>), + options_from_collection_for_select(albums, "id", "genre", :disabled => nil) + ) + end + + def test_collection_options_with_disabled_value_as_array + albums = [ Album.new(1.0, "first","rap"), Album.new(2.0, "second","pop")] + + assert_dom_equal( + %(<option disabled="disabled" value="1.0">rap</option>\n<option disabled="disabled" value="2.0">pop</option>), + options_from_collection_for_select(albums, "id", "genre", :disabled => ["1.0", 2.0]) + ) + end + + def test_collection_options_with_preselected_values_as_string_array_and_option_value_is_float + albums = [ Album.new(1.0, "first","rap"), Album.new(2.0, "second","pop"), Album.new(3.0, "third","country") ] + + assert_dom_equal( + %(<option value="1.0" selected="selected">rap</option>\n<option value="2.0">pop</option>\n<option value="3.0" selected="selected">country</option>), + options_from_collection_for_select(albums, "id", "genre", ["1.0","3.0"]) + ) + end + def test_option_groups_from_collection_for_select assert_dom_equal( "<optgroup label=\"<Africa>\"><option value=\"<sa>\"><South Africa></option>\n<option value=\"so\">Somalia</option></optgroup><optgroup label=\"Europe\"><option value=\"dk\" selected=\"selected\">Denmark</option>\n<option value=\"ie\">Ireland</option></optgroup>", @@ -289,6 +364,10 @@ class FormOptionsHelperTest < ActionView::TestCase opts end + def test_time_zone_options_returns_html_safe_string + assert time_zone_options_for_select.html_safe? + end + def test_select @post = Post.new @post.category = "<mus>" @@ -298,6 +377,15 @@ class FormOptionsHelperTest < ActionView::TestCase ) end + def test_select_with_boolean_method + @post = Post.new + @post.allow_comments = false + assert_dom_equal( + "<select id=\"post_allow_comments\" name=\"post[allow_comments]\"><option value=\"true\">true</option>\n<option value=\"false\" selected=\"selected\">false</option></select>", + select("post", "allow_comments", %w( true false )) + ) + end + def test_select_under_fields_for @post = Post.new @post.category = "<mus>" @@ -305,13 +393,26 @@ class FormOptionsHelperTest < ActionView::TestCase output_buffer = fields_for :post, @post do |f| concat f.select(:category, %w( abe <mus> hest)) end - + assert_dom_equal( "<select id=\"post_category\" name=\"post[category]\"><option value=\"abe\">abe</option>\n<option value=\"<mus>\" selected=\"selected\"><mus></option>\n<option value=\"hest\">hest</option></select>", output_buffer ) end + def test_fields_for_with_record_inherited_from_hash + map = Map.new + + output_buffer = fields_for :map, map do |f| + concat f.select(:category, %w( abe <mus> hest)) + end + + assert_dom_equal( + "<select id=\"map_category\" name=\"map[category]\"><option value=\"abe\">abe</option>\n<option value=\"<mus>\" selected=\"selected\"><mus></option>\n<option value=\"hest\">hest</option></select>", + output_buffer + ) + end + def test_select_under_fields_for_with_index @post = Post.new @post.category = "<mus>" @@ -438,11 +539,11 @@ class FormOptionsHelperTest < ActionView::TestCase def test_select_with_index_option @album = Album.new @album.id = 1 - - expected = "<select id=\"album__genre\" name=\"album[][genre]\"><option value=\"rap\">rap</option>\n<option value=\"rock\">rock</option>\n<option value=\"country\">country</option></select>" + + expected = "<select id=\"album__genre\" name=\"album[][genre]\"><option value=\"rap\">rap</option>\n<option value=\"rock\">rock</option>\n<option value=\"country\">country</option></select>" assert_dom_equal( - expected, + expected, select("album[]", "genre", %w[rap rock country], {}, { :index => nil }) ) end @@ -491,7 +592,7 @@ class FormOptionsHelperTest < ActionView::TestCase output_buffer = fields_for :post, @post do |f| concat f.collection_select(:author_name, dummy_posts, :author_name, :author_name) end - + assert_dom_equal( "<select id=\"post_author_name\" name=\"post[author_name]\"><option value=\"<Abe>\"><Abe></option>\n<option value=\"Babe\" selected=\"selected\">Babe</option>\n<option value=\"Cabe\">Cabe</option></select>", output_buffer @@ -599,7 +700,7 @@ class FormOptionsHelperTest < ActionView::TestCase output_buffer = fields_for :firm, @firm do |f| concat f.time_zone_select(:time_zone) end - + assert_dom_equal( "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + "<option value=\"A\">A</option>\n" + diff --git a/actionpack/test/template/form_tag_helper_test.rb b/actionpack/test/template/form_tag_helper_test.rb index 6c85952d40..f8671f2980 100644 --- a/actionpack/test/template/form_tag_helper_test.rb +++ b/actionpack/test/template/form_tag_helper_test.rb @@ -12,20 +12,24 @@ class FormTagHelperTest < ActionView::TestCase method = options[:method] txt = %{<div style="margin:0;padding:0;display:inline">} - txt << %{<input name="_e" type="hidden" value="☃" />} - txt << %{<input name="_method" type="hidden" value="#{method}" />} if method + txt << %{<input name="utf8" type="hidden" value="✓" />} + if (method && !['get','post'].include?(method.to_s)) + txt << %{<input name="_method" type="hidden" value="#{method}" />} + end txt << %{</div>} end def form_text(action = "http://www.example.com", options = {}) - remote, enctype, html_class, id = options.values_at(:remote, :enctype, :html_class, :id) + remote, enctype, html_class, id, method = options.values_at(:remote, :enctype, :html_class, :id, :method) + + method = method.to_s == "get" ? "get" : "post" txt = %{<form accept-charset="UTF-8" action="#{action}"} txt << %{ enctype="multipart/form-data"} if enctype txt << %{ data-remote="true"} if remote txt << %{ class="#{html_class}"} if html_class txt << %{ id="#{id}"} if id - txt << %{ method="post">} + txt << %{ method="#{method}">} end def whole_form(action = "http://www.example.com", options = {}) @@ -201,12 +205,6 @@ class FormTagHelperTest < ActionView::TestCase assert_dom_equal expected, actual end - def test_select_tag_with_array_options - assert_deprecated /array/ do - select_tag "people", ["<option>david</option>"] - end - end - def test_text_area_tag_size_string actual = text_area_tag "body", "hello world", "size" => "20x40" expected = %(<textarea cols="20" id="body" name="body" rows="40">hello world</textarea>) @@ -387,6 +385,57 @@ class FormTagHelperTest < ActionView::TestCase ) end + def test_button_tag + assert_dom_equal( + %(<button name="button" type="submit">Button</button>), + button_tag + ) + end + + def test_button_tag_with_submit_type + assert_dom_equal( + %(<button name="button" type="submit">Save</button>), + button_tag("Save", :type => "submit") + ) + end + + def test_button_tag_with_button_type + assert_dom_equal( + %(<button name="button" type="button">Button</button>), + button_tag("Button", :type => "button") + ) + end + + def test_button_tag_with_reset_type + assert_dom_equal( + %(<button name="button" type="reset">Reset</button>), + button_tag("Reset", :type => "reset") + ) + end + + def test_button_tag_with_disabled_option + assert_dom_equal( + %(<button name="button" type="reset" disabled="disabled">Reset</button>), + button_tag("Reset", :type => "reset", :disabled => true) + ) + end + + def test_button_tag_escape_content + assert_dom_equal( + %(<button name="button" type="reset" disabled="disabled"><b>Reset</b></button>), + button_tag("<b>Reset</b>", :type => "reset", :disabled => true) + ) + end + + def test_button_tag_with_block + assert_dom_equal('<button name="button" type="submit">Content</button>', button_tag { 'Content' }) + end + + def test_button_tag_with_block_and_options + output = button_tag(:name => 'temptation', :type => 'button') { content_tag(:strong, 'Do not press me') } + assert_dom_equal('<button name="temptation" type="button"><strong>Do not press me</strong></button>', output) + end + def test_image_submit_tag_with_confirmation assert_dom_equal( %(<input type="image" src="/images/save.gif" data-confirm="Are you sure?" />), diff --git a/actionpack/test/template/html-scanner/document_test.rb b/actionpack/test/template/html-scanner/document_test.rb index c68f04fa75..ddfb351595 100644 --- a/actionpack/test/template/html-scanner/document_test.rb +++ b/actionpack/test/template/html-scanner/document_test.rb @@ -15,7 +15,7 @@ class DocumentTest < Test::Unit::TestCase assert_match %r{\s+}m, doc.root.children[1].content assert_equal "html", doc.root.children[2].name end - + def test_find_img doc = HTML::Document.new <<-HTML.strip <html> diff --git a/actionpack/test/template/html-scanner/node_test.rb b/actionpack/test/template/html-scanner/node_test.rb index b0df36877e..f4b9b198e8 100644 --- a/actionpack/test/template/html-scanner/node_test.rb +++ b/actionpack/test/template/html-scanner/node_test.rb @@ -1,39 +1,39 @@ require 'abstract_unit' class NodeTest < Test::Unit::TestCase - + class MockNode def initialize(matched, value) @matched = matched @value = value end - + def find(conditions) @matched && self end - + def to_s @value.to_s end end - + def setup @node = HTML::Node.new("parent") @node.children.concat [MockNode.new(false,1), MockNode.new(true,"two"), MockNode.new(false,:three)] end - + def test_match assert !@node.match("foo") end - + def test_tag assert !@node.tag? end - + def test_to_s assert_equal "1twothree", @node.to_s end - + def test_find assert_equal "two", @node.find('blah').to_s end @@ -58,7 +58,7 @@ class NodeTest < Test::Unit::TestCase assert node.attributes.has_key?("bar") assert "<b foo bar>", node.to_s end - + def test_parse_with_unclosed_tag s = "<span onmouseover='bang'" node = nil diff --git a/actionpack/test/template/html-scanner/sanitizer_test.rb b/actionpack/test/template/html-scanner/sanitizer_test.rb index c9edde8892..fcc3782f04 100644 --- a/actionpack/test/template/html-scanner/sanitizer_test.rb +++ b/actionpack/test/template/html-scanner/sanitizer_test.rb @@ -24,11 +24,11 @@ class SanitizerTest < ActionController::TestCase def test_strip_links sanitizer = HTML::LinkSanitizer.new - assert_equal "Dont touch me", sanitizer.sanitize("Dont touch me") + assert_equal "Dont touch me", sanitizer.sanitize("Dont touch me") assert_equal "on my mind\nall day long", sanitizer.sanitize("<a href='almost'>on my mind</a>\n<A href='almost'>all day long</A>") - assert_equal "0wn3d", sanitizer.sanitize("<a href='http://www.rubyonrails.com/'><a href='http://www.rubyonrails.com/' onlclick='steal()'>0wn3d</a></a>") - assert_equal "Magic", sanitizer.sanitize("<a href='http://www.rubyonrails.com/'>Mag<a href='http://www.ruby-lang.org/'>ic") - assert_equal "FrrFox", sanitizer.sanitize("<href onlclick='steal()'>FrrFox</a></href>") + assert_equal "0wn3d", sanitizer.sanitize("<a href='http://www.rubyonrails.com/'><a href='http://www.rubyonrails.com/' onlclick='steal()'>0wn3d</a></a>") + assert_equal "Magic", sanitizer.sanitize("<a href='http://www.rubyonrails.com/'>Mag<a href='http://www.ruby-lang.org/'>ic") + assert_equal "FrrFox", sanitizer.sanitize("<href onlclick='steal()'>FrrFox</a></href>") assert_equal "My mind\nall <b>day</b> long", sanitizer.sanitize("<a href='almost'>My mind</a>\n<A href='almost'>all <b>day</b> long</A>") assert_equal "all <b>day</b> long", sanitizer.sanitize("<<a>a href='hello'>all <b>day</b> long<</A>/a>") @@ -58,7 +58,7 @@ class SanitizerTest < ActionController::TestCase raw = %{href="javascript:bang" <a href="javascript:bang" name="hello">foo</a>, <span href="javascript:bang">bar</span>} assert_sanitized raw, %{href="javascript:bang" <a name="hello">foo</a>, <span>bar</span>} end - + def test_sanitize_image_src raw = %{src="javascript:bang" <img src="javascript:bang" width="5">foo</img>, <span src="javascript:bang">bar</span>} assert_sanitized raw, %{src="javascript:bang" <img width="5">foo</img>, <span>bar</span>} @@ -130,6 +130,13 @@ class SanitizerTest < ActionController::TestCase assert sanitizer.send(:contains_bad_protocols?, 'src', "#{proto}://bad") end end + + def test_should_accept_good_protocols_ignoring_case + sanitizer = HTML::WhiteListSanitizer.new + HTML::WhiteListSanitizer.allowed_protocols.each do |proto| + assert !sanitizer.send(:contains_bad_protocols?, 'src', "#{proto.capitalize}://good") + end + end def test_should_accept_good_protocols sanitizer = HTML::WhiteListSanitizer.new @@ -147,9 +154,9 @@ class SanitizerTest < ActionController::TestCase assert_sanitized %(<SCRIPT\nSRC=http://ha.ckers.org/xss.js></SCRIPT>), "" end - [%(<IMG SRC="javascript:alert('XSS');">), - %(<IMG SRC=javascript:alert('XSS')>), - %(<IMG SRC=JaVaScRiPt:alert('XSS')>), + [%(<IMG SRC="javascript:alert('XSS');">), + %(<IMG SRC=javascript:alert('XSS')>), + %(<IMG SRC=JaVaScRiPt:alert('XSS')>), %(<IMG """><SCRIPT>alert("XSS")</SCRIPT>">), %(<IMG SRC=javascript:alert("XSS")>), %(<IMG SRC=javascript:alert(String.fromCharCode(88,83,83))>), @@ -166,28 +173,28 @@ class SanitizerTest < ActionController::TestCase assert_sanitized img_hack, "<img>" end end - + def test_should_sanitize_tag_broken_up_by_null assert_sanitized %(<SCR\0IPT>alert(\"XSS\")</SCR\0IPT>), "alert(\"XSS\")" end - + def test_should_sanitize_invalid_script_tag assert_sanitized %(<SCRIPT/XSS SRC="http://ha.ckers.org/xss.js"></SCRIPT>), "" end - + def test_should_sanitize_script_tag_with_multiple_open_brackets assert_sanitized %(<<SCRIPT>alert("XSS");//<</SCRIPT>), "<" assert_sanitized %(<iframe src=http://ha.ckers.org/scriptlet.html\n<a), %(<a) end - + def test_should_sanitize_unclosed_script assert_sanitized %(<SCRIPT SRC=http://ha.ckers.org/xss.js?<B>), "<b>" end - + def test_should_sanitize_half_open_scripts assert_sanitized %(<IMG SRC="javascript:alert('XSS')"), "<img>" end - + def test_should_not_fall_for_ridiculous_hack img_hack = %(<IMG\nSRC\n=\n"\nj\na\nv\na\ns\nc\nr\ni\np\nt\n:\na\nl\ne\nr\nt\n(\n'\nX\nS\nS\n'\n)\n"\n>) assert_sanitized img_hack, "<img>" @@ -214,15 +221,15 @@ class SanitizerTest < ActionController::TestCase raw = %(-moz-binding:url('http://ha.ckers.org/xssmoz.xml#xss')) assert_equal '', sanitize_css(raw) end - + def test_should_sanitize_invalid_tag_names assert_sanitized(%(a b c<script/XSS src="http://ha.ckers.org/xss.js"></script>d e f), "a b cd e f") end - + def test_should_sanitize_non_alpha_and_non_digit_characters_in_tags assert_sanitized('<a onclick!#$%&()*~+-_.,:;?@[/|\]^`=alert("XSS")>foo</a>', "<a>foo</a>") end - + def test_should_sanitize_invalid_tag_names_in_single_tags assert_sanitized('<img/src="http://ha.ckers.org/xss.js"/>', "<img />") end diff --git a/actionpack/test/template/html-scanner/tag_node_test.rb b/actionpack/test/template/html-scanner/tag_node_test.rb index d1d4667378..0d87f1bd42 100644 --- a/actionpack/test/template/html-scanner/tag_node_test.rb +++ b/actionpack/test/template/html-scanner/tag_node_test.rb @@ -7,7 +7,7 @@ class TagNodeTest < Test::Unit::TestCase assert_equal Hash.new, node.attributes assert_nil node.closing end - + def test_open_with_attributes node = tag("<TAG1 foo=hey_ho x:bar=\"blah blah\" BAZ='blah blah blah' >") assert_equal "tag1", node.name @@ -15,28 +15,28 @@ class TagNodeTest < Test::Unit::TestCase assert_equal "blah blah", node["x:bar"] assert_equal "blah blah blah", node["baz"] end - + def test_self_closing_without_attributes node = tag("<tag/>") assert_equal "tag", node.name assert_equal Hash.new, node.attributes assert_equal :self, node.closing end - + def test_self_closing_with_attributes node = tag("<tag a=b/>") assert_equal "tag", node.name assert_equal( { "a" => "b" }, node.attributes ) assert_equal :self, node.closing end - + def test_closing_without_attributes node = tag("</tag>") assert_equal "tag", node.name assert_nil node.attributes assert_equal :close, node.closing end - + def test_bracket_op_when_no_attributes node = tag("</tag>") assert_nil node["foo"] @@ -46,27 +46,27 @@ class TagNodeTest < Test::Unit::TestCase node = tag("<tag a=b/>") assert_equal "b", node["a"] end - + def test_attributes_with_escaped_quotes node = tag("<tag a='b\\'c' b=\"bob \\\"float\\\"\">") assert_equal "b\\'c", node["a"] assert_equal "bob \\\"float\\\"", node["b"] end - + def test_to_s node = tag("<a b=c d='f' g=\"h 'i'\" />") - assert_equal %(<a b='c' d='f' g='h \\'i\\'' />), node.to_s + assert_equal %(<a b="c" d="f" g="h 'i'" />), node.to_s end - + def test_tag assert tag("<tag>").tag? end - + def test_match_tag_as_string assert tag("<tag>").match(:tag => "tag") assert !tag("<tag>").match(:tag => "b") end - + def test_match_tag_as_regexp assert tag("<tag>").match(:tag => /t.g/) assert !tag("<tag>").match(:tag => /t[bqs]g/) @@ -77,45 +77,45 @@ class TagNodeTest < Test::Unit::TestCase assert t.match(:attributes => {"a" => "something"}) assert t.match(:attributes => {"b" => "else"}) end - + def test_match_attributes_as_regexp t = tag("<tag a=something b=else />") assert t.match(:attributes => {"a" => /^something$/}) assert t.match(:attributes => {"b" => /e.*e/}) assert t.match(:attributes => {"a" => /me..i/, "b" => /.ls.$/}) end - + def test_match_attributes_as_number t = tag("<tag a=15 b=3.1415 />") assert t.match(:attributes => {"a" => 15}) assert t.match(:attributes => {"b" => 3.1415}) assert t.match(:attributes => {"a" => 15, "b" => 3.1415}) end - + def test_match_attributes_exist t = tag("<tag a=15 b=3.1415 />") assert t.match(:attributes => {"a" => true}) assert t.match(:attributes => {"b" => true}) assert t.match(:attributes => {"a" => true, "b" => true}) end - + def test_match_attributes_not_exist t = tag("<tag a=15 b=3.1415 />") assert t.match(:attributes => {"c" => false}) assert t.match(:attributes => {"c" => nil}) assert t.match(:attributes => {"a" => true, "c" => false}) end - + def test_match_parent_success t = tag("<tag a=15 b='hello'>", tag("<foo k='value'>")) assert t.match(:parent => {:tag => "foo", :attributes => {"k" => /v.l/, "j" => false}}) end - + def test_match_parent_fail t = tag("<tag a=15 b='hello'>", tag("<foo k='value'>")) assert !t.match(:parent => {:tag => /kafka/}) end - + def test_match_child_success t = tag("<tag x:k='something'>") tag("<child v=john a=kelly>", t) @@ -123,7 +123,7 @@ class TagNodeTest < Test::Unit::TestCase assert t.match(:child => { :tag => "sib", :attributes => {"v" => /j/}}) assert t.match(:child => { :attributes => {"a" => "kelly"}}) end - + def test_match_child_fail t = tag("<tag x:k='something'>") tag("<child v=john a=kelly>", t) @@ -131,13 +131,13 @@ class TagNodeTest < Test::Unit::TestCase assert !t.match(:child => { :tag => "sib", :attributes => {"v" => /r/}}) assert !t.match(:child => { :attributes => {"v" => false}}) end - + def test_match_ancestor_success t = tag("<tag x:k='something'>", tag("<parent v=john a=kelly>", tag("<grandparent m=vaughn v=james>"))) assert t.match(:ancestor => {:tag => "parent", :attributes => {"a" => /ll/}}) assert t.match(:ancestor => {:attributes => {"m" => "vaughn"}}) end - + def test_match_ancestor_fail t = tag("<tag x:k='something'>", tag("<parent v=john a=kelly>", tag("<grandparent m=vaughn v=james>"))) assert !t.match(:ancestor => {:tag => /^parent/, :attributes => {"v" => /m/}}) @@ -149,13 +149,13 @@ class TagNodeTest < Test::Unit::TestCase assert t.match(:descendant => {:tag => "child", :attributes => {"a" => /ll/}}) assert t.match(:descendant => {:attributes => {"m" => "vaughn"}}) end - + def test_match_descendant_fail tag("<grandchild m=vaughn v=james>", tag("<child v=john a=kelly>", t = tag("<tag x:k='something'>"))) assert !t.match(:descendant => {:tag => /^child/, :attributes => {"v" => /m/}}) assert !t.match(:descendant => {:attributes => {"v" => false}}) end - + def test_match_child_count t = tag("<tag x:k='something'>") tag("hello", t) @@ -221,7 +221,7 @@ class TagNodeTest < Test::Unit::TestCase assert !m.match(:after => {:tag => "span", :attributes => {:k => true}}) end - def test_to_s + def test_tag_to_s t = tag("<b x='foo'>") tag("hello", t) tag("<hr />", t) @@ -229,7 +229,7 @@ class TagNodeTest < Test::Unit::TestCase end private - + def tag(content, parent=nil) node = HTML::Node.parse(parent,0,0,content) parent.children << node if parent diff --git a/actionpack/test/template/html-scanner/text_node_test.rb b/actionpack/test/template/html-scanner/text_node_test.rb index 1ab3f4454e..6f61253ffa 100644 --- a/actionpack/test/template/html-scanner/text_node_test.rb +++ b/actionpack/test/template/html-scanner/text_node_test.rb @@ -4,27 +4,27 @@ class TextNodeTest < Test::Unit::TestCase def setup @node = HTML::Text.new(nil, 0, 0, "hello, howdy, aloha, annyeong") end - + def test_to_s assert_equal "hello, howdy, aloha, annyeong", @node.to_s end - + def test_find_string assert_equal @node, @node.find("hello, howdy, aloha, annyeong") assert_equal false, @node.find("bogus") end - + def test_find_regexp assert_equal @node, @node.find(/an+y/) assert_nil @node.find(/b/) end - + def test_find_hash assert_equal @node, @node.find(:content => /howdy/) assert_nil @node.find(:content => /^howdy$/) assert_equal false, @node.find(:content => "howdy") end - + def test_find_other assert_nil @node.find(:hello) end diff --git a/actionpack/test/template/html-scanner/tokenizer_test.rb b/actionpack/test/template/html-scanner/tokenizer_test.rb index a001bcbbad..bf45a7c2e3 100644 --- a/actionpack/test/template/html-scanner/tokenizer_test.rb +++ b/actionpack/test/template/html-scanner/tokenizer_test.rb @@ -29,7 +29,7 @@ class TokenizerTest < Test::Unit::TestCase tokenize "</tag>" assert_next "</tag>" end - + def test_tag_with_single_quoted_attribute tokenize %{<tag a='hello'>x} assert_next %{<tag a='hello'>} @@ -49,7 +49,7 @@ class TokenizerTest < Test::Unit::TestCase tokenize %{<tag a="hello\\"">x} assert_next %{<tag a="hello\\"">} end - + def test_tag_with_unquoted_attribute tokenize %{<tag a=hello>x} assert_next %{<tag a=hello>} @@ -59,12 +59,12 @@ class TokenizerTest < Test::Unit::TestCase tokenize %{<tag a="x < y">x} assert_next %{<tag a="x < y">} end - + def test_tag_with_gt_char_in_attribute tokenize %{<tag a="x > y">x} assert_next %{<tag a="x > y">} end - + def test_doctype_tag tokenize %{<!DOCTYPE "blah" "blah" "blah">\n <html>} assert_next %{<!DOCTYPE "blah" "blah" "blah">} @@ -90,7 +90,7 @@ class TokenizerTest < Test::Unit::TestCase assert_next %{original } assert_next %{< hello > world} end - + def test_less_than_without_matching_greater_than tokenize %{hello <span onmouseover="gotcha"\n<b>foo</b>\nbar</span>} assert_next %{hello } @@ -109,22 +109,22 @@ class TokenizerTest < Test::Unit::TestCase assert_next %{<!-- neverending...} assert_end end - + private - + def tokenize(text) @tokenizer = HTML::Tokenizer.new(text) end - + def assert_next(expected, message=nil) token = @tokenizer.next assert_equal expected, token, message end - + def assert_sequence(*expected) assert_next expected.shift until expected.empty? end - + def assert_end(message=nil) assert_nil @tokenizer.next, message end diff --git a/actionpack/test/template/javascript_helper_test.rb b/actionpack/test/template/javascript_helper_test.rb index a8ca19931b..2e7484afaf 100644 --- a/actionpack/test/template/javascript_helper_test.rb +++ b/actionpack/test/template/javascript_helper_test.rb @@ -22,8 +22,6 @@ class JavaScriptHelperTest < ActionView::TestCase ActiveSupport.escape_html_entities_in_json = false end - def _evaluate_assigns_and_ivars() end - def test_escape_javascript assert_equal '', escape_javascript(nil) assert_equal %(This \\"thing\\" is really\\n netos\\'), escape_javascript(%(This "thing" is really\n netos')) diff --git a/actionpack/test/template/log_subscriber_test.rb b/actionpack/test/template/log_subscriber_test.rb index eb1e548672..8b8b005a1d 100644 --- a/actionpack/test/template/log_subscriber_test.rb +++ b/actionpack/test/template/log_subscriber_test.rb @@ -29,7 +29,7 @@ class AVLogSubscriberTest < ActiveSupport::TestCase wait assert_equal 1, @logger.logged(:info).size - assert_match /Rendered test\/hello_world\.erb/, @logger.logged(:info).last + assert_match(/Rendered test\/hello_world\.erb/, @logger.logged(:info).last) end def test_render_text_template @@ -37,7 +37,7 @@ class AVLogSubscriberTest < ActiveSupport::TestCase wait assert_equal 1, @logger.logged(:info).size - assert_match /Rendered text template/, @logger.logged(:info).last + assert_match(/Rendered text template/, @logger.logged(:info).last) end def test_render_inline_template @@ -45,7 +45,7 @@ class AVLogSubscriberTest < ActiveSupport::TestCase wait assert_equal 1, @logger.logged(:info).size - assert_match /Rendered inline template/, @logger.logged(:info).last + assert_match(/Rendered inline template/, @logger.logged(:info).last) end def test_render_partial_template @@ -53,16 +53,16 @@ class AVLogSubscriberTest < ActiveSupport::TestCase wait assert_equal 1, @logger.logged(:info).size - assert_match /Rendered test\/_customer.erb/, @logger.logged(:info).last + assert_match(/Rendered test\/_customer.erb/, @logger.logged(:info).last) end def test_render_partial_with_implicit_path - @view.stubs(:controller_path).returns("test") + @view.stubs(:controller_prefixes).returns(%w(test)) @view.render(Customer.new("david"), :greeting => "hi") wait assert_equal 1, @logger.logged(:info).size - assert_match /Rendered customers\/_customer\.html\.erb/, @logger.logged(:info).last + assert_match(/Rendered customers\/_customer\.html\.erb/, @logger.logged(:info).last) end def test_render_collection_template @@ -70,24 +70,24 @@ class AVLogSubscriberTest < ActiveSupport::TestCase wait assert_equal 1, @logger.logged(:info).size - assert_match /Rendered test\/_customer.erb/, @logger.logged(:info).last + assert_match(/Rendered test\/_customer.erb/, @logger.logged(:info).last) end def test_render_collection_with_implicit_path - @view.stubs(:controller_path).returns("test") + @view.stubs(:controller_prefixes).returns(%w(test)) @view.render([ Customer.new("david"), Customer.new("mary") ], :greeting => "hi") wait assert_equal 1, @logger.logged(:info).size - assert_match /Rendered customers\/_customer\.html\.erb/, @logger.logged(:info).last + assert_match(/Rendered customers\/_customer\.html\.erb/, @logger.logged(:info).last) end def test_render_collection_template_without_path - @view.stubs(:controller_path).returns("test") + @view.stubs(:controller_prefixes).returns(%w(test)) @view.render([ GoodCustomer.new("david"), Customer.new("mary") ], :greeting => "hi") wait assert_equal 1, @logger.logged(:info).size - assert_match /Rendered collection/, @logger.logged(:info).last + assert_match(/Rendered collection/, @logger.logged(:info).last) end -end
\ No newline at end of file +end diff --git a/actionpack/test/template/lookup_context_test.rb b/actionpack/test/template/lookup_context_test.rb index cc71cb42d0..8d063e66b0 100644 --- a/actionpack/test/template/lookup_context_test.rb +++ b/actionpack/test/template/lookup_context_test.rb @@ -47,7 +47,7 @@ class LookupContextTest < ActiveSupport::TestCase end test "handles */* formats" do - @lookup_context.formats = [:"*/*"] + @lookup_context.formats = ["*/*"] assert_equal Mime::SET, @lookup_context.formats end @@ -73,25 +73,25 @@ class LookupContextTest < ActiveSupport::TestCase assert_equal :pt, I18n.locale assert_equal :pt, @lookup_context.locale ensure - I18n.config = I18n.config.i18n_config + I18n.config = I18n.config.original_config end assert_equal :pt, I18n.locale end test "find templates using the given view paths and configured details" do - template = @lookup_context.find("hello_world", "test") + template = @lookup_context.find("hello_world", %w(test)) assert_equal "Hello world!", template.source @lookup_context.locale = :da - template = @lookup_context.find("hello_world", "test") + template = @lookup_context.find("hello_world", %w(test)) assert_equal "Hey verden", template.source end test "found templates respects given formats if one cannot be found from template or handler" do ActionView::Template::Handlers::ERB.expects(:default_format).returns(nil) @lookup_context.formats = [:text] - template = @lookup_context.find("hello_world", "test") + template = @lookup_context.find("hello_world", %w(test)) assert_equal [:text], template.formats end @@ -100,8 +100,8 @@ class LookupContextTest < ActiveSupport::TestCase @lookup_context.with_fallbacks do assert_equal 3, @lookup_context.view_paths.size - assert @lookup_context.view_paths.include?(ActionView::FileSystemResolver.new("")) - assert @lookup_context.view_paths.include?(ActionView::FileSystemResolver.new("/")) + assert @lookup_context.view_paths.include?(ActionView::FallbackFileSystemResolver.new("")) + assert @lookup_context.view_paths.include?(ActionView::FallbackFileSystemResolver.new("/")) end end @@ -137,30 +137,137 @@ class LookupContextTest < ActiveSupport::TestCase test "gives the key forward to the resolver, so it can be used as cache key" do @lookup_context.view_paths = ActionView::FixtureResolver.new("test/_foo.erb" => "Foo") - template = @lookup_context.find("foo", "test", true) + template = @lookup_context.find("foo", %w(test), true) assert_equal "Foo", template.source # Now we are going to change the template, but it won't change the returned template # since we will hit the cache. @lookup_context.view_paths.first.hash["test/_foo.erb"] = "Bar" - template = @lookup_context.find("foo", "test", true) + template = @lookup_context.find("foo", %w(test), true) assert_equal "Foo", template.source # This time we will change the locale. The updated template should be picked since # lookup_context generated a new key after we changed the locale. @lookup_context.locale = :da - template = @lookup_context.find("foo", "test", true) + template = @lookup_context.find("foo", %w(test), true) assert_equal "Bar", template.source # Now we will change back the locale and it will still pick the old template. # This is expected because lookup_context will reuse the previous key for :en locale. @lookup_context.locale = :en - template = @lookup_context.find("foo", "test", true) + template = @lookup_context.find("foo", %w(test), true) assert_equal "Foo", template.source # Finally, we can expire the cache. And the expected template will be used. @lookup_context.view_paths.first.clear_cache - template = @lookup_context.find("foo", "test", true) + template = @lookup_context.find("foo", %w(test), true) assert_equal "Bar", template.source end -end
\ No newline at end of file + + test "can disable the cache on demand" do + @lookup_context.view_paths = ActionView::FixtureResolver.new("test/_foo.erb" => "Foo") + old_template = @lookup_context.find("foo", %w(test), true) + + template = @lookup_context.find("foo", %w(test), true) + assert_equal template, old_template + + assert @lookup_context.cache + template = @lookup_context.disable_cache do + assert !@lookup_context.cache + @lookup_context.find("foo", %w(test), true) + end + assert @lookup_context.cache + + assert_not_equal template, old_template + end + + test "data can be stored in cached templates" do + template = @lookup_context.find("hello_world", %w(test)) + template.data["cached"] = "data" + assert_equal "Hello world!", template.source + + template = @lookup_context.find("hello_world", %w(test)) + assert_equal "data", template.data["cached"] + assert_equal "Hello world!", template.source + end +end + +class LookupContextWithFalseCaching < ActiveSupport::TestCase + def setup + @resolver = ActionView::FixtureResolver.new("test/_foo.erb" => ["Foo", Time.utc(2000)]) + @resolver.stubs(:caching?).returns(false) + @lookup_context = ActionView::LookupContext.new(@resolver, {}) + end + + test "templates are always found in the resolver but timestamp is checked before being compiled" do + template = @lookup_context.find("foo", %w(test), true) + assert_equal "Foo", template.source + + # Now we are going to change the template, but it won't change the returned template + # since the timestamp is the same. + @resolver.hash["test/_foo.erb"][0] = "Bar" + template = @lookup_context.find("foo", %w(test), true) + assert_equal "Foo", template.source + + # Now update the timestamp. + @resolver.hash["test/_foo.erb"][1] = Time.now.utc + template = @lookup_context.find("foo", %w(test), true) + assert_equal "Bar", template.source + end + + test "if no template was found in the second lookup, with no cache, raise error" do + template = @lookup_context.find("foo", %w(test), true) + assert_equal "Foo", template.source + + @resolver.hash.clear + assert_raise ActionView::MissingTemplate do + @lookup_context.find("foo", %w(test), true) + end + end + + test "if no template was cached in the first lookup, retrieval should work in the second call" do + @resolver.hash.clear + assert_raise ActionView::MissingTemplate do + @lookup_context.find("foo", %w(test), true) + end + + @resolver.hash["test/_foo.erb"] = ["Foo", Time.utc(2000)] + template = @lookup_context.find("foo", %w(test), true) + assert_equal "Foo", template.source + end + + test "data can be stored as long as template was not updated" do + template = @lookup_context.find("foo", %w(test), true) + template.data["cached"] = "data" + assert_equal "Foo", template.source + + template = @lookup_context.find("foo", %w(test), true) + assert_equal "data", template.data["cached"] + assert_equal "Foo", template.source + + @resolver.hash["test/_foo.erb"][1] = Time.now.utc + template = @lookup_context.find("foo", %w(test), true) + assert_nil template.data["cached"] + assert_equal "Foo", template.source + end +end + +class TestMissingTemplate < ActiveSupport::TestCase + def setup + @lookup_context = ActionView::LookupContext.new("/Path/to/views", {}) + end + + test "if no template was found we get a helpful error message including the inheritance chain" do + e = assert_raise ActionView::MissingTemplate do + @lookup_context.find("foo", %w(parent child)) + end + assert_match %r{Missing template parent/foo, child/foo with .* Searched in:\n \* "/Path/to/views"\n}, e.message + end + + test "if no partial was found we get a helpful error message including the inheritance chain" do + e = assert_raise ActionView::MissingTemplate do + @lookup_context.find("foo", %w(parent child), true) + end + assert_match %r{Missing partial parent/foo, child/foo with .* Searched in:\n \* "/Path/to/views"\n}, e.message + end +end diff --git a/actionpack/test/template/number_helper_i18n_test.rb b/actionpack/test/template/number_helper_i18n_test.rb index 8561019461..5df09b4d3b 100644 --- a/actionpack/test/template/number_helper_i18n_test.rb +++ b/actionpack/test/template/number_helper_i18n_test.rb @@ -7,7 +7,7 @@ class NumberHelperTest < ActionView::TestCase I18n.backend.store_translations 'ts', :number => { :format => { :precision => 3, :delimiter => ',', :separator => '.', :significant => false, :strip_insignificant_zeros => false }, - :currency => { :format => { :unit => '&$', :format => '%u - %n', :precision => 2 } }, + :currency => { :format => { :unit => '&$', :format => '%u - %n', :negative_format => '(%u - %n)', :precision => 2 } }, :human => { :format => { :precision => 2, @@ -41,17 +41,20 @@ class NumberHelperTest < ActionView::TestCase :custom_units_for_number_to_human => {:mili => "mm", :centi => "cm", :deci => "dm", :unit => "m", :ten => "dam", :hundred => "hm", :thousand => "km"} end - def test_number_to_currency + def test_number_to_i18n_currency assert_equal("&$ - 10.00", number_to_currency(10, :locale => 'ts')) + assert_equal("(&$ - 10.00)", number_to_currency(-10, :locale => 'ts')) + assert_equal("-10.00 - &$", number_to_currency(-10, :locale => 'ts', :format => "%n - %u")) end def test_number_to_currency_with_clean_i18n_settings clean_i18n do assert_equal("$10.00", number_to_currency(10)) + assert_equal("-$10.00", number_to_currency(-10)) end end - def test_number_with_precision + def test_number_with_i18n_precision #Delimiter was set to "" assert_equal("10000", number_with_precision(10000, :locale => 'ts')) @@ -60,12 +63,12 @@ class NumberHelperTest < ActionView::TestCase end - def test_number_with_delimiter + def test_number_with_i18n_delimiter #Delimiter "," and separator "." assert_equal("1,000,000.234", number_with_delimiter(1000000.234, :locale => 'ts')) end - def test_number_to_percentage + def test_number_to_i18n_percentage # to see if strip_insignificant_zeros is true assert_equal("1%", number_to_percentage(1, :locale => 'ts')) # precision is 2, significant should be inherited @@ -74,7 +77,7 @@ class NumberHelperTest < ActionView::TestCase assert_equal("12434%", number_to_percentage(12434, :locale => 'ts')) end - def test_number_to_human_size + def test_number_to_i18n_human_size #b for bytes and k for kbytes assert_equal("2 k", number_to_human_size(2048, :locale => 'ts')) assert_equal("42 b", number_to_human_size(42, :locale => 'ts')) diff --git a/actionpack/test/template/number_helper_test.rb b/actionpack/test/template/number_helper_test.rb index 7f787b7b00..c8d50ebf75 100644 --- a/actionpack/test/template/number_helper_test.rb +++ b/actionpack/test/template/number_helper_test.rb @@ -45,11 +45,15 @@ class NumberHelperTest < ActionView::TestCase def test_number_to_currency assert_equal("$1,234,567,890.50", number_to_currency(1234567890.50)) assert_equal("$1,234,567,890.51", number_to_currency(1234567890.506)) + assert_equal("-$1,234,567,890.50", number_to_currency(-1234567890.50)) + assert_equal("-$ 1,234,567,890.50", number_to_currency(-1234567890.50, {:format => "%u %n"})) + assert_equal("($1,234,567,890.50)", number_to_currency(-1234567890.50, {:negative_format => "(%u%n)"})) assert_equal("$1,234,567,892", number_to_currency(1234567891.50, {:precision => 0})) assert_equal("$1,234,567,890.5", number_to_currency(1234567890.50, {:precision => 1})) assert_equal("£1234567890,50", number_to_currency(1234567890.50, {:unit => "£", :separator => ",", :delimiter => ""})) assert_equal("$1,234,567,890.50", number_to_currency("1234567890.50")) assert_equal("1,234,567,890.50 Kč", number_to_currency("1234567890.50", {:unit => "Kč", :format => "%n %u"})) + assert_equal("1,234,567,890.50 - Kč", number_to_currency("-1234567890.50", {:unit => "Kč", :format => "%n %u", :negative_format => "%n - %u"})) end def test_number_to_percentage @@ -83,6 +87,7 @@ class NumberHelperTest < ActionView::TestCase end def test_number_with_precision + assert_equal("-111.235", number_with_precision(-111.2346)) assert_equal("111.235", number_with_precision(111.2346)) assert_equal("31.83", number_with_precision(31.825, :precision => 2)) assert_equal("111.23", number_with_precision(111.2346, :precision => 2)) @@ -95,6 +100,8 @@ class NumberHelperTest < ActionView::TestCase assert_equal("0", number_with_precision(0, :precision => 0)) assert_equal("0.00100", number_with_precision(0.001, :precision => 5)) assert_equal("0.001", number_with_precision(0.00111, :precision => 3)) + assert_equal("10.00", number_with_precision(9.995, :precision => 2)) + assert_equal("11.00", number_with_precision(10.995, :precision => 2)) end def test_number_with_precision_with_custom_delimiter_and_separator @@ -116,10 +123,13 @@ class NumberHelperTest < ActionView::TestCase assert_equal "9775.00", number_with_precision(9775, :precision => 6, :significant => true ) assert_equal "5.392900", number_with_precision(5.3929, :precision => 7, :significant => true ) assert_equal "0.0", number_with_precision(0, :precision => 2, :significant => true ) - assert_equal "0", number_with_precision(0, :precision => 1, :significant => true ) + assert_equal "0", number_with_precision(0, :precision => 1, :significant => true ) assert_equal "0.0001", number_with_precision(0.0001, :precision => 1, :significant => true ) assert_equal "0.000100", number_with_precision(0.0001, :precision => 3, :significant => true ) assert_equal "0.0001", number_with_precision(0.0001111, :precision => 1, :significant => true ) + assert_equal "10.0", number_with_precision(9.995, :precision => 3, :significant => true) + assert_equal "9.99", number_with_precision(9.994, :precision => 3, :significant => true) + assert_equal "11.0", number_with_precision(10.995, :precision => 3, :significant => true) end def test_number_with_precision_with_strip_insignificant_zeros @@ -130,7 +140,7 @@ class NumberHelperTest < ActionView::TestCase def test_number_with_precision_with_significant_true_and_zero_precision # Zero precision with significant is a mistake (would always return zero), - # so we treat it as if significant was false (increases backwards compatibily for number_to_human_size) + # so we treat it as if significant was false (increases backwards compatibility for number_to_human_size) assert_equal "124", number_with_precision(123.987, :precision => 0, :significant => true) assert_equal "12", number_with_precision(12, :precision => 0, :significant => true ) assert_equal "12", number_with_precision("12.3", :precision => 0, :significant => true ) @@ -184,53 +194,57 @@ class NumberHelperTest < ActionView::TestCase end def test_number_to_human - assert_equal '123', number_to_human(123) - assert_equal '1.23 Thousand', number_to_human(1234) - assert_equal '12.3 Thousand', number_to_human(12345) - assert_equal '1.23 Million', number_to_human(1234567) - assert_equal '1.23 Billion', number_to_human(1234567890) - assert_equal '1.23 Trillion', number_to_human(1234567890123) - assert_equal '1.23 Quadrillion', number_to_human(1234567890123456) - assert_equal '1230 Quadrillion', number_to_human(1234567890123456789) - assert_equal '490 Thousand', number_to_human(489939, :precision => 2) - assert_equal '489.9 Thousand', number_to_human(489939, :precision => 4) - assert_equal '489 Thousand', number_to_human(489000, :precision => 4) - assert_equal '489.0 Thousand', number_to_human(489000, :precision => 4, :strip_insignificant_zeros => false) - assert_equal '1.2346 Million', number_to_human(1234567, :precision => 4, :significant => false) - assert_equal '1,2 Million', number_to_human(1234567, :precision => 1, :significant => false, :separator => ',') - assert_equal '1 Million', number_to_human(1234567, :precision => 0, :significant => true, :separator => ',') #significant forced to false + assert_equal '-123', number_to_human(-123) + assert_equal '-0.5', number_to_human(-0.5) + assert_equal '0', number_to_human(0) + assert_equal '0.5', number_to_human(0.5) + assert_equal '123', number_to_human(123) + assert_equal '1.23 Thousand', number_to_human(1234) + assert_equal '12.3 Thousand', number_to_human(12345) + assert_equal '1.23 Million', number_to_human(1234567) + assert_equal '1.23 Billion', number_to_human(1234567890) + assert_equal '1.23 Trillion', number_to_human(1234567890123) + assert_equal '1.23 Quadrillion', number_to_human(1234567890123456) + assert_equal '1230 Quadrillion', number_to_human(1234567890123456789) + assert_equal '490 Thousand', number_to_human(489939, :precision => 2) + assert_equal '489.9 Thousand', number_to_human(489939, :precision => 4) + assert_equal '489 Thousand', number_to_human(489000, :precision => 4) + assert_equal '489.0 Thousand', number_to_human(489000, :precision => 4, :strip_insignificant_zeros => false) + assert_equal '1.2346 Million', number_to_human(1234567, :precision => 4, :significant => false) + assert_equal '1,2 Million', number_to_human(1234567, :precision => 1, :significant => false, :separator => ',') + assert_equal '1 Million', number_to_human(1234567, :precision => 0, :significant => true, :separator => ',') #significant forced to false end def test_number_to_human_with_custom_units - #Only integers - volume = {:unit => "ml", :thousand => "lt", :million => "m3"} - assert_equal '123 lt', number_to_human(123456, :units => volume) - assert_equal '12 ml', number_to_human(12, :units => volume) - assert_equal '1.23 m3', number_to_human(1234567, :units => volume) - - #Including fractionals - distance = {:mili => "mm", :centi => "cm", :deci => "dm", :unit => "m", :ten => "dam", :hundred => "hm", :thousand => "km"} - assert_equal '1.23 mm', number_to_human(0.00123, :units => distance) - assert_equal '1.23 cm', number_to_human(0.0123, :units => distance) - assert_equal '1.23 dm', number_to_human(0.123, :units => distance) - assert_equal '1.23 m', number_to_human(1.23, :units => distance) - assert_equal '1.23 dam', number_to_human(12.3, :units => distance) - assert_equal '1.23 hm', number_to_human(123, :units => distance) - assert_equal '1.23 km', number_to_human(1230, :units => distance) - assert_equal '1.23 km', number_to_human(1230, :units => distance) - assert_equal '1.23 km', number_to_human(1230, :units => distance) - assert_equal '12.3 km', number_to_human(12300, :units => distance) - - #The quantifiers don't need to be a continuous sequence - gangster = {:hundred => "hundred bucks", :million => "thousand quids"} - assert_equal '1 hundred bucks', number_to_human(100, :units => gangster) - assert_equal '25 hundred bucks', number_to_human(2500, :units => gangster) - assert_equal '25 thousand quids', number_to_human(25000000, :units => gangster) - assert_equal '12300 thousand quids', number_to_human(12345000000, :units => gangster) - - #Spaces are stripped from the resulting string - assert_equal '4', number_to_human(4, :units => {:unit => "", :ten => 'tens '}) - assert_equal '4.5 tens', number_to_human(45, :units => {:unit => "", :ten => ' tens '}) + #Only integers + volume = {:unit => "ml", :thousand => "lt", :million => "m3"} + assert_equal '123 lt', number_to_human(123456, :units => volume) + assert_equal '12 ml', number_to_human(12, :units => volume) + assert_equal '1.23 m3', number_to_human(1234567, :units => volume) + + #Including fractionals + distance = {:mili => "mm", :centi => "cm", :deci => "dm", :unit => "m", :ten => "dam", :hundred => "hm", :thousand => "km"} + assert_equal '1.23 mm', number_to_human(0.00123, :units => distance) + assert_equal '1.23 cm', number_to_human(0.0123, :units => distance) + assert_equal '1.23 dm', number_to_human(0.123, :units => distance) + assert_equal '1.23 m', number_to_human(1.23, :units => distance) + assert_equal '1.23 dam', number_to_human(12.3, :units => distance) + assert_equal '1.23 hm', number_to_human(123, :units => distance) + assert_equal '1.23 km', number_to_human(1230, :units => distance) + assert_equal '1.23 km', number_to_human(1230, :units => distance) + assert_equal '1.23 km', number_to_human(1230, :units => distance) + assert_equal '12.3 km', number_to_human(12300, :units => distance) + + #The quantifiers don't need to be a continuous sequence + gangster = {:hundred => "hundred bucks", :million => "thousand quids"} + assert_equal '1 hundred bucks', number_to_human(100, :units => gangster) + assert_equal '25 hundred bucks', number_to_human(2500, :units => gangster) + assert_equal '25 thousand quids', number_to_human(25000000, :units => gangster) + assert_equal '12300 thousand quids', number_to_human(12345000000, :units => gangster) + + #Spaces are stripped from the resulting string + assert_equal '4', number_to_human(4, :units => {:unit => "", :ten => 'tens '}) + assert_equal '4.5 tens', number_to_human(45, :units => {:unit => "", :ten => ' tens '}) end def test_number_to_human_with_custom_format @@ -286,7 +300,8 @@ class NumberHelperTest < ActionView::TestCase assert number_to_percentage("asdf".html_safe).html_safe? assert number_to_phone(1).html_safe? - assert !number_to_phone("<script></script>").html_safe? + assert_equal "<script></script>", number_to_phone("<script></script>") + assert number_to_phone("<script></script>").html_safe? assert number_to_phone("asdf".html_safe).html_safe? assert number_with_delimiter(1).html_safe? @@ -341,7 +356,7 @@ class NumberHelperTest < ActionView::TestCase end assert_raise InvalidNumberError do - number_with_delimiter("x", :raise => true) + number_with_delimiter("x", :raise => true) end begin number_with_delimiter("x", :raise => true) @@ -350,7 +365,7 @@ class NumberHelperTest < ActionView::TestCase end assert_raise InvalidNumberError do - number_to_phone("x", :raise => true) + number_to_phone("x", :raise => true) end begin number_to_phone("x", :raise => true) diff --git a/actionpack/test/template/output_safety_helper_test.rb b/actionpack/test/template/output_safety_helper_test.rb new file mode 100644 index 0000000000..fc127c24e9 --- /dev/null +++ b/actionpack/test/template/output_safety_helper_test.rb @@ -0,0 +1,30 @@ +require 'abstract_unit' +require 'testing_sandbox' + +class OutputSafetyHelperTest < ActionView::TestCase + tests ActionView::Helpers::OutputSafetyHelper + include TestingSandbox + + def setup + @string = "hello" + end + + test "raw returns the safe string" do + result = raw(@string) + assert_equal @string, result + assert result.html_safe? + end + + test "raw handles nil values correctly" do + assert_equal "", raw(nil) + end + + test "safe_join should html_escape any items, including the separator, if they are not html_safe" do + joined = safe_join(["<p>foo</p>".html_safe, "<p>bar</p>"], "<br />") + assert_equal "<p>foo</p><br /><p>bar</p>", joined + + joined = safe_join(["<p>foo</p>".html_safe, "<p>bar</p>".html_safe], "<br />".html_safe) + assert_equal "<p>foo</p><br /><p>bar</p>", joined + end + +end
\ No newline at end of file diff --git a/actionpack/test/template/prototype_helper_test.rb b/actionpack/test/template/prototype_helper_test.rb index 036a44730c..a6aa848a00 100644 --- a/actionpack/test/template/prototype_helper_test.rb +++ b/actionpack/test/template/prototype_helper_test.rb @@ -71,7 +71,7 @@ class PrototypeHelperBaseTest < ActionView::TestCase end def create_generator - block = Proc.new { |*args| yield *args if block_given? } + block = Proc.new { |*args| yield(*args) if block_given? } JavaScriptGenerator.new self, &block end end diff --git a/actionpack/test/template/raw_output_helper_test.rb b/actionpack/test/template/raw_output_helper_test.rb deleted file mode 100644 index 598aa5b1d8..0000000000 --- a/actionpack/test/template/raw_output_helper_test.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'abstract_unit' -require 'testing_sandbox' - -class RawOutputHelperTest < ActionView::TestCase - tests ActionView::Helpers::RawOutputHelper - include TestingSandbox - - def setup - @string = "hello" - end - - test "raw returns the safe string" do - result = raw(@string) - assert_equal @string, result - assert result.html_safe? - end - - test "raw handles nil values correctly" do - assert_equal "", raw(nil) - end -end
\ No newline at end of file diff --git a/actionpack/test/template/render_test.rb b/actionpack/test/template/render_test.rb index 059dcedad8..dd86bfed04 100644 --- a/actionpack/test/template/render_test.rb +++ b/actionpack/test/template/render_test.rb @@ -69,8 +69,6 @@ module RenderTestCases end def test_render_update - # TODO: You should not have to stub out template because template is self! - @view.instance_variable_set(:@template, @view) assert_equal 'alert("Hello, World!");', @view.render(:update) { |page| page.alert('Hello, World!') } end @@ -116,7 +114,7 @@ module RenderTestCases end def test_render_sub_template_with_errors - @view.render(:file => "test/sub_template_raise") + @view.render(:template => "test/sub_template_raise") flunk "Render did not raise Template::Error" rescue ActionView::Template::Error => e assert_match %r!method.*doesnt_exist!, e.message @@ -125,15 +123,35 @@ module RenderTestCases assert_equal File.expand_path("#{FIXTURE_LOAD_PATH}/test/_raise.html.erb"), e.file_name end + def test_render_file_with_errors + @view.render(:file => File.expand_path("test/_raise", FIXTURE_LOAD_PATH)) + flunk "Render did not raise Template::Error" + rescue ActionView::Template::Error => e + assert_match %r!method.*doesnt_exist!, e.message + assert_equal "", e.sub_template_message + assert_equal "1", e.line_number + assert_equal "1: <%= doesnt_exist %>", e.annoted_source_code.strip + assert_equal File.expand_path("#{FIXTURE_LOAD_PATH}/test/_raise.html.erb"), e.file_name + end + def test_render_object assert_equal "Hello: david", @view.render(:partial => "test/customer", :object => Customer.new("david")) end + def test_render_object_with_array + assert_equal "[1, 2, 3]", @view.render(:partial => "test/object_inspector", :object => [1, 2, 3]) + end + def test_render_partial_collection assert_equal "Hello: davidHello: mary", @view.render(:partial => "test/customer", :collection => [ Customer.new("david"), Customer.new("mary") ]) end - def test_render_partial_collection_as + def test_render_partial_collection_as_by_string + assert_equal "david david davidmary mary mary", + @view.render(:partial => "test/customer_with_var", :collection => [ Customer.new("david"), Customer.new("mary") ], :as => 'customer') + end + + def test_render_partial_collection_as_by_symbol assert_equal "david david davidmary mary mary", @view.render(:partial => "test/customer_with_var", :collection => [ Customer.new("david"), Customer.new("mary") ], :as => :customer) end @@ -191,6 +209,11 @@ module RenderTestCases @view.formats = nil end + def test_render_layout_with_block_and_other_partial_inside + render = @view.render(:layout => "test/layout_with_partial_and_yield.html.erb") { "Yield!" } + assert_equal "Before\npartial html\nYield!\nAfter\n", render + end + def test_render_inline assert_equal "Hello, World!", @view.render(:inline => "Hello, World!") end @@ -208,6 +231,20 @@ module RenderTestCases "@output_buffer << 'source: #{template.source.inspect}'\n" end + WithViewHandler = lambda do |template, view| + %'"#{template.class} #{view.class}"' + end + + def test_render_inline_with_render_from_to_proc + ActionView::Template.register_template_handler :ruby_handler, :source.to_proc + assert_equal '3', @view.render(:inline => "(1 + 2).to_s", :type => :ruby_handler) + end + + def test_render_inline_with_template_handler_with_view + ActionView::Template.register_template_handler :with_view, WithViewHandler + assert_equal 'ActionView::Template ActionView::Base', @view.render(:inline => "Hello, World!", :type => :with_view) + end + def test_render_inline_with_compilable_custom_type ActionView::Template.register_template_handler :foo, CustomHandler assert_equal 'source: "Hello, World!"', @view.render(:inline => "Hello, World!", :type => :foo) @@ -234,13 +271,49 @@ module RenderTestCases @view.render(:file => "test/hello_world.erb", :layout => "layouts/yield_with_render_inline_inside") end + def test_render_with_layout_which_renders_another_partial + assert_equal %(partial html\nHello world!\n), + @view.render(:file => "test/hello_world.erb", :layout => "layouts/yield_with_render_partial_inside") + end - # TODO: Move to deprecated_tests.rb - def test_render_with_nested_layout_deprecated - assert_deprecated do - assert_equal %(<title>title</title>\n\n<div id="column">column</div>\n<div id="content">content</div>\n), - @view.render(:file => "test/deprecated_nested_layout.erb", :layout => "layouts/yield") - end + def test_render_layout_with_block_and_yield + assert_equal %(Content from block!\n), + @view.render(:layout => "layouts/yield_only") { "Content from block!" } + end + + def test_render_layout_with_block_and_yield_with_params + assert_equal %(Yield! Content from block!\n), + @view.render(:layout => "layouts/yield_with_params") { |param| "#{param} Content from block!" } + end + + def test_render_layout_with_block_which_renders_another_partial_and_yields + assert_equal %(partial html\nContent from block!\n), + @view.render(:layout => "layouts/partial_and_yield") { "Content from block!" } + end + + def test_render_partial_and_layout_without_block_with_locals + assert_equal %(Before (Foo!)\npartial html\nAfter), + @view.render(:partial => 'test/partial', :layout => 'test/layout_for_partial', :locals => { :name => 'Foo!'}) + end + + def test_render_partial_and_layout_without_block_with_locals_and_rendering_another_partial + assert_equal %(Before (Foo!)\npartial html\npartial with partial\n\nAfter), + @view.render(:partial => 'test/partial_with_partial', :layout => 'test/layout_for_partial', :locals => { :name => 'Foo!'}) + end + + def test_render_layout_with_a_nested_render_layout_call + assert_equal %(Before (Foo!)\nBefore (Bar!)\npartial html\nAfter\npartial with layout\n\nAfter), + @view.render(:partial => 'test/partial_with_layout', :layout => 'test/layout_for_partial', :locals => { :name => 'Foo!'}) + end + + def test_render_layout_with_a_nested_render_layout_call_using_block_with_render_partial + assert_equal %(Before (Foo!)\nBefore (Bar!)\n\n partial html\n\nAfterpartial with layout\n\nAfter), + @view.render(:partial => 'test/partial_with_layout_block_partial', :layout => 'test/layout_for_partial', :locals => { :name => 'Foo!'}) + end + + def test_render_layout_with_a_nested_render_layout_call_using_block_with_render_content + assert_equal %(Before (Foo!)\nBefore (Bar!)\n\n Content from inside layout!\n\nAfterpartial with layout\n\nAfter), + @view.render(:partial => 'test/partial_with_layout_block_content', :layout => 'test/layout_for_partial', :locals => { :name => 'Foo!'}) end def test_render_with_nested_layout @@ -252,6 +325,11 @@ module RenderTestCases assert_equal %(\n<title>title</title>\n\n), @view.render(:file => "test/layout_render_file.erb") end + + def test_render_layout_with_object + assert_equal %(<title>David</title>), + @view.render(:file => "test/layout_render_object.erb") + end end class CachedViewRenderTest < ActiveSupport::TestCase @@ -303,7 +381,7 @@ class LazyViewRenderTest < ActiveSupport::TestCase end def test_render_utf8_template_with_incompatible_external_encoding - with_external_encoding Encoding::SJIS do + with_external_encoding Encoding::SHIFT_JIS do begin result = @view.render(:file => "test/utf8.html.erb", :layouts => "layouts/yield") flunk 'Should have raised incompatible encoding error' @@ -314,7 +392,7 @@ class LazyViewRenderTest < ActiveSupport::TestCase end def test_render_utf8_template_with_partial_with_incompatible_encoding - with_external_encoding Encoding::SJIS do + with_external_encoding Encoding::SHIFT_JIS do begin result = @view.render(:file => "test/utf8_magic_with_bare_partial.html.erb", :layouts => "layouts/yield") flunk 'Should have raised incompatible encoding error' @@ -325,10 +403,11 @@ class LazyViewRenderTest < ActiveSupport::TestCase end def with_external_encoding(encoding) - old, Encoding.default_external = Encoding.default_external, encoding + old = Encoding.default_external + silence_warnings { Encoding.default_external = encoding } yield ensure - Encoding.default_external = old + silence_warnings { Encoding.default_external = old } end end end diff --git a/actionpack/test/template/resolver_patterns_test.rb b/actionpack/test/template/resolver_patterns_test.rb new file mode 100644 index 0000000000..97b1bad055 --- /dev/null +++ b/actionpack/test/template/resolver_patterns_test.rb @@ -0,0 +1,31 @@ +require 'abstract_unit' + +class ResolverPatternsTest < ActiveSupport::TestCase + def setup + path = File.expand_path("../../fixtures/", __FILE__) + pattern = ":prefix/{:formats/,}:action{.:formats,}{.:handlers,}" + @resolver = ActionView::FileSystemResolver.new(path, pattern) + end + + def test_should_return_empty_list_for_unknown_path + templates = @resolver.find_all("unknown", "custom_pattern", false, {:locale => [], :formats => [:html], :handlers => [:erb]}) + assert_equal [], templates, "expected an empty list of templates" + end + + def test_should_return_template_for_declared_path + templates = @resolver.find_all("path", "custom_pattern", false, {:locale => [], :formats => [:html], :handlers => [:erb]}) + assert_equal 1, templates.size, "expected one template" + assert_equal "Hello custom patterns!", templates.first.source + assert_equal "custom_pattern/path", templates.first.virtual_path + assert_equal [:html], templates.first.formats + end + + def test_should_return_all_templates_when_ambigous_pattern + templates = @resolver.find_all("another", "custom_pattern", false, {:locale => [], :formats => [:html], :handlers => [:erb]}) + assert_equal 2, templates.size, "expected two templates" + assert_equal "Another template!", templates[0].source + assert_equal "custom_pattern/another", templates[0].virtual_path + assert_equal "Hello custom patterns!", templates[1].source + assert_equal "custom_pattern/another", templates[1].virtual_path + end +end diff --git a/actionpack/test/template/tag_helper_test.rb b/actionpack/test/template/tag_helper_test.rb index 507cdca8d0..60b466a9ff 100644 --- a/actionpack/test/template/tag_helper_test.rb +++ b/actionpack/test/template/tag_helper_test.rb @@ -11,8 +11,8 @@ class TagHelperTest < ActionView::TestCase def test_tag_options str = tag("p", "class" => "show", :class => "elsewhere") - assert_match /class="show"/, str - assert_match /class="elsewhere"/, str + assert_match(/class="show"/, str) + assert_match(/class="elsewhere"/, str) end def test_tag_options_rejects_nil_option @@ -90,24 +90,31 @@ class TagHelperTest < ActionView::TestCase def test_cdata_section assert_equal "<![CDATA[<hello world>]]>", cdata_section("<hello world>") end - + def test_escape_once assert_equal '1 < 2 & 3', escape_once('1 < 2 & 3') end - + def test_tag_honors_html_safe_for_param_values ['1&2', '1 < 2', '“test“'].each do |escaped| assert_equal %(<a href="#{escaped}" />), tag('a', :href => escaped.html_safe) end end - + def test_skip_invalid_escaped_attributes ['&1;', 'dfa3;', '& #123;'].each do |escaped| - assert_equal %(<a href="#{escaped.gsub /&/, '&'}" />), tag('a', :href => escaped) + assert_equal %(<a href="#{escaped.gsub(/&/, '&')}" />), tag('a', :href => escaped) end end def test_disable_escaping assert_equal '<a href="&" />', tag('a', { :href => '&' }, false, false) end + + def test_data_attributes + ['data', :data].each { |data| + assert_dom_equal '<a data-a-number="1" data-array="[1,2,3]" data-hash="{"key":"value"}" data-string="hello" data-symbol="foo" />', + tag('a', { data => { :a_number => 1, :string => 'hello', :symbol => :foo, :array => [1, 2, 3], :hash => { :key => 'value'} } }) + } + end end diff --git a/actionpack/test/template/template_error_test.rb b/actionpack/test/template/template_error_test.rb new file mode 100644 index 0000000000..3a874082d9 --- /dev/null +++ b/actionpack/test/template/template_error_test.rb @@ -0,0 +1,13 @@ +require "abstract_unit" + +class TemplateErrorTest < ActiveSupport::TestCase + def test_provides_original_message + error = ActionView::Template::Error.new("test", {}, Exception.new("original")) + assert_equal "original", error.message + end + + def test_provides_useful_inspect + error = ActionView::Template::Error.new("test", {}, Exception.new("original")) + assert_equal "#<ActionView::Template::Error: original>", error.inspect + end +end diff --git a/actionpack/test/template/template_test.rb b/actionpack/test/template/template_test.rb index 18e0e83ec3..3432a02c3c 100644 --- a/actionpack/test/template/template_test.rb +++ b/actionpack/test/template/template_test.rb @@ -1,18 +1,21 @@ require "abstract_unit" - -if "ruby".encoding_aware? - # These are the normal settings that will be set up by Railties - # TODO: Have these tests support other combinations of these values - Encoding.default_internal = "UTF-8" - Encoding.default_external = "UTF-8" -end +require "logger" class TestERBTemplate < ActiveSupport::TestCase - ERBHandler = ActionView::Template::Handlers::ERB + ERBHandler = ActionView::Template::Handlers::ERB.new + + class LookupContext + def disable_cache + yield + end + end class Context + attr_accessor :_template + def initialize @output_buffer = "original" + @_virtual_path = nil end def hello @@ -21,15 +24,18 @@ class TestERBTemplate < ActiveSupport::TestCase def partial ActionView::Template.new( - "<%= @_virtual_path %>", + "<%= @_template.virtual_path %>", "partial", ERBHandler, :virtual_path => "partial" ) end + def lookup_context + @lookup_context ||= LookupContext.new + end + def logger - require "logger" Logger.new(STDERR) end @@ -38,16 +44,16 @@ class TestERBTemplate < ActiveSupport::TestCase end end - def new_template(body = "<%= hello %>", handler = ERBHandler, details = {}) - ActionView::Template.new(body, "hello template", ERBHandler, {:virtual_path => "hello"}) + def new_template(body = "<%= hello %>", details = {}) + ActionView::Template.new(body, "hello template", ERBHandler, {:virtual_path => "hello"}.merge!(details)) end def render(locals = {}) - @template.render(@obj, locals) + @template.render(@context, locals) end def setup - @obj = Context.new + @context = Context.new end def test_basic_template @@ -55,24 +61,96 @@ class TestERBTemplate < ActiveSupport::TestCase assert_equal "Hello", render end + def test_template_loses_its_source_after_rendering + @template = new_template + render + assert_nil @template.source + end + + def test_template_does_not_lose_its_source_after_rendering_if_it_does_not_have_a_virtual_path + @template = new_template("Hello", :virtual_path => nil) + render + assert_equal "Hello", @template.source + end + def test_locals @template = new_template("<%= my_local %>") + @template.locals = [:my_local] assert_equal "I'm a local", render(:my_local => "I'm a local") end def test_restores_buffer @template = new_template assert_equal "Hello", render - assert_equal "original", @obj.my_buffer + assert_equal "original", @context.my_buffer end def test_virtual_path - @template = new_template("<%= @_virtual_path %>" \ + @template = new_template("<%= @_template.virtual_path %>" \ "<%= partial.render(self, {}) %>" \ - "<%= @_virtual_path %>") + "<%= @_template.virtual_path %>") assert_equal "hellopartialhello", render end + def test_refresh_with_templates + @template = new_template("Hello", :virtual_path => "test/foo/bar") + @template.locals = [:key] + @context.lookup_context.expects(:find_template).with("bar", %w(test/foo), false, [:key]).returns("template") + assert_equal "template", @template.refresh(@context) + end + + def test_refresh_with_partials + @template = new_template("Hello", :virtual_path => "test/_foo") + @template.locals = [:key] + @context.lookup_context.expects(:find_template).with("foo", %w(test), true, [:key]).returns("partial") + assert_equal "partial", @template.refresh(@context) + end + + def test_refresh_raises_an_error_without_virtual_path + @template = new_template("Hello", :virtual_path => nil) + assert_raise RuntimeError do + @template.refresh(@context) + end + end + + def test_template_expire_sets_the_timestamp_to_1970 + @template = new_template("Hello", :updated_at => Time.utc(2010)) + assert_equal Time.utc(2010), @template.updated_at + @template.expire! + assert_equal Time.utc(1970), @template.updated_at + end + + def test_template_rerender_renders_a_template_like_self + @template = new_template("Hello", :virtual_path => "test/foo_bar") + @context.expects(:render).with(:template => "test/foo_bar").returns("template") + assert_equal "template", @template.rerender(@context) + end + + def test_template_rerender_renders_a_root_template_like_self + @template = new_template("Hello", :virtual_path => "foo_bar") + @context.expects(:render).with(:template => "foo_bar").returns("template") + assert_equal "template", @template.rerender(@context) + end + + def test_template_rerender_renders_a_partial_like_self + @template = new_template("Hello", :virtual_path => "test/_foo_bar") + @context.expects(:render).with(:partial => "test/foo_bar").returns("partial") + assert_equal "partial", @template.rerender(@context) + end + + def test_template_rerender_renders_a_root_partial_like_self + @template = new_template("Hello", :virtual_path => "_foo_bar") + @context.expects(:render).with(:partial => "foo_bar").returns("partial") + assert_equal "partial", @template.rerender(@context) + end + + def test_rerender_raises_an_error_without_virtual_path + @template = new_template("Hello", :virtual_path => nil) + assert_raise RuntimeError do + @template.rerender(@context) + end + end + if "ruby".encoding_aware? def test_resulting_string_is_utf8 @template = new_template @@ -89,12 +167,11 @@ class TestERBTemplate < ActiveSupport::TestCase # is set to something other than UTF-8, we don't # get any errors and get back a UTF-8 String. def test_default_external_works - Encoding.default_external = "ISO-8859-1" - @template = new_template("hello \xFCmlat") - assert_equal Encoding::UTF_8, render.encoding - assert_equal "hello \u{fc}mlat", render - ensure - Encoding.default_external = "UTF-8" + with_external_encoding "ISO-8859-1" do + @template = new_template("hello \xFCmlat") + assert_equal Encoding::UTF_8, render.encoding + assert_equal "hello \u{fc}mlat", render + end end def test_encoding_can_be_specified_with_magic_comment @@ -108,14 +185,14 @@ class TestERBTemplate < ActiveSupport::TestCase # inside Rails. def test_lying_with_magic_comment assert_raises(ActionView::Template::Error) do - @template = new_template("# encoding: UTF-8\nhello \xFCmlat") + @template = new_template("# encoding: UTF-8\nhello \xFCmlat", :virtual_path => nil) render end end def test_encoding_can_be_specified_with_magic_comment_in_erb with_external_encoding Encoding::UTF_8 do - @template = new_template("<%# encoding: ISO-8859-1 %>hello \xFCmlat") + @template = new_template("<%# encoding: ISO-8859-1 %>hello \xFCmlat", :virtual_path => nil) result = render assert_equal Encoding::UTF_8, render.encoding assert_equal "hello \u{fc}mlat", render @@ -124,16 +201,17 @@ class TestERBTemplate < ActiveSupport::TestCase def test_error_when_template_isnt_valid_utf8 assert_raises(ActionView::Template::Error, /\xFC/) do - @template = new_template("hello \xFCmlat") + @template = new_template("hello \xFCmlat", :virtual_path => nil) render end end def with_external_encoding(encoding) - old, Encoding.default_external = Encoding.default_external, encoding + old = Encoding.default_external + silence_warnings { Encoding.default_external = encoding } yield ensure - Encoding.default_external = old + silence_warnings { Encoding.default_external = old } end end -end
\ No newline at end of file +end diff --git a/actionpack/test/template/test_case_test.rb b/actionpack/test/template/test_case_test.rb index a0c46f8a59..11c355dc6d 100644 --- a/actionpack/test/template/test_case_test.rb +++ b/actionpack/test/template/test_case_test.rb @@ -112,7 +112,59 @@ module ActionView @controller.controller_path = 'test' @customers = [stub(:name => 'Eloy'), stub(:name => 'Manfred')] - assert_match /Hello: EloyHello: Manfred/, render(:partial => 'test/from_helper') + assert_match(/Hello: EloyHello: Manfred/, render(:partial => 'test/from_helper')) + end + end + + class ControllerHelperMethod < ActionView::TestCase + module SomeHelper + def some_method + render :partial => 'test/from_helper' + end + end + + helper SomeHelper + + test "can call a helper method defined on the current controller from a helper" do + @controller.singleton_class.class_eval <<-EOF, __FILE__, __LINE__ + 1 + def render_from_helper + 'controller_helper_method' + end + EOF + @controller.class.helper_method :render_from_helper + + assert_equal 'controller_helper_method', some_method + end + end + + class AssignsTest < ActionView::TestCase + setup do + ActiveSupport::Deprecation.stubs(:warn) + end + + test "_assigns delegates to user_defined_ivars" do + self.expects(:view_assigns) + _assigns + end + + test "_assigns is deprecated" do + ActiveSupport::Deprecation.expects(:warn) + _assigns + end + end + + class ViewAssignsTest < ActionView::TestCase + test "view_assigns returns a Hash of user defined ivars" do + @a = 'b' + @c = 'd' + assert_equal({:a => 'b', :c => 'd'}, view_assigns) + end + + test "view_assigns excludes internal ivars" do + INTERNAL_IVARS.each do |ivar| + assert defined?(ivar), "expected #{ivar} to be defined" + assert !view_assigns.keys.include?(ivar.sub('@','').to_sym), "expected #{ivar} to be excluded from view_assigns" + end end end @@ -172,7 +224,7 @@ module ActionView test "is able to use named routes" do with_routing do |set| - set.draw { |map| resources :contents } + set.draw { resources :contents } assert_equal 'http://test.host/contents/new', new_content_url assert_equal 'http://test.host/contents/1', content_url(:id => 1) end @@ -180,7 +232,7 @@ module ActionView test "named routes can be used from helper included in view" do with_routing do |set| - set.draw { |map| resources :contents } + set.draw { resources :contents } _helpers.module_eval do def render_from_helper new_content_url @@ -201,7 +253,7 @@ module ActionView @controller.controller_path = "test" @customers = [stub(:name => 'Eloy'), stub(:name => 'Manfred')] - assert_match /Hello: EloyHello: Manfred/, render(:file => 'test/list') + assert_match(/Hello: EloyHello: Manfred/, render(:file => 'test/list')) end test "is able to render partials from templates and also use instance variables after view has been referenced" do @@ -210,7 +262,7 @@ module ActionView view @customers = [stub(:name => 'Eloy'), stub(:name => 'Manfred')] - assert_match /Hello: EloyHello: Manfred/, render(:file => 'test/list') + assert_match(/Hello: EloyHello: Manfred/, render(:file => 'test/list')) end end @@ -253,4 +305,17 @@ module ActionView end end end + + module AHelperWithInitialize + def initialize(*) + super + @called_initialize = true + end + end + + class AHelperWithInitializeTest < ActionView::TestCase + test "the helper's initialize was actually called" do + assert @called_initialize + end + end end diff --git a/actionpack/test/template/test_test.rb b/actionpack/test/template/test_test.rb index 68e790cf46..3d0bbba435 100644 --- a/actionpack/test/template/test_test.rb +++ b/actionpack/test/template/test_test.rb @@ -39,7 +39,7 @@ class PeopleHelperTest < ActionView::TestCase with_test_route_set do person = mock(:name => "David") person.class.extend ActiveModel::Naming - expects(:mocha_mock_path).with(person).returns("/people/1") + _routes.url_helpers.expects(:hash_for_mocha_mock_path).with(person).returns("/people/1") assert_equal '<a href="/people/1">David</a>', link_to_person(person) end end @@ -47,7 +47,7 @@ class PeopleHelperTest < ActionView::TestCase private def with_test_route_set with_routing do |set| - set.draw do |map| + set.draw do match 'people', :to => 'people#index', :as => :people end yield diff --git a/actionpack/test/template/text_helper_test.rb b/actionpack/test/template/text_helper_test.rb index d22b9fe406..d0d4286393 100644 --- a/actionpack/test/template/text_helper_test.rb +++ b/actionpack/test/template/text_helper_test.rb @@ -1,4 +1,4 @@ -# encoding: us-ascii +# encoding: utf-8 require 'abstract_unit' require 'testing_sandbox' @@ -99,7 +99,7 @@ class TextHelperTest < ActionView::TestCase def test_highlight_should_be_html_safe assert highlight("This is a beautiful morning", "beautiful").html_safe? end - + def test_highlight assert_equal( "This is a <strong class=\"highlight\">beautiful</strong> morning", @@ -415,6 +415,12 @@ class TextHelperTest < ActionView::TestCase link10_raw = 'http://www.mail-archive.com/ruby-talk@ruby-lang.org/' link10_result = generate_result(link10_raw) assert_equal %(<p>#{link10_result} Link</p>), auto_link("<p>#{link10_raw} Link</p>") + + link11_raw = 'http://asakusa.rubyist.net/' + link11_result = generate_result(link11_raw) + with_kcode 'u' do + assert_equal %(浅草.rbの公式サイトはこちら#{link11_result}), auto_link("浅草.rbの公式サイトはこちら#{link11_raw}") + end end def test_auto_link_should_sanitize_input_when_sanitize_option_is_not_false @@ -430,7 +436,7 @@ class TextHelperTest < ActionView::TestCase def test_auto_link_other_protocols ftp_raw = 'ftp://example.com/file.txt' assert_equal %(Download #{generate_result(ftp_raw)}), auto_link("Download #{ftp_raw}") - + file_scheme = 'file:///home/username/RomeoAndJuliet.pdf' z39_scheme = 'z39.50r://host:696/db' chrome_scheme = 'chrome://package/section/path' @@ -452,7 +458,7 @@ class TextHelperTest < ActionView::TestCase assert_equal linked3, auto_link(linked3) assert_equal linked4, auto_link(linked4) assert_equal linked5, auto_link(linked5) - + linked_email = %Q(<a href="mailto:david@loudthinking.com">Mail me</a>) assert_equal linked_email, auto_link(linked_email) end @@ -491,13 +497,13 @@ class TextHelperTest < ActionView::TestCase url = "http://api.rubyonrails.com/Foo.html" email = "fantabulous@shiznadel.ic" - assert_equal %(<p><a href="#{url}">#{url[0...7]}...</a><br /><a href="mailto:#{email}">#{email[0...7]}...</a><br /></p>), auto_link("<p>#{url}<br />#{email}<br /></p>") { |url| truncate(url, :length => 10) } + assert_equal %(<p><a href="#{url}">#{url[0...7]}...</a><br /><a href="mailto:#{email}">#{email[0...7]}...</a><br /></p>), auto_link("<p>#{url}<br />#{email}<br /></p>") { |_url| truncate(_url, :length => 10) } end - + def test_auto_link_with_block_with_html pic = "http://example.com/pic.png" url = "http://example.com/album?a&b=c" - + assert_equal %(My pic: <a href="#{pic}"><img src="#{pic}" width="160px"></a> -- full album here #{generate_result(url)}), auto_link("My pic: #{pic} -- full album here #{url}") { |link| if link =~ /\.(jpg|gif|png|bmp|tif)$/i raw %(<img src="#{link}" width="160px">) @@ -512,7 +518,7 @@ class TextHelperTest < ActionView::TestCase auto_link("Welcome to my new blog at http://www.myblog.com/. Please e-mail me at me@email.com.", :link => :all, :html => { :class => "menu", :target => "_blank" }) end - + def test_auto_link_with_multiple_trailing_punctuations url = "http://youtube.com" url_result = generate_result(url) diff --git a/actionpack/test/template/translation_helper_test.rb b/actionpack/test/template/translation_helper_test.rb index 1be418a206..9b5c6d127c 100644 --- a/actionpack/test/template/translation_helper_test.rb +++ b/actionpack/test/template/translation_helper_test.rb @@ -4,60 +4,78 @@ class TranslationHelperTest < ActiveSupport::TestCase include ActionView::Helpers::TagHelper include ActionView::Helpers::TranslationHelper - attr_reader :request + attr_reader :request, :view + def setup + I18n.backend.store_translations(:en, + :translations => { + :templates => { + :found => { :foo => 'Foo' }, + :array => { :foo => { :bar => 'Foo Bar' } } + }, + :foo => 'Foo', + :hello => '<a>Hello World</a>', + :html => '<a>Hello World</a>', + :hello_html => '<a>Hello World</a>', + :array_html => %w(foo bar), + :array => %w(foo bar) + } + ) + @view = ::ActionView::Base.new(ActionController::Base.view_paths, {}) end - - def test_delegates_to_i18n_setting_the_raise_option - I18n.expects(:translate).with(:foo, :locale => 'en', :raise => true).returns("") + + def test_delegates_to_i18n_setting_the_rescue_format_option_to_html + I18n.expects(:translate).with(:foo, :locale => 'en', :rescue_format => :html).returns("") translate :foo, :locale => 'en' end - + + def test_delegates_localize_to_i18n + @time = Time.utc(2008, 7, 8, 12, 18, 38) + I18n.expects(:localize).with(@time) + localize @time + end + def test_returns_missing_translation_message_wrapped_into_span - expected = '<span class="translation_missing">en, foo</span>' - assert_equal expected, translate(:foo) + expected = '<span class="translation_missing" title="translation missing: en.translations.missing">Missing</span>' + assert_equal expected, translate(:"translations.missing") + end + + def test_returns_missing_translation_message_using_nil_as_rescue_format + expected = 'translation missing: en.translations.missing' + assert_equal expected, translate(:"translations.missing", :rescue_format => nil) end def test_translation_returning_an_array - I18n.expects(:translate).with(:foo, :raise => true).returns(["foo", "bar"]) - assert_equal ["foo", "bar"], translate(:foo) + expected = %w(foo bar) + assert_equal expected, translate(:"translations.array") end - def test_delegates_localize_to_i18n - @time = Time.utc(2008, 7, 8, 12, 18, 38) - I18n.expects(:localize).with(@time) - localize @time + def test_finds_translation_scoped_by_partial + assert_equal 'Foo', view.render(:file => 'translations/templates/found').strip end - - def test_scoping_by_partial - I18n.expects(:translate).with("test.translation.helper", :raise => true).returns("helper") - @view = ActionView::Base.new(ActionController::Base.view_paths, {}) - assert_equal "helper", @view.render(:file => "test/translation") + + def test_finds_array_of_translations_scoped_by_partial + assert_equal 'Foo Bar', @view.render(:file => 'translations/templates/array').strip end - def test_scoping_by_partial_of_an_array - I18n.expects(:translate).with("test.scoped_translation.foo.bar", :raise => true).returns(["foo", "bar"]) - @view = ActionView::Base.new(ActionController::Base.view_paths, {}) - assert_equal "foobar", @view.render(:file => "test/scoped_translation") + def test_missing_translation_scoped_by_partial + expected = '<span class="translation_missing" title="translation missing: en.translations.templates.missing.missing">Missing</span>' + assert_equal expected, view.render(:file => 'translations/templates/missing').strip end - + def test_translate_does_not_mark_plain_text_as_safe_html - I18n.expects(:translate).with("hello", :raise => true).returns("Hello World") - assert_equal false, translate("hello").html_safe? + assert_equal false, translate(:'translations.hello').html_safe? end def test_translate_marks_translations_named_html_as_safe_html - I18n.expects(:translate).with("html", :raise => true).returns("<a>Hello World</a>") - assert translate("html").html_safe? + assert translate(:'translations.html').html_safe? end def test_translate_marks_translations_with_a_html_suffix_as_safe_html - I18n.expects(:translate).with("hello_html", :raise => true).returns("<a>Hello World</a>") - assert translate("hello_html").html_safe? + assert translate(:'translations.hello_html').html_safe? end def test_translation_returning_an_array_ignores_html_suffix - I18n.expects(:translate).with(:foo_html, :raise => true).returns(["foo", "bar"]) - assert_equal ["foo", "bar"], translate(:foo_html) + assert_equal ["foo", "bar"], translate(:'translations.array_html') end end diff --git a/actionpack/test/template/url_helper_test.rb b/actionpack/test/template/url_helper_test.rb index d59bbec4a9..fc330f7a73 100644 --- a/actionpack/test/template/url_helper_test.rb +++ b/actionpack/test/template/url_helper_test.rb @@ -9,7 +9,7 @@ class UrlHelperTest < ActiveSupport::TestCase # or request. # # In those cases, we'll set up a simple mock - attr_accessor :controller, :request + attr_accessor :controller, :request, :_template routes = ActionDispatch::Routing::RouteSet.new routes.draw do @@ -20,17 +20,11 @@ class UrlHelperTest < ActiveSupport::TestCase include routes.url_helpers include ActionView::Helpers::UrlHelper + include ActionView::Helpers::JavaScriptHelper include ActionDispatch::Assertions::DomAssertions include ActionView::Context include RenderERBUtils - # self.default_url_options = {:host => "www.example.com"} - - # TODO: This shouldn't be needed (see template.rb:53) - def assigns - {} - end - def hash_for(opts = []) ActiveSupport::OrderedHash[*([:controller, "foo", :action, "bar"].concat(opts))] end @@ -57,6 +51,14 @@ class UrlHelperTest < ActiveSupport::TestCase assert_dom_equal "<form method=\"post\" action=\"http://www.example.com\" class=\"button_to\"><div><input type=\"submit\" value=\"Hello\" /></div></form>", button_to("Hello", "http://www.example.com") end + def test_button_to_with_form_class + assert_dom_equal "<form method=\"post\" action=\"http://www.example.com\" class=\"custom-class\"><div><input type=\"submit\" value=\"Hello\" /></div></form>", button_to("Hello", "http://www.example.com", :form_class => 'custom-class') + end + + def test_button_to_with_form_class_escapes + assert_dom_equal "<form method=\"post\" action=\"http://www.example.com\" class=\"<script>evil_js</script>\"><div><input type=\"submit\" value=\"Hello\" /></div></form>", button_to("Hello", "http://www.example.com", :form_class => '<script>evil_js</script>') + end + def test_button_to_with_query assert_dom_equal "<form method=\"post\" action=\"http://www.example.com/q1=v1&q2=v2\" class=\"button_to\"><div><input type=\"submit\" value=\"Hello\" /></div></form>", button_to("Hello", "http://www.example.com/q1=v1&q2=v2") end @@ -76,6 +78,13 @@ class UrlHelperTest < ActiveSupport::TestCase ) end + def test_button_to_with_javascript_disable_with + assert_dom_equal( + "<form method=\"post\" action=\"http://www.example.com\" class=\"button_to\"><div><input data-disable-with=\"Greeting...\" type=\"submit\" value=\"Hello\" /></div></form>", + button_to("Hello", "http://www.example.com", :disable_with => "Greeting...") + ) + end + def test_button_to_with_remote_and_javascript_confirm assert_dom_equal( "<form method=\"post\" action=\"http://www.example.com\" class=\"button_to\" data-remote=\"true\"><div><input data-confirm=\"Are you sure?\" type=\"submit\" value=\"Hello\" /></div></form>", @@ -83,6 +92,20 @@ class UrlHelperTest < ActiveSupport::TestCase ) end + def test_button_to_with_remote_and_javascript_disable_with + assert_dom_equal( + "<form method=\"post\" action=\"http://www.example.com\" class=\"button_to\" data-remote=\"true\"><div><input data-disable-with=\"Greeting...\" type=\"submit\" value=\"Hello\" /></div></form>", + button_to("Hello", "http://www.example.com", :remote => true, :disable_with => "Greeting...") + ) + end + + def test_button_to_with_remote_and_javascript_confirm_and_javascript_disable_with + assert_dom_equal( + "<form method=\"post\" action=\"http://www.example.com\" class=\"button_to\" data-remote=\"true\"><div><input data-disable-with=\"Greeting...\" data-confirm=\"Are you sure?\" type=\"submit\" value=\"Hello\" /></div></form>", + button_to("Hello", "http://www.example.com", :remote => true, :confirm => "Are you sure?", :disable_with => "Greeting...") + ) + end + def test_button_to_with_remote_false assert_dom_equal( "<form method=\"post\" action=\"http://www.example.com\" class=\"button_to\"><div><input type=\"submit\" value=\"Hello\" /></div></form>", @@ -249,12 +272,7 @@ class UrlHelperTest < ActiveSupport::TestCase assert_equal "<strong>Showing</strong>", link_to_unless(true, "Showing", url_hash) { |name| - "<strong>#{name}</strong>" - } - - assert_equal "<strong>Showing</strong>", - link_to_unless(true, "Showing", url_hash) { |name| - "<strong>#{name}</strong>" + "<strong>#{name}</strong>".html_safe } assert_equal "test", @@ -350,13 +368,13 @@ class UrlHelperTest < ActiveSupport::TestCase def test_mail_to_with_javascript snippet = mail_to("me@domain.com", "My email", :encode => "javascript") - assert_dom_equal "<script type=\"text/javascript\">eval(decodeURIComponent('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%22%6d%61%69%6c%74%6f%3a%6d%65%40%64%6f%6d%61%69%6e%2e%63%6f%6d%22%3e%4d%79%20%65%6d%61%69%6c%3c%2f%61%3e%27%29%3b'))</script>", snippet + assert_dom_equal "<script type=\"text/javascript\">eval(decodeURIComponent('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%5c%22%6d%61%69%6c%74%6f%3a%6d%65%40%64%6f%6d%61%69%6e%2e%63%6f%6d%5c%22%3e%4d%79%20%65%6d%61%69%6c%3c%5c%2f%61%3e%27%29%3b'))</script>", snippet assert snippet.html_safe? end def test_mail_to_with_javascript_unicode snippet = mail_to("unicode@example.com", "únicode", :encode => "javascript") - assert_dom_equal "<script type=\"text/javascript\">eval(decodeURIComponent('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%22%6d%61%69%6c%74%6f%3a%75%6e%69%63%6f%64%65%40%65%78%61%6d%70%6c%65%2e%63%6f%6d%22%3e%c3%ba%6e%69%63%6f%64%65%3c%2f%61%3e%27%29%3b'))</script>", snippet + assert_dom_equal "<script type=\"text/javascript\">eval(decodeURIComponent('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%5c%22%6d%61%69%6c%74%6f%3a%75%6e%69%63%6f%64%65%40%65%78%61%6d%70%6c%65%2e%63%6f%6d%5c%22%3e%c3%ba%6e%69%63%6f%64%65%3c%5c%2f%61%3e%27%29%3b'))</script>", snippet assert snippet.html_safe end @@ -382,8 +400,8 @@ class UrlHelperTest < ActiveSupport::TestCase assert_dom_equal "<a href=\"mailto:%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d\">me(at)domain.com</a>", mail_to("me@domain.com", nil, :encode => "hex", :replace_at => "(at)") assert_dom_equal "<a href=\"mailto:%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d\">My email</a>", mail_to("me@domain.com", "My email", :encode => "hex", :replace_at => "(at)") assert_dom_equal "<a href=\"mailto:%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d\">me(at)domain(dot)com</a>", mail_to("me@domain.com", nil, :encode => "hex", :replace_at => "(at)", :replace_dot => "(dot)") - assert_dom_equal "<script type=\"text/javascript\">eval(decodeURIComponent('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%22%6d%61%69%6c%74%6f%3a%6d%65%40%64%6f%6d%61%69%6e%2e%63%6f%6d%22%3e%4d%79%20%65%6d%61%69%6c%3c%2f%61%3e%27%29%3b'))</script>", mail_to("me@domain.com", "My email", :encode => "javascript", :replace_at => "(at)", :replace_dot => "(dot)") - assert_dom_equal "<script type=\"text/javascript\">eval(decodeURIComponent('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%22%6d%61%69%6c%74%6f%3a%6d%65%40%64%6f%6d%61%69%6e%2e%63%6f%6d%22%3e%6d%65%28%61%74%29%64%6f%6d%61%69%6e%28%64%6f%74%29%63%6f%6d%3c%2f%61%3e%27%29%3b'))</script>", mail_to("me@domain.com", nil, :encode => "javascript", :replace_at => "(at)", :replace_dot => "(dot)") + assert_dom_equal "<script type=\"text/javascript\">eval(decodeURIComponent('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%5c%22%6d%61%69%6c%74%6f%3a%6d%65%40%64%6f%6d%61%69%6e%2e%63%6f%6d%5c%22%3e%4d%79%20%65%6d%61%69%6c%3c%5c%2f%61%3e%27%29%3b'))</script>", mail_to("me@domain.com", "My email", :encode => "javascript", :replace_at => "(at)", :replace_dot => "(dot)") + assert_dom_equal "<script type=\"text/javascript\">eval(decodeURIComponent('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%5c%22%6d%61%69%6c%74%6f%3a%6d%65%40%64%6f%6d%61%69%6e%2e%63%6f%6d%5c%22%3e%6d%65%28%61%74%29%64%6f%6d%61%69%6e%28%64%6f%74%29%63%6f%6d%3c%5c%2f%61%3e%27%29%3b'))</script>", mail_to("me@domain.com", nil, :encode => "javascript", :replace_at => "(at)", :replace_dot => "(dot)") end # TODO: button_to looks at this ... why? @@ -401,7 +419,7 @@ end class UrlHelperControllerTest < ActionController::TestCase class UrlHelperController < ActionController::Base - test_routes do |map| + test_routes do match 'url_helper_controller_test/url_helper/show/:id', :to => 'url_helper_controller_test/url_helper#show', :as => :show @@ -414,12 +432,15 @@ class UrlHelperControllerTest < ActionController::TestCase :to => 'url_helper_controller_test/url_helper#show_named_route', :as => :show_named_route - map.connect ":controller/:action/:id" - # match "/:controller(/:action(/:id))" + match "/:controller(/:action(/:id))" match 'url_helper_controller_test/url_helper/normalize_recall_params', :to => UrlHelperController.action(:normalize_recall), :as => :normalize_recall_params + + match '/url_helper_controller_test/url_helper/override_url_helper/default', + :to => 'url_helper_controller_test/url_helper#override_url_helper', + :as => :override_url_helper end def show @@ -455,6 +476,15 @@ class UrlHelperControllerTest < ActionController::TestCase end def rescue_action(e) raise e end + + def override_url_helper + render :inline => '<%= override_url_helper_path %>' + end + + def override_url_helper_path + '/url_helper_controller_test/url_helper/override_url_helper/override' + end + helper_method :override_url_helper_path end tests UrlHelperController @@ -514,6 +544,11 @@ class UrlHelperControllerTest < ActionController::TestCase get :show, :name => '123' assert_equal 'ok', @response.body end + + def test_url_helper_can_be_overriden + get :override_url_helper + assert_equal '/url_helper_controller_test/url_helper/override_url_helper/override', @response.body + end end class TasksController < ActionController::Base @@ -555,24 +590,6 @@ class LinkToUnlessCurrentWithControllerTest < ActionController::TestCase end end -class Workshop - extend ActiveModel::Naming - include ActiveModel::Conversion - attr_accessor :id - - def initialize(id) - @id = id - end - - def persisted? - id.present? - end - - def to_s - id.to_s - end -end - class Session extend ActiveModel::Naming include ActiveModel::Conversion @@ -654,10 +671,10 @@ class PolymorphicControllerTest < ActionController::TestCase get :index, :workshop_id => 1 assert_equal "/workshops/1/sessions\n<a href=\"/workshops/1/sessions\">Session</a>", @response.body end - + def test_existing_nested_resource @controller = SessionsController.new - + get :show, :workshop_id => 1, :id => 1 assert_equal "/workshops/1/sessions/1\n<a href=\"/workshops/1/sessions/1\">Session</a>", @response.body end |