stack level too deep after_save callback in Rails4 - ruby-on-rails

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

Related

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.

Update another column if specific column exist on updation

I have a model which have two columns admin_approved and approval_date. Admin update admin_approved by using activeadmin. I want when admin update this column approval_date also update by current_time.
I cant understand how I do this.Which call_back I use.
#app/models/model.rb
class Model < ActiveRecord::Base
before_update 'self.approval_date = Time.now', if: "admin_approved?"
end
This assumes you have admin_approved (bool) and approval_date (datetime) in your table.
The way it works is to use a string to evaluate whether the admin_approved attribute is "true" before update. If it is, it sets the approval_date to the current time.
Use after_save callback inside your model.
It would be something like this:
after_save do
if admin_approved_changed?
self.approval_date = Time.now
save!
end
end
Or change the condition as you like!
You could set the approval_date before your model instance will be saved. So you save a database write process instead of usage of after_save where you save your instance and in the after_save callback you would save it again.
class MyModel < ActiveRecord::Base
before_save :set_approval_date
# ... your model code ...
private
def set_approval_date
if admin_approved_changed?
self.approval_date = Time.now
end
end
end
May be in your controller:
my_instance = MyModel.find(params[:id])
my_instance.admin_approved = true
my_instance.save

In Rails can I save a record without it calling my callback?

When I do my_record.save can I pass a parameter or something to tell the record to not call it's Callback? Below is the callback I have set for my object.
class Measurable < ActiveRecord::Base
after_save :summarize_measurables_for_player
# ...
def summarize_measurables_for_player
# ...
end
end
Edit
This callback is used for when someone changes a value on measurable, then it calculates the preferred value for the Measurable_Type and then it stores that value on a column of another object. This allows me to retrieve the information much faster. I however, don't want this to be called when I import information. Because it would then summarize after each change. It would be a faster process to import all the information and then summarize all the values at once I would think.
In your case, I'd add an attr_accessor to the model and skip this method if set to true.
class Measurable < ActiveRecord::Base
after_save :summarize_measurables_for_player, :unless => :skip_summarize
attr_accessor :skip_summarize
# ...
def summarize_measurables_for_player
# ...
end
end
Then in the import, you can set :skip_summarize => true in the attributes of the imported object.

Run a callback only if an attribute has changed in 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

Rails model with array field, modify push method

I've been trying to find an answer to my question but so far no luck.
I have a model with an array field and I'd like method calls to happen when something gets pushed into the array.
class Shop::Order
include Mongoid::Document
include Mongoid::Timestamps
embeds_many :items,class_name: 'Shop::OrderItem', inverse_of: :order
accepts_nested_attributes_for :items
field :price, type: Money, default: Money.new(0)
field :untaxed_price, type: Money, default: Money.new(0)
end
So when doing order.items << Shop::OrderItem.new(...)
I'd like a method foo to be called.
EDIT: Add reason
So the reason for this is that I want to update the price and untaxed_price of an order each time an item is added to it.
Does it need to happen as soon as you push it? Or can it happen before you save the order? If you can wait until you save, you can do this:
before_validate :update_tax_info
def update_tax_info
if items_changed?
calculate_tax #whatever that may be
end
end
Throwing it in a validation would allow that callback to be called without saving. You could call #order.valid? to update the tax information.
I think monkey patching << is a bad idea. I have two ideas:
Use some kind of observer which listens on create of OrderItem and performs appropriate action
Overwrite OrderItem.create method (or even better provide abstraction):
```
class OrderItem
def add(params)
if create(params)
calculate_something
end
end
end
```
This documentation gives you the range of choices that you have to implement the desired behavior: http://mongoid.org/en/mongoid/docs/callbacks.html
To paraphrase, you have the option of using callbacks like before_save and before_update to do your calculations, or you can implement an Observer class to do this for you.
You can also use the changed method to see if the items array has changed and whether you need to update the derived fields.
Here's some example code:
class OrderObserver < Mongoid::Observer
def before_save(order)
do_something
end
end
Do remember to instantiate your observer in application.rb using:
config.mongoid.observers = :order_observer

Resources