diff options
57 files changed, 945 insertions, 279 deletions
diff --git a/actionmailer/README.rdoc b/actionmailer/README.rdoc index 59c33b7940..9feb2add5b 100644 --- a/actionmailer/README.rdoc +++ b/actionmailer/README.rdoc @@ -98,7 +98,7 @@ Example: class Mailman < ActionMailer::Base def receive(email) - page = Page.find_by_address(email.to.first) + page = Page.find_by(address: email.to.first) page.emails.create( subject: email.subject, body: email.body ) diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 2cb7af05e5..2b6425caa5 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,5 +1,50 @@ ## Rails 4.0.0 (unreleased) ## +* `BestStandardsSupport` no longer duplicates `X-UA-Compatible` values on + each request to prevent header size from blowing up. + + *Edward Anderson* + +* Change the behavior of route defaults so that explicit defaults are no longer + required where the key is not part of the path. For example: + + resources :posts, bucket_type: 'posts' + + will be required whenever constructing the url from a hash such as a functional + test or using url_for directly. However using the explicit form alters the + behavior so it's not required: + + resources :projects, defaults: { bucket_type: 'projects' } + + This changes existing behavior slightly in that any routes which only differ + in their defaults will match the first route rather than the closest match. + + *Andrew White* + +* Add support for routing constraints other than Regexp and String. + For example this now allows the use of arrays like this: + + get '/foo/:action', to: 'foo', constraints: { subdomain: %w[www admin] } + + or constraints where the request method returns an Fixnum like this: + + get '/foo', to: 'foo#index', constraints: { port: 8080 } + + Note that this only applies to constraints on the request - path constraints + still need to be specified as Regexps as the various constraints are compiled + into a single Regexp. + + *Andrew White* + +* Fix a bug in integration tests where setting the port via a url passed to + the process method was ignored when constructing the request environment. + + *Andrew White* + +* Allow `:selected` to be set on `date_select` tag helper. + + *Colin Burn-Murdoch* + * Fixed json params parsing regression for non-object JSON content. *Dylan Smith* @@ -447,10 +492,13 @@ *Richard Schneeman* -* Deprecate availbility of `ActionView::RecordIdentifier` in controllers by default. - It's view specific and can be easily included in controller manually if someone - really needs it. RecordIdentifier will be removed from `ActionController::Base` - in Rails 4.1. *Piotr Sarnacki* +* Deprecate availability of `ActionView::RecordIdentifier` in controllers by default. + It's view specific and can be easily included in controllers manually if someone + really needs it. Also deprecate calling `ActionController::RecordIdentifier.dom_id` and + `dom_class` directly, in favor of `ActionView::RecordIdentifier.dom_id` and `dom_class`. + `RecordIdentifier` will be removed from `ActionController::Base` in Rails 4.1. + + *Piotr Sarnacki* * Fix `ActionView::RecordIdentifier` to work as a singleton. *Piotr Sarnacki* diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index 13ec55fe92..59b91a240e 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -65,7 +65,6 @@ module ActionController def redirect_to(options = {}, response_status = {}) #:doc: raise ActionControllerError.new("Cannot redirect to nil!") unless options raise AbstractController::DoubleRenderError if response_body - logger.debug { "Redirected by #{caller(1).first rescue "unknown"}" } if logger self.status = _extract_redirect_to_status(options, response_status) self.location = _compute_redirect_to_location(options) diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index 941b6e210f..e6e58ce6cd 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -373,12 +373,6 @@ module ActionController extend ActiveSupport::Concern include ActiveSupport::Rescuable - included do - rescue_from(ActionController::ParameterMissing) do |parameter_missing_exception| - render text: "Required parameter missing: #{parameter_missing_exception.param}", status: :bad_request - end - end - # Returns a new ActionController::Parameters object that # has been instantiated with the <tt>request.parameters</tt>. def params diff --git a/actionpack/lib/action_controller/record_identifier.rb b/actionpack/lib/action_controller/record_identifier.rb index 70e0a820c4..d598bac467 100644 --- a/actionpack/lib/action_controller/record_identifier.rb +++ b/actionpack/lib/action_controller/record_identifier.rb @@ -2,17 +2,29 @@ require 'action_view/record_identifier' module ActionController module RecordIdentifier - MESSAGE = 'method will no longer be included by default in controllers since Rails 4.1. ' + - 'If you would like to use it in controllers, please include ' + - 'ActionView::RecordIdentifier module.' + MODULE_MESSAGE = 'Calling ActionController::RecordIdentifier.%s is deprecated and ' \ + 'will be removed in Rails 4.1, please call using ActionView::RecordIdentifier instead.' + INSTANCE_MESSAGE = '%s method will no longer be included by default in controllers ' \ + 'since Rails 4.1. If you would like to use it in controllers, please include ' \ + 'ActionView::RecordIdentifier module.' def dom_id(record, prefix = nil) - ActiveSupport::Deprecation.warn('dom_id ' + MESSAGE) + ActiveSupport::Deprecation.warn(INSTANCE_MESSAGE % 'dom_id') ActionView::RecordIdentifier.dom_id(record, prefix) end def dom_class(record, prefix = nil) - ActiveSupport::Deprecation.warn('dom_class ' + MESSAGE) + ActiveSupport::Deprecation.warn(INSTANCE_MESSAGE % 'dom_class') + ActionView::RecordIdentifier.dom_class(record, prefix) + end + + def self.dom_id(record, prefix = nil) + ActiveSupport::Deprecation.warn(MODULE_MESSAGE % 'dom_id') + ActionView::RecordIdentifier.dom_id(record, prefix) + end + + def self.dom_class(record, prefix = nil) + ActiveSupport::Deprecation.warn(MODULE_MESSAGE % 'dom_class') ActionView::RecordIdentifier.dom_class(record, prefix) end end diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 331d15d403..d8206b573d 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -81,8 +81,7 @@ module ActionController # # assert that the "_customer" partial was rendered with a specific object # assert_template partial: '_customer', locals: { customer: @customer } def assert_template(options = {}, message = nil) - # Force body to be read in case the - # template is being streamed + # Force body to be read in case the template is being streamed. response.body case options diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb index cf755bfbeb..82c55660ea 100644 --- a/actionpack/lib/action_dispatch/journey/formatter.rb +++ b/actionpack/lib/action_dispatch/journey/formatter.rb @@ -1,3 +1,5 @@ +require 'action_controller/metal/exceptions' + module ActionDispatch module Journey # The Formatter class is used for formatting URLs. For example, parameters @@ -27,7 +29,10 @@ module ActionDispatch return [route.format(parameterized_parts), params] end - raise Router::RoutingError.new "missing required keys: #{missing_keys}" + message = "No route matches #{constraints.inspect}" + message << " missing required keys: #{missing_keys.inspect}" if name + + raise ActionController::UrlGenerationError, message end def clear diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb index d3988cf31e..063302e0f2 100644 --- a/actionpack/lib/action_dispatch/journey/route.rb +++ b/actionpack/lib/action_dispatch/journey/route.rb @@ -1,7 +1,7 @@ module ActionDispatch module Journey # :nodoc: class Route # :nodoc: - attr_reader :app, :path, :verb, :defaults, :ip, :name + attr_reader :app, :path, :defaults, :name attr_reader :constraints alias :conditions :constraints @@ -12,15 +12,11 @@ module ActionDispatch # +path+ is a path constraint. # +constraints+ is a hash of constraints to be applied to this route. def initialize(name, app, path, constraints, defaults = {}) - constraints = constraints.dup @name = name @app = app @path = path - @verb = constraints[:request_method] || // - @ip = constraints.delete(:ip) || // @constraints = constraints - @constraints.keep_if { |_,v| Regexp === v || String === v } @defaults = defaults @required_defaults = nil @required_parts = nil @@ -49,7 +45,7 @@ module ActionDispatch end def required_keys - path.required_names.map { |x| x.to_sym } + required_defaults.keys + required_parts + required_defaults.keys end def score(constraints) @@ -83,12 +79,38 @@ module ActionDispatch @required_parts ||= path.required_names.map { |n| n.to_sym } end + def required_default?(key) + (constraints[:required_defaults] || []).include?(key) + end + def required_defaults - @required_defaults ||= begin - matches = parts - @defaults.dup.delete_if { |k,_| matches.include?(k) } + @required_defaults ||= @defaults.dup.delete_if do |k,_| + parts.include?(k) || !required_default?(k) + end + end + + def matches?(request) + constraints.all? do |method, value| + next true unless request.respond_to?(method) + + case value + when Regexp, String + value === request.send(method).to_s + when Array + value.include?(request.send(method)) + else + value === request.send(method) + end end end + + def ip + constraints[:ip] || // + end + + def verb + constraints[:request_method] || // + end end end end diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb index 1fc45a2109..31868b1814 100644 --- a/actionpack/lib/action_dispatch/journey/router.rb +++ b/actionpack/lib/action_dispatch/journey/router.rb @@ -131,11 +131,7 @@ module ActionDispatch } routes.concat get_routes_as_head(routes) - routes.sort_by!(&:precedence).select! { |r| - r.constraints.all? { |k, v| v === req.send(k) } && - r.verb === req.request_method - } - routes.reject! { |r| req.ip && !(r.ip === req.ip) } + routes.sort_by!(&:precedence).select! { |r| r.matches?(req) } routes.map! { |r| match_data = r.path.match(req.path_info) diff --git a/actionpack/lib/action_dispatch/middleware/best_standards_support.rb b/actionpack/lib/action_dispatch/middleware/best_standards_support.rb index d338996240..94efeb79fa 100644 --- a/actionpack/lib/action_dispatch/middleware/best_standards_support.rb +++ b/actionpack/lib/action_dispatch/middleware/best_standards_support.rb @@ -17,7 +17,9 @@ module ActionDispatch status, headers, body = @app.call(env) if headers["X-UA-Compatible"] && @header - headers["X-UA-Compatible"] << "," << @header.to_s + unless headers["X-UA-Compatible"][@header] + headers["X-UA-Compatible"] << "," << @header.to_s + end else headers["X-UA-Compatible"] = @header end diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb index 869d0aa7af..7489ce8028 100644 --- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb +++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb @@ -12,7 +12,8 @@ module ActionDispatch 'ActionController::NotImplemented' => :not_implemented, 'ActionController::UnknownFormat' => :not_acceptable, 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity, - 'ActionController::BadRequest' => :bad_request + 'ActionController::BadRequest' => :bad_request, + 'ActionController::ParameterMissing' => :bad_request ) cattr_accessor :rescue_templates diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 1046a7d75a..6d93f609a6 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -8,6 +8,8 @@ require 'action_dispatch/routing/redirection' module ActionDispatch module Routing class Mapper + URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port] + class Constraints #:nodoc: def self.new(app, constraints, request = Rack::Request) if constraints.any? @@ -45,37 +47,68 @@ module ActionDispatch end class Mapping #:nodoc: - IGNORE_OPTIONS = [:to, :as, :via, :on, :constraints, :defaults, :only, :except, :anchor, :shallow, :shallow_path, :shallow_prefix] + IGNORE_OPTIONS = [:to, :as, :via, :on, :constraints, :defaults, :only, :except, :anchor, :shallow, :shallow_path, :shallow_prefix, :format] ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z} SHORTHAND_REGEX = %r{/[\w/]+$} WILDCARD_PATH = %r{\*([^/\)]+)\)?$} - def initialize(set, scope, path, options) - @set, @scope = set, scope - @segment_keys = nil - @options = (@scope[:options] || {}).merge(options) - @path = normalize_path(path) - normalize_options! + attr_reader :scope, :path, :options, :requirements, :conditions, :defaults - via_all = @options.delete(:via) if @options[:via] == :all + def initialize(set, scope, path, options) + @set, @scope, @path, @options = set, scope, path, options + @requirements, @conditions, @defaults = {}, {}, {} - if !via_all && request_method_condition.empty? - msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \ - "If you want to expose your action to GET, use `get` in the router:\n\n" \ - " Instead of: match \"controller#action\"\n" \ - " Do: get \"controller#action\"" - raise msg - end + normalize_path! + normalize_options! + normalize_requirements! + normalize_conditions! + normalize_defaults! end def to_route - [ app, conditions, requirements, defaults, @options[:as], @options[:anchor] ] + [ app, conditions, requirements, defaults, options[:as], options[:anchor] ] end private + def normalize_path! + raise ArgumentError, "path is required" if @path.blank? + @path = Mapper.normalize_path(@path) + + if required_format? + @path = "#{@path}.:format" + elsif optional_format? + @path = "#{@path}(.:format)" + end + end + + def required_format? + options[:format] == true + end + + def optional_format? + options[:format] != false && !path.include?(':format') && !path.end_with?('/') + end + def normalize_options! - path_without_format = @path.sub(/\(\.:format\)$/, '') + @options.reverse_merge!(scope[:options]) if scope[:options] + path_without_format = path.sub(/\(\.:format\)$/, '') + + # Add a constraint for wildcard route to make it non-greedy and match the + # optional format part of the route by default + if path_without_format.match(WILDCARD_PATH) && @options[:format] != false + @options[$1.to_sym] ||= /.+?/ + end + + if path_without_format.match(':controller') + raise ArgumentError, ":controller segment is not allowed within a namespace block" if scope[:module] + + # Add a default constraint for :controller path segments that matches namespaced + # controllers with default routes like :controller/:action/:id(.:format), e.g: + # GET /admin/products/show/1 + # => { controller: 'admin/products', action: 'show', id: '1' } + @options[:controller] ||= /.+?/ + end if using_match_shorthand?(path_without_format, @options) to_shorthand = @options[:to].blank? @@ -83,85 +116,101 @@ module ActionDispatch end @options.merge!(default_controller_and_action(to_shorthand)) + end - requirements.each do |name, requirement| - # segment_keys.include?(k.to_s) || k == :controller - next unless Regexp === requirement && !constraints[name] + # match "account/overview" + def using_match_shorthand?(path, options) + path && (options[:to] || options[:action]).nil? && path =~ SHORTHAND_REGEX + end + + def normalize_format! + if options[:format] == true + options[:format] = /.+/ + elsif options[:format] == false + options.delete(:format) + end + end + + def normalize_requirements! + constraints.each do |key, requirement| + next unless segment_keys.include?(key) || key == :controller if requirement.source =~ ANCHOR_CHARACTERS_REGEX raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}" end + if requirement.multiline? - raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}" + raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{requirement.inspect}" end + + @requirements[key] = requirement end - if @options[:constraints].is_a?(Hash) - (@options[:defaults] ||= {}).reverse_merge!(defaults_from_constraints(@options[:constraints])) + if options[:format] == true + @requirements[:format] = /.+/ + elsif Regexp === options[:format] + @requirements[:format] = options[:format] + elsif String === options[:format] + @requirements[:format] = Regexp.compile(options[:format]) end end - # match "account/overview" - def using_match_shorthand?(path, options) - path && (options[:to] || options[:action]).nil? && path =~ SHORTHAND_REGEX - end + def normalize_defaults! + @defaults.merge!(scope[:defaults]) if scope[:defaults] + @defaults.merge!(options[:defaults]) if options[:defaults] - def normalize_path(path) - raise ArgumentError, "path is required" if path.blank? - path = Mapper.normalize_path(path) + options.each do |key, default| + next if Regexp === default || IGNORE_OPTIONS.include?(key) + @defaults[key] = default + end - if path.match(':controller') - raise ArgumentError, ":controller segment is not allowed within a namespace block" if @scope[:module] + if options[:constraints].is_a?(Hash) + options[:constraints].each do |key, default| + next unless URL_OPTIONS.include?(key) && (String === default || Fixnum === default) + @defaults[key] ||= default + end + end - # Add a default constraint for :controller path segments that matches namespaced - # controllers with default routes like :controller/:action/:id(.:format), e.g: - # GET /admin/products/show/1 - # => { controller: 'admin/products', action: 'show', id: '1' } - @options[:controller] ||= /.+?/ + if Regexp === options[:format] + @defaults[:format] = nil + elsif String === options[:format] + @defaults[:format] = options[:format] end + end - # Add a constraint for wildcard route to make it non-greedy and match the - # optional format part of the route by default - if path.match(WILDCARD_PATH) && @options[:format] != false - @options[$1.to_sym] ||= /.+?/ + def normalize_conditions! + @conditions.merge!(:path_info => path) + + constraints.each do |key, condition| + next if segment_keys.include?(key) || key == :controller + @conditions[key] = condition end - if @options[:format] == false - @options.delete(:format) - path - elsif path.include?(":format") || path.end_with?('/') - path - elsif @options[:format] == true - "#{path}.:format" - else - "#{path}(.:format)" + @conditions[:required_defaults] = [] + options.each do |key, required_default| + next if segment_keys.include?(key) || IGNORE_OPTIONS.include?(key) + next if Regexp === required_default + @conditions[:required_defaults] << key end - end - def app - Constraints.new( - to.respond_to?(:call) ? to : Routing::RouteSet::Dispatcher.new(:defaults => defaults), - blocks, - @set.request_class - ) - end + via_all = options.delete(:via) if options[:via] == :all - def conditions - { :path_info => @path }.merge!(constraints).merge!(request_method_condition) - end + if !via_all && options[:via].blank? + msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \ + "If you want to expose your action to GET, use `get` in the router:\n\n" \ + " Instead of: match \"controller#action\"\n" \ + " Do: get \"controller#action\"" + raise msg + end - def requirements - @requirements ||= (@options[:constraints].is_a?(Hash) ? @options[:constraints] : {}).tap do |requirements| - requirements.reverse_merge!(@scope[:constraints]) if @scope[:constraints] - @options.each { |k, v| requirements[k] ||= v if v.is_a?(Regexp) } + if via = options[:via] + list = Array(via).map { |m| m.to_s.dasherize.upcase } + @conditions.merge!(:request_method => list) end end - def defaults - @defaults ||= (@options[:defaults] || {}).tap do |defaults| - defaults.reverse_merge!(@scope[:defaults]) if @scope[:defaults] - @options.each { |k, v| defaults[k] = v unless v.is_a?(Regexp) || IGNORE_OPTIONS.include?(k.to_sym) } - end + def app + Constraints.new(endpoint, blocks, @set.request_class) end def default_controller_and_action(to_shorthand=nil) @@ -188,11 +237,11 @@ module ActionDispatch controller = controller.to_s unless controller.is_a?(Regexp) action = action.to_s unless action.is_a?(Regexp) - if controller.blank? && segment_keys.exclude?("controller") + if controller.blank? && segment_keys.exclude?(:controller) raise ArgumentError, "missing :controller" end - if action.blank? && segment_keys.exclude?("action") + if action.blank? && segment_keys.exclude?(:action) raise ArgumentError, "missing :action" end @@ -204,50 +253,55 @@ module ActionDispatch end def blocks - constraints = @options[:constraints] - if constraints.present? && !constraints.is_a?(Hash) - [constraints] + if options[:constraints].present? && !options[:constraints].is_a?(Hash) + [options[:constraints]] else - @scope[:blocks] || [] + scope[:blocks] || [] end end def constraints - @constraints ||= requirements.reject { |k, v| segment_keys.include?(k.to_s) || k == :controller } - end + @constraints ||= {}.tap do |constraints| + constraints.merge!(scope[:constraints]) if scope[:constraints] - def request_method_condition - if via = @options[:via] - list = Array(via).map { |m| m.to_s.dasherize.upcase } - { :request_method => list } - else - { } + options.except(*IGNORE_OPTIONS).each do |key, option| + constraints[key] = option if Regexp === option + end + + constraints.merge!(options[:constraints]) if options[:constraints].is_a?(Hash) end end def segment_keys - return @segment_keys if @segment_keys + @segment_keys ||= path_pattern.names.map{ |s| s.to_sym } + end + + def path_pattern + Journey::Path::Pattern.new(strexp) + end - @segment_keys = Journey::Path::Pattern.new( - Journey::Router::Strexp.compile(@path, requirements, SEPARATORS) - ).names + def strexp + Journey::Router::Strexp.compile(path, requirements, SEPARATORS) + end + + def endpoint + to.respond_to?(:call) ? to : dispatcher + end + + def dispatcher + Routing::RouteSet::Dispatcher.new(:defaults => defaults) end def to - @options[:to] + options[:to] end def default_controller - @options[:controller] || @scope[:controller] + options[:controller] || scope[:controller] end def default_action - @options[:action] || @scope[:action] - end - - def defaults_from_constraints(constraints) - url_keys = [:protocol, :subdomain, :domain, :host, :port] - constraints.select { |k, v| url_keys.include?(k) && (v.is_a?(String) || v.is_a?(Fixnum)) } + options[:action] || scope[:action] end end @@ -641,7 +695,11 @@ module ActionDispatch options[:constraints] ||= {} if options[:constraints].is_a?(Hash) - (options[:defaults] ||= {}).reverse_merge!(defaults_from_constraints(options[:constraints])) + defaults = options[:constraints].select do + |k, v| URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Fixnum)) + end + + (options[:defaults] ||= {}).reverse_merge!(defaults) else block, options[:constraints] = options[:constraints], {} end @@ -846,11 +904,6 @@ module ActionDispatch def override_keys(child) #:nodoc: child.key?(:only) || child.key?(:except) ? [:only, :except] : [] end - - def defaults_from_constraints(constraints) - url_keys = [:protocol, :subdomain, :domain, :host, :port] - constraints.select { |k, v| url_keys.include?(k) && (v.is_a?(String) || v.is_a?(Fixnum)) } - end end # Resource routing allows you to quickly declare all of the common routes diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index b1959e388c..c72310cca3 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -421,7 +421,7 @@ module ActionDispatch end conditions.keep_if do |k, _| - k == :action || k == :controller || + k == :action || k == :controller || k == :required_defaults || @request_class.public_method_defined?(k) || path_values.include?(k) end end @@ -527,12 +527,10 @@ module ActionDispatch recall[:action] = options.delete(:action) if options[:action] == 'index' end - # Generates a path from routes, returns [path, params] - # if no path is returned the formatter will raise Journey::Router::RoutingError + # Generates a path from routes, returns [path, params]. + # If no route is generated the formatter will raise ActionController::UrlGenerationError def generate @set.formatter.generate(:path_info, named_route, options, recall, PARAMETERIZE) - rescue Journey::Router::RoutingError => e - raise ActionController::UrlGenerationError, "No route matches #{options.inspect} #{e.message}" end def different_controller? diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 1fc5933e98..ed4e88aab6 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -273,7 +273,7 @@ module ActionDispatch if path =~ %r{://} location = URI.parse(path) https! URI::HTTPS === location if location.scheme - host! location.host if location.host + host! "#{location.host}:#{location.port}" if location.host path = location.query ? "#{location.path}?#{location.query}" : location.path end diff --git a/actionpack/lib/action_view/helpers.rb b/actionpack/lib/action_view/helpers.rb index 269e78a021..8a78685ae1 100644 --- a/actionpack/lib/action_view/helpers.rb +++ b/actionpack/lib/action_view/helpers.rb @@ -1,3 +1,5 @@ +require 'active_support/benchmarkable' + module ActionView #:nodoc: module Helpers #:nodoc: extend ActiveSupport::Autoload @@ -6,7 +8,6 @@ module ActionView #:nodoc: autoload :AssetTagHelper autoload :AssetUrlHelper autoload :AtomFeedHelper - autoload :BenchmarkHelper autoload :CacheHelper autoload :CaptureHelper autoload :ControllerHelper @@ -29,11 +30,11 @@ module ActionView #:nodoc: extend ActiveSupport::Concern + include ActiveSupport::Benchmarkable include ActiveModelHelper include AssetTagHelper include AssetUrlHelper include AtomFeedHelper - include BenchmarkHelper include CacheHelper include CaptureHelper include ControllerHelper diff --git a/actionpack/lib/action_view/helpers/benchmark_helper.rb b/actionpack/lib/action_view/helpers/benchmark_helper.rb deleted file mode 100644 index 87fbf8f1a8..0000000000 --- a/actionpack/lib/action_view/helpers/benchmark_helper.rb +++ /dev/null @@ -1,13 +0,0 @@ -require 'active_support/benchmarkable' - -module ActionView - module Helpers - module BenchmarkHelper #:nodoc: - include ActiveSupport::Benchmarkable - - def benchmark(*) - capture { super } - end - end - end -end diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb index 1fbf61a5a9..cf978d8e83 100644 --- a/actionpack/lib/action_view/helpers/date_helper.rb +++ b/actionpack/lib/action_view/helpers/date_helper.rb @@ -193,6 +193,7 @@ module ActionView # * <tt>:include_blank</tt> - Include a blank option in every select field so it's possible to set empty # dates. # * <tt>:default</tt> - Set a default date if the affected date isn't set or is nil. + # * <tt>:selected</tt> - Set a date that overrides the actual value. # * <tt>:disabled</tt> - Set to true if you want show the select fields as disabled. # * <tt>:prompt</tt> - Set to true (for a generic prompt), a prompt string or a hash of prompt strings # for <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>, <tt>:hour</tt>, <tt>:minute</tt> and <tt>:second</tt>. @@ -234,6 +235,10 @@ module ActionView # # which is initially set to the date 3 days from the current date # date_select("article", "written_on", default: 3.days.from_now) # + # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute + # # which is set in the form with todays date, regardless of the value in the Active Record object. + # date_select("article", "written_on", selected: Date.today) + # # # Generates a date select that when POSTed is stored in the credit_card variable, in the bill_due attribute # # that will have a default day of 20. # date_select("credit_card", "bill_due", default: { day: 20 }) diff --git a/actionpack/lib/action_view/helpers/form_options_helper.rb b/actionpack/lib/action_view/helpers/form_options_helper.rb index bcad05e033..ae7bfd1ec6 100644 --- a/actionpack/lib/action_view/helpers/form_options_helper.rb +++ b/actionpack/lib/action_view/helpers/form_options_helper.rb @@ -772,12 +772,12 @@ module ActionView @template.time_zone_select(@object_name, method, priority_zones, objectify_options(options), @default_options.merge(html_options)) end - def collection_check_boxes(method, collection, value_method, text_method, options = {}, html_options = {}) - @template.collection_check_boxes(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options)) + def collection_check_boxes(method, collection, value_method, text_method, options = {}, html_options = {}, &block) + @template.collection_check_boxes(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options), &block) end - def collection_radio_buttons(method, collection, value_method, text_method, options = {}, html_options = {}) - @template.collection_radio_buttons(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options)) + def collection_radio_buttons(method, collection, value_method, text_method, options = {}, html_options = {}, &block) + @template.collection_radio_buttons(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options), &block) end end end diff --git a/actionpack/lib/action_view/helpers/tags/date_select.rb b/actionpack/lib/action_view/helpers/tags/date_select.rb index 6c400f85cb..734591394b 100644 --- a/actionpack/lib/action_view/helpers/tags/date_select.rb +++ b/actionpack/lib/action_view/helpers/tags/date_select.rb @@ -27,7 +27,7 @@ module ActionView end def datetime_selector(options, html_options) - datetime = value(object) || default_datetime(options) + datetime = options.fetch(:selected) { value(object) || default_datetime(options) } @auto_index ||= nil options = options.dup diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index cf561d913a..e2239c05c7 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -466,6 +466,58 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest assert_equal 'http://www.example.com/foo', url_for(:controller => "foo") end + def test_port_via_host! + with_test_route_set do + host! 'www.example.com:8080' + get '/get' + assert_equal 8080, request.port + end + end + + def test_port_via_process + with_test_route_set do + get 'http://www.example.com:8080/get' + assert_equal 8080, request.port + end + end + + def test_https_and_port_via_host_and_https! + with_test_route_set do + host! 'www.example.com' + https! true + + get '/get' + assert_equal 443, request.port + assert_equal true, request.ssl? + + host! 'www.example.com:443' + https! true + + get '/get' + assert_equal 443, request.port + assert_equal true, request.ssl? + + host! 'www.example.com:8443' + https! true + + get '/get' + assert_equal 8443, request.port + assert_equal true, request.ssl? + end + end + + def test_https_and_port_via_process + with_test_route_set do + get 'https://www.example.com/get' + assert_equal 443, request.port + assert_equal true, request.ssl? + + get 'https://www.example.com:8443/get' + assert_equal 8443, request.port + assert_equal true, request.ssl? + end + end + private def with_test_route_set with_routing do |set| diff --git a/actionpack/test/controller/layout_test.rb b/actionpack/test/controller/layout_test.rb index 94a8d2f180..365fa04570 100644 --- a/actionpack/test/controller/layout_test.rb +++ b/actionpack/test/controller/layout_test.rb @@ -80,7 +80,7 @@ end class StreamingLayoutController < LayoutTest def render(*args) - options = args.extract_options! || {} + options = args.extract_options! super(*args, options.merge(:stream => true)) end end diff --git a/actionpack/test/controller/record_identifier_test.rb b/actionpack/test/controller/record_identifier_test.rb new file mode 100644 index 0000000000..ff5d7fd3bd --- /dev/null +++ b/actionpack/test/controller/record_identifier_test.rb @@ -0,0 +1,34 @@ +require 'abstract_unit' +require 'controller/fake_models' + +class ControllerRecordIdentifierTest < ActiveSupport::TestCase + include ActionController::RecordIdentifier + + def setup + @record = Comment.new + end + + def test_dom_id_deprecation + assert_deprecated(/dom_id method will no longer be included by default in controllers/) do + dom_id(@record) + end + end + + def test_dom_class_deprecation + assert_deprecated(/dom_class method will no longer be included by default in controllers/) do + dom_class(@record) + end + end + + def test_dom_id_from_module_deprecation + assert_deprecated(/Calling ActionController::RecordIdentifier.dom_id is deprecated/) do + ActionController::RecordIdentifier.dom_id(@record) + end + end + + def test_dom_class_from_module_deprecation + assert_deprecated(/Calling ActionController::RecordIdentifier.dom_class is deprecated/) do + ActionController::RecordIdentifier.dom_class(@record) + end + end +end diff --git a/actionpack/test/controller/required_params_test.rb b/actionpack/test/controller/required_params_test.rb index 661bcb3945..343d57c300 100644 --- a/actionpack/test/controller/required_params_test.rb +++ b/actionpack/test/controller/required_params_test.rb @@ -11,20 +11,17 @@ class ActionControllerRequiredParamsTest < ActionController::TestCase tests BooksController test "missing required parameters will raise exception" do - post :create, { magazine: { name: "Mjallo!" } } - assert_response :bad_request + assert_raise ActionController::ParameterMissing do + post :create, { magazine: { name: "Mjallo!" } } + end - post :create, { book: { title: "Mjallo!" } } - assert_response :bad_request + assert_raise ActionController::ParameterMissing do + post :create, { book: { title: "Mjallo!" } } + end end test "required parameters that are present will not raise" do post :create, { book: { name: "Mjallo!" } } assert_response :ok end - - test "missing parameters will be mentioned in the return" do - post :create, { magazine: { name: "Mjallo!" } } - assert_equal "Required parameter missing: book", response.body - end end diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb index e4d78d58b9..df31338f09 100644 --- a/actionpack/test/controller/test_case_test.rb +++ b/actionpack/test/controller/test_case_test.rb @@ -931,3 +931,34 @@ class AnonymousControllerTest < ActionController::TestCase assert_equal 'anonymous', @response.body end end + +class RoutingDefaultsTest < ActionController::TestCase + def setup + @controller = Class.new(ActionController::Base) do + def post + render :text => request.fullpath + end + + def project + render :text => request.fullpath + end + end.new + + @routes = ActionDispatch::Routing::RouteSet.new.tap do |r| + r.draw do + get '/posts/:id', :to => 'anonymous#post', :bucket_type => 'post' + get '/projects/:id', :to => 'anonymous#project', :defaults => { :bucket_type => 'project' } + end + end + end + + def test_route_option_can_be_passed_via_process + get :post, :id => 1, :bucket_type => 'post' + assert_equal '/posts/1', @response.body + end + + def test_route_default_is_not_required_for_building_request_uri + get :project, :id => 2 + assert_equal '/projects/2', @response.body + end +end diff --git a/actionpack/test/dispatch/best_standards_support_test.rb b/actionpack/test/dispatch/best_standards_support_test.rb index 0737c40a39..551bb9621a 100644 --- a/actionpack/test/dispatch/best_standards_support_test.rb +++ b/actionpack/test/dispatch/best_standards_support_test.rb @@ -16,9 +16,10 @@ class BestStandardsSupportTest < ActiveSupport::TestCase assert_equal nil, headers["X-UA-Compatible"] end - def test_appends_to_app_headers + def test_appends_to_app_headers_without_duplication_after_multiple_requests app_headers = { "X-UA-Compatible" => "requiresActiveX=true" } _, headers, _ = app(true, app_headers).call({}) + _, headers, _ = app(true, app_headers).call({}) expects = "requiresActiveX=true,IE=Edge,chrome=1" assert_equal expects, headers["X-UA-Compatible"] diff --git a/actionpack/test/dispatch/debug_exceptions_test.rb b/actionpack/test/dispatch/debug_exceptions_test.rb index 1319eba9ac..6035f0361e 100644 --- a/actionpack/test/dispatch/debug_exceptions_test.rb +++ b/actionpack/test/dispatch/debug_exceptions_test.rb @@ -39,6 +39,8 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest raise ActionController::BadRequest when "/missing_keys" raise ActionController::UrlGenerationError, "No route matches" + when "/parameter_missing" + raise ActionController::ParameterMissing, :missing_param_key else raise "puke!" end @@ -114,6 +116,10 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest get "/bad_request", {}, {'action_dispatch.show_exceptions' => true} assert_response 400 assert_match(/ActionController::BadRequest/, body) + + get "/parameter_missing", {}, {'action_dispatch.show_exceptions' => true} + assert_response 400 + assert_match(/ActionController::ParameterMissing/, body) end test "does not show filtered parameters" do diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index 02675c7f8c..39923d0d2b 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -462,6 +462,27 @@ class RequestTest < ActiveSupport::TestCase assert request.put? end + test "post uneffected by local inflections" do + existing_acrnoyms = ActiveSupport::Inflector.inflections.acronyms.dup + existing_acrnoym_regex = ActiveSupport::Inflector.inflections.acronym_regex.dup + begin + ActiveSupport::Inflector.inflections do |inflect| + inflect.acronym "POS" + end + assert_equal "pos_t", "POST".underscore + request = stub_request "REQUEST_METHOD" => "POST" + assert_equal :post, ActionDispatch::Request::HTTP_METHOD_LOOKUP["POST"] + assert_equal :post, request.method_symbol + assert request.post? + ensure + # Reset original acronym set + ActiveSupport::Inflector.inflections do |inflect| + inflect.send(:instance_variable_set,"@acronyms",existing_acrnoyms) + inflect.send(:instance_variable_set,"@acronym_regex",existing_acrnoym_regex) + end + end + end + test "xml format" do request = stub_request request.expects(:parameters).at_least_once.returns({ :format => 'xml' }) diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index cb5299e8d3..da7474e73c 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -3252,3 +3252,79 @@ class TestOptionalRootSegments < ActionDispatch::IntegrationTest assert_equal '/page/1', root_path(:page => '1') end end + +class TestPortConstraints < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } + + get '/integer', to: ok, constraints: { :port => 8080 } + get '/string', to: ok, constraints: { :port => '8080' } + get '/array', to: ok, constraints: { :port => [8080] } + get '/regexp', to: ok, constraints: { :port => /8080/ } + end + end + + include Routes.url_helpers + def app; Routes end + + def test_integer_port_constraints + get 'http://www.example.com/integer' + assert_response :not_found + + get 'http://www.example.com:8080/integer' + assert_response :success + end + + def test_string_port_constraints + get 'http://www.example.com/string' + assert_response :not_found + + get 'http://www.example.com:8080/string' + assert_response :success + end + + def test_array_port_constraints + get 'http://www.example.com/array' + assert_response :not_found + + get 'http://www.example.com:8080/array' + assert_response :success + end + + def test_regexp_port_constraints + get 'http://www.example.com/regexp' + assert_response :not_found + + get 'http://www.example.com:8080/regexp' + assert_response :success + end +end + +class TestRouteDefaults < ActionDispatch::IntegrationTest + stub_controllers do |routes| + Routes = routes + Routes.draw do + resources :posts, bucket_type: 'post' + resources :projects, defaults: { bucket_type: 'project' } + end + end + + def app + Routes + end + + include Routes.url_helpers + + def test_route_options_are_required_for_url_for + assert_raises(ActionController::UrlGenerationError) do + assert_equal '/posts/1', url_for(controller: 'posts', action: 'show', id: 1, only_path: true) + end + + assert_equal '/posts/1', url_for(controller: 'posts', action: 'show', id: 1, bucket_type: 'post', only_path: true) + end + + def test_route_defaults_are_not_required_for_url_for + assert_equal '/projects/1', url_for(controller: 'projects', action: 'show', id: 1, only_path: true) + end +end diff --git a/actionpack/test/journey/route_test.rb b/actionpack/test/journey/route_test.rb index 78608a5c6b..cbe6284714 100644 --- a/actionpack/test/journey/route_test.rb +++ b/actionpack/test/journey/route_test.rb @@ -6,18 +6,18 @@ module ActionDispatch def test_initialize app = Object.new path = Path::Pattern.new '/:controller(/:action(/:id(.:format)))' - defaults = Object.new + defaults = {} route = Route.new("name", app, path, {}, defaults) assert_equal app, route.app assert_equal path, route.path - assert_equal defaults, route.defaults + assert_same defaults, route.defaults end def test_route_adds_itself_as_memo app = Object.new path = Path::Pattern.new '/:controller(/:action(/:id(.:format)))' - defaults = Object.new + defaults = {} route = Route.new("name", app, path, {}, defaults) route.ast.grep(Nodes::Terminal).each do |node| @@ -82,11 +82,14 @@ module ActionDispatch end def test_score + constraints = {:required_defaults => [:controller, :action]} + defaults = {:controller=>"pages", :action=>"show"} + path = Path::Pattern.new "/page/:id(/:action)(.:format)" - specific = Route.new "name", nil, path, {}, {:controller=>"pages", :action=>"show"} + specific = Route.new "name", nil, path, constraints, defaults path = Path::Pattern.new "/:controller(/:action(/:id))(.:format)" - generic = Route.new "name", nil, path, {} + generic = Route.new "name", nil, path, constraints knowledge = {:id=>20, :controller=>"pages", :action=>"show"} diff --git a/actionpack/test/journey/router_test.rb b/actionpack/test/journey/router_test.rb index 27bdb0108a..3d52b2e9ee 100644 --- a/actionpack/test/journey/router_test.rb +++ b/actionpack/test/journey/router_test.rb @@ -155,7 +155,7 @@ module ActionDispatch Router::Strexp.new("/foo/:id", { :id => /\d/ }, ['/', '.', '?'], false) ] - assert_raises(Router::RoutingError) do + assert_raises(ActionController::UrlGenerationError) do @formatter.generate(:path_info, nil, { :id => '10' }, { }) end end @@ -168,7 +168,7 @@ module ActionDispatch path, _ = @formatter.generate(:path_info, nil, { :id => '10' }, { }) assert_equal '/foo/10', path - assert_raises(Router::RoutingError) do + assert_raises(ActionController::UrlGenerationError) do @formatter.generate(:path_info, nil, { :id => 'aa' }, { }) end end @@ -194,11 +194,11 @@ module ActionDispatch path = Path::Pattern.new pattern @router.routes.add_route nil, path, {}, {}, route_name - error = assert_raises(Router::RoutingError) do + error = assert_raises(ActionController::UrlGenerationError) do @formatter.generate(:path_info, route_name, { }, { }) end - assert_match(/required keys: \[:id\]/, error.message) + assert_match(/missing required keys: \[:id\]/, error.message) end def test_X_Cascade diff --git a/actionpack/test/template/benchmark_helper_test.rb b/actionpack/test/template/benchmark_helper_test.rb deleted file mode 100644 index 8c198d2562..0000000000 --- a/actionpack/test/template/benchmark_helper_test.rb +++ /dev/null @@ -1,24 +0,0 @@ -require 'abstract_unit' -require 'stringio' - -class BenchmarkHelperTest < ActionView::TestCase - include RenderERBUtils - tests ActionView::Helpers::BenchmarkHelper - - def test_output_in_erb - output = render_erb("Hello <%= benchmark do %>world<% end %>") - expected = 'Hello world' - assert_equal expected, output - end - - def test_returns_value_from_block - assert_equal 'test', benchmark { 'test' } - end - - def test_default_message - log = StringIO.new - self.stubs(:logger).returns(Logger.new(log)) - benchmark {} - assert_match(/Benchmarking \(\d+.\d+ms\)/, log.rewind && log.read) - end -end diff --git a/actionpack/test/template/date_helper_test.rb b/actionpack/test/template/date_helper_test.rb index f9ce63fcb0..f11adefad8 100644 --- a/actionpack/test/template/date_helper_test.rb +++ b/actionpack/test/template/date_helper_test.rb @@ -1510,6 +1510,44 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, date_select("post", "written_on") end + + def test_date_select_with_selected + @post = Post.new + @post.written_on = Date.new(2004, 6, 15) + + expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} + expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} + expected << "</select>\n" + + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n} + expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7" selected="selected">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n} + expected << "</select>\n" + + expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n} + expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10" selected="selected">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n} + + expected << "</select>\n" + + assert_dom_equal expected, date_select("post", "written_on", :selected => Date.new(2004, 07, 10)) + end + + def test_date_select_with_selected_nil + @post = Post.new + @post.written_on = Date.new(2004, 6, 15) + + expected = '<input id="post_written_on_1i" name="post[written_on(1i)]" type="hidden" value="1"/>' + "\n" + + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n} + expected << %{<option value=""></option>\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n} + expected << "</select>\n" + + expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n} + expected << %{<option value=""></option>\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n} + + expected << "</select>\n" + + assert_dom_equal expected, date_select("post", "written_on", include_blank: true, discard_year: true, selected: nil) + end def test_date_select_without_day @post = Post.new @@ -1968,6 +2006,44 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, time_select("post", "written_on") end + def test_time_select_with_selected + @post = Post.new + @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) + + expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n} + expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n} + expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n} + + expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]">\n) + 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 12}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + expected << " : " + expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]">\n) + 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 20}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, time_select("post", "written_on", selected: Time.local(2004, 6, 15, 12, 20, 30)) + end + + def test_time_select_with_selected_nil + @post = Post.new + @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) + + expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="1" />\n} + expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="1" />\n} + expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="1" />\n} + + expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]">\n) + 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}">#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + expected << " : " + expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]">\n) + 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}">#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, time_select("post", "written_on", discard_year: true, discard_month: true, discard_day: true, selected: nil) + end + def test_time_select_without_date_hidden_fields @post = Post.new @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) @@ -2165,6 +2241,62 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, datetime_select("post", "updated_at") end + def test_datetime_select_with_selected + @post = Post.new + @post.updated_at = Time.local(2004, 6, 15, 16, 35) + + expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n} + expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} + expected << "</select>\n" + + expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n} + expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3" selected="selected">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n} + expected << "</select>\n" + + expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n} + expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10" selected="selected">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n} + expected << "</select>\n" + + expected << " — " + + expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n} + expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12" selected="selected">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n} + expected << "</select>\n" + expected << " : " + expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n} + expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<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" selected="selected">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" + + assert_dom_equal expected, datetime_select("post", "updated_at", :selected => Time.local(2004, 3, 10, 12, 30)) + end + + def test_datetime_select_with_selected_nil + @post = Post.new + @post.updated_at = Time.local(2004, 6, 15, 16, 35) + + expected = '<input id="post_updated_at_1i" name="post[updated_at(1i)]" type="hidden" value="1"/>' + "\n" + + expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n} + expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n} + expected << "</select>\n" + + expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n} + expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n} + expected << "</select>\n" + + expected << " — " + + expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n} + expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n} + expected << "</select>\n" + expected << " : " + expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n} + expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<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" + + assert_dom_equal expected, datetime_select("post", "updated_at", discard_year: true, selected: nil) + end + def test_datetime_select_defaults_to_time_zone_now_when_config_time_zone_is_set # The love zone is UTC+0 mytz = Class.new(ActiveSupport::TimeZone) { diff --git a/actionpack/test/template/form_helper_test.rb b/actionpack/test/template/form_helper_test.rb index 247068de4e..f9890a2eef 100644 --- a/actionpack/test/template/form_helper_test.rb +++ b/actionpack/test/template/form_helper_test.rb @@ -1120,6 +1120,28 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end + def test_form_for_with_collection_radio_buttons_with_custom_builder_block + post = Post.new + def post.active; false; end + form_for(post) do |f| + rendered_radio_buttons = f.collection_radio_buttons(:active, [true, false], :to_s, :to_s) do |b| + b.label { b.radio_button + b.text } + end + concat rendered_radio_buttons + end + + expected = whole_form("/posts", "new_post" , "new_post") do + "<label for='post_active_true'>"+ + "<input id='post_active_true' name='post[active]' type='radio' value='true' />" + + "true</label>" + + "<label for='post_active_false'>"+ + "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" + + "false</label>" + end + + assert_dom_equal expected, output_buffer + end + def test_form_for_with_collection_check_boxes post = Post.new def post.tag_ids; [1, 3]; end @@ -1141,6 +1163,33 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end + def test_form_for_with_collection_check_boxes_with_custom_builder_block + post = Post.new + def post.tag_ids; [1, 3]; end + collection = (1..3).map{|i| [i, "Tag #{i}"] } + form_for(post) do |f| + rendered_check_boxes = f.collection_check_boxes(:tag_ids, collection, :first, :last) do |b| + b.label { b.check_box + b.text } + end + concat rendered_check_boxes + end + + expected = whole_form("/posts", "new_post" , "new_post") do + "<label for='post_tag_ids_1'>" + + "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" + + "Tag 1</label>" + + "<label for='post_tag_ids_2'>" + + "<input id='post_tag_ids_2' name='post[tag_ids][]' type='checkbox' value='2' />" + + "Tag 2</label>" + + "<label for='post_tag_ids_3'>" + + "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" + + "Tag 3</label>" + + "<input name='post[tag_ids][]' type='hidden' value='' />" + end + + assert_dom_equal expected, output_buffer + end + def test_form_for_with_file_field_generate_multipart Post.send :attr_accessor, :file @@ -2163,14 +2212,14 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end - class FakeAssociatonProxy + class FakeAssociationProxy def to_ary [1, 2, 3] end end def test_nested_fields_for_with_child_index_option_override_on_a_nested_attributes_collection_association_with_proxy - @post.comments = FakeAssociatonProxy.new + @post.comments = FakeAssociationProxy.new form_for(@post) do |f| concat f.fields_for(:comments, Comment.new(321), :child_index => 'abc') { |cf| diff --git a/activemodel/README.rdoc b/activemodel/README.rdoc index 3cc2d9ad8e..1b1fe2fa2b 100644 --- a/activemodel/README.rdoc +++ b/activemodel/README.rdoc @@ -1,7 +1,7 @@ = Active Model -- model interfaces for Rails Active Model provides a known set of interfaces for usage in model classes. -They allow for Action Pack helpers to interact with non-ActiveRecord models, +They allow for Action Pack helpers to interact with non-Active Record models, for example. Active Model also helps building custom ORMs for use outside of the Rails framework. @@ -11,7 +11,7 @@ code from Rails, or monkey patch entire helpers to make them handle objects that did not exactly conform to the Active Record interface. This would result in code duplication and fragile applications that broke on upgrades. Active Model solves this by defining an explicit API. You can read more about the -API in ActiveModel::Lint::Tests. +API in <tt>ActiveModel::Lint::Tests</tt>. Active Model provides a default module that implements the basic API required to integrate with Action Pack out of the box: <tt>ActiveModel::Model</tt>. diff --git a/activemodel/test/cases/conversion_test.rb b/activemodel/test/cases/conversion_test.rb index d679ad41aa..a037666cbc 100644 --- a/activemodel/test/cases/conversion_test.rb +++ b/activemodel/test/cases/conversion_test.rb @@ -29,4 +29,8 @@ class ConversionTest < ActiveModel::TestCase assert_equal "helicopters/helicopter", Helicopter.new.to_partial_path, "ActiveModel::Conversion#to_partial_path caching should be class-specific" end + + test "to_partial_path handles namespaced models" do + assert_equal "helicopter/comanches/comanche", Helicopter::Comanche.new.to_partial_path + end end diff --git a/activemodel/test/cases/dirty_test.rb b/activemodel/test/cases/dirty_test.rb index 0b9f9537e2..ba45089cca 100644 --- a/activemodel/test/cases/dirty_test.rb +++ b/activemodel/test/cases/dirty_test.rb @@ -46,7 +46,7 @@ class DirtyTest < ActiveModel::TestCase assert @model.name_changed? end - test "list of changed attributes" do + test "list of changed attribute keys" do assert_equal [], @model.changed @model.name = "Paul" assert_equal ['name'], @model.changed @@ -106,6 +106,17 @@ class DirtyTest < ActiveModel::TestCase assert_equal [nil, "Jericho Cane"], @model.previous_changes['name'] end + test "previous value is preserved when changed after save" do + assert_equal({}, @model.changed_attributes) + @model.name = "Paul" + assert_equal({ "name" => nil }, @model.changed_attributes) + + @model.save + + @model.name = "John" + assert_equal({ "name" => "Paul" }, @model.changed_attributes) + end + test "changing the same attribute multiple times retains the correct original value" do @model.name = "Otto" @model.save diff --git a/activemodel/test/cases/model_test.rb b/activemodel/test/cases/model_test.rb index d93fd96b88..588d8e661e 100644 --- a/activemodel/test/cases/model_test.rb +++ b/activemodel/test/cases/model_test.rb @@ -20,7 +20,13 @@ class ModelTest < ActiveModel::TestCase def test_initialize_with_nil_or_empty_hash_params_does_not_explode assert_nothing_raised do BasicModel.new() + BasicModel.new nil BasicModel.new({}) end end + + def test_persisted_is_always_false + object = BasicModel.new(:attr => "value") + assert object.persisted? == false + end end diff --git a/activemodel/test/models/helicopter.rb b/activemodel/test/models/helicopter.rb index a52b6fb4dd..933f3c463a 100644 --- a/activemodel/test/models/helicopter.rb +++ b/activemodel/test/models/helicopter.rb @@ -1,3 +1,7 @@ class Helicopter include ActiveModel::Conversion end + +class Helicopter::Comanche + include ActiveModel::Conversion +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index a6013f754a..20a5ca2baa 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -7,13 +7,15 @@ module ActiveRecord module ConnectionHandling # Establishes a connection to the database that's used by all Active Record objects. def mysql2_connection(config) + config = config.symbolize_keys + config[:username] = 'root' if config[:username].nil? if Mysql2::Client.const_defined? :FOUND_ROWS config[:flags] = Mysql2::Client::FOUND_ROWS end - client = Mysql2::Client.new(config.symbolize_keys) + client = Mysql2::Client.new(config) options = [config[:host], config[:username], config[:password], config[:database], config[:port], config[:socket], 0] ConnectionAdapters::Mysql2Adapter.new(client, logger, options, config) end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index e10b562fa4..8c68576bdc 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -18,9 +18,9 @@ module ActiveRecord # create_database config[:database], config # create_database 'foo_development', encoding: 'unicode' def create_database(name, options = {}) - options = options.reverse_merge(:encoding => "utf8") + options = { encoding: 'utf8' }.merge!(options.symbolize_keys) - option_string = options.symbolize_keys.sum do |key, value| + option_string = options.sum do |key, value| case key when :owner " OWNER = \"#{value}\"" diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 3011f959a5..1b2aa9349e 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -236,26 +236,26 @@ module ActiveRecord alias update_attributes! update! - # Updates a single attribute of an object, without having to explicitly call save on that object. - # - # * Validation is skipped. - # * Callbacks are skipped. - # * updated_at/updated_on column is not updated if that column is available. - # - # Raises an +ActiveRecordError+ when called on new objects, or when the +name+ - # attribute is marked as readonly. + # Equivalent to <code>update_columns(name => value)</code>. def update_column(name, value) update_columns(name => value) end - # Updates the attributes from the passed-in hash, without having to explicitly call save on that object. + # Updates the attributes directly in the database issuing an UPDATE SQL + # statement and sets them in the receiver: # - # * Validation is skipped. + # user.update_columns(last_request_at: Time.current) + # + # This is the fastest way to update attributes because it goes straight to + # the database, but take into account that in consequence the regular update + # procedures are totally bypassed. In particular: + # + # * Validations are skipped. # * Callbacks are skipped. - # * updated_at/updated_on column is not updated if that column is available. + # * +updated_at+/+updated_on+ are not updated. # - # Raises an +ActiveRecordError+ when called on new objects, or when at least - # one of the attributes is marked as readonly. + # This method raises an +ActiveRecord::ActiveRecordError+ when called on new + # objects, or when at least one of the attributes is marked as readonly. def update_columns(attributes) raise ActiveRecordError, "can not update on a new record object" unless persisted? diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 83074e72c1..883d25d80b 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -7,12 +7,12 @@ module ActiveRecord table = default_table if value.is_a?(Hash) - table = Arel::Table.new(column, default_table.engine) - association = klass.reflect_on_association(column.to_sym) - if value.empty? - queries.concat ['1 = 2'] + queries << '1 = 2' else + table = Arel::Table.new(column, default_table.engine) + association = klass.reflect_on_association(column.to_sym) + value.each do |k, v| queries.concat expand(association && association.klass, table, k, v) end diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index 1b4ed1cac4..17378969a5 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -15,18 +15,20 @@ module ActiveRecord establish_connection configuration_without_database connection.create_database configuration['database'], creation_options establish_connection configuration - rescue error_class => error - raise error unless error.errno == ACCESS_DENIED_ERROR - - $stdout.print error.error - establish_connection root_configuration_without_database - connection.create_database configuration['database'], creation_options - connection.execute grant_statement.gsub(/\s+/, ' ').strip - establish_connection configuration - rescue ActiveRecord::StatementInvalid, error_class => error + rescue ActiveRecord::StatementInvalid => error if /database exists/ === error.message raise DatabaseAlreadyExists else + raise + end + rescue error_class => error + if error.respond_to?(:errno) && error.errno == ACCESS_DENIED_ERROR + $stdout.print error.error + establish_connection root_configuration_without_database + connection.create_database configuration['database'], creation_options + connection.execute grant_statement.gsub(/\s+/, ' ').strip + establish_connection configuration + else $stderr.puts "Couldn't create database for #{configuration.inspect}, #{creation_options.inspect}" $stderr.puts "(If you set the charset manually, make sure you have a matching collation)" if configuration['encoding'] end @@ -96,6 +98,8 @@ module ActiveRecord Mysql2::Error elsif defined?(Mysql) Mysql::Error + else + StandardError end end diff --git a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb index 1b4f4a5fc9..01c3e6b49b 100644 --- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb @@ -16,6 +16,7 @@ class PostgresqlActiveSchemaTest < ActiveRecord::TestCase def test_create_database_with_encoding assert_equal %(CREATE DATABASE "matt" ENCODING = 'utf8'), create_database(:matt) assert_equal %(CREATE DATABASE "aimonetti" ENCODING = 'latin1'), create_database(:aimonetti, :encoding => :latin1) + assert_equal %(CREATE DATABASE "aimonetti" ENCODING = 'latin1'), create_database(:aimonetti, 'encoding' => :latin1) end def test_create_database_with_collation_and_ctype diff --git a/activesupport/test/core_ext/range_ext_test.rb b/activesupport/test/core_ext/range_ext_test.rb index 0051c48984..6e94d5e10d 100644 --- a/activesupport/test/core_ext/range_ext_test.rb +++ b/activesupport/test/core_ext/range_ext_test.rb @@ -13,6 +13,12 @@ class RangeTest < ActiveSupport::TestCase date_range = Time.utc(2005, 12, 10, 15, 30)..Time.utc(2005, 12, 10, 17, 30) assert_equal "BETWEEN '2005-12-10 15:30:00' AND '2005-12-10 17:30:00'", date_range.to_s(:db) end + + def test_date_range + assert_instance_of Range, DateTime.new..DateTime.new + assert_instance_of Range, DateTime::Infinity.new..DateTime::Infinity.new + assert_instance_of Range, DateTime.new..DateTime::Infinity.new + end def test_overlaps_last_inclusive assert((1..5).overlaps?(5..10)) diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md index 7c6ef52f4a..590ad5738e 100644 --- a/guides/source/action_mailer_basics.md +++ b/guides/source/action_mailer_basics.md @@ -575,3 +575,23 @@ end ``` In the test we send the email and store the returned object in the `email` variable. We then ensure that it was sent (the first assert), then, in the second batch of assertions, we ensure that the email does indeed contain what we expect. + +Intercepting Emails +------------------- +There are situations where you need to edit an email before it's delivered. Fortunately Action Mailer provides hooks to intercept every email. You can register an interceptor to make modifications to mail messages right before they are handed to the delivery agents. + +```ruby +class SandboxEmailInterceptor + def self.delivering_email(message) + message.to = ['sandbox@example.com'] + end +end +``` + +Before the interceptor can do its job you need to register it with the Action Mailer framework. You can do this in an initializer file `config/initializers/sandbox_email_interceptor.rb` + +```ruby +ActionMailer::Base.register_interceptor(SandboxEmailInterceptor) if Rails.env.staging? +``` + +NOTE: The example above uses a custom environment called "staging" for a production like server but for testing purposes. diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md index a911d6b941..eaa47d4ebf 100644 --- a/guides/source/active_record_validations.md +++ b/guides/source/active_record_validations.md @@ -117,7 +117,6 @@ database only if the object is valid: * `save` * `save!` * `update` -* `update` * `update!` The bang versions (e.g. `save!`) raise an exception if the record is invalid. diff --git a/guides/source/command_line.md b/guides/source/command_line.md index 12d39ea1cc..2790a4740a 100644 --- a/guides/source/command_line.md +++ b/guides/source/command_line.md @@ -355,7 +355,7 @@ rake assets:clean # Remove compiled assets rake assets:precompile # Compile all the assets named in config.assets.precompile rake db:create # Create the database from config/database.yml for the current Rails.env ... -rake log:clear # Truncates all *.log files in log/ to zero bytes +rake log:clear # Truncates all *.log files in log/ to zero bytes (specify which logs with LOGS=test,development) rake middleware # Prints out your Rack middleware stack ... rake tmp:clear # Clear session, cache, and socket files from tmp/ (narrow w/ tmp:sessions:clear, tmp:cache:clear, tmp:sockets:clear) @@ -442,7 +442,7 @@ app/model/post.rb: NOTE. When using specific annotations and custom annotations, the annotation name (FIXME, BUG etc) is not displayed in the output lines. -By default, `rake notes` will look in the `app`, `config`, `lib`, `script` and `test` directories. If you would like to search other directories, you can provide them as a comma separated list in an environment variable `SOURCE_ANNOTATION_DIRECTORIES`. +By default, `rake notes` will look in the `app`, `config`, `lib`, `bin` and `test` directories. If you would like to search other directories, you can provide them as a comma separated list in an environment variable `SOURCE_ANNOTATION_DIRECTORIES`. ```bash $ export SOURCE_ANNOTATION_DIRECTORIES='rspec,vendor' diff --git a/guides/source/documents.yaml b/guides/source/documents.yaml index 13f982d7e2..c73bbeb90d 100644 --- a/guides/source/documents.yaml +++ b/guides/source/documents.yaml @@ -102,7 +102,6 @@ description: This guide documents the asset pipeline. - name: Working with JavaScript in Rails - work_in_progress: true url: working_with_javascript_in_rails.html description: This guide covers the built-in Ajax/JavaScript functionality of Rails. - diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 4fd1a2369f..5dbca2e9b4 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,5 +1,16 @@ ## Rails 4.0.0 (unreleased) ## +* Specify which logs to clear when using the `rake log:clear` task. + (e.g. rake log:clear LOGS=test,staging) + + *Matt Bridges* + +* Allow a `:dirs` key in the `SourceAnnotationExtractor.enumerate` options + to explicitly set the directories to be traversed so it's easier to define + custom rake tasks. + + *Brian D. Burns* + * Deprecate `Rails::Generators::ActiveModel#update_attributes` in favor of `#update`. ORMs that implement `Generators::ActiveModel#update_attributes` should change diff --git a/railties/lib/rails/info.rb b/railties/lib/rails/info.rb index aacc1be2fc..592e74726e 100644 --- a/railties/lib/rails/info.rb +++ b/railties/lib/rails/info.rb @@ -46,7 +46,7 @@ module Rails alias inspect to_s def to_html - (table = '<table>').tap do + '<table>'.tap do |table| properties.each do |(name, value)| table << %(<tr><td class="name">#{CGI.escapeHTML(name.to_s)}</td>) formatted_value = if value.kind_of?(Array) diff --git a/railties/lib/rails/source_annotation_extractor.rb b/railties/lib/rails/source_annotation_extractor.rb index 971fbf627b..2cbb0a435c 100644 --- a/railties/lib/rails/source_annotation_extractor.rb +++ b/railties/lib/rails/source_annotation_extractor.rb @@ -32,15 +32,24 @@ class SourceAnnotationExtractor end # Prints all annotations with tag +tag+ under the root directories +app+, - # +config+, +lib+, and +test+ (recursively). Filenames with extension - # +.builder+, +.rb+, +.erb+, +.haml+, +.slim+, +.css+, +.scss+, +.js+, - # +.coffee+, and +.rake+ are taken into account. The +options+ hash is - # passed to each annotation's +to_s+. + # +config+, +db+, +lib+, and +test+ (recursively). + # + # Additional directories may be added using a comma-delimited list set using + # <tt>ENV['SOURCE_ANNOTATION_DIRECTORIES']</tt>. + # + # Directories may also be explicitly set using the <tt>:dirs</tt> key in +options+. + # + # SourceAnnotationExtractor.enumerate 'TODO|FIXME', dirs: %w(app lib), tag: true + # + # If +options+ has a <tt>:tag</tt> flag, it will be passed to each annotation's +to_s+. + # + # See <tt>#find_in</tt> for a list of file extensions that will be taken into account. # # This class method is the single entry point for the rake tasks. def self.enumerate(tag, options={}) extractor = new(tag) - extractor.display(extractor.find, options) + dirs = options.delete(:dirs) || Annotation.directories + extractor.display(extractor.find(dirs), options) end attr_reader :tag @@ -51,7 +60,7 @@ class SourceAnnotationExtractor # Returns a hash that maps filenames under +dirs+ (recursively) to arrays # with their annotations. - def find(dirs = Annotation.directories) + def find(dirs) dirs.inject({}) { |h, dir| h.update(find_in(dir)) } end @@ -68,16 +77,22 @@ class SourceAnnotationExtractor if File.directory?(item) results.update(find_in(item)) - elsif item =~ /\.(builder|rb|coffee|rake)$/ - results.update(extract_annotations_from(item, /#\s*(#{tag}):?\s*(.*)$/)) - elsif item =~ /\.(css|scss|js)$/ - results.update(extract_annotations_from(item, /\/\/\s*(#{tag}):?\s*(.*)$/)) - elsif item =~ /\.erb$/ - results.update(extract_annotations_from(item, /<%\s*#\s*(#{tag}):?\s*(.*?)\s*%>/)) - elsif item =~ /\.haml$/ - results.update(extract_annotations_from(item, /-\s*#\s*(#{tag}):?\s*(.*)$/)) - elsif item =~ /\.slim$/ - results.update(extract_annotations_from(item, /\/\s*\s*(#{tag}):?\s*(.*)$/)) + else + pattern = + case item + when /\.(builder|rb|coffee|rake)$/ + /#\s*(#{tag}):?\s*(.*)$/ + when /\.(css|scss|js)$/ + /\/\/\s*(#{tag}):?\s*(.*)$/ + when /\.erb$/ + /<%\s*#\s*(#{tag}):?\s*(.*?)\s*%>/ + when /\.haml$/ + /-\s*#\s*(#{tag}):?\s*(.*)$/ + when /\.slim$/ + /\/\s*\s*(#{tag}):?\s*(.*)$/ + else nil + end + results.update(extract_annotations_from(item, pattern)) if pattern end end diff --git a/railties/lib/rails/tasks/log.rake b/railties/lib/rails/tasks/log.rake index 6e1334692e..6c3f02eb0c 100644 --- a/railties/lib/rails/tasks/log.rake +++ b/railties/lib/rails/tasks/log.rake @@ -1,9 +1,23 @@ namespace :log do - desc "Truncates all *.log files in log/ to zero bytes" + desc "Truncates all *.log files in log/ to zero bytes (specify which logs with LOGS=test,development)" task :clear do - FileList["log/*.log"].each do |log_file| - f = File.open(log_file, "w") - f.close + log_files.each do |file| + clear_log_file(file) end end + + def log_files + if ENV['LOGS'] + ENV['LOGS'].split(',') + .map { |file| "log/#{file.strip}.log" } + .select { |file| File.exists?(file) } + else + FileList["log/*.log"] + end + end + + def clear_log_file(file) + f = File.open(file, "w") + f.close + end end diff --git a/railties/test/application/middleware/cache_test.rb b/railties/test/application/middleware/cache_test.rb index c99666d7e4..b8e0c9be60 100644 --- a/railties/test/application/middleware/cache_test.rb +++ b/railties/test/application/middleware/cache_test.rb @@ -81,8 +81,8 @@ module ApplicationTests add_to_config "config.action_dispatch.rack_cache = true" get "/expires/expires_header" - assert_equal "miss, store", last_response.headers["X-Rack-Cache"] - assert_equal "max-age=10, public", last_response.headers["Cache-Control"] + assert_equal "miss, ignore, store", last_response.headers["X-Rack-Cache"] + assert_equal "max-age=10, public", last_response.headers["Cache-Control"] body = last_response.body @@ -115,8 +115,8 @@ module ApplicationTests add_to_config "config.action_dispatch.rack_cache = true" get "/expires/expires_etag" - assert_equal "miss, store", last_response.headers["X-Rack-Cache"] - assert_equal "public", last_response.headers["Cache-Control"] + assert_equal "miss, ignore, store", last_response.headers["X-Rack-Cache"] + assert_equal "public", last_response.headers["Cache-Control"] body = last_response.body etag = last_response.headers["ETag"] @@ -149,8 +149,8 @@ module ApplicationTests add_to_config "config.action_dispatch.rack_cache = true" get "/expires/expires_last_modified" - assert_equal "miss, store", last_response.headers["X-Rack-Cache"] - assert_equal "public", last_response.headers["Cache-Control"] + assert_equal "miss, ignore, store", last_response.headers["X-Rack-Cache"] + assert_equal "public", last_response.headers["Cache-Control"] body = last_response.body last = last_response.headers["Last-Modified"] diff --git a/railties/test/application/middleware/static_test.rb b/railties/test/application/middleware/static_test.rb new file mode 100644 index 0000000000..0a793f8f60 --- /dev/null +++ b/railties/test/application/middleware/static_test.rb @@ -0,0 +1,31 @@ +# encoding: utf-8 +require 'isolation/abstract_unit' +require 'rack/test' + +module ApplicationTests + class MiddlewareStaticTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + include Rack::Test::Methods + + def setup + build_app + FileUtils.rm_rf "#{app_path}/config/environments" + end + + def teardown + teardown_app + end + + # Regression test to #8907 + # See https://github.com/rails/rails/commit/9cc82b77196d21a5c7021f6dca59ab9b2b158a45#commitcomment-2416514 + test "doesn't set Cache-Control header when it is nil" do + app_file "public/foo.html", 'static' + + require "#{app_path}/config/environment" + + get 'foo' + + assert_not last_response.headers.has_key?('Cache-Control'), "Cache-Control should not be set" + end + end +end diff --git a/railties/test/application/rake/notes_test.rb b/railties/test/application/rake/notes_test.rb index 5893d58925..3508f4225a 100644 --- a/railties/test/application/rake/notes_test.rb +++ b/railties/test/application/rake/notes_test.rb @@ -130,6 +130,45 @@ module ApplicationTests end end + test 'custom rake task finds specific notes in specific directories' do + app_file "app/controllers/some_controller.rb", "# TODO: note in app directory" + app_file "lib/some_file.rb", "# OPTIMIZE: note in lib directory\n" << "# FIXME: note in lib directory" + app_file "test/some_test.rb", 1000.times.map { "" }.join("\n") << "# TODO: note in test directory" + + app_file "lib/tasks/notes_custom.rake", <<-EOS + require 'rails/source_annotation_extractor' + task :notes_custom do + tags = 'TODO|FIXME' + opts = { dirs: %w(lib test), tag: true } + SourceAnnotationExtractor.enumerate(tags, opts) + end + EOS + + boot_rails + + require 'rake' + require 'rdoc/task' + require 'rake/testtask' + + Rails.application.load_tasks + + Dir.chdir(app_path) do + output = `bundle exec rake notes_custom` + lines = output.scan(/\[([0-9\s]+)\]/).flatten + + assert_match(/\[FIXME\] note in lib directory/, output) + assert_match(/\[TODO\] note in test directory/, output) + assert_no_match(/OPTIMIZE/, output) + assert_no_match(/note in app directory/, output) + + assert_equal 2, lines.size + + lines.each do |line_number| + assert_equal 4, line_number.size + end + end + end + private def boot_rails super diff --git a/railties/test/application/runner_test.rb b/railties/test/application/runner_test.rb index f65b5e2f2d..6595c40f8b 100644 --- a/railties/test/application/runner_test.rb +++ b/railties/test/application/runner_test.rb @@ -37,27 +37,27 @@ module ApplicationTests end def test_should_run_file - app_file "script/count_users.rb", <<-SCRIPT + app_file "bin/count_users.rb", <<-SCRIPT puts User.count SCRIPT - assert_match "42", Dir.chdir(app_path) { `bundle exec rails runner "script/count_users.rb"` } + assert_match "42", Dir.chdir(app_path) { `bundle exec rails runner "bin/count_users.rb"` } end def test_should_set_dollar_0_to_file - app_file "script/dollar0.rb", <<-SCRIPT + app_file "bin/dollar0.rb", <<-SCRIPT puts $0 SCRIPT - assert_match "script/dollar0.rb", Dir.chdir(app_path) { `bundle exec rails runner "script/dollar0.rb"` } + assert_match "bin/dollar0.rb", Dir.chdir(app_path) { `bundle exec rails runner "bin/dollar0.rb"` } end def test_should_set_dollar_program_name_to_file - app_file "script/program_name.rb", <<-SCRIPT + app_file "bin/program_name.rb", <<-SCRIPT puts $PROGRAM_NAME SCRIPT - assert_match "script/program_name.rb", Dir.chdir(app_path) { `bundle exec rails runner "script/program_name.rb"` } + assert_match "bin/program_name.rb", Dir.chdir(app_path) { `bundle exec rails runner "bin/program_name.rb"` } end def test_with_hook |