I have some trouble to use after_destroy in model
Here is this one:
class Transaction < ActiveRecord::Base
belongs_to :user
delegate :first_name, :last_name, :email, to: :user, prefix: true
belongs_to :project
delegate :name, :thanking_msg, to: :project, prefix: true
validates_presence_of :project_id
after_save :update_collected_amount_in_project
after_update :update_collected_amount_if_disclaimer
after_destroy :update_collected_amount_after_destroy
def currency_symbol
currency = Rails.application.config.supported_currencies.fetch(self.currency)
currency[:symbol]
end
private
def update_collected_amount
new_collected_amount = project.transactions.where(success: true, transaction_type: 'invest').sum(:amount)
project.update_attributes(collected_amount: (new_collected_amount / 100).to_f) # Stored in € not cents inside projects table
end
def update_collected_amount_in_project
update_collected_amount if transaction_type == 'invest' && success == true
end
def update_collected_amount_if_disclaimer
update_collected_amount if transaction_type == 'invest' && self.changes.keys.include?('success') && self.changes.fetch('success', []).fetch(1) == false
end
def update_collected_amount_after_destroy
update_collected_amount
end
end
When I use something like:
Transaction.last.delete
It never go inside my after_destroy, I tried to include some outputs but nothing. I don't know if I'm wrong on how I use this after_destroy, I also tried before_destroy and I have the same problem. after_save and after_update work perfectly.
The after_destroy callbacks aren't called on delete. They are only called if you call destroy, like so:
Transaction.last.destroy
That's actually the only difference between the two methods. Delete bypasses the callbacks.
Delete also won't execute any :dependent association options.
The reason for this is that it never instantiates any of the active record objects that you are deleting, it simply executes an SQL delete statement against the database.
I think you can use rails-observer gem, that can help you in adding after_destroy callback
Related
I'm trying to use an AR callback (after_save) to check, if an association was added on the latest 'update' or 'create'. However, I can't seem to find the correct way to do it.
class Submission < ActiveRecord
has_many :notes, inverse_of: :submission, dependent: :destroy
accepts_nested_attributes_for :notes, :reject_if => proc { |attributes| attributes['message'].blank? }, allow_destroy: true
end
Here is my after_save
after_save :build_conversation
in that method I want to see, if a new note table was added on update or create...
def build_conversation
if self.notes.any?
binding.pry
end
end
This logic does not make sense, because if notes can exist, which is fine. Nevertheless, I only want to enter this block, if there is a new note added on update or create...
Check out this post. Basically, you add include ActiveModel::Dirty in the model, then in the after_change callback you check if_notes_changed. This method is defined using method_missing so for example if you have a name column you can use if_name_changed and so on. If you need to compare the old vs new values, you can use previous_changes.
Alternatively, you can use around_save like so:
around_save :build_conversation
def build_conversation
old_notes = notes.to_a # the self in self.notes is unnecessary
yield # important - runs the save
new_notes = notes.to_a
# binding.pry
end
I have two associated classes like this:
class Purchase < ActiveRecord::Base
has_many :actions
before_create do |p|
self.actions.build
end
end
class Action < ActiveRecord::Base
belongs_to :purchase
before_save do |a|
false
end
end
The block in the Action class prevents it from saving. I was thinking doing Purchase.create will fail because it cannot save the child object. But while it does not save the Action, it commits the Purchase. How can i prevent the parent object to be saved when there is an error in the child object?
It turns out you have to rollback the transaction explicitly, errors from the child objects does not propagate. So i ended up with:
class Purchase < ActiveRecord::Base
has_many :actions
after_create do |p|
a = Action.new(purchase: p)
if !a.save
raise ActiveRecord::Rollback
end
end
end
class Action < ActiveRecord::Base
belongs_to :purchase
before_save do |a|
false
end
end
Take note that i also changed the before_create callback to after_create. Otherwise, since belongs_to also causes the parent to be saved, you will get a SystemStackError: stack level too deep.
I ran into this problem when dealing with race conditions where the child objects would pass a uniqueness validation, but then fail the database constraint (when trying to save the parent object), leading to childless (invalid) parent objects in the database.
A slightly more general solution to the one suggested by #lunr:
class Purchase < ActiveRecord::Base
has_many :actions
after_save do
actions.each do |action|
raise ActiveRecord::Rollback unless action.save
end
end
end
class Action < ActiveRecord::Base
belongs_to :purchase
before_save do |a|
false
end
end
Try to use this code in Purchase class:
validate :all_children_are_valid
def all_children_are_valid
self.actions.each do |action|
unless action.valid?
self.errors.add(:actions, "aren't valid")
break
end
end
end
Or use validates_associated in Purchase class:
validates_associated :actions
If in your business logic you can't save purchase without any action, then add a presence validator on actions inside purchases
validates :actions, length: {minimum: 1}, presence: true
I have a model called Block that has a blocker_id (a user_id) and a blocked_user_id field (also a user_id). The Block model lets one user block another. When one user blocks another, I want it to destroy the Relationship between them using a before_save method for the Block class. The Relationship table has a follower_id and a followed_id.
This is where things get tricky. I know I could achieve this goal by using multiple return if Relationship.xyz.nil? statements and then using multiple Relationship.find_by(follower_id: , followed_id: ).destroy statements, but this gets to be way over complicated because each blocker and blocked_user could be either the follower and followed id, both, or neither. Is there any easier way to do this?
Here's my models for reference: (also the Block class has a blocked_post field, which I'm having no trouble with)
class Block < ActiveRecord::Base
validates :blocker_id, presence: true
validates :blocked_user_id, uniqueness: {scope: :blocker_id}, allow_nil: true
validates :blocked_post_id, uniqueness: {scope: :blocker_id}, allow_nil: true
validate :blocked_user_or_post
after_validation :validate_block
before_save :destroy_blocked_relationships
belongs_to(
:blocker,
class_name: "User"
)
has_one(
:blocked_user,
class_name: "User"
)
has_one(
:blocked_post,
class_name: "Post"
)
private
def blocked_user_or_post
blocked_user_id.blank? ^ blocked_post_id.blank?
end
def validate_block
if blocked_user_id.present?
!(blocker_id == blocked_user_id)
elsif blocked_post_id.present?
blocked_post = Post.find_by(id: self.blocked_post_id).user_id
!(blocker_id == blocked_post)
else
false
end
end
def destroy_blocked_relationships
#my over-complex code was here
end
end
relationship.rb:
class Relationship < ActiveRecord::Base
validates :follower_id, :followed_id, presence: {message: 'Need an eligible follower and followee id'}
validates :followed_id, uniqueness: { scope: :follower_id}
belongs_to(
:follower,
class_name: "User"
)
belongs_to(
:followed,
class_name: "User"
)
end
If there is any way to do this that doesn't require massive amounts of code, I'd really like to know. Thanks in advance.
I'm not sure of your exact use case, but my thoughts about a system where people can follow each other, it seems that the blocker would always be the person being followed. If this is the case, here's an implementation:
def destroy_blocked_relationships
Relationship.where(follower_id:blocked_user_id, followed_id:blocker_id).destroy_all
true
end
If it makes sense to also block someone from being being followed, you could add this:
Relationship.where(follower_id:blocker_id, followed_id:blocked_user_id).destory_all
Here it is all together, and stopping the save of the Block if there are no relationships:
before_save :destroy_blocked_relationships
def destroy_blocked_relationships
relationships = Relationship.where("(follower_id = ? AND followed_id = ?) OR (followed_id = ? AND follower_id = ? )", blocked_user_id, blocker_id, blocked_user_id, blocker_id)
relationships.destroy_all
relationships.present? # Omit this line if the save should continue regardless
end
here is my understanding:
a Block is a relationship between two users, OR between a User and a Post
when a Block is created between user A and Post X, an implicit block is also created between User A and User B, where User B is Post X's author
Consider making two models, BlockedPost and BlockedUser. Then, make two #make methods. This makes all the related logic easier to reason about.
# class BlockedPost
def make(user, post)
transaction do
create!(user: user, post: post)
BlockedUser.make(user, post.author)
end
end
# class BlockedUser
def make(user, blocked_user)
transaction do
create!(user: user, blocked_user: blocked_user)
Relationship.where(follower: user, following: blocked_user).destroy_all
Relationship.where(follower: blocked_user, following: user).destroy_all
end
end
I have an UnreadEntry model and am using an after_commit callback to send a notification to a pusher service. The problem is event fires just fine when adding records but when a delete_all is sent on the model, neither: after_commit, after_destroy are fired.
How can I add catch delete_all and add a callback to it?
class UnreadEntry < ActiveRecord::Base
belongs_to :user
belongs_to :feed
belongs_to :entry
after_commit :send_pusher_notification, if: PUSHER_ENABLED
validates_uniqueness_of :user_id, scope: :entry_id
def self.create_from_owners(user, entry)
create(user_id: user.id, feed_id: entry.feed_id, entry_id: entry.id, published: entry.published, entry_created_at: entry.created_at)
end
def self.sort_preference(sort)
if sort == 'ASC'
order("published ASC")
else
order("published DESC")
end
end
def send_pusher_notification(user = nil, from = 'UnreadEntry#callback')
if user.nil?
unread_count = UnreadEntry.where(user_id: self.user_id).count
else
unread_count = UnreadEntry.where(user_id: user.id).count
end
Pusher['rssapp'].trigger('unread_count', {
message: unread_count
})
end
end
Simple - don't use delete_all. delete_all and update_all are specifically designed to query the database directly, bypassing the ActiveRecord model logic - including validations and callbacks. If you want callbacks, call destroy on each model instance individually.
my_collection.each(&:destroy)
I have two models Ticket and TicketComment, the TicketComment is a child of Ticket.
ticket.rb
class Ticket < ActiveRecord::Base
has_many :ticket_comments, :dependent => :destroy, :order => 'created_at DESC'
# allow the ticket comments to be created from within a ticket form
accepts_nested_attributes_for :ticket_comments, :reject_if => proc { |attributes| attributes['comment'].blank? }
end
ticket_comment.rb
class TicketComment < ActiveRecord::Base
belongs_to :ticket
validates_presence_of :comment
end
What I want to do is mimic the functionality in Trac, where if a user makes a change to the ticket, and/or adds a comment, an email is sent to the people assigned to the ticket.
I want to use an after_update or after_save callback, so that I know the information was all saved before I send out emails.
How can I detect changes to the model (ticket.changes) as well as whether a new comment was created or not (ticket.comments) and send this update (x changes to y, user added comment 'text') in ONE email in a callback method?
you could use the ActiveRecord::Dirty module, which allows you to track unsaved changes.
E.g.
t1 = Ticket.first
t1.some_attribute = some_new_value
t1.changed? => true
t1.some_attribute_changed? => true
t1.some_attribute_was => old_value
So inside a before_update of before_create you should those (you can only check before the save!).
A very nice place to gather all these methods is in a Observer-class TicketObserver, so you can seperate your "observer"-code from your actual model.
E.g.
class TicketObserver < ActiveRecord::Observer
def before_update
.. do some checking here ..
end
end
to enable the observer-class, you need to add this in your environment.rb:
config.active_record.observers = :ticket_observer
This should get you started :)
What concerns the linked comments. If you do this:
new_comment = ticket.ticket_comments.build
new_comment.new_record? => true
ticket.comments.changed => true
So that would be exactly what you would need. Does that not work for you?
Note again: you need to check this before saving, of course :)
I imagine that you have to collect the data that has changed in a before_create or before_update, and in an after_update/create actually send the mail (because then you are sure it succeeded).
Apparently it still is not clear. I will make it a bit more explicit. I would recommend using the TicketObserver class. But if you want to use the callback, it would be like this:
class Ticked
before_save :check_state
after_save :send_mail_if_needed
def check_state
#logmsg=""
if ticket_comments.changed
# find the comment
ticket_comments.each do |c|
#logmsg << "comment changed" if c.changed?
#logmsg << "comment added" if c.new_record?
end
end
end
end
def send_mail_if_needed
if #logmsg.size > 0
..send mail..
end
end