diff options
Diffstat (limited to 'actionpack')
66 files changed, 1107 insertions, 390 deletions
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index dc152b7a6e..b819f2e613 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,9 +1,22 @@ ## Rails 4.0.0 (unreleased) ## +* `respond_to` and `respond_with` now raise ActionController::UnknownFormat instead + of directly returning head 406. The exception is rescued and converted to 406 + in the exception handling middleware. *Steven Soroka* + +* Allows `assert_redirected_to` to match against a regular expression. *Andy Lindeman* + +* Add backtrace to development routing error page. *Richard Schneeman* + +* Replace `include_seconds` boolean argument with `:include_seconds => true` option + in `distance_of_time_in_words` and `time_ago_in_words` signature. *Dmitriy Kiriyenko* + +* Remove `button_to_function` and `link_to_function` helpers. *Rafael Mendonça França* + * Make current object and counter (when it applies) variables accessible when rendering templates with :object / :collection. *Carlos Antonio da Silva* -* JSONP now uses mimetype application/javascript instead of application/json *omjokine* +* JSONP now uses mimetype application/javascript instead of application/json. *omjokine* * Allow to lazy load `default_form_builder` by passing a `String` instead of a constant. *Piotr Sarnacki* diff --git a/actionpack/Rakefile b/actionpack/Rakefile index bb1e704767..17d95bfd1d 100755..100644 --- a/actionpack/Rakefile +++ b/actionpack/Rakefile @@ -1,4 +1,3 @@ -#!/usr/bin/env rake require 'rake/testtask' require 'rake/packagetask' require 'rubygems/package_task' diff --git a/actionpack/lib/action_controller/caching/actions.rb b/actionpack/lib/action_controller/caching/actions.rb index 59ec197347..80901b8bf3 100644 --- a/actionpack/lib/action_controller/caching/actions.rb +++ b/actionpack/lib/action_controller/caching/actions.rb @@ -133,6 +133,8 @@ module ActionController #:nodoc: end def filter(controller) + cache_layout = @cache_layout.respond_to?(:call) ? @cache_layout.call(controller) : @cache_layout + path_options = if @cache_path.respond_to?(:call) controller.instance_exec(controller, &@cache_path) else @@ -144,13 +146,13 @@ module ActionController #:nodoc: body = controller.read_fragment(cache_path.path, @store_options) unless body - controller.action_has_layout = false unless @cache_layout + controller.action_has_layout = false unless cache_layout yield controller.action_has_layout = true body = controller._save_fragment(cache_path.path, @store_options) end - body = controller.render_to_string(:text => body, :layout => true) unless @cache_layout + body = controller.render_to_string(:text => body, :layout => true) unless cache_layout controller.response_body = body controller.content_type = Mime[cache_path.extension || :html] diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb index 369741fb12..379ff97048 100644 --- a/actionpack/lib/action_controller/metal/data_streaming.rb +++ b/actionpack/lib/action_controller/metal/data_streaming.rb @@ -8,15 +8,13 @@ module ActionController #:nodoc: include ActionController::Rendering - DEFAULT_SEND_FILE_OPTIONS = { - :type => 'application/octet-stream'.freeze, - :disposition => 'attachment'.freeze, - }.freeze + DEFAULT_SEND_FILE_TYPE = 'application/octet-stream'.freeze #:nodoc: + DEFAULT_SEND_FILE_DISPOSITION = 'attachment'.freeze #:nodoc: protected # Sends the file. This uses a server-appropriate method (such as X-Sendfile) # via the Rack::Sendfile middleware. The header to use is set via - # config.action_dispatch.x_sendfile_header. + # +config.action_dispatch.x_sendfile_header+. # Your server can also configure this for you by setting the X-Sendfile-Type header. # # Be careful to sanitize the path parameter if it is coming from a web @@ -127,7 +125,7 @@ module ActionController #:nodoc: # # See +send_file+ for more information on HTTP Content-* headers and caching. def send_data(data, options = {}) #:doc: - send_file_headers! options.dup + send_file_headers! options render options.slice(:status, :content_type).merge(:text => data) end @@ -135,15 +133,8 @@ module ActionController #:nodoc: def send_file_headers!(options) type_provided = options.has_key?(:type) - options.update(DEFAULT_SEND_FILE_OPTIONS.merge(options)) - [:type, :disposition].each do |arg| - raise ArgumentError, ":#{arg} option required" if options[arg].nil? - end - - disposition = options[:disposition] - disposition += %(; filename="#{options[:filename]}") if options[:filename] - - content_type = options[:type] + content_type = options.fetch(:type, DEFAULT_SEND_FILE_TYPE) + raise ArgumentError, ":type option required" if content_type.nil? if content_type.is_a?(Symbol) extension = Mime[content_type] @@ -157,10 +148,13 @@ module ActionController #:nodoc: self.content_type = content_type end - headers.merge!( - 'Content-Disposition' => disposition, - 'Content-Transfer-Encoding' => 'binary' - ) + disposition = options.fetch(:disposition, DEFAULT_SEND_FILE_DISPOSITION) + unless disposition.nil? + disposition += %(; filename="#{options[:filename]}") if options[:filename] + headers['Content-Disposition'] = disposition + end + + headers['Content-Transfer-Encoding'] = 'binary' response.sending_file = true diff --git a/actionpack/lib/action_controller/metal/exceptions.rb b/actionpack/lib/action_controller/metal/exceptions.rb index 9a9db0fe5f..90648c37ad 100644 --- a/actionpack/lib/action_controller/metal/exceptions.rb +++ b/actionpack/lib/action_controller/metal/exceptions.rb @@ -38,4 +38,7 @@ module ActionController class UnknownHttpMethod < ActionControllerError #:nodoc: end + + class UnknownFormat < ActionControllerError #:nodoc: + end end diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb index a618533d09..5bdbde9ebb 100644 --- a/actionpack/lib/action_controller/metal/head.rb +++ b/actionpack/lib/action_controller/metal/head.rb @@ -27,8 +27,28 @@ module ActionController self.status = status self.location = url_for(location) if location - self.content_type = Mime[formats.first] if formats + + if include_content_headers?(self.status) + self.content_type = Mime[formats.first] if formats + else + headers.delete('Content-Type') + headers.delete('Content-Length') + end + self.response_body = " " end + + private + # :nodoc: + def include_content_headers?(status) + case status + when 100..199 + false + when 204, 205, 304 + false + else + true + end + end end end diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index f467b74256..7917926978 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -375,8 +375,7 @@ module ActionController #:nodoc: lookup_context.rendered_format = lookup_context.formats.first collector else - head :not_acceptable - nil + raise ActionController::UnknownFormat end end diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb index 17e2db74d4..1f52c164de 100644 --- a/actionpack/lib/action_controller/metal/params_wrapper.rb +++ b/actionpack/lib/action_controller/metal/params_wrapper.rb @@ -48,7 +48,7 @@ module ActionController # method attribute_names. # # If you're going to pass the parameters to an +ActiveModel+ object (such as - # +User.new(params[:user])+), you might consider passing the model class to + # <tt>User.new(params[:user])</tt>), you might consider passing the model class to # the method instead. The +ParamsWrapper+ will actually try to determine the # list of attribute names from the model and only wrap those attributes: # @@ -166,8 +166,9 @@ module ActionController unless options[:include] || options[:exclude] model ||= _default_wrap_model - if model.respond_to?(:accessible_attributes) && model.accessible_attributes.present? - options[:include] = model.accessible_attributes.to_a + role = options.fetch(:as, :default) + if model.respond_to?(:accessible_attributes) && model.accessible_attributes(role).present? + options[:include] = model.accessible_attributes(role).to_a elsif model.respond_to?(:attribute_names) && model.attribute_names.present? options[:include] = model.attribute_names end diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index 35ded8a56c..5e7bd44562 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -45,6 +45,16 @@ module ActionController # integer, or a symbol representing the downcased, underscored and symbolized description. # Note that the status code must be a 3xx HTTP code, or redirection will not occur. # + # If you are using XHR requests other than GET or POST and redirecting after the + # request then some browsers will follow the redirect using the original request + # method. This may lead to undesirable behavior such as a double DELETE. To work + # around this you can return a <tt>303 See Other</tt> status code which will be + # followed using a GET request. + # + # Examples: + # redirect_to posts_url, :status => :see_other + # redirect_to :action => 'index', :status => 303 + # # It is also possible to assign a flash message as part of the redirection. There are two special accessors for the commonly used flash names # +alert+ and +notice+ as well as a general purpose +flash+ bucket. # diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index d2581296cf..ad02375f12 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -20,7 +20,12 @@ module ActionController ActiveSupport::Notifications.subscribe("render_template.action_view") do |name, start, finish, id, payload| path = payload[:layout] - @layouts[path] += 1 + if path + @layouts[path] += 1 + if path =~ /^layouts\/(.*)/ + @layouts[$1] += 1 + end + end end ActiveSupport::Notifications.subscribe("!render_template.action_view") do |name, start, finish, id, payload| @@ -59,6 +64,15 @@ module ActionController # # assert that the exact template "admin/posts/new" was rendered # assert_template %r{\Aadmin/posts/new\Z} # + # # assert that the layout 'admin' was rendered + # assert_template :layout => 'admin' + # assert_template :layout => 'layouts/admin' + # assert_template :layout => :admin + # + # # assert that no layout was rendered + # assert_template :layout => nil + # assert_template :layout => false + # # # assert that the "_customer" partial was rendered twice # assert_template :partial => '_customer', :count => 2 # @@ -90,16 +104,17 @@ module ActionController end end when Hash - if expected_layout = options[:layout] + if options.key?(:layout) + expected_layout = options[:layout] msg = message || sprintf("expecting layout <%s> but action rendered <%s>", expected_layout, @layouts.keys) case expected_layout - when String - assert_includes @layouts.keys, expected_layout, msg + when String, Symbol + assert_includes @layouts.keys, expected_layout.to_s, msg when Regexp assert(@layouts.keys.any? {|l| l =~ expected_layout }, msg) - when nil + when nil, false assert(@layouts.empty?, msg) end end @@ -120,7 +135,7 @@ module ActionController options[:partial], @partials.keys) assert_includes @partials, expected_partial, msg end - else + elsif options.key?(:partial) assert @partials.empty?, "Expected no partials to be rendered" end @@ -479,7 +494,6 @@ module ActionController @request.session.update(session) if session @request.session["flash"] = @request.flash.update(flash || {}) - @request.session["flash"].sweep @controller.request = @request build_request_uri(action, parameters) diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb index 040b51e040..a3bb25f75a 100644 --- a/actionpack/lib/action_dispatch/http/headers.rb +++ b/actionpack/lib/action_dispatch/http/headers.rb @@ -14,17 +14,18 @@ module ActionDispatch end def [](header_name) - if include?(header_name) - super - else - super(env_name(header_name)) - end + super env_name(header_name) + end + + def fetch(header_name, default=nil, &block) + super env_name(header_name), default, &block end private - # Converts a HTTP header name to an environment variable name. + # Converts a HTTP header name to an environment variable name if it is + # not contained within the headers hash. def env_name(header_name) - @@env_cache[header_name] + include?(header_name) ? header_name : @@env_cache[header_name] end end end diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index 26f4c16218..0eaae80461 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -179,11 +179,11 @@ module Mime end end - # input: 'text' - # returned value: [Mime::JSON, Mime::XML, Mime::ICS, Mime::HTML, Mime::CSS, Mime::CSV, Mime::JS, Mime::YAML, Mime::TEXT] + # For an input of <tt>'text'</tt>, returns <tt>[Mime::JSON, Mime::XML, Mime::ICS, + # Mime::HTML, Mime::CSS, Mime::CSV, Mime::JS, Mime::YAML, Mime::TEXT]</tt>. # - # input: 'application' - # returned value: [Mime::HTML, Mime::JS, Mime::XML, Mime::YAML, Mime::ATOM, Mime::JSON, Mime::RSS, Mime::URL_ENCODED_FORM] + # For an input of <tt>'application'</tt>, returns <tt>[Mime::HTML, Mime::JS, + # Mime::XML, Mime::YAML, Mime::ATOM, Mime::JSON, Mime::RSS, Mime::URL_ENCODED_FORM]</tt>. def parse_data_with_trailing_star(input) Mime::SET.select { |m| m =~ input } end @@ -192,7 +192,7 @@ module Mime # # Usage: # - # Mime::Type.unregister(:mobile) + # Mime::Type.unregister(:mobile) def unregister(symbol) symbol = symbol.to_s.upcase mime = Mime.const_get(symbol) diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index d9b63faf5e..bcfd0b0d00 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -35,6 +35,10 @@ module ActionDispatch @env["action_dispatch.request.path_parameters"] ||= {} end + def reset_parameters #:nodoc: + @env.delete("action_dispatch.request.parameters") + end + private # TODO: Validate that the characters are UTF-8. If they aren't, diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 796e0dbc45..9748956052 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -6,6 +6,7 @@ require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/string/access' require 'active_support/inflector' require 'action_dispatch/http/headers' +require 'action_dispatch/request/session' require 'action_controller/metal/exceptions' module ActionDispatch @@ -220,11 +221,11 @@ module ActionDispatch end def session=(session) #:nodoc: - @env['rack.session'] = session + Session.set @env, session end def session_options=(options) - @env['rack.session.options'] = options + Session::Options.set @env, options end # Override Rack's GET method to support indifferent access diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 0c717c8503..771f075275 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -2,7 +2,7 @@ require 'active_support/core_ext/object/blank' require 'active_support/core_ext/hash/keys' module ActionDispatch - class Request + class Request < Rack::Request def cookie_jar env['action_dispatch.cookies'] ||= Cookies::CookieJar.build(self) end @@ -26,9 +26,9 @@ module ActionDispatch # # 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. + # # Sets a signed cookie, which prevents users 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. + # # It can be read using the signed method <tt>cookies.signed[:key]</tt> # cookies.signed[:user_id] = current_user.id # # # Sets a "permanent" cookie (which expires in 20 years from now). @@ -39,9 +39,10 @@ module ActionDispatch # # Examples for reading: # - # cookies[:user_name] # => "david" - # cookies.size # => 2 - # cookies[:lat_lon] # => [47.68, -122.37] + # cookies[:user_name] # => "david" + # cookies.size # => 2 + # cookies[:lat_lon] # => [47.68, -122.37] + # cookies.signed[:login] # => "XJ-122" # # Example for deleting: # @@ -82,7 +83,7 @@ module ActionDispatch TOKEN_KEY = "action_dispatch.secret_token".freeze # Raised when storing more than 4K of session data. - class CookieOverflow < StandardError; end + CookieOverflow = Class.new StandardError class CookieJar #:nodoc: include Enumerable @@ -117,7 +118,6 @@ module ActionDispatch @delete_cookies = {} @host = host @secure = secure - @closed = false @cookies = {} end @@ -154,7 +154,7 @@ module ActionDispatch 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] } + options[:domain] = options[:domain].find {|domain| @host.include? domain.sub(/^\./, '') } end end @@ -169,12 +169,14 @@ module ActionDispatch options = { :value => value } end - @cookies[key.to_s] = value - handle_options(options) - @set_cookies[key.to_s] = options - @delete_cookies.delete(key.to_s) + if @cookies[key.to_s] != value or options[:expires] + @cookies[key.to_s] = value + @set_cookies[key.to_s] = options + @delete_cookies.delete(key.to_s) + end + value end @@ -182,8 +184,9 @@ module ActionDispatch # and setting its expiration date into the past. Like <tt>[]=</tt>, you can pass in # an options hash to delete cookies with extra data such as a <tt>:path</tt>. def delete(key, options = {}) - options.symbolize_keys! + return unless @cookies.has_key? key.to_s + options.symbolize_keys! handle_options(options) value = @cookies.delete(key.to_s) @@ -225,7 +228,7 @@ module ActionDispatch # cookie was tampered with by the user (or a 3rd party), an ActiveSupport::MessageVerifier::InvalidSignature exception will # be raised. # - # This jar requires that you set a suitable secret for the verification on your app's config.secret_token. + # This jar requires that you set a suitable secret for the verification on your app's +config.secret_token+. # # Example: # @@ -339,7 +342,6 @@ module ActionDispatch end def call(env) - cookie_jar = nil status, headers, body = @app.call(env) if cookie_jar = env['action_dispatch.cookies'] diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb index c0532c80c4..982f6641bf 100644 --- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb +++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb @@ -10,6 +10,7 @@ module ActionDispatch 'AbstractController::ActionNotFound' => :not_found, 'ActionController::MethodNotAllowed' => :method_not_allowed, 'ActionController::NotImplemented' => :not_implemented, + 'ActionController::UnknownFormat' => :not_acceptable, 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity ) diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index c92c91df65..17776c2356 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -1,10 +1,10 @@ module ActionDispatch - class Request + class Request < Rack::Request # Access the contents of the flash. Use <tt>flash["notice"]</tt> to # read a notice you put there or <tt>flash["notice"] = "hello"</tt> # to put a new one. def flash - @env[Flash::KEY] ||= (session["flash"] || Flash::FlashHash.new) + @env[Flash::KEY] ||= (session["flash"] || Flash::FlashHash.new).tap(&:sweep) end end @@ -79,7 +79,6 @@ module ActionDispatch def initialize #:nodoc: @discard = Set.new - @closed = false @flashes = {} @now = nil end @@ -217,13 +216,9 @@ module ActionDispatch end def call(env) - if (session = env['rack.session']) && (flash = session['flash']) - flash.sweep - end - @app.call(env) ensure - session = env['rack.session'] || {} + session = Request::Session.find(env) || {} flash_hash = env[KEY] if flash_hash @@ -237,7 +232,8 @@ module ActionDispatch env[KEY] = new_hash end - if session.key?('flash') && session['flash'].empty? + if (!session.respond_to?(:loaded?) || session.loaded?) && # (reset_session uses {}, which doesn't implement #loaded?) + session.key?('flash') && session['flash'].empty? session.delete('flash') end end diff --git a/actionpack/lib/action_dispatch/middleware/reloader.rb b/actionpack/lib/action_dispatch/middleware/reloader.rb index a0388e0e13..2f6968eb2e 100644 --- a/actionpack/lib/action_dispatch/middleware/reloader.rb +++ b/actionpack/lib/action_dispatch/middleware/reloader.rb @@ -18,10 +18,10 @@ module ActionDispatch # 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 + # 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. + # middleware stack, but are executed only when <tt>ActionDispatch::Reloader.prepare!</tt> + # or <tt>ActionDispatch::Reloader.cleanup!</tt> are called manually. # class Reloader include ActiveSupport::Callbacks diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb index 6a8e690d18..64159fa8e7 100644 --- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb @@ -2,26 +2,23 @@ require 'rack/utils' require 'rack/request' require 'rack/session/abstract/id' require 'action_dispatch/middleware/cookies' +require 'action_dispatch/request/session' require 'active_support/core_ext/object/blank' module ActionDispatch module Session class SessionRestoreError < StandardError #:nodoc: - end + attr_reader :original_exception + + def initialize(const_error) + @original_exception = const_error - 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 + super("Session contains objects whose class definition isn't available.\n" + + "Remember to require the classes for all objects kept in the session.\n" + + "(Original exception: #{const_error.message} [#{const_error.class}])\n") end end - ::Rack::Session::Abstract::SessionHash.send :include, DestroyableSession - module Compatibility def initialize(app, options = {}) options[:key] ||= '_session_id' @@ -58,11 +55,8 @@ module ActionDispatch 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.\n" + - "Remember to require the classes for all objects kept in the session.\n" + - "(Original exception: #{const_error.message} [#{const_error.class}])\n" + rescue LoadError, NameError => e + raise ActionDispatch::Session::SessionRestoreError, e, e.backtrace end retry else @@ -71,9 +65,27 @@ module ActionDispatch end end + module SessionObject # :nodoc: + def prepare_session(env) + Request::Session.create(self, env, @default_options) + end + + def loaded_session?(session) + !session.is_a?(Request::Session) || session.loaded? + end + end + class AbstractStore < Rack::Session::Abstract::ID include Compatibility include StaleSessionCheck + include SessionObject + + private + + def set_cookie(env, session_id, cookie) + request = ActionDispatch::Request.new(env) + request.cookie_jar[key] = cookie + 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 a4866f5a8f..dbcf703ec3 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -43,6 +43,7 @@ module ActionDispatch class CookieStore < Rack::Session::Cookie include Compatibility include StaleSessionCheck + include SessionObject private 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 4dd9a946c2..38a737cd2b 100644 --- a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb @@ -6,6 +6,7 @@ module ActionDispatch class MemCacheStore < Rack::Session::Memcache include Compatibility include StaleSessionCheck + include SessionObject def initialize(app, options = {}) require 'memcache' 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 0c5bafa666..823f5d25b6 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 @@ -12,8 +12,8 @@ request_dump = clean_params.empty? ? 'None' : clean_params.inspect.gsub(',', ",\n") - def debug_hash(hash) - hash.sort_by { |k, v| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n") + def debug_hash(object) + object.to_hash.sort_by { |k, v| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n") end unless self.class.method_defined?(:debug_hash) %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb index f06c07daa5..177d383e94 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb @@ -12,4 +12,6 @@ <% end %> <p> Try running <code>rake routes</code> for more information on available routes. -</p>
\ No newline at end of file +</p> + +<%= render :template => "rescues/_trace" %> diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb new file mode 100644 index 0000000000..4ad7071820 --- /dev/null +++ b/actionpack/lib/action_dispatch/request/session.rb @@ -0,0 +1,166 @@ +require 'rack/session/abstract/id' + +module ActionDispatch + class Request < Rack::Request + # SessionHash is responsible to lazily load the session from store. + class Session # :nodoc: + ENV_SESSION_KEY = Rack::Session::Abstract::ENV_SESSION_KEY # :nodoc: + ENV_SESSION_OPTIONS_KEY = Rack::Session::Abstract::ENV_SESSION_OPTIONS_KEY # :nodoc: + + def self.create(store, env, default_options) + session_was = find env + session = Request::Session.new(store, env) + session.merge! session_was if session_was + + set(env, session) + Options.set(env, Request::Session::Options.new(store, env, default_options)) + session + end + + def self.find(env) + env[ENV_SESSION_KEY] + end + + def self.set(env, session) + env[ENV_SESSION_KEY] = session + end + + class Options #:nodoc: + def self.set(env, options) + env[ENV_SESSION_OPTIONS_KEY] = options + end + + def self.find(env) + env[ENV_SESSION_OPTIONS_KEY] + end + + def initialize(by, env, default_options) + @by = by + @env = env + @delegate = default_options.dup + end + + def [](key) + if key == :id + @delegate.fetch(key) { + @delegate[:id] = @by.send(:extract_session_id, @env) + } + else + @delegate[key] + end + end + + def []=(k,v); @delegate[k] = v; end + def to_hash; @delegate.dup; end + def values_at(*args); @delegate.values_at(*args); end + end + + def initialize(by, env) + @by = by + @env = env + @delegate = {} + @loaded = false + @exists = nil # we haven't checked yet + end + + def options + Options.find @env + end + + def destroy + clear + options = self.options || {} + @by.send(:destroy_session, @env, options[:id], options) + options[:id] = nil + @loaded = false + end + + def [](key) + load_for_read! + @delegate[key.to_s] + end + + def has_key?(key) + load_for_read! + @delegate.key?(key.to_s) + end + alias :key? :has_key? + alias :include? :has_key? + + def []=(key, value) + load_for_write! + @delegate[key.to_s] = value + end + + def clear + load_for_write! + @delegate.clear + end + + def to_hash + load_for_read! + @delegate.dup.delete_if { |_,v| v.nil? } + end + + def update(hash) + load_for_write! + @delegate.update stringify_keys(hash) + end + + def delete(key) + load_for_write! + @delegate.delete key.to_s + end + + def inspect + if loaded? + super + else + "#<#{self.class}:0x#{(object_id << 1).to_s(16)} not yet loaded>" + end + end + + def exists? + return @exists unless @exists.nil? + @exists = @by.send(:session_exists?, @env) + end + + def loaded? + @loaded + end + + def empty? + load_for_read! + @delegate.empty? + end + + def merge!(other) + load_for_write! + @delegate.merge!(other) + end + + private + + def load_for_read! + load! if !loaded? && exists? + end + + def load_for_write! + load! unless loaded? + end + + def load! + id, session = @by.load_session @env + options[:id] = id + @delegate.replace(stringify_keys(session)) + @loaded = true + end + + def stringify_keys(other) + other.each_with_object({}) { |(key, value), hash| + hash[key.to_s] = value + } + end + end + end +end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 1a1a054985..2a7d540517 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -35,6 +35,8 @@ module ActionDispatch } return true + ensure + req.reset_parameters end def call(env) @@ -533,7 +535,8 @@ module ActionDispatch private def map_method(method, args, &block) options = args.extract_options! - options[:via] = method + options[:via] = method + options[:path] ||= args.first if args.first.is_a?(String) match(*args, options, &block) self end @@ -1507,7 +1510,7 @@ module ActionDispatch prefix = shallow_scoping? ? "#{@scope[:shallow_path]}/#{parent_resource.path}/:id" : @scope[:path] - path = if canonical_action?(action, path.blank?) + if canonical_action?(action, path.blank?) prefix.to_s else "#{prefix}/#{action_path(action, path)}" diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb index 013cf93dbc..8fde667108 100644 --- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb +++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb @@ -43,16 +43,14 @@ module ActionDispatch # edit_polymorphic_path(@post) # => "/posts/1/edit" # polymorphic_path(@post, :format => :pdf) # => "/posts/1.pdf" # - # == Using with mounted engines + # == Usage 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: + # If you are using a mounted engine and you need to use a polymorphic_url + # pointing at the engine's routes, pass in the engine's route proxy as the first + # argument to the method. For example: # - # For example: - # - # polymorphic_url([blog, @post]) # it will call blog.post_path(@post) - # form_for([blog, @post]) # => "/blog/posts/1 + # polymorphic_url([blog, @post]) # calls 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 diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb index f4084d9f4f..95c588c00a 100644 --- a/actionpack/lib/action_dispatch/routing/redirection.rb +++ b/actionpack/lib/action_dispatch/routing/redirection.rb @@ -1,4 +1,6 @@ require 'action_dispatch/http/request' +require 'active_support/core_ext/uri' +require 'active_support/core_ext/array/extract_options' require 'rack/utils' module ActionDispatch @@ -47,8 +49,17 @@ module ActionDispatch :params => request.query_parameters }.merge options + if !params.empty? && url_options[:path].match(/%\{\w*\}/) + url_options[:path] = (url_options[:path] % escape_path(params)) + end + ActionDispatch::Http::URL.url_for url_options end + + private + def escape_path(params) + Hash[params.map{ |k,v| [k, URI.parser.escape(v)] }] + end end module Redirection @@ -89,7 +100,7 @@ module ActionDispatch # match 'accounts/:name' => redirect(SubdomainRedirector.new('api')) # def redirect(*args, &block) - options = args.last.is_a?(Hash) ? args.pop : {} + options = args.extract_options! status = options.delete(:status) || 301 return OptionRedirect.new(status, options) if options.any? diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 7a7810a95c..8fc0f283fc 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -99,12 +99,12 @@ module ActionDispatch @module = Module.new do protected - def handle_positional_args(args, options, route) + def handle_positional_args(args, options, segment_keys) inner_options = args.extract_options! result = options.dup if args.any? - keys = route.segment_keys + keys = segment_keys if args.size < keys.size - 1 # take format into account keys -= self.url_options.keys if self.respond_to?(:url_options) keys -= options.keys @@ -161,34 +161,13 @@ module ActionDispatch end end - def hash_access_name(name, only_path) - if only_path - :"hash_for_#{name}_path" - else - :"hash_for_#{name}_url" - end - end - def define_named_route_methods(name, route) [true, false].each do |only_path| hash = route.defaults.merge(:use_route => name, :only_path => only_path) - define_hash_access route, name, hash define_url_helper route, name, hash end end - def define_hash_access(route, name, options) - selector = hash_access_name(name, options[:only_path]) - - @module.module_eval do - redefine_method(selector) do |*args| - self.handle_positional_args(args, options, route) - end - protected selector - end - helpers << selector - end - # Create a url helper allowing ordered parameters to be associated # with corresponding dynamic segments, so you can do: # @@ -204,36 +183,27 @@ module ActionDispatch # def define_url_helper(route, name, options) selector = url_helper_name(name, options[:only_path]) - hash_access_method = hash_access_name(name, options[:only_path]) - - if optimize_helper?(route) - @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1 - remove_possible_method :#{selector} - def #{selector}(*args) - if args.size == #{route.required_parts.size} && !args.last.is_a?(Hash) && optimize_routes_generation? - options = #{options.inspect}.merge!(url_options) - options[:path] = "#{optimized_helper(route)}" - ActionDispatch::Http::URL.url_for(options) - else - url_for(#{hash_access_method}(*args)) - end - end - END_EVAL - else - @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1 - remove_possible_method :#{selector} - def #{selector}(*args) - url_for(#{hash_access_method}(*args)) + + @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1 + remove_possible_method :#{selector} + def #{selector}(*args) + if #{optimize_helper?(route)} && args.size == #{route.required_parts.size} && !args.last.is_a?(Hash) && optimize_routes_generation? + options = #{options.inspect} + options.merge!(url_options) if respond_to?(:url_options) + options[:path] = "#{optimized_helper(route)}" + ActionDispatch::Http::URL.url_for(options) + else + url_for(handle_positional_args(args, #{options.inspect}, #{route.segment_keys.inspect})) end - END_EVAL - end + end + END_EVAL helpers << selector end # Clause check about when we need to generate an optimized helper. def optimize_helper?(route) #:nodoc: - route.ast.grep(Journey::Nodes::Star).empty? && route.requirements.except(:controller, :action).empty? + route.requirements.except(:controller, :action).empty? end # Generates the interpolation to be used in the optimized helper. @@ -245,7 +215,10 @@ module ActionDispatch end route.required_parts.each_with_index do |part, i| - string_route.gsub!(part.inspect, "\#{Journey::Router::Utils.escape_fragment(args[#{i}].to_param)}") + # Replace each route parameter + # e.g. :id for regular parameter or *path for globbing + # with ruby string interpolation code + string_route.gsub!(/(\*|:)#{part}/, "\#{Journey::Router::Utils.escape_fragment(args[#{i}].to_param)}") end string_route diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index d75bb1c2de..ee02f4b531 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -68,7 +68,7 @@ module ActionDispatch # This generates, among other things, the method <tt>users_path</tt>. By default, # this method is accessible from your controllers, views and mailers. If you need # to access this auto-generated method from other places (such as a model), then - # you can do that by including ActionController::UrlFor in your class: + # you can do that by including Rails.application.routes.url_helpers in your class: # # class User < ActiveRecord::Base # include Rails.application.routes.url_helpers diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb index 40d38c59d6..8f6fff5d32 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/response.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb @@ -53,15 +53,18 @@ module ActionDispatch # # assert that the redirection was to the url for @customer # assert_redirected_to @customer # + # # asserts that the redirection matches the regular expression + # assert_redirected_to %r(\Ahttp://example.org) + # def assert_redirected_to(options = {}, message=nil) assert_response(:redirect, message) - return true if options == @response.location + return true if options === @response.location redirect_is = normalize_argument_to_redirection(@response.location) redirect_expected = normalize_argument_to_redirection(options) message ||= "Expected response to be a redirect to <#{redirect_expected}> but was a redirect to <#{redirect_is}>" - assert_equal redirect_expected, redirect_is, message + assert_operator redirect_expected, :===, redirect_is, message end private @@ -71,17 +74,21 @@ module ActionDispatch end def normalize_argument_to_redirection(fragment) - case fragment - when %r{^\w[A-Za-z\d+.-]*:.*} - fragment - when String - @request.protocol + @request.host_with_port + fragment - when :back - raise RedirectBackError unless refer = @request.headers["Referer"] - refer - else - @controller.url_for(fragment) - end.delete("\0\r\n") + normalized = case fragment + when Regexp + fragment + when %r{^\w[A-Za-z\d+.-]*:.*} + fragment + when String + @request.protocol + @request.host_with_port + fragment + when :back + raise RedirectBackError unless refer = @request.headers["Referer"] + refer + else + @controller.url_for(fragment) + end + + normalized.respond_to?(:delete) ? normalized.delete("\0\r\n") : normalized 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 index dd4e9ae4cc..35f91cec18 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb @@ -1,5 +1,6 @@ require 'thread' require 'active_support/core_ext/file' +require 'active_support/core_ext/module/attribute_accessors' module ActionView module Helpers diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb index 60d4af178a..4bcb8b9718 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb @@ -113,7 +113,7 @@ module ActionView # == 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 + # 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: # diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb index 81f856feda..f0a593d2c1 100644 --- a/actionpack/lib/action_view/helpers/date_helper.rb +++ b/actionpack/lib/action_view/helpers/date_helper.rb @@ -19,7 +19,7 @@ module ActionView # of \date[month]. module DateHelper # Reports the approximate distance in time between two Time, Date or DateTime objects or integers as seconds. - # Set <tt>include_seconds</tt> to true if you want more detailed approximations when distance < 1 min, 29 secs. + # Pass <tt>:include_seconds => true</tt> if you want more detailed approximations when distance < 1 min, 29 secs. # Distances are reported based on the following table: # # 0 <-> 29 secs # => less than a minute @@ -29,14 +29,15 @@ module ActionView # 89 mins, 30 secs <-> 23 hrs, 59 mins, 29 secs # => about [2..24] hours # 23 hrs, 59 mins, 30 secs <-> 41 hrs, 59 mins, 29 secs # => 1 day # 41 hrs, 59 mins, 30 secs <-> 29 days, 23 hrs, 59 mins, 29 secs # => [2..29] days - # 29 days, 23 hrs, 59 mins, 30 secs <-> 59 days, 23 hrs, 59 mins, 29 secs # => about 1 month + # 29 days, 23 hrs, 59 mins, 30 secs <-> 44 days, 23 hrs, 59 mins, 29 secs # => about 1 month + # 44 days, 23 hrs, 59 mins, 30 secs <-> 59 days, 23 hrs, 59 mins, 29 secs # => about 2 months # 59 days, 23 hrs, 59 mins, 30 secs <-> 1 yr minus 1 sec # => [2..12] months # 1 yr <-> 1 yr, 3 months # => about 1 year # 1 yr, 3 months <-> 1 yr, 9 months # => over 1 year # 1 yr, 9 months <-> 2 yr minus 1 sec # => almost 2 years # 2 yrs <-> max time or date # => (same rules as 1 yr) # - # With <tt>include_seconds</tt> = true and the difference < 1 minute 29 seconds: + # With <tt>:include_seconds => true</tt> and the difference < 1 minute 29 seconds: # 0-4 secs # => less than 5 seconds # 5-9 secs # => less than 10 seconds # 10-19 secs # => less than 20 seconds @@ -46,28 +47,33 @@ module ActionView # # ==== Examples # from_time = Time.now - # distance_of_time_in_words(from_time, from_time + 50.minutes) # => about 1 hour - # distance_of_time_in_words(from_time, 50.minutes.from_now) # => about 1 hour - # distance_of_time_in_words(from_time, from_time + 15.seconds) # => less than a minute - # distance_of_time_in_words(from_time, from_time + 15.seconds, true) # => less than 20 seconds - # distance_of_time_in_words(from_time, 3.years.from_now) # => about 3 years - # distance_of_time_in_words(from_time, from_time + 60.hours) # => 3 days - # distance_of_time_in_words(from_time, from_time + 45.seconds, true) # => less than a minute - # distance_of_time_in_words(from_time, from_time - 45.seconds, true) # => less than a minute - # distance_of_time_in_words(from_time, 76.seconds.from_now) # => 1 minute - # distance_of_time_in_words(from_time, from_time + 1.year + 3.days) # => about 1 year - # distance_of_time_in_words(from_time, from_time + 3.years + 6.months) # => over 3 years + # distance_of_time_in_words(from_time, from_time + 50.minutes) # => about 1 hour + # distance_of_time_in_words(from_time, 50.minutes.from_now) # => about 1 hour + # distance_of_time_in_words(from_time, from_time + 15.seconds) # => less than a minute + # distance_of_time_in_words(from_time, from_time + 15.seconds, :include_seconds => true) # => less than 20 seconds + # distance_of_time_in_words(from_time, 3.years.from_now) # => about 3 years + # distance_of_time_in_words(from_time, from_time + 60.hours) # => 3 days + # distance_of_time_in_words(from_time, from_time + 45.seconds, :include_seconds => true) # => less than a minute + # distance_of_time_in_words(from_time, from_time - 45.seconds, :include_seconds => true) # => less than a minute + # distance_of_time_in_words(from_time, 76.seconds.from_now) # => 1 minute + # distance_of_time_in_words(from_time, from_time + 1.year + 3.days) # => about 1 year + # distance_of_time_in_words(from_time, from_time + 3.years + 6.months) # => over 3 years # distance_of_time_in_words(from_time, from_time + 4.years + 9.days + 30.minutes + 5.seconds) # => about 4 years # # to_time = Time.now + 6.years + 19.days - # distance_of_time_in_words(from_time, to_time, true) # => about 6 years - # distance_of_time_in_words(to_time, from_time, true) # => about 6 years - # distance_of_time_in_words(Time.now, Time.now) # => less than a minute + # distance_of_time_in_words(from_time, to_time, :include_seconds => true) # => about 6 years + # distance_of_time_in_words(to_time, from_time, :include_seconds => true) # => about 6 years + # distance_of_time_in_words(Time.now, Time.now) # => less than a minute # - # distance_of_time_in_words(70) # => 1 minute - # distance_of_time_in_words(60*60) # => about 1 hour - # - def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false, options = {}) + def distance_of_time_in_words(from_time, to_time = 0, include_seconds_or_options = {}, options = {}) + if include_seconds_or_options.is_a?(Hash) + options = include_seconds_or_options + else + ActiveSupport::Deprecation.warn "distance_of_time_in_words and time_ago_in_words now accept :include_seconds " + + "as a part of options hash, not a boolean argument", caller + options[:include_seconds] ||= !!include_seconds_or_options + end + from_time = from_time.to_time if from_time.respond_to?(:to_time) to_time = to_time.to_time if to_time.respond_to?(:to_time) from_time, to_time = to_time, from_time if from_time > to_time @@ -79,7 +85,7 @@ module ActionView when 0..1 return distance_in_minutes == 0 ? locale.t(:less_than_x_minutes, :count => 1) : - locale.t(:x_minutes, :count => distance_in_minutes) unless include_seconds + locale.t(:x_minutes, :count => distance_in_minutes) unless options[:include_seconds] case distance_in_seconds when 0..4 then locale.t :less_than_x_seconds, :count => 5 @@ -90,13 +96,18 @@ module ActionView else locale.t :x_minutes, :count => 1 end - when 2..44 then locale.t :x_minutes, :count => distance_in_minutes - when 45..89 then locale.t :about_x_hours, :count => 1 - when 90..1439 then locale.t :about_x_hours, :count => (distance_in_minutes.to_f / 60.0).round - when 1440..2519 then locale.t :x_days, :count => 1 - when 2520..43199 then locale.t :x_days, :count => (distance_in_minutes.to_f / 1440.0).round - when 43200..86399 then locale.t :about_x_months, :count => 1 - when 86400..525599 then locale.t :x_months, :count => (distance_in_minutes.to_f / 43200.0).round + when 2...45 then locale.t :x_minutes, :count => distance_in_minutes + when 45...90 then locale.t :about_x_hours, :count => 1 + # 90 mins up to 24 hours + when 90...1440 then locale.t :about_x_hours, :count => (distance_in_minutes.to_f / 60.0).round + # 24 hours up to 42 hours + when 1440...2520 then locale.t :x_days, :count => 1 + # 42 hours up to 30 days + when 2520...43200 then locale.t :x_days, :count => (distance_in_minutes.to_f / 1440.0).round + # 30 days up to 60 days + when 43200...86400 then locale.t :about_x_months, :count => (distance_in_minutes.to_f / 43200.0).round + # 60 days up to 365 days + when 86400...525600 then locale.t :x_months, :count => (distance_in_minutes.to_f / 43200.0).round else if from_time.acts_like?(:time) && to_time.acts_like?(:time) fyear = from_time.year @@ -130,15 +141,16 @@ module ActionView # Like <tt>distance_of_time_in_words</tt>, but where <tt>to_time</tt> is fixed to <tt>Time.now</tt>. # # ==== Examples - # time_ago_in_words(3.minutes.from_now) # => 3 minutes - # time_ago_in_words(Time.now - 15.hours) # => about 15 hours - # time_ago_in_words(Time.now) # => less than a minute + # time_ago_in_words(3.minutes.from_now) # => 3 minutes + # time_ago_in_words(Time.now - 15.hours) # => about 15 hours + # time_ago_in_words(Time.now) # => less than a minute + # time_ago_in_words(Time.now, :include_seconds => true) # => less than 5 seconds # # from_time = Time.now - 3.days - 14.minutes - 25.seconds # time_ago_in_words(from_time) # => 3 days # - def time_ago_in_words(from_time, include_seconds = false) - distance_of_time_in_words(from_time, Time.now, include_seconds) + def time_ago_in_words(from_time, include_seconds_or_options = {}) + distance_of_time_in_words(from_time, Time.now, include_seconds_or_options) end alias_method :distance_of_time_in_words_to_now, :time_ago_in_words @@ -1005,6 +1017,8 @@ module ActionView # Returns the separator for a given datetime component. def separator(type) + return "" if @options[:use_hidden] + case type when :year, :month, :day @options[:"discard_#{type}"] ? "" : @options[:date_separator] diff --git a/actionpack/lib/action_view/helpers/form_options_helper.rb b/actionpack/lib/action_view/helpers/form_options_helper.rb index d61c2bbee2..cafcd93f58 100644 --- a/actionpack/lib/action_view/helpers/form_options_helper.rb +++ b/actionpack/lib/action_view/helpers/form_options_helper.rb @@ -288,38 +288,55 @@ module ActionView # # Examples (call, result): # options_for_select([["Dollar", "$"], ["Kroner", "DKK"]]) - # <option value="$">Dollar</option>\n<option value="DKK">Kroner</option> + # # <option value="$">Dollar</option> + # # <option value="DKK">Kroner</option> # # options_for_select([ "VISA", "MasterCard" ], "MasterCard") - # <option>VISA</option>\n<option selected="selected">MasterCard</option> + # # <option>VISA</option> + # # <option selected="selected">MasterCard</option> # # options_for_select({ "Basic" => "$20", "Plus" => "$40" }, "$40") - # <option value="$20">Basic</option>\n<option value="$40" selected="selected">Plus</option> + # # <option value="$20">Basic</option> + # # <option value="$40" selected="selected">Plus</option> # # options_for_select([ "VISA", "MasterCard", "Discover" ], ["VISA", "Discover"]) - # <option selected="selected">VISA</option>\n<option>MasterCard</option>\n<option selected="selected">Discover</option> + # # <option selected="selected">VISA</option> + # # <option>MasterCard</option> + # # <option selected="selected">Discover</option> # # You can optionally provide html attributes as the last element of the array. # # Examples: # options_for_select([ "Denmark", ["USA", {:class => 'bold'}], "Sweden" ], ["USA", "Sweden"]) - # <option value="Denmark">Denmark</option>\n<option value="USA" class="bold" selected="selected">USA</option>\n<option value="Sweden" selected="selected">Sweden</option> + # # <option value="Denmark">Denmark</option> + # # <option value="USA" class="bold" selected="selected">USA</option> + # # <option value="Sweden" selected="selected">Sweden</option> # # options_for_select([["Dollar", "$", {:class => "bold"}], ["Kroner", "DKK", {:onclick => "alert('HI');"}]]) - # <option value="$" class="bold">Dollar</option>\n<option value="DKK" onclick="alert('HI');">Kroner</option> + # # <option value="$" class="bold">Dollar</option> + # # <option value="DKK" onclick="alert('HI');">Kroner</option> # # If you wish to specify disabled option tags, set +selected+ to be a hash, with <tt>:disabled</tt> being either a value # or array of values to be disabled. In this case, you can use <tt>:selected</tt> to specify selected option tags. # # Examples: # options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], :disabled => "Super Platinum") - # <option value="Free">Free</option>\n<option value="Basic">Basic</option>\n<option value="Advanced">Advanced</option>\n<option value="Super Platinum" disabled="disabled">Super Platinum</option> + # # <option value="Free">Free</option> + # # <option value="Basic">Basic</option> + # # <option value="Advanced">Advanced</option> + # # <option value="Super Platinum" disabled="disabled">Super Platinum</option> # # options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], :disabled => ["Advanced", "Super Platinum"]) - # <option value="Free">Free</option>\n<option value="Basic">Basic</option>\n<option value="Advanced" disabled="disabled">Advanced</option>\n<option value="Super Platinum" disabled="disabled">Super Platinum</option> + # # <option value="Free">Free</option> + # # <option value="Basic">Basic</option> + # # <option value="Advanced" disabled="disabled">Advanced</option> + # # <option value="Super Platinum" disabled="disabled">Super Platinum</option> # # options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], :selected => "Free", :disabled => "Super Platinum") - # <option value="Free" selected="selected">Free</option>\n<option value="Basic">Basic</option>\n<option value="Advanced">Advanced</option>\n<option value="Super Platinum" disabled="disabled">Super Platinum</option> + # # <option value="Free" selected="selected">Free</option> + # # <option value="Basic">Basic</option> + # # <option value="Advanced">Advanced</option> + # # <option value="Super Platinum" disabled="disabled">Super Platinum</option> # # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag. def options_for_select(container, selected = nil) diff --git a/actionpack/lib/action_view/helpers/form_tag_helper.rb b/actionpack/lib/action_view/helpers/form_tag_helper.rb index b5e0970612..0d824bd574 100644 --- a/actionpack/lib/action_view/helpers/form_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/form_tag_helper.rb @@ -45,7 +45,7 @@ module ActionView # # => <form action="/posts" method="post"> # # form_tag('/posts/1', :method => :put) - # # => <form action="/posts/1" method="put"> + # # => <form action="/posts/1" method="post"> ... <input name="_method" type="hidden" value="put" /> ... # # form_tag('/upload', :multipart => true) # # => <form action="/upload" method="post" enctype="multipart/form-data"> @@ -408,7 +408,7 @@ module ActionView # # => <input class="form_submit" name="commit" type="submit" /> # # submit_tag "Edit", :disable_with => "Editing...", :class => "edit_button" - # # => <input class="edit_button" data-disable_with="Editing..." name="commit" type="submit" value="Edit" /> + # # => <input class="edit_button" data-disable-with="Editing..." name="commit" type="submit" value="Edit" /> # # submit_tag "Save", :confirm => "Are you sure?" # # => <input name='commit' type='submit' value='Save' data-confirm="Are you sure?" /> diff --git a/actionpack/lib/action_view/helpers/javascript_helper.rb b/actionpack/lib/action_view/helpers/javascript_helper.rb index 5a869d6303..cc20518b93 100644 --- a/actionpack/lib/action_view/helpers/javascript_helper.rb +++ b/actionpack/lib/action_view/helpers/javascript_helper.rb @@ -15,7 +15,6 @@ module ActionView JS_ESCAPE_MAP["\342\200\250".force_encoding('UTF-8').encode!] = '
' JS_ESCAPE_MAP["\342\200\251".force_encoding('UTF-8').encode!] = '
' - # Escapes carriage returns and single and double quotes for JavaScript segments. # @@ -68,40 +67,6 @@ module ActionView def javascript_cdata_section(content) #:nodoc: "\n//#{cdata_section("\n#{content}\n//")}\n".html_safe end - - # Returns a button whose +onclick+ handler triggers the passed JavaScript. - # - # The helper receives a name, JavaScript code, and an optional hash of HTML options. The - # name is used as button label and the JavaScript code goes into its +onclick+ attribute. - # If +html_options+ has an <tt>:onclick</tt>, that one is put before +function+. - # - # button_to_function "Greeting", "alert('Hello world!')", :class => "ok" - # # => <input class="ok" onclick="alert('Hello world!');" type="button" value="Greeting" /> - # - def button_to_function(name, function=nil, html_options={}) - onclick = "#{"#{html_options[:onclick]}; " if html_options[:onclick]}#{function};" - - tag(:input, html_options.merge(:type => 'button', :value => name, :onclick => onclick)) - end - - # Returns a link whose +onclick+ handler triggers the passed JavaScript. - # - # The helper receives a name, JavaScript code, and an optional hash of HTML options. The - # name is used as the link text and the JavaScript code goes into the +onclick+ attribute. - # If +html_options+ has an <tt>:onclick</tt>, that one is put before +function+. Once all - # the JavaScript is set, the helper appends "; return false;". - # - # The +href+ attribute of the tag is set to "#" unless +html_options+ has one. - # - # link_to_function "Greeting", "alert('Hello world!')", :class => "nav_link" - # # => <a class="nav_link" href="#" onclick="alert('Hello world!'); return false;">Greeting</a> - # - def link_to_function(name, function, html_options={}) - onclick = "#{"#{html_options[:onclick]}; " if html_options[:onclick]}#{function}; return false;" - href = html_options[:href] || '#' - - content_tag(:a, name, html_options.merge(:href => href, :onclick => onclick)) - end end end end diff --git a/actionpack/lib/action_view/helpers/tag_helper.rb b/actionpack/lib/action_view/helpers/tag_helper.rb index 23777721f6..f7afa48256 100644 --- a/actionpack/lib/action_view/helpers/tag_helper.rb +++ b/actionpack/lib/action_view/helpers/tag_helper.rb @@ -109,8 +109,12 @@ module ActionView # # cdata_section(File.read("hello_world.txt")) # # => <![CDATA[<hello from a text file]]> + # + # cdata_section("hello]]>world") + # # => <![CDATA[hello]]]]><![CDATA[>world]]> def cdata_section(content) - "<![CDATA[#{content}]]>".html_safe + splitted = content.gsub(']]>', ']]]]><![CDATA[>') + "<![CDATA[#{splitted}]]>".html_safe end # Returns an escaped version of +html+ without affecting existing escaped entities. diff --git a/actionpack/lib/action_view/helpers/tags/collection_helpers.rb b/actionpack/lib/action_view/helpers/tags/collection_helpers.rb index 6a1479069f..4e33e79a36 100644 --- a/actionpack/lib/action_view/helpers/tags/collection_helpers.rb +++ b/actionpack/lib/action_view/helpers/tags/collection_helpers.rb @@ -49,7 +49,7 @@ module ActionView accept = if current_value.respond_to?(:call) current_value.call(item) else - Array(current_value).include?(value) + Array(current_value).map(&:to_s).include?(value.to_s) end if accept diff --git a/actionpack/lib/action_view/helpers/text_helper.rb b/actionpack/lib/action_view/helpers/text_helper.rb index 12bb162da2..fffc37ce9e 100644 --- a/actionpack/lib/action_view/helpers/text_helper.rb +++ b/actionpack/lib/action_view/helpers/text_helper.rb @@ -45,7 +45,7 @@ module ActionView # if logged_in # concat "Logged in!" # else - # concat link_to('login', :action => login) + # concat link_to('login', :action => :login) # end # # will either display "Logged in!" or a login link # %> diff --git a/actionpack/lib/action_view/helpers/translation_helper.rb b/actionpack/lib/action_view/helpers/translation_helper.rb index cc74eff53a..fd06bfa2a8 100644 --- a/actionpack/lib/action_view/helpers/translation_helper.rb +++ b/actionpack/lib/action_view/helpers/translation_helper.rb @@ -45,6 +45,7 @@ module ActionView # you know what kind of output to expect when you call translate in a template. def translate(key, options = {}) options.merge!(:rescue_format => :html) unless options.key?(:rescue_format) + options[:default] = wrap_translate_defaults(options[:default]) if options[:default] if html_safe_translation_key?(key) html_safe_options = options.dup options.except(*I18n::RESERVED_KEYS).each do |name, value| @@ -83,6 +84,21 @@ module ActionView def html_safe_translation_key?(key) key.to_s =~ /(\b|_|\.)html$/ end + + def wrap_translate_defaults(defaults) + new_defaults = [] + defaults = Array(defaults) + while key = defaults.shift + if key.is_a?(Symbol) + new_defaults << lambda { |_, options| translate key, options.merge(:default => defaults) } + break + else + new_defautls << key + end + end + + new_defaults + end end end end diff --git a/actionpack/lib/action_view/helpers/url_helper.rb b/actionpack/lib/action_view/helpers/url_helper.rb index d0f716cc80..1145f348c2 100644 --- a/actionpack/lib/action_view/helpers/url_helper.rb +++ b/actionpack/lib/action_view/helpers/url_helper.rb @@ -303,7 +303,10 @@ module ActionView # # <%= button_to "Create", :action => "create", :remote => true, :form => { "data-type" => "json" } %> # # => "<form method="post" action="/images/create" class="button_to" data-remote="true" data-type="json"> - # # <div><input value="Create" type="submit" /></div> + # # <div> + # # <input value="Create" type="submit" /> + # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/> + # # </div> # # </form>" # # @@ -312,7 +315,8 @@ module ActionView # # => "<form method="post" action="/images/delete/1" class="button_to"> # # <div> # # <input type="hidden" name="_method" value="delete" /> - # # <input data-confirm='Are you sure?' value="Delete" type="submit" /> + # # <input data-confirm='Are you sure?' value="Delete Image" type="submit" /> + # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/> # # </div> # # </form>" # @@ -323,6 +327,7 @@ module ActionView # # <div> # # <input name='_method' value='delete' type='hidden' /> # # <input value='Destroy' type='submit' disable_with='loading...' data-confirm='Are you sure?' /> + # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/> # # </div> # # </form>" # # diff --git a/actionpack/lib/action_view/renderer/partial_renderer.rb b/actionpack/lib/action_view/renderer/partial_renderer.rb index c5d5540510..87609fd5ff 100644 --- a/actionpack/lib/action_view/renderer/partial_renderer.rb +++ b/actionpack/lib/action_view/renderer/partial_renderer.rb @@ -343,8 +343,8 @@ module ActionView if String === partial && @variable.to_s !~ /^[a-z_][a-zA-Z_0-9]*$/ raise ArgumentError.new("The partial name (#{partial}) is not a valid Ruby identifier; " + - "make sure your partial name starts with a letter or underscore, " + - "and is followed by any combinations of letters, numbers, or underscores.") + "make sure your partial name starts with a lowercase letter or underscore, " + + "and is followed by any combination of letters, numbers and underscores.") end self diff --git a/actionpack/test/activerecord/active_record_store_test.rb b/actionpack/test/activerecord/active_record_store_test.rb index fceebe1858..275f25f597 100644 --- a/actionpack/test/activerecord/active_record_store_test.rb +++ b/actionpack/test/activerecord/active_record_store_test.rb @@ -254,6 +254,13 @@ class ActiveRecordStoreTest < ActionDispatch::IntegrationTest end end + def test_session_store_with_all_domains + with_test_route_set(:domain => :all) do + get '/set_session_value' + assert_response :success + end + end + private def with_test_route_set(options = {}) diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb index b121ca9481..9b0d4d0f4c 100644 --- a/actionpack/test/controller/action_pack_assertions_test.rb +++ b/actionpack/test/controller/action_pack_assertions_test.rb @@ -76,6 +76,11 @@ class ActionPackAssertionsController < ActionController::Base render "test/hello_world", :layout => "layouts/standard" end + def render_with_layout_and_partial + @variable_for_layout = nil + render "test/hello_world_with_partial", :layout => "layouts/standard" + end + def session_stuffing session['xmas'] = 'turkey' render :text => "ho ho ho" @@ -179,6 +184,9 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase assert_redirected_to 'http://test.host/route_two' end assert_raise(ActiveSupport::TestCase::Assertion) do + assert_redirected_to %r(^http://test.host/route_two) + end + assert_raise(ActiveSupport::TestCase::Assertion) do assert_redirected_to :controller => 'action_pack_assertions', :action => 'nothing', :id => 'two' end assert_raise(ActiveSupport::TestCase::Assertion) do @@ -212,6 +220,7 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase 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 assert_redirected_to "/action_pack_assertions/foo" + assert_redirected_to %r(/action_pack_assertions/foo) end end @@ -469,11 +478,43 @@ class AssertTemplateTest < ActionController::TestCase end end + def test_fails_expecting_no_layout + get :render_with_layout + assert_raise(ActiveSupport::TestCase::Assertion) do + assert_template :layout => nil + end + end + def test_passes_with_correct_layout get :render_with_layout assert_template :layout => "layouts/standard" end + def test_passes_with_layout_and_partial + get :render_with_layout_and_partial + assert_template :layout => "layouts/standard" + end + + def test_passed_with_no_layout + get :hello_world + assert_template :layout => nil + end + + def test_passed_with_no_layout_false + get :hello_world + assert_template :layout => false + end + + def test_passes_with_correct_layout_without_layouts_prefix + get :render_with_layout + assert_template :layout => "standard" + end + + def test_passes_with_correct_layout_symbol + get :render_with_layout + assert_template :layout => :standard + end + def test_assert_template_reset_between_requests get :hello_world assert_template 'test/hello_world' diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index e8546cb154..9efe328d62 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -236,6 +236,7 @@ class ActionCachingTestController < CachingController caches_action :with_layout caches_action :with_format_and_http_param, :cache_path => Proc.new { |c| { :key => 'value' } } caches_action :layout_false, :layout => false + caches_action :with_layout_proc_param, :layout => Proc.new { |c| c.params[:layout] } caches_action :record_not_found, :four_oh_four, :simple_runtime_error caches_action :streaming @@ -282,6 +283,7 @@ class ActionCachingTestController < CachingController alias_method :edit, :index alias_method :destroy, :index alias_method :layout_false, :with_layout + alias_method :with_layout_proc_param, :with_layout def expire expire_action :controller => 'action_caching_test', :action => 'index' @@ -403,11 +405,40 @@ class ActionCacheTest < ActionController::TestCase get :layout_false assert_response :success assert_not_equal cached_time, @response.body - body = body_to_string(read_fragment('hostname.com/action_caching_test/layout_false')) assert_equal cached_time, body end + def test_action_cache_with_layout_and_layout_cache_false_via_proc + get :with_layout_proc_param, :layout => false + assert_response :success + cached_time = content_to_cache + assert_not_equal cached_time, @response.body + assert fragment_exist?('hostname.com/action_caching_test/with_layout_proc_param') + reset! + + get :with_layout_proc_param, :layout => false + assert_response :success + assert_not_equal cached_time, @response.body + body = body_to_string(read_fragment('hostname.com/action_caching_test/with_layout_proc_param')) + assert_equal cached_time, body + end + + def test_action_cache_with_layout_and_layout_cache_true_via_proc + get :with_layout_proc_param, :layout => true + assert_response :success + cached_time = content_to_cache + assert_not_equal cached_time, @response.body + assert fragment_exist?('hostname.com/action_caching_test/with_layout_proc_param') + reset! + + get :with_layout_proc_param, :layout => true + assert_response :success + assert_not_equal cached_time, @response.body + body = body_to_string(read_fragment('hostname.com/action_caching_test/with_layout_proc_param')) + assert_equal @response.body, body + end + def test_action_cache_conditional_options @request.env['HTTP_ACCEPT'] = 'application/json' get :index diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index 877b91b563..9f2dbda25f 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -405,6 +405,15 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest end end + def test_request_with_bad_format + with_test_route_set do + xhr :get, '/get.php' + assert_equal 406, status + assert_response 406 + assert_response :not_acceptable + end + end + def test_get_with_query_string with_test_route_set do get '/get_with_params?foo=bar' diff --git a/actionpack/test/controller/mime_responds_test.rb b/actionpack/test/controller/mime_responds_test.rb index ac056319fc..bdcd5561a8 100644 --- a/actionpack/test/controller/mime_responds_test.rb +++ b/actionpack/test/controller/mime_responds_test.rb @@ -207,8 +207,9 @@ class RespondToControllerTest < ActionController::TestCase get :html_or_xml assert_equal 'HTML', @response.body - get :just_xml - assert_response 406 + assert_raises(ActionController::UnknownFormat) do + get :just_xml + end end def test_all @@ -239,8 +240,10 @@ class RespondToControllerTest < ActionController::TestCase assert_equal 'HTML', @response.body @request.accept = "text/javascript, text/html" - xhr :get, :just_xml - assert_response 406 + + assert_raises(ActionController::UnknownFormat) do + xhr :get, :just_xml + end end def test_json_or_yaml_with_leading_star_star @@ -495,9 +498,9 @@ class RespondToControllerTest < ActionController::TestCase end def test_invalid_format - get :using_defaults, :format => "invalidformat" - assert_equal " ", @response.body - assert_equal "text/html", @response.content_type + assert_raises(ActionController::UnknownFormat) do + get :using_defaults, :format => "invalidformat" + end end end @@ -701,12 +704,14 @@ class RespondWithControllerTest < ActionController::TestCase def test_not_acceptable @request.accept = "application/xml" - get :using_resource_with_block - assert_equal 406, @response.status + assert_raises(ActionController::UnknownFormat) do + get :using_resource_with_block + end @request.accept = "text/javascript" - get :using_resource_with_overwrite_block - assert_equal 406, @response.status + assert_raises(ActionController::UnknownFormat) do + get :using_resource_with_overwrite_block + end end def test_using_resource_for_post_with_html_redirects_on_success @@ -984,8 +989,9 @@ class RespondWithControllerTest < ActionController::TestCase def test_clear_respond_to @controller = InheritedRespondWithController.new @request.accept = "text/html" - get :index - assert_equal 406, @response.status + assert_raises(ActionController::UnknownFormat) do + get :index + end end def test_first_in_respond_to_has_higher_priority diff --git a/actionpack/test/controller/new_base/bare_metal_test.rb b/actionpack/test/controller/new_base/bare_metal_test.rb index c424dcbd8d..5bcd79ebec 100644 --- a/actionpack/test/controller/new_base/bare_metal_test.rb +++ b/actionpack/test/controller/new_base/bare_metal_test.rb @@ -37,6 +37,36 @@ module BareMetalTest def index head :not_found end + + def continue + self.content_type = "text/html" + head 100 + end + + def switching_protocols + self.content_type = "text/html" + head 101 + end + + def processing + self.content_type = "text/html" + head 102 + end + + def no_content + self.content_type = "text/html" + head 204 + end + + def reset_content + self.content_type = "text/html" + head 205 + end + + def not_modified + self.content_type = "text/html" + head 304 + end end class HeadTest < ActiveSupport::TestCase @@ -44,6 +74,42 @@ module BareMetalTest status = HeadController.action(:index).call(Rack::MockRequest.env_for("/")).first assert_equal 404, status end + + test "head :continue (100) does not return a content-type header" do + headers = HeadController.action(:continue).call(Rack::MockRequest.env_for("/")).second + assert_nil headers['Content-Type'] + assert_nil headers['Content-Length'] + end + + test "head :continue (101) does not return a content-type header" do + headers = HeadController.action(:continue).call(Rack::MockRequest.env_for("/")).second + assert_nil headers['Content-Type'] + assert_nil headers['Content-Length'] + end + + test "head :processing (102) does not return a content-type header" do + headers = HeadController.action(:processing).call(Rack::MockRequest.env_for("/")).second + assert_nil headers['Content-Type'] + assert_nil headers['Content-Length'] + end + + test "head :no_content (204) does not return a content-type header" do + headers = HeadController.action(:no_content).call(Rack::MockRequest.env_for("/")).second + assert_nil headers['Content-Type'] + assert_nil headers['Content-Length'] + end + + test "head :reset_content (205) does not return a content-type header" do + headers = HeadController.action(:reset_content).call(Rack::MockRequest.env_for("/")).second + assert_nil headers['Content-Type'] + assert_nil headers['Content-Length'] + end + + test "head :not_modified (304) does not return a content-type header" do + headers = HeadController.action(:not_modified).call(Rack::MockRequest.env_for("/")).second + assert_nil headers['Content-Type'] + assert_nil headers['Content-Length'] + end end class BareControllerTest < ActionController::TestCase diff --git a/actionpack/test/controller/params_wrapper_test.rb b/actionpack/test/controller/params_wrapper_test.rb index c4d2614200..fa1608b9df 100644 --- a/actionpack/test/controller/params_wrapper_test.rb +++ b/actionpack/test/controller/params_wrapper_test.rb @@ -174,7 +174,7 @@ class ParamsWrapperTest < ActionController::TestCase def test_accessible_wrapped_keys_from_matching_model User.expects(:respond_to?).with(:accessible_attributes).returns(true) - User.expects(:accessible_attributes).twice.returns(["username"]) + User.expects(:accessible_attributes).with(:default).twice.returns(["username"]) with_default_wrapper_options do @request.env['CONTENT_TYPE'] = 'application/json' @@ -186,7 +186,7 @@ class ParamsWrapperTest < ActionController::TestCase def test_accessible_wrapped_keys_from_specified_model with_default_wrapper_options do Person.expects(:respond_to?).with(:accessible_attributes).returns(true) - Person.expects(:accessible_attributes).twice.returns(["username"]) + Person.expects(:accessible_attributes).with(:default).twice.returns(["username"]) UsersController.wrap_parameters Person @@ -195,6 +195,19 @@ class ParamsWrapperTest < ActionController::TestCase assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'person' => { 'username' => 'sikachu' }}) end end + + def test_accessible_wrapped_keys_with_role_from_specified_model + with_default_wrapper_options do + Person.expects(:respond_to?).with(:accessible_attributes).returns(true) + Person.expects(:accessible_attributes).with(:admin).twice.returns(["username"]) + + UsersController.wrap_parameters Person, :as => :admin + + @request.env['CONTENT_TYPE'] = 'application/json' + post :parse, { 'username' => 'sikachu', 'title' => 'Developer' } + assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'person' => { 'username' => 'sikachu' }}) + end + end def test_not_wrapping_abstract_model User.expects(:respond_to?).with(:accessible_attributes).returns(false) diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index 10f62dad65..5e699c0daf 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -102,12 +102,12 @@ class TestController < ActionController::Base end def conditional_hello_with_expires_in_with_public_with_more_keys - expires_in 1.minute, :public => true, 'max-stale' => 5.hours + expires_in 1.minute, :public => true, 's-maxage' => 5.hours render :action => 'hello_world' end def conditional_hello_with_expires_in_with_public_with_more_keys_old_syntax - expires_in 1.minute, :public => true, :private => nil, 'max-stale' => 5.hours + expires_in 1.minute, :public => true, :private => nil, 's-maxage' => 5.hours render :action => 'hello_world' end @@ -1478,12 +1478,12 @@ class ExpiresInRenderTest < ActionController::TestCase def test_expires_in_header_with_additional_headers get :conditional_hello_with_expires_in_with_public_with_more_keys - assert_equal "max-age=60, public, max-stale=18000", @response.headers["Cache-Control"] + assert_equal "max-age=60, public, s-maxage=18000", @response.headers["Cache-Control"] end def test_expires_in_old_syntax get :conditional_hello_with_expires_in_with_public_with_more_keys_old_syntax - assert_equal "max-age=60, public, max-stale=18000", @response.headers["Cache-Control"] + assert_equal "max-age=60, public, s-maxage=18000", @response.headers["Cache-Control"] end def test_expires_now diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index 9441b46d47..cd91064ab8 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -891,23 +891,6 @@ class RouteSetTest < ActiveSupport::TestCase MockController.build(set.url_helpers).new end - def test_named_route_hash_access_method - controller = setup_named_route_test - - assert_equal( - { :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.send(:hash_for_index_url)) - - assert_equal( - { :controller => 'people', :action => 'show', :id => 5, :use_route => "show", :only_path => true }, - controller.send(:hash_for_show_path, :id => 5) - ) - end - def test_named_route_url_method controller = setup_named_route_test @@ -919,7 +902,6 @@ 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', url_for(set, controller.send(:hash_for_users_url), { :controller => 'users', :action => 'index' }) end def test_named_route_url_method_with_anchor @@ -1423,7 +1405,7 @@ class RouteSetTest < ActiveSupport::TestCase end end end - + def test_route_with_subdomain_and_constraints_must_receive_params name_param = nil set.draw do @@ -1436,7 +1418,7 @@ class RouteSetTest < ActiveSupport::TestCase set.recognize_path('http://subdomain.example.org/page/mypage')) assert_equal(name_param, 'mypage') end - + def test_route_requirement_recognize_with_ignore_case set.draw do get 'page/:name' => 'pages#show', diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb index 3af17f495c..6fc3556e31 100644 --- a/actionpack/test/controller/send_file_test.rb +++ b/actionpack/test/controller/send_file_test.rb @@ -154,6 +154,17 @@ class SendFileTest < ActionController::TestCase end end + def test_send_file_with_default_content_disposition_header + process('data') + assert_equal 'attachment', @controller.headers['Content-Disposition'] + end + + def test_send_file_without_content_disposition_header + @controller.options = {:disposition => nil} + process('data') + assert_nil @controller.headers['Content-Disposition'] + end + %w(file data).each do |method| define_method "test_send_#{method}_status" do @controller.options = { :stream => false, :status => 500 } diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb index 3e48d97e67..2467654a70 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -38,6 +38,8 @@ class CookiesTest < ActionController::TestCase head :ok end + alias delete_cookie logout + def delete_cookie_with_path cookies.delete("user_name", :path => '/beaten') head :ok @@ -179,6 +181,18 @@ class CookiesTest < ActionController::TestCase assert_equal({"user_name" => "david"}, @response.cookies) end + def test_setting_the_same_value_to_cookie + request.cookies[:user_name] = 'david' + get :authenticate + assert response.cookies.empty? + end + + def test_setting_the_same_value_to_permanent_cookie + request.cookies[:user_name] = 'Jamie' + get :set_permanent_cookie + assert response.cookies, 'user_name' => 'Jamie' + end + def test_setting_with_escapable_characters get :set_with_with_escapable_characters assert_cookie_header "that+%26+guy=foo+%26+bar+%3D%3E+baz; path=/" @@ -235,23 +249,33 @@ class CookiesTest < ActionController::TestCase end def test_expiring_cookie + request.cookies[:user_name] = 'Joe' get :logout assert_cookie_header "user_name=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT" assert_equal({"user_name" => nil}, @response.cookies) end def test_delete_cookie_with_path + request.cookies[:user_name] = 'Joe' get :delete_cookie_with_path assert_cookie_header "user_name=; path=/beaten; expires=Thu, 01-Jan-1970 00:00:00 GMT" end + def test_delete_unexisting_cookie + request.cookies.clear + get :delete_cookie + assert @response.cookies.empty? + end + def test_deleted_cookie_predicate + cookies[:user_name] = 'Joe' cookies.delete("user_name") assert cookies.deleted?("user_name") assert_equal false, cookies.deleted?("another") end def test_deleted_cookie_predicate_with_mismatching_options + cookies[:user_name] = 'Joe' cookies.delete("user_name", :path => "/path") assert_equal false, cookies.deleted?("user_name", :path => "/different") end @@ -284,6 +308,7 @@ class CookiesTest < ActionController::TestCase end def test_delete_and_set_cookie + request.cookies[:user_name] = 'Joe' get :delete_and_set_cookie assert_cookie_header "user_name=david; path=/; expires=Mon, 10-Oct-2005 05:00:00 GMT" assert_equal({"user_name" => "david"}, @response.cookies) @@ -387,6 +412,7 @@ class CookiesTest < ActionController::TestCase end def test_deleting_cookie_with_all_domain_option + request.cookies[:user_name] = 'Joe' 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" @@ -413,6 +439,7 @@ class CookiesTest < ActionController::TestCase end def test_deleting_cookie_with_all_domain_option_and_tld_length + request.cookies[:user_name] = 'Joe' 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" @@ -441,6 +468,7 @@ class CookiesTest < ActionController::TestCase def test_deletings_cookie_with_several_preset_domains_using_one_of_these_domains @request.host = "example2.com" + request.cookies[:user_name] = 'Joe' 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" @@ -448,19 +476,19 @@ class CookiesTest < ActionController::TestCase def test_deletings_cookie_with_several_preset_domains_using_other_domain @request.host = "other-domain.com" + request.cookies[:user_name] = 'Joe' 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 - get :symbol_key - assert_equal "david", cookies[:user_name] - assert_equal "david", cookies['user_name'] - get :string_key - assert_equal "dhh", cookies[:user_name] - assert_equal "dhh", cookies['user_name'] + get :symbol_key + assert_equal "david", cookies[:user_name] + assert_equal "david", cookies['user_name'] + get :string_key + assert_equal "dhh", cookies[:user_name] + assert_equal "dhh", cookies['user_name'] end @@ -575,4 +603,4 @@ class CookiesTest < ActionController::TestCase assert_not_equal expected.split("\n"), header end end -end
\ No newline at end of file +end diff --git a/actionpack/test/dispatch/header_test.rb b/actionpack/test/dispatch/header_test.rb index ec6ba494dc..bc7cad8db5 100644 --- a/actionpack/test/dispatch/header_test.rb +++ b/actionpack/test/dispatch/header_test.rb @@ -13,4 +13,9 @@ class HeaderTest < ActiveSupport::TestCase assert_equal "text/plain", @headers["CONTENT_TYPE"] assert_equal "text/plain", @headers["HTTP_CONTENT_TYPE"] end + + test "fetch" do + assert_equal "text/plain", @headers.fetch("content-type", nil) + assert_equal "not found", @headers.fetch('not-found', 'not found') + end end diff --git a/actionpack/test/dispatch/request/session_test.rb b/actionpack/test/dispatch/request/session_test.rb new file mode 100644 index 0000000000..4d24456ba6 --- /dev/null +++ b/actionpack/test/dispatch/request/session_test.rb @@ -0,0 +1,48 @@ +require 'abstract_unit' +require 'action_dispatch/middleware/session/abstract_store' + +module ActionDispatch + class Request + class SessionTest < ActiveSupport::TestCase + def test_create_adds_itself_to_env + env = {} + s = Session.create(store, env, {}) + assert_equal s, env[Rack::Session::Abstract::ENV_SESSION_KEY] + end + + def test_to_hash + env = {} + s = Session.create(store, env, {}) + s['foo'] = 'bar' + assert_equal 'bar', s['foo'] + assert_equal({'foo' => 'bar'}, s.to_hash) + end + + def test_create_merges_old + env = {} + s = Session.create(store, env, {}) + s['foo'] = 'bar' + + s1 = Session.create(store, env, {}) + refute_equal s, s1 + assert_equal 'bar', s1['foo'] + end + + def test_find + env = {} + assert_nil Session.find(env) + + s = Session.create(store, env, {}) + assert_equal s, Session.find(env) + end + + private + def store + Class.new { + def load_session(env); [1, {}]; end + def session_exists?(env); true; end + }.new + end + end + end +end diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index ce5cc16ace..e5345754cd 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -63,8 +63,13 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest get 'secure', :to => redirect("/secure/login") get 'mobile', :to => redirect(:subdomain => 'mobile') + get 'documentation', :to => redirect(:domain => 'example-documentation.com', :path => '') + get 'new_documentation', :to => redirect(:path => '/documentation/new') get 'super_new_documentation', :to => redirect(:host => 'super-docs.com') + get 'stores/:name', :to => redirect(:subdomain => 'stores', :path => '/%{name}') + get 'stores/:name(*rest)', :to => redirect(:subdomain => 'stores', :path => '/%{name}%{rest}') + get 'youtube_favorites/:youtube_id/:name', :to => redirect(YoutubeFavoritesRedirector) constraints(lambda { |req| true }) do @@ -166,6 +171,8 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest post :preview, :on => :collection end end + + post 'new', :action => 'new', :on => :collection, :as => :new end resources :replies do @@ -693,11 +700,31 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest verify_redirect 'http://mobile.example.com/mobile' end + def test_redirect_hash_with_domain_and_path + get '/documentation' + verify_redirect 'http://www.example-documentation.com' + end + + def test_redirect_hash_with_path + get '/new_documentation' + verify_redirect 'http://www.example.com/documentation/new' + end + def test_redirect_hash_with_host get '/super_new_documentation?section=top' verify_redirect 'http://super-docs.com/super_new_documentation?section=top' end + def test_redirect_hash_path_substitution + get '/stores/iernest' + verify_redirect 'http://stores.example.com/iernest' + end + + def test_redirect_hash_path_substitution_with_catch_all + get '/stores/iernest/products' + verify_redirect 'http://stores.example.com/iernest/products' + end + def test_redirect_class get '/youtube_favorites/oHg5SJYRHA0/rick-rolld' verify_redirect 'http://www.youtube.com/watch?v=oHg5SJYRHA0' @@ -851,6 +878,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal '/projects/1/edit', edit_project_path(:id => '1') end + def test_projects_with_post_action_and_new_path_on_collection + post '/projects/new' + assert_equal "project#new", @response.body + assert_equal "/projects/new", new_projects_path + end + def test_projects_involvements get '/projects/1/involvements' assert_equal 'involvements#index', @response.body @@ -2425,7 +2458,6 @@ class TestMultipleNestedController < ActionDispatch::IntegrationTest get "/foo/bar/baz" assert_equal "/pooh", @response.body end - end class TestTildeAndMinusPaths < ActionDispatch::IntegrationTest @@ -2459,17 +2491,23 @@ class TestRedirectInterpolation < ActionDispatch::IntegrationTest ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } get "/foo/:id" => redirect("/foo/bar/%{id}") + get "/bar/:id" => redirect(:path => "/foo/bar/%{id}") get "/foo/bar/:id" => ok end end def app; Routes end - test "redirect escapes interpolated parameters" do + test "redirect escapes interpolated parameters with redirect proc" do get "/foo/1%3E" verify_redirect "http://www.example.com/foo/bar/1%3E" end + test "redirect escapes interpolated parameters with option proc" do + get "/bar/1%3E" + verify_redirect "http://www.example.com/foo/bar/1%3E" + end + private def verify_redirect(url, status=301) assert_equal status, @response.status @@ -2481,3 +2519,46 @@ private %(<html><body>You are being <a href="#{ERB::Util.h(url)}">redirected</a>.</body></html>) end end + +class TestConstraintsAccessingParameters < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } + + get "/:foo" => ok, :constraints => lambda { |r| r.params[:foo] == 'foo' } + get "/:bar" => ok + end + end + + def app; Routes end + + test "parameters are reset between constraint checks" do + get "/bar" + assert_equal nil, @request.params[:foo] + assert_equal "bar", @request.params[:bar] + end +end + +class TestOptimizedNamedRoutes < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } + get '/foo' => ok, as: :foo + end + end + + include Routes.url_helpers + def app; Routes end + + test 'enabled when not mounted and default_url_options is empty' do + assert Routes.url_helpers.optimize_routes_generation? + end + + test 'named route called as singleton method' do + assert_equal '/foo', Routes.url_helpers.foo_path + end + + test 'named route called on included module' do + assert_equal '/foo', foo_path + end +end diff --git a/actionpack/test/dispatch/session/abstract_store_test.rb b/actionpack/test/dispatch/session/abstract_store_test.rb new file mode 100644 index 0000000000..8daf3d3f5e --- /dev/null +++ b/actionpack/test/dispatch/session/abstract_store_test.rb @@ -0,0 +1,56 @@ +require 'abstract_unit' +require 'action_dispatch/middleware/session/abstract_store' + +module ActionDispatch + module Session + class AbstractStoreTest < ActiveSupport::TestCase + class MemoryStore < AbstractStore + def initialize(app) + @sessions = {} + super + end + + def get_session(env, sid) + sid ||= 1 + session = @sessions[sid] ||= {} + [sid, session] + end + + def set_session(env, sid, session, options) + @sessions[sid] = session + end + end + + def test_session_is_set + env = {} + as = MemoryStore.new app + as.call(env) + + assert @env + assert Request::Session.find @env + end + + def test_new_session_object_is_merged_with_old + env = {} + as = MemoryStore.new app + as.call(env) + + assert @env + session = Request::Session.find @env + session['foo'] = 'bar' + + as.call(@env) + session1 = Request::Session.find @env + + refute_equal session, session1 + assert_equal session.to_hash, session1.to_hash + end + + private + def app(&block) + @env = nil + lambda { |env| @env = env } + end + end + end +end diff --git a/actionpack/test/fixtures/test/hello_world_with_partial.html.erb b/actionpack/test/fixtures/test/hello_world_with_partial.html.erb new file mode 100644 index 0000000000..ec31545356 --- /dev/null +++ b/actionpack/test/fixtures/test/hello_world_with_partial.html.erb @@ -0,0 +1,2 @@ +Hello world! +<%= render '/test/partial' %> diff --git a/actionpack/test/fixtures/translations/templates/default.erb b/actionpack/test/fixtures/translations/templates/default.erb new file mode 100644 index 0000000000..8b70031071 --- /dev/null +++ b/actionpack/test/fixtures/translations/templates/default.erb @@ -0,0 +1 @@ +<%= t('.missing', :default => :'.foo') %> diff --git a/actionpack/test/template/date_helper_i18n_test.rb b/actionpack/test/template/date_helper_i18n_test.rb index 82b62e64f3..63066d40cd 100644 --- a/actionpack/test/template/date_helper_i18n_test.rb +++ b/actionpack/test/template/date_helper_i18n_test.rb @@ -12,24 +12,24 @@ class DateHelperDistanceOfTimeInWordsI18nTests < ActiveSupport::TestCase def test_distance_of_time_in_words_calls_i18n { # with include_seconds - [2.seconds, true] => [:'less_than_x_seconds', 5], - [9.seconds, true] => [:'less_than_x_seconds', 10], - [19.seconds, true] => [:'less_than_x_seconds', 20], - [30.seconds, true] => [:'half_a_minute', nil], - [59.seconds, true] => [:'less_than_x_minutes', 1], - [60.seconds, true] => [:'x_minutes', 1], + [2.seconds, { :include_seconds => true }] => [:'less_than_x_seconds', 5], + [9.seconds, { :include_seconds => true }] => [:'less_than_x_seconds', 10], + [19.seconds, { :include_seconds => true }] => [:'less_than_x_seconds', 20], + [30.seconds, { :include_seconds => true }] => [:'half_a_minute', nil], + [59.seconds, { :include_seconds => true }] => [:'less_than_x_minutes', 1], + [60.seconds, { :include_seconds => true }] => [:'x_minutes', 1], # without include_seconds - [29.seconds, false] => [:'less_than_x_minutes', 1], - [60.seconds, false] => [:'x_minutes', 1], - [44.minutes, false] => [:'x_minutes', 44], - [61.minutes, false] => [:'about_x_hours', 1], - [24.hours, false] => [:'x_days', 1], - [30.days, false] => [:'about_x_months', 1], - [60.days, false] => [:'x_months', 2], - [1.year, false] => [:'about_x_years', 1], - [3.years + 6.months, false] => [:'over_x_years', 3], - [3.years + 10.months, false] => [:'almost_x_years', 4] + [29.seconds, { :include_seconds => false }] => [:'less_than_x_minutes', 1], + [60.seconds, { :include_seconds => false }] => [:'x_minutes', 1], + [44.minutes, { :include_seconds => false }] => [:'x_minutes', 44], + [61.minutes, { :include_seconds => false }] => [:'about_x_hours', 1], + [24.hours, { :include_seconds => false }] => [:'x_days', 1], + [30.days, { :include_seconds => false }] => [:'about_x_months', 1], + [60.days, { :include_seconds => false }] => [:'x_months', 2], + [1.year, { :include_seconds => false }] => [:'about_x_years', 1], + [3.years + 6.months, { :include_seconds => false }] => [:'over_x_years', 3], + [3.years + 10.months, { :include_seconds => false }] => [:'almost_x_years', 4] }.each do |passed, expected| assert_distance_of_time_in_words_translates_key passed, expected @@ -37,7 +37,7 @@ class DateHelperDistanceOfTimeInWordsI18nTests < ActiveSupport::TestCase end def assert_distance_of_time_in_words_translates_key(passed, expected) - diff, include_seconds = *passed + diff, passed_options = *passed key, count = *expected to = @from + diff @@ -45,7 +45,12 @@ class DateHelperDistanceOfTimeInWordsI18nTests < ActiveSupport::TestCase options[:count] = count if count I18n.expects(:t).with(key, options) - distance_of_time_in_words(@from, to, include_seconds, :locale => 'en') + distance_of_time_in_words(@from, to, passed_options.merge(:locale => 'en')) + end + + def test_time_ago_in_words_passes_locale + I18n.expects(:t).with(:less_than_x_minutes, :scope => :'datetime.distance_in_words', :count => 1, :locale => 'ru') + time_ago_in_words(15.seconds.ago, :locale => 'ru') end def test_distance_of_time_pluralizations diff --git a/actionpack/test/template/date_helper_test.rb b/actionpack/test/template/date_helper_test.rb index 4835bc578f..ff85a675a2 100644 --- a/actionpack/test/template/date_helper_test.rb +++ b/actionpack/test/template/date_helper_test.rb @@ -21,56 +21,80 @@ class DateHelperTest < ActionView::TestCase def assert_distance_of_time_in_words(from, to=nil) to ||= from - # 0..1 with include_seconds - assert_equal "less than 5 seconds", distance_of_time_in_words(from, to + 0.seconds, true) - assert_equal "less than 5 seconds", distance_of_time_in_words(from, to + 4.seconds, true) - assert_equal "less than 10 seconds", distance_of_time_in_words(from, to + 5.seconds, true) - assert_equal "less than 10 seconds", distance_of_time_in_words(from, to + 9.seconds, true) - assert_equal "less than 20 seconds", distance_of_time_in_words(from, to + 10.seconds, true) - assert_equal "less than 20 seconds", distance_of_time_in_words(from, to + 19.seconds, true) - assert_equal "half a minute", distance_of_time_in_words(from, to + 20.seconds, true) - assert_equal "half a minute", distance_of_time_in_words(from, to + 39.seconds, true) - assert_equal "less than a minute", distance_of_time_in_words(from, to + 40.seconds, true) - assert_equal "less than a minute", distance_of_time_in_words(from, to + 59.seconds, true) - assert_equal "1 minute", distance_of_time_in_words(from, to + 60.seconds, true) - assert_equal "1 minute", distance_of_time_in_words(from, to + 89.seconds, true) - - # First case 0..1 + # 0..1 minute with :include_seconds => true + assert_equal "less than 5 seconds", distance_of_time_in_words(from, to + 0.seconds, :include_seconds => true) + assert_equal "less than 5 seconds", distance_of_time_in_words(from, to + 4.seconds, :include_seconds => true) + assert_equal "less than 10 seconds", distance_of_time_in_words(from, to + 5.seconds, :include_seconds => true) + assert_equal "less than 10 seconds", distance_of_time_in_words(from, to + 9.seconds, :include_seconds => true) + assert_equal "less than 20 seconds", distance_of_time_in_words(from, to + 10.seconds, :include_seconds => true) + assert_equal "less than 20 seconds", distance_of_time_in_words(from, to + 19.seconds, :include_seconds => true) + assert_equal "half a minute", distance_of_time_in_words(from, to + 20.seconds, :include_seconds => true) + assert_equal "half a minute", distance_of_time_in_words(from, to + 39.seconds, :include_seconds => true) + assert_equal "less than a minute", distance_of_time_in_words(from, to + 40.seconds, :include_seconds => true) + assert_equal "less than a minute", distance_of_time_in_words(from, to + 59.seconds, :include_seconds => true) + assert_equal "1 minute", distance_of_time_in_words(from, to + 60.seconds, :include_seconds => true) + assert_equal "1 minute", distance_of_time_in_words(from, to + 89.seconds, :include_seconds => true) + + # 0..1 minute with :include_seconds => false + assert_equal "less than a minute", distance_of_time_in_words(from, to + 0.seconds, :include_seconds => false) + assert_equal "less than a minute", distance_of_time_in_words(from, to + 4.seconds, :include_seconds => false) + assert_equal "less than a minute", distance_of_time_in_words(from, to + 5.seconds, :include_seconds => false) + assert_equal "less than a minute", distance_of_time_in_words(from, to + 9.seconds, :include_seconds => false) + assert_equal "less than a minute", distance_of_time_in_words(from, to + 10.seconds, :include_seconds => false) + assert_equal "less than a minute", distance_of_time_in_words(from, to + 19.seconds, :include_seconds => false) + assert_equal "less than a minute", distance_of_time_in_words(from, to + 20.seconds, :include_seconds => false) + assert_equal "1 minute", distance_of_time_in_words(from, to + 39.seconds, :include_seconds => false) + assert_equal "1 minute", distance_of_time_in_words(from, to + 40.seconds, :include_seconds => false) + assert_equal "1 minute", distance_of_time_in_words(from, to + 59.seconds, :include_seconds => false) + assert_equal "1 minute", distance_of_time_in_words(from, to + 60.seconds, :include_seconds => false) + assert_equal "1 minute", distance_of_time_in_words(from, to + 89.seconds, :include_seconds => false) + + # Note that we are including a 30-second boundary around the interval we + # want to test. For instance, "1 minute" is actually 30s to 1m29s. The + # reason for doing this is simple -- in `distance_of_time_to_words`, when we + # take the distance between our two Time objects in seconds and convert it + # to minutes, we round the number. So 29s gets rounded down to 0m, 30s gets + # rounded up to 1m, and 1m29s gets rounded down to 1m. A similar thing + # happens with the other cases. + + # First case 0..1 minute assert_equal "less than a minute", distance_of_time_in_words(from, to + 0.seconds) assert_equal "less than a minute", distance_of_time_in_words(from, to + 29.seconds) assert_equal "1 minute", distance_of_time_in_words(from, to + 30.seconds) assert_equal "1 minute", distance_of_time_in_words(from, to + 1.minutes + 29.seconds) - # 2..44 + # 2 minutes up to 45 minutes assert_equal "2 minutes", distance_of_time_in_words(from, to + 1.minutes + 30.seconds) assert_equal "44 minutes", distance_of_time_in_words(from, to + 44.minutes + 29.seconds) - # 45..89 + # 45 minutes up to 90 minutes assert_equal "about 1 hour", distance_of_time_in_words(from, to + 44.minutes + 30.seconds) assert_equal "about 1 hour", distance_of_time_in_words(from, to + 89.minutes + 29.seconds) - # 90..1439 + # 90 minutes up to 24 hours assert_equal "about 2 hours", distance_of_time_in_words(from, to + 89.minutes + 30.seconds) assert_equal "about 24 hours", distance_of_time_in_words(from, to + 23.hours + 59.minutes + 29.seconds) - # 1440..2519 + # 24 hours up to 42 hours assert_equal "1 day", distance_of_time_in_words(from, to + 23.hours + 59.minutes + 30.seconds) assert_equal "1 day", distance_of_time_in_words(from, to + 41.hours + 59.minutes + 29.seconds) - # 2520..43199 + # 42 hours up to 30 days assert_equal "2 days", distance_of_time_in_words(from, to + 41.hours + 59.minutes + 30.seconds) assert_equal "3 days", distance_of_time_in_words(from, to + 2.days + 12.hours) assert_equal "30 days", distance_of_time_in_words(from, to + 29.days + 23.hours + 59.minutes + 29.seconds) - # 43200..86399 + # 30 days up to 60 days assert_equal "about 1 month", distance_of_time_in_words(from, to + 29.days + 23.hours + 59.minutes + 30.seconds) - assert_equal "about 1 month", distance_of_time_in_words(from, to + 59.days + 23.hours + 59.minutes + 29.seconds) + assert_equal "about 1 month", distance_of_time_in_words(from, to + 44.days + 23.hours + 59.minutes + 29.seconds) + assert_equal "about 2 months", distance_of_time_in_words(from, to + 44.days + 23.hours + 59.minutes + 30.seconds) + assert_equal "about 2 months", distance_of_time_in_words(from, to + 59.days + 23.hours + 59.minutes + 29.seconds) - # 86400..525599 + # 60 days up to 365 days assert_equal "2 months", distance_of_time_in_words(from, to + 59.days + 23.hours + 59.minutes + 30.seconds) assert_equal "12 months", distance_of_time_in_words(from, to + 1.years - 31.seconds) - # > 525599 + # >= 365 days assert_equal "about 1 year", distance_of_time_in_words(from, to + 1.years - 30.seconds) assert_equal "about 1 year", distance_of_time_in_words(from, to + 1.years + 3.months - 1.day) assert_equal "over 1 year", distance_of_time_in_words(from, to + 1.years + 6.months) @@ -95,7 +119,8 @@ class DateHelperTest < ActionView::TestCase # test to < from assert_equal "about 4 hours", distance_of_time_in_words(from + 4.hours, to) - assert_equal "less than 20 seconds", distance_of_time_in_words(from + 19.seconds, to, true) + assert_equal "less than 20 seconds", distance_of_time_in_words(from + 19.seconds, to, :include_seconds => true) + assert_equal "less than a minute", distance_of_time_in_words(from + 19.seconds, to, :include_seconds => false) end def test_distance_in_words @@ -103,6 +128,11 @@ class DateHelperTest < ActionView::TestCase assert_distance_of_time_in_words(from) end + def test_time_ago_in_words_passes_include_seconds + assert_equal "less than 20 seconds", time_ago_in_words(15.seconds.ago, :include_seconds => true) + assert_equal "less than a minute", time_ago_in_words(15.seconds.ago, :include_seconds => false) + end + def test_distance_in_words_with_time_zones from = Time.mktime(2004, 6, 6, 21, 45, 0) assert_distance_of_time_in_words(from.in_time_zone('Alaska')) @@ -148,10 +178,10 @@ class DateHelperTest < ActionView::TestCase assert_equal "about 1 hour", distance_of_time_in_words(60.minutes) # include seconds - assert_equal "half a minute", distance_of_time_in_words(39.seconds, 0, true) - assert_equal "less than a minute", distance_of_time_in_words(40.seconds, 0, true) - assert_equal "less than a minute", distance_of_time_in_words(59.seconds, 0, true) - assert_equal "1 minute", distance_of_time_in_words(60.seconds, 0, true) + assert_equal "half a minute", distance_of_time_in_words(39.seconds, 0, :include_seconds => true) + assert_equal "less than a minute", distance_of_time_in_words(40.seconds, 0, :include_seconds => true) + assert_equal "less than a minute", distance_of_time_in_words(59.seconds, 0, :include_seconds => true) + assert_equal "1 minute", distance_of_time_in_words(60.seconds, 0, :include_seconds => true) end def test_time_ago_in_words @@ -587,7 +617,7 @@ class DateHelperTest < ActionView::TestCase end def test_select_minute_with_html_options - expected = expected = %(<select id="date_minute" name="date[minute]" class="selector" accesskey="M">\n) + expected = %(<select id="date_minute" name="date[minute]" class="selector" accesskey="M">\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" selected="selected">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<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<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" @@ -968,6 +998,15 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { :date_separator => " / ", :discard_month => true, :discard_day => true, :start_year => 2003, :end_year => 2005, :prefix => "date[first]"}) end + def test_select_date_with_hidden + 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), { :prefix => "date[first]", :use_hidden => true }) + assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { :date_separator => " / ", :prefix => "date[first]", :use_hidden => true }) + end + def test_select_datetime 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) @@ -1204,6 +1243,18 @@ class DateHelperTest < ActionView::TestCase :prompt => {:day => 'Choose day', :month => 'Choose month', :year => 'Choose year', :hour => 'Choose hour', :minute => 'Choose minute'}) end + def test_select_datetime_with_hidden + 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) + expected << %(<input id="date_first_hour" name="date[first][hour]" type="hidden" value="8" />\n) + expected << %(<input id="date_first_minute" name="date[first][minute]" type="hidden" value="4" />\n) + + assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :prefix => "date[first]", :use_hidden => true) + assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :datetime_separator => "—", :date_separator => "/", + :time_separator => ":", :prefix => "date[first]", :use_hidden => true) + end + def test_select_time 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) @@ -1379,6 +1430,17 @@ class DateHelperTest < ActionView::TestCase :prompt => {:hour => 'Choose hour', :minute => 'Choose minute', :second => 'Choose seconds'}) end + def test_select_time_with_hidden + 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) + expected << %(<input id="date_first_hour" name="date[first][hour]" type="hidden" value="8" />\n) + expected << %(<input id="date_first_minute" name="date[first][minute]" type="hidden" value="4" />\n) + + assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :prefix => "date[first]", :use_hidden => true) + assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :time_separator => ":", :prefix => "date[first]", :use_hidden => true) + end + def test_date_select @post = Post.new @post.written_on = Date.new(2004, 6, 15) diff --git a/actionpack/test/template/form_collections_helper_test.rb b/actionpack/test/template/form_collections_helper_test.rb index 4d878635ef..c73e80ed88 100644 --- a/actionpack/test/template/form_collections_helper_test.rb +++ b/actionpack/test/template/form_collections_helper_test.rb @@ -195,6 +195,15 @@ class FormCollectionsHelperTest < ActionView::TestCase assert_no_select 'input[type=checkbox][value=2][checked=checked]' end + test 'collection check boxes accepts selected string values as :checked option' do + collection = (1..3).map{|i| [i, "Category #{i}"] } + with_collection_check_boxes :user, :category_ids, collection, :first, :last, :checked => ['1', '3'] + + assert_select 'input[type=checkbox][value=1][checked=checked]' + assert_select 'input[type=checkbox][value=3][checked=checked]' + assert_no_select 'input[type=checkbox][value=2][checked=checked]' + end + test 'collection check boxes accepts a single checked value' do collection = (1..3).map{|i| [i, "Category #{i}"] } with_collection_check_boxes :user, :category_ids, collection, :first, :last, :checked => 3 diff --git a/actionpack/test/template/javascript_helper_test.rb b/actionpack/test/template/javascript_helper_test.rb index 0b96985002..fe7607ee26 100644 --- a/actionpack/test/template/javascript_helper_test.rb +++ b/actionpack/test/template/javascript_helper_test.rb @@ -42,36 +42,6 @@ class JavaScriptHelperTest < ActionView::TestCase assert_instance_of ActiveSupport::SafeBuffer, escape_javascript(ActiveSupport::SafeBuffer.new(given)) end - def test_button_to_function - assert_dom_equal %(<input type="button" onclick="alert('Hello world!');" value="Greeting" />), - button_to_function("Greeting", "alert('Hello world!')") - end - - def test_button_to_function_with_onclick - assert_dom_equal "<input onclick=\"alert('Goodbye World :('); alert('Hello world!');\" type=\"button\" value=\"Greeting\" />", - button_to_function("Greeting", "alert('Hello world!')", :onclick => "alert('Goodbye World :(')") - end - - def test_button_to_function_without_function - assert_dom_equal "<input onclick=\";\" type=\"button\" value=\"Greeting\" />", - button_to_function("Greeting") - end - - def test_link_to_function - assert_dom_equal %(<a href="#" onclick="alert('Hello world!'); return false;">Greeting</a>), - link_to_function("Greeting", "alert('Hello world!')") - end - - def test_link_to_function_with_existing_onclick - assert_dom_equal %(<a href="#" onclick="confirm('Sanity!'); alert('Hello world!'); return false;">Greeting</a>), - link_to_function("Greeting", "alert('Hello world!')", :onclick => "confirm('Sanity!')") - end - - def test_function_with_href - assert_dom_equal %(<a href="http://example.com/" onclick="alert('Hello world!'); return false;">Greeting</a>), - link_to_function("Greeting", "alert('Hello world!')", :href => 'http://example.com/') - end - def test_javascript_tag self.output_buffer = 'foo' diff --git a/actionpack/test/template/render_test.rb b/actionpack/test/template/render_test.rb index cdaca56ef2..e7f5f100bf 100644 --- a/actionpack/test/template/render_test.rb +++ b/actionpack/test/template/render_test.rb @@ -153,15 +153,15 @@ module RenderTestCases def test_render_partial_with_invalid_name e = assert_raises(ArgumentError) { @view.render(:partial => "test/200") } assert_equal "The partial name (test/200) is not a valid Ruby identifier; " + - "make sure your partial name starts with a letter or underscore, " + - "and is followed by any combinations of letters, numbers, or underscores.", e.message + "make sure your partial name starts with a lowercase letter or underscore, " + + "and is followed by any combination of letters, numbers and underscores.", e.message end def test_render_partial_with_missing_filename e = assert_raises(ArgumentError) { @view.render(:partial => "test/") } assert_equal "The partial name (test/) is not a valid Ruby identifier; " + - "make sure your partial name starts with a letter or underscore, " + - "and is followed by any combinations of letters, numbers, or underscores.", e.message + "make sure your partial name starts with a lowercase letter or underscore, " + + "and is followed by any combination of letters, numbers and underscores.", e.message end def test_render_partial_with_incompatible_object diff --git a/actionpack/test/template/tag_helper_test.rb b/actionpack/test/template/tag_helper_test.rb index 32b33b4a55..7161d107b3 100644 --- a/actionpack/test/template/tag_helper_test.rb +++ b/actionpack/test/template/tag_helper_test.rb @@ -91,6 +91,11 @@ class TagHelperTest < ActionView::TestCase assert_equal "<![CDATA[<hello world>]]>", cdata_section("<hello world>") end + def test_cdata_section_splitted + assert_equal "<![CDATA[hello]]]]><![CDATA[>world]]>", cdata_section("hello]]>world") + assert_equal "<![CDATA[hello]]]]><![CDATA[>world]]]]><![CDATA[>again]]>", cdata_section("hello]]>world]]>again") + end + def test_escape_once assert_equal '1 < 2 & 3', escape_once('1 < 2 & 3') end diff --git a/actionpack/test/template/translation_helper_test.rb b/actionpack/test/template/translation_helper_test.rb index 397de9c2ce..97777ccff0 100644 --- a/actionpack/test/template/translation_helper_test.rb +++ b/actionpack/test/template/translation_helper_test.rb @@ -11,7 +11,8 @@ class TranslationHelperTest < ActiveSupport::TestCase :translations => { :templates => { :found => { :foo => 'Foo' }, - :array => { :foo => { :bar => 'Foo Bar' } } + :array => { :foo => { :bar => 'Foo Bar' } }, + :default => { :foo => 'Foo' } }, :foo => 'Foo', :hello => '<a>Hello World</a>', @@ -71,6 +72,10 @@ class TranslationHelperTest < ActiveSupport::TestCase assert_equal 'Foo Bar', @view.render(:file => 'translations/templates/array').strip end + def test_default_lookup_scoped_by_partial + assert_equal 'Foo', view.render(:file => 'translations/templates/default').strip + end + 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 @@ -102,4 +107,22 @@ class TranslationHelperTest < ActiveSupport::TestCase def test_translation_returning_an_array_ignores_html_suffix assert_equal ["foo", "bar"], translate(:'translations.array_html') end + + def test_translate_with_default_named_html + translation = translate(:'translations.missing', :default => :'translations.hello_html') + assert_equal '<a>Hello World</a>', translation + assert translation.html_safe? + end + + def test_translate_with_two_defaults_named_html + translation = translate(:'translations.missing', :default => [:'translations.missing_html', :'translations.hello_html']) + assert_equal '<a>Hello World</a>', translation + assert translation.html_safe? + end + + def test_translate_with_last_default_named_html + translation = translate(:'translations.missing', :default => [:'translations.missing', :'translations.hello_html']) + assert_equal '<a>Hello World</a>', translation + assert translation.html_safe? + end end |