Cannot access children in after_create method in Rails 5 - ruby-on-rails

In my app I have bookings and passengers where one booking has many passengers.
After creating a new booking I want to send a notification to all passengers, so in my Booking model I have:
after_create :send_notification
def send_notification
self.passengers.each do |passenger|
#DO STUFF
end
end
This does not do anything and if I try
puts(self.passengers.count)
it returns 0.
When I user after_save everything works, so I assume that after_create it created the parent but not the children yet.
Problem is that I can't use after_save because this trigger also after updating.
Any ideas?

You can add condition for after_save:
after_save :send_notification, if: :id_changed?
# or :id_previously_changed? I don't remember if in after_save you can access dirty attributes

Change your callback to this:
after_save :send_notification, on: :create
So that your callback will fire after saving instead of creating the object, but will only occur when it's first created.

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.

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.

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

Why does after_save not trigger when using touch?

Recent days , I was trying to cache rails app use Redis store.
I have two models:
class Category < ActiveRecord::Base
has_many :products
after_save :clear_redis_cache
private
def clear_redis_cache
puts "heelllooooo"
$redis.del 'products'
end
end
and
class Product < ActiveRecord::Base
belongs_to :category, touch: true
end
in controller
def index
#products = $redis.get('products')
if #products.nil?
#products = Product.joins(:category).pluck("products.id", "products.name", "categories.name")
$redis.set('products', #products)
$redis.expire('products', 3.hour.to_i)
end
#products = JSON.load(#products) if #products.is_a?(String)
end
With this code , the cache worked fine.
But when I updated or created new product (I have used touch method in relationship) it's not trigger after_save callback in Category model.
Can you explain me why ?
Have you read documentation for touch method?
Saves the record with the updated_at/on attributes set to the current
time. Please note that no validation is performed and only the
after_touch, after_commit and after_rollback callbacks are executed.
If an attribute name is passed, that attribute is updated along with
updated_at/on attributes.
If you want after_save callbacks to be executed when calling touch on some model, you can add
after_touch :save
to this model.
If you set the :touch option to :true, then the updated_at or updated_on timestamp on the associated object will be set to the current time whenever this object is saved or destroyed
this the doc :
http://guides.rubyonrails.org/association_basics.html#touch
You can also (at least in rails 6) just use after_touch: callback
From the docs:
after_touch: Registers a callback to be called after a record is touched. See ActiveRecord::Callbacks for more information.

before_update syntax

post.rb Model
after_update :assign_owner
def assign_owner
self.owner = "test"
end
The above method works in terminal but does not change the value of Post.new.owner in Rails. What am I missing?
This is an after update (object needs to be saved) so
post = Post.new.save
Then
post.owner # will be test
If you wanna do this you may want to use after_initialize
for e.g in post.rb
class Post < ActiveRecord::Base
protected
def after_initialize
self.owner = "test
end
end
after_update only fires when you update your object. after_update will not call when you create.
You can use after_create callback when you want to call method on creating new object.
after_create :assign_owner
after_update :assign_owner
def assign_owner
self.owner = "test"
end
after_update and after_create are called after the object is saved. You do set the value of the owner but you don't save it.
Two possible options: use before_update instead --> your object is not yet saved and your change will be saved correctly.
Or use after_update and write it as follows:
def assign_owner
self.update_attribute :owner, "test"
end
Note: any callback will only be called right before or right after saving, so Post.new.owner will still be wrong. But Post.create(:context => 'blabla') should trigger it correctly (or Post.new.save).
Hope this helps.

Resources