Skip to content

Commit

Permalink
MONGOID-4528 Add more dirty methods
Browse files Browse the repository at this point in the history
  • Loading branch information
comandeo-mongo committed Aug 10, 2022
1 parent 5f825ec commit 250b1b1
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 0 deletions.
42 changes: 42 additions & 0 deletions docs/release-notes/mongoid-8.1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,48 @@ The complete list of releases is available `on GitHub
please consult GitHub releases for detailed release notes and JIRA for
the complete list of issues fixed in each release, including bug fixes.

Added ``attribute_before_last_save``, ``saved_change_to_attribute`` and ``will_save_change_to_attribute?`` methods
------------------------------------------------------------------------------------------------------------------

These new methods behave identically to corresponding methods
from ``ActiveRecord::AttributeMethods::Dirty``. The methods are particularly useful in
callbacks:

.. code-block:: ruby

class Person
include Mongoid::Document

field :name, type: String

before_save do
puts "will_save_change_to_attribute?(:name): #{will_save_change_to_attribute?(:name)}"
end

after_save do
puts "attribute_was(:name): #{attribute_was(:name)}"
puts "attribute_before_last_save(:name): #{attribute_before_last_save(:name)}"
puts "saved_change_to_attribute(:name): #{saved_change_to_attribute(:name)}"
end
end

person = Person.create(name: 'John')
# "will_save_change_to_attribute?(:name): true"
# attribute_was(:name): John => New value
# attribute_before_last_save(:name): nil => Value before save
# saved_change_to_attribute(:name): [nil, "John"] => Both values
person.name = 'Jane'
person.save
# "will_save_change_to_attribute?(:name): true"
# attribute_was(:name): Jane => New value
# attribute_before_last_save(:name): John => Value before save
# saved_change_to_attribute(:name): ["John", "Jane"] => Both values

For all of the new method there is also a shorter form created dynamically, e.g.
``attribute_before_last_save(:name)`` is equivalent to ``name_before_last_save``,
``saved_change_to_attribute(:name)`` is equivalent to ````saved_change_to_name``,
and ``will_save_change_to_attribute?(:name)`` is equivalent to ``will_save_change_to_name?``.


Configuration DSL No Longer Requires an Argument to its Block
-------------------------------------------------------------
Expand Down
47 changes: 47 additions & 0 deletions lib/mongoid/changeable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ def changes
# @example Move the changes to previous.
# person.move_changes
def move_changes
@changes_before_last_save = @previous_changes
@previous_changes = changes
@attributes_before_last_save = @previous_attributes
@previous_attributes = attributes.dup
Atomic::UPDATES.each do |update|
send(update).clear
Expand Down Expand Up @@ -133,6 +135,34 @@ def setters
mods
end

# Returns the original value of an attribute before the last save.
#
# This method is useful in after callbacks to get the original value of
# an attribute before the save that triggered the callbacks to run.
#
# @param [ Symbol | String ] attr The name of the attribute.
#
# @return [ Object ] Value of the attribute before the last save.
def attribute_before_last_save(attr)
attr = database_field_name(attr)
attributes_before_last_save[attr]
end

# Returns the change to an attribute during the last save.
#
# @param [ Symbol | String ] attr The name of the attribute.
#
# @return [ Array<Object> | nil ] If the attribute was changed, returns
# an array containing the original value and the saved value, otherwise nil.
def saved_change_to_attribute(attr)
attr = database_field_name(attr)
previous_changes[attr]
end

def will_save_change_to_attribute?(attr, **kwargs)
attribute_changed?(attr, **kwargs)
end

private

# Get attributes of the document before the document was saved.
Expand All @@ -142,6 +172,14 @@ def previous_attributes
@previous_attributes ||= {}
end

def changes_before_last_save
@changes_before_last_save ||= {}
end

def attributes_before_last_save
@attributes_before_last_save ||= {}
end

# Get the old and new value for the provided attribute.
#
# @example Get the attribute change.
Expand Down Expand Up @@ -317,6 +355,9 @@ def create_dirty_change_check(name, meth)
re_define_method("#{meth}_changed?") do |**kwargs|
attribute_changed?(name, **kwargs)
end
re_define_method("will_save_change_to_#{meth}?") do |**kwargs|
will_save_change_to_attribute?(name, **kwargs)
end
end
end

Expand Down Expand Up @@ -350,6 +391,12 @@ def create_dirty_previous_value_accessor(name, meth)
re_define_method("#{meth}_previously_was") do
attribute_previously_was(name)
end
re_define_method("#{meth}_before_last_save") do
attribute_before_last_save(name)
end
re_define_method("saved_change_to_#{meth}") do
saved_change_to_attribute(name)
end
end
end

Expand Down
84 changes: 84 additions & 0 deletions spec/integration/callbacks_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -447,4 +447,88 @@ class PreviouslyPersistedPerson
expect(saved_person.previously_persisted_value).to be_truthy
end
end

context 'saved_change_to_attribute and attribute_before_last_save' do
class TestSCTAAndABLSInCallbacks
include Mongoid::Document

field :name, type: String
field :age, type: Integer

set_callback :save, :before do |doc|
[:name, :age].each do |attr|
saved_change_to_attribute_values_before[attr] += [saved_change_to_attribute(attr)]
attribute_before_last_save_values_before[attr] += [attribute_before_last_save(attr)]
will_save_change_to_attribute_values_before[attr] += [will_save_change_to_attribute?(attr)]
end
end

set_callback :save, :after do |doc|
[:name, :age].each do |attr|
saved_change_to_attribute_values_after[attr] += [saved_change_to_attribute(attr)]
attribute_before_last_save_values_after[attr] += [attribute_before_last_save(attr)]
end
end

def saved_change_to_attribute_values_before
@saved_change_to_attribute_values_before ||= Hash.new([])
end

def attribute_before_last_save_values_before
@attribute_before_last_save_values_before ||= Hash.new([])
end

def saved_change_to_attribute_values_after
@saved_change_to_attribute_values_after ||= Hash.new([])
end

def attribute_before_last_save_values_after
@attribute_before_last_save_values_after ||= Hash.new([])
end

def will_save_change_to_attribute_values_before
@will_save_change_to_attribute_values_before ||= Hash.new([])
end
end

it 'respoduces ActiveRecord::AttributeMethods::Dirty behavior' do
subject = TestSCTAAndABLSInCallbacks.new(name: 'Name 1')
subject.save!
subject.age = 18
subject.save!
subject.name = 'Name 2'
subject.save!

expect(subject.saved_change_to_attribute_values_before).to eq(
{
:name => [nil, [nil, "Name 1"], nil],
:age => [nil, nil, [nil, 18]],
}
)
expect(subject.saved_change_to_attribute_values_after).to eq(
{
:name => [[nil, "Name 1"], nil, ["Name 1", "Name 2"]],
:age => [nil, [nil, 18], nil],
}
)
expect(subject.attribute_before_last_save_values_before).to eq(
{
:name => [nil, nil, "Name 1"],
:age => [nil, nil, nil]
}
)
expect(subject.attribute_before_last_save_values_after).to eq(
{
:name => [nil, "Name 1", "Name 1"],
:age => [nil, nil, 18]
}
)
expect(subject.will_save_change_to_attribute_values_before).to eq(
{
:name => [true, false, true],
:age => [false, true, false]
}
)
end
end
end
74 changes: 74 additions & 0 deletions spec/mongoid/changeable_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1661,6 +1661,80 @@
end
end

describe '#attribute_before_last_save' do
let(:person) do
Person.create!(title: "Grand Poobah")
end

before do
person.title = "Captain Obvious"
end

context "when the document has been saved" do
before do
person.save!
end

it "returns the changes" do
expect(person.attribute_before_last_save(:title)).to eq("Grand Poobah")
expect(person.title_before_last_save).to eq("Grand Poobah")
end
end

context "when the document has not been saved" do
it "returns no changes" do
expect(person.attribute_before_last_save(:title)).to be_nil
expect(person.title_before_last_save).to be_nil
end
end
end

describe '#saved_change_to_attribute' do
let(:person) do
Person.create!(title: "Grand Poobah")
end

before do
person.title = "Captain Obvious"
end

context "when the document has been saved" do
before do
person.save!
end

it "returns the changes" do
expect(person.saved_change_to_attribute(:title)).to eq(["Grand Poobah", "Captain Obvious"])
expect(person.saved_change_to_title).to eq(["Grand Poobah", "Captain Obvious"])
end
end

context "when the document has not been saved" do
it "returns changes for the previous save" do
expect(person.saved_change_to_attribute(:title)).to eq([nil, "Grand Poobah"])
expect(person.saved_change_to_title).to eq([nil, "Grand Poobah"])
end
end
end

describe '#will_save_change_to_attribute?' do
let(:person) do
Person.create!(title: "Grand Poobah")
end

before do
person.title = "Captain Obvious"
end

it 'correctly detects changes' do
expect(person.will_save_change_to_attribute?(:title)).to eq(true)
expect(person.will_save_change_to_title?).to eq(true)
expect(person.will_save_change_to_attribute?(:score)).to eq(false)
expect(person.will_save_change_to_score?).to eq(false)
end

end

context "when fields have been defined pre-dirty inclusion" do

let(:document) do
Expand Down

0 comments on commit 250b1b1

Please sign in to comment.