before_destroy callback that halts but also updates model? - ruby-on-rails

I am using a callback to halt a destroy action and instead want to set the :archived_at field. Problem is, the "return false" in my before_destroy causes a rollback that eliminates my update. Solutions to this?
class MarkArchived
def before_destroy(model)
update_attribute(:archived_at, Time.now) and return false
end
end
class User < ActiveRecord::Base
before_destroy MarkArchived
end
class Account < ActiveRecord::Base
before_destroy MarkArchived
end

If I understand correctly, you want some your objects (or some classes of objects) to be archived, never destroyed ? I think this gem might be suited for your use

Something like this?
def hide_or_destroy
if shifts.any?
update(hidden: true)
else
destroy
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.

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

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.

Test if Has One Relationship has Changed in 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

Rails after_create callback can't access model's attributes

I can't access my model's attributes in the after_create callback... seems like I should be able to right?
controller:
#dog = Dog.new(:color => 'brown', :gender => 'male')
#dog.user_id = current_user.id
#dog.save
model:
class Dog < ActiveRecord::Base
def after_create
logger.debug "[DOG CREATED] color:#{color} gender:#{gender} user:#{user_id}"
end
end
console: (all seems well)
>>Dog.last
=>#<Dog id: 1, color: "brown", gender: "male", user_id: 1>
log: (wtf!?)
...
[DOG CREATED] color: gender:male user
...
Some of my attributes show up and others don't! oh no! Anyone know what I'm doing wrong? I've always been able to user after_create in such ways in the past.
Note: The actual variable names and values I used were different, but the methods and code are the same.
Figured out my own problem.
One of the attributes was a virtual one, in which I used self.update_attribute...oops!
def price=(amt)
self.update_attribute(:price_in_cents, (amt*100.0).to_i)
end
So for the record, update_attribute will actually create database record (and trigger after_create) if it hasn't been created yet.
Next time I'll be sure to post full code!
Try this instead:
class Dog < ActiveRecord::Base
def after_create(dog)
logger.debug "[DOG CREATED] color:#{dog.color} gender:#{dog.gender} user:#{dog.user_id}"
end
end
Try using after_save instead of after_create may be that works. no tested though.
after_create () Is called after Base.save on new objects that haven‘t been saved yet (no record exists). Note that this callback is still wrapped in the transaction around save. For example, if you invoke an external indexer at this point it won‘t see the changes in the database.
The macro style callback is probably a better idea generally than simply overriding the method. It lets you fire a few different methods at the same time in the lifecycle (if you want). I think what you need is this:
class Dog < ActiveRecord::Base
after_create :dog_logger
def dog_logger
logger.debug "[DOG CREATED] color:#{self.color} gender:#{self.gender} user:#{self.user_id}"
end
end

Resources