diff options
141 files changed, 2119 insertions, 895 deletions
@@ -66,7 +66,7 @@ task :update_versions do version_file = File.read("version.rb") PROJECTS.each do |project| - Dir["#{project}/lib/*/version.rb"].each do |file| + Dir["#{project}/lib/*/gem_version.rb"].each do |file| File.open(file, "w") do |f| f.write version_file.gsub(/Rails/, constants[project]) end diff --git a/actionmailer/README.rdoc b/actionmailer/README.rdoc index e425282fa8..67b64fe469 100644 --- a/actionmailer/README.rdoc +++ b/actionmailer/README.rdoc @@ -102,7 +102,7 @@ Example: ) if email.has_attachments? - email.attachments.each do |attachment| + email.attachments.each do |attachment| page.attachments.create({ file: attachment, description: email.subject }) diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 18a41ba7a4..951a3e5fb5 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -94,7 +94,7 @@ module ActionMailer # Hi <%= @account.name %>, # Thanks for joining our service! Please check back often. # - # You can even use Action Pack helpers in these views. For example: + # You can even use Action View helpers in these views. For example: # # You got a new note! # <%= truncate(@note.body, length: 25) %> diff --git a/actionmailer/lib/action_mailer/gem_version.rb b/actionmailer/lib/action_mailer/gem_version.rb new file mode 100644 index 0000000000..b564813ccf --- /dev/null +++ b/actionmailer/lib/action_mailer/gem_version.rb @@ -0,0 +1,15 @@ +module ActionMailer + # Returns the version of the currently loaded ActionMailer as a <tt>Gem::Version</tt> + def self.gem_version + Gem::Version.new VERSION::STRING + end + + module VERSION + MAJOR = 4 + MINOR = 2 + TINY = 0 + PRE = "alpha" + + STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") + end +end diff --git a/actionmailer/lib/action_mailer/version.rb b/actionmailer/lib/action_mailer/version.rb index 7b2cf305c2..a98aec913f 100644 --- a/actionmailer/lib/action_mailer/version.rb +++ b/actionmailer/lib/action_mailer/version.rb @@ -1,11 +1,8 @@ +require_relative 'gem_version' + module ActionMailer - # Returns the version of the currently loaded ActionMailer as a Gem::Version + # Returns the version of the currently loaded ActionMailer as a <tt>Gem::Version</tt> def self.version - Gem::Version.new "4.2.0.alpha" - end - - module VERSION #:nodoc: - MAJOR, MINOR, TINY, PRE = ActionMailer.version.segments - STRING = ActionMailer.version.to_s + gem_version end end diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb index 02707d0b5f..d4a89a17ee 100644 --- a/actionmailer/test/base_test.rb +++ b/actionmailer/test/base_test.rb @@ -13,6 +13,7 @@ class BaseTest < ActiveSupport::TestCase def teardown ActionMailer::Base.asset_host = nil ActionMailer::Base.assets_dir = nil + ActionMailer::Base.preview_interceptors.clear end test "method call to mail does not raise error" do diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 66cef08b1b..c23577de9b 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,9 @@ +* Make logging of CSRF failures optional (but on by default) with the + `log_warning_on_csrf_failure` configuration setting in + `ActionController::RequestForgeryProtection`. + + *John Barton* + * Fix URL generation in controller tests with request-dependent `default_url_options` methods. diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index e920a33765..b1acca2435 100644 --- a/actionpack/lib/action_controller/log_subscriber.rb +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -54,9 +54,9 @@ module ActionController end def deep_munge(event) - message = "Value for params[:#{event.payload[:keys].join('][:')}] was set"\ - "to nil, because it was one of [], [null] or [null, null, ...]."\ - "Go to http://guides.rubyonrails.org/security.html#unsafe-query-generation"\ + message = "Value for params[:#{event.payload[:keys].join('][:')}] was set "\ + "to nil, because it was one of [], [null] or [null, null, ...]. "\ + "Go to http://guides.rubyonrails.org/security.html#unsafe-query-generation "\ "for more information."\ debug(message) diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb index fdf4ef293d..41b997a755 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -107,8 +107,11 @@ module ActionController end class Buffer < ActionDispatch::Response::Buffer #:nodoc: + include MonitorMixin + def initialize(response) - @error_callback = nil + @error_callback = lambda { true } + @cv = new_cond super(response, SizedQueue.new(10)) end @@ -122,14 +125,25 @@ module ActionController end def each + @response.sending! while str = @buf.pop yield str end + @response.sent! end def close - super - @buf.push nil + synchronize do + super + @buf.push nil + @cv.broadcast + end + end + + def await_close + synchronize do + @cv.wait_until { @closed } + end end def on_error(&block) @@ -165,12 +179,20 @@ module ActionController end end - def commit! - headers.freeze + private + + def before_committed super + jar = request.cookie_jar + # The response can be committed multiple times + jar.write self unless committed? end - private + def before_sending + super + request.cookie_jar.commit! + headers.freeze + end def build_buffer(response, body) buf = Live::Buffer.new response @@ -191,6 +213,7 @@ module ActionController t1 = Thread.current locals = t1.keys.map { |key| [key, t1[key]] } + error = nil # This processes the action in a child thread. It lets us return the # response code and headers back up the rack stack, and still process # the body in parallel with sending data to the client @@ -205,8 +228,9 @@ module ActionController begin super(name) rescue => e - @_response.status = 500 unless @_response.committed? - @_response.status = 400 if e.class == ActionController::BadRequest + unless @_response.committed? + error = e + end begin @_response.stream.write(ActionView::Base.streaming_completion_on_exception) if request.format == :html @_response.stream.call_on_error @@ -222,6 +246,7 @@ module ActionController } @_response.await_commit + raise error if error end def log_error(exception) diff --git a/actionpack/lib/action_controller/metal/rack_delegation.rb b/actionpack/lib/action_controller/metal/rack_delegation.rb index e1bee9e60c..bdf6e88699 100644 --- a/actionpack/lib/action_controller/metal/rack_delegation.rb +++ b/actionpack/lib/action_controller/metal/rack_delegation.rb @@ -5,8 +5,8 @@ module ActionController module RackDelegation extend ActiveSupport::Concern - delegate :headers, :status=, :location=, :content_type=, :no_content_type=, - :status, :location, :content_type, :no_content_type, :to => "@_response" + delegate :headers, :status=, :location=, :content_type=, + :status, :location, :content_type, :to => "@_response" def dispatch(action, request) set_response!(request) diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index 3c4ef596c7..93e7d6954c 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -45,9 +45,7 @@ module ActionController def _process_format(format, options = {}) super - if options[:body] - self.headers.delete "Content-Type" - elsif options[:plain] + if options[:plain] self.content_type = Mime::TEXT else self.content_type ||= format.to_s diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index c88074d4c6..e3b1f5ae7c 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -68,6 +68,10 @@ module ActionController #:nodoc: config_accessor :allow_forgery_protection self.allow_forgery_protection = true if allow_forgery_protection.nil? + # Controls whether a CSRF failure logs a warning. On by default. + config_accessor :log_warning_on_csrf_failure + self.log_warning_on_csrf_failure = true + helper_method :form_authenticity_token helper_method :protect_against_forgery? end @@ -193,7 +197,9 @@ module ActionController #:nodoc: mark_for_same_origin_verification! if !verified_request? - logger.warn "Can't verify CSRF token authenticity" if logger + if logger && log_warning_on_csrf_failure + logger.warn "Can't verify CSRF token authenticity" + end handle_unverified_request end end diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 8650b75400..e9166d8747 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -258,6 +258,29 @@ module ActionController end end + class LiveTestResponse < Live::Response + def recycle! + @body = nil + initialize + end + + def body + @body ||= super + end + + # Was the response successful? + alias_method :success?, :successful? + + # Was the URL not found? + alias_method :missing?, :not_found? + + # Were we redirected? + alias_method :redirect?, :redirection? + + # Was there a server-side error? + alias_method :error?, :server_error? + end + # Methods #destroy and #load! are overridden to avoid calling methods on the # @store object, which does not exist for the TestSession class. class TestSession < Rack::Session::Abstract::SessionHash #:nodoc: @@ -572,7 +595,9 @@ module ActionController @controller.process(name) if cookies = @request.env['action_dispatch.cookies'] - cookies.write(@response) + unless @response.committed? + cookies.write(@response) + end end @response.prepare! @@ -583,13 +608,14 @@ module ActionController end def setup_controller_request_and_response - @request = build_request - @response = build_response - @response.request = @request - @controller = nil unless defined? @controller + response_klass = TestResponse + if klass = self.class.controller_class + if klass < ActionController::Live + response_klass = LiveTestResponse + end unless @controller begin @controller = klass.new @@ -599,6 +625,10 @@ module ActionController end end + @request = build_request + @response = build_response response_klass + @response.request = @request + if @controller @controller.request = @request @controller.params = {} @@ -609,8 +639,8 @@ module ActionController TestRequest.new end - def build_response - TestResponse.new + def build_response(klass) + klass.new end included do diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 1318c62fbe..daa06e96e6 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -152,6 +152,13 @@ module ActionDispatch Http::Headers.new(@env) end + # Returns a +String+ with the last requested path including their params. + # + # # get '/foo' + # request.original_fullpath # => '/foo' + # + # # get '/foo?bar' + # request.original_fullpath # => '/foo?bar' def original_fullpath @original_fullpath ||= (env["ORIGINAL_FULLPATH"] || fullpath) end diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index f14ca1ea44..3d27ff2b24 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -63,8 +63,6 @@ module ActionDispatch # :nodoc: # content you're giving them, so we need to send that along. attr_accessor :charset - attr_accessor :no_content_type # :nodoc: - CONTENT_TYPE = "Content-Type".freeze SET_COOKIE = "Set-Cookie".freeze LOCATION = "Location".freeze @@ -93,7 +91,10 @@ module ActionDispatch # :nodoc: end def each(&block) - @buf.each(&block) + @response.sending! + x = @buf.each(&block) + @response.sent! + x end def close @@ -120,6 +121,8 @@ module ActionDispatch # :nodoc: @blank = false @cv = new_cond @committed = false + @sending = false + @sent = false @content_type = nil @charset = nil @@ -140,17 +143,37 @@ module ActionDispatch # :nodoc: end end + def await_sent + synchronize { @cv.wait_until { @sent } } + end + def commit! synchronize do + before_committed @committed = true @cv.broadcast end end - def committed? - @committed + def sending! + synchronize do + before_sending + @sending = true + @cv.broadcast + end + end + + def sent! + synchronize do + @sent = true + @cv.broadcast + end end + def sending?; synchronize { @sending }; end + def committed?; synchronize { @committed }; end + def sent?; synchronize { @sent }; end + # Sets the HTTP status code. def status=(status) @status = Rack::Utils.status_code(status) @@ -275,6 +298,12 @@ module ActionDispatch # :nodoc: private + def before_committed + end + + def before_sending + end + def merge_default_headers(original, default) return original unless default.respond_to?(:merge) @@ -305,17 +334,8 @@ module ActionDispatch # :nodoc: !@sending_file && @charset != false end - def remove_content_type! - headers.delete CONTENT_TYPE - end - def rack_response(status, header) - if no_content_type - remove_content_type! - else - assign_default_content_type_and_charset!(header) - end - + assign_default_content_type_and_charset!(header) handle_conditional_get! header[SET_COOKIE] = header[SET_COOKIE].join("\n") if header[SET_COOKIE].respond_to?(:join) diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb index 4410c1b5d5..57f0963731 100644 --- a/actionpack/lib/action_dispatch/journey/formatter.rb +++ b/actionpack/lib/action_dispatch/journey/formatter.rb @@ -121,9 +121,9 @@ module ActionDispatch def possibles(cache, options, depth = 0) cache.fetch(:___routes) { [] } + options.find_all { |pair| cache.key?(pair) - }.map { |pair| + }.flat_map { |pair| possibles(cache[pair], options, depth + 1) - }.flatten(1) + } end # Returns +true+ if no missing keys are present, otherwise +false+. diff --git a/actionpack/lib/action_dispatch/journey/gtg/builder.rb b/actionpack/lib/action_dispatch/journey/gtg/builder.rb index 7d2791714b..450588cda6 100644 --- a/actionpack/lib/action_dispatch/journey/gtg/builder.rb +++ b/actionpack/lib/action_dispatch/journey/gtg/builder.rb @@ -27,7 +27,7 @@ module ActionDispatch marked[s] = true # mark s s.group_by { |state| symbol(state) }.each do |sym, ps| - u = ps.map { |l| followpos(l) }.flatten + u = ps.flat_map { |l| followpos(l) } next if u.empty? if u.uniq == [DUMMY] @@ -90,7 +90,7 @@ module ActionDispatch firstpos(node.left) end when Nodes::Or - node.children.map { |c| firstpos(c) }.flatten.uniq + node.children.flat_map { |c| firstpos(c) }.uniq when Nodes::Unary firstpos(node.left) when Nodes::Terminal @@ -105,7 +105,7 @@ module ActionDispatch when Nodes::Star firstpos(node.left) when Nodes::Or - node.children.map { |c| lastpos(c) }.flatten.uniq + node.children.flat_map { |c| lastpos(c) }.uniq when Nodes::Cat if nullable?(node.right) lastpos(node.left) | lastpos(node.right) diff --git a/actionpack/lib/action_dispatch/journey/gtg/simulator.rb b/actionpack/lib/action_dispatch/journey/gtg/simulator.rb index 58ad803841..254c2befc4 100644 --- a/actionpack/lib/action_dispatch/journey/gtg/simulator.rb +++ b/actionpack/lib/action_dispatch/journey/gtg/simulator.rb @@ -31,7 +31,7 @@ module ActionDispatch return if acceptance_states.empty? - memos = acceptance_states.map { |x| tt.memo(x) }.flatten.compact + memos = acceptance_states.flat_map { |x| tt.memo(x) }.compact MatchData.new(memos) end diff --git a/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb index 5a79059ed6..e6212b1ee2 100644 --- a/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb +++ b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb @@ -114,17 +114,17 @@ module ActionDispatch end def states - ss = @string_states.keys + @string_states.values.map(&:values).flatten - rs = @regexp_states.keys + @regexp_states.values.map(&:values).flatten + ss = @string_states.keys + @string_states.values.flat_map(&:values) + rs = @regexp_states.keys + @regexp_states.values.flat_map(&:values) (ss + rs).uniq end def transitions - @string_states.map { |from, hash| + @string_states.flat_map { |from, hash| hash.map { |s, to| [from, s, to] } - }.flatten(1) + @regexp_states.map { |from, hash| + } + @regexp_states.flat_map { |from, hash| hash.map { |s, to| [from, s, to] } - }.flatten(1) + } end private @@ -143,11 +143,11 @@ module ActionDispatch def move_regexp(t, a) return [] if t.empty? - t.map { |s| + t.flat_map { |s| if states = @regexp_states[s] states.map { |re, v| re === a ? v : nil } end - }.flatten.compact.uniq + }.compact.uniq end def move_string(t, a) diff --git a/actionpack/lib/action_dispatch/journey/nfa/dot.rb b/actionpack/lib/action_dispatch/journey/nfa/dot.rb index 5c33a872e5..47bf76bdbf 100644 --- a/actionpack/lib/action_dispatch/journey/nfa/dot.rb +++ b/actionpack/lib/action_dispatch/journey/nfa/dot.rb @@ -16,9 +16,9 @@ module ActionDispatch # end # " #{n.object_id} [label=\"#{label}\", shape=box];" #} - #memo_edges = memos.map { |k, memos| + #memo_edges = memos.flat_map { |k, memos| # (memos || []).map { |v| " #{k} -> #{v.object_id};" } - #}.flatten.uniq + #}.uniq <<-eodot digraph nfa { diff --git a/actionpack/lib/action_dispatch/journey/nfa/simulator.rb b/actionpack/lib/action_dispatch/journey/nfa/simulator.rb index 5b40da6569..b23270db3c 100644 --- a/actionpack/lib/action_dispatch/journey/nfa/simulator.rb +++ b/actionpack/lib/action_dispatch/journey/nfa/simulator.rb @@ -34,7 +34,7 @@ module ActionDispatch return if acceptance_states.empty? - memos = acceptance_states.map { |x| tt.memo(x) }.flatten.compact + memos = acceptance_states.flat_map { |x| tt.memo(x) }.compact MatchData.new(memos) end diff --git a/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb b/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb index a3017aeea1..66e414213a 100644 --- a/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb +++ b/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb @@ -42,7 +42,7 @@ module ActionDispatch end def states - (@table.keys + @table.values.map(&:keys).flatten).uniq + (@table.keys + @table.values.flat_map(&:keys)).uniq end # Returns a generalized transition graph with reduced states. The states @@ -93,7 +93,7 @@ module ActionDispatch # Returns set of NFA states to which there is a transition on ast symbol # +a+ from some state +s+ in +t+. def following_states(t, a) - Array(t).map { |s| inverted[s][a] }.flatten.uniq + Array(t).flat_map { |s| inverted[s][a] }.uniq end # Returns set of NFA states to which there is a transition on ast symbol @@ -107,7 +107,7 @@ module ActionDispatch end def alphabet - inverted.values.map(&:keys).flatten.compact.uniq.sort_by { |x| x.to_s } + inverted.values.flat_map(&:keys).compact.uniq.sort_by { |x| x.to_s } end # Returns a set of NFA states reachable from some NFA state +s+ in set @@ -131,9 +131,9 @@ module ActionDispatch end def transitions - @table.map { |to, hash| + @table.flat_map { |to, hash| hash.map { |from, sym| [from, sym, to] } - }.flatten(1) + } end private diff --git a/actionpack/lib/action_dispatch/journey/path/pattern.rb b/actionpack/lib/action_dispatch/journey/path/pattern.rb index d37aa1fbe5..fb155e516f 100644 --- a/actionpack/lib/action_dispatch/journey/path/pattern.rb +++ b/actionpack/lib/action_dispatch/journey/path/pattern.rb @@ -53,9 +53,9 @@ module ActionDispatch end def optional_names - @optional_names ||= spec.grep(Nodes::Group).map { |group| + @optional_names ||= spec.grep(Nodes::Group).flat_map { |group| group.grep(Nodes::Symbol) - }.flatten.map { |n| n.name }.uniq + }.map { |n| n.name }.uniq end class RegexpOffsets < Journey::Visitors::Visitor # :nodoc: diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 18e64704f6..c0039fa3f5 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -74,7 +74,7 @@ module ActionDispatch # # domain: nil # Does not sets cookie domain. (default) # domain: :all # Allow the cookie for the top most level - # domain and subdomains. + # # domain and subdomains. # # * <tt>:expires</tt> - The time at which this cookie expires, as a \Time object. # * <tt>:secure</tt> - Whether this cookie is only transmitted to HTTPS servers. @@ -237,6 +237,15 @@ module ActionDispatch @secure = secure @options = options @cookies = {} + @committed = false + end + + def committed?; @committed; end + + def commit! + @committed = true + @set_cookies.freeze + @delete_cookies.freeze end def each(&block) @@ -336,8 +345,8 @@ module ActionDispatch end def recycle! #:nodoc: - @set_cookies.clear - @delete_cookies.clear + @set_cookies = {} + @delete_cookies = {} end mattr_accessor :always_write_cookie @@ -551,9 +560,11 @@ module ActionDispatch status, headers, body = @app.call(env) if cookie_jar = env['action_dispatch.cookies'] - cookie_jar.write(headers) - if headers[HTTP_HEADER].respond_to?(:join) - headers[HTTP_HEADER] = headers[HTTP_HEADER].join("\n") + unless cookie_jar.committed? + cookie_jar.write(headers) + if headers[HTTP_HEADER].respond_to?(:join) + headers[HTTP_HEADER] = headers[HTTP_HEADER].join("\n") + end end end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 0b762aa9a4..357829e59f 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -708,7 +708,8 @@ module ActionDispatch options[:constraints] ||= {} unless shallow? - options[:shallow_path] = options[:path] if args.any? + options[:shallow_path] ||= options[:path] if options.key?(:path) + options[:shallow_prefix] ||= options[:as] if options.key?(:as) end if options[:constraints].is_a?(Hash) @@ -792,9 +793,16 @@ module ActionDispatch # end def namespace(path, options = {}) path = path.to_s - options = { :path => path, :as => path, :module => path, - :shallow_path => path, :shallow_prefix => path }.merge!(options) - scope(options) { yield } + + defaults = { + module: path, + path: options.fetch(:path, path), + as: options.fetch(:as, path), + shallow_path: options.fetch(:path, path), + shallow_prefix: options.fetch(:as, path) + } + + scope(defaults.merge!(options)) { yield } end # === Parameter Restriction @@ -1323,8 +1331,10 @@ module ActionDispatch end with_scope_level(:member) do - scope(parent_resource.member_scope) do - yield + if shallow? + shallow_scope(parent_resource.member_scope) { yield } + else + scope(parent_resource.member_scope) { yield } end end end @@ -1347,16 +1357,8 @@ module ActionDispatch end with_scope_level(:nested) do - if shallow? - with_exclusive_scope do - if @scope[:shallow_path].blank? - scope(parent_resource.nested_scope, nested_options) { yield } - else - scope(@scope[:shallow_path], :as => @scope[:shallow_prefix]) do - scope(parent_resource.nested_scope, nested_options) { yield } - end - end - end + if shallow? && nesting_depth > 1 + shallow_scope(parent_resource.nested_scope, nested_options) { yield } else scope(parent_resource.nested_scope, nested_options) { yield } end @@ -1558,21 +1560,23 @@ module ActionDispatch end end - def with_scope_level(kind, resource = parent_resource) + def with_scope_level(kind) old, @scope[:scope_level] = @scope[:scope_level], kind - old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource yield ensure @scope[:scope_level] = old - @scope[:scope_level_resource] = old_resource end def resource_scope(kind, resource) #:nodoc: - with_scope_level(kind, resource) do - scope(parent_resource.resource_scope) do - yield - end + old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource + @nesting.push(resource) + + with_scope_level(kind) do + scope(parent_resource.resource_scope) { yield } end + ensure + @nesting.pop + @scope[:scope_level_resource] = old_resource end def nested_options #:nodoc: @@ -1584,6 +1588,10 @@ module ActionDispatch options end + def nesting_depth #:nodoc: + @nesting.size + end + def param_constraint? #:nodoc: @scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp) end @@ -1596,18 +1604,20 @@ module ActionDispatch flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s) end - def shallow_scoping? #:nodoc: - shallow? && @scope[:scope_level] == :member + def shallow_scope(path, options = {}) #:nodoc: + old_name_prefix, old_path = @scope[:as], @scope[:path] + @scope[:as], @scope[:path] = @scope[:shallow_prefix], @scope[:shallow_path] + + scope(path, options) { yield } + ensure + @scope[:as], @scope[:path] = old_name_prefix, old_path end def path_for_action(action, path) #:nodoc: - prefix = shallow_scoping? ? - "#{@scope[:shallow_path]}/#{parent_resource.shallow_scope}" : @scope[:path] - if canonical_action?(action, path.blank?) - prefix.to_s + @scope[:path].to_s else - "#{prefix}/#{action_path(action, path)}" + "#{@scope[:path]}/#{action_path(action, path)}" end end @@ -1645,7 +1655,7 @@ module ActionDispatch when :new [prefix, :new, name_prefix, member_name] when :member - [prefix, shallow_scoping? ? @scope[:shallow_prefix] : name_prefix, member_name] + [prefix, name_prefix, member_name] when :root [name_prefix, collection_name, prefix] else @@ -1786,6 +1796,7 @@ module ActionDispatch @set = set @scope = { :path_names => @set.resources_path_names } @concerns = {} + @nesting = [] end include Base diff --git a/actionpack/lib/action_pack/gem_version.rb b/actionpack/lib/action_pack/gem_version.rb new file mode 100644 index 0000000000..beaf35d3da --- /dev/null +++ b/actionpack/lib/action_pack/gem_version.rb @@ -0,0 +1,15 @@ +module ActionPack + # Returns the version of the currently loaded ActionPack as a <tt>Gem::Version</tt> + def self.gem_version + Gem::Version.new VERSION::STRING + end + + module VERSION + MAJOR = 4 + MINOR = 2 + TINY = 0 + PRE = "alpha" + + STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") + end +end diff --git a/actionpack/lib/action_pack/version.rb b/actionpack/lib/action_pack/version.rb index 75fb0d9532..7088cd2760 100644 --- a/actionpack/lib/action_pack/version.rb +++ b/actionpack/lib/action_pack/version.rb @@ -1,11 +1,8 @@ +require_relative 'gem_version' + module ActionPack - # Returns the version of the currently loaded ActionPack as a Gem::Version + # Returns the version of the currently loaded ActionPack as a <tt>Gem::Version</tt> def self.version - Gem::Version.new "4.2.0.alpha" - end - - module VERSION #:nodoc: - MAJOR, MINOR, TINY, PRE = ActionPack.version.segments - STRING = ActionPack.version.to_s + gem_version end end diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index 57b45b8f7b..591d0eccc3 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -243,8 +243,8 @@ CACHED end private - def template_digest(name, format) - ActionView::Digestor.digest(name, format, @controller.lookup_context) + def template_digest(name, format, variant = nil) + ActionView::Digestor.digest(name: name, format: format, variant: variant, finder: @controller.lookup_context) end end diff --git a/actionpack/test/controller/live_stream_test.rb b/actionpack/test/controller/live_stream_test.rb index fb6a750089..2fd5c930ba 100644 --- a/actionpack/test/controller/live_stream_test.rb +++ b/actionpack/test/controller/live_stream_test.rb @@ -1,5 +1,6 @@ require 'abstract_unit' require 'active_support/concurrency/latch' +Thread.abort_on_exception = true module ActionController class SSETest < ActionController::TestCase @@ -43,9 +44,7 @@ module ActionController tests SSETestController def wait_for_response_stream_close - while !response.stream.closed? - sleep 0.01 - end + response.body end def test_basic_sse @@ -91,6 +90,9 @@ module ActionController end class LiveStreamTest < ActionController::TestCase + class Exception < StandardError + end + class TestController < ActionController::Base include ActionController::Live @@ -100,6 +102,12 @@ module ActionController 'test' end + def set_cookie + cookies[:hello] = "world" + response.stream.write "hello world" + response.close + end + def render_text render :text => 'zomg' end @@ -153,11 +161,12 @@ module ActionController response.stream.close end + response.stream.write "" # make sure the response is committed raise 'An exception occurred...' end def exception_in_controller - raise 'Exception in controller' + raise Exception, 'Exception in controller' end def bad_request_error @@ -169,24 +178,16 @@ module ActionController response.stream.on_error do raise 'We need to go deeper.' end + response.stream.write '' response.stream.write params[:widget][:didnt_check_for_nil] end end tests TestController - class TestResponse < Live::Response - def recycle! - initialize - end - end - - def build_response - TestResponse.new - end - def assert_stream_closed assert response.stream.closed?, 'stream should be closed' + assert response.sent?, 'stream should be sent' end def capture_log_output @@ -200,6 +201,13 @@ module ActionController end end + def test_set_cookie + @controller = TestController.new + get :set_cookie + assert_equal({'hello' => 'world'}, @response.cookies) + assert_equal "hello world", @response.body + end + def test_set_response! @controller.set_response!(@request) assert_kind_of(Live::Response, @controller.response) @@ -221,6 +229,7 @@ module ActionController @controller.response = @response t = Thread.new(@response) { |resp| + resp.await_commit resp.stream.each do |part| assert_equal parts.shift, part ol = @controller.latch @@ -257,24 +266,20 @@ module ActionController end def test_exception_handling_html - capture_log_output do |output| + assert_raises(ActionView::MissingTemplate) do get :exception_in_view - assert_match %r((window\.location = "/500\.html"</script></html>)$), response.body - assert_match 'Missing template test/doesntexist', output.rewind && output.read - assert_stream_closed end + assert response.body + assert_stream_closed end def test_exception_handling_plain_text - capture_log_output do |output| + assert_raises(ActionView::MissingTemplate) do get :exception_in_view, format: :json - assert_equal '', response.body - assert_match 'Missing template test/doesntexist', output.rewind && output.read - assert_stream_closed end end - def test_exception_callback + def test_exception_callback_when_committed capture_log_output do |output| get :exception_with_callback, format: 'text/event-stream' assert_equal %(data: "500 Internal Server Error"\n\n), response.body @@ -284,16 +289,18 @@ module ActionController end def test_exception_in_controller_before_streaming - response = get :exception_in_controller, format: 'text/event-stream' - assert_equal 500, response.status + assert_raises(ActionController::LiveStreamTest::Exception) do + get :exception_in_controller, format: 'text/event-stream' + end end def test_bad_request_in_controller_before_streaming - response = get :bad_request_error, format: 'text/event-stream' - assert_equal 400, response.status + assert_raises(ActionController::BadRequest) do + get :bad_request_error, format: 'text/event-stream' + end end - def test_exceptions_raised_handling_exceptions + def test_exceptions_raised_handling_exceptions_and_committed capture_log_output do |output| get :exception_in_exception_callback, format: 'text/event-stream' assert_equal '', response.body @@ -313,4 +320,11 @@ module ActionController assert_equal 304, @response.status.to_i end end + + class BufferTest < ActionController::TestCase + def test_nil_callback + buf = ActionController::Live::Buffer.new nil + assert buf.call_on_error + end + end end diff --git a/actionpack/test/controller/new_base/render_body_test.rb b/actionpack/test/controller/new_base/render_body_test.rb index a7e4f87bd9..fad848349a 100644 --- a/actionpack/test/controller/new_base/render_body_test.rb +++ b/actionpack/test/controller/new_base/render_body_test.rb @@ -65,6 +65,11 @@ module RenderBody render body: "hello world", layout: "greetings" end + def with_custom_content_type + response.headers['Content-Type'] = 'application/json' + render body: '["troll","face"]' + end + def with_ivar_in_layout @ivar = "hello world" render body: "hello world", layout: "ivar" @@ -141,6 +146,13 @@ module RenderBody assert_status 200 end + test "specified content type should not be removed" do + get "/render_body/with_layout/with_custom_content_type" + + assert_equal %w{ troll face }, JSON.parse(response.body) + assert_equal 'application/json', response.headers['Content-Type'] + end + test "rendering body with layout: false" do get "/render_body/with_layout/with_layout_false" @@ -154,22 +166,5 @@ module RenderBody assert_body "hello world" assert_status 200 end - - test "rendering from minimal controller returns response with no content type" do - get "/render_body/minimal/index" - - assert_header_no_content_type - end - - test "rendering from normal controller returns response with no content type" do - get "/render_body/simple/index" - - assert_header_no_content_type - end - - def assert_header_no_content_type - assert_not response.headers.has_key?("Content-Type"), - %(Expect response not to have Content-Type header, got "#{response.headers["Content-Type"]}") - end end end diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb index 1f5fc06410..99229b3baf 100644 --- a/actionpack/test/controller/request_forgery_protection_test.rb +++ b/actionpack/test/controller/request_forgery_protection_test.rb @@ -289,6 +289,22 @@ module RequestForgeryProtectionTests end end + def test_should_not_warn_if_csrf_logging_disabled + old_logger = ActionController::Base.logger + logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new + ActionController::Base.logger = logger + ActionController::Base.log_warning_on_csrf_failure = false + + begin + assert_blocked { post :index } + + assert_equal 0, logger.logged(:warn).size + ensure + ActionController::Base.logger = old_logger + ActionController::Base.log_warning_on_csrf_failure = true + end + end + def test_should_only_allow_same_origin_js_get_with_xhr_header assert_cross_origin_blocked { get :same_origin_js } assert_cross_origin_blocked { get :same_origin_js, format: 'js' } diff --git a/actionpack/test/dispatch/live_response_test.rb b/actionpack/test/dispatch/live_response_test.rb index e0cfb73acf..512f3a8a7a 100644 --- a/actionpack/test/dispatch/live_response_test.rb +++ b/actionpack/test/dispatch/live_response_test.rb @@ -6,6 +6,7 @@ module ActionController class ResponseTest < ActiveSupport::TestCase def setup @response = Live::Response.new + @response.request = ActionDispatch::Request.new({}) #yolo end def test_header_merge @@ -34,6 +35,7 @@ module ActionController @response.stream.close } + @response.await_commit @response.each do |part| assert_equal 'foo', part latch.release @@ -58,21 +60,30 @@ module ActionController assert_nil @response.headers['Content-Length'] end - def test_headers_cannot_be_written_after_write + def test_headers_cannot_be_written_after_webserver_reads @response.stream.write 'omg' + latch = ActiveSupport::Concurrency::Latch.new + t = Thread.new { + @response.stream.each do |chunk| + latch.release + end + } + + latch.await assert @response.headers.frozen? e = assert_raises(ActionDispatch::IllegalStateError) do @response.headers['Content-Length'] = "zomg" end assert_equal 'header already sent', e.message + @response.stream.close + t.join end def test_headers_cannot_be_written_after_close @response.stream.close - assert @response.headers.frozen? e = assert_raises(ActionDispatch::IllegalStateError) do @response.headers['Content-Length'] = "zomg" end diff --git a/actionpack/test/dispatch/rack_test.rb b/actionpack/test/dispatch/rack_test.rb deleted file mode 100644 index ef1964fd19..0000000000 --- a/actionpack/test/dispatch/rack_test.rb +++ /dev/null @@ -1,191 +0,0 @@ -require 'abstract_unit' - -# TODO: Merge these tests into RequestTest - -class BaseRackTest < ActiveSupport::TestCase - def setup - @env = { - "HTTP_MAX_FORWARDS" => "10", - "SERVER_NAME" => "glu.ttono.us", - "FCGI_ROLE" => "RESPONDER", - "AUTH_TYPE" => "Basic", - "HTTP_X_FORWARDED_HOST" => "glu.ttono.us", - "HTTP_ACCEPT_CHARSET" => "UTF-8", - "HTTP_ACCEPT_ENCODING" => "gzip, deflate", - "HTTP_CACHE_CONTROL" => "no-cache, max-age=0", - "HTTP_PRAGMA" => "no-cache", - "HTTP_USER_AGENT" => "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en)", - "PATH_INFO" => "/homepage/", - "HTTP_ACCEPT_LANGUAGE" => "en", - "HTTP_NEGOTIATE" => "trans", - "HTTP_HOST" => "glu.ttono.us:8007", - "HTTP_REFERER" => "http://www.google.com/search?q=glu.ttono.us", - "HTTP_FROM" => "googlebot", - "SERVER_PROTOCOL" => "HTTP/1.1", - "REDIRECT_URI" => "/dispatch.fcgi", - "SCRIPT_NAME" => "/dispatch.fcgi", - "SERVER_ADDR" => "207.7.108.53", - "REMOTE_ADDR" => "207.7.108.53", - "REMOTE_HOST" => "google.com", - "REMOTE_IDENT" => "kevin", - "REMOTE_USER" => "kevin", - "SERVER_SOFTWARE" => "lighttpd/1.4.5", - "HTTP_COOKIE" => "_session_id=c84ace84796670c052c6ceb2451fb0f2; is_admin=yes", - "HTTP_X_FORWARDED_SERVER" => "glu.ttono.us", - "REQUEST_URI" => "/admin", - "DOCUMENT_ROOT" => "/home/kevinc/sites/typo/public", - "PATH_TRANSLATED" => "/home/kevinc/sites/typo/public/homepage/", - "SERVER_PORT" => "8007", - "QUERY_STRING" => "", - "REMOTE_PORT" => "63137", - "GATEWAY_INTERFACE" => "CGI/1.1", - "HTTP_X_FORWARDED_FOR" => "65.88.180.234", - "HTTP_ACCEPT" => "*/*", - "SCRIPT_FILENAME" => "/home/kevinc/sites/typo/public/dispatch.fcgi", - "REDIRECT_STATUS" => "200", - "REQUEST_METHOD" => "GET" - } - @request = ActionDispatch::Request.new(@env) - # some Nokia phone browsers omit the space after the semicolon separator. - # some developers have grown accustomed to using comma in cookie values. - @alt_cookie_fmt_request = ActionDispatch::Request.new(@env.merge({"HTTP_COOKIE"=>"_session_id=c84ace847,96670c052c6ceb2451fb0f2;is_admin=yes"})) - end - - private - def set_content_data(data) - @request.env['REQUEST_METHOD'] = 'POST' - @request.env['CONTENT_LENGTH'] = data.length - @request.env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded; charset=utf-8' - @request.env['rack.input'] = StringIO.new(data) - end -end - -class RackRequestTest < BaseRackTest - test "proxy request" do - assert_equal 'glu.ttono.us', @request.host_with_port - end - - test "http host" do - @env.delete "HTTP_X_FORWARDED_HOST" - @env['HTTP_HOST'] = "rubyonrails.org:8080" - assert_equal "rubyonrails.org", @request.host - assert_equal "rubyonrails.org:8080", @request.host_with_port - - @env['HTTP_X_FORWARDED_HOST'] = "www.firsthost.org, www.secondhost.org" - assert_equal "www.secondhost.org", @request.host - end - - test "http host with default port overrides server port" do - @env.delete "HTTP_X_FORWARDED_HOST" - @env['HTTP_HOST'] = "rubyonrails.org" - assert_equal "rubyonrails.org", @request.host_with_port - end - - test "host with port defaults to server name if no host headers" do - @env.delete "HTTP_X_FORWARDED_HOST" - @env.delete "HTTP_HOST" - assert_equal "glu.ttono.us:8007", @request.host_with_port - end - - test "host with port falls back to server addr if necessary" do - @env.delete "HTTP_X_FORWARDED_HOST" - @env.delete "HTTP_HOST" - @env.delete "SERVER_NAME" - assert_equal "207.7.108.53", @request.host - assert_equal 8007, @request.port - assert_equal "207.7.108.53:8007", @request.host_with_port - end - - test "host with port if http standard port is specified" do - @env['HTTP_X_FORWARDED_HOST'] = "glu.ttono.us:80" - assert_equal "glu.ttono.us", @request.host_with_port - end - - test "host with port if https standard port is specified" do - @env['HTTP_X_FORWARDED_PROTO'] = "https" - @env['HTTP_X_FORWARDED_HOST'] = "glu.ttono.us:443" - assert_equal "glu.ttono.us", @request.host_with_port - end - - test "host if ipv6 reference" do - @env.delete "HTTP_X_FORWARDED_HOST" - @env['HTTP_HOST'] = "[2001:1234:5678:9abc:def0::dead:beef]" - assert_equal "[2001:1234:5678:9abc:def0::dead:beef]", @request.host - end - - test "host if ipv6 reference with port" do - @env.delete "HTTP_X_FORWARDED_HOST" - @env['HTTP_HOST'] = "[2001:1234:5678:9abc:def0::dead:beef]:8008" - assert_equal "[2001:1234:5678:9abc:def0::dead:beef]", @request.host - end - - test "CGI environment variables" do - assert_equal "Basic", @request.auth_type - assert_equal 0, @request.content_length - assert_equal nil, @request.content_mime_type - assert_equal "CGI/1.1", @request.gateway_interface - assert_equal "*/*", @request.accept - assert_equal "UTF-8", @request.accept_charset - assert_equal "gzip, deflate", @request.accept_encoding - assert_equal "en", @request.accept_language - assert_equal "no-cache, max-age=0", @request.cache_control - assert_equal "googlebot", @request.from - assert_equal "glu.ttono.us", @request.host - assert_equal "trans", @request.negotiate - assert_equal "no-cache", @request.pragma - assert_equal "http://www.google.com/search?q=glu.ttono.us", @request.referer - assert_equal "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en)", @request.user_agent - assert_equal "/homepage/", @request.path_info - assert_equal "/home/kevinc/sites/typo/public/homepage/", @request.path_translated - assert_equal "", @request.query_string - assert_equal "207.7.108.53", @request.remote_addr - assert_equal "google.com", @request.remote_host - assert_equal "kevin", @request.remote_ident - assert_equal "kevin", @request.remote_user - assert_equal "GET", @request.request_method - assert_equal "/dispatch.fcgi", @request.script_name - assert_equal "glu.ttono.us", @request.server_name - assert_equal 8007, @request.server_port - assert_equal "HTTP/1.1", @request.server_protocol - assert_equal "lighttpd", @request.server_software - end - - test "cookie syntax resilience" do - cookies = @request.cookies - assert_equal "c84ace84796670c052c6ceb2451fb0f2", cookies["_session_id"], cookies.inspect - assert_equal "yes", cookies["is_admin"], cookies.inspect - - alt_cookies = @alt_cookie_fmt_request.cookies - #assert_equal "c84ace847,96670c052c6ceb2451fb0f2", alt_cookies["_session_id"], alt_cookies.inspect - assert_equal "yes", alt_cookies["is_admin"], alt_cookies.inspect - end -end - -class RackRequestParamsParsingTest < BaseRackTest - test "doesnt break when content type has charset" do - set_content_data 'flamenco=love' - - assert_equal({"flamenco"=> "love"}, @request.request_parameters) - end - - test "doesnt interpret request uri as query string when missing" do - @request.env['REQUEST_URI'] = 'foo' - assert_equal({}, @request.query_parameters) - end -end - -class RackRequestNeedsRewoundTest < BaseRackTest - test "body should be rewound" do - data = 'foo' - @env['rack.input'] = StringIO.new(data) - @env['CONTENT_LENGTH'] = data.length - @env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded; charset=utf-8' - - # Read the request body by parsing params. - request = ActionDispatch::Request.new(@env) - request.request_parameters - - # Should have rewound the body. - assert_equal 0, request.body.pos - end -end diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index 40e32cb4d3..6e21b4a258 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -1,12 +1,34 @@ require 'abstract_unit' -class RequestTest < ActiveSupport::TestCase +class BaseRequestTest < ActiveSupport::TestCase + def setup + @env = { + :ip_spoofing_check => true, + :tld_length => 1, + "rack.input" => "foo" + } + end def url_for(options = {}) options = { host: 'www.example.com' }.merge!(options) ActionDispatch::Http::URL.url_for(options) end + protected + def stub_request(env = {}) + ip_spoofing_check = env.key?(:ip_spoofing_check) ? env.delete(:ip_spoofing_check) : true + @trusted_proxies ||= nil + ip_app = ActionDispatch::RemoteIp.new(Proc.new { }, ip_spoofing_check, @trusted_proxies) + tld_length = env.key?(:tld_length) ? env.delete(:tld_length) : 1 + ip_app.call(env) + ActionDispatch::Http::URL.tld_length = tld_length + + env = @env.merge(env) + ActionDispatch::Request.new(env) + end +end + +class RequestUrlFor < BaseRequestTest test "url_for class method" do e = assert_raise(ArgumentError) { url_for(:host => nil) } assert_match(/Please provide the :host parameter/, e.message) @@ -31,7 +53,9 @@ class RequestTest < ActiveSupport::TestCase assert_equal 'http://www.example.com?params=', url_for(:params => '') assert_equal 'http://www.example.com?params=1', url_for(:params => 1) end +end +class RequestIP < BaseRequestTest test "remote ip" do request = stub_request 'REMOTE_ADDR' => '1.2.3.4' assert_equal '1.2.3.4', request.remote_ip @@ -220,10 +244,12 @@ class RequestTest < ActiveSupport::TestCase end test "remote ip middleware not present still returns an IP" do - request = ActionDispatch::Request.new({'REMOTE_ADDR' => '127.0.0.1'}) + request = stub_request('REMOTE_ADDR' => '127.0.0.1') assert_equal '127.0.0.1', request.remote_ip end +end +class RequestDomain < BaseRequestTest test "domains" do request = stub_request 'HTTP_HOST' => 'www.rubyonrails.org' assert_equal "rubyonrails.org", request.domain @@ -281,7 +307,9 @@ class RequestTest < ActiveSupport::TestCase assert_equal [], request.subdomains assert_equal "", request.subdomain end +end +class RequestPort < BaseRequestTest test "standard_port" do request = stub_request assert_equal 80, request.standard_port @@ -323,7 +351,9 @@ class RequestTest < ActiveSupport::TestCase request = stub_request 'HTTP_HOST' => 'www.example.org:8080' assert_equal ':8080', request.port_string end +end +class RequestPath < BaseRequestTest test "full path" do request = stub_request 'SCRIPT_NAME' => '', 'PATH_INFO' => '/path/of/some/uri', 'QUERY_STRING' => 'mapped=1' assert_equal "/path/of/some/uri?mapped=1", request.fullpath @@ -354,6 +384,32 @@ class RequestTest < ActiveSupport::TestCase assert_equal "/of/some/uri", request.path_info end + test "original_fullpath returns ORIGINAL_FULLPATH" do + request = stub_request('ORIGINAL_FULLPATH' => "/foo?bar") + + path = request.original_fullpath + assert_equal "/foo?bar", path + end + + test "original_url returns url built using ORIGINAL_FULLPATH" do + request = stub_request('ORIGINAL_FULLPATH' => "/foo?bar", + 'HTTP_HOST' => "example.org", + 'rack.url_scheme' => "http") + + url = request.original_url + assert_equal "http://example.org/foo?bar", url + end + + test "original_fullpath returns fullpath if ORIGINAL_FULLPATH is not present" do + request = stub_request('PATH_INFO' => "/foo", + 'QUERY_STRING' => "bar") + + path = request.original_fullpath + assert_equal "/foo?bar", path + end +end + +class RequestHost < BaseRequestTest test "host with default port" do request = stub_request 'HTTP_HOST' => 'rubyonrails.org:80' assert_equal "rubyonrails.org", request.host_with_port @@ -364,15 +420,174 @@ class RequestTest < ActiveSupport::TestCase assert_equal "rubyonrails.org:81", request.host_with_port end - test "server software" do - request = stub_request - assert_equal nil, request.server_software + test "proxy request" do + request = stub_request 'HTTP_HOST' => 'glu.ttono.us:80' + assert_equal "glu.ttono.us", request.host_with_port + end + + test "http host" do + request = stub_request 'HTTP_HOST' => "rubyonrails.org:8080" + assert_equal "rubyonrails.org", request.host + assert_equal "rubyonrails.org:8080", request.host_with_port + + request = stub_request 'HTTP_X_FORWARDED_HOST' => "www.firsthost.org, www.secondhost.org" + assert_equal "www.secondhost.org", request.host + end + + test "http host with default port overrides server port" do + request = stub_request 'HTTP_HOST' => "rubyonrails.org" + assert_equal "rubyonrails.org", request.host_with_port + end + + test "host with port if http standard port is specified" do + request = stub_request 'HTTP_X_FORWARDED_HOST' => "glu.ttono.us:80" + assert_equal "glu.ttono.us", request.host_with_port + end + + test "host with port if https standard port is specified" do + request = stub_request( + 'HTTP_X_FORWARDED_PROTO' => "https", + 'HTTP_X_FORWARDED_HOST' => "glu.ttono.us:443" + ) + assert_equal "glu.ttono.us", request.host_with_port + end + + test "host if ipv6 reference" do + request = stub_request 'HTTP_HOST' => "[2001:1234:5678:9abc:def0::dead:beef]" + assert_equal "[2001:1234:5678:9abc:def0::dead:beef]", request.host + end + + test "host if ipv6 reference with port" do + request = stub_request 'HTTP_HOST' => "[2001:1234:5678:9abc:def0::dead:beef]:8008" + assert_equal "[2001:1234:5678:9abc:def0::dead:beef]", request.host + end +end + +class RequestCGI < BaseRequestTest + test "CGI environment variables" do + request = stub_request( + "AUTH_TYPE" => "Basic", + "GATEWAY_INTERFACE" => "CGI/1.1", + "HTTP_ACCEPT" => "*/*", + "HTTP_ACCEPT_CHARSET" => "UTF-8", + "HTTP_ACCEPT_ENCODING" => "gzip, deflate", + "HTTP_ACCEPT_LANGUAGE" => "en", + "HTTP_CACHE_CONTROL" => "no-cache, max-age=0", + "HTTP_FROM" => "googlebot", + "HTTP_HOST" => "glu.ttono.us:8007", + "HTTP_NEGOTIATE" => "trans", + "HTTP_PRAGMA" => "no-cache", + "HTTP_REFERER" => "http://www.google.com/search?q=glu.ttono.us", + "HTTP_USER_AGENT" => "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en)", + "PATH_INFO" => "/homepage/", + "PATH_TRANSLATED" => "/home/kevinc/sites/typo/public/homepage/", + "QUERY_STRING" => "", + "REMOTE_ADDR" => "207.7.108.53", + "REMOTE_HOST" => "google.com", + "REMOTE_IDENT" => "kevin", + "REMOTE_USER" => "kevin", + "REQUEST_METHOD" => "GET", + "SCRIPT_NAME" => "/dispatch.fcgi", + "SERVER_NAME" => "glu.ttono.us", + "SERVER_PORT" => "8007", + "SERVER_PROTOCOL" => "HTTP/1.1", + "SERVER_SOFTWARE" => "lighttpd/1.4.5", + ) + + assert_equal "Basic", request.auth_type + assert_equal 0, request.content_length + assert_equal nil, request.content_mime_type + assert_equal "CGI/1.1", request.gateway_interface + assert_equal "*/*", request.accept + assert_equal "UTF-8", request.accept_charset + assert_equal "gzip, deflate", request.accept_encoding + assert_equal "en", request.accept_language + assert_equal "no-cache, max-age=0", request.cache_control + assert_equal "googlebot", request.from + assert_equal "glu.ttono.us", request.host + assert_equal "trans", request.negotiate + assert_equal "no-cache", request.pragma + assert_equal "http://www.google.com/search?q=glu.ttono.us", request.referer + assert_equal "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en)", request.user_agent + assert_equal "/homepage/", request.path_info + assert_equal "/home/kevinc/sites/typo/public/homepage/", request.path_translated + assert_equal "", request.query_string + assert_equal "207.7.108.53", request.remote_addr + assert_equal "google.com", request.remote_host + assert_equal "kevin", request.remote_ident + assert_equal "kevin", request.remote_user + assert_equal "GET", request.request_method + assert_equal "/dispatch.fcgi", request.script_name + assert_equal "glu.ttono.us", request.server_name + assert_equal 8007, request.server_port + assert_equal "HTTP/1.1", request.server_protocol + assert_equal "lighttpd", request.server_software + end +end + +class RequestCookie < BaseRequestTest + test "cookie syntax resilience" do + request = stub_request("HTTP_COOKIE" => "_session_id=c84ace84796670c052c6ceb2451fb0f2; is_admin=yes") + assert_equal "c84ace84796670c052c6ceb2451fb0f2", request.cookies["_session_id"], request.cookies.inspect + assert_equal "yes", request.cookies["is_admin"], request.cookies.inspect + + # some Nokia phone browsers omit the space after the semicolon separator. + # some developers have grown accustomed to using comma in cookie values. + request = stub_request("HTTP_COOKIE"=>"_session_id=c84ace847,96670c052c6ceb2451fb0f2;is_admin=yes") + assert_equal "c84ace847", request.cookies["_session_id"], request.cookies.inspect + assert_equal "yes", request.cookies["is_admin"], request.cookies.inspect + end +end + +class RequestParamsParsing < BaseRequestTest + test "doesnt break when content type has charset" do + request = stub_request( + 'REQUEST_METHOD' => 'POST', + 'CONTENT_LENGTH' => "flamenco=love".length, + 'CONTENT_TYPE' => 'application/x-www-form-urlencoded; charset=utf-8', + 'rack.input' => StringIO.new("flamenco=love") + ) + + assert_equal({"flamenco"=> "love"}, request.request_parameters) + end + + test "doesnt interpret request uri as query string when missing" do + request = stub_request('REQUEST_URI' => 'foo') + assert_equal({}, request.query_parameters) + end +end - request = stub_request 'SERVER_SOFTWARE' => 'Apache3.422' - assert_equal 'apache', request.server_software +class RequestRewind < BaseRequestTest + test "body should be rewound" do + data = 'rewind' + env = { + 'rack.input' => StringIO.new(data), + 'CONTENT_LENGTH' => data.length, + 'CONTENT_TYPE' => 'application/x-www-form-urlencoded; charset=utf-8' + } + + # Read the request body by parsing params. + request = stub_request(env) + request.request_parameters + + # Should have rewound the body. + assert_equal 0, request.body.pos + end + + test "raw_post rewinds rack.input if RAW_POST_DATA is nil" do + request = stub_request( + 'rack.input' => StringIO.new("raw"), + 'CONTENT_LENGTH' => 3 + ) + assert_equal "raw", request.raw_post + assert_equal "raw", request.env['rack.input'].read + end +end - request = stub_request 'SERVER_SOFTWARE' => 'lighttpd(1.1.4)' - assert_equal 'lighttpd', request.server_software +class RequestProtocol < BaseRequestTest + test "server software" do + assert_equal 'lighttpd', stub_request('SERVER_SOFTWARE' => 'lighttpd/1.4.5').server_software + assert_equal 'apache', stub_request('SERVER_SOFTWARE' => 'Apache3.422').server_software end test "xml http request" do @@ -391,19 +606,12 @@ class RequestTest < ActiveSupport::TestCase end test "reports ssl" do - request = stub_request - assert !request.ssl? - - request = stub_request 'HTTPS' => 'on' - assert request.ssl? + assert !stub_request.ssl? + assert stub_request('HTTPS' => 'on').ssl? end test "reports ssl when proxied via lighttpd" do - request = stub_request - assert !request.ssl? - - request = stub_request 'HTTP_X_FORWARDED_PROTO' => 'https' - assert request.ssl? + assert stub_request('HTTP_X_FORWARDED_PROTO' => 'https').ssl? end test "scheme returns https when proxied" do @@ -411,63 +619,72 @@ class RequestTest < ActiveSupport::TestCase assert !request.ssl? assert_equal 'http', request.scheme - request = stub_request 'rack.url_scheme' => 'http', 'HTTP_X_FORWARDED_PROTO' => 'https' + request = stub_request( + 'rack.url_scheme' => 'http', + 'HTTP_X_FORWARDED_PROTO' => 'https' + ) assert request.ssl? assert_equal 'https', request.scheme end +end - test "String request methods" do - [:get, :post, :patch, :put, :delete].each do |method| - request = stub_request 'REQUEST_METHOD' => method.to_s.upcase - assert_equal method.to_s.upcase, request.method - end - end +class RequestMethod < BaseRequestTest + test "request methods" do + [:post, :get, :patch, :put, :delete].each do |method| + request = stub_request('REQUEST_METHOD' => method.to_s.upcase) - test "Symbol forms of request methods via method_symbol" do - [:get, :post, :patch, :put, :delete].each do |method| - request = stub_request 'REQUEST_METHOD' => method.to_s.upcase + assert_equal method.to_s.upcase, request.method assert_equal method, request.method_symbol end end test "invalid http method raises exception" do assert_raise(ActionController::UnknownHttpMethod) do - request = stub_request 'REQUEST_METHOD' => 'RANDOM_METHOD' - request.request_method + stub_request('REQUEST_METHOD' => 'RANDOM_METHOD').request_method end end test "allow method hacking on post" do %w(GET OPTIONS PATCH PUT POST DELETE).each do |method| - request = stub_request "REQUEST_METHOD" => method.to_s.upcase + request = stub_request 'REQUEST_METHOD' => method.to_s.upcase + assert_equal(method == "HEAD" ? "GET" : method, request.method) end end test "invalid method hacking on post raises exception" do assert_raise(ActionController::UnknownHttpMethod) do - request = stub_request "REQUEST_METHOD" => "_RANDOM_METHOD" - request.request_method + stub_request('REQUEST_METHOD' => '_RANDOM_METHOD').request_method end end test "restrict method hacking" do [:get, :patch, :put, :delete].each do |method| - request = stub_request 'REQUEST_METHOD' => method.to_s.upcase, - 'action_dispatch.request.request_parameters' => { :_method => 'put' } + request = stub_request( + 'action_dispatch.request.request_parameters' => { :_method => 'put' }, + 'REQUEST_METHOD' => method.to_s.upcase + ) + assert_equal method.to_s.upcase, request.method end end test "post masquerading as patch" do - request = stub_request 'REQUEST_METHOD' => 'PATCH', "rack.methodoverride.original_method" => "POST" + request = stub_request( + 'REQUEST_METHOD' => 'PATCH', + "rack.methodoverride.original_method" => "POST" + ) + assert_equal "POST", request.method assert_equal "PATCH", request.request_method assert request.patch? end test "post masquerading as put" do - request = stub_request 'REQUEST_METHOD' => 'PUT', "rack.methodoverride.original_method" => "POST" + request = stub_request( + 'REQUEST_METHOD' => 'PUT', + "rack.methodoverride.original_method" => "POST" + ) assert_equal "POST", request.method assert_equal "PUT", request.request_method assert request.put? @@ -493,7 +710,9 @@ class RequestTest < ActiveSupport::TestCase end end end +end +class RequestFormat < BaseRequestTest test "xml format" do request = stub_request request.expects(:parameters).at_least_once.returns({ :format => 'xml' }) @@ -513,109 +732,55 @@ class RequestTest < ActiveSupport::TestCase end test "XMLHttpRequest" do - request = stub_request 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest', - 'HTTP_ACCEPT' => - [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(",") + request = stub_request( + 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest', + 'HTTP_ACCEPT' => [Mime::JS, Mime::HTML, Mime::XML, "text/xml", Mime::ALL].join(",") + ) request.expects(:parameters).at_least_once.returns({}) assert request.xhr? assert_equal Mime::JS, request.format end - test "content type" do - request = stub_request 'CONTENT_TYPE' => 'text/html' - assert_equal Mime::HTML, request.content_mime_type - end - - test "can override format with parameter" do + test "can override format with parameter negative" do request = stub_request request.expects(:parameters).at_least_once.returns({ :format => :txt }) assert !request.format.xml? + end + test "can override format with parameter positive" do request = stub_request request.expects(:parameters).at_least_once.returns({ :format => :xml }) assert request.format.xml? end - test "no content type" do - request = stub_request - assert_equal nil, request.content_mime_type - end - - test "content type is XML" do - request = stub_request 'CONTENT_TYPE' => 'application/xml' - assert_equal Mime::XML, request.content_mime_type - end - - test "content type with charset" do - request = stub_request 'CONTENT_TYPE' => 'application/xml; charset=UTF-8' - assert_equal Mime::XML, request.content_mime_type - end - - test "user agent" do - request = stub_request 'HTTP_USER_AGENT' => 'TestAgent' - assert_equal 'TestAgent', request.user_agent - end - - test "parameters" do - request = stub_request - request.stubs(:request_parameters).returns({ "foo" => 1 }) - request.stubs(:query_parameters).returns({ "bar" => 2 }) - - assert_equal({"foo" => 1, "bar" => 2}, request.parameters) - assert_equal({"foo" => 1}, request.request_parameters) - assert_equal({"bar" => 2}, request.query_parameters) - end - - test "parameters still accessible after rack parse error" do - mock_rack_env = { "QUERY_STRING" => "x[y]=1&x[y][][w]=2", "rack.input" => "foo" } - request = nil - request = stub_request(mock_rack_env) - - assert_raises(ActionController::BadRequest) do - # rack will raise a TypeError when parsing this query string - request.parameters - end - - assert_equal({}, request.parameters) - end - - test "we have access to the original exception" do - mock_rack_env = { "QUERY_STRING" => "x[y]=1&x[y][][w]=2", "rack.input" => "foo" } - request = nil - request = stub_request(mock_rack_env) - - e = assert_raises(ActionController::BadRequest) do - # rack will raise a TypeError when parsing this query string - request.parameters - end - - assert e.original_exception - assert_equal e.original_exception.backtrace, e.backtrace - end - - test "formats with accept header" do + test "formats text/html with accept header" do request = stub_request 'HTTP_ACCEPT' => 'text/html' - request.expects(:parameters).at_least_once.returns({}) assert_equal [Mime::HTML], request.formats + end + test "formats blank with accept header" do request = stub_request 'HTTP_ACCEPT' => '' - request.expects(:parameters).at_least_once.returns({}) assert_equal [Mime::HTML], request.formats + end - request = stub_request 'HTTP_ACCEPT' => '', - 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest" - request.expects(:parameters).at_least_once.returns({}) + test "formats XMLHttpRequest with accept header" do + request = stub_request 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest" assert_equal [Mime::JS], request.formats + end - request = stub_request 'CONTENT_TYPE' => 'application/xml; charset=UTF-8', - 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest" - request.expects(:parameters).at_least_once.returns({}) + test "formats application/xml with accept header" do + request = stub_request('CONTENT_TYPE' => 'application/xml; charset=UTF-8', + 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest") assert_equal [Mime::XML], request.formats + end + test "formats format:text with accept header" do request = stub_request request.expects(:parameters).at_least_once.returns({ :format => :txt }) assert_equal [Mime::TEXT], request.formats + end + test "formats format:unknown with accept header" do request = stub_request request.expects(:parameters).at_least_once.returns({ :format => :unknown }) assert_instance_of Mime::NullType, request.format @@ -669,30 +834,87 @@ class RequestTest < ActiveSupport::TestCase ActionDispatch::Request.ignore_accept_header = false end end +end - test "negotiate_mime" do - request = stub_request 'HTTP_ACCEPT' => 'text/html', - 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest" +class RequestMimeType < BaseRequestTest + test "content type" do + assert_equal Mime::HTML, stub_request('CONTENT_TYPE' => 'text/html').content_mime_type + end - request.expects(:parameters).at_least_once.returns({}) + test "no content type" do + assert_equal nil, stub_request.content_mime_type + end + + test "content type is XML" do + assert_equal Mime::XML, stub_request('CONTENT_TYPE' => 'application/xml').content_mime_type + end + + test "content type with charset" do + assert_equal Mime::XML, stub_request('CONTENT_TYPE' => 'application/xml; charset=UTF-8').content_mime_type + end + + test "user agent" do + assert_equal 'TestAgent', stub_request('HTTP_USER_AGENT' => 'TestAgent').user_agent + end + + test "negotiate_mime" do + request = stub_request( + 'HTTP_ACCEPT' => 'text/html', + 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest" + ) assert_equal nil, request.negotiate_mime([Mime::XML, Mime::JSON]) assert_equal Mime::HTML, request.negotiate_mime([Mime::XML, Mime::HTML]) assert_equal Mime::HTML, request.negotiate_mime([Mime::XML, Mime::ALL]) + end + + test "negotiate_mime with content_type" do + request = stub_request( + 'CONTENT_TYPE' => 'application/xml; charset=UTF-8', + 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest" + ) - request = stub_request 'CONTENT_TYPE' => 'application/xml; charset=UTF-8', - 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest" - request.expects(:parameters).at_least_once.returns({}) assert_equal Mime::XML, request.negotiate_mime([Mime::XML, Mime::CSV]) end +end - test "raw_post rewinds rack.input if RAW_POST_DATA is nil" do - request = stub_request('rack.input' => StringIO.new("foo"), - 'CONTENT_LENGTH' => 3) - assert_equal "foo", request.raw_post - assert_equal "foo", request.env['rack.input'].read +class RequestParameters < BaseRequestTest + test "parameters" do + request = stub_request + request.expects(:request_parameters).at_least_once.returns({ "foo" => 1 }) + request.expects(:query_parameters).at_least_once.returns({ "bar" => 2 }) + + assert_equal({"foo" => 1, "bar" => 2}, request.parameters) + assert_equal({"foo" => 1}, request.request_parameters) + assert_equal({"bar" => 2}, request.query_parameters) + end + + test "parameters still accessible after rack parse error" do + request = stub_request("QUERY_STRING" => "x[y]=1&x[y][][w]=2") + + assert_raises(ActionController::BadRequest) do + # rack will raise a TypeError when parsing this query string + request.parameters + end + + assert_equal({}, request.parameters) end + test "we have access to the original exception" do + request = stub_request("QUERY_STRING" => "x[y]=1&x[y][][w]=2") + + e = assert_raises(ActionController::BadRequest) do + # rack will raise a TypeError when parsing this query string + request.parameters + end + + assert e.original_exception + assert_equal e.original_exception.backtrace, e.backtrace + end +end + + +class RequestParameterFilter < BaseRequestTest test "process parameter filter" do test_hashes = [ [{'foo'=>'bar'},{'foo'=>'bar'},%w'food'], @@ -721,9 +943,14 @@ class RequestTest < ActiveSupport::TestCase end test "filtered_parameters returns params filtered" do - request = stub_request('action_dispatch.request.parameters' => - { 'lifo' => 'Pratik', 'amount' => '420', 'step' => '1' }, - 'action_dispatch.parameter_filter' => [:lifo, :amount]) + request = stub_request( + 'action_dispatch.request.parameters' => { + 'lifo' => 'Pratik', + 'amount' => '420', + 'step' => '1' + }, + 'action_dispatch.parameter_filter' => [:lifo, :amount] + ) params = request.filtered_parameters assert_equal "[FILTERED]", params["lifo"] @@ -732,10 +959,14 @@ class RequestTest < ActiveSupport::TestCase end test "filtered_env filters env as a whole" do - request = stub_request('action_dispatch.request.parameters' => - { 'amount' => '420', 'step' => '1' }, "RAW_POST_DATA" => "yada yada", - 'action_dispatch.parameter_filter' => [:lifo, :amount]) - + request = stub_request( + 'action_dispatch.request.parameters' => { + 'amount' => '420', + 'step' => '1' + }, + "RAW_POST_DATA" => "yada yada", + 'action_dispatch.parameter_filter' => [:lifo, :amount] + ) request = stub_request(request.filtered_env) assert_equal "[FILTERED]", request.raw_post @@ -745,9 +976,11 @@ class RequestTest < ActiveSupport::TestCase test "filtered_path returns path with filtered query string" do %w(; &).each do |sep| - request = stub_request('QUERY_STRING' => %w(username=sikachu secret=bd4f21f api_key=b1bc3b3cd352f68d79d7).join(sep), + request = stub_request( + 'QUERY_STRING' => %w(username=sikachu secret=bd4f21f api_key=b1bc3b3cd352f68d79d7).join(sep), 'PATH_INFO' => '/authenticate', - 'action_dispatch.parameter_filter' => [:secret, :api_key]) + 'action_dispatch.parameter_filter' => [:secret, :api_key] + ) path = request.filtered_path assert_equal %w(/authenticate?username=sikachu secret=[FILTERED] api_key=[FILTERED]).join(sep), path @@ -755,56 +988,40 @@ class RequestTest < ActiveSupport::TestCase end test "filtered_path should not unescape a genuine '[FILTERED]' value" do - request = stub_request('QUERY_STRING' => "secret=bd4f21f&genuine=%5BFILTERED%5D", + request = stub_request( + 'QUERY_STRING' => "secret=bd4f21f&genuine=%5BFILTERED%5D", 'PATH_INFO' => '/authenticate', - 'action_dispatch.parameter_filter' => [:secret]) + 'action_dispatch.parameter_filter' => [:secret] + ) path = request.filtered_path - assert_equal "/authenticate?secret=[FILTERED]&genuine=%5BFILTERED%5D", path + assert_equal request.script_name + "/authenticate?secret=[FILTERED]&genuine=%5BFILTERED%5D", path end test "filtered_path should preserve duplication of keys in query string" do - request = stub_request('QUERY_STRING' => "username=sikachu&secret=bd4f21f&username=fxn", + request = stub_request( + 'QUERY_STRING' => "username=sikachu&secret=bd4f21f&username=fxn", 'PATH_INFO' => '/authenticate', - 'action_dispatch.parameter_filter' => [:secret]) + 'action_dispatch.parameter_filter' => [:secret] + ) path = request.filtered_path - assert_equal "/authenticate?username=sikachu&secret=[FILTERED]&username=fxn", path + assert_equal request.script_name + "/authenticate?username=sikachu&secret=[FILTERED]&username=fxn", path end test "filtered_path should ignore searchparts" do - request = stub_request('QUERY_STRING' => "secret", + request = stub_request( + 'QUERY_STRING' => "secret", 'PATH_INFO' => '/authenticate', - 'action_dispatch.parameter_filter' => [:secret]) + 'action_dispatch.parameter_filter' => [:secret] + ) path = request.filtered_path - assert_equal "/authenticate?secret", path - end - - test "original_fullpath returns ORIGINAL_FULLPATH" do - request = stub_request('ORIGINAL_FULLPATH' => "/foo?bar") - - path = request.original_fullpath - assert_equal "/foo?bar", path - end - - test "original_url returns url built using ORIGINAL_FULLPATH" do - request = stub_request('ORIGINAL_FULLPATH' => "/foo?bar", - 'HTTP_HOST' => "example.org", - 'rack.url_scheme' => "http") - - url = request.original_url - assert_equal "http://example.org/foo?bar", url - end - - test "original_fullpath returns fullpath if ORIGINAL_FULLPATH is not present" do - request = stub_request('PATH_INFO' => "/foo", - 'QUERY_STRING' => "bar") - - path = request.original_fullpath - assert_equal "/foo?bar", path + assert_equal request.script_name + "/authenticate?secret", path end +end +class RequestEtag < BaseRequestTest test "if_none_match_etags none" do request = stub_request @@ -843,7 +1060,9 @@ class RequestTest < ActiveSupport::TestCase assert request.etag_matches?(etag), etag end end +end +class RequestVarient < BaseRequestTest test "setting variant" do request = stub_request @@ -868,16 +1087,4 @@ class RequestTest < ActiveSupport::TestCase request.variant = "mobile" end end - -protected - - def stub_request(env = {}) - ip_spoofing_check = env.key?(:ip_spoofing_check) ? env.delete(:ip_spoofing_check) : true - @trusted_proxies ||= nil - ip_app = ActionDispatch::RemoteIp.new(Proc.new { }, ip_spoofing_check, @trusted_proxies) - tld_length = env.key?(:tld_length) ? env.delete(:tld_length) : 1 - ip_app.call(env) - ActionDispatch::Http::URL.tld_length = tld_length - ActionDispatch::Request.new(env) - end end diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb index 1360ede3f8..959a3bc5cd 100644 --- a/actionpack/test/dispatch/response_test.rb +++ b/actionpack/test/dispatch/response_test.rb @@ -235,14 +235,6 @@ class ResponseTest < ActiveSupport::TestCase assert_equal @response.body, body.each.to_a.join end end - - test "does not add default content-type if Content-Type is none" do - resp = ActionDispatch::Response.new.tap { |response| - response.no_content_type = true - } - - assert_not resp.headers.has_key?('Content-Type') - end end class ResponseIntegrationTest < ActionDispatch::IntegrationTest diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 1fa2cc6cf2..a47050adce 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -1031,6 +1031,136 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal 'users/home#index', @response.body end + def test_namespaced_shallow_routes_with_module_option + draw do + namespace :foo, module: 'bar' do + resources :posts, only: [:index, :show] do + resources :comments, only: [:index, :show], shallow: true + end + end + end + + get '/foo/posts' + assert_equal '/foo/posts', foo_posts_path + assert_equal 'bar/posts#index', @response.body + + get '/foo/posts/1' + assert_equal '/foo/posts/1', foo_post_path('1') + assert_equal 'bar/posts#show', @response.body + + get '/foo/posts/1/comments' + assert_equal '/foo/posts/1/comments', foo_post_comments_path('1') + assert_equal 'bar/comments#index', @response.body + + get '/foo/comments/2' + assert_equal '/foo/comments/2', foo_comment_path('2') + assert_equal 'bar/comments#show', @response.body + end + + def test_namespaced_shallow_routes_with_path_option + draw do + namespace :foo, path: 'bar' do + resources :posts, only: [:index, :show] do + resources :comments, only: [:index, :show], shallow: true + end + end + end + + get '/bar/posts' + assert_equal '/bar/posts', foo_posts_path + assert_equal 'foo/posts#index', @response.body + + get '/bar/posts/1' + assert_equal '/bar/posts/1', foo_post_path('1') + assert_equal 'foo/posts#show', @response.body + + get '/bar/posts/1/comments' + assert_equal '/bar/posts/1/comments', foo_post_comments_path('1') + assert_equal 'foo/comments#index', @response.body + + get '/bar/comments/2' + assert_equal '/bar/comments/2', foo_comment_path('2') + assert_equal 'foo/comments#show', @response.body + end + + def test_namespaced_shallow_routes_with_as_option + draw do + namespace :foo, as: 'bar' do + resources :posts, only: [:index, :show] do + resources :comments, only: [:index, :show], shallow: true + end + end + end + + get '/foo/posts' + assert_equal '/foo/posts', bar_posts_path + assert_equal 'foo/posts#index', @response.body + + get '/foo/posts/1' + assert_equal '/foo/posts/1', bar_post_path('1') + assert_equal 'foo/posts#show', @response.body + + get '/foo/posts/1/comments' + assert_equal '/foo/posts/1/comments', bar_post_comments_path('1') + assert_equal 'foo/comments#index', @response.body + + get '/foo/comments/2' + assert_equal '/foo/comments/2', bar_comment_path('2') + assert_equal 'foo/comments#show', @response.body + end + + def test_namespaced_shallow_routes_with_shallow_path_option + draw do + namespace :foo, shallow_path: 'bar' do + resources :posts, only: [:index, :show] do + resources :comments, only: [:index, :show], shallow: true + end + end + end + + get '/foo/posts' + assert_equal '/foo/posts', foo_posts_path + assert_equal 'foo/posts#index', @response.body + + get '/foo/posts/1' + assert_equal '/foo/posts/1', foo_post_path('1') + assert_equal 'foo/posts#show', @response.body + + get '/foo/posts/1/comments' + assert_equal '/foo/posts/1/comments', foo_post_comments_path('1') + assert_equal 'foo/comments#index', @response.body + + get '/bar/comments/2' + assert_equal '/bar/comments/2', foo_comment_path('2') + assert_equal 'foo/comments#show', @response.body + end + + def test_namespaced_shallow_routes_with_shallow_prefix_option + draw do + namespace :foo, shallow_prefix: 'bar' do + resources :posts, only: [:index, :show] do + resources :comments, only: [:index, :show], shallow: true + end + end + end + + get '/foo/posts' + assert_equal '/foo/posts', foo_posts_path + assert_equal 'foo/posts#index', @response.body + + get '/foo/posts/1' + assert_equal '/foo/posts/1', foo_post_path('1') + assert_equal 'foo/posts#show', @response.body + + get '/foo/posts/1/comments' + assert_equal '/foo/posts/1/comments', foo_post_comments_path('1') + assert_equal 'foo/comments#index', @response.body + + get '/foo/comments/2' + assert_equal '/foo/comments/2', bar_comment_path('2') + assert_equal 'foo/comments#show', @response.body + end + def test_namespace_containing_numbers draw do namespace :v2 do @@ -3033,6 +3163,114 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal '/admin/posts/1/comments', admin_post_comments_path('1') end + def test_shallow_path_and_prefix_are_not_added_to_non_shallow_routes + draw do + scope shallow_path: 'projects', shallow_prefix: 'project' do + resources :projects do + resources :files, controller: 'project_files', shallow: true + end + end + end + + get '/projects' + assert_equal 'projects#index', @response.body + assert_equal '/projects', projects_path + + get '/projects/new' + assert_equal 'projects#new', @response.body + assert_equal '/projects/new', new_project_path + + post '/projects' + assert_equal 'projects#create', @response.body + + get '/projects/1' + assert_equal 'projects#show', @response.body + assert_equal '/projects/1', project_path('1') + + get '/projects/1/edit' + assert_equal 'projects#edit', @response.body + assert_equal '/projects/1/edit', edit_project_path('1') + + patch '/projects/1' + assert_equal 'projects#update', @response.body + + delete '/projects/1' + assert_equal 'projects#destroy', @response.body + + get '/projects/1/files' + assert_equal 'project_files#index', @response.body + assert_equal '/projects/1/files', project_files_path('1') + + get '/projects/1/files/new' + assert_equal 'project_files#new', @response.body + assert_equal '/projects/1/files/new', new_project_file_path('1') + + post '/projects/1/files' + assert_equal 'project_files#create', @response.body + + get '/projects/files/2' + assert_equal 'project_files#show', @response.body + assert_equal '/projects/files/2', project_file_path('2') + + get '/projects/files/2/edit' + assert_equal 'project_files#edit', @response.body + assert_equal '/projects/files/2/edit', edit_project_file_path('2') + + patch '/projects/files/2' + assert_equal 'project_files#update', @response.body + + delete '/projects/files/2' + assert_equal 'project_files#destroy', @response.body + end + + def test_scope_path_is_copied_to_shallow_path + draw do + scope path: 'foo' do + resources :posts do + resources :comments, shallow: true + end + end + end + + assert_equal '/foo/comments/1', comment_path('1') + end + + def test_scope_as_is_copied_to_shallow_prefix + draw do + scope as: 'foo' do + resources :posts do + resources :comments, shallow: true + end + end + end + + assert_equal '/comments/1', foo_comment_path('1') + end + + def test_scope_shallow_prefix_is_not_overwritten_by_as + draw do + scope as: 'foo', shallow_prefix: 'bar' do + resources :posts do + resources :comments, shallow: true + end + end + end + + assert_equal '/comments/1', bar_comment_path('1') + end + + def test_scope_shallow_path_is_not_overwritten_by_path + draw do + scope path: 'foo', shallow_path: 'bar' do + resources :posts do + resources :comments, shallow: true + end + end + end + + assert_equal '/bar/comments/1', comment_path('1') + end + private def draw(&block) diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index 28508a5dfa..e46f55a875 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1 +1,17 @@ +* Fixed a problem where the default options for the `button_tag` helper is not + applied correctly. + + Fixes #14254. + + *Sergey Prikhodko* + +* Take variants into account when calculating template digests in ActionView::Digestor. + + The arguments to ActionView::Digestor#digest are now being passed as a hash + to support variants and allow more flexibility in the future. The support for + regular (required) arguments is deprecated and will be removed in Rails 5.0 or later. + + *Piotr Chmolowski, Łukasz Strzałkowski* + + Please check [4-1-stable](https://github.com/rails/rails/blob/4-1-stable/actionview/CHANGELOG.md) for previous changes. diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb index 5570e2a8dc..da43fef2e3 100644 --- a/actionview/lib/action_view/digestor.rb +++ b/actionview/lib/action_view/digestor.rb @@ -9,23 +9,56 @@ module ActionView @@digest_monitor = Monitor.new class << self - def digest(name, format, finder, options = {}) + # Supported options: + # + # * <tt>name</tt> - Template name + # * <tt>format</tt> - Template format + # * <tt>variant</tt> - Variant of +format+ (optional) + # * <tt>finder</tt> - An instance of ActionView::LookupContext + # * <tt>dependencies</tt> - An array of dependent views + # * <tt>partial</tt> - Specifies whether the template is a partial + def digest(*args) + options = _setup_options(*args) + + name = options[:name] + format = options[:format] + variant = options[:variant] + finder = options[:finder] + details_key = finder.details_key.hash dependencies = Array.wrap(options[:dependencies]) - cache_key = ([name, details_key, format] + dependencies).join('.') + cache_key = ([name, details_key, format, variant].compact + dependencies).join('.') # this is a correctly done double-checked locking idiom # (ThreadSafe::Cache's lookups have volatile semantics) @@cache[cache_key] || @@digest_monitor.synchronize do @@cache.fetch(cache_key) do # re-check under lock - compute_and_store_digest(cache_key, name, format, finder, options) + compute_and_store_digest(cache_key, options) end end end + def _setup_options(*args) + unless args.first.is_a?(Hash) + ActiveSupport::Deprecation.warn("Arguments to ActionView::Digestor should be provided as a hash. The support for regular arguments will be removed in Rails 5.0 or later") + + { + name: args.first, + format: args.second, + finder: args.third, + }.merge(args.fourth || {}) + else + options = args.first + options.assert_valid_keys(:name, :format, :variant, :finder, :dependencies, :partial) + + options + end + end + private - def compute_and_store_digest(cache_key, name, format, finder, options) # called under @@digest_monitor lock - klass = if options[:partial] || name.include?("/_") + + def compute_and_store_digest(cache_key, options) # called under @@digest_monitor lock + klass = if options[:partial] || options[:name].include?("/_") # Prevent re-entry or else recursive templates will blow the stack. # There is no need to worry about other threads seeing the +false+ value, # as they will then have to wait for this thread to let go of the @@digest_monitor lock. @@ -35,20 +68,25 @@ module ActionView Digestor end - digest = klass.new(name, format, finder, options).digest + digest = klass.new(options).digest # Store the actual digest if config.cache_template_loading is true @@cache[cache_key] = stored_digest = digest if ActionView::Resolver.caching? digest ensure # something went wrong or ActionView::Resolver.caching? is false, make sure not to corrupt the @@cache - @@cache.delete_pair(cache_key, false) if pre_stored && !stored_digest + @@cache.delete_pair(cache_key, false) if pre_stored && !stored_digest end end - attr_reader :name, :format, :finder, :options + attr_reader :name, :format, :variant, :finder, :options + + def initialize(*args) + @options = self.class._setup_options(*args) - def initialize(name, format, finder, options={}) - @name, @format, @finder, @options = name, format, finder, options + @name = @options.delete(:name) + @format = @options.delete(:format) + @variant = @options.delete(:variant) + @finder = @options.delete(:finder) end def digest @@ -68,7 +106,7 @@ module ActionView def nested_dependencies dependencies.collect do |dependency| - dependencies = PartialDigestor.new(dependency, format, finder).nested_dependencies + dependencies = PartialDigestor.new(name: dependency, format: format, finder: finder).nested_dependencies dependencies.any? ? { dependency => dependencies } : dependency end end @@ -88,7 +126,7 @@ module ActionView end def template - @template ||= finder.find(logical_name, [], partial?, formats: [ format ]) + @template ||= finder.find(logical_name, [], partial?, formats: [ format ], variants: [ variant ]) end def source @@ -97,7 +135,7 @@ module ActionView def dependency_digest template_digests = dependencies.collect do |template_name| - Digestor.digest(template_name, format, finder, partial: true) + Digestor.digest(name: template_name, format: format, finder: finder, partial: true) end (template_digests + injected_dependencies).join("-") diff --git a/actionview/lib/action_view/gem_version.rb b/actionview/lib/action_view/gem_version.rb new file mode 100644 index 0000000000..9266e55c47 --- /dev/null +++ b/actionview/lib/action_view/gem_version.rb @@ -0,0 +1,15 @@ +module ActionView + # Returns the version of the currently loaded ActionView as a <tt>Gem::Version</tt> + def self.gem_version + Gem::Version.new VERSION::STRING + end + + module VERSION + MAJOR = 4 + MINOR = 2 + TINY = 0 + PRE = "alpha" + + STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") + end +end diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb index b3af1d4da4..30e4e5e329 100644 --- a/actionview/lib/action_view/helpers/cache_helper.rb +++ b/actionview/lib/action_view/helpers/cache_helper.rb @@ -167,7 +167,7 @@ module ActionView if @virtual_path [ *Array(name.is_a?(Hash) ? controller.url_for(name).split("://").last : name), - Digestor.digest(@virtual_path, formats.last.to_sym, lookup_context, dependencies: view_cache_dependencies) + Digestor.digest(name: @virtual_path, format: formats.last.to_sym, variant: request.variant, finder: lookup_context, dependencies: view_cache_dependencies) ] else name diff --git a/actionview/lib/action_view/helpers/date_helper.rb b/actionview/lib/action_view/helpers/date_helper.rb index 698f0ca31c..1738df9cac 100644 --- a/actionview/lib/action_view/helpers/date_helper.rb +++ b/actionview/lib/action_view/helpers/date_helper.rb @@ -19,6 +19,10 @@ module ActionView # the <tt>select_month</tt> method would use simply "date" (which can be overwritten using <tt>:prefix</tt>) instead # of \date[month]. module DateHelper + MINUTES_IN_YEAR = 525600 + MINUTES_IN_QUARTER_YEAR = 131400 + MINUTES_IN_THREE_QUARTERS_YEAR = 394200 + # Reports the approximate distance in time between two Time, Date or DateTime objects or integers as seconds. # 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: @@ -120,11 +124,11 @@ module ActionView else minutes_with_offset = distance_in_minutes end - remainder = (minutes_with_offset % 525600) - distance_in_years = (minutes_with_offset.div 525600) - if remainder < 131400 + remainder = (minutes_with_offset % MINUTES_IN_YEAR) + distance_in_years = (minutes_with_offset.div MINUTES_IN_YEAR) + if remainder < MINUTES_IN_QUARTER_YEAR locale.t(:about_x_years, :count => distance_in_years) - elsif remainder < 394200 + elsif remainder < MINUTES_IN_THREE_QUARTERS_YEAR locale.t(:over_x_years, :count => distance_in_years) else locale.t(:almost_x_years, :count => distance_in_years + 1) diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index 5235962f9f..1ff090f244 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -457,7 +457,7 @@ module ActionView # doesn't create the form tags themselves. This makes fields_for suitable # for specifying additional model objects in the same form. # - # Although the usage and purpose of +field_for+ is similar to +form_for+'s, + # Although the usage and purpose of +fields_for+ is similar to +form_for+'s, # its method signature is slightly different. Like +form_for+, it yields # a FormBuilder object associated with a particular model object to a block, # and within the block allows methods to be called on the builder to @@ -1268,7 +1268,7 @@ module ActionView # doesn't create the form tags themselves. This makes fields_for suitable # for specifying additional model objects in the same form. # - # Although the usage and purpose of +field_for+ is similar to +form_for+'s, + # Although the usage and purpose of +fields_for+ is similar to +form_for+'s, # its method signature is slightly different. Like +form_for+, it yields # a FormBuilder object associated with a particular model object to a block, # and within the block allows methods to be called on the builder to diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb index 80f066b3be..0bbe08166b 100644 --- a/actionview/lib/action_view/helpers/form_tag_helper.rb +++ b/actionview/lib/action_view/helpers/form_tag_helper.rb @@ -465,17 +465,23 @@ module ActionView # # <strong>Ask me!</strong> # # </button> # - # button_tag "Checkout", data: { :disable_with => "Please wait..." } + # button_tag "Checkout", data: { disable_with: "Please wait..." } # # => <button data-disable-with="Please wait..." name="button" type="submit">Checkout</button> # def button_tag(content_or_options = nil, options = nil, &block) - options = content_or_options if block_given? && content_or_options.is_a?(Hash) - options ||= {} - options = options.stringify_keys + if content_or_options.is_a? Hash + options = content_or_options + else + options ||= {} + end - options.reverse_merge! 'name' => 'button', 'type' => 'submit' + options = { 'name' => 'button', 'type' => 'submit' }.merge!(options.stringify_keys) - content_tag :button, content_or_options || 'Button', options, &block + if block_given? + content_tag :button, options, &block + else + content_tag :button, content_or_options || 'Button', options + end end # Displays an image which when clicked will submit the form. diff --git a/actionview/lib/action_view/helpers/rendering_helper.rb b/actionview/lib/action_view/helpers/rendering_helper.rb index 15b88bcda6..ebfc35a4c7 100644 --- a/actionview/lib/action_view/helpers/rendering_helper.rb +++ b/actionview/lib/action_view/helpers/rendering_helper.rb @@ -17,8 +17,9 @@ module ActionView # * <tt>:html</tt> - Renders the html safe string passed in out, otherwise # performs html escape on the string first. Setting the content type as # <tt>text/html</tt>. - # * <tt>:body</tt> - Renders the text passed in, and does not set content - # type in the response. + # * <tt>:body</tt> - Renders the text passed in, and inherits the content + # type of <tt>text/html</tt> from <tt>ActionDispatch::Response</tt> + # object. # # If no options hash is passed or :update specified, the default is to render a partial and use the second parameter # as the locals hash. diff --git a/actionview/lib/action_view/helpers/tag_helper.rb b/actionview/lib/action_view/helpers/tag_helper.rb index 3528381781..a9f3b0ffbc 100644 --- a/actionview/lib/action_view/helpers/tag_helper.rb +++ b/actionview/lib/action_view/helpers/tag_helper.rb @@ -42,7 +42,8 @@ module ActionView # For example, a key +user_id+ would render as <tt>data-user-id</tt> and # thus accessed as <tt>dataset.userId</tt>. # - # Values are encoded to JSON, with the exception of strings and symbols. + # Values are encoded to JSON, with the exception of strings, symbols and + # BigDecimals. # This may come in handy when using jQuery's HTML5-aware <tt>.data()</tt> # from 1.4.3. # @@ -56,6 +57,9 @@ module ActionView # tag("input", type: 'text', disabled: true) # # => <input type="text" disabled="disabled" /> # + # tag("input", type: 'text', class: ["strong", "highlight"]) + # # => <input class="strong highlight" type="text" /> + # # tag("img", src: "open & shut.png") # # => <img src="open & shut.png" /> # @@ -75,7 +79,7 @@ module ActionView # Set escape to false to disable attribute value escaping. # # ==== Options - # The +options+ hash is used with attributes with no value like (<tt>disabled</tt> and + # The +options+ hash can be used with attributes with no value like (<tt>disabled</tt> and # <tt>readonly</tt>), which you can give a value of true in the +options+ hash. You can use # symbols or strings for the attribute names. # @@ -84,6 +88,8 @@ module ActionView # # => <p>Hello world!</p> # content_tag(:div, content_tag(:p, "Hello world!"), class: "strong") # # => <div class="strong"><p>Hello world!</p></div> + # content_tag(:div, "Hello world!", class: ["strong", "highlight"]) + # # => <div class="strong highlight">Hello world!</div> # content_tag("select", options, multiple: true) # # => <select multiple="multiple">...options...</select> # diff --git a/actionview/lib/action_view/rendering.rb b/actionview/lib/action_view/rendering.rb index f96587c816..017302d40f 100644 --- a/actionview/lib/action_view/rendering.rb +++ b/actionview/lib/action_view/rendering.rb @@ -102,11 +102,6 @@ module ActionView # Assign the rendered format to lookup context. def _process_format(format, options = {}) #:nodoc: super - - if options[:body] - self.no_content_type = true - end - lookup_context.formats = [format.to_sym] lookup_context.rendered_format = lookup_context.formats.first end diff --git a/actionview/lib/action_view/version.rb b/actionview/lib/action_view/version.rb index b3ff415775..f55d3fdaef 100644 --- a/actionview/lib/action_view/version.rb +++ b/actionview/lib/action_view/version.rb @@ -1,11 +1,8 @@ +require_relative 'gem_version' + module ActionView - # Returns the version of the currently loaded ActionView as a Gem::Version + # Returns the version of the currently loaded ActionView as a <tt>Gem::Version</tt> def self.version - Gem::Version.new "4.2.0.alpha" - end - - module VERSION #:nodoc: - MAJOR, MINOR, TINY, PRE = ActionView.version.segments - STRING = ActionView.version.to_s + gem_version end end diff --git a/actionview/test/fixtures/digestor/messages/new.html+iphone.erb b/actionview/test/fixtures/digestor/messages/new.html+iphone.erb new file mode 100644 index 0000000000..791e1d36b4 --- /dev/null +++ b/actionview/test/fixtures/digestor/messages/new.html+iphone.erb @@ -0,0 +1,15 @@ +<%# Template Dependency: messages/message %> + +<%= render "header" %> +<%= render "comments/comments" %> + +<%= render "messages/actions/move" %> + +<%= render @message.history.events %> + +<%# render "something_missing" %> +<%# render "something_missing_1" %> + +<% + # Template Dependency: messages/form +%>
\ No newline at end of file diff --git a/actionview/test/template/digestor_test.rb b/actionview/test/template/digestor_test.rb index 779a7fb53c..2406a64310 100644 --- a/actionview/test/template/digestor_test.rb +++ b/actionview/test/template/digestor_test.rb @@ -26,7 +26,11 @@ class FixtureFinder end def find(logical_name, keys, partial, options) - FixtureTemplate.new("digestor/#{partial ? logical_name.gsub(%r|/([^/]+)$|, '/_\1') : logical_name}.#{options[:formats].first}.erb") + partial_name = partial ? logical_name.gsub(%r|/([^/]+)$|, '/_\1') : logical_name + format = options[:formats].first.to_s + format += "+#{options[:variants].first}" if options[:variants].any? + + FixtureTemplate.new("digestor/#{partial_name}.#{format}.erb") end end @@ -194,6 +198,13 @@ class TemplateDigestorTest < ActionView::TestCase end end + def test_variants + assert_digest_difference("messages/new", false, variant: :iphone) do + change_template("messages/new", :iphone) + change_template("messages/_header", :iphone) + end + end + def test_dependencies_via_options_results_in_different_digest digest_plain = digest("comments/_comment") digest_fridge = digest("comments/_comment", dependencies: ["fridge"]) @@ -242,6 +253,11 @@ class TemplateDigestorTest < ActionView::TestCase ActionView::Resolver.caching = resolver_before end + def test_arguments_deprecation + assert_deprecated(/should be provided as a hash/) { ActionView::Digestor.digest('messages/show', :html, finder) } + assert_deprecated(/should be provided as a hash/) { ActionView::Digestor.new('messages/show', :html, finder) } + end + private def assert_logged(message) old_logger = ActionView::Base.logger @@ -258,26 +274,29 @@ class TemplateDigestorTest < ActionView::TestCase end end - def assert_digest_difference(template_name, persistent = false) - previous_digest = digest(template_name) + def assert_digest_difference(template_name, persistent = false, options = {}) + previous_digest = digest(template_name, options) ActionView::Digestor.cache.clear unless persistent yield - assert previous_digest != digest(template_name), "digest didn't change" + assert previous_digest != digest(template_name, options), "digest didn't change" ActionView::Digestor.cache.clear end - def digest(template_name, options={}) - ActionView::Digestor.digest(template_name, :html, finder, options) + def digest(template_name, options = {}) + options = options.dup + ActionView::Digestor.digest({ name: template_name, format: :html, finder: finder }.merge(options)) end def finder @finder ||= FixtureFinder.new end - def change_template(template_name) - File.open("digestor/#{template_name}.html.erb", "w") do |f| + def change_template(template_name, variant = nil) + variant = "+#{variant}" if variant.present? + + File.open("digestor/#{template_name}.html#{variant}.erb", "w") do |f| f.write "\nTHIS WAS CHANGED!" end end diff --git a/actionview/test/template/form_tag_helper_test.rb b/actionview/test/template/form_tag_helper_test.rb index 0d5831dc6f..cf824e2733 100644 --- a/actionview/test/template/form_tag_helper_test.rb +++ b/actionview/test/template/form_tag_helper_test.rb @@ -476,6 +476,11 @@ class FormTagHelperTest < ActionView::TestCase assert_dom_equal('<button name="temptation" type="button"><strong>Do not press me</strong></button>', output) end + def test_button_tag_defaults_with_block_and_options + output = button_tag(:name => 'temptation', :value => 'within') { content_tag(:strong, 'Do not press me') } + assert_dom_equal('<button name="temptation" value="within" type="submit" ><strong>Do not press me</strong></button>', output) + end + def test_button_tag_with_confirmation assert_dom_equal( %(<button name="button" type="submit" data-confirm="Are you sure?">Save</button>), diff --git a/activemodel/README.rdoc b/activemodel/README.rdoc index 4c00755532..500be2a04a 100644 --- a/activemodel/README.rdoc +++ b/activemodel/README.rdoc @@ -49,7 +49,8 @@ behavior out of the box: send("#{attr}=", nil) end end - + + person = Person.new person.clear_name person.clear_age @@ -78,7 +79,21 @@ behavior out of the box: class Person include ActiveModel::Dirty - attr_accessor :name + define_attribute_methods :name + + def name + @name + end + + def name=(val) + name_will_change! unless val == @name + @name = val + end + + def save + # do persistence work + changes_applied + end end person = Person.new @@ -88,6 +103,7 @@ behavior out of the box: person.changed? # => true person.changed # => ['name'] person.changes # => { 'name' => [nil, 'bob'] } + person.save person.name = 'robert' person.save person.previous_changes # => {'name' => ['bob, 'robert']} @@ -116,7 +132,10 @@ behavior out of the box: "Name" end end - + + person = Person.new + person.name = nil + person.validate! person.errors.full_messages # => ["Name cannot be nil"] @@ -180,41 +199,41 @@ behavior out of the box: * Validation support - class Person - include ActiveModel::Validations + class Person + include ActiveModel::Validations - attr_accessor :first_name, :last_name + attr_accessor :first_name, :last_name - validates_each :first_name, :last_name do |record, attr, value| - record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z - end - end + validates_each :first_name, :last_name do |record, attr, value| + record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z + end + end - person = Person.new - person.first_name = 'zoolander' - person.valid? # => false + person = Person.new + person.first_name = 'zoolander' + person.valid? # => false {Learn more}[link:classes/ActiveModel/Validations.html] * Custom validators + + class HasNameValidator < ActiveModel::Validator + def validate(record) + record.errors[:name] = "must exist" if record.name.blank? + end + end + + class ValidatorPerson + include ActiveModel::Validations + validates_with HasNameValidator + attr_accessor :name + end - class ValidatorPerson - include ActiveModel::Validations - validates_with HasNameValidator - attr_accessor :name - end - - class HasNameValidator < ActiveModel::Validator - def validate(record) - record.errors[:name] = "must exist" if record.name.blank? - end - end - - p = ValidatorPerson.new - p.valid? # => false - p.errors.full_messages # => ["Name must exist"] - p.name = "Bob" - p.valid? # => true + p = ValidatorPerson.new + p.valid? # => false + p.errors.full_messages # => ["Name must exist"] + p.name = "Bob" + p.valid? # => true {Learn more}[link:classes/ActiveModel/Validator.html] diff --git a/activemodel/lib/active_model/gem_version.rb b/activemodel/lib/active_model/gem_version.rb new file mode 100644 index 0000000000..964b24398d --- /dev/null +++ b/activemodel/lib/active_model/gem_version.rb @@ -0,0 +1,15 @@ +module ActiveModel + # Returns the version of the currently loaded ActiveModel as a <tt>Gem::Version</tt> + def self.gem_version + Gem::Version.new VERSION::STRING + end + + module VERSION + MAJOR = 4 + MINOR = 2 + TINY = 0 + PRE = "alpha" + + STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") + end +end diff --git a/activemodel/lib/active_model/version.rb b/activemodel/lib/active_model/version.rb index 5c9d2bc6bc..b1f9082ea7 100644 --- a/activemodel/lib/active_model/version.rb +++ b/activemodel/lib/active_model/version.rb @@ -1,11 +1,8 @@ +require_relative 'gem_version' + module ActiveModel - # Returns the version of the currently loaded ActiveModel as a Gem::Version + # Returns the version of the currently loaded ActiveModel as a <tt>Gem::Version</tt> def self.version - Gem::Version.new "4.2.0.alpha" - end - - module VERSION #:nodoc: - MAJOR, MINOR, TINY, PRE = ActiveModel.version.segments - STRING = ActiveModel.version.to_s + gem_version end end diff --git a/activemodel/test/cases/attribute_methods_test.rb b/activemodel/test/cases/attribute_methods_test.rb index e9cb5ccc96..e81b7ac424 100644 --- a/activemodel/test/cases/attribute_methods_test.rb +++ b/activemodel/test/cases/attribute_methods_test.rb @@ -104,10 +104,14 @@ class AttributeMethodsTest < ActiveModel::TestCase end test '#define_attribute_method generates attribute method' do - ModelWithAttributes.define_attribute_method(:foo) + begin + ModelWithAttributes.define_attribute_method(:foo) - assert_respond_to ModelWithAttributes.new, :foo - assert_equal "value of foo", ModelWithAttributes.new.foo + assert_respond_to ModelWithAttributes.new, :foo + assert_equal "value of foo", ModelWithAttributes.new.foo + ensure + ModelWithAttributes.undefine_attribute_methods + end end test '#define_attribute_method does not generate attribute method if already defined in attribute module' do @@ -134,24 +138,36 @@ class AttributeMethodsTest < ActiveModel::TestCase end test '#define_attribute_method generates attribute method with invalid identifier characters' do - ModelWithWeirdNamesAttributes.define_attribute_method(:'a?b') + begin + ModelWithWeirdNamesAttributes.define_attribute_method(:'a?b') - assert_respond_to ModelWithWeirdNamesAttributes.new, :'a?b' - assert_equal "value of a?b", ModelWithWeirdNamesAttributes.new.send('a?b') + assert_respond_to ModelWithWeirdNamesAttributes.new, :'a?b' + assert_equal "value of a?b", ModelWithWeirdNamesAttributes.new.send('a?b') + ensure + ModelWithWeirdNamesAttributes.undefine_attribute_methods + end end test '#define_attribute_methods works passing multiple arguments' do - ModelWithAttributes.define_attribute_methods(:foo, :baz) + begin + ModelWithAttributes.define_attribute_methods(:foo, :baz) - assert_equal "value of foo", ModelWithAttributes.new.foo - assert_equal "value of baz", ModelWithAttributes.new.baz + assert_equal "value of foo", ModelWithAttributes.new.foo + assert_equal "value of baz", ModelWithAttributes.new.baz + ensure + ModelWithAttributes.undefine_attribute_methods + end end test '#define_attribute_methods generates attribute methods' do - ModelWithAttributes.define_attribute_methods(:foo) + begin + ModelWithAttributes.define_attribute_methods(:foo) - assert_respond_to ModelWithAttributes.new, :foo - assert_equal "value of foo", ModelWithAttributes.new.foo + assert_respond_to ModelWithAttributes.new, :foo + assert_equal "value of foo", ModelWithAttributes.new.foo + ensure + ModelWithAttributes.undefine_attribute_methods + end end test '#alias_attribute generates attribute_aliases lookup hash' do @@ -164,26 +180,38 @@ class AttributeMethodsTest < ActiveModel::TestCase end test '#define_attribute_methods generates attribute methods with spaces in their names' do - ModelWithAttributesWithSpaces.define_attribute_methods(:'foo bar') + begin + ModelWithAttributesWithSpaces.define_attribute_methods(:'foo bar') - assert_respond_to ModelWithAttributesWithSpaces.new, :'foo bar' - assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.send(:'foo bar') + assert_respond_to ModelWithAttributesWithSpaces.new, :'foo bar' + assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.send(:'foo bar') + ensure + ModelWithAttributesWithSpaces.undefine_attribute_methods + end end test '#alias_attribute works with attributes with spaces in their names' do - ModelWithAttributesWithSpaces.define_attribute_methods(:'foo bar') - ModelWithAttributesWithSpaces.alias_attribute(:'foo_bar', :'foo bar') + begin + ModelWithAttributesWithSpaces.define_attribute_methods(:'foo bar') + ModelWithAttributesWithSpaces.alias_attribute(:'foo_bar', :'foo bar') - assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.foo_bar + assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.foo_bar + ensure + ModelWithAttributesWithSpaces.undefine_attribute_methods + end end test '#alias_attribute works with attributes named as a ruby keyword' do - ModelWithRubyKeywordNamedAttributes.define_attribute_methods([:begin, :end]) - ModelWithRubyKeywordNamedAttributes.alias_attribute(:from, :begin) - ModelWithRubyKeywordNamedAttributes.alias_attribute(:to, :end) - - assert_equal "value of begin", ModelWithRubyKeywordNamedAttributes.new.from - assert_equal "value of end", ModelWithRubyKeywordNamedAttributes.new.to + begin + ModelWithRubyKeywordNamedAttributes.define_attribute_methods([:begin, :end]) + ModelWithRubyKeywordNamedAttributes.alias_attribute(:from, :begin) + ModelWithRubyKeywordNamedAttributes.alias_attribute(:to, :end) + + assert_equal "value of begin", ModelWithRubyKeywordNamedAttributes.new.from + assert_equal "value of end", ModelWithRubyKeywordNamedAttributes.new.to + ensure + ModelWithRubyKeywordNamedAttributes.undefine_attribute_methods + end end test '#undefine_attribute_methods removes attribute methods' do diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index def28578f8..42d0365521 100644 --- a/activemodel/test/cases/errors_test.rb +++ b/activemodel/test/cases/errors_test.rb @@ -82,6 +82,13 @@ class ErrorsTest < ActiveModel::TestCase assert_equal({ foo: "omg" }, errors.messages) end + test "error access is indifferent" do + errors = ActiveModel::Errors.new(self) + errors[:foo] = "omg" + + assert_equal ["omg"], errors["foo"] + end + test "values returns an array of messages" do errors = ActiveModel::Errors.new(self) errors.set(:foo, "omg") diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb index 82fd291064..bcd1e04a0f 100644 --- a/activemodel/test/cases/secure_password_test.rb +++ b/activemodel/test/cases/secure_password_test.rb @@ -4,6 +4,8 @@ require 'models/visitor' class SecurePasswordTest < ActiveModel::TestCase setup do + # Used only to speed up tests + @original_min_cost = ActiveModel::SecurePassword.min_cost ActiveModel::SecurePassword.min_cost = true @user = User.new @@ -15,7 +17,7 @@ class SecurePasswordTest < ActiveModel::TestCase end teardown do - ActiveModel::SecurePassword.min_cost = false + ActiveModel::SecurePassword.min_cost = @original_min_cost end test "create and updating without validations" do @@ -147,7 +149,7 @@ class SecurePasswordTest < ActiveModel::TestCase test "setting a nil password should clear an existing password" do @existing_user.password = nil assert_equal nil, @existing_user.password_digest - end + end test "authenticate" do @user.password = "secret" @@ -164,11 +166,16 @@ class SecurePasswordTest < ActiveModel::TestCase end test "Password digest cost honors bcrypt cost attribute when min_cost is false" do - ActiveModel::SecurePassword.min_cost = false - BCrypt::Engine.cost = 5 - - @user.password = "secret" - assert_equal BCrypt::Engine.cost, @user.password_digest.cost + begin + original_bcrypt_cost = BCrypt::Engine.cost + ActiveModel::SecurePassword.min_cost = false + BCrypt::Engine.cost = 5 + + @user.password = "secret" + assert_equal BCrypt::Engine.cost, @user.password_digest.cost + ensure + BCrypt::Engine.cost = original_bcrypt_cost + end end test "Password digest cost can be set to bcrypt min cost to speed up tests" do diff --git a/activemodel/test/cases/serializers/json_serialization_test.rb b/activemodel/test/cases/serializers/json_serialization_test.rb index bc185c737f..60414a6570 100644 --- a/activemodel/test/cases/serializers/json_serialization_test.rb +++ b/activemodel/test/cases/serializers/json_serialization_test.rb @@ -30,11 +30,6 @@ class JsonSerializationTest < ActiveModel::TestCase @contact.preferences = { 'shows' => 'anime' } end - def teardown - # set to the default value - Contact.include_root_in_json = false - end - test "should not include root in json (class method)" do json = @contact.to_json @@ -47,19 +42,25 @@ class JsonSerializationTest < ActiveModel::TestCase end test "should include root in json if include_root_in_json is true" do - Contact.include_root_in_json = true - json = @contact.to_json - - assert_match %r{^\{"contact":\{}, json - assert_match %r{"name":"Konata Izumi"}, json - assert_match %r{"age":16}, json - assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))})) - assert_match %r{"awesome":true}, json - assert_match %r{"preferences":\{"shows":"anime"\}}, json + begin + original_include_root_in_json = Contact.include_root_in_json + Contact.include_root_in_json = true + json = @contact.to_json + + assert_match %r{^\{"contact":\{}, json + assert_match %r{"name":"Konata Izumi"}, json + assert_match %r{"age":16}, json + assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))})) + assert_match %r{"awesome":true}, json + assert_match %r{"preferences":\{"shows":"anime"\}}, json + ensure + Contact.include_root_in_json = original_include_root_in_json + end end test "should include root in json (option) even if the default is set to false" do json = @contact.to_json(root: true) + assert_match %r{^\{"contact":\{}, json end @@ -145,13 +146,18 @@ class JsonSerializationTest < ActiveModel::TestCase end test "as_json should return a hash if include_root_in_json is true" do - Contact.include_root_in_json = true - json = @contact.as_json - - assert_kind_of Hash, json - assert_kind_of Hash, json['contact'] - %w(name age created_at awesome preferences).each do |field| - assert_equal @contact.send(field), json['contact'][field] + begin + original_include_root_in_json = Contact.include_root_in_json + Contact.include_root_in_json = true + json = @contact.as_json + + assert_kind_of Hash, json + assert_kind_of Hash, json['contact'] + %w(name age created_at awesome preferences).each do |field| + assert_equal @contact.send(field), json['contact'][field] + end + ensure + Contact.include_root_in_json = original_include_root_in_json end end diff --git a/activemodel/test/cases/serializers/xml_serialization_test.rb b/activemodel/test/cases/serializers/xml_serialization_test.rb index 11ee17bb27..73f316d5e0 100644 --- a/activemodel/test/cases/serializers/xml_serialization_test.rb +++ b/activemodel/test/cases/serializers/xml_serialization_test.rb @@ -57,48 +57,48 @@ class XmlSerializationTest < ActiveModel::TestCase end test "should serialize default root" do - @xml = @contact.to_xml - assert_match %r{^<contact>}, @xml - assert_match %r{</contact>$}, @xml + xml = @contact.to_xml + assert_match %r{^<contact>}, xml + assert_match %r{</contact>$}, xml end test "should serialize namespaced root" do - @xml = Admin::Contact.new(@contact.attributes).to_xml - assert_match %r{^<contact>}, @xml - assert_match %r{</contact>$}, @xml + xml = Admin::Contact.new(@contact.attributes).to_xml + assert_match %r{^<contact>}, xml + assert_match %r{</contact>$}, xml end test "should serialize default root with namespace" do - @xml = @contact.to_xml namespace: "http://xml.rubyonrails.org/contact" - assert_match %r{^<contact xmlns="http://xml.rubyonrails.org/contact">}, @xml - assert_match %r{</contact>$}, @xml + xml = @contact.to_xml namespace: "http://xml.rubyonrails.org/contact" + assert_match %r{^<contact xmlns="http://xml.rubyonrails.org/contact">}, xml + assert_match %r{</contact>$}, xml end test "should serialize custom root" do - @xml = @contact.to_xml root: 'xml_contact' - assert_match %r{^<xml-contact>}, @xml - assert_match %r{</xml-contact>$}, @xml + xml = @contact.to_xml root: 'xml_contact' + assert_match %r{^<xml-contact>}, xml + assert_match %r{</xml-contact>$}, xml end test "should allow undasherized tags" do - @xml = @contact.to_xml root: 'xml_contact', dasherize: false - assert_match %r{^<xml_contact>}, @xml - assert_match %r{</xml_contact>$}, @xml - assert_match %r{<created_at}, @xml + xml = @contact.to_xml root: 'xml_contact', dasherize: false + assert_match %r{^<xml_contact>}, xml + assert_match %r{</xml_contact>$}, xml + assert_match %r{<created_at}, xml end test "should allow camelized tags" do - @xml = @contact.to_xml root: 'xml_contact', camelize: true - assert_match %r{^<XmlContact>}, @xml - assert_match %r{</XmlContact>$}, @xml - assert_match %r{<CreatedAt}, @xml + xml = @contact.to_xml root: 'xml_contact', camelize: true + assert_match %r{^<XmlContact>}, xml + assert_match %r{</XmlContact>$}, xml + assert_match %r{<CreatedAt}, xml end test "should allow lower-camelized tags" do - @xml = @contact.to_xml root: 'xml_contact', camelize: :lower - assert_match %r{^<xmlContact>}, @xml - assert_match %r{</xmlContact>$}, @xml - assert_match %r{<createdAt}, @xml + xml = @contact.to_xml root: 'xml_contact', camelize: :lower + assert_match %r{^<xmlContact>}, xml + assert_match %r{</xmlContact>$}, xml + assert_match %r{<createdAt}, xml end test "should use serializable hash" do @@ -106,22 +106,22 @@ class XmlSerializationTest < ActiveModel::TestCase @contact.name = 'aaron stack' @contact.age = 25 - @xml = @contact.to_xml - assert_match %r{<name>aaron stack</name>}, @xml - assert_match %r{<age type="integer">25</age>}, @xml - assert_no_match %r{<awesome>}, @xml + xml = @contact.to_xml + assert_match %r{<name>aaron stack</name>}, xml + assert_match %r{<age type="integer">25</age>}, xml + assert_no_match %r{<awesome>}, xml end test "should allow skipped types" do - @xml = @contact.to_xml skip_types: true - assert_match %r{<age>25</age>}, @xml + xml = @contact.to_xml skip_types: true + assert_match %r{<age>25</age>}, xml end test "should include yielded additions" do - @xml = @contact.to_xml do |xml| + xml = @contact.to_xml do |xml| xml.creator "David" end - assert_match %r{<creator>David</creator>}, @xml + assert_match %r{<creator>David</creator>}, xml end test "should serialize string" do @@ -162,7 +162,7 @@ class XmlSerializationTest < ActiveModel::TestCase assert_match %r{<nationality>unknown</nationality>}, xml end - test 'should supply serializable to second proc argument' do + test "should supply serializable to second proc argument" do proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) } xml = @contact.to_xml(procs: [ proc ]) assert_match %r{<name-reverse>kcats noraa</name-reverse>}, xml diff --git a/activemodel/test/cases/translation_test.rb b/activemodel/test/cases/translation_test.rb index deb4e1ed0a..cedc812ec7 100644 --- a/activemodel/test/cases/translation_test.rb +++ b/activemodel/test/cases/translation_test.rb @@ -7,6 +7,10 @@ class ActiveModelI18nTests < ActiveModel::TestCase I18n.backend = I18n::Backend::Simple.new end + def teardown + I18n.backend.reload! + end + def test_translated_model_attributes I18n.backend.store_translations 'en', activemodel: { attributes: { person: { name: 'person name attribute' } } } assert_equal 'person name attribute', Person.human_attribute_name('name') diff --git a/activemodel/test/cases/validations/confirmation_validation_test.rb b/activemodel/test/cases/validations/confirmation_validation_test.rb index 4957ba5d0a..65a2a1eb49 100644 --- a/activemodel/test/cases/validations/confirmation_validation_test.rb +++ b/activemodel/test/cases/validations/confirmation_validation_test.rb @@ -53,22 +53,25 @@ class ConfirmationValidationTest < ActiveModel::TestCase end def test_title_confirmation_with_i18n_attribute - @old_load_path, @old_backend = I18n.load_path.dup, I18n.backend - I18n.load_path.clear - I18n.backend = I18n::Backend::Simple.new - I18n.backend.store_translations('en', { - errors: { messages: { confirmation: "doesn't match %{attribute}" } }, - activemodel: { attributes: { topic: { title: 'Test Title'} } } - }) - - Topic.validates_confirmation_of(:title) - - t = Topic.new("title" => "We should be confirmed","title_confirmation" => "") - assert t.invalid? - assert_equal ["doesn't match Test Title"], t.errors[:title_confirmation] - - I18n.load_path.replace @old_load_path - I18n.backend = @old_backend + begin + @old_load_path, @old_backend = I18n.load_path.dup, I18n.backend + I18n.load_path.clear + I18n.backend = I18n::Backend::Simple.new + I18n.backend.store_translations('en', { + errors: { messages: { confirmation: "doesn't match %{attribute}" } }, + activemodel: { attributes: { topic: { title: 'Test Title'} } } + }) + + Topic.validates_confirmation_of(:title) + + t = Topic.new("title" => "We should be confirmed","title_confirmation" => "") + assert t.invalid? + assert_equal ["doesn't match Test Title"], t.errors[:title_confirmation] + ensure + I18n.load_path.replace @old_load_path + I18n.backend = @old_backend + I18n.backend.reload! + end end test "does not override confirmation reader if present" do diff --git a/activemodel/test/cases/validations/i18n_validation_test.rb b/activemodel/test/cases/validations/i18n_validation_test.rb index d10010537e..96084a32ba 100644 --- a/activemodel/test/cases/validations/i18n_validation_test.rb +++ b/activemodel/test/cases/validations/i18n_validation_test.rb @@ -19,6 +19,7 @@ class I18nValidationTest < ActiveModel::TestCase Person.clear_validators! I18n.load_path.replace @old_load_path I18n.backend = @old_backend + I18n.backend.reload! end def test_full_message_encoding diff --git a/activemodel/test/cases/validations/numericality_validation_test.rb b/activemodel/test/cases/validations/numericality_validation_test.rb index f77cf47fb7..e1657407cf 100644 --- a/activemodel/test/cases/validations/numericality_validation_test.rb +++ b/activemodel/test/cases/validations/numericality_validation_test.rb @@ -119,6 +119,7 @@ class NumericalityValidationTest < ActiveModel::TestCase invalid!([3, 4]) valid!([5, 6]) + ensure Topic.send(:remove_method, :min_approved) end @@ -128,6 +129,7 @@ class NumericalityValidationTest < ActiveModel::TestCase invalid!([6]) valid!([4, 5]) + ensure Topic.send(:remove_method, :max_approved) end diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb index cfce8fb8e6..4ea5670d32 100644 --- a/activemodel/test/cases/validations_test.rb +++ b/activemodel/test/cases/validations_test.rb @@ -139,6 +139,8 @@ class ValidationsTest < ActiveModel::TestCase assert_equal 4, hits assert_equal %w(gotcha gotcha), t.errors[:title] assert_equal %w(gotcha gotcha), t.errors[:content] + ensure + CustomReader.clear_validators! end def test_validate_block diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 58f680a03f..209eb9cb95 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,10 +1,57 @@ -* `before_add` callbacks are fired before the record is saved on - `has_and_belongs_to_many` assocations *and* on `has_many :through` - associations. Before this change, `before_add` callbacks would be fired - before the record was saved on `has_and_belongs_to_many` associations, but - *not* on `has_many :through` associations. +* Only use BINARY for mysql case sensitive uniqueness check when column has a case insensitive collation. - Fixes #14144 + *Ryuta Kamizono* + +* Support for Mysql 5.6 Fractional Seconds. + + *arthurnn*, *Tatsuhiko Miyagawa* + +* Support for Postgres `citext` data type enabling case-insensitive where + values without needing to wrap in UPPER/LOWER sql functions. + + *Troy Kruthoff*, *Lachlan Sylvester* + +* Only save has_one associations if record has changes. + Previously after save related callbacks, such as `#after_commit`, were triggered when the has_one + object did not get saved to the db. + + *Alan Kennedy* + +* Allow strings to specify the `#order` value. + + Example: + + Model.order(id: 'asc').to_sql == Model.order(id: :asc).to_sql + + *Marcelo Casiraghi*, *Robin Dupret* + +* Dynamically register PostgreSQL enum OIDs. This prevents "unknown OID" + warnings on enum columns. + + *Dieter Komendera* + +* `includes` is able to detect the right preloading strategy when string + joins are involved. + + Fixes #14109. + + *Aaron Patterson*, *Yves Senn* + +* Fixed error with validation with enum fields for records where the + value for any enum attribute is always evaluated as 0 during + uniqueness validation. + + Fixes #14172. + + *Vilius Luneckas* *Ahmed AbouElhamayed* + +* `before_add` callbacks are fired before the record is saved on + `has_and_belongs_to_many` assocations *and* on `has_many :through` + associations. Before this change, `before_add` callbacks would be fired + before the record was saved on `has_and_belongs_to_many` associations, but + *not* on `has_many :through` associations. + + Fixes #14144. * Fixed STI classes not defining an attribute method if there is a conflicting private method defined on its ancestors. @@ -13,8 +60,7 @@ *Godfrey Chan* -* Coerce strings when reading attributes. - Fixes #10485. +* Coerce strings when reading attributes. Fixes #10485. Example: diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 53f7591226..f725356cd9 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -530,8 +530,8 @@ module ActiveRecord # end # # @firm = Firm.first - # @firm.clients.collect { |c| c.invoices }.flatten # select all invoices for all clients of the firm - # @firm.invoices # selects all invoices by going through the Client join model + # @firm.clients.flat_map { |c| c.invoices } # select all invoices for all clients of the firm + # @firm.invoices # selects all invoices by going through the Client join model # # Similarly you can go through a +has_one+ association on the join model: # diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 270871c866..9a2900843e 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -359,7 +359,9 @@ module ActiveRecord if owner.new_record? replace_records(other_array, original_target) else - transaction { replace_records(other_array, original_target) } + if other_array != original_target + transaction { replace_records(other_array, original_target) } + end end end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 6457182195..3e4b7902c0 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -71,15 +71,15 @@ module ActiveRecord [association_scope.limit_value, count].compact.min end - def has_cached_counter?(reflection = reflection) + def has_cached_counter?(reflection = reflection()) owner.attribute_present?(cached_counter_attribute_name(reflection)) end - def cached_counter_attribute_name(reflection = reflection) + def cached_counter_attribute_name(reflection = reflection()) options[:counter_cache] || "#{reflection.name}_count" end - def update_counter(difference, reflection = reflection) + def update_counter(difference, reflection = reflection()) if has_cached_counter?(reflection) counter = cached_counter_attribute_name(reflection) owner.class.update_counters(owner.id, counter => difference) @@ -98,7 +98,7 @@ module ActiveRecord # it will be decremented twice. # # Hence this method. - def inverse_updates_counter_cache?(reflection = reflection) + def inverse_updates_counter_cache?(reflection = reflection()) counter_name = cached_counter_attribute_name(reflection) reflection.klass.reflect_on_all_associations(:belongs_to).any? { |inverse_reflection| inverse_reflection.counter_cache_column == counter_name diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index 2a8530af62..70e97432e4 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -23,7 +23,7 @@ module ActiveRecord reset_association owners, through_reflection.name - middle_records = through_records.map { |(_,rec)| rec }.flatten + middle_records = through_records.flat_map { |(_,rec)| rec } preloaders = preloader.preload(middle_records, source_reflection.name, diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 57ceec2fc5..ea48a13ea8 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -105,9 +105,9 @@ module ActiveRecord super else # If B < A and A defines its own attribute method, then we don't want to overwrite that. - defined = method_defined_within?(method_name, superclass) && - superclass.instance_method(method_name).owner != superclass.generated_attribute_methods - defined || super + defined = method_defined_within?(method_name, superclass, superclass.generated_attribute_methods) + base_defined = Base.method_defined?(method_name) || Base.private_method_defined?(method_name) + defined && !base_defined || super end end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index e9622ca0c1..213b72b933 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -377,15 +377,16 @@ module ActiveRecord def save_has_one_association(reflection) association = association_instance_get(reflection.name) record = association && association.load_target + if record && !record.destroyed? autosave = reflection.options[:autosave] if autosave && record.marked_for_destruction? record.destroy - else + elsif autosave != false key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id - if autosave != false && (autosave || new_record? || record_changed?(reflection, record, key)) + if (autosave && record.changed_for_autosave?) || new_record? || record_changed?(reflection, record, key) unless reflection.through_reflection record[reflection.foreign_key] = key end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index ea81cf1736..4828e45f62 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -346,6 +346,11 @@ module ActiveRecord node end + def case_sensitive_comparison(table, attribute, column, value) + value = case_sensitive_modifier(value) unless value.nil? + table[attribute].eq(value) + end + def case_insensitive_comparison(table, attribute, column, value) table[attribute].lower.eq(table.lower(value)) end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 23edc8b955..9a819720f7 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -459,7 +459,7 @@ module ActiveRecord end def bulk_change_table(table_name, operations) #:nodoc: - sqls = operations.map do |command, args| + sqls = operations.flat_map do |command, args| table, arguments = args.shift, args method = :"#{command}_sql" @@ -468,7 +468,7 @@ module ActiveRecord else raise "Unknown method called : #{method}(#{arguments.inspect})" end - end.flatten.join(", ") + end.join(", ") execute("ALTER TABLE #{quote_table_name(table_name)} #{sqls}") end @@ -592,6 +592,14 @@ module ActiveRecord Arel::Nodes::Bin.new(node) end + def case_sensitive_comparison(table, attribute, column, value) + if column.case_sensitive? + table[attribute].eq(value) + else + super + end + end + def case_insensitive_comparison(table, attribute, column, value) if column.case_sensitive? super diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 69c2a361ee..81b9703684 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -87,6 +87,14 @@ module ActiveRecord @connection.escape(string) end + def quoted_date(value) + if value.acts_like?(:time) && value.respond_to?(:usec) + "#{super}.#{sprintf("%06d", value.usec)}" + else + super + end + end + # CONNECTION MANAGEMENT ==================================== def active? diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index e7df073627..5d32aaed50 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -229,6 +229,12 @@ This is not reliable and will be removed in the future. end end + class Enum < Type + def type_cast(value) + value.to_s + end + end + class Hstore < Type def type_cast_for_write(value) ConnectionAdapters::PostgreSQLColumn.hstore_to_string value @@ -369,6 +375,7 @@ This is not reliable and will be removed in the future. register_type 'circle', OID::Text.new register_type 'hstore', OID::Hstore.new register_type 'json', OID::Json.new + register_type 'citext', OID::Text.new register_type 'ltree', OID::Text.new register_type 'cidr', OID::Cidr.new diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index d1fb132b60..e5f7913c70 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -215,6 +215,8 @@ module ActiveRecord # Character types when /^(?:character varying|bpchar)(?:\(\d+\))?$/ :string + when /^citext(?:\(\d+\))?$/ + :citext # Binary data types when 'bytea' :binary @@ -353,6 +355,10 @@ module ActiveRecord def json(name, options = {}) column(name, 'json', options) end + + def citext(name, options = {}) + column(name, 'citext', options) + end end class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition @@ -393,6 +399,10 @@ module ActiveRecord column name, type, options end + def citext(name, options = {}) + column(name, 'citext', options) + end + def column(name, type = nil, options = {}) super column = self[name] @@ -441,7 +451,8 @@ module ActiveRecord macaddr: { name: "macaddr" }, uuid: { name: "uuid" }, json: { name: "json" }, - ltree: { name: "ltree" } + ltree: { name: "ltree" }, + citext: { name: "citext" } } include Quoting @@ -801,6 +812,12 @@ module ActiveRecord leaves, nodes = nodes.partition { |row| row['typelem'] == '0' } arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' } + # populate the enum types + enums, leaves = leaves.partition { |row| row['typinput'] == 'enum_in' } + enums.each do |row| + type_map[row['oid'].to_i] = OID::Enum.new + end + # populate the base types leaves.find_all { |row| OID.registered_type? row['typname'] }.each do |row| type_map[row['oid'].to_i] = OID::NAMES[row['typname']] @@ -822,7 +839,7 @@ module ActiveRecord # populate range types ranges.find_all { |row| type_map.key? row['rngsubtype'].to_i }.each do |row| subtype = type_map[row['rngsubtype'].to_i] - range = OID::Range.new type_map[row['rngsubtype'].to_i] + range = OID::Range.new subtype type_map[row['oid'].to_i] = range end end diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index 4ba4e09777..bbb866cedf 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -18,14 +18,14 @@ module ActiveRecord # Example for SQLite database: # # ActiveRecord::Base.establish_connection( - # adapter: "sqlite", + # adapter: "sqlite3", # database: "path/to/dbfile" # ) # # Also accepts keys as strings (for parsing from YAML for example): # # ActiveRecord::Base.establish_connection( - # "adapter" => "sqlite", + # "adapter" => "sqlite3", # "database" => "path/to/dbfile" # ) # diff --git a/activerecord/lib/active_record/gem_version.rb b/activerecord/lib/active_record/gem_version.rb new file mode 100644 index 0000000000..4a7aace460 --- /dev/null +++ b/activerecord/lib/active_record/gem_version.rb @@ -0,0 +1,15 @@ +module ActiveRecord + # Returns the version of the currently loaded ActiveRecord as a <tt>Gem::Version</tt> + def self.gem_version + Gem::Version.new VERSION::STRING + end + + module VERSION + MAJOR = 4 + MINOR = 2 + TINY = 0 + PRE = "alpha" + + STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") + end +end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 203928fb3f..4e63206cf4 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -214,6 +214,8 @@ module ActiveRecord # # This method raises an +ActiveRecord::ActiveRecordError+ if the # attribute is marked as readonly. + # + # See also +update_column+. def update_attribute(name, value) name = name.to_s verify_readonly_attribute(name) diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 0959860ec6..d95f01e89c 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -618,7 +618,9 @@ module ActiveRecord def references_eager_loaded_tables? joined_tables = arel.join_sources.map do |join| - unless join.is_a?(Arel::Nodes::StringJoin) + if join.is_a?(Arel::Nodes::StringJoin) + tables_in_string(join.left) + else [join.left.table_name, join.left.table_alias] end end @@ -630,5 +632,12 @@ module ActiveRecord (references_values - joined_tables).any? end + + def tables_in_string(string) + return [] if string.blank? + # always convert table names to downcase as in Oracle quoted table names are in uppercase + # ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries + string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map{ |s| s.downcase }.uniq - ['raw_sql_'] + end end end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 4519a756a7..61b129a71e 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -292,7 +292,12 @@ module ActiveRecord when Array, Hash relation = relation.where(conditions) else - relation = relation.where(table[primary_key].eq(conditions)) if conditions != :none + if conditions != :none + column = columns_hash[primary_key] + substitute = connection.substitute_at(column, bind_values.length) + relation = where(table[primary_key].eq(substitute)) + relation.bind_values += [[column, conditions]] + end end connection.select_value(relation, "#{name} Exists", relation.bind_values) ? true : false diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index d722a32f45..c5a695c91b 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -202,7 +202,7 @@ module ActiveRecord # fields are retrieved: # # Model.select(:field) - # # => [#<Model field:value>] + # # => [#<Model id: nil, field: "value">] # # Although in the above example it looks as though this method returns an # array, it actually returns a relation object and can have other query @@ -211,12 +211,12 @@ module ActiveRecord # The argument to the method can also be an array of fields. # # Model.select(:field, :other_field, :and_one_more) - # # => [#<Model field: "value", other_field: "value", and_one_more: "value">] + # # => [#<Model id: nil, field: "value", other_field: "value", and_one_more: "value">] # # You can also use one or more strings, which will be used unchanged as SELECT fields. # # Model.select('field AS field_one', 'other_field AS field_two') - # # => [#<Model field: "value", other_field: "value">] + # # => [#<Model id: nil, field: "value", other_field: "value">] # # If an alias was specified, it will be accessible from the resulting objects: # @@ -224,7 +224,7 @@ module ActiveRecord # # => "value" # # Accessing attributes of an object that do not have fields retrieved by a select - # will throw <tt>ActiveModel::MissingAttributeError</tt>: + # except +id+ will throw <tt>ActiveModel::MissingAttributeError</tt>: # # Model.select(:field).first.other_field # # => ActiveModel::MissingAttributeError: missing attribute: other_field @@ -275,15 +275,6 @@ module ActiveRecord # Allows to specify an order attribute: # - # User.order('name') - # => SELECT "users".* FROM "users" ORDER BY name - # - # User.order('name DESC') - # => SELECT "users".* FROM "users" ORDER BY name DESC - # - # User.order('name DESC, email') - # => SELECT "users".* FROM "users" ORDER BY name DESC, email - # # User.order(:name) # => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC # @@ -292,6 +283,15 @@ module ActiveRecord # # User.order(:name, email: :desc) # => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC + # + # User.order('name') + # => SELECT "users".* FROM "users" ORDER BY name + # + # User.order('name DESC') + # => SELECT "users".* FROM "users" ORDER BY name DESC + # + # User.order('name DESC, email') + # => SELECT "users".* FROM "users" ORDER BY name DESC, email def order(*args) check_if_method_has_arguments!(:order, args) spawn.order!(*args) @@ -1065,10 +1065,15 @@ module ActiveRecord arel.order(*orders) unless orders.empty? end + VALID_DIRECTIONS = [:asc, :desc, :ASC, :DESC, + 'asc', 'desc', 'ASC', 'DESC'] # :nodoc: + def validate_order_args(args) - args.grep(Hash) do |h| - unless (h.values - [:asc, :desc]).empty? - raise ArgumentError, 'Direction should be :asc or :desc' + args.each do |arg| + next unless arg.is_a?(Hash) + arg.each do |_key, value| + raise ArgumentError, "Direction \"#{value}\" is invalid. Valid " \ + "directions are: #{VALID_DIRECTIONS.inspect}" unless VALID_DIRECTIONS.include?(value) end end end @@ -1090,7 +1095,7 @@ module ActiveRecord when Hash arg.map { |field, dir| field = klass.attribute_alias(field) if klass.attribute_alias?(field) - table[field].send(dir) + table[field].send(dir.downcase) } else arg diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index 01fec31544..73ab3b39aa 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -11,7 +11,7 @@ module ActiveRecord end module ClassMethods - # Returns a scope for the model without the +default_scope+. + # Returns a scope for the model without the previously set scopes. # # class Post < ActiveRecord::Base # def self.default_scope @@ -19,11 +19,12 @@ module ActiveRecord # end # end # - # Post.all # Fires "SELECT * FROM posts WHERE published = true" - # Post.unscoped.all # Fires "SELECT * FROM posts" + # Post.all # Fires "SELECT * FROM posts WHERE published = true" + # Post.unscoped.all # Fires "SELECT * FROM posts" + # Post.where(published: false).unscoped.all # Fires "SELECT * FROM posts" # # This method also accepts a block. All queries inside the block will - # not use the +default_scope+: + # not use the previously set scopes. # # Post.unscoped { # Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10" diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 7ebe9dfec0..71c71cb4b1 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -13,6 +13,7 @@ module ActiveRecord def validate_each(record, attribute, value) finder_class = find_finder_class_for(record) table = finder_class.arel_table + value = map_enum_attribute(finder_class, attribute, value) value = deserialize_attribute(record, attribute, value) relation = build_relation(finder_class, table, attribute, value) @@ -67,8 +68,7 @@ module ActiveRecord # will use SQL LOWER function before comparison, unless it detects a case insensitive collation klass.connection.case_insensitive_comparison(table, attribute, column, value) else - value = klass.connection.case_sensitive_modifier(value) unless value.nil? - table[attribute].eq(value) + klass.connection.case_sensitive_comparison(table, attribute, column, value) end end @@ -91,6 +91,12 @@ module ActiveRecord value = coder.dump value if value && coder value end + + def map_enum_attribute(klass, attribute, value) + mapping = klass.enum_mapping_for(attribute.to_s) + value = mapping[value] if value && mapping + value + end end module ClassMethods diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb index 5eb7f7e205..cf76a13b44 100644 --- a/activerecord/lib/active_record/version.rb +++ b/activerecord/lib/active_record/version.rb @@ -1,11 +1,8 @@ +require_relative 'gem_version' + module ActiveRecord - # Returns the version of the currently loaded ActiveRecord as a Gem::Version + # Returns the version of the currently loaded ActiveRecord as a <tt>Gem::Version</tt> def self.version - Gem::Version.new "4.2.0.alpha" - end - - module VERSION #:nodoc: - MAJOR, MINOR, TINY, PRE = ActiveRecord.version.segments - STRING = ActiveRecord.version.to_s + gem_version end end diff --git a/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb index 97adb6b297..340fc95503 100644 --- a/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb +++ b/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb @@ -3,10 +3,10 @@ require 'models/person' class MysqlCaseSensitivityTest < ActiveRecord::TestCase class CollationTest < ActiveRecord::Base - validates_uniqueness_of :string_cs_column, :case_sensitive => false - validates_uniqueness_of :string_ci_column, :case_sensitive => false end + repair_validations(CollationTest) + def test_columns_include_collation_different_from_table assert_equal 'utf8_bin', CollationTest.columns_hash['string_cs_column'].collation assert_equal 'utf8_general_ci', CollationTest.columns_hash['string_ci_column'].collation @@ -18,6 +18,7 @@ class MysqlCaseSensitivityTest < ActiveRecord::TestCase end def test_case_insensitive_comparison_for_ci_column + CollationTest.validates_uniqueness_of(:string_ci_column, :case_sensitive => false) CollationTest.create!(:string_ci_column => 'A') invalid = CollationTest.new(:string_ci_column => 'a') queries = assert_sql { invalid.save } @@ -26,10 +27,29 @@ class MysqlCaseSensitivityTest < ActiveRecord::TestCase end def test_case_insensitive_comparison_for_cs_column + CollationTest.validates_uniqueness_of(:string_cs_column, :case_sensitive => false) CollationTest.create!(:string_cs_column => 'A') invalid = CollationTest.new(:string_cs_column => 'a') queries = assert_sql { invalid.save } cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/) } assert_match(/lower/i, cs_uniqueness_query) end + + def test_case_sensitive_comparison_for_ci_column + CollationTest.validates_uniqueness_of(:string_ci_column, :case_sensitive => true) + CollationTest.create!(:string_ci_column => 'A') + invalid = CollationTest.new(:string_ci_column => 'A') + queries = assert_sql { invalid.save } + ci_uniqueness_query = queries.detect { |q| q.match(/string_ci_column/) } + assert_match(/binary/i, ci_uniqueness_query) + end + + def test_case_sensitive_comparison_for_cs_column + CollationTest.validates_uniqueness_of(:string_cs_column, :case_sensitive => true) + CollationTest.create!(:string_cs_column => 'A') + invalid = CollationTest.new(:string_cs_column => 'A') + queries = assert_sql { invalid.save } + cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/) } + assert_no_match(/binary/i, cs_uniqueness_query) + end end diff --git a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb index 6bcc113482..09bebf3071 100644 --- a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb +++ b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb @@ -3,10 +3,10 @@ require 'models/person' class Mysql2CaseSensitivityTest < ActiveRecord::TestCase class CollationTest < ActiveRecord::Base - validates_uniqueness_of :string_cs_column, :case_sensitive => false - validates_uniqueness_of :string_ci_column, :case_sensitive => false end + repair_validations(CollationTest) + def test_columns_include_collation_different_from_table assert_equal 'utf8_bin', CollationTest.columns_hash['string_cs_column'].collation assert_equal 'utf8_general_ci', CollationTest.columns_hash['string_ci_column'].collation @@ -18,6 +18,7 @@ class Mysql2CaseSensitivityTest < ActiveRecord::TestCase end def test_case_insensitive_comparison_for_ci_column + CollationTest.validates_uniqueness_of(:string_ci_column, :case_sensitive => false) CollationTest.create!(:string_ci_column => 'A') invalid = CollationTest.new(:string_ci_column => 'a') queries = assert_sql { invalid.save } @@ -26,10 +27,29 @@ class Mysql2CaseSensitivityTest < ActiveRecord::TestCase end def test_case_insensitive_comparison_for_cs_column + CollationTest.validates_uniqueness_of(:string_cs_column, :case_sensitive => false) CollationTest.create!(:string_cs_column => 'A') invalid = CollationTest.new(:string_cs_column => 'a') queries = assert_sql { invalid.save } cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/)} assert_match(/lower/i, cs_uniqueness_query) end + + def test_case_sensitive_comparison_for_ci_column + CollationTest.validates_uniqueness_of(:string_ci_column, :case_sensitive => true) + CollationTest.create!(:string_ci_column => 'A') + invalid = CollationTest.new(:string_ci_column => 'A') + queries = assert_sql { invalid.save } + ci_uniqueness_query = queries.detect { |q| q.match(/string_ci_column/) } + assert_match(/binary/i, ci_uniqueness_query) + end + + def test_case_sensitive_comparison_for_cs_column + CollationTest.validates_uniqueness_of(:string_cs_column, :case_sensitive => true) + CollationTest.create!(:string_cs_column => 'A') + invalid = CollationTest.new(:string_cs_column => 'A') + queries = assert_sql { invalid.save } + cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/) } + assert_no_match(/binary/i, cs_uniqueness_query) + end end diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index 9b7202c915..98febd2d75 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -97,6 +97,13 @@ class MysqlConnectionTest < ActiveRecord::TestCase @connection.execute "DROP TABLE `bar_baz`" end + if mysql_56? + def test_quote_time_usec + assert_equal "'1970-01-01 00:00:00.000000'", @connection.quote(Time.at(0)) + assert_equal "'1970-01-01 00:00:00.000000'", @connection.quote(Time.at(0).to_datetime) + end + end + private def run_without_connection diff --git a/activerecord/test/cases/adapters/postgresql/citext_test.rb b/activerecord/test/cases/adapters/postgresql/citext_test.rb new file mode 100644 index 0000000000..ebb9ff98b1 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/citext_test.rb @@ -0,0 +1,77 @@ +# encoding: utf-8 + +require 'cases/helper' +require 'active_record/base' +require 'active_record/connection_adapters/postgresql_adapter' + +class PostgresqlCitextTest < ActiveRecord::TestCase + class Citext < ActiveRecord::Base + self.table_name = 'citexts' + end + + def setup + @connection = ActiveRecord::Base.connection + + unless @connection.extension_enabled?('citext') + @connection.enable_extension 'citext' + @connection.commit_db_transaction + end + + @connection.reconnect! + + @connection.create_table('citexts') do |t| + t.citext 'cival' + end + @column = Citext.columns_hash['cival'] + end + + def teardown + @connection.execute 'DROP TABLE IF EXISTS citexts;' + @connection.execute 'DROP EXTENSION IF EXISTS citext CASCADE;' + end + + def test_citext_enabled + assert @connection.extension_enabled?('citext') + end + + def test_column_type + assert_equal :citext, @column.type + end + + def test_column_sql_type + assert_equal 'citext', @column.sql_type + end + + def test_change_table_supports_json + @connection.transaction do + @connection.change_table('citexts') do |t| + t.citext 'username' + end + Citext.reset_column_information + column = Citext.columns.find { |c| c.name == 'username' } + assert_equal :citext, column.type + + raise ActiveRecord::Rollback # reset the schema change + end + ensure + Citext.reset_column_information + end + + def test_write + x = Citext.new(cival: 'Some CI Text') + x.save! + citext = Citext.first + assert_equal "Some CI Text", citext.cival + + citext.cival = "Some NEW CI Text" + citext.save! + + assert_equal "Some NEW CI Text", citext.reload.cival + end + + def test_select_case_insensitive + @connection.execute "insert into citexts (cival) values('Cased Text')" + x = Citext.where(cival: 'cased text').first + assert_equal 'Cased Text', x.cival + end +end diff --git a/activerecord/test/cases/adapters/postgresql/enum_test.rb b/activerecord/test/cases/adapters/postgresql/enum_test.rb index bf8dbd6c3f..62f84caf91 100644 --- a/activerecord/test/cases/adapters/postgresql/enum_test.rb +++ b/activerecord/test/cases/adapters/postgresql/enum_test.rb @@ -23,6 +23,8 @@ class PostgresqlEnumTest < ActiveRecord::TestCase t.column :current_mood, :mood end end + # reload type map after creating the enum type + @connection.send(:reload_type_map) end def test_enum_mapping @@ -35,4 +37,28 @@ class PostgresqlEnumTest < ActiveRecord::TestCase assert_equal "happy", enum.reload.current_mood end + + def test_invalid_enum_update + @connection.execute "INSERT INTO postgresql_enums VALUES (1, 'sad');" + enum = PostgresqlEnum.first + enum.current_mood = "angry" + + assert_raise ActiveRecord::StatementInvalid do + enum.save + end + end + + def test_no_oid_warning + @connection.execute "INSERT INTO postgresql_enums VALUES (1, 'sad');" + stderr_output = capture(:stderr) { PostgresqlEnum.first } + + assert stderr_output.blank? + end + + def test_enum_type_cast + enum = PostgresqlEnum.new + enum.current_mood = :happy + + assert_equal "happy", enum.current_mood + end end diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb index 306bb09a0d..4e84335e9b 100644 --- a/activerecord/test/cases/adapters/postgresql/range_test.rb +++ b/activerecord/test/cases/adapters/postgresql/range_test.rb @@ -156,7 +156,7 @@ _SQL assert_equal 0.5..0.7, @first_range.float_range assert_equal 0.5...0.7, @second_range.float_range assert_equal 0.5...Float::INFINITY, @third_range.float_range - assert_equal -Float::INFINITY...Float::INFINITY, @fourth_range.float_range + assert_equal (-Float::INFINITY...Float::INFINITY), @fourth_range.float_range assert_nil @empty_range.float_range end diff --git a/activerecord/test/cases/associations/callbacks_test.rb b/activerecord/test/cases/associations/callbacks_test.rb index e555c52281..cf71bc1597 100644 --- a/activerecord/test/cases/associations/callbacks_test.rb +++ b/activerecord/test/cases/associations/callbacks_test.rb @@ -159,7 +159,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase activerecord.reload assert activerecord.developers_with_callbacks.size == 2 end - log_array = activerecord.developers_with_callbacks.collect {|d| ["before_removing#{d.id}","after_removing#{d.id}"]}.flatten.sort + log_array = activerecord.developers_with_callbacks.flat_map {|d| ["before_removing#{d.id}","after_removing#{d.id}"]}.sort assert activerecord.developers_with_callbacks.clear assert_equal log_array, activerecord.developers_log.sort end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 498a4e8144..5522a33b79 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -1194,4 +1194,13 @@ class EagerAssociationTest < ActiveRecord::TestCase authors(:david).essays.includes(:writer).any? end end + + test "preloading associations with string joins and order references" do + author = assert_queries(2) { + Author.includes(:posts).joins("LEFT JOIN posts ON posts.author_id = authors.id").order("posts.title DESC").first + } + assert_no_queries { + assert_equal 5, author.posts.size + } + end end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index a86fb15719..49d3301044 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -1242,6 +1242,16 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal orig_accounts, firm.accounts end + def test_replace_with_same_content + firm = Firm.first + firm.clients = [] + firm.save + + assert_queries(0, ignore_none: true) do + firm.clients = [] + end + end + def test_transactions_when_replacing_on_persisted good = Client.new(:name => "Good") bad = Client.new(:name => "Bad", :raise_on_save => true) diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 1a1e442df0..173081cb28 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -746,6 +746,19 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert "unknown attribute: hello", error.message end + def test_methods_override_in_multi_level_subclass + klass = Class.new(Developer) do + def name + "dev:#{read_attribute(:name)}" + end + end + + 2.times { klass = Class.new klass } + dev = klass.new(name: 'arthurnn') + dev.save! + assert_equal 'dev:arthurnn', dev.reload.name + end + def test_global_methods_are_overwritten klass = Class.new(ActiveRecord::Base) do self.table_name = 'computers' diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index d2f97df0fc..09892d50ba 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -64,10 +64,6 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase private - def base - ActiveRecord::Base - end - def assert_no_difference_when_adding_callbacks_twice_for(model, association_name) reflection = model.reflect_on_association(association_name) assert_no_difference "callbacks_for_model(#{model.name}).length" do @@ -76,9 +72,9 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase end def callbacks_for_model(model) - model.instance_variables.grep(/_callbacks$/).map do |ivar| + model.instance_variables.grep(/_callbacks$/).flat_map do |ivar| model.instance_variable_get(ivar) - end.flatten + end end end @@ -622,15 +618,15 @@ end class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase self.use_transactional_fixtures = false - def setup - super + setup do @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?") @ship = @pirate.create_ship(:name => 'Nights Dirty Lightning') end - def teardown + teardown do # We are running without transactional fixtures and need to cleanup. Bird.delete_all + Parrot.delete_all @ship.delete @pirate.delete end @@ -687,10 +683,23 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase end end + @ship.pirate.catchphrase = "Changed Catchphrase" + assert_raise(RuntimeError) { assert !@pirate.save } assert_not_nil @pirate.reload.ship end + def test_should_save_changed_has_one_changed_object_if_child_is_saved + @pirate.ship.name = "NewName" + assert @pirate.save + assert_equal "NewName", @pirate.ship.reload.name + end + + def test_should_not_save_changed_has_one_unchanged_object_if_child_is_saved + @pirate.ship.expects(:save).never + assert @pirate.save + end + # belongs_to def test_should_destroy_a_parent_association_as_part_of_the_save_transaction_if_it_was_marked_for_destroyal assert !@ship.pirate.marked_for_destruction? diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 8a0b0b9589..6acb342d0b 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -212,7 +212,7 @@ class BasicsTest < ActiveRecord::TestCase ) # For adapters which support microsecond resolution. - if current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter) + if current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter) || mysql_56? assert_equal 11, Topic.find(1).written_on.sec assert_equal 223300, Topic.find(1).written_on.usec assert_equal 9900, Topic.find(2).written_on.usec diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index 1b95708cb3..f8ebd7caee 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -222,4 +222,31 @@ class EnumTest < ActiveRecord::TestCase end end end + + test "validate uniqueness" do + klass = Class.new(ActiveRecord::Base) do + def self.name; 'Book'; end + enum status: [:proposed, :written] + validates_uniqueness_of :status + end + klass.delete_all + klass.create!(status: "proposed") + book = klass.new(status: "written") + assert book.valid? + book.status = "proposed" + assert_not book.valid? + end + + test "validate inclusion of value in array" do + klass = Class.new(ActiveRecord::Base) do + def self.name; 'Book'; end + enum status: [:proposed, :written] + validates_inclusion_of :status, in: ["written"] + end + klass.delete_all + invalid_book = klass.new(status: "proposed") + assert_not invalid_book.valid? + valid_book = klass.new(status: "written") + assert valid_book.valid? + end end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index b1eded6494..98b1dafdf0 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -58,15 +58,25 @@ class FinderTest < ActiveRecord::TestCase assert_equal false, Topic.exists?(45) assert_equal false, Topic.exists?(Topic.new) - begin - assert_equal false, Topic.exists?("foo") - rescue ActiveRecord::StatementInvalid - # PostgreSQL complains about string comparison with integer field - rescue Exception - flunk + assert_raise(NoMethodError) { Topic.exists?([1,2]) } + end + + def test_exists_fails_when_parameter_has_invalid_type + if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter) + assert_raises ActiveRecord::StatementInvalid do + Topic.exists?(("9"*53).to_i) # number that's bigger than int + end + else + assert_equal false, Topic.exists?(("9"*53).to_i) # number that's bigger than int end - assert_raise(NoMethodError) { Topic.exists?([1,2]) } + if current_adapter?(:PostgreSQLAdapter) + assert_raises ActiveRecord::StatementInvalid do + Topic.exists?("foo") + end + else + assert_equal false, Topic.exists?("foo") + end end def test_exists_does_not_select_columns_without_alias diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 3758224b0c..8a49dfbb44 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -41,6 +41,11 @@ def in_memory_db? ActiveRecord::Base.connection_pool.spec.config[:database] == ":memory:" end +def mysql_56? + current_adapter?(:Mysql2Adapter) && + ActiveRecord::Base.connection.send(:version).join(".") >= "5.6.0" +end + def supports_savepoints? ActiveRecord::Base.connection.supports_savepoints? end diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index b9f0624f76..046fe83e54 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -452,7 +452,7 @@ class PersistenceTest < ActiveRecord::TestCase def test_update_attribute_for_updated_at_on developer = Developer.find(1) - prev_month = Time.now.prev_month + prev_month = Time.now.prev_month.change(usec: 0) developer.update_attribute(:updated_at, prev_month) assert_equal prev_month, developer.updated_at @@ -523,7 +523,7 @@ class PersistenceTest < ActiveRecord::TestCase def test_update_column_should_not_modify_updated_at developer = Developer.find(1) - prev_month = Time.now.prev_month + prev_month = Time.now.prev_month.change(usec: 0) developer.update_column(:updated_at, prev_month) assert_equal prev_month, developer.updated_at @@ -620,7 +620,7 @@ class PersistenceTest < ActiveRecord::TestCase def test_update_columns_should_not_modify_updated_at developer = Developer.find(1) - prev_month = Time.now.prev_month + prev_month = Time.now.prev_month.change(usec: 0) developer.update_columns(updated_at: prev_month) assert_equal prev_month, developer.updated_at diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 21d05e4ac0..b74139d6f5 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -171,7 +171,6 @@ class RelationTest < ActiveRecord::TestCase assert_equal topics(:first).title, topics.first.title end - def test_finding_with_arel_order topics = Topic.order(Topic.arel_table[:id].asc) assert_equal 5, topics.to_a.size @@ -194,8 +193,33 @@ class RelationTest < ActiveRecord::TestCase assert_equal Topic.order(:id).to_sql, Topic.order(:id => :asc).to_sql end + def test_finding_with_desc_order_with_string + topics = Topic.order(id: "desc") + assert_equal 5, topics.to_a.size + assert_equal [topics(:fifth), topics(:fourth), topics(:third), topics(:second), topics(:first)], topics.to_a + end + + def test_finding_with_asc_order_with_string + topics = Topic.order(id: 'asc') + assert_equal 5, topics.to_a.size + assert_equal [topics(:first), topics(:second), topics(:third), topics(:fourth), topics(:fifth)], topics.to_a + end + + def test_support_upper_and_lower_case_directions + assert_includes Topic.order(id: "ASC").to_sql, "ASC" + assert_includes Topic.order(id: "asc").to_sql, "ASC" + assert_includes Topic.order(id: :ASC).to_sql, "ASC" + assert_includes Topic.order(id: :asc).to_sql, "ASC" + + assert_includes Topic.order(id: "DESC").to_sql, "DESC" + assert_includes Topic.order(id: "desc").to_sql, "DESC" + assert_includes Topic.order(id: :DESC).to_sql, "DESC" + assert_includes Topic.order(id: :desc).to_sql,"DESC" + end + def test_raising_exception_on_invalid_hash_params - assert_raise(ArgumentError) { Topic.order(:name, "id DESC", :id => :DeSc) } + e = assert_raise(ArgumentError) { Topic.order(:name, "id DESC", id: :asfsdf) } + assert_equal 'Direction "asfsdf" is invalid. Valid directions are: [:asc, :desc, :ASC, :DESC, "asc", "desc", "ASC", "DESC"]', e.message end def test_finding_last_with_arel_order diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index c085663efb..575eb34a9c 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -177,7 +177,7 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_schema_dumps_index_columns_in_right_order index_definition = standard_dump.split(/\n/).grep(/add_index.*companies/).first.strip - if current_adapter?(:MysqlAdapter) || current_adapter?(:Mysql2Adapter) || current_adapter?(:PostgreSQLAdapter) + if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) assert_equal 'add_index "companies", ["firm_id", "type", "rating"], name: "company_index", using: :btree', index_definition else assert_equal 'add_index "companies", ["firm_id", "type", "rating"], name: "company_index"', index_definition @@ -188,7 +188,7 @@ class SchemaDumperTest < ActiveRecord::TestCase index_definition = standard_dump.split(/\n/).grep(/add_index.*company_partial_index/).first.strip if current_adapter?(:PostgreSQLAdapter) assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)", using: :btree', index_definition - elsif current_adapter?(:MysqlAdapter) || current_adapter?(:Mysql2Adapter) + elsif current_adapter?(:MysqlAdapter, :Mysql2Adapter) assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", using: :btree', index_definition elsif current_adapter?(:SQLite3Adapter) && ActiveRecord::Base.connection.supports_partial_index? assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "rating > 10"', index_definition @@ -319,6 +319,13 @@ class SchemaDumperTest < ActiveRecord::TestCase end end + def test_schema_dump_includes_citext_shorthand_definition + output = standard_dump + if %r{create_table "postgresql_citext"} =~ output + assert_match %r[t.citext "text_citext"], output + end + end + def test_schema_dump_includes_ltrees_shorthand_definition output = standard_dump if %r{create_table "postgresql_ltrees"} =~ output diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index a86a188bcf..4fcbf4dbd2 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -1,7 +1,7 @@ ActiveRecord::Schema.define do %w(postgresql_tsvectors postgresql_hstores postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_bit_strings postgresql_uuids postgresql_ltrees - postgresql_oids postgresql_xml_data_type defaults geometrics postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent postgresql_json_data_type).each do |table_name| + postgresql_oids postgresql_xml_data_type defaults geometrics postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent postgresql_json_data_type postgresql_citext).each do |table_name| execute "DROP TABLE IF EXISTS #{quote_table_name table_name}" end @@ -99,6 +99,15 @@ _SQL _SQL end + if 't' == select_value("select 'citext'=ANY(select typname from pg_type)") + execute <<_SQL + CREATE TABLE postgresql_citext ( + id SERIAL PRIMARY KEY, + text_citext citext default ''::citext + ); +_SQL + end + if 't' == select_value("select 'json'=ANY(select typname from pg_type)") execute <<_SQL CREATE TABLE postgresql_json_data_type ( diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 5f2d6acbcb..b44e72a67c 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -674,7 +674,11 @@ ActiveRecord::Schema.define do t.string :title t.string :author_name t.string :author_email_address - t.datetime :written_on + if mysql_56? + t.datetime :written_on, limit: 6 + else + t.datetime :written_on + end t.time :bonus_time t.date :last_read # use VARCHAR2(4000) instead of CLOB datatype as CLOB data type has many limitations in diff --git a/activesupport/lib/active_support/gem_version.rb b/activesupport/lib/active_support/gem_version.rb new file mode 100644 index 0000000000..83a3bf7a5d --- /dev/null +++ b/activesupport/lib/active_support/gem_version.rb @@ -0,0 +1,15 @@ +module ActiveSupport + # Returns the version of the currently loaded ActiveSupport as a <tt>Gem::Version</tt> + def self.gem_version + Gem::Version.new VERSION::STRING + end + + module VERSION + MAJOR = 4 + MINOR = 2 + TINY = 0 + PRE = "alpha" + + STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") + end +end diff --git a/activesupport/lib/active_support/ordered_hash.rb b/activesupport/lib/active_support/ordered_hash.rb index 58a2ce2105..4680d5acb7 100644 --- a/activesupport/lib/active_support/ordered_hash.rb +++ b/activesupport/lib/active_support/ordered_hash.rb @@ -28,6 +28,10 @@ module ActiveSupport coder.represent_seq '!omap', map { |k,v| { k => v } } end + def select(*args, &block) + dup.tap { |hash| hash.select!(*args, &block) } + end + def reject(*args, &block) dup.tap { |hash| hash.reject!(*args, &block) } end diff --git a/activesupport/lib/active_support/testing/declarative.rb b/activesupport/lib/active_support/testing/declarative.rb index c349bb5fb1..e709e6edf5 100644 --- a/activesupport/lib/active_support/testing/declarative.rb +++ b/activesupport/lib/active_support/testing/declarative.rb @@ -34,7 +34,7 @@ module ActiveSupport # end def test(name, &block) test_name = "test_#{name.gsub(/\s+/,'_')}".to_sym - defined = instance_method(test_name) rescue false + defined = method_defined? test_name raise "#{test_name} is already defined in #{self}" if defined if block_given? define_method(test_name, &block) diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index eb785d46ce..38f0d268f4 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -282,7 +282,7 @@ module ActiveSupport # # Time.zone.now # => Fri, 31 Dec 1999 14:00:00 HST -10:00 # Time.zone.parse('22:30:00') # => Fri, 31 Dec 1999 22:30:00 HST -10:00 - def parse(str, now=now) + def parse(str, now=now()) parts = Date._parse(str, false) return if parts.empty? diff --git a/activesupport/lib/active_support/version.rb b/activesupport/lib/active_support/version.rb index 38f726ea34..fe03984546 100644 --- a/activesupport/lib/active_support/version.rb +++ b/activesupport/lib/active_support/version.rb @@ -1,11 +1,8 @@ +require_relative 'gem_version' + module ActiveSupport - # Returns the version of the currently loaded ActiveSupport as a Gem::Version + # Returns the version of the currently loaded ActiveSupport as a <tt>Gem::Version</tt> def self.version - Gem::Version.new "4.2.0.alpha" - end - - module VERSION #:nodoc: - MAJOR, MINOR, TINY, PRE = ActiveSupport.version.segments - STRING = ActiveSupport.version.to_s + gem_version end end diff --git a/activesupport/test/inflector_test_cases.rb b/activesupport/test/inflector_test_cases.rb index 4bd1b2e47c..dd03a61176 100644 --- a/activesupport/test/inflector_test_cases.rb +++ b/activesupport/test/inflector_test_cases.rb @@ -314,7 +314,7 @@ module InflectorTestCases 'child' => 'children', 'sex' => 'sexes', 'move' => 'moves', - 'cow' => 'kine', + 'cow' => 'kine', # Test inflections with different starting letters 'zombie' => 'zombies', 'genus' => 'genera' } diff --git a/activesupport/test/ordered_hash_test.rb b/activesupport/test/ordered_hash_test.rb index 0b54026c64..460a61613e 100644 --- a/activesupport/test/ordered_hash_test.rb +++ b/activesupport/test/ordered_hash_test.rb @@ -120,7 +120,9 @@ class OrderedHashTest < ActiveSupport::TestCase end def test_select - assert_equal @keys, @ordered_hash.select { true }.map(&:first) + new_ordered_hash = @ordered_hash.select { true } + assert_equal @keys, new_ordered_hash.map(&:first) + assert_instance_of ActiveSupport::OrderedHash, new_ordered_hash end def test_delete_if @@ -143,6 +145,7 @@ class OrderedHashTest < ActiveSupport::TestCase assert_equal copy, @ordered_hash assert !new_ordered_hash.keys.include?('pink') assert @ordered_hash.keys.include?('pink') + assert_instance_of ActiveSupport::OrderedHash, new_ordered_hash end def test_clear diff --git a/guides/source/4_1_release_notes.md b/guides/source/4_1_release_notes.md index a859553b1b..f368268a37 100644 --- a/guides/source/4_1_release_notes.md +++ b/guides/source/4_1_release_notes.md @@ -388,7 +388,7 @@ for detailed changes. * Removed deprecated `scope` use without passing a callable object. * Removed deprecated `transaction_joinable=` in favor of `begin_transaction` - with `d:joinable` option. + with a `:joinable` option. * Removed deprecated `decrement_open_transactions`. @@ -457,10 +457,10 @@ for detailed changes. ### Notable changes -* Default scopes are no longer overriden by chained conditions. +* Default scopes are no longer overridden by chained conditions. Before this change when you defined a `default_scope` in a model - it was overriden by chained conditions in the same field. Now it + it was overridden by chained conditions in the same field. Now it is merged like any other scope. [More Details](upgrading_ruby_on_rails.html#changes-on-default-scopes). * Added `ActiveRecord::Base.to_param` for convenient "pretty" URLs derived from @@ -543,15 +543,15 @@ for detailed changes. * Make `touch` fire the `after_commit` and `after_rollback` callbacks. ([Pull Request](https://github.com/rails/rails/pull/12031)) -* Enable partial indexes for `sqlite >= - 3.8.0`. ([Pull Request](https://github.com/rails/rails/pull/13350)) +* Enable partial indexes for `sqlite >= 3.8.0`. + ([Pull Request](https://github.com/rails/rails/pull/13350)) * Make `change_column_null` - revertable. ([Commit](https://github.com/rails/rails/commit/724509a9d5322ff502aefa90dd282ba33a281a96)) + revertible. ([Commit](https://github.com/rails/rails/commit/724509a9d5322ff502aefa90dd282ba33a281a96)) * Added a flag to disable schema dump after migration. This is set to `false` - by defualt in the production environment for new applications. ([Pull Request](https://github.com/rails/rails/pull/13948)) - + by default in the production environment for new applications. + ([Pull Request](https://github.com/rails/rails/pull/13948)) Active Model ------------ @@ -709,7 +709,8 @@ for detailed changes. responsibilities within a class. ([Commit](https://github.com/rails/rails/commit/1eee0ca6de975b42524105a59e0521d18b38ab81)) -* Added `Object#present_in` to simplify value whitelisting. ([Commit](https://github.com/rails/rails/commit/4edca106daacc5a159289eae255207d160f22396)) +* Added `Object#presence_in` to simplify value whitelisting. + ([Commit](https://github.com/rails/rails/commit/4edca106daacc5a159289eae255207d160f22396)) Credits diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md index 5b5f53c9be..1f9342ca25 100644 --- a/guides/source/action_controller_overview.md +++ b/guides/source/action_controller_overview.md @@ -381,16 +381,31 @@ You can also pass a `:domain` key and specify the domain name for the cookie: YourApp::Application.config.session_store :cookie_store, key: '_your_app_session', domain: ".example.com" ``` -Rails sets up (for the CookieStore) a secret key used for signing the session data. This can be changed in `config/initializers/secret_token.rb` +Rails sets up (for the CookieStore) a secret key used for signing the session data. This can be changed in `config/secrets.yml` ```ruby # Be sure to restart your server when you modify this file. -# Your secret key for verifying the integrity of signed cookies. +# Your secret key is used for verifying the integrity of signed cookies. # If you change this key, all old signed cookies will become invalid! + # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. -YourApp::Application.config.secret_key_base = '49d3f3de9ed86c74b94ad6bd0...' +# You can use `rake secret` to generate a secure secret key. + +# Make sure the secrets in this file are kept private +# if you're sharing your code publicly. + +development: + secret_key_base: a75d... + +test: + secret_key_base: 492f... + +# Do not keep production secrets in the repository, +# instead read values from the environment. +production: + secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> ``` NOTE: Changing the secret when using the `CookieStore` will invalidate all existing sessions. diff --git a/guides/source/action_view_overview.md b/guides/source/action_view_overview.md index 6a355a5177..74f95bfcfd 100644 --- a/guides/source/action_view_overview.md +++ b/guides/source/action_view_overview.md @@ -1550,7 +1550,7 @@ end Sanitizes a block of CSS code. -#### strip_links(html) +#### strip_links(html) Strips all link tags from text leaving just the link text. ```ruby @@ -1568,9 +1568,9 @@ strip_links('Blog: <a href="http://myblog.com/">Visit</a>.') # => Blog: Visit. ``` -#### strip_tags(html) +#### strip_tags(html) -Strips all HTML tags from the html, including comments. +Strips all HTML tags from the html, including comments. This uses the html-scanner tokenizer and so its HTML parsing ability is limited by that of html-scanner. ```ruby @@ -1585,6 +1585,17 @@ strip_tags("<b>Bold</b> no more! <a href='more.html'>See more</a>") NB: The output may still contain unescaped '<', '>', '&' characters and confuse browsers. +### CsrfHelper + +Returns meta tags "csrf-param" and "csrf-token" with the name of the cross-site +request forgery protection parameter and token, respectively. + +```html +<%= csrf_meta_tags %> +``` + +NOTE: Regular forms generate hidden fields so they do not use these tags. More +details can be found in the [Rails Security Guide](security.html#cross-site-request-forgery-csrf). Localized Views --------------- diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md index 2ad09f599b..5698dc0413 100644 --- a/guides/source/active_support_core_extensions.md +++ b/guides/source/active_support_core_extensions.md @@ -157,9 +157,9 @@ Active Support provides `duplicable?` to programmatically query an object about ```ruby "foo".duplicable? # => true -"".duplicable? # => true +"".duplicable? # => true 0.0.duplicable? # => false -false.duplicable? # => false +false.duplicable? # => false ``` By definition all objects are `duplicable?` except `nil`, `false`, `true`, symbols, numbers, class, and module objects. @@ -2719,11 +2719,14 @@ The method `transform_keys` accepts a block and returns a hash that has applied # => {"" => nil, "A" => :a, "1" => 1} ``` -The result in case of collision is undefined: +In case of key collision, one of the values will be chosen. The chosen value may not always be the same given the same hash: ```ruby {"a" => 1, a: 2}.transform_keys { |key| key.to_s.upcase } -# => {"A" => 2}, in my test, can't rely on this result though +# The result could either be +# => {"A"=>2} +# or +# => {"A"=>1} ``` This method may be useful for example to build specialized conversions. For instance `stringify_keys` and `symbolize_keys` use `transform_keys` to perform their key conversions: @@ -2758,11 +2761,14 @@ The method `stringify_keys` returns a hash that has a stringified version of the # => {"" => nil, "a" => :a, "1" => 1} ``` -The result in case of collision is undefined: +In case of key collision, one of the values will be chosen. The chosen value may not always be the same given the same hash: ```ruby {"a" => 1, a: 2}.stringify_keys -# => {"a" => 2}, in my test, can't rely on this result though +# The result could either be +# => {"a"=>2} +# or +# => {"a"=>1} ``` This method may be useful for example to easily accept both symbols and strings as options. For instance `ActionView::Helpers::FormHelper` defines: @@ -2799,11 +2805,14 @@ The method `symbolize_keys` returns a hash that has a symbolized version of the WARNING. Note in the previous example only one key was symbolized. -The result in case of collision is undefined: +In case of key collision, one of the values will be chosen. The chosen value may not always be the same given the same hash: ```ruby {"a" => 1, a: 2}.symbolize_keys -# => {:a=>2}, in my test, can't rely on this result though +# The result could either be +# => {:a=>2} +# or +# => {:a=>1} ``` This method may be useful for example to easily accept both symbols and strings as options. For instance `ActionController::UrlRewriter` defines diff --git a/guides/source/caching_with_rails.md b/guides/source/caching_with_rails.md index 0d45e5fb28..e898d75d1a 100644 --- a/guides/source/caching_with_rails.md +++ b/guides/source/caching_with_rails.md @@ -140,6 +140,26 @@ You can also combine the two schemes which is called "Russian Doll Caching": It's called "Russian Doll Caching" because it nests multiple fragments. The advantage is that if a single product is updated, all the other inner fragments can be reused when regenerating the outer fragment. +### Low-Level Caching + +Sometimes you need to cache a particular value or query result, instead of caching view fragments. Rails caching mechanism works great for storing __any__ kind of information. + +The most efficient way to implement low-level caching is using the `Rails.cache.fetch` method. This method does both reading and writing to the cache. When passed only a single argument, the key is fetched and value from the cache is returned. If a block is passed, the result of the block will be cached to the given key and the result is returned. + +Consider the following example. An application has a `Product` model with an instance method that looks up the product’s price on a competing website. The data returned by this method would be perfect for low-level caching: + +```ruby +class Product < ActiveRecord::Base + def competing_price + Rails.cache.fetch("#{cache_key}/competing_price", expires_in: 12.hours) do + Competitor::API.find_price(id) + end + end +end +``` + +NOTE: Notice that in this example we used `cache_key` method, so the resulting cache-key will be something like `products/233-20140225082222765838000/competing_price`. `cache_key` generates a string based on the model’s `id` and `updated_at` attributes. This is a common convention and has the benefit of invalidating the cache whenever the product is updated. In general, when you use low-level caching for instance level information, you need to generate a cache key. + ### SQL Caching Query caching is a Rails feature that caches the result set returned by each query so that if Rails encounters the same query again for that request, it will use the cached result set as opposed to running the query against the database again. diff --git a/guides/source/configuring.md b/guides/source/configuring.md index 7b72e27b96..2cece90294 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -118,7 +118,7 @@ numbers. New applications filter out passwords by adding the following `config.f * `config.reload_classes_only_on_change` enables or disables reloading of classes only when tracked files change. By default tracks everything on autoload paths and is set to true. If `config.cache_classes` is true, this option is ignored. -* `config.secret_key_base` used for specifying a key which allows sessions for the application to be verified against a known secure key to prevent tampering. Applications get `config.secret_key_base` initialized to a random key in `config/initializers/secret_token.rb`. +* `secrets.secret_key_base` is used for specifying a key which allows sessions for the application to be verified against a known secure key to prevent tampering. Applications get `secrets.secret_key_base` initialized to a random key present in `config/secrets.yml`. * `config.serve_static_assets` configures Rails itself to serve static assets. Defaults to true, but in the production environment is turned off as the server software (e.g. Nginx or Apache) used to run the application should serve static assets instead. Unlike the default setting set this to true when running (absolutely not recommended!) or testing your app in production mode using WEBrick. Otherwise you won't be able use page caching and requests for files that exist regularly under the public directory will anyway hit your Rails app. @@ -274,7 +274,7 @@ All these configuration options are delegated to the `I18n` library. * `config.active_record.pluralize_table_names` specifies whether Rails will look for singular or plural table names in the database. If set to true (the default), then the Customer class will use the `customers` table. If set to false, then the Customer class will use the `customer` table. -* `config.active_record.default_timezone` determines whether to use `Time.local` (if set to `:local`) or `Time.utc` (if set to `:utc`) when pulling dates and times from the database. The default is `:utc` for Rails, although Active Record defaults to `:local` when used outside of Rails. +* `config.active_record.default_timezone` determines whether to use `Time.local` (if set to `:local`) or `Time.utc` (if set to `:utc`) when pulling dates and times from the database. The default is `:utc`. * `config.active_record.schema_format` controls the format for dumping the database schema to a file. The options are `:ruby` (the default) for a database-independent version that depends on migrations, or `:sql` for a set of (potentially database-dependent) SQL statements. @@ -939,4 +939,4 @@ ActiveRecord::ConnectionTimeoutError - could not obtain a database connection wi If you get the above error, you might want to increase the size of connection pool by incrementing the `pool` option in `database.yml` -NOTE. As Rails is multi-threaded by default, there could be a chance that several threads may be accessing multiple connections simultaneously. So depending on your current request load, you could very well have multiple threads contending for a limited amount of connections. +NOTE. If you are running in a multi-threaded environment, there could be a chance that several threads may be accessing multiple connections simultaneously. So depending on your current request load, you could very well have multiple threads contending for a limited amount of connections. diff --git a/guides/source/documents.yaml b/guides/source/documents.yaml index e4653b47fc..a160c462b2 100644 --- a/guides/source/documents.yaml +++ b/guides/source/documents.yaml @@ -167,7 +167,6 @@ - name: Ruby on Rails 4.1 Release Notes url: 4_1_release_notes.html - work_in_progress: true description: Release notes for Rails 4.1. - name: Ruby on Rails 4.0 Release Notes diff --git a/guides/source/i18n.md b/guides/source/i18n.md index d72717fa3b..bef738b75b 100644 --- a/guides/source/i18n.md +++ b/guides/source/i18n.md @@ -137,7 +137,7 @@ If you want to translate your Rails application to a **single language other tha However, you would probably like to **provide support for more locales** in your application. In such case, you need to set and pass the locale between requests. -WARNING: You may be tempted to store the chosen locale in a _session_ or a <em>cookie</em>, however **do not do this**. The locale should be transparent and a part of the URL. This way you won't break people's basic assumptions about the web itself: if you send a URL to a friend, they should see the same page and content as you. A fancy word for this would be that you're being [<em>RESTful</em>](http://en.wikipedia.org/wiki/Representational_State_Transfer). Read more about the RESTful approach in [Stefan Tilkov's articles](http://www.infoq.com/articles/rest-introduction). Sometimes there are exceptions to this rule and those are discussed below. +WARNING: You may be tempted to store the chosen locale in a _session_ or a <em>cookie</em>. However, **do not do this**. The locale should be transparent and a part of the URL. This way you won't break people's basic assumptions about the web itself: if you send a URL to a friend, they should see the same page and content as you. A fancy word for this would be that you're being [<em>RESTful</em>](http://en.wikipedia.org/wiki/Representational_State_Transfer). Read more about the RESTful approach in [Stefan Tilkov's articles](http://www.infoq.com/articles/rest-introduction). Sometimes there are exceptions to this rule and those are discussed below. The _setting part_ is easy. You can set the locale in a `before_action` in the `ApplicationController` like this: diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md index 66ed6f2e08..bd33c5a146 100644 --- a/guides/source/layouts_and_rendering.md +++ b/guides/source/layouts_and_rendering.md @@ -304,10 +304,13 @@ type, by using the `:body` option to `render`: render body: "raw" ``` -TIP: This option should be used only if you explicitly want the content type to -be unset. Using `:plain` or `:html` might be more appropriate in most of the +TIP: This option should be used only if you don't care about the content type of +the response. Using `:plain` or `:html` might be more appropriate in most of the time. +NOTE: Unless overriden, your response returned from this render option will be +`text/html`, as that is the default content type of Action Dispatch response. + #### Options for `render` Calls to the `render` method generally accept four options: diff --git a/guides/source/security.md b/guides/source/security.md index ece431dae7..a40c99cbfd 100644 --- a/guides/source/security.md +++ b/guides/source/security.md @@ -95,9 +95,16 @@ Rails 2 introduced a new default session storage, CookieStore. CookieStore saves That means the security of this storage depends on this secret (and on the digest algorithm, which defaults to SHA1, for compatibility). So _don't use a trivial secret, i.e. a word from a dictionary, or one which is shorter than 30 characters_. -`config.secret_key_base` is used for specifying a key which allows sessions for the application to be verified against a known secure key to prevent tampering. Applications get `config.secret_key_base` initialized to a random key in `config/initializers/secret_token.rb`, e.g.: +`secrets.secret_key_base` is used for specifying a key which allows sessions for the application to be verified against a known secure key to prevent tampering. Applications get `secrets.secret_key_base` initialized to a random key present in `config/secrets.yml`, e.g.: - YourApp::Application.config.secret_key_base = '49d3f3de9ed86c74b94ad6bd0...' + development: + secret_key_base: a75d... + + test: + secret_key_base: 492f... + + production: + secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> Older versions of Rails use CookieStore, which uses `secret_token` instead of `secret_key_base` that is used by EncryptedCookieStore. Read the upgrade documentation for more information. @@ -1005,7 +1012,7 @@ Used to control which sites are allowed to bypass same origin policies and send Environmental Security ---------------------- -It is beyond the scope of this guide to inform you on how to secure your application code and environments. However, please secure your database configuration, e.g. `config/database.yml`, and your server-side secret, e.g. stored in `config/initializers/secret_token.rb`. You may want to further restrict access, using environment-specific versions of these files and any others that may contain sensitive information. +It is beyond the scope of this guide to inform you on how to secure your application code and environments. However, please secure your database configuration, e.g. `config/database.yml`, and your server-side secret, e.g. stored in `config/secrets.yml`. You may want to further restrict access, using environment-specific versions of these files and any others that may contain sensitive information. Additional Resources -------------------- diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index da124e21a4..7467648d49 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -111,6 +111,26 @@ in your application, you can add an initializer file with the following content: This would transparently migrate your existing `Marshal`-serialized cookies into the new `JSON`-based format. +### Flash structure changes + +Flash message keys are +[normalized to strings](https://github.com/rails/rails/commit/a668beffd64106a1e1fedb71cc25eaaa11baf0c1). They +can still be accessed using either symbols or strings. Lopping through the flash +will always yield string keys: + +```ruby +flash["string"] = "a string" +flash[:symbol] = "a symbol" + +# Rails < 4.1 +flash.keys # => ["string", :symbol] + +# Rails >= 4.1 +flash.keys # => ["string", "symbol"] +``` + +Make sure you are comparing Flash message keys against strings. + ### Changes in JSON handling There are a few major changes related to JSON handling in Rails 4.1. diff --git a/guides/source/working_with_javascript_in_rails.md b/guides/source/working_with_javascript_in_rails.md index a8695ec034..aba3c9ed61 100644 --- a/guides/source/working_with_javascript_in_rails.md +++ b/guides/source/working_with_javascript_in_rails.md @@ -111,7 +111,9 @@ paintIt = (element, backgroundColor, textColor) -> element.style.color = textColor $ -> - $("a[data-background-color]").click -> + $("a[data-background-color]").click (e) -> + e.preventDefault() + backgroundColor = $(this).data("background-color") textColor = $(this).data("text-color") paintIt(this, backgroundColor, textColor) diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 18f2546c73..35b3a379ae 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1 +1,35 @@ +* Removed unnecessary `rails application` command. + + *Arun Agrawal* + +* Make the `rails:template` rake task load the application's initializers. + + Fixes #12133. + + *Robin Dupret* + +* Introduce `Rails.gem_version` as a convenience method to return + `Gem::Version.new(Rails.version)`, suggesting a more reliable way to perform + version comparison. + + Example: + + Rails.version #=> "4.1.2" + Rails.gem_version #=> #<Gem::Version "4.1.2"> + + Rails.version > "4.1.10" #=> false + Rails.gem_version > Gem::Version.new("4.1.10") #=> true + Gem::Requirement.new("~> 4.1.2") =~ Rails.gem_version #=> true + + *Prem Sichanugrist* + +* Avoid namespacing routes inside engines. + + Mountable engines are namespaced by default so the generated routes + were too while they should not. + + Fixes #14079. + + *Yves Senn*, *Carlos Antonio da Silva*, *Robin Dupret* + Please check [4-1-stable](https://github.com/rails/rails/blob/4-1-stable/railties/CHANGELOG.md) for previous changes. diff --git a/railties/lib/rails.rb b/railties/lib/rails.rb index be7570a5ba..ecd8c22dd8 100644 --- a/railties/lib/rails.rb +++ b/railties/lib/rails.rb @@ -80,10 +80,6 @@ module Rails groups end - def version - VERSION::STRING - end - def public_path application && Pathname.new(application.paths["public"].first) end diff --git a/railties/lib/rails/commands/commands_tasks.rb b/railties/lib/rails/commands/commands_tasks.rb index de60423784..f061c2bf74 100644 --- a/railties/lib/rails/commands/commands_tasks.rb +++ b/railties/lib/rails/commands/commands_tasks.rb @@ -20,7 +20,6 @@ The most common rails commands are: new application called MyApp in "./my_app" In addition to those, there are: - application Generate the Rails application code destroy Undo code generated with "generate" (short-cut alias: "d") plugin new Generates skeleton for developing a Rails plugin runner Run a piece of code in the application environment (short-cut alias: "r") @@ -28,7 +27,7 @@ In addition to those, there are: All commands can be run with -h (or --help) for more information. EOT - COMMAND_WHITELIST = %(plugin generate destroy console server dbconsole application runner new version help) + COMMAND_WHITELIST = %(plugin generate destroy console server dbconsole runner new version help) def initialize(argv) @argv = argv @@ -87,10 +86,6 @@ EOT Rails::DBConsole.start end - def application - require_command!("application") - end - def runner require_command!("runner") end diff --git a/railties/lib/rails/commands/plugin.rb b/railties/lib/rails/commands/plugin.rb index f7a0b99005..95bbdd4cdf 100644 --- a/railties/lib/rails/commands/plugin.rb +++ b/railties/lib/rails/commands/plugin.rb @@ -11,7 +11,7 @@ else end if File.exist?(railsrc) extra_args_string = File.read(railsrc) - extra_args = extra_args_string.split(/\n+/).map {|l| l.split}.flatten + extra_args = extra_args_string.split(/\n+/).flat_map {|l| l.split} puts "Using #{extra_args.join(" ")} from #{railsrc}" ARGV.insert(1, *extra_args) end diff --git a/railties/lib/rails/gem_version.rb b/railties/lib/rails/gem_version.rb new file mode 100644 index 0000000000..c7397c4f15 --- /dev/null +++ b/railties/lib/rails/gem_version.rb @@ -0,0 +1,15 @@ +module Rails + # Returns the version of the currently loaded Rails as a <tt>Gem::Version</tt> + def self.gem_version + Gem::Version.new VERSION::STRING + end + + module VERSION + MAJOR = 4 + MINOR = 2 + TINY = 0 + PRE = "alpha" + + STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") + end +end diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb index afdbf5c241..625f031c94 100644 --- a/railties/lib/rails/generators/actions.rb +++ b/railties/lib/rails/generators/actions.rb @@ -188,7 +188,7 @@ module Rails # generate(:authenticated, "user session") def generate(what, *args) log :generate, what - argument = args.map {|arg| arg.to_s }.flatten.join(" ") + argument = args.flat_map {|arg| arg.to_s }.join(" ") in_root { run_ruby_script("bin/rails generate #{what} #{argument}", verbose: false) } end diff --git a/railties/lib/rails/generators/erb.rb b/railties/lib/rails/generators/erb.rb index cfd77097d5..0755ac335c 100644 --- a/railties/lib/rails/generators/erb.rb +++ b/railties/lib/rails/generators/erb.rb @@ -6,7 +6,7 @@ module Erb # :nodoc: protected def formats - format + [format] end def format @@ -17,7 +17,7 @@ module Erb # :nodoc: :erb end - def filename_with_extensions(name, format) + def filename_with_extensions(name, format = self.format) [name, format, handler].compact.join(".") end end diff --git a/railties/lib/rails/generators/erb/controller/controller_generator.rb b/railties/lib/rails/generators/erb/controller/controller_generator.rb index e62aece7c5..94c1b835d1 100644 --- a/railties/lib/rails/generators/erb/controller/controller_generator.rb +++ b/railties/lib/rails/generators/erb/controller/controller_generator.rb @@ -11,7 +11,7 @@ module Erb # :nodoc: actions.each do |action| @action = action - Array(formats).each do |format| + formats.each do |format| @path = File.join(base_path, filename_with_extensions(action, format)) template filename_with_extensions(:view, format), @path end diff --git a/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb index b219f459ac..c94829a0ae 100644 --- a/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb +++ b/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb @@ -14,7 +14,7 @@ module Erb # :nodoc: def copy_view_files available_views.each do |view| - Array(formats).each do |format| + formats.each do |format| filename = filename_with_extensions(view, format) template filename, File.join("app/views", controller_file_path, filename) end diff --git a/railties/lib/rails/generators/rails/controller/controller_generator.rb b/railties/lib/rails/generators/rails/controller/controller_generator.rb index 33a0d81bf6..7588a558e7 100644 --- a/railties/lib/rails/generators/rails/controller/controller_generator.rb +++ b/railties/lib/rails/generators/rails/controller/controller_generator.rb @@ -27,11 +27,11 @@ module Rails # end # end def generate_routing_code(action) - depth = class_path.length + depth = regular_class_path.length # Create 'namespace' ladder # namespace :foo do # namespace :bar do - namespace_ladder = class_path.each_with_index.map do |ns, i| + namespace_ladder = regular_class_path.each_with_index.map do |ns, i| indent("namespace :#{ns} do\n", i * 2) end.join diff --git a/railties/lib/rails/paths.rb b/railties/lib/rails/paths.rb index 117bb37487..3eb66c07af 100644 --- a/railties/lib/rails/paths.rb +++ b/railties/lib/rails/paths.rb @@ -101,7 +101,7 @@ module Rails def filter_by(&block) all_paths.find_all(&block).flat_map { |path| paths = path.existent - paths - path.children.map { |p| yield(p) ? [] : p.existent }.flatten + paths - path.children.flat_map { |p| yield(p) ? [] : p.existent } }.uniq end end diff --git a/railties/lib/rails/source_annotation_extractor.rb b/railties/lib/rails/source_annotation_extractor.rb index 3cf6a005ea..83e28090f8 100644 --- a/railties/lib/rails/source_annotation_extractor.rb +++ b/railties/lib/rails/source_annotation_extractor.rb @@ -115,7 +115,7 @@ class SourceAnnotationExtractor # Prints the mapping from filenames to annotations in +results+ ordered by filename. # The +options+ hash is passed to each annotation's +to_s+. def display(results, options={}) - options[:indent] = results.map { |f, a| a.map(&:line) }.flatten.max.to_s.size + options[:indent] = results.flat_map { |f, a| a.map(&:line) }.max.to_s.size results.keys.sort.each do |file| puts "#{file}:" results[file].each do |note| diff --git a/railties/lib/rails/tasks/framework.rake b/railties/lib/rails/tasks/framework.rake index e669315934..3c8f8c6b87 100644 --- a/railties/lib/rails/tasks/framework.rake +++ b/railties/lib/rails/tasks/framework.rake @@ -3,7 +3,7 @@ namespace :rails do task update: [ "update:configs", "update:bin" ] desc "Applies the template supplied by LOCATION=(/path/to/template) or URL" - task :template do + task template: :environment do template = ENV["LOCATION"] raise "No LOCATION value given. Please set LOCATION either as path to a file or a URL" if template.blank? template = File.expand_path(template) if template !~ %r{\A[A-Za-z][A-Za-z0-9+\-\.]*://} diff --git a/railties/lib/rails/version.rb b/railties/lib/rails/version.rb index 79313c936a..df351c4238 100644 --- a/railties/lib/rails/version.rb +++ b/railties/lib/rails/version.rb @@ -1,10 +1,8 @@ -module Rails - module VERSION - MAJOR = 4 - MINOR = 2 - TINY = 0 - PRE = "alpha" +require_relative 'gem_version' - STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") +module Rails + # Returns the version of the currently loaded Rails as a string. + def self.version + VERSION::STRING end end diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb index 317e73245c..e8c8de9f73 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -271,5 +271,16 @@ module ApplicationTests end end end + + def test_template_load_initializers + app_file "config/initializers/dummy.rb", "puts 'Hello, World!'" + app_file "template.rb", "" + + output = Dir.chdir(app_path) do + `bundle exec rake rails:template LOCATION=template.rb` + end + + assert_match(/Hello, World!/, output) + end end end diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb index 932cd75bcb..b2fc3a2a4f 100644 --- a/railties/test/generators/plugin_generator_test.rb +++ b/railties/test/generators/plugin_generator_test.rb @@ -355,6 +355,18 @@ class PluginGeneratorTest < Rails::Generators::TestCase FileUtils.rm gemfile_path end + def test_generating_controller_inside_mountable_engine + run_generator [destination_root, "--mountable"] + + capture(:stdout) do + `#{destination_root}/bin/rails g controller admin/dashboard foo` + end + + assert_file "config/routes.rb" do |contents| + assert_match(/namespace :admin/, contents) + assert_no_match(/namespace :bukkit/, contents) + end + end protected def action(*args, &block) diff --git a/railties/test/version_test.rb b/railties/test/version_test.rb new file mode 100644 index 0000000000..f270d8f0c9 --- /dev/null +++ b/railties/test/version_test.rb @@ -0,0 +1,12 @@ +require 'abstract_unit' + +class VersionTest < ActiveSupport::TestCase + def test_rails_version_returns_a_string + assert Rails.version.is_a? String + end + + def test_rails_gem_version_returns_a_correct_gem_version_object + assert Rails.gem_version.is_a? Gem::Version + assert_equal Rails.version, Rails.gem_version.to_s + end +end diff --git a/version.rb b/version.rb index 79313c936a..c7397c4f15 100644 --- a/version.rb +++ b/version.rb @@ -1,4 +1,9 @@ module Rails + # Returns the version of the currently loaded Rails as a <tt>Gem::Version</tt> + def self.gem_version + Gem::Version.new VERSION::STRING + end + module VERSION MAJOR = 4 MINOR = 2 |