Callback for Active Storage file upload - ruby-on-rails

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.

Related

Skip rails counter_cache update

I have a model that uses rails' built-in counter_cache association to increment/decrement counts. I have a requirement wherein I need to disable this when I destroy the model for a specific situation. I have tried to do something like Model.skip_callback(:destroy, :belongs_to_counter_cache_after_update) but it doesn't seem to work as expected (i.e it still ends up decrementing the associated model). Any helpful pointers would be appreciated.
One option is to temporarily override the method responsible for updating the cache count in case of destroy.
For example if you have following two models
class Category < ActiveRecord::Base
has_many :products
end
class Product < ActiveRecord::Base
belongs_to :category, counter_cache: true
end
Now you can try to find the methods responsible for updating cache count with following
2.1.5 :038 > Product.new.methods.map(&:to_s).grep(/counter_cache/)
This shows all the product instance methods which are related to counter_cache, with following results
=> ["belongs_to_counter_cache_before_destroy_for_category", "belongs_to_counter_cache_after_create_for_category", "belongs_to_counter_cache_after_update_for_category"]
From the names of the methods it shows that
"belongs_to_counter_cache_after_create_for_category"
might be responsible for counter cache update after destroy.
So I decided to temporarily override this method with one fake method which doesn't do anything(to skip counter cache update)
Product.class_eval do
def fake_belongs_to_counter_cache_before_destroy_for_category; end
alias_method :real_belongs_to_counter_cache_before_destroy_for_category, :belongs_to_counter_cache_before_destroy_for_category
alias_method :belongs_to_counter_cache_before_destroy_for_category, :fake_belongs_to_counter_cache_before_destroy_for_category
end
Now if you will destroy any product object, it will not update counter cache in Category table.
But its very important to restore the actual method after you have run your code to destroy specific objects. To restore to actual class methods you can do following
Product.class_eval do
alias_method :belongs_to_counter_cache_before_destroy_for_category, :real_belongs_to_counter_cache_before_destroy_for_category
remove_method :real_belongs_to_counter_cache_before_destroy_for_category
remove_method :fake_belongs_to_counter_cache_before_destroy_for_category
end
To ensure that the methods definitions always restored after your specific destroy tasks, you can write a class method, that will make sure to run both override and restore code
class Product < ActiveRecord::Base
belongs_to :category, counter_cache: true
def self.without_counter_cache_update_on_destroy(&block)
self.class_eval do
def fake_belongs_to_counter_cache_before_destroy_for_category; end
alias_method :real_belongs_to_counter_cache_before_destroy_for_category, :belongs_to_counter_cache_before_destroy_for_category
alias_method :belongs_to_counter_cache_before_destroy_for_category, :fake_belongs_to_counter_cache_before_destroy_for_category
end
yield
self.class_eval do
alias_method :belongs_to_counter_cache_before_destroy_for_category, :real_belongs_to_counter_cache_before_destroy_for_category
remove_method :real_belongs_to_counter_cache_before_destroy_for_category
remove_method :fake_belongs_to_counter_cache_before_destroy_for_category
end
end
end
Now if you destroy any product object as given following
Product.without_counter_cache_update_on_destroy { Product.last.destroy }
it will not update the counter cache in Category table.
References:
Disabling ActiveModel callbacks https://jeffkreeftmeijer.com/2010/disabling-activemodel-callbacks/
Temporary overriding methods: https://gist.github.com/aeden/1069124
You can create a flag to decide when callback should be run, something like:
class YourModel
attr_accessor :skip_counter_cache_update
def decrement_callback
return if #skip_counter_cache_update
# Run callback to decrement counter cache
...
end
end
so before you destroy your object of a Model, just set value for skip_counter_cache_update:
#object = YourModel.find(some_id)
#object.skip_counter_cache_update = true
#object.destroy
so it will not run decrement callback.

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.

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.

Is there a more direct way to do a pub/sub pattern in Rails than Observers?

I have a model which has a dependency on a separate, joined model.
class Magazine < ActiveRecord::Base
has_one :cover_image, dependent: :destroy, as: :imageable
end
class Image < ActiveRecord::Base
belongs_to :imageable, polymorphic: true
end
Images are polymorphic and can be attached to many objects (pages and articles) not just magazines.
The magazine needs to update itself when anything about its associated image changes
The magazine also stores a screenshot of itself that can be used for publicising it:
class Magazine < ActiveRecord::Base
has_one :cover_image, dependent: :destroy, as: :imageable
has_one :screenshot
def generate_screenshot
# go and create a screenshot of the magazine
end
end
Now if the image changes, the magazine also needs to update its screenshot. So the magazine really needs to know when something happens to the image.
So we could naively trigger screenshot updates directly from the cover image
class Image < ActiveRecord::Base
belongs_to :imageable, polymorphic: true
after_save { update_any_associated_magazine }
def update_any_associated_magazine
# figure out if this belongs to a magazine and trigger
# screenshot to regenerate
end
end
...however the image shouldn't be doing stuff on behalf of the magazine
However the image could be used in lots of different objects and really shouldn't be doing actions specific to the Magazine as it's not the Image's responsibility to worry about. The image might be attached to pages or articles as well and doesn't need to be doing all sorts of stuff for them.
The 'normal' rails approach is to use an observer
If we were to take a Rails(y) approach then we could create a third party observer that would then trigger an event on the associated magazine:
class ImageObserver < ActiveRecord::Observer
observe :image
def after_save image
Magazine.update_magazine_if_includes_image image
end
end
However this feels like a bit of a crappy solution to me.
We've avoided the Image being burdened by updating the magazine which was great but we've really just punted the problem downstream. It's not obvious that this observer exists, it's not clear inside the Magazine object that the update to the Image will in fact trigger an update to the magazine and we've got a weird floating object which has logic that really just belongs in Magazine.
I don't want an observer - I just want one object to be able to subscribe to events on another object.
Is there any way to subscribe to one model's changes directly from another?
What I would much rather do is have the magazine subscribe directly to events on the image. So the code would instead look like:
class Magazine < ActiveRecord::Base
...
Image.add_after_save_listener Magazine, :handle_image_after_save
def self.handle_image_after_save image
# determine if image belongs to magazine and if so update it
end
end
class Image < ActiveRecord::Base
...
def self.add_after_save_listener class_name, method
##after_save_listeners << [class_name, method]
end
after_save :notify_after_save_listeners
def notify_after_save_listeners
##after_save_listeners.map{ |listener|
class_name = listener[0]
listener_method = listener[1]
class_name.send listener_method
}
end
Is this a valid approach and if not why not?
This pattern seems sensible to me. It uses class variables and methods so doesn't make any assumptions of particular instances being available.
However, I'm old enough and wise enough now to know that if something seemingly obvious hasn't been done already in Rails there's probably a good reason for it.
This seems cool to me. What's wrong with it though? Why do all the other solutions I see all draft in a third party object to deal with things? Would this work?
I use Redis:
In an initializer I set up Redis:
# config/initializers/redis.rb
uri = URI.parse ENV.fetch("REDISTOGO_URL", 'http://127.0.0.1:6379')
REDIS_CONFIG = { host: uri.host, port: uri.port, password: uri.password }
REDIS = Redis.new REDIS_CONFIG
It'll default to my local redis installation in development but on Heroku it'll use Redis To Go.
Then I publish using model callbacks:
class MyModel < ActiveRecord::Base
after_save { REDIS.publish 'my_channel', to_json }
end
Then I can subscribe from anywhere, such as a controller I'm using to push events using Event Source
class Admin::EventsController < Admin::BaseController
include ActionController::Live
def show
response.headers["Content-Type"] = "text/event-stream"
REDIS.psubscribe params[:event] do |on|
on.pmessage do |pattern, event, data|
response.stream.write "event: #{event}\n"
response.stream.write "data: #{data}\n\n"
end
end
rescue IOError => e
logger.info "Stream closed: #{e.message}"
ensure
redis.quit
response.stream.close
end
end
Redis is great for flexible pub/sub. That code I have in the controller can be placed anywhere, let's say in an initializer:
# config/initializers/subscribers.rb
REDIS.psubscribe "image_update_channel" do |on|
on.pmessage do |pattern, event, data|
image = Image.find data['id']
image.imageable # update that shiz
end
end
Now that will handle messages when you update your image:
class Image < ActiveRecord::Base
belongs_to :imageable, polymorphic: true
after_save { REDIS.publish 'image_update_channel', to_json }
end
There is ActiveSupport Notifications mechanism for implementing pub/sub in Rails.
First, you should define instrument which will publish events:
class Image < ActiveRecord::Base
...
after_save :publish_image_changed
private
def publish_image_changed
ActiveSupport::Notifications.instrument('image.changed', image: self)
end
end
Then you should subscribe for this event (you can put this code in initializer):
ActiveSupport::Notifications.subscribe('image.changed') do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
image = event.payload[:image]
# If you have no other cases than magazine, you can check it when you publish event.
return unless image.imageable.is_a?(Magazine)
MagazineImageUpdater.new(image.imageable).run
end
I'll give it a shot...
Use public_send to notify the parent class of a change:
class BaseModel < ActiveRecord::Base
has_one :child_model
def respond_to_child
# now generate the screenshot
end
end
class ChildModel < ActiveRecord::Base
belongs_to :base_model
after_update :alert_base
def alert_base
self.base_model.public_send( :respond_to_child )
end
end

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

Resources