aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.codeclimate.yml6
-rw-r--r--Gemfile.lock2
-rw-r--r--actionpack/lib/action_dispatch/journey/routes.rb1
-rw-r--r--actionview/lib/action_view/template/handlers.rb6
-rw-r--r--activejob/lib/active_job/test_helper.rb4
-rw-r--r--activemodel/lib/active_model/secure_password.rb74
-rw-r--r--activemodel/test/cases/secure_password_test.rb14
-rw-r--r--activemodel/test/models/user.rb7
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb1
-rw-r--r--activerecord/lib/active_record/querying.rb2
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb6
-rw-r--r--activerecord/lib/active_record/relation/merger.rb23
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb31
-rw-r--r--activerecord/lib/active_record/touch_later.rb2
-rw-r--r--activerecord/test/cases/associations/inner_join_association_test.rb5
-rw-r--r--activerecord/test/cases/associations/left_outer_join_association_test.rb10
-rw-r--r--activerecord/test/cases/base_test.rb5
-rw-r--r--activerecord/test/cases/relation/delegation_test.rb4
-rw-r--r--activerecord/test/cases/relation/update_all_test.rb32
-rw-r--r--activerecord/test/cases/touch_later_test.rb2
-rw-r--r--activesupport/lib/active_support/subscriber.rb61
-rw-r--r--activesupport/test/subscriber_test.rb23
-rw-r--r--guides/source/6_0_release_notes.md46
-rw-r--r--guides/source/active_record_callbacks.md27
-rw-r--r--guides/source/layouts_and_rendering.md2
25 files changed, 296 insertions, 100 deletions
diff --git a/.codeclimate.yml b/.codeclimate.yml
index 3c6e743df6..b8bebe4d42 100644
--- a/.codeclimate.yml
+++ b/.codeclimate.yml
@@ -28,9 +28,3 @@ engines:
ratings:
paths:
- "**.rb"
-
-exclude_paths:
- - ci/
- - guides/
- - tasks/
- - tools/
diff --git a/Gemfile.lock b/Gemfile.lock
index 20f6b3ac21..92dcee9542 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -108,7 +108,7 @@ GEM
activerecord-jdbcsqlite3-adapter (52.1-java)
activerecord-jdbc-adapter (= 52.1)
jdbc-sqlite3 (~> 3.8, < 3.30)
- addressable (2.5.2)
+ addressable (2.6.0)
public_suffix (>= 2.0.2, < 4.0)
amq-protocol (2.3.0)
ansi (1.5.0)
diff --git a/actionpack/lib/action_dispatch/journey/routes.rb b/actionpack/lib/action_dispatch/journey/routes.rb
index c0377459d5..3ba8361d77 100644
--- a/actionpack/lib/action_dispatch/journey/routes.rb
+++ b/actionpack/lib/action_dispatch/journey/routes.rb
@@ -56,7 +56,6 @@ module ActionDispatch
end
def simulator
- return if ast.nil?
@simulator ||= begin
gtg = GTG::Builder.new(ast).transition_table
GTG::Simulator.new(gtg)
diff --git a/actionview/lib/action_view/template/handlers.rb b/actionview/lib/action_view/template/handlers.rb
index ddaac7a100..6450513003 100644
--- a/actionview/lib/action_view/template/handlers.rb
+++ b/actionview/lib/action_view/template/handlers.rb
@@ -45,12 +45,12 @@ module ActionView #:nodoc:
unless params.find_all { |type, _| type == :req || type == :opt }.length >= 2
ActiveSupport::Deprecation.warn <<~eowarn
- Single arity template handlers are deprecated. Template handlers must
+ Single arity template handlers are deprecated. Template handlers must
now accept two parameters, the view object and the source for the view object.
Change:
- >> #{handler.class}#call(#{params.map(&:last).join(", ")})
+ >> #{handler}.call(#{params.map(&:last).join(", ")})
To:
- >> #{handler.class}#call(#{params.map(&:last).join(", ")}, source)
+ >> #{handler}.call(#{params.map(&:last).join(", ")}, source)
eowarn
handler = LegacyHandlerWrapper.new(handler)
end
diff --git a/activejob/lib/active_job/test_helper.rb b/activejob/lib/active_job/test_helper.rb
index e5e2b086bc..463020a332 100644
--- a/activejob/lib/active_job/test_helper.rb
+++ b/activejob/lib/active_job/test_helper.rb
@@ -353,7 +353,7 @@ module ActiveJob
#
#
# The +args+ argument also accepts a proc which will get passed the actual
- # job's arguments. Your proc needs to returns a boolean value determining if
+ # job's arguments. Your proc needs to return a boolean value determining if
# the job's arguments matches your expectation. This is useful to check only
# for a subset of arguments.
#
@@ -426,7 +426,7 @@ module ActiveJob
# end
#
# The +args+ argument also accepts a proc which will get passed the actual
- # job's arguments. Your proc needs to returns a boolean value determining if
+ # job's arguments. Your proc needs to return a boolean value determining if
# the job's arguments matches your expectation. This is useful to check only
# for a subset of arguments.
#
diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb
index cc1368d3a0..5f409326bd 100644
--- a/activemodel/lib/active_model/secure_password.rb
+++ b/activemodel/lib/active_model/secure_password.rb
@@ -69,42 +69,7 @@ module ActiveModel
raise
end
- mod = Module.new do
- attr_reader attribute
-
- define_method("#{attribute}=") do |unencrypted_password|
- if unencrypted_password.nil?
- self.send("#{attribute}_digest=", nil)
- elsif !unencrypted_password.empty?
- instance_variable_set("@#{attribute}", unencrypted_password)
- cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
- self.send("#{attribute}_digest=", BCrypt::Password.create(unencrypted_password, cost: cost))
- end
- end
-
- define_method("#{attribute}_confirmation=") do |unencrypted_password|
- instance_variable_set("@#{attribute}_confirmation", unencrypted_password)
- end
-
- # Returns +self+ if the password is correct, otherwise +false+.
- #
- # class User < ActiveRecord::Base
- # has_secure_password validations: false
- # end
- #
- # user = User.new(name: 'david', password: 'mUc3m00RsqyRe')
- # user.save
- # user.authenticate_password('notright') # => false
- # user.authenticate_password('mUc3m00RsqyRe') # => user
- define_method("authenticate_#{attribute}") do |unencrypted_password|
- attribute_digest = send("#{attribute}_digest")
- BCrypt::Password.new(attribute_digest).is_password?(unencrypted_password) && self
- end
-
- alias_method :authenticate, :authenticate_password if attribute == :password
- end
-
- include mod
+ include InstanceMethodsOnActivation.new(attribute)
if validations
include ActiveModel::Validations
@@ -122,5 +87,42 @@ module ActiveModel
end
end
end
+
+ class InstanceMethodsOnActivation < Module
+ def initialize(attribute)
+ attr_reader attribute
+
+ define_method("#{attribute}=") do |unencrypted_password|
+ if unencrypted_password.nil?
+ self.send("#{attribute}_digest=", nil)
+ elsif !unencrypted_password.empty?
+ instance_variable_set("@#{attribute}", unencrypted_password)
+ cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
+ self.send("#{attribute}_digest=", BCrypt::Password.create(unencrypted_password, cost: cost))
+ end
+ end
+
+ define_method("#{attribute}_confirmation=") do |unencrypted_password|
+ instance_variable_set("@#{attribute}_confirmation", unencrypted_password)
+ end
+
+ # Returns +self+ if the password is correct, otherwise +false+.
+ #
+ # class User < ActiveRecord::Base
+ # has_secure_password validations: false
+ # end
+ #
+ # user = User.new(name: 'david', password: 'mUc3m00RsqyRe')
+ # user.save
+ # user.authenticate_password('notright') # => false
+ # user.authenticate_password('mUc3m00RsqyRe') # => user
+ define_method("authenticate_#{attribute}") do |unencrypted_password|
+ attribute_digest = send("#{attribute}_digest")
+ BCrypt::Password.new(attribute_digest).is_password?(unencrypted_password) && self
+ end
+
+ alias_method :authenticate, :authenticate_password if attribute == :password
+ end
+ end
end
end
diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb
index bbf443290b..0aca714bd2 100644
--- a/activemodel/test/cases/secure_password_test.rb
+++ b/activemodel/test/cases/secure_password_test.rb
@@ -184,6 +184,20 @@ class SecurePasswordTest < ActiveModel::TestCase
assert_nil @existing_user.password_digest
end
+ test "override secure password attribute" do
+ assert_nil @user.password_called
+
+ @user.password = "secret"
+
+ assert_equal "secret", @user.password
+ assert_equal 1, @user.password_called
+
+ @user.password = "terces"
+
+ assert_equal "terces", @user.password
+ assert_equal 2, @user.password_called
+ end
+
test "authenticate" do
@user.password = "secret"
@user.recovery_password = "42password"
diff --git a/activemodel/test/models/user.rb b/activemodel/test/models/user.rb
index bb1b187694..fc4a9e4334 100644
--- a/activemodel/test/models/user.rb
+++ b/activemodel/test/models/user.rb
@@ -10,4 +10,11 @@ class User
has_secure_password :recovery_password, validations: false
attr_accessor :password_digest, :recovery_password_digest
+ attr_accessor :password_called
+
+ def password=(unencrypted_password)
+ self.password_called ||= 0
+ self.password_called += 1
+ super
+ end
end
diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb
index c7cd87f9d4..6b57e5093a 100644
--- a/activerecord/lib/active_record/associations/preloader.rb
+++ b/activerecord/lib/active_record/associations/preloader.rb
@@ -150,7 +150,6 @@ module ActiveRecord
def grouped_records(association, records, polymorphic_parent)
h = {}
records.each do |record|
- next unless record
reflection = record.class._reflect_on_association(association)
next if polymorphic_parent && !reflection || !record.association(association).klass
(h[reflection] ||= []) << record
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
index ae1501f5a1..08cfc3fe5f 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -10,7 +10,7 @@ module ActiveRecord
:first_or_create, :first_or_create!, :first_or_initialize,
:find_or_create_by, :find_or_create_by!, :find_or_initialize_by,
:create_or_find_by, :create_or_find_by!,
- :destroy_all, :delete_all, :update_all, :destroy_by, :delete_by,
+ :destroy_all, :delete_all, :update_all, :touch_all, :destroy_by, :delete_by,
:find_each, :find_in_batches, :in_batches,
:select, :reselect, :order, :reorder, :group, :limit, :offset, :joins, :left_joins, :left_outer_joins,
:where, :rewhere, :preload, :extract_associated, :eager_load, :includes, :from, :lock, :readonly, :extending, :or,
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index e2efd4aa0d..fcb0da1a42 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -370,12 +370,6 @@ module ActiveRecord
relation
end
- def construct_join_dependency(associations)
- ActiveRecord::Associations::JoinDependency.new(
- klass, table, associations
- )
- end
-
def apply_join_dependency(eager_loading: group_values.empty?)
join_dependency = construct_join_dependency(eager_load_values + includes_values)
relation = except(:includes, :eager_load, :preload).joins!(join_dependency)
diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb
index 4de7465128..6bb77b355c 100644
--- a/activerecord/lib/active_record/relation/merger.rb
+++ b/activerecord/lib/active_record/relation/merger.rb
@@ -117,16 +117,14 @@ module ActiveRecord
if other.klass == relation.klass
relation.joins!(*other.joins_values)
else
- joins_dependency = other.joins_values.map do |join|
+ associations, others = other.joins_values.partition do |join|
case join
- when Hash, Symbol, Array
- other.send(:construct_join_dependency, join)
- else
- join
+ when Hash, Symbol, Array; true
end
end
- relation.joins!(*joins_dependency)
+ join_dependency = other.construct_join_dependency(associations)
+ relation.joins!(join_dependency, *others)
end
end
@@ -136,16 +134,9 @@ module ActiveRecord
if other.klass == relation.klass
relation.left_outer_joins!(*other.left_outer_joins_values)
else
- joins_dependency = other.left_outer_joins_values.map do |join|
- case join
- when Hash, Symbol, Array
- other.send(:construct_join_dependency, join)
- else
- join
- end
- end
-
- relation.left_outer_joins!(*joins_dependency)
+ associations = other.left_outer_joins_values
+ join_dependency = other.construct_join_dependency(associations)
+ relation.joins!(join_dependency)
end
end
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 5f728f2263..7ea18c3069 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -992,6 +992,12 @@ module ActiveRecord
@arel ||= build_arel(aliases)
end
+ def construct_join_dependency(associations) # :nodoc:
+ ActiveRecord::Associations::JoinDependency.new(
+ klass, table, associations
+ )
+ end
+
protected
def build_subquery(subquery_alias, select_value) # :nodoc:
subquery = except(:optimizer_hints).arel.as(subquery_alias)
@@ -1021,8 +1027,11 @@ module ActiveRecord
def build_arel(aliases)
arel = Arel::SelectManager.new(table)
- aliases = build_joins(arel, joins_values.flatten, aliases) unless joins_values.empty?
- build_left_outer_joins(arel, left_outer_joins_values.flatten, aliases) unless left_outer_joins_values.empty?
+ if !joins_values.empty?
+ build_joins(arel, joins_values.flatten, aliases)
+ elsif !left_outer_joins_values.empty?
+ build_left_outer_joins(arel, left_outer_joins_values.flatten, aliases)
+ end
arel.where(where_clause.ast) unless where_clause.empty?
arel.having(having_clause.ast) unless having_clause.empty?
@@ -1072,22 +1081,28 @@ module ActiveRecord
end
end
- def build_left_outer_joins(manager, outer_joins, aliases)
- buckets = outer_joins.group_by do |join|
- case join
+ def valid_association_list(associations)
+ associations.each do |association|
+ case association
when Hash, Symbol, Array
- :association_join
- when ActiveRecord::Associations::JoinDependency
- :stashed_join
+ # valid
else
raise ArgumentError, "only Hash, Symbol and Array are allowed"
end
end
+ end
+ def build_left_outer_joins(manager, outer_joins, aliases)
+ buckets = { association_join: valid_association_list(outer_joins) }
build_join_query(manager, buckets, Arel::Nodes::OuterJoin, aliases)
end
def build_joins(manager, joins, aliases)
+ unless left_outer_joins_values.empty?
+ left_joins = valid_association_list(left_outer_joins_values.flatten)
+ joins << construct_join_dependency(left_joins)
+ end
+
buckets = joins.group_by do |join|
case join
when String
diff --git a/activerecord/lib/active_record/touch_later.rb b/activerecord/lib/active_record/touch_later.rb
index f70b7c50a2..5dc88fb26c 100644
--- a/activerecord/lib/active_record/touch_later.rb
+++ b/activerecord/lib/active_record/touch_later.rb
@@ -2,7 +2,7 @@
module ActiveRecord
# = Active Record Touch Later
- module TouchLater
+ module TouchLater # :nodoc:
extend ActiveSupport::Concern
included do
diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb
index c33dcdee61..e0dac01f4a 100644
--- a/activerecord/test/cases/associations/inner_join_association_test.rb
+++ b/activerecord/test/cases/associations/inner_join_association_test.rb
@@ -29,7 +29,10 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase
def test_construct_finder_sql_does_not_table_name_collide_on_duplicate_associations_with_left_outer_joins
sql = Person.joins(agents: :agents).left_outer_joins(agents: :agents).to_sql
- assert_match(/agents_people_4/i, sql)
+ assert_match(/agents_people_2/i, sql)
+ assert_match(/INNER JOIN/i, sql)
+ assert_no_match(/agents_people_4/i, sql)
+ assert_no_match(/LEFT OUTER JOIN/i, sql)
end
def test_construct_finder_sql_does_not_table_name_collide_with_string_joins
diff --git a/activerecord/test/cases/associations/left_outer_join_association_test.rb b/activerecord/test/cases/associations/left_outer_join_association_test.rb
index 0e54e8c1b0..0a8863c35d 100644
--- a/activerecord/test/cases/associations/left_outer_join_association_test.rb
+++ b/activerecord/test/cases/associations/left_outer_join_association_test.rb
@@ -46,6 +46,12 @@ class LeftOuterJoinAssociationTest < ActiveRecord::TestCase
assert queries.any? { |sql| /LEFT OUTER JOIN/i.match?(sql) }
end
+ def test_left_outer_joins_is_deduped_when_same_association_is_joined
+ queries = capture_sql { Author.joins(:posts).left_outer_joins(:posts).to_a }
+ assert queries.any? { |sql| /INNER JOIN/i.match?(sql) }
+ assert queries.none? { |sql| /LEFT OUTER JOIN/i.match?(sql) }
+ end
+
def test_construct_finder_sql_ignores_empty_left_outer_joins_hash
queries = capture_sql { Author.left_outer_joins({}).to_a }
assert queries.none? { |sql| /LEFT OUTER JOIN/i.match?(sql) }
@@ -60,6 +66,10 @@ class LeftOuterJoinAssociationTest < ActiveRecord::TestCase
assert_raise(ArgumentError) { Author.left_outer_joins('LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"').to_a }
end
+ def test_left_outer_joins_with_string_join
+ assert_equal 16, Author.left_outer_joins(:posts).joins("LEFT OUTER JOIN comments ON comments.post_id = posts.id").count
+ end
+
def test_join_conditions_added_to_join_clause
queries = capture_sql { Author.left_outer_joins(:essays).to_a }
assert queries.any? { |sql| /writer_type.*?=.*?(Author|\?|\$1|\:a1)/i.match?(sql) }
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index 866818b2ab..99f47cfe37 100644
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -1035,11 +1035,6 @@ class BasicsTest < ActiveRecord::TestCase
end
end
- def test_find_last
- last = Developer.last
- assert_equal last, Developer.all.merge!(order: "id desc").first
- end
-
def test_last
assert_equal Developer.all.merge!(order: "id desc").first, Developer.last
end
diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb
index 172fa20bc3..085006c9a2 100644
--- a/activerecord/test/cases/relation/delegation_test.rb
+++ b/activerecord/test/cases/relation/delegation_test.rb
@@ -51,12 +51,12 @@ module ActiveRecord
ActiveRecord::SpawnMethods.public_instance_methods(false) - [:spawn, :merge!] +
ActiveRecord::QueryMethods.public_instance_methods(false).reject { |method|
method.to_s.end_with?("=", "!", "value", "values", "clause")
- } - [:reverse_order, :arel, :extensions] + [
+ } - [:reverse_order, :arel, :extensions, :construct_join_dependency] + [
:any?, :many?, :none?, :one?,
:first_or_create, :first_or_create!, :first_or_initialize,
:find_or_create_by, :find_or_create_by!, :find_or_initialize_by,
:create_or_find_by, :create_or_find_by!,
- :destroy_all, :delete_all, :update_all, :delete_by, :destroy_by
+ :destroy_all, :delete_all, :update_all, :touch_all, :delete_by, :destroy_by
]
def test_delegate_querying_methods
diff --git a/activerecord/test/cases/relation/update_all_test.rb b/activerecord/test/cases/relation/update_all_test.rb
index 0500574f28..e45531b4a9 100644
--- a/activerecord/test/cases/relation/update_all_test.rb
+++ b/activerecord/test/cases/relation/update_all_test.rb
@@ -241,6 +241,38 @@ class UpdateAllTest < ActiveRecord::TestCase
end
end
+ def test_klass_level_update_all
+ travel 5.seconds do
+ now = Time.now.utc
+
+ Person.all.each do |person|
+ assert_not_equal now, person.updated_at
+ end
+
+ Person.update_all(updated_at: now)
+
+ Person.all.each do |person|
+ assert_equal now, person.updated_at
+ end
+ end
+ end
+
+ def test_klass_level_touch_all
+ travel 5.seconds do
+ now = Time.now.utc
+
+ Person.all.each do |person|
+ assert_not_equal now, person.updated_at
+ end
+
+ Person.touch_all(time: now)
+
+ Person.all.each do |person|
+ assert_equal now, person.updated_at
+ end
+ end
+ end
+
# Oracle UPDATE does not support ORDER BY
unless current_adapter?(:OracleAdapter)
def test_update_all_ignores_order_without_limit_from_association
diff --git a/activerecord/test/cases/touch_later_test.rb b/activerecord/test/cases/touch_later_test.rb
index cd3d5ed7d1..f1a9cf2d05 100644
--- a/activerecord/test/cases/touch_later_test.rb
+++ b/activerecord/test/cases/touch_later_test.rb
@@ -10,7 +10,7 @@ require "models/tree"
class TouchLaterTest < ActiveRecord::TestCase
fixtures :nodes, :trees
- def test_touch_laster_raise_if_non_persisted
+ def test_touch_later_raise_if_non_persisted
invoice = Invoice.new
Invoice.transaction do
assert_not_predicate invoice, :persisted?
diff --git a/activesupport/lib/active_support/subscriber.rb b/activesupport/lib/active_support/subscriber.rb
index f3e902f9dd..c3cd175a52 100644
--- a/activesupport/lib/active_support/subscriber.rb
+++ b/activesupport/lib/active_support/subscriber.rb
@@ -24,6 +24,10 @@ module ActiveSupport
# After configured, whenever a "sql.active_record" notification is published,
# it will properly dispatch the event (ActiveSupport::Notifications::Event) to
# the +sql+ method.
+ #
+ # We can detach a subscriber as well:
+ #
+ # ActiveRecord::StatsSubscriber.detach_from(:active_record)
class Subscriber
class << self
# Attach the subscriber to a namespace.
@@ -40,6 +44,25 @@ module ActiveSupport
end
end
+ # Detach the subscriber from a namespace.
+ def detach_from(namespace, notifier = ActiveSupport::Notifications)
+ @namespace = namespace
+ @subscriber = find_attached_subscriber
+ @notifier = notifier
+
+ return unless subscriber
+
+ subscribers.delete(subscriber)
+
+ # Remove event subscribers of all existing methods on the class.
+ subscriber.public_methods(false).each do |event|
+ remove_event_subscriber(event)
+ end
+
+ # Reset notifier so that event subscribers will not add for new methods added to the class.
+ @notifier = nil
+ end
+
# Adds event subscribers for all new methods added to the class.
def method_added(event)
# Only public methods are added as subscribers, and only if a notifier
@@ -58,15 +81,41 @@ module ActiveSupport
attr_reader :subscriber, :notifier, :namespace
def add_event_subscriber(event) # :doc:
- return if %w{ start finish }.include?(event.to_s)
+ return if invalid_event?(event.to_s)
- pattern = "#{event}.#{namespace}"
+ pattern = prepare_pattern(event)
# Don't add multiple subscribers (eg. if methods are redefined).
- return if subscriber.patterns.include?(pattern)
+ return if pattern_subscribed?(pattern)
+
+ subscriber.patterns[pattern] = notifier.subscribe(pattern, subscriber)
+ end
+
+ def remove_event_subscriber(event) # :doc:
+ return if invalid_event?(event.to_s)
+
+ pattern = prepare_pattern(event)
+
+ return unless pattern_subscribed?(pattern)
+
+ notifier.unsubscribe(subscriber.patterns[pattern])
+ subscriber.patterns.delete(pattern)
+ end
+
+ def find_attached_subscriber
+ subscribers.find { |attached_subscriber| attached_subscriber.instance_of?(self) }
+ end
+
+ def invalid_event?(event)
+ %w{ start finish }.include?(event.to_s)
+ end
+
+ def prepare_pattern(event)
+ "#{event}.#{namespace}"
+ end
- subscriber.patterns << pattern
- notifier.subscribe(pattern, subscriber)
+ def pattern_subscribed?(pattern)
+ subscriber.patterns.key?(pattern)
end
end
@@ -74,7 +123,7 @@ module ActiveSupport
def initialize
@queue_key = [self.class.name, object_id].join "-"
- @patterns = []
+ @patterns = {}
super
end
diff --git a/activesupport/test/subscriber_test.rb b/activesupport/test/subscriber_test.rb
index 6b012e43af..bc8d8f1c13 100644
--- a/activesupport/test/subscriber_test.rb
+++ b/activesupport/test/subscriber_test.rb
@@ -23,6 +23,21 @@ class TestSubscriber < ActiveSupport::Subscriber
end
end
+class TestSubscriber2 < ActiveSupport::Subscriber
+ attach_to :doodle
+ detach_from :doodle
+
+ cattr_reader :events
+
+ def self.clear
+ @@events = []
+ end
+
+ def open_party(event)
+ events << event
+ end
+end
+
# Monkey patch subscriber to test that only one subscriber per method is added.
class TestSubscriber
remove_method :open_party
@@ -34,6 +49,7 @@ end
class SubscriberTest < ActiveSupport::TestCase
def setup
TestSubscriber.clear
+ TestSubscriber2.clear
end
def test_attaches_subscribers
@@ -53,4 +69,11 @@ class SubscriberTest < ActiveSupport::TestCase
assert_equal [], TestSubscriber.events
end
+
+ def test_detaches_subscribers
+ ActiveSupport::Notifications.instrument("open_party.doodle")
+
+ assert_equal [], TestSubscriber2.events
+ assert_equal 1, TestSubscriber.events.size
+ end
end
diff --git a/guides/source/6_0_release_notes.md b/guides/source/6_0_release_notes.md
index 6c3091153f..a4edaa1180 100644
--- a/guides/source/6_0_release_notes.md
+++ b/guides/source/6_0_release_notes.md
@@ -267,10 +267,56 @@ Please refer to the [Changelog][active-job] for detailed changes.
### Removals
+* Remove support for Qu gem.
+ ([Pull Request](https://github.com/rails/rails/pull/32300))
+
### Deprecations
### Notable changes
+* Add support for custom serializers for Active Job arguments.
+ ([Pull Request](https://github.com/rails/rails/pull/30941))
+
+* Add support for executing Active Jobs in the timezone in which
+ they were enqueued.
+ ([Pull Request](https://github.com/rails/rails/pull/32085))
+
+* Allow passing multiple exceptions to `retry_on`/`discard_on`.
+ ([Commit](https://github.com/rails/rails/commit/3110caecbebdad7300daaf26bfdff39efda99e25))
+
+* Allow calling `assert_enqueued_with` and `assert_enqueued_email_with` without a block.
+ ([Pull Request](https://github.com/rails/rails/pull/33258))
+
+* Wrap the notifications for `enqueue` and `enqueue_at` in the `around_enqueue`
+ callback instead of `after_enqueue` callback.
+ ([Pull Request](https://github.com/rails/rails/pull/33171))
+
+* Allow calling `perform_enqueued_jobs` without a block.
+ ([Pull Request](https://github.com/rails/rails/pull/33626))
+
+* Allow calling `assert_performed_with` without a block.
+ ([Pull Request](https://github.com/rails/rails/pull/33635))
+
+* Add `:queue` option to job assertions and helpers.
+ ([Pull Request](https://github.com/rails/rails/pull/33635))
+
+* Add hooks to Active Job around retries and discards.
+ ([Pull Request](https://github.com/rails/rails/pull/33751))
+
+* Add a way to test for subset of arguments when performing jobs.
+ ([Pull Request](https://github.com/rails/rails/pull/33995))
+
+* Include deserialized arguments in jobs returned by Active Job
+ test helpers.
+ ([Pull Request](https://github.com/rails/rails/pull/34204))
+
+* Allow Active Job assertion helpers to accept Proc for `only`
+ keyword.
+ ([Pull Request](https://github.com/rails/rails/pull/34339))
+
+* Drop microseconds and nanoseconds from the job arguments in assertion helpers.
+ ([Pull Request](https://github.com/rails/rails/pull/35713))
+
Ruby on Rails Guides
--------------------
diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md
index 614737c342..8f54e78224 100644
--- a/guides/source/active_record_callbacks.md
+++ b/guides/source/active_record_callbacks.md
@@ -473,10 +473,33 @@ end
=> User was saved to database
```
-To register callbacks for both create and update actions, use `after_commit` instead.
+There is also an alias for using the `after_commit` callback for both create and update together:
+
+* `after_save_commit`
+
+```ruby
+class User < ApplicationRecord
+ after_save_commit :log_user_saved_to_db
+
+ private
+ def log_user_saved_to_db
+ puts 'User was saved to database'
+ end
+end
+
+# creating a User
+>> @user = User.create
+=> User was saved to database
+
+# updating @user
+>> @user.save
+=> User was saved to database
+```
+
+To register callbacks for both create and destroy actions, use `after_commit` instead.
```ruby
class User < ApplicationRecord
- after_commit :log_user_saved_to_db, on: [:create, :update]
+ after_commit :log_user_saved_to_db, on: [:create, :destroy]
end
```
diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md
index 43cc958cb1..2808527141 100644
--- a/guides/source/layouts_and_rendering.md
+++ b/guides/source/layouts_and_rendering.md
@@ -420,7 +420,7 @@ If a template with the specified format does not exist an `ActionView::MissingTe
##### The `:variants` Option
-This tells rails to look for template variations of the same format.
+This tells Rails to look for template variations of the same format.
You can specify a list of variants by passing the `:variants` option with a symbol or an array.
An example of use would be this.