Skip to content

Commit 429f0a7

Browse files
hmcguire-shopifyadrianna-chang-shopifyshioyama
committed
Fix undefined attribute method with attr_readonly
The problem is where an abstract class defines a readonly attribute. When another class subclasses that parent with `raise_on_assign_to_attr_readonly`, it will define the readonly method before the abstract parent has defined its attributes. As a result, when those methods are defined, they will not define the readonly attribute writer, so that `super` called from the readonly accessor will fail with a `NoMethodError`. Instead of trying to make the overriden attribute method lazier by defining it after the real attribute method has been defined, this commit changes the method override to be one layer down on _write_attribute. This avoids the issue of the attribute method being undefined because the real _write_attribute will always be defined. Co-authored-by: Adrianna Chang <[email protected]> Co-authored-by: Chris Salzberg <[email protected]>
1 parent e357d72 commit 429f0a7

File tree

4 files changed

+31
-22
lines changed

4 files changed

+31
-22
lines changed

activerecord/lib/active_record/associations/collection_association.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ def merge_target_lists(persisted, memory)
324324
persisted.map! do |record|
325325
if mem_record = memory.delete(record)
326326

327-
((record.attribute_names & mem_record.attribute_names) - mem_record.changed_attribute_names_to_save).each do |name|
327+
((record.attribute_names & mem_record.attribute_names) - mem_record.changed_attribute_names_to_save - mem_record.class._attr_readonly).each do |name|
328328
mem_record._write_attribute(name, record[name])
329329
end
330330

activerecord/lib/active_record/readonly_attributes.rb

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,21 +28,11 @@ module ClassMethods
2828
# post.title = "a different title" # raises ActiveRecord::ReadonlyAttributeError
2929
# post.update(title: "a different title") # raises ActiveRecord::ReadonlyAttributeError
3030
def attr_readonly(*attributes)
31-
new_attributes = attributes.map(&:to_s).reject { |a| _attr_readonly.include?(a) }
31+
self._attr_readonly |= attributes.map(&:to_s)
3232

3333
if ActiveRecord.raise_on_assign_to_attr_readonly
34-
new_attributes.each do |attribute|
35-
define_method("#{attribute}=") do |value|
36-
raise ReadonlyAttributeError.new(attribute) unless new_record?
37-
38-
super(value)
39-
end
40-
end
41-
4234
include(HasReadonlyAttributes)
4335
end
44-
45-
self._attr_readonly = Set.new(new_attributes) + _attr_readonly
4636
end
4737

4838
# Returns an array of all the attributes that have been specified as readonly.
@@ -56,12 +46,14 @@ def readonly_attribute?(name) # :nodoc:
5646
end
5747

5848
module HasReadonlyAttributes # :nodoc:
59-
def write_attribute(attr_name, value)
60-
if !new_record? && self.class.readonly_attribute?(attr_name.to_s)
61-
raise ReadonlyAttributeError.new(attr_name)
62-
end
49+
[:write_attribute, :_write_attribute].each do |name|
50+
define_method(name) do |attr_name, value|
51+
if !new_record? && self.class.readonly_attribute?(attr_name.to_s)
52+
raise ReadonlyAttributeError.new(attr_name)
53+
end
6354

64-
super
55+
super(attr_name, value)
56+
end
6557
end
6658
end
6759
end

activerecord/test/cases/base_test.rb

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,16 @@ class ReadonlyTitlePost < Post
6666
attr_readonly :title
6767
end
6868

69+
class ReadonlyTitleAbstractPost < ActiveRecord::Base
70+
self.abstract_class = true
71+
72+
attr_readonly :title
73+
end
74+
75+
class ReadonlyTitlePostWithAbstractParent < ReadonlyTitleAbstractPost
76+
self.table_name = "posts"
77+
end
78+
6979
previous_value, ActiveRecord.raise_on_assign_to_attr_readonly = ActiveRecord.raise_on_assign_to_attr_readonly, false
7080

7181
class NonRaisingPost < Post
@@ -699,7 +709,7 @@ def test_comparison_with_different_objects_in_array
699709
end
700710

701711
def test_readonly_attributes
702-
assert_equal Set.new([ "title" ]), ReadonlyTitlePost.readonly_attributes
712+
assert_equal [ "title" ], ReadonlyTitlePost.readonly_attributes
703713

704714
post = ReadonlyTitlePost.create(title: "cannot change this", body: "changeable")
705715
assert_equal "cannot change this", post.title
@@ -750,7 +760,7 @@ def test_readonly_attributes
750760
end
751761

752762
def test_readonly_attributes_on_a_new_record
753-
assert_equal Set.new([ "title" ]), ReadonlyTitlePost.readonly_attributes
763+
assert_equal [ "title" ], ReadonlyTitlePost.readonly_attributes
754764

755765
post = ReadonlyTitlePost.new(title: "can change this until you save", body: "changeable")
756766
assert_equal "can change this until you save", post.title
@@ -766,7 +776,6 @@ def test_readonly_attributes_on_a_new_record
766776
assert_equal "changed via write_attribute", post.title
767777
assert_equal "changed via write_attribute", post.body
768778

769-
770779
post.assign_attributes(body: "changed via assign_attributes", title: "changed via assign_attributes")
771780
assert_equal "changed via assign_attributes", post.title
772781
assert_equal "changed via assign_attributes", post.body
@@ -783,8 +792,16 @@ def test_readonly_attributes_on_a_new_record
783792
assert_equal "changed via []=", post.body
784793
end
785794

795+
def test_readonly_attributes_in_abstract_class_descendant
796+
assert_equal [ "title" ], ReadonlyTitlePostWithAbstractParent.readonly_attributes
797+
798+
assert_nothing_raised do
799+
ReadonlyTitlePostWithAbstractParent.new(title: "can change this until you save")
800+
end
801+
end
802+
786803
def test_readonly_attributes_when_configured_to_not_raise
787-
assert_equal Set.new([ "title" ]), NonRaisingPost.readonly_attributes
804+
assert_equal [ "title" ], NonRaisingPost.readonly_attributes
788805

789806
post = NonRaisingPost.create(title: "cannot change this", body: "changeable")
790807
assert_equal "cannot change this", post.title

activerecord/test/cases/locking_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,7 @@ def test_lock_with_custom_column_without_default_queries_count
455455
end
456456

457457
def test_readonly_attributes
458-
assert_equal Set.new([ "name" ]), ReadonlyNameShip.readonly_attributes
458+
assert_equal [ "name" ], ReadonlyNameShip.readonly_attributes
459459

460460
s = ReadonlyNameShip.create(name: "unchangeable name")
461461
s.reload

0 commit comments

Comments
 (0)