diff options
31 files changed, 218 insertions, 127 deletions
@@ -5,6 +5,9 @@ gemspec # We need a newish Rake since Active Job sets its test tasks' descriptions. gem 'rake', '>= 10.3' +# Active Job depends on the URI::GID::MissingModelIDError, which isn't released yet. +gem 'globalid', github: 'rails/globalid' + # This needs to be with require false as it is # loaded after loading the test library to # ensure correct loading order diff --git a/Gemfile.lock b/Gemfile.lock index 2140881170..960a77af92 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -26,6 +26,13 @@ GIT arel (7.0.0.alpha) GIT + remote: git://github.com/rails/globalid.git + revision: 4df66fb9e9f0c832d29119aa8bc30be55a614b71 + specs: + globalid (0.3.5) + activesupport (>= 4.1.0) + +GIT remote: git://github.com/rails/jquery-rails.git revision: 272abdd319bb3381b23182b928b25320590096b0 branch: master @@ -138,8 +145,6 @@ GEM delayed_job (>= 3.0, < 4.1) erubis (2.7.0) execjs (2.3.0) - globalid (0.3.3) - activesupport (>= 4.1.0) hitimes (1.2.2) hitimes (1.2.2-x86-mingw32) i18n (0.7.0) @@ -272,6 +277,7 @@ DEPENDENCIES dalli (>= 2.2.1) delayed_job delayed_job_active_record + globalid! jquery-rails! json kindlerb (= 0.1.1) @@ -308,4 +314,4 @@ DEPENDENCIES w3c_validators BUNDLED WITH - 1.10.4 + 1.10.5 diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index cb5e7516fb..46856ffc5d 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,13 @@ +* Add ability to filter parameters based on parent keys. + + # matches {credit_card: {code: "xxxx"}} + # doesn't match {file: { code: "xxxx"}} + config.filter_parameters += [ "credit_card.code" ] + + See #13897. + + *Guillaume Malette* + * Deprecate passing first parameter as `Hash` and default status code for `head` method. *Mehmet Emin İNAÇ* diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 96f161fb09..5489a0fde0 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -65,14 +65,6 @@ module ActionController @method = @request_method = nil @fullpath = @ip = @remote_ip = @protocol = nil @env['action_dispatch.request.query_parameters'] = {} - @set_cookies ||= {} - @set_cookies.update(Hash[cookie_jar.instance_variable_get("@set_cookies").map{ |k,o| [k,o[:value]] }]) - deleted_cookies = cookie_jar.instance_variable_get("@delete_cookies") - @set_cookies.reject!{ |k,v| deleted_cookies.include?(k) } - cookie_jar.update(rack_cookies) - cookie_jar.update(cookies) - cookie_jar.update(@set_cookies) - cookie_jar.recycle! end private @@ -83,17 +75,9 @@ module ActionController end class TestResponse < ActionDispatch::TestResponse - def recycle! - initialize - end end class LiveTestResponse < Live::Response - def recycle! - @body = nil - initialize - end - def body @body ||= super end @@ -317,7 +301,9 @@ module ActionController # Note that the request method is not verified. The different methods are # available to make the tests more expressive. def get(action, *args) - process_with_kwargs("GET", action, *args) + res = process_with_kwargs("GET", action, *args) + cookies.update res.cookies + res end # Simulate a POST request with the given parameters and set/volley the response. @@ -451,8 +437,13 @@ module ActionController @controller.extend(Testing::Functional) end + self.cookies.update @request.cookies + @request.env['HTTP_COOKIE'] = cookies.to_header + @request.env['action_dispatch.cookies'] = nil + @request.recycle! - @response.recycle! + @response = build_response @response_klass + @response.request = @request @controller.recycle! @request.env['REQUEST_METHOD'] = http_method @@ -479,9 +470,12 @@ module ActionController @controller.recycle! @controller.process(action) + @request.env.delete 'HTTP_COOKIE' + if cookies = @request.env['action_dispatch.cookies'] unless @response.committed? cookies.write(@response) + self.cookies.update(cookies.instance_variable_get(:@cookies)) end end @response.prepare! @@ -503,11 +497,11 @@ module ActionController def setup_controller_request_and_response @controller = nil unless defined? @controller - response_klass = TestResponse + @response_klass = TestResponse if klass = self.class.controller_class if klass < ActionController::Live - response_klass = LiveTestResponse + @response_klass = LiveTestResponse end unless @controller begin @@ -519,7 +513,8 @@ module ActionController end @request = build_request - @response = build_response response_klass + @request.env["rack.request.cookie_hash"] = {}.with_indifferent_access + @response = build_response @response_klass @response.request = @request if @controller diff --git a/actionpack/lib/action_dispatch/http/parameter_filter.rb b/actionpack/lib/action_dispatch/http/parameter_filter.rb index df4b073a17..6e058b829e 100644 --- a/actionpack/lib/action_dispatch/http/parameter_filter.rb +++ b/actionpack/lib/action_dispatch/http/parameter_filter.rb @@ -30,36 +30,46 @@ module ActionDispatch when Regexp regexps << item else - strings << item.to_s + strings << Regexp.escape(item.to_s) end end + deep_regexps, regexps = regexps.partition { |r| r.to_s.include?("\\.") } + deep_strings, strings = strings.partition { |s| s.include?("\\.") } + regexps << Regexp.new(strings.join('|'), true) unless strings.empty? - new regexps, blocks + deep_regexps << Regexp.new(deep_strings.join('|'), true) unless deep_strings.empty? + + new regexps, deep_regexps, blocks end - attr_reader :regexps, :blocks + attr_reader :regexps, :deep_regexps, :blocks - def initialize(regexps, blocks) + def initialize(regexps, deep_regexps, blocks) @regexps = regexps + @deep_regexps = deep_regexps.any? ? deep_regexps : nil @blocks = blocks end - def call(original_params) + def call(original_params, parents = []) filtered_params = {} original_params.each do |key, value| + parents.push(key) if deep_regexps if regexps.any? { |r| key =~ r } value = FILTERED + elsif deep_regexps && (joined = parents.join('.')) && deep_regexps.any? { |r| joined =~ r } + value = FILTERED elsif value.is_a?(Hash) - value = call(value) + value = call(value, parents) elsif value.is_a?(Array) - value = value.map { |v| v.is_a?(Hash) ? call(v) : v } + value = value.map { |v| v.is_a?(Hash) ? call(v, parents) : v } elsif blocks.any? key = key.dup if key.duplicable? value = value.dup if value.duplicable? blocks.each { |b| b.call(key, value) } end + parents.pop if deep_regexps filtered_params[key] = value end diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index dd1f140051..07d97bd6bd 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -8,7 +8,7 @@ require 'active_support/json' module ActionDispatch class Request < Rack::Request def cookie_jar - env['action_dispatch.cookies'] ||= Cookies::CookieJar.build(self) + env['action_dispatch.cookies'] ||= Cookies::CookieJar.build(env, host, ssl?, cookies) end end @@ -228,16 +228,12 @@ module ActionDispatch } end - def self.build(request) - env = request.env + def self.build(env, host, secure, cookies) key_generator = env[GENERATOR_KEY] options = options_for_env env - host = request.host - secure = request.ssl? - new(key_generator, host, secure, options).tap do |hash| - hash.update(request.cookies) + hash.update(cookies) end end @@ -283,6 +279,10 @@ module ActionDispatch self end + def to_header + @cookies.map { |k,v| "#{k}=#{v}" }.join ';' + end + def handle_options(options) #:nodoc: options[:path] ||= "/" diff --git a/actionpack/lib/action_dispatch/testing/test_process.rb b/actionpack/lib/action_dispatch/testing/test_process.rb index 415ef80cd2..494644cd46 100644 --- a/actionpack/lib/action_dispatch/testing/test_process.rb +++ b/actionpack/lib/action_dispatch/testing/test_process.rb @@ -19,7 +19,7 @@ module ActionDispatch end def cookies - @request.cookie_jar + @cookie_jar ||= Cookies::CookieJar.build(@request.env, @request.host, @request.ssl?, @request.cookies) end def redirect_to_url diff --git a/actionpack/lib/action_dispatch/testing/test_request.rb b/actionpack/lib/action_dispatch/testing/test_request.rb index 4b9a088265..b40da46703 100644 --- a/actionpack/lib/action_dispatch/testing/test_request.rb +++ b/actionpack/lib/action_dispatch/testing/test_request.rb @@ -4,15 +4,12 @@ require 'rack/utils' module ActionDispatch class TestRequest < Request DEFAULT_ENV = Rack::MockRequest.env_for('/', - 'HTTP_HOST' => 'test.host', - 'REMOTE_ADDR' => '0.0.0.0', - 'HTTP_USER_AGENT' => 'Rails Testing' + 'HTTP_HOST' => 'test.host', + 'REMOTE_ADDR' => '0.0.0.0', + 'HTTP_USER_AGENT' => 'Rails Testing', + "rack.request.cookie_hash" => {}.with_indifferent_access ) - def self.new(env = {}) - super - end - def initialize(env = {}) env = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application super(default_env.merge(env)) @@ -63,12 +60,6 @@ module ActionDispatch @env['HTTP_ACCEPT'] = Array(mime_types).collect(&:to_s).join(",") end - alias :rack_cookies :cookies - - def cookies - @cookies ||= {}.with_indifferent_access - end - private def default_env diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index f21dd87400..4f5af85fea 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -381,6 +381,7 @@ class AutomaticCollectionCacheTest < ActionController::TestCase @controller.perform_caching = true @controller.partial_rendered_times = 0 @controller.cache_store = ActiveSupport::Cache::MemoryStore.new + ActionView::PartialRenderer.collection_cache = @controller.cache_store end def test_collection_fetches_cached_views diff --git a/actionpack/test/controller/mime/respond_to_test.rb b/actionpack/test/controller/mime/respond_to_test.rb index 8591bdb4d9..71660c87a2 100644 --- a/actionpack/test/controller/mime/respond_to_test.rb +++ b/actionpack/test/controller/mime/respond_to_test.rb @@ -4,6 +4,13 @@ require "active_support/log_subscriber/test_helper" class RespondToController < ActionController::Base layout :set_layout + before_action { + case params[:v] + when String then request.variant = params[:v].to_sym + when Array then request.variant = params[:v].map(&:to_sym) + end + } + def html_xml_or_rss respond_to do |type| type.html { render :text => "HTML" } @@ -612,8 +619,7 @@ class RespondToControllerTest < ActionController::TestCase logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new old_logger, ActionController::Base.logger = ActionController::Base.logger, logger - @request.variant = :invalid - get :variant_with_implicit_rendering + get :variant_with_implicit_rendering, params: { v: :invalid } assert_response :no_content assert_equal 1, logger.logged(:info).select{ |s| s =~ /No template found/ }.size, "Implicit head :no_content not logged" ensure @@ -626,28 +632,24 @@ class RespondToControllerTest < ActionController::TestCase end def test_variant_with_implicit_rendering - @request.variant = :implicit - get :variant_with_implicit_rendering + get :variant_with_implicit_rendering, params: { v: :implicit } assert_response :no_content end def test_variant_with_implicit_template_rendering - @request.variant = :mobile - get :variant_with_implicit_rendering + get :variant_with_implicit_rendering, params: { v: :mobile } assert_equal "text/html", @response.content_type assert_equal "mobile", @response.body end def test_variant_with_format_and_custom_render - @request.variant = :phone - get :variant_with_format_and_custom_render + get :variant_with_format_and_custom_render, params: { v: :phone } assert_equal "text/html", @response.content_type assert_equal "mobile", @response.body end def test_multiple_variants_for_format - @request.variant = :tablet - get :multiple_variants_for_format + get :multiple_variants_for_format, params: { v: :tablet } assert_equal "text/html", @response.content_type assert_equal "tablet", @response.body end @@ -667,32 +669,27 @@ class RespondToControllerTest < ActionController::TestCase assert_equal "text/html", @response.content_type assert_equal "none", @response.body - @request.variant = :phone - get :variant_inline_syntax + get :variant_inline_syntax, params: { v: :phone } assert_equal "text/html", @response.content_type assert_equal "phone", @response.body end def test_variant_inline_syntax_without_block - @request.variant = :phone - get :variant_inline_syntax_without_block + get :variant_inline_syntax_without_block, params: { v: :phone } assert_equal "text/html", @response.content_type assert_equal "phone", @response.body end def test_variant_any - @request.variant = :phone - get :variant_any + get :variant_any, params: { v: :phone } assert_equal "text/html", @response.content_type assert_equal "phone", @response.body - @request.variant = :tablet - get :variant_any + get :variant_any, params: { v: :tablet } assert_equal "text/html", @response.content_type assert_equal "any", @response.body - @request.variant = :phablet - get :variant_any + get :variant_any, params: { v: :phablet } assert_equal "text/html", @response.content_type assert_equal "any", @response.body end @@ -702,54 +699,45 @@ class RespondToControllerTest < ActionController::TestCase assert_equal "text/html", @response.content_type assert_equal "any", @response.body - @request.variant = :phone - get :variant_any_any + get :variant_any_any, params: { v: :phone } assert_equal "text/html", @response.content_type assert_equal "phone", @response.body - @request.variant = :yolo - get :variant_any_any + get :variant_any_any, params: { v: :yolo } assert_equal "text/html", @response.content_type assert_equal "any", @response.body end def test_variant_inline_any - @request.variant = :phone - get :variant_any + get :variant_any, params: { v: :phone } assert_equal "text/html", @response.content_type assert_equal "phone", @response.body - @request.variant = :tablet - get :variant_inline_any + get :variant_inline_any, params: { v: :tablet } assert_equal "text/html", @response.content_type assert_equal "any", @response.body - @request.variant = :phablet - get :variant_inline_any + get :variant_inline_any, params: { v: :phablet } assert_equal "text/html", @response.content_type assert_equal "any", @response.body end def test_variant_inline_any_any - @request.variant = :phone - get :variant_inline_any_any + get :variant_inline_any_any, params: { v: :phone } assert_equal "text/html", @response.content_type assert_equal "phone", @response.body - @request.variant = :yolo - get :variant_inline_any_any + get :variant_inline_any_any, params: { v: :yolo } assert_equal "text/html", @response.content_type assert_equal "any", @response.body end def test_variant_any_implicit_render - @request.variant = :tablet - get :variant_any_implicit_render + get :variant_any_implicit_render, params: { v: :tablet } assert_equal "text/html", @response.content_type assert_equal "tablet", @response.body - @request.variant = :phablet - get :variant_any_implicit_render + get :variant_any_implicit_render, params: { v: :phablet } assert_equal "text/html", @response.content_type assert_equal "phablet", @response.body end @@ -759,36 +747,31 @@ class RespondToControllerTest < ActionController::TestCase assert_equal "text/html", @response.content_type assert_equal "none or phone", @response.body - @request.variant = :phone - get :variant_any_with_none + get :variant_any_with_none, params: { v: :phone } assert_equal "text/html", @response.content_type assert_equal "none or phone", @response.body end def test_format_any_variant_any - @request.variant = :tablet - get :format_any_variant_any, format: :js + get :format_any_variant_any, format: :js, params: { v: :tablet } assert_equal "text/javascript", @response.content_type assert_equal "tablet", @response.body end def test_variant_negotiation_inline_syntax - @request.variant = [:tablet, :phone] - get :variant_inline_syntax_without_block + get :variant_inline_syntax_without_block, params: { v: [:tablet, :phone] } assert_equal "text/html", @response.content_type assert_equal "phone", @response.body end def test_variant_negotiation_block_syntax - @request.variant = [:tablet, :phone] - get :variant_plus_none_for_format + get :variant_plus_none_for_format, params: { v: [:tablet, :phone] } assert_equal "text/html", @response.content_type assert_equal "phone", @response.body end def test_variant_negotiation_without_block - @request.variant = [:tablet, :phone] - get :variant_inline_syntax_without_block + get :variant_inline_syntax_without_block, params: { v: [:tablet, :phone] } assert_equal "text/html", @response.content_type assert_equal "phone", @response.body end diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index 27ee8603e4..ff63c10e8d 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -989,6 +989,7 @@ class RequestParameterFilter < BaseRequestTest [{'foo'=>'bar', 'baz'=>'foo'},{'foo'=>'[FILTERED]', 'baz'=>'[FILTERED]'},%w'foo baz'], [{'bar'=>{'foo'=>'bar','bar'=>'foo'}},{'bar'=>{'foo'=>'[FILTERED]','bar'=>'foo'}},%w'fo'], [{'foo'=>{'foo'=>'bar','bar'=>'foo'}},{'foo'=>'[FILTERED]'},%w'f banana'], + [{'deep'=>{'cc'=>{'code'=>'bar','bar'=>'foo'},'ss'=>{'code'=>'bar'}}},{'deep'=>{'cc'=>{'code'=>'[FILTERED]','bar'=>'foo'},'ss'=>{'code'=>'bar'}}},%w'deep.cc.code'], [{'baz'=>[{'foo'=>'baz'}, "1"]}, {'baz'=>[{'foo'=>'[FILTERED]'}, "1"]}, [/foo/]]] test_hashes.each do |before_filter, after_filter, filter_words| diff --git a/actionpack/test/dispatch/test_request_test.rb b/actionpack/test/dispatch/test_request_test.rb index cc35d4594e..a4c124070a 100644 --- a/actionpack/test/dispatch/test_request_test.rb +++ b/actionpack/test/dispatch/test_request_test.rb @@ -24,8 +24,6 @@ class TestRequestTest < ActiveSupport::TestCase assert_equal true, env.delete("rack.multithread") assert_equal true, env.delete("rack.multiprocess") assert_equal false, env.delete("rack.run_once") - - assert env.empty?, env.inspect end test "cookie jar" do diff --git a/activejob/lib/active_job/arguments.rb b/activejob/lib/active_job/arguments.rb index cdb37879b6..8e462bfe5d 100644 --- a/activejob/lib/active_job/arguments.rb +++ b/activejob/lib/active_job/arguments.rb @@ -16,13 +16,13 @@ module ActiveJob end end - # Raised when an unsupported argument type is being set as job argument. We + # Raised when an unsupported argument type is set as a job argument. We # currently support NilClass, Fixnum, Float, String, TrueClass, FalseClass, - # Bignum and object that can be represented as GlobalIDs (ex: Active Record). - # Also raised if you set the key for a Hash something else than a string or - # a symbol. - class SerializationError < ArgumentError - end + # Bignum and objects that can be represented as GlobalIDs (ex: Active Record). + # Raised if you set the key for a Hash something else than a string or + # a symbol. Also raised when trying to serialize an object which can't be + # identified with a Global ID - such as an unpersisted Active Record model. + class SerializationError < ArgumentError; end module Arguments extend self @@ -59,7 +59,7 @@ module ActiveJob when *TYPE_WHITELIST argument when GlobalID::Identification - { GLOBALID_KEY => argument.to_global_id.to_s } + convert_to_global_id_hash(argument) when Array argument.map { |arg| serialize_argument(arg) } when ActiveSupport::HashWithIndifferentAccess @@ -147,5 +147,12 @@ module ActiveJob end end end + + def convert_to_global_id_hash(argument) + { GLOBALID_KEY => argument.to_global_id.to_s } + rescue URI::GID::MissingModelIdError + raise SerializationError, "Unable to serialize #{argument.class} " \ + "without an id. (Maybe you forgot to call save?)" + end end end diff --git a/activejob/test/cases/argument_serialization_test.rb b/activejob/test/cases/argument_serialization_test.rb index 8b9b62190f..933972a52b 100644 --- a/activejob/test/cases/argument_serialization_test.rb +++ b/activejob/test/cases/argument_serialization_test.rb @@ -88,6 +88,13 @@ class ArgumentSerializationTest < ActiveSupport::TestCase assert_equal "Job with argument: 2", JobBuffer.last_value end + test 'raises a friendly SerializationError for records without ids' do + err = assert_raises ActiveJob::SerializationError do + ActiveJob::Arguments.serialize [Person.new(nil)] + end + assert_match 'Unable to serialize Person without an id.', err.message + end + private def assert_arguments_unchanged(*args) assert_arguments_roundtrip args diff --git a/activemodel/README.rdoc b/activemodel/README.rdoc index 5c36b1277e..d954467387 100644 --- a/activemodel/README.rdoc +++ b/activemodel/README.rdoc @@ -154,7 +154,7 @@ behavior out of the box: * Making objects serializable - ActiveModel::Serialization provides a standard interface for your object + <tt>ActiveModel::Serialization</tt> provides a standard interface for your object to provide +to_json+ or +to_xml+ serialization. class SerialPerson diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index c1019169e1..72094a26c0 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -87,8 +87,7 @@ module ActiveModel validates_with BlockValidator, _merge_attributes(attr_names), &block end - # :nodoc: - VALID_OPTIONS_FOR_VALIDATE = [:on, :if, :unless, :prepend].freeze + VALID_OPTIONS_FOR_VALIDATE = [:on, :if, :unless, :prepend].freeze # :nodoc: # Adds a validation method or block to the class. This is useful when # overriding the +validate+ instance method becomes too unwieldy and @@ -130,6 +129,9 @@ module ActiveModel # end # end # + # Note that the return value of validation methods is not relevant. + # It's not possible to halt the validate callback chain. + # # Options: # * <tt>:on</tt> - Specifies the contexts where this validation is active. # Runs in all validation contexts by default (nil). You can pass a symbol diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 6054b08f33..e4a6734009 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -21,7 +21,7 @@ Example: change_column_default :posts, :status, from: nil, to: "draft" - change_column_default :users, authorized, from: true, to: false + change_column_default :users, :authorized, from: true, to: false *Prem Sichanugrist* diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index dd8f0aa298..0b38666ce9 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -87,8 +87,8 @@ module ActiveRecord return if other.preload_values.empty? && other.includes_values.empty? if other.klass == relation.klass - relation.preload! other.preload_values unless other.preload_values.empty? - relation.includes! other.includes_values unless other.includes_values.empty? + relation.preload!(*other.preload_values) unless other.preload_values.empty? + relation.includes!(other.includes_values) unless other.includes_values.empty? else reflection = relation.klass.reflect_on_all_associations.find do |r| r.class_name == other.klass.name diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 6b4addd52f..d72225f3d3 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -658,6 +658,16 @@ module NestedAttributesOnACollectionAssociationTests assert_equal "Couldn't find #{@child_1.class.name} with ID=1234567890 for Pirate with ID=#{@pirate.id}", exception.message end + def test_should_raise_RecordNotFound_if_an_id_belonging_to_a_different_record_is_given + other_pirate = Pirate.create! catchphrase: 'Ahoy!' + other_child = other_pirate.send(@association_name).create! name: 'Buccaneers Servant' + + exception = assert_raise ActiveRecord::RecordNotFound do + @pirate.attributes = { association_getter => [{ id: other_child.id }] } + end + assert_equal "Couldn't find #{@child_1.class.name} with ID=#{other_child.id} for Pirate with ID=#{@pirate.id}", exception.message + end + def test_should_automatically_build_new_associated_models_for_each_entry_in_a_hash_where_the_id_is_missing @pirate.send(@association_name).destroy_all @pirate.reload.attributes = { diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index acbf85d398..5f48c2b40f 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -647,6 +647,25 @@ class RelationTest < ActiveRecord::TestCase end end + def test_preloading_with_associations_default_scopes_and_merges + post = Post.create! title: 'Uhuu', body: 'body' + reader = Reader.create! post_id: post.id, person_id: 1 + + post_rel = PostWithPreloadDefaultScope.preload(:readers).joins(:readers).where(title: 'Uhuu') + result_post = PostWithPreloadDefaultScope.all.merge(post_rel).to_a.first + + assert_no_queries do + assert_equal [reader], result_post.readers.to_a + end + + post_rel = PostWithIncludesDefaultScope.includes(:readers).where(title: 'Uhuu') + result_post = PostWithIncludesDefaultScope.all.merge(post_rel).to_a.first + + assert_no_queries do + assert_equal [reader], result_post.readers.to_a + end + end + def test_loading_with_one_association posts = Post.preload(:comments) post = posts.find { |p| p.id == 1 } diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 052b1c9690..10f13b67da 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -208,6 +208,22 @@ class PostWithDefaultScope < ActiveRecord::Base default_scope { order(:title) } end +class PostWithPreloadDefaultScope < ActiveRecord::Base + self.table_name = 'posts' + + has_many :readers, foreign_key: 'post_id' + + default_scope { preload(:readers) } +end + +class PostWithIncludesDefaultScope < ActiveRecord::Base + self.table_name = 'posts' + + has_many :readers, foreign_key: 'post_id' + + default_scope { includes(:readers) } +end + class SpecialPostWithDefaultScope < ActiveRecord::Base self.table_name = 'posts' default_scope { where(:id => [1, 5,6]) } diff --git a/activesupport/lib/active_support/core_ext/module/concerning.rb b/activesupport/lib/active_support/core_ext/module/concerning.rb index e26b594fc4..65b88b9bbd 100644 --- a/activesupport/lib/active_support/core_ext/module/concerning.rb +++ b/activesupport/lib/active_support/core_ext/module/concerning.rb @@ -99,7 +99,7 @@ class Module # end # # Todo.ancestors - # # => Todo, Todo::EventTracking, Object + # # => [Todo, Todo::EventTracking, Object] # # This small step has some wonderful ripple effects. We can # * grok the behavior of our class in one glance, diff --git a/activesupport/lib/active_support/ordered_options.rb b/activesupport/lib/active_support/ordered_options.rb index bc0326473d..45864990ce 100644 --- a/activesupport/lib/active_support/ordered_options.rb +++ b/activesupport/lib/active_support/ordered_options.rb @@ -6,6 +6,7 @@ module ActiveSupport # h[:girl] = 'Mary' # h[:boy] # => 'John' # h[:girl] # => 'Mary' + # h[:dog] # => nil # # Using +OrderedOptions+, the above code could be reduced to: # @@ -14,6 +15,13 @@ module ActiveSupport # h.girl = 'Mary' # h.boy # => 'John' # h.girl # => 'Mary' + # h.dog # => nil + # + # To raise an exception when the value is blank, append a + # bang to the key name, like: + # + # h.dog! # => raises KeyError + # class OrderedOptions < Hash alias_method :_get, :[] # preserve the original #[] method protected :_get # make it protected diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md index 7932853c11..71ca7a0f66 100644 --- a/guides/source/active_record_validations.md +++ b/guides/source/active_record_validations.md @@ -965,8 +965,9 @@ own custom validators. You can also create methods that verify the state of your models and add messages to the `errors` collection when they are invalid. You must then -register these methods by using the `validate` class method, passing in the -symbols for the validation methods' names. +register these methods by using the `validate` +([API](http://api.rubyonrails.org/classes/ActiveModel/Validations/ClassMethods.html#method-i-validate)) +class method, passing in the symbols for the validation methods' names. You can pass more than one symbol for each class method and the respective validations will be run in the same order as they were registered. diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md index c19813c8a5..846b8a007d 100644 --- a/guides/source/contributing_to_ruby_on_rails.md +++ b/guides/source/contributing_to_ruby_on_rails.md @@ -295,7 +295,7 @@ You can run a single test through ruby. For instance: ```bash $ cd actionmailer -$ ruby -w -Itest test/mail_layout_test.rb -n test_explicit_class_layout +$ bundle exec ruby -w -Itest test/mail_layout_test.rb -n test_explicit_class_layout ``` The `-n` option allows you to run a single method instead of the whole diff --git a/guides/source/security.md b/guides/source/security.md index 93580d4d4e..485b108d12 100644 --- a/guides/source/security.md +++ b/guides/source/security.md @@ -1026,6 +1026,29 @@ 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/secrets.yml`. You may want to further restrict access, using environment-specific versions of these files and any others that may contain sensitive information. +### Custom secrets + +Rails generates a `config/secrets.yml`. By default, this file contains the +application's `secret_key_base`, but it could also be used to store other +secrets such as access keys for external APIs. + +The secrets added to this file are accessible via `Rails.application.secrets`. +For example, with the following `config/secrets.yml`: + + development: + secret_key_base: 3b7cd727ee24e8444053437c36cc66c3 + some_api_key: SOMEKEY + +`Rails.application.secrets.some_api_key` returns `SOMEKEY` in the development +environment. + +If you want an exception to be raised when some key is blank, use the bang +version: + +```ruby +Rails.application.secrets.some_api_key! # => raises KeyError +``` + Additional Resources -------------------- diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index 4b73313388..b813b083f4 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -356,7 +356,9 @@ module Rails if app_const =~ /^\d/ raise Error, "Invalid application name #{app_name}. Please give a name which does not start with numbers." elsif RESERVED_NAMES.include?(app_name) - raise Error, "Invalid application name #{app_name}. Please give a name which does not match one of the reserved rails words: #{RESERVED_NAMES}" + raise Error, "Invalid application name #{app_name}. Please give a " \ + "name which does not match one of the reserved rails " \ + "words: #{RESERVED_NAMES.join(", ")}" elsif Object.const_defined?(app_const_base) raise Error, "Invalid application name #{app_name}, constant #{app_const_base} is already in use. Please choose another application name." end diff --git a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb index 7f5bf0c8b8..66111004aa 100644 --- a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb +++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb @@ -364,7 +364,9 @@ task default: :test elsif camelized =~ /^\d/ raise Error, "Invalid plugin name #{original_name}. Please give a name which does not start with numbers." elsif RESERVED_NAMES.include?(name) - raise Error, "Invalid plugin name #{original_name}. Please give a name which does not match one of the reserved rails words: #{RESERVED_NAMES}" + raise Error, "Invalid plugin name #{original_name}. Please give a " \ + "name which does not match one of the reserved rails " \ + "words: #{RESERVED_NAMES.join(", ")}" elsif Object.const_defined?(camelized) raise Error, "Invalid plugin name #{original_name}, constant #{camelized} is already in use. Please choose another plugin name." end diff --git a/railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb b/railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb index 824caecb24..f5d1ec2046 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb @@ -1,10 +1,6 @@ require 'test_helper' class NavigationTest < ActionDispatch::IntegrationTest -<% unless options[:skip_active_record] -%> - fixtures :all -<% end -%> - # test "the truth" do # assert true # end diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb index d6e5f4bd89..73e68863f4 100644 --- a/railties/test/generators/plugin_generator_test.rb +++ b/railties/test/generators/plugin_generator_test.rb @@ -38,7 +38,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_equal "Invalid plugin name 43things. Please give a name which does not start with numbers.\n", content content = capture(:stderr){ run_generator [File.join(destination_root, "plugin")] } - assert_equal "Invalid plugin name plugin. Please give a name which does not match one of the reserved rails words: [\"application\", \"destroy\", \"plugin\", \"runner\", \"test\"]\n", content + assert_equal "Invalid plugin name plugin. Please give a name which does not match one of the reserved rails words: application, destroy, plugin, runner, test\n", content content = capture(:stderr){ run_generator [File.join(destination_root, "Digest")] } assert_equal "Invalid plugin name Digest, constant Digest is already in use. Please choose another plugin name.\n", content diff --git a/railties/test/generators/shared_generator_tests.rb b/railties/test/generators/shared_generator_tests.rb index f983b45d2b..77372fb514 100644 --- a/railties/test/generators/shared_generator_tests.rb +++ b/railties/test/generators/shared_generator_tests.rb @@ -56,7 +56,7 @@ module SharedGeneratorTests reserved_words = %w[application destroy plugin runner test] reserved_words.each do |reserved| content = capture(:stderr){ run_generator [File.join(destination_root, reserved)] } - assert_match(/Invalid \w+ name #{reserved}. Please give a name which does not match one of the reserved rails words: \["application", "destroy", "plugin", "runner", "test"\]\n/, content) + assert_match(/Invalid \w+ name #{reserved}. Please give a name which does not match one of the reserved rails words: application, destroy, plugin, runner, test\n/, content) end end |