diff options
19 files changed, 160 insertions, 63 deletions
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/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..0718c5076e 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 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/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/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 |