diff options
31 files changed, 416 insertions, 112 deletions
@@ -8,7 +8,7 @@ gem 'mocha', '~> 0.13.0', require: false gem 'rack-test', github: 'brynary/rack-test' gem 'rack-cache', '~> 1.2' gem 'bcrypt-ruby', '~> 3.0.0' -gem 'jquery-rails', '~> 2.1.4', github: 'rails/jquery-rails' +gem 'jquery-rails', '~> 2.2.0', github: 'rails/jquery-rails' gem 'turbolinks' gem 'coffee-rails', github: 'rails/coffee-rails' diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index e6e58ce6cd..2c96e03f55 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -19,6 +19,20 @@ module ActionController end end + # Raised when a supplied parameter is not expected. + # + # params = ActionController::Parameters.new(a: "123", b: "456") + # params.permit(:c) + # # => ActionController::UnpermittedParameters: found unexpected keys: a, b + class UnpermittedParameters < IndexError + attr_reader :params # :nodoc: + + def initialize(params) # :nodoc: + @params = params + super("found unpermitted parameters: #{params.join(", ")}") + end + end + # == Action Controller \Parameters # # Allows to choose which attributes should be whitelisted for mass updating @@ -43,10 +57,15 @@ module ActionController # Person.first.update!(permitted) # # => #<Person id: 1, name: "Francesco", age: 22, role: "user"> # - # It provides a +permit_all_parameters+ option that controls the top-level - # behavior of new instances. If it's +true+, all the parameters will be - # permitted by default. The default value for +permit_all_parameters+ - # option is +false+. + # It provides two options that controls the top-level behavior of new instances: + # + # * +permit_all_parameters+ - If it's +true+, all the parameters will be + # permitted by default. The default is +false+. + # * +action_on_unpermitted_parameters+ - Allow to control the behavior when parameters + # that are not explicitly permitted are found. The values can be <tt>:log</tt> to + # write a message on the logger or <tt>:raise</tt> to raise + # ActionController::UnpermittedParameters exception. The default value is <tt>:log</tt> + # in test and development environments, +false+ otherwise. # # params = ActionController::Parameters.new # params.permitted? # => false @@ -56,6 +75,16 @@ module ActionController # params = ActionController::Parameters.new # params.permitted? # => true # + # params = ActionController::Parameters.new(a: "123", b: "456") + # params.permit(:c) + # # => {} + # + # ActionController::Parameters.action_on_unpermitted_parameters = :raise + # + # params = ActionController::Parameters.new(a: "123", b: "456") + # params.permit(:c) + # # => ActionController::UnpermittedParameters: found unpermitted keys: a, b + # # <tt>ActionController::Parameters</tt> is inherited from # <tt>ActiveSupport::HashWithIndifferentAccess</tt>, this means # that you can fetch values using either <tt>:key</tt> or <tt>"key"</tt>. @@ -65,6 +94,11 @@ module ActionController # params["key"] # => "value" class Parameters < ActiveSupport::HashWithIndifferentAccess cattr_accessor :permit_all_parameters, instance_accessor: false + cattr_accessor :action_on_unpermitted_parameters, instance_accessor: false + + # Never raise an UnpermittedParameters exception because of these params + # are present. They are added by Rails and it's of no concern. + NEVER_UNPERMITTED_PARAMS = %w( controller action ) # Returns a new instance of <tt>ActionController::Parameters</tt>. # Also, sets the +permitted+ attribute to the default value of @@ -222,6 +256,8 @@ module ActionController end end + unpermitted_parameters!(params) + params.permit! end @@ -300,6 +336,22 @@ module ActionController yield object end end + + def unpermitted_parameters!(params) + unpermitted_keys = unpermitted_keys(params) + if unpermitted_keys.any? + case self.class.action_on_unpermitted_parameters + when :log + ActionController::Base.logger.debug "Unpermitted parameters: #{unpermitted_keys.join(", ")}" + when :raise + raise ActionController::UnpermittedParameters.new(unpermitted_keys) + end + end + end + + def unpermitted_keys(params) + self.keys - params.keys - NEVER_UNPERMITTED_PARAMS + end end # == Strong \Parameters diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb index 3e44155f73..5379547c57 100644 --- a/actionpack/lib/action_controller/railtie.rb +++ b/actionpack/lib/action_controller/railtie.rb @@ -20,22 +20,27 @@ module ActionController end initializer "action_controller.parameters_config" do |app| - ActionController::Parameters.permit_all_parameters = app.config.action_controller.delete(:permit_all_parameters) { false } + options = app.config.action_controller + + ActionController::Parameters.permit_all_parameters = options.delete(:permit_all_parameters) { false } + ActionController::Parameters.action_on_unpermitted_parameters = options.delete(:action_on_unpermitted_parameters) do + (Rails.env.test? || Rails.env.development?) ? :log : false + end end initializer "action_controller.set_configs" do |app| paths = app.config.paths options = app.config.action_controller - options.logger ||= Rails.logger - options.cache_store ||= Rails.cache + options.logger ||= Rails.logger + options.cache_store ||= Rails.cache - options.javascripts_dir ||= paths["public/javascripts"].first - options.stylesheets_dir ||= paths["public/stylesheets"].first + options.javascripts_dir ||= paths["public/javascripts"].first + options.stylesheets_dir ||= paths["public/stylesheets"].first # Ensure readers methods get compiled - options.asset_host ||= app.config.asset_host - options.relative_url_root ||= app.config.relative_url_root + options.asset_host ||= app.config.asset_host + options.relative_url_root ||= app.config.relative_url_root ActiveSupport.on_load(:action_controller) do include app.routes.mounted_helpers diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 331d15d403..d8206b573d 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -81,8 +81,7 @@ module ActionController # # assert that the "_customer" partial was rendered with a specific object # assert_template partial: '_customer', locals: { customer: @customer } def assert_template(options = {}, message = nil) - # Force body to be read in case the - # template is being streamed + # Force body to be read in case the template is being streamed. response.body case options diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index 57660e93c4..89a7b12818 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -122,7 +122,7 @@ module ActionDispatch def valid_accept_header (xhr? && (accept || content_mime_type)) || - (accept && accept !~ BROWSER_LIKE_ACCEPTS) + (accept.present? && accept !~ BROWSER_LIKE_ACCEPTS) end def use_accept_header diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb index ea3e8357d4..6c970f3024 100644 --- a/actionpack/lib/action_dispatch/routing/inspector.rb +++ b/actionpack/lib/action_dispatch/routing/inspector.rb @@ -73,10 +73,11 @@ module ActionDispatch routes_to_display = filter_routes(filter) routes = collect_routes(routes_to_display) - formatter.section :application, 'Application routes', routes + formatter.section routes @engines.each do |name, engine_routes| - formatter.section :engine, "Routes for #{name}", engine_routes + formatter.section_title "Routes for #{name}" + formatter.section engine_routes end formatter.result @@ -125,8 +126,11 @@ module ActionDispatch @buffer.join("\n") end - def section(type, title, routes) - @buffer << "\n#{title}:" unless type == :application + def section_title(title) + @buffer << "\n#{title}:" + end + + def section(routes) @buffer << draw_section(routes) end @@ -148,8 +152,11 @@ module ActionDispatch @buffer = [] end - def section(type, title, routes) + def section_title(title) @buffer << %(<tr><th colspan="4">#{title}</th></tr>) + end + + def section(routes) @buffer << @view.render(partial: "routes/route", collection: routes) end diff --git a/actionpack/test/controller/layout_test.rb b/actionpack/test/controller/layout_test.rb index 94a8d2f180..365fa04570 100644 --- a/actionpack/test/controller/layout_test.rb +++ b/actionpack/test/controller/layout_test.rb @@ -80,7 +80,7 @@ end class StreamingLayoutController < LayoutTest def render(*args) - options = args.extract_options! || {} + options = args.extract_options! super(*args, options.merge(:stream => true)) end end diff --git a/actionpack/test/controller/parameters/log_on_unpermitted_params_test.rb b/actionpack/test/controller/parameters/log_on_unpermitted_params_test.rb new file mode 100644 index 0000000000..22e603b881 --- /dev/null +++ b/actionpack/test/controller/parameters/log_on_unpermitted_params_test.rb @@ -0,0 +1,50 @@ +require 'abstract_unit' +require 'action_controller/metal/strong_parameters' + +class LogOnUnpermittedParamsTest < ActiveSupport::TestCase + def setup + ActionController::Parameters.action_on_unpermitted_parameters = :log + end + + def teardown + ActionController::Parameters.action_on_unpermitted_parameters = false + end + + test "logs on unexpected params" do + params = ActionController::Parameters.new({ + book: { pages: 65 }, + fishing: "Turnips" + }) + + assert_logged("Unpermitted parameters: fishing") do + params.permit(book: [:pages]) + end + end + + test "logs on unexpected nested params" do + params = ActionController::Parameters.new({ + book: { pages: 65, title: "Green Cats and where to find then." } + }) + + assert_logged("Unpermitted parameters: title") do + params.permit(book: [:pages]) + end + end + + private + + def assert_logged(message) + old_logger = ActionController::Base.logger + log = StringIO.new + ActionController::Base.logger = Logger.new(log) + + begin + yield + + log.rewind + assert_match message, log.read + ensure + ActionController::Base.logger = old_logger + end + end +end diff --git a/actionpack/test/controller/parameters/raise_on_unpermitted_params_test.rb b/actionpack/test/controller/parameters/raise_on_unpermitted_params_test.rb new file mode 100644 index 0000000000..f9cc9f96f1 --- /dev/null +++ b/actionpack/test/controller/parameters/raise_on_unpermitted_params_test.rb @@ -0,0 +1,33 @@ +require 'abstract_unit' +require 'action_controller/metal/strong_parameters' + +class RaiseOnUnpermittedParamsTest < ActiveSupport::TestCase + def setup + ActionController::Parameters.action_on_unpermitted_parameters = :raise + end + + def teardown + ActionController::Parameters.action_on_unpermitted_parameters = false + end + + test "raises on unexpected params" do + params = ActionController::Parameters.new({ + book: { pages: 65 }, + fishing: "Turnips" + }) + + assert_raises(ActionController::UnpermittedParameters) do + params.permit(book: [:pages]) + end + end + + test "raises on unexpected nested params" do + params = ActionController::Parameters.new({ + book: { pages: 65, title: "Green Cats and where to find then." } + }) + + assert_raises(ActionController::UnpermittedParameters) do + params.permit(book: [:pages]) + end + end +end diff --git a/actionpack/test/controller/record_identifier_test.rb b/actionpack/test/controller/record_identifier_test.rb index 3067daf697..ff5d7fd3bd 100644 --- a/actionpack/test/controller/record_identifier_test.rb +++ b/actionpack/test/controller/record_identifier_test.rb @@ -9,25 +9,25 @@ class ControllerRecordIdentifierTest < ActiveSupport::TestCase end def test_dom_id_deprecation - assert_deprecated /dom_id method will no longer be included by default in controllers/ do + assert_deprecated(/dom_id method will no longer be included by default in controllers/) do dom_id(@record) end end def test_dom_class_deprecation - assert_deprecated /dom_class method will no longer be included by default in controllers/ do + assert_deprecated(/dom_class method will no longer be included by default in controllers/) do dom_class(@record) end end def test_dom_id_from_module_deprecation - assert_deprecated /Calling ActionController::RecordIdentifier.dom_id is deprecated/ do + assert_deprecated(/Calling ActionController::RecordIdentifier.dom_id is deprecated/) do ActionController::RecordIdentifier.dom_id(@record) end end def test_dom_class_from_module_deprecation - assert_deprecated /Calling ActionController::RecordIdentifier.dom_class is deprecated/ do + assert_deprecated(/Calling ActionController::RecordIdentifier.dom_class is deprecated/) do ActionController::RecordIdentifier.dom_class(@record) end end diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index 39923d0d2b..8a01b29340 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -587,6 +587,10 @@ class RequestTest < ActiveSupport::TestCase request.expects(:parameters).at_least_once.returns({}) assert_equal [Mime::HTML], request.formats + request = stub_request 'HTTP_ACCEPT' => '' + request.expects(:parameters).at_least_once.returns({}) + assert_equal [Mime::HTML], request.formats + request = stub_request 'CONTENT_TYPE' => 'application/xml; charset=UTF-8', 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest" request.expects(:parameters).at_least_once.returns({}) diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 5a0c391154..45a2b59d47 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,5 +1,16 @@ ## Rails 4.0.0 (unreleased) ## +* Collection associations `#empty?` always respects builded records. + Fix #8879. + + Example: + + widget = Widget.new + widget.things.build + widget.things.empty? # => false + + *Yves Senn* + * Remove support for parsing YAML parameters from request. *Aaron Patterson* @@ -974,7 +985,6 @@ * `:conditions` becomes `:where`. * `:include` becomes `:includes`. - * `:extend` becomes `:extending`. The code to implement the deprecated features has been moved out to the `activerecord-deprecated_finders` gem. This gem is a dependency diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index 31ddb01123..bfc2e54aba 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -25,5 +25,5 @@ Gem::Specification.new do |s| s.add_dependency 'activemodel', version s.add_dependency 'arel', '~> 3.0.2' - s.add_dependency 'activerecord-deprecated_finders', '0.0.1' + s.add_dependency 'activerecord-deprecated_finders', '0.0.2' end diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 1303822868..300f67959d 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -16,6 +16,7 @@ module ActiveRecord def scope scope = klass.unscoped scope.merge! eval_scope(klass, reflection.scope) if reflection.scope + scope.extending! Array(options[:extend]) add_constraints(scope) end diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb index fcdfc1e150..fdead16761 100644 --- a/activerecord/lib/active_record/associations/builder/collection_association.rb +++ b/activerecord/lib/active_record/associations/builder/collection_association.rb @@ -6,7 +6,8 @@ module ActiveRecord::Associations::Builder CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove] def valid_options - super + [:table_name, :finder_sql, :counter_sql, :before_add, :after_add, :before_remove, :after_remove] + super + [:table_name, :finder_sql, :counter_sql, :before_add, + :after_add, :before_remove, :after_remove, :extend] end attr_reader :block_extension, :extension_module diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 832b963052..5feb149946 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -273,7 +273,7 @@ module ActiveRecord if loaded? || options[:counter_sql] size.zero? else - !scope.exists? + @target.blank? && !scope.exists? end end diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 33dce58982..e93e700c93 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -33,6 +33,7 @@ module ActiveRecord def initialize(klass, association) #:nodoc: @association = association super klass, klass.arel_table + self.default_scoped = true merge! association.scope(nullify: false) end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 3011f959a5..1b2aa9349e 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -236,26 +236,26 @@ module ActiveRecord alias update_attributes! update! - # Updates a single attribute of an object, without having to explicitly call save on that object. - # - # * Validation is skipped. - # * Callbacks are skipped. - # * updated_at/updated_on column is not updated if that column is available. - # - # Raises an +ActiveRecordError+ when called on new objects, or when the +name+ - # attribute is marked as readonly. + # Equivalent to <code>update_columns(name => value)</code>. def update_column(name, value) update_columns(name => value) end - # Updates the attributes from the passed-in hash, without having to explicitly call save on that object. + # Updates the attributes directly in the database issuing an UPDATE SQL + # statement and sets them in the receiver: # - # * Validation is skipped. + # user.update_columns(last_request_at: Time.current) + # + # This is the fastest way to update attributes because it goes straight to + # the database, but take into account that in consequence the regular update + # procedures are totally bypassed. In particular: + # + # * Validations are skipped. # * Callbacks are skipped. - # * updated_at/updated_on column is not updated if that column is available. + # * +updated_at+/+updated_on+ are not updated. # - # Raises an +ActiveRecordError+ when called on new objects, or when at least - # one of the attributes is marked as readonly. + # This method raises an +ActiveRecord::ActiveRecordError+ when called on new + # objects, or when at least one of the attributes is marked as readonly. def update_columns(attributes) raise ActiveRecordError, "can not update on a new record object" unless persisted? diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 6ec5cf3e18..0053530f73 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -276,7 +276,7 @@ module ActiveRecord stmt.table(table) stmt.key = table[primary_key] - if joins_values.any? + if with_default_scope.joins_values.any? @klass.connection.join_to_update(stmt, arel) else stmt.take(arel.limit) @@ -401,7 +401,7 @@ module ActiveRecord stmt = Arel::DeleteManager.new(arel.engine) stmt.from(table) - if joins_values.any? + if with_default_scope.joins_values.any? @klass.connection.join_to_delete(stmt, arel, table[primary_key]) else stmt.wheres = arel.constraints @@ -474,16 +474,16 @@ module ActiveRecord # Returns sql statement for the relation. # - # Users.where(name: 'Oscar').to_sql + # User.where(name: 'Oscar').to_sql # # => SELECT "users".* FROM "users" WHERE "users"."name" = 'Oscar' def to_sql @to_sql ||= klass.connection.to_sql(arel, bind_values.dup) end - # Returns a hash of where conditions + # Returns a hash of where conditions. # - # Users.where(name: 'Oscar').where_values_hash - # # => {name: "oscar"} + # User.where(name: 'Oscar').where_values_hash + # # => {name: "Oscar"} def where_values_hash equalities = with_default_scope.where_values.grep(Arel::Nodes::Equality).find_all { |node| node.left.relation.name == table_name diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 83074e72c1..883d25d80b 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -7,12 +7,12 @@ module ActiveRecord table = default_table if value.is_a?(Hash) - table = Arel::Table.new(column, default_table.engine) - association = klass.reflect_on_association(column.to_sym) - if value.empty? - queries.concat ['1 = 2'] + queries << '1 = 2' else + table = Arel::Table.new(column, default_table.engine) + association = klass.reflect_on_association(column.to_sym) + value.each do |k, v| queries.concat expand(association && association.klass, table, k, v) end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 7e6c7d5862..d42630e1b7 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -625,6 +625,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 3, company.clients_of_firm.size end + def test_collection_not_empty_after_building + company = companies(:first_firm) + assert_predicate company.contracts, :empty? + company.contracts.build + assert_not_predicate company.contracts, :empty? + end + def test_collection_size_twice_for_regressions post = posts(:thinking) assert_equal 0, post.readers.size @@ -1705,4 +1712,21 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 0, post.comments.count end end + + test "collection proxy respects default scope" do + author = authors(:mary) + assert !author.first_posts.exists? + end + + test "association with extend option" do + post = posts(:welcome) + assert_equal "lifo", post.comments_with_extend.author + assert_equal "hello", post.comments_with_extend.greeting + end + + test "association with extend option with multiple extensions" do + post = posts(:welcome) + assert_equal "lifo", post.comments_with_extend_2.author + assert_equal "hello", post.comments_with_extend_2.greeting + end end diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/relation_scoping_test.rb index 78fb91d321..7388324a0d 100644 --- a/activerecord/test/cases/relation_scoping_test.rb +++ b/activerecord/test/cases/relation_scoping_test.rb @@ -161,6 +161,28 @@ class RelationScopingTest < ActiveRecord::TestCase assert !Developer.all.where_values.include?("name = 'Jamis'") end + + def test_default_scope_filters_on_joins + assert_equal 1, DeveloperFilteredOnJoins.all.count + assert_equal DeveloperFilteredOnJoins.all.first, developers(:david).becomes(DeveloperFilteredOnJoins) + end + + def test_update_all_default_scope_filters_on_joins + DeveloperFilteredOnJoins.update_all(:salary => 65000) + assert_equal 65000, Developer.find(developers(:david).id).salary + + # has not changed jamis + assert_not_equal 65000, Developer.find(developers(:jamis).id).salary + end + + def test_delete_all_default_scope_filters_on_joins + assert_not_equal [], DeveloperFilteredOnJoins.all + + DeveloperFilteredOnJoins.delete_all() + + assert_equal [], DeveloperFilteredOnJoins.all + assert_not_equal [], Developer.all + end end class NestedRelationScopingTest < ActiveRecord::TestCase diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb index 683cb54a10..81bc87bd42 100644 --- a/activerecord/test/models/developer.rb +++ b/activerecord/test/models/developer.rb @@ -101,6 +101,15 @@ class DeveloperWithIncludes < ActiveRecord::Base default_scope { includes(:audit_logs) } end +class DeveloperFilteredOnJoins < ActiveRecord::Base + self.table_name = 'developers' + has_and_belongs_to_many :projects, -> { order('projects.id') }, :foreign_key => 'developer_id', :join_table => 'developers_projects' + + def self.default_scope + joins(:projects).where(:projects => { :name => 'Active Controller' }) + end +end + class DeveloperOrderedBySalary < ActiveRecord::Base self.table_name = 'developers' default_scope { order('salary DESC') } diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index c995f59a15..4433550dd5 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -5,6 +5,12 @@ class Post < ActiveRecord::Base end end + module NamedExtension2 + def greeting + "hello" + end + end + scope :containing_the_letter_a, -> { where("body LIKE '%a%'") } scope :ranked_by_comments, -> { order("comments_count DESC") } @@ -43,6 +49,14 @@ class Post < ActiveRecord::Base end end + has_many :comments_with_extend, extend: NamedExtension, class_name: "Comment", foreign_key: "post_id" do + def greeting + "hello" + end + end + + has_many :comments_with_extend_2, extend: [NamedExtension, NamedExtension2], class_name: "Comment", foreign_key: "post_id" + has_many :author_favorites, :through => :author has_many :author_categorizations, :through => :author, :source => :categorizations has_many :author_addresses, :through => :author diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index 5a5548d567..6a1f1fefb8 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -275,34 +275,14 @@ module ActiveSupport if block_given? options = merged_options(options) key = namespaced_key(name, options) - unless options[:force] - entry = instrument(:read, name, options) do |payload| - payload[:super_operation] = :fetch if payload - read_entry(key, options) - end - end - if entry && entry.expired? - race_ttl = options[:race_condition_ttl].to_i - if race_ttl && (Time.now - entry.expires_at <= race_ttl) - # When an entry has :race_condition_ttl defined, put the stale entry back into the cache - # for a brief period while the entry is begin recalculated. - entry.expires_at = Time.now + race_ttl - write_entry(key, entry, :expires_in => race_ttl * 2) - else - delete_entry(key, options) - end - entry = nil - end + + cached_entry = find_cached_entry(key, name, options) unless options[:force] + entry = handle_expired_entry(cached_entry, key, options) if entry - instrument(:fetch_hit, name, options) { |payload| } - entry.value + get_entry_value(entry, name, options) else - result = instrument(:generate, name, options) do |payload| - yield(name) - end - write(name, result, options) - result + save_block_result_to_cache(name, options) { |name| yield name } end else read(name, options) @@ -531,6 +511,42 @@ module ActiveSupport return unless logger && logger.debug? && !silence? logger.debug("Cache #{operation}: #{key}#{options.blank? ? "" : " (#{options.inspect})"}") end + + def find_cached_entry(key, name, options) + instrument(:read, name, options) do |payload| + payload[:super_operation] = :fetch if payload + read_entry(key, options) + end + end + + def handle_expired_entry(entry, key, options) + if entry && entry.expired? + race_ttl = options[:race_condition_ttl].to_i + if race_ttl && (Time.now - entry.expires_at <= race_ttl) + # When an entry has :race_condition_ttl defined, put the stale entry back into the cache + # for a brief period while the entry is begin recalculated. + entry.expires_at = Time.now + race_ttl + write_entry(key, entry, :expires_in => race_ttl * 2) + else + delete_entry(key, options) + end + entry = nil + end + entry + end + + def get_entry_value(entry, name, options) + instrument(:fetch_hit, name, options) { |payload| } + entry.value + end + + def save_block_result_to_cache(name, options) + result = instrument(:generate, name, options) do |payload| + yield(name) + end + write(name, result, options) + result + end end # This class is used to represent cache entries. Cache entries have a value and an optional diff --git a/activesupport/lib/active_support/testing/isolation.rb b/activesupport/lib/active_support/testing/isolation.rb index aa87598926..e4f8959e7a 100644 --- a/activesupport/lib/active_support/testing/isolation.rb +++ b/activesupport/lib/active_support/testing/isolation.rb @@ -43,32 +43,36 @@ module ActiveSupport module Isolation require 'thread' - class ParallelEach - include Enumerable - - # default to 2 cores - CORES = (ENV['TEST_CORES'] || 2).to_i - - def initialize list - @list = list - @queue = SizedQueue.new CORES - end + # Recent versions of MiniTest (such as the one shipped with Ruby 2.0) already define + # a ParallelEach class. + unless defined? ParallelEach + class ParallelEach + include Enumerable + + # default to 2 cores + CORES = (ENV['TEST_CORES'] || 2).to_i + + def initialize list + @list = list + @queue = SizedQueue.new CORES + end - def grep pattern - self.class.new super - end + def grep pattern + self.class.new super + end - def each - threads = CORES.times.map { - Thread.new { - while job = @queue.pop - yield job - end + def each + threads = CORES.times.map { + Thread.new { + while job = @queue.pop + yield job + end + } } - } - @list.each { |i| @queue << i } - CORES.times { @queue << nil } - threads.each(&:join) + @list.each { |i| @queue << i } + CORES.times { @queue << nil } + threads.each(&:join) + end end end @@ -84,10 +88,14 @@ module ActiveSupport !ENV["NO_FORK"] && ((RbConfig::CONFIG['host_os'] !~ /mswin|mingw/) && (RUBY_PLATFORM !~ /java/)) end + @@class_setup_mutex = Mutex.new + def _run_class_setup # class setup method should only happen in parent - unless defined?(@@ran_class_setup) || ENV['ISOLATION_TEST'] - self.class.setup if self.class.respond_to?(:setup) - @@ran_class_setup = true + @@class_setup_mutex.synchronize do + unless defined?(@@ran_class_setup) || ENV['ISOLATION_TEST'] + self.class.setup if self.class.respond_to?(:setup) + @@ran_class_setup = true + end end end diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md index 7f992025e4..590ad5738e 100644 --- a/guides/source/action_mailer_basics.md +++ b/guides/source/action_mailer_basics.md @@ -588,7 +588,7 @@ class SandboxEmailInterceptor end ``` -Before the Interceptor can do it's job you need to register it with the Action Mailer framework. You can do this in an initializer file `config/initializers/sandbox_email_interceptor.rb` +Before the interceptor can do its job you need to register it with the Action Mailer framework. You can do this in an initializer file `config/initializers/sandbox_email_interceptor.rb` ```ruby ActionMailer::Base.register_interceptor(SandboxEmailInterceptor) if Rails.env.staging? diff --git a/guides/source/command_line.md b/guides/source/command_line.md index 0081e6427e..2790a4740a 100644 --- a/guides/source/command_line.md +++ b/guides/source/command_line.md @@ -442,7 +442,7 @@ app/model/post.rb: NOTE. When using specific annotations and custom annotations, the annotation name (FIXME, BUG etc) is not displayed in the output lines. -By default, `rake notes` will look in the `app`, `config`, `lib`, `script` and `test` directories. If you would like to search other directories, you can provide them as a comma separated list in an environment variable `SOURCE_ANNOTATION_DIRECTORIES`. +By default, `rake notes` will look in the `app`, `config`, `lib`, `bin` and `test` directories. If you would like to search other directories, you can provide them as a comma separated list in an environment variable `SOURCE_ANNOTATION_DIRECTORIES`. ```bash $ export SOURCE_ANNOTATION_DIRECTORIES='rspec,vendor' diff --git a/railties/lib/rails/info.rb b/railties/lib/rails/info.rb index aacc1be2fc..592e74726e 100644 --- a/railties/lib/rails/info.rb +++ b/railties/lib/rails/info.rb @@ -46,7 +46,7 @@ module Rails alias inspect to_s def to_html - (table = '<table>').tap do + '<table>'.tap do |table| properties.each do |(name, value)| table << %(<tr><td class="name">#{CGI.escapeHTML(name.to_s)}</td>) formatted_value = if value.kind_of?(Array) diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index f9108ed7b9..78aefa1437 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -577,6 +577,54 @@ module ApplicationTests assert_equal 'permitted', last_response.body end + test "config.action_controller.action_on_unpermitted_parameters = :raise" do + app_file 'app/controllers/posts_controller.rb', <<-RUBY + class PostsController < ActionController::Base + def create + render text: params.require(:post).permit(:name) + end + end + RUBY + + add_to_config <<-RUBY + routes.prepend do + resources :posts + end + config.action_controller.action_on_unpermitted_parameters = :raise + RUBY + + require "#{app_path}/config/environment" + + assert_equal :raise, ActionController::Parameters.action_on_unpermitted_parameters + + post "/posts", {post: {"title" =>"zomg"}} + assert_match "We're sorry, but something went wrong", last_response.body + end + + test "config.action_controller.action_on_unpermitted_parameters is :log by default on development" do + ENV["RAILS_ENV"] = "development" + + require "#{app_path}/config/environment" + + assert_equal :log, ActionController::Parameters.action_on_unpermitted_parameters + end + + test "config.action_controller.action_on_unpermitted_parameters is :log by defaul on test" do + ENV["RAILS_ENV"] = "test" + + require "#{app_path}/config/environment" + + assert_equal :log, ActionController::Parameters.action_on_unpermitted_parameters + end + + test "config.action_controller.action_on_unpermitted_parameters is false by default on production" do + ENV["RAILS_ENV"] = "production" + + require "#{app_path}/config/environment" + + assert_equal false, ActionController::Parameters.action_on_unpermitted_parameters + end + test "config.action_dispatch.ignore_accept_header" do make_basic_app do |app| app.config.action_dispatch.ignore_accept_header = true diff --git a/railties/test/application/runner_test.rb b/railties/test/application/runner_test.rb index f65b5e2f2d..6595c40f8b 100644 --- a/railties/test/application/runner_test.rb +++ b/railties/test/application/runner_test.rb @@ -37,27 +37,27 @@ module ApplicationTests end def test_should_run_file - app_file "script/count_users.rb", <<-SCRIPT + app_file "bin/count_users.rb", <<-SCRIPT puts User.count SCRIPT - assert_match "42", Dir.chdir(app_path) { `bundle exec rails runner "script/count_users.rb"` } + assert_match "42", Dir.chdir(app_path) { `bundle exec rails runner "bin/count_users.rb"` } end def test_should_set_dollar_0_to_file - app_file "script/dollar0.rb", <<-SCRIPT + app_file "bin/dollar0.rb", <<-SCRIPT puts $0 SCRIPT - assert_match "script/dollar0.rb", Dir.chdir(app_path) { `bundle exec rails runner "script/dollar0.rb"` } + assert_match "bin/dollar0.rb", Dir.chdir(app_path) { `bundle exec rails runner "bin/dollar0.rb"` } end def test_should_set_dollar_program_name_to_file - app_file "script/program_name.rb", <<-SCRIPT + app_file "bin/program_name.rb", <<-SCRIPT puts $PROGRAM_NAME SCRIPT - assert_match "script/program_name.rb", Dir.chdir(app_path) { `bundle exec rails runner "script/program_name.rb"` } + assert_match "bin/program_name.rb", Dir.chdir(app_path) { `bundle exec rails runner "bin/program_name.rb"` } end def test_with_hook |