Run a callback only if an attribute has changed in Rails - ruby-on-rails

I have the following association in my app:
# Page
belongs_to :status
I want to run a callback anytime the status_id of a page has changed.
So, if page.status_id goes from 4 to 5, I want to be able to catch that.
How to do so?

Rails 5.1+
class Page < ActiveRecord::Base
before_save :do_something, if: :will_save_change_to_status_id?
private
def do_something
# ...
end
end
The commit that changed ActiveRecord::Dirty is here: https://github.com/rails/rails/commit/16ae3db5a5c6a08383b974ae6c96faac5b4a3c81
Here is a blog post on these changes: https://www.fastruby.io/blog/rails/upgrades/active-record-5-1-api-changes
Here is the summary I made for myself on the changes to ActiveRecord::Dirty in Rails 5.1+:
ActiveRecord::Dirty
https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Dirty.html
Before Saving (OPTIONAL CHANGE)
After modifying an object and before saving to the database, or within the before_save filter:
changes should now be changes_to_save
changed? should now be has_changes_to_save?
changed should now be changed_attribute_names_to_save
<attribute>_change should now be <attribute>_change_to_be_saved
<attribute>_changed? should now be will_save_change_to_<attribute>?
<attribute>_was should now be <attribute>_in_database
After Saving (BREAKING CHANGE)
After modifying an object and after saving to the database, or within the after_save filter:
saved_changes (replaces previous_changes)
saved_changes?
saved_change_to_<attribute>
saved_change_to_<attribute>?
<attribute>_before_last_save
Rails <= 5.0
class Page < ActiveRecord::Base
before_save :do_something, if: :status_id_changed?
private
def do_something
# ...
end
end
This utilizes the fact that the before_save callback can conditionally execute based on the return value of a method call. The status_id_changed? method comes from ActiveModel::Dirty, which allows us to check if a specific attribute has changed by simply appending _changed? to the attribute name.
When the do_something method should be called is up to your needs. It could be before_save or after_save or any of the defined ActiveRecord::Callbacks.

The attribute_changed? is deprecated in Rails 5.1, now just use will_save_change_to_attribute?.
For more information, see this issue.

Try this
after_validation :do_something, if: ->(obj){ obj.status_id.present? and obj.status_id_changed? }
def do_something
# your code
end
Reference - http://apidock.com/rails/ActiveRecord/Dirty

Related

How to determine changed attributes on after_save callback

I can't catch the changed attributes on after_save callback but I can catch them on after_update callback. I think after_save is just a combination of after_create and after_update. I'd appreciate it if someone give me at least a hint.
class Student < ApplicationRecord
after_save: after_save_callback
def after_save_callback
if username_changed? ### This is always false
# do something
end
end
end
student = Student.create(name: 'John Doe', username: 'abcdef')
student.username = '123456'
student.save
My Rails version is 5.0.7.
You can use saved_change_to_username?
The behavior of attribute_changed? inside of after callbacks will be changing in the next version of Rails. The new return value will reflect the behavior of calling the method after save returned (e.g. the opposite of what it returns now). To maintain the current behavior, use saved_change_to_attribute? instead.

Callback for Active Storage file upload

Is there a callback for active storage files on a model
after_update or after_save is getting called when a field on the model is changed. However when you update (or rather upload a new file) no callback seems to be called?
context:
class Person < ApplicationRecord
#name :string
has_one_attached :id_document
after_update :call_some_service
def call_some_service
#do something
end
end
When a new id_document is uploaded after_update is not called however when the name of the person is changed the after_update callback is executed
For now, it seems like there is no callback for this case.
What you could do is create a model to handle the creation of an active storage attachment which is what is created when you attach a file to your person model.
So create a new model
class ActiveStorageAttachment < ActiveRecord::Base
after_update :after_update
private
def after_update
if record_type == 'Person'
record.do_something
end
end
end
You normally have created the model table already in your database so no need for a migration, just create this model
Erm i would just comment but since this is not possible without rep..
Uelb's answer works but you need to fix the error in comments and add it as an initializer instead of model. Eg:
require 'active_storage/attachment'
class ActiveStorage::Attachment
before_save :do_something
def do_something
puts 'yeah!'
end
end
In my case tracking attachment timestamp worked
class Person < ApplicationRecord
has_one_attached :id_document
after_save do
if id_document.attached? && (Time.now - id_document.attachment.created_at)<5
Rails.logger.info "id_document change detected"
end
end
end
The answer from #Uleb got me 90% of the way, but for completion sake I will post my final solution.
The issue I had was that I was not able to monkey patch the class (not sure why, even requiring the class as per #user10692737 did not help)
So I copied the source code (https://github.com/rails/rails/blob/fc5dd0b85189811062c85520fd70de8389b55aeb/activestorage/app/models/active_storage/attachment.rb#L20)
and modified it to include the callback
require "active_support/core_ext/module/delegation"
# Attachments associate records with blobs. Usually that's a one record-many blobs relationship,
# but it is possible to associate many different records with the same blob. If you're doing that,
# you'll want to declare with <tt>has_one/many_attached :thingy, dependent: false</tt>, so that destroying
# any one record won't destroy the blob as well. (Then you'll need to do your own garbage collecting, though).
class ActiveStorage::Attachment < ActiveRecord::Base
self.table_name = "active_storage_attachments"
belongs_to :record, polymorphic: true, touch: true
belongs_to :blob, class_name: "ActiveStorage::Blob"
delegate_missing_to :blob
#CUSTOMIZED AT THE END:
after_create_commit :analyze_blob_later, :identify_blob, :do_something
# Synchronously purges the blob (deletes it from the configured service) and destroys the attachment.
def purge
blob.purge
destroy
end
# Destroys the attachment and asynchronously purges the blob (deletes it from the configured service).
def purge_later
blob.purge_later
destroy
end
private
def identify_blob
blob.identify
end
def analyze_blob_later
blob.analyze_later unless blob.analyzed?
end
#CUSTOMIZED:
def do_something
end
end
Not sure its the best method, and will update if I find a better solution
None of these really hit the nail on the head, but you can achieve what you were looking for by following this blog post https://redgreen.no/2021/01/25/active-storage-callbacks.html
I was able to modify the code there to work on attachments instead of blobs like this
Rails.configuration.to_prepare do
module ActiveStorage::Attachment::Callbacks
# Gives us some convenient shortcuts, like `prepended`
extend ActiveSupport::Concern
# When prepended into a class, define our callback
prepended do
after_commit :attachment_changed, on: %i[create update]
end
# callback method
def attachment_changed
record.after_attachment_update(self) if record.respond_to? :after_attachment_update
end
end
# After defining the module, call on ActiveStorage::Blob to prepend it in.
ActiveStorage::Attachment.prepend ActiveStorage::Attachment::Callbacks
end
What I do is add a callback on my record:
after_touch :check_after_touch_data
This gets called if an ActiveStorage object is added, edited or deleted. I use this callback to check if something changed.

How do I invoke a method only when my object (model) is first created in Rails 5?

I'm using Rails 5. I want a method invoked on my model only when the model is first created. I have tried this ...
class UserSubscription < ApplicationRecord
belongs_to :user
belongs_to :scenario
def self.find_active_subscriptions_by_user(user)
UserSubscription.joins(:scenario)
.where(["user_id = ? and start_date < NOW() and end_date > NOW()", user.id])
end
after_initialize do |user_subscription|
self.consumer_key = SecureRandom.urlsafe_base64(10)
self.consumer_secret = SecureRandom.urlsafe_base64(25)
end
end
but I noticed this gets called every tiem I retrieve a model from a finder method in addition to its begin created. How can I create such functionality in my model?
You want to use after_create (from active record docs) or after_create_commit which was introduced in Rails 5 as a shortcut for after_commit :hook, on: :create.
after_create always executes after the transactions block whereas after_create_commit does so after the commit but within the same transactions block. These details likely don't matter here, but it's a new capability if you need that extra control for ensuring the model state is correct before you execute the after call.
Pyrce's answer is good. Another way is to keep the after_initialize method but only run if it's a new record:
after_initialize :set_defaults
def set_defaults
if self.new_record?
self.consumer_key = SecureRandom.urlsafe_base64(10)
self.consumer_secret = SecureRandom.urlsafe_base64(25)
end
end
(It's generally considered better to not override the after_initialize method. Instead provide the name of a method to run, as I did above.

stack level too deep after_save callback in Rails4

My model is like this,
class Slot
include Mongoid::Document
after_save :calculate_period
field :slot, type: Array
def calculate_period
if condition
do something
end
self.slot = true
save
end
end
After submit button it will show this error,
SystemStackError in SlotsController#create
stack level too deep
and also consuming more time. If i remove the save from def calculate_period then the values are not storing after_save callback.
Any solution...!!!
You should change this to before_save, that way you can change the model's attributes, and then they will be saved to the database as normal.
class Slot
include Mongoid::Document
before_save :calculate_period
def calculate_period
if condition
#do something
end
end
end
You have infinite loop - calling save in calculate_period method invokes callbacks, including your calculate_period callback. The first solution that came into my mind is to add virtual attribute and check it before calling your callback method:
class Slot
include Mongoid::Document
after_save :calculate_period, unless: :period_calculated # I'm not sure if Mongoid allows this
attr_accessor :period_calculated
def calculate_period
if condition
# do something
end
self.period_calculated = true
save
end
end

why update_attributes is not calling before_validation call back in my model

I am using rails 2.3.8 version is there any particular reason why update_attributes doesn't trigger before_validation callback
class x < ActiveRecord::Base
before_validation :testing
def testing
debugger
end
end
x.update_attributes(:name => "hello") doesn't trigger before_validation
The answer will be very clear to you if you Googled this method: here is the documentation which clearly states that update_attribute does not go through the validation process. You should use update_attributes instead if you want to trigger validations.

Resources