Skip to content

Commit

Permalink
Fixes support for ActiveModel::Dirty
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
beerlington committed Oct 24, 2014
1 parent 0e7f44f commit 9a0fe92
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 8 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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
Expand Down
40 changes: 32 additions & 8 deletions lib/classy_enum/active_record.rb
Expand Up @@ -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.
Expand Down
72 changes: 72 additions & 0 deletions spec/classy_enum/active_record_spec.rb
Expand Up @@ -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 }
Expand Down

0 comments on commit 9a0fe92

Please sign in to comment.