From 9a0fe92fd21dca2eeec3f20c2d3210e0014265dd Mon Sep 17 00:00:00 2001 From: beerlington Date: Sun, 5 Oct 2014 16:23:01 -0400 Subject: [PATCH] Fixes support for ActiveModel::Dirty This has always been broken, but I was waiting to see how it was addressed in ActiveRecord::Enum before fixing to make sure I had something that was more future-proof. The fix does not work in Rails 3.2 or 4.0, so it is disabled in ActiveRecord <= 4.0 which seems like a good compromise. Previously when using dirty attribute methods, it was hard to predict whether you'd get the string value or the enum class instance. Now it will always return the enum class instance for any of the attribute change methods. --- CHANGELOG.md | 1 + lib/classy_enum/active_record.rb | 40 +++++++++++--- spec/classy_enum/active_record_spec.rb | 72 ++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67e0f25..5521fd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * [BREAKING] Removed use of null objects. Blank values are now returned as is from Enum.build. * [BREAKING] Removed serialize_as_json option. #as_json should be overriden in ClassyEnum::Base subclasses instead. * [BREAKING] Removed allow_blank option from Enum.build. This was used internally for legacy reasons and is no longer needed. +* [BREAKING] Fixes support for ActiveModel::Dirty. Now dirty attribute methods always return enum class instance (instead of string). * Prefer 'class_name' over 'enum' as optional class name argument ## 3.5.0 diff --git a/lib/classy_enum/active_record.rb b/lib/classy_enum/active_record.rb index aa4fba2..04bc325 100644 --- a/lib/classy_enum/active_record.rb +++ b/lib/classy_enum/active_record.rb @@ -67,17 +67,41 @@ def classy_enum_attr(attribute, options={}) allow_blank: allow_blank, allow_nil: allow_nil - # Define getter method that returns a ClassyEnum instance - define_method attribute do - enum.build(read_attribute(attribute), owner: self) - end + # Use a module so that the reader methods can be overridden in classes and + # use super to get the enum value. + mod = Module.new do + + # Define getter method that returns a ClassyEnum instance + define_method attribute do + enum.build(read_attribute(attribute), owner: self) + end + + # Define setter method that accepts string, symbol, instance or class for member + define_method "#{attribute}=" do |value| + value = ClassyEnum._normalize_value(value, default, (allow_nil || allow_blank)) + super(value) + end - # Define setter method that accepts string, symbol, instance or class for member - define_method "#{attribute}=" do |value| - value = ClassyEnum._normalize_value(value, default, (allow_nil || allow_blank)) - super(value) + define_method :save_changed_attribute do |attr_name, arg| + if attribute.to_s == attr_name.to_s && !attribute_changed?(attr_name) + arg = enum.build(arg) + current_value = clone_attribute_value(:read_attribute, attr_name) + + if arg != current_value + if respond_to?(:set_attribute_was, true) + set_attribute_was(attr_name, enum.build(arg, owner: self)) + else + changed_attributes[attr_name] = enum.build(current_value, owner: self) + end + end + else + super(attr_name, arg) + end + end end + include mod + # Initialize the object with the default value if it is present # because this will let you store the default value in the # database and make it searchable. diff --git a/spec/classy_enum/active_record_spec.rb b/spec/classy_enum/active_record_spec.rb index ae21ec4..91a76bb 100644 --- a/spec/classy_enum/active_record_spec.rb +++ b/spec/classy_enum/active_record_spec.rb @@ -79,6 +79,78 @@ class OtherDog < Dog end end + if ::ActiveRecord::VERSION::MAJOR == 4 && ::ActiveRecord::VERSION::MINOR > 0 + context "works with ActiveModel's attributes" do + subject { DefaultDog.create(breed: :golden_retriever) } + let(:old_breed) { Breed::GoldenRetriever.new } + + it "sets changed_attributes to enum object" do + subject.breed = :snoop + subject.changed_attributes[:breed].should eq(old_breed) + end + + it "sets changes to array" do + subject.breed = :snoop + subject.changes[:breed].should eq([old_breed, :snoop]) + end + + it "works with attribute_changed?" do + subject.breed = :snoop + subject.breed_was.should eq(old_breed) + subject.breed_changed?.should be_true + + if subject.respond_to? :attribute_changed? + subject.attribute_changed?(:breed, to: Breed::Snoop.new).should be_true + subject.breed_changed?( + from: Breed::GoldenRetriever.new, + to: Breed::Snoop.new + ).should be_true + end + end + + it "returns enum object for *_was" do + subject.breed = :snoop + subject.breed_was.golden_retriever?.should be_true + end + + it "reverts changes" do + subject.breed = :snoop + subject.breed_changed?.should be_true + subject.breed = old_breed + subject.breed_changed?.should be_false + end + + it "does not track the same value" do + subject.breed = :golden_retriever + subject.breed_changed?.should be_false + end + + it "retains changes with multiple assignments" do + subject.breed = :snoop + subject.breed_changed?.should be_true + subject.breed = :husky + subject.breed_changed?.should be_true + end + + it "allows tracks changes when nil is allowed" do + dog = AllowNilBreedDog.create(breed: :snoop) + dog.breed = nil + dog.save! + dog.breed = :snoop + dog.breed_changed?.should be_true + dog.breed = nil + dog.breed_changed?.should be_false + end + + it "restores breed (Rails 4.2+)" do + if subject.respond_to?(:restore_breed) + subject.restore_breed! + subject.breed.should eq(:golden_retriever) + end + end + end + end + context "with invalid breed options" do subject { DefaultDog.new(breed: :fake_breed) } it { should_not be_valid }