Test if Has One Relationship has Changed in Rails - ruby-on-rails

Is it possible to have a 'before_save' callback that detects if a 'has_one' relationship has changed (the relationship not the model at the end of the relationship)? For example, something that would act like this:
#person.picture = #picture
#person.picture_changed? # true
#person.save
#person.picture_changed? # false

Maybe it works with
#person.picture_id_changed?

Try relation.changed?... You may also want to look into observers, depending on what you are trying to accomblish.
Example:
class Model < ActiveRecord::Base
has_one :relation
before_save :check_relation_changed
private def check_relation_changed
do_something if relation.changed?
end
end
Ref: http://ryandaigle.com/articles/2008/3/31/what-s-new-in-edge-rails-dirty-objects

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.

Rails association callback with cached query

I have two models:
class Basket < ActiveRecord::Base
has_many :products
def recalculate_price!
price = products.map(&:price).sum
save
end
end
class Product < ActiveRecord::Base
belongs_to :basket
after_save :update_basket
def update_basket
basket.recalculate_price!
end
end
and than I call it like this:
basket = Basket.new
basket.products.build(price)
basket.save
The problem: basket.price don't update.
I've investigated the problem and found out that there is some kind of caching in update_basket method. The problem could be solved with placing reload before basket.recalculate_price!.
Is there any option to leave the update_basket method untouched? I have many such cases in my application.
I would like to understand how it works and avoid similar problems in the future.
note: I recently upgraded rails from 3.2 to 4.2. In a previous version everything worked fine.
Is there any option to leave the update_basket method untouched?
Absolutely. You can place a condition on your after_create to avoid executing it some times:
after_save :update_basket if: :basked_should_be_updated
[...]
private
def basked_should_be_updated
[...]
# return true or false in here
end

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

Attribute that indicates is activerecord model in saving state?

I need to know if model is in saving state (between before_validate and after_save).
class ModelA < ActiveRecord::Base
before_save: do_before
def do_before
modelb.create(:attr => 123, :ref => self)
end
end
class ModelB < ActiveRecord::Base
before_create: do_before
def do_before
self.ref.my_attr = 321
self.ref.save! unless self.ref.is_saving?
end
end
I need "is_saving?" attribute for every model instance in my project. What is the best way to implement that?
Can't you rather set an instance variable in between :before_save and :after_save by using ActiveRecord's :around_save callback and then yield your save? Anyway, the question is not too clear to me. What is the purpose of is_saving? method?

How can I make sure a method I put in my model is accessible from outside?

Let's say I have a simple model:
class Subscription < ActiveRecord::Base
attr_accessor :subscribed
def subscribed
#subscribed = Subscription.where(task_id, user_id)
!#subscribed.empty?
end
end
I'd like to make sure I can take a #subscription instance and call #subscription.subscribed - at the moment it can't find the method.
I think you are doing something like:
#subscription = Subscription.where(task_id, user_id)
#subscription.subscribed
instead of something like this:
#subscription = Subscription.where(task_id, user_id).first
#subscription.subscribed
It's just a guess.
I believe the problem is your where call. Try this:
class Subscription < ActiveRecord::Base
belongs_to :task
belongs_to :user
attr_accessor :subscribed
def subscribed?
#subscribed ||= !Subscription.where(:task_id => task.id, :user_id => user.id).empty?
end
end
If I'm correctly understanding what you're trying to do I think that should work.
But it still doesn't make a lot of sense to me to have this method on the Subscription model. If a subscription exists wouldn't that mean that the user is subscribed automatically. Was this meant to be a class method mabye? I'd think user.subscribed_to? issue or Issue.has_subscriber? user would be a better place to handle this.
If I understand your problem, you can do:
class Subscription < ActiveRecord::Base
attr_accessor :subscribed
def subscribed?
#subscribed = Subscription.where(task_id, user_id)
!#subscribed.empty?
end
end
and you can do (a.i):
s = Subscription.first
s.subscribed? # true or false
s.subscribed # the object (#subscribed)
From your error in the comments, your object called #subscription is not pointing to an instance of Subscription but instead a relation.
Probably you did something like
#subscription = Subscription.where(... some condition ...)
which returns a relation, which you can access as an array. So call .first to get the first element out of it. Or use Subscription.find.
Hope this helps.

Resources