Tracking model changes in after_commit :on => :create callback - ruby-on-rails

Is there a way to track changes to model on after_commit when a record is created? I have tried using dirty module and was able to track changes when the record was updated, but when record is created changes are not recorded.

You can't use the rails changed? method, as it will always return false. To track changes after the transaction is committed, use the previous_changes method. It will return a hash with attribute name as key. You can can then check if your attribute_name is in the hash:
after_commit :foo
def foo
if previous_changes[attribute_name]
#do your task
end
end

Related

Update record in before_destroy and prevent its destruction

I am using Rails 3.2, Ruby 1.9.3.
I need to prevent a record from being destroyed and update it in the before_destroy callback.
Given two classes with the following associations
class Course:
has_many :attendants
...
class Attendant:
belongs_to :course
before_destroy :dont_really_destroy
...
I got a before_destroy callback in Attendant:
def dont_really_destroy
update_attribute :deleted_at, Time.now
false
end
The callback does in fact prevent the delete when I call the destroy method. However, the record is not updated. It seemed reasonable since I by returning false I might be aborting any update (I tried with update_column as well). However, somehow, it does work as expected when the attendant record is "destroyed" from its association's (Course) form, by setting a _destroy form element. The record is correctly updated with deteled_at set, and not destroyed.
I've tried debugging to see the if the instances are different when I try to destroy from the course form vs directly destroying the attendant but I cannot see any differences.
When I do it via the course form, the record is updated like this:
course.assign_attributes(params[:course], :without_protection => true)
...
course.save
Hi instead of using callbacks here, why don't you simply update the destroy action?
#AttendantsController.rb
def destroy
update_attribute :deleted_at, Time.now
head :ok
end
And you default scope in your model
class Attendant:
belongs_to :course
default_scope -> { where(deleted_at: nil) }
...
Hope this will help you.
You could also use ActAsParanoid Gem which introduces soft deletion for rails.
https://github.com/ActsAsParanoid/acts_as_paranoid

Rails model set boolean when record updated

I have model that I want to set a boolean to false whenever it is changed. Taht way I can reprocess it later to make sure the related records are up to date. I've tried a few things, but keep getting myself into a loop.
/app/model/contest.rb
class Contest < ActiveRecord::Base
has_many :results, dependent: :destroy
after_update :set_contest_not_updated
def set_contest_not_updated
self.contest_updated=false
self.save
end
def set_contest_updated
self.update_column(:contest_updated_at, Time.now)
self.update_column(:contesy_updated, true)
end
Expected action:
contest.update_atrributes(flag_that_effects_scoring: true)
I would expect that is the above is run, the contest.contest_updated boolean would be set to false and only be set to true when the contest.set_contest_updated() method is run.
Of course calling save in the after_update callback will get you into a loop. It keeps saving over and over again until the end of time.
However, before_update should get the job done.
PS: Don't call save on *_save and *_update callbacks. This will always get you into loops.
before_update :set_contest_not_updated
def set_contest_not_updated
self.contest_updated = false
end
This will never work as when your after_update is called, it will invoke another after_update call and you will get the stack level too deep message.
The best solution is to have a foriegn key in another table/model such as contest_updates.
If you want your solution however, set the flag in a before_update filter

Rails, using the dirty or changed? flag with after_commit

I heard rails has a dirty/change flag. Is it possible to use that in the after_commit callback?
In my user model I have:
after_commit :push_changes
In def push_changes I would like a way to know if the name field changed. Is that possible?
You can use previous_changes in after_commit to access a model's attribute values from before it was saved.
see this post for more info:
after_commit for an attribute
You can do a few things to check...
First and foremost, you can check an individual attribute as such:
user = User.find(1)
user.name_changed? # => false
user.name = "Bob"
user.name_changed? # => true
But, you can also check which attributes have changed in the entire model:
user = User.find(1)
user.changed # => []
user.name = "Bob"
user.age = 42
user.changed # => ['name', 'age']
There's a few more things you can do too - check out http://api.rubyonrails.org/classes/ActiveModel/Dirty.html for details.
Edit:
But, given that this is happening in an after_commit callback, the model has already been saved, meaning knowledge of the changes that occurred before the save are lost. You could try using the before_save callback to pick out the changes yourself, store them somewhere, then access them again when using after_commit.
Since Rails 5.1, in after_commit you should use saved_change_to_attribute?
Ref: Rails 5.1.1 deprecation warning changed_attributes

How to detect attribute changes from model?

I'd like to create a callback function in rails that executes after a model is saved.
I have this model, Claim that has a attribute 'status' which changes depending on the state of the claim, possible values are pending, endorsed, approved, rejected
The database has 'state' with the default value of 'pending'.
I'd like to perform certain tasks after the model is created on the first time or updated from one state to another, depending on which state it changes from.
My idea is to have a function in the model:
after_save :check_state
def check_state
# if status changed from nil to pending (created)
do this
# if status changed from pending to approved
performthistask
end
My question is how do I check for the previous value before the change within the model?
You should look at ActiveModel::Dirty module:
You should be able to perform following actions on your Claim model:
claim.status_changed? # returns true if 'status' attribute has changed
claim.status_was # returns the previous value of 'status' attribute
claim.status_change # => ['old value', 'new value'] returns the old and
# new value for 'status' attribute
claim.name = 'Bob'
claim.changed # => ["name"]
claim.changes # => {"name" => ["Bill", "Bob"]}
Oh! the joys of Rails!
you can use this
self.changed
it return an array of all columns that changed in this record
you can also use
self.changes
which returns a hash of columns that changed and before and after results as arrays
For Rails 5.1+, you should use active record attribute method: saved_change_to_attribute?
saved_change_to_attribute?(attr_name, **options)`
Did this attribute change when we last saved? This method can be
invoked as saved_change_to_name? instead of
saved_change_to_attribute?("name"). Behaves similarly to
attribute_changed?. This method is useful in after callbacks to
determine if the call to save changed a certain attribute.
Options
from When passed, this method will return false unless the original
value is equal to the given option
to When passed, this method will return false unless the value was
changed to the given value
So your model will look like this, if you want to call some method based on the change in attribute value:
class Claim < ApplicationRecord
after_save :do_this, if: Proc.new { saved_change_to_status?(from: nil, to: 'pending') }
after_save :do_that, if: Proc.new { saved_change_to_status?(from: 'pending', to: 'approved') }
def do_this
..
..
end
def do_that
..
..
end
end
And if you don't want to check for value change in callback, you can do the following::
class Claim < ApplicationRecord
after_save: :do_this, if: saved_change_to_status?
def do_this
..
..
end
end
I recommend you have a look at one of the available state machine plugins:
acts_as_state_machine
alter_ego
Either one will let you setup states and transitions between states. Very useful and easy way of handling your requirements.
I've seen the question rise in many places, so I wrote a tiny rubygem for it, to make the code a little nicer (and avoid a million if/else statements everywhere): https://github.com/ronna-s/on_change.
I hope that helps.
You will be much better off using a well tested solution such as the state_machine gem.

Rails: check if the model was really saved in after_save

ActiveRecord use to call after_save callback each time save method is called even if the model was not changed and no insert/update query spawned.
This is the default behaviour actually. And that is ok in most cases.
But some of the after_save callbacks are sensitive to the thing that if the model was actually saved or not.
Is there a way to determine if the model was actually saved in the after_save?
I am running the following test code:
class Stage < ActiveRecord::Base
after_save do
pp changes
end
end
s = Stage.first
s.name = "q1"
s.save!
ActiveRecord use to call after_save
callback each time save method is
called even if the model was not
changed and no insert/update query
spawned.
ActiveRecord executes :after_save callbacks each time the record is successfully saved regardless it was changed.
# record invalid, after_save not triggered
Record.new.save
# record valid, after_save triggered
r = Record.new(:attr => value)
# record valid and not changed, after_save triggered
r.save
What you want to know is if the record is changed, not if the record is saved.
You can easily accomplish this using record.changed?
class Record
after_save :do_something_if_changed
protected
def do_something_if_changed
if changed?
# ...
end
end
end
After a save, check to see if the object saved is a new object
a = ModelName.new
a.id = 1
a.save
a.new_record?
#=> false
For anyone else landing here, latest rails 5 has a model method saved_changes? and a corresponding method saved_changes which shows those changes.
book = Book.first
book.saved_changes?
=> false
book.title = "new"
book.save
book.saved_changes?
=> true
book.saved_changes
=> {"title" => ["old", "new"]},

Resources