I've got a Match model and a Team model.
I want to run an instance method (written inside the Team model) after a Match has been saved. Here's what I've got.
team.rb
def goals_sum
unless goal_count_cache
goal_count = a_goals_sum + b_goals_sum
update_attribute(:goal_count_cache, goal_count)
end
goal_count_cache
end
and it works. Now I need to run this whenever a match gets saved. So I tried this:
match.rb
after_save :Team.goals_sum
after_destroy :Team.goals_sum
And it doesn't work.
I know I'm missing something basic, but I still can't go through with it. Any tips?
You can just define a private method on Match that delegates to the method on Team (otherwise, how would it know which team to run the method on? You say it's an instance method, and I assume a match has teams that are playing it).
after_save :update_teams_goals_sum
after_destroy :update_teams_goals_sum
private
def update_teams_goals_sum
[team_a, team_b].each &:goals_sum
end
after_save :notify_team
after_destroy :notify_team
private
def notify_team
Team.goals_sum
end
Related
I am trying to update a rails record if a duplicate exists as follow:
class Rating < ActiveRecord::Base
before_create :update_rating_if_already_exists
def update_rating_if_already_exists
original_rating = Rating.where(user: self.user, article: self.article)
if original_rating.blank?
true
else
original_rating[0].update_attribute(:score, self.score)
false
end
end
end
the problem however is that when I use after_create as above, this will not work for normal usage from controller action, as the controller will first build a new instance of the class with given params and then save (not create) the new object.
However, if I change the above to an after_save, it takes care of the controller problem, but then leads to another problem:
original_rating[0].update_attribute(:score, self.score)
will try to update the original record with a save method, which will also trigger the before_save...and on, and on... and this leads to a SystemStackError: stack level too deep error.
This is the dilemma now and my question is how could I go about this?
Thanks for all contributions. :)
You can use before_save with a new_record? condition to avoid executing the callback on an update_attribute call.
before_save :update_rating_if_already_exists, if: :new_record?
def update_rating_if_already_exists
# method code
end
I have the following classes:
class AwardBase
class AwardOne < AwardBase
class Post < ActiveRecord::Base
The Post is an ActiveRecord, and the Award has a can_award? class method which takes a post object and checks to see if it meets some criteria. If yes, it updates post.owner.awards.
I know I can do this using an Observer pattern (I tested it and the code works fine). However, that requires me to add additional code to the model. I'd like not to touch the model at all if possible. What I'd like to do is run the Award checks like this (the trigger will be invoked at class load time):
class AwardOne < AwardBase
trigger :post, :after_save
def self.can_award?(post)
...
end
end
The intention with the above code is that it should automatically add AwardOne.can_award? to Post's after_save method
So essentially what I'm trying to do is to get the trigger call be equivalent to:
class Post < ActiveRecord::Base
after_save AwardOne.can_award?(self)
...
end
which is basically:
class Post < ActiveRecord::Base
after_save :check_award
def check_award
AwardOne.can_award?(self)
end
end
How can I do this without modifying the Post class?
Here's what I've done (which does not appear to work):
class AwardBase
def self.trigger (klass, active_record_event)
model_class = klass.to_class
this = self
model_class.instance_eval do
def award_callback
this.can_award?(self)
end
end
model_class.class_eval do
self.send(active_record_event, :award_callback)
end
end
def self.can_award? (model)
raise NotImplementedError
end
end
The above code fails with the error:
NameError (undefined local variable or method `award_callback' for #<Post:0x002b57c04d52e0>):
You should think about why you want to do it this way. I would argue it is even worse than using the observer pattern. You are violating the principle of least surprise (also called principle of least astonishment).
Imagine that this is a larger project and I come as a new developer to this project. I am debugging an issue where a Post does not save correctly.
Naturally, I will first go through the code of the model. I might even go through the code of the posts controller. Doing that there will be no indication that there is a second class involved in saving the Post. It would be much harder for me to figure out what the issue is since I would have no idea that the code from AwardOne is even involved.
In this case it would actually be most preferable to do this in the controller. It is the place that is easiest to debug and understand (since models have enough responsibilities already and are generally larger).
This is a common issue with metaprogramming. Most of the time it is better to avoid it precisely because of principle of least surprise. You will be glad you didn't use it a year from now when you get back to this code because of some issue you need to debug. You will forget what "clever" thing you have done. If you don't have a hell-of-a-good reason then just stick to the established conventions, they are there for a reason.
If nothing else then at least figure out a way to do this elegantly by declaring something in the Post model. For example by registering an awardable class method on ActiveRecord::Base. But the best approach would probably be doing it in the controller or via a service object. It is not the responsibility of AwardOne to handle how Post should be saved!
Because you are adding award_callback as class method. I bet it will be registered if you grep class methods.
So change your code like below. It should work fine.
model_class.class_eval do ## Changed to class_eval
def award_callback
this.can_award?(self)
end
end
Let me give a detailed example if it sounds confusing.
class Test
end
Test.instance_eval do
def class_fun
p "from class method "
end
end
Test.class_eval do
def instance_fun
p "from instance method "
end
end
Test.methods.grep /class_fun/
# => [:class_fun]
Test.instance_methods.grep /instance_fun/
# => [:instance_fun]
Test.class_fun
# => "from class method "
Test.new.instance_fun
# => "from instance method "
For Mongoid 3+, is there a diagram/description of the various callbacks?
http://mongoid.org/en/mongoid/v3/callbacks.html
For example, what's the difference between before_upsert vs. before_save. Isn't a save caused by an insert or update call? Or does save also get called by destroy?
Also, what's difference between before_xxx and around_xxx?
Cheers,
With before_xxx the code is executed before the action and with around_xxx you have the option to execute code before and after the action itself.
For example, imagine you want to update all the user belongings after destroy a user project (User has_many :proyects and Project belongs_to User) :
class ProjectsController < ApplicationController
around_destroy :destroy_belongings
def destroy_belongings
old_user = self.user
...
# Here the before_destroy ends.
yield # Here the destroy is performed itself.
# Here the after_destroy starts. It's needed to do this operation after destroy the project because, imagine, the update_belongings method calculates something related to the current number of proyects. And a simple after_destroy is not useful as we would have lost the project owner.
old_user.update_belongings
end
end
You can also see related answers here and here. Moreover this other article could be useful for you.
We have two Rails models: Person and Administrator. We're disallowing removal of Administrators at the model level:
class Person < ActiveRecord::Base
end
class Administrator < Person
def destroy
raise "Can't remove administrators."
end
end
me = Administrator.new
me.destroy # raises an exception
I'd like to be able to get around this during testing, but only for specific instances created during setup and teardown. I don't want to change the behavior of the class, so class_eval and remove_method aren't feasible.
I tried to redefine the actual instance's #destroy method:
def me.destroy
super
end
or redefine it on the singleton class:
class << me
def destroy
super
end
end
but those still raised the exception. I couldn't figure out how to get it to call the superclass method implicitly. I ended up creating my own destroy! method (since that's not actually a method in ActiveRecord), which sort of violates my desire not to change the behavior of the class:
def destroy!
ActiveRecord::Persistence.instance_method(:destroy).bind(self).call
end
Is there any simple way to tell a single instance method to call its superclass method?
Final Answer: Based on the article Holger Just linked to, I was able to simply call the superclass method explicitly:
def me.destroy
self.class.superclass.instance_method(:destroy).bind(self).call
end
I'd try to refactor the behavior to be more test-friendly. E.g. you could allow an optional parameter to destroy e.g. i_know_what_im_doing that has to be set to true to carry out the destroy. Alternatively you could cancel the destroy with a before_destroy hook like
class Administrator < Person
def before_destroy(record)
# You can't destroy me
false
end
end
In your tests, you can then call Administrator.skip_callback :before_destroy to ignore it and to have a proper destroy.
Finally, you could overwrite / stub the method in your tests. While you say you don't want to modify the class's behavior, you still have to do that (and implicitly do that with your destroy! method today).
I'm not familiar with Ruby metaprograming, so I wont answer if you can a call a method of the super class on an instance without modifying it. But you can create a hook to a superclass method with alias :
class Administrator < Person
alias :force_destroy :destroy
def destroy
raise "Can't remove administrators."
end
end
With this, admin.destroy will raise an exception, but admin.force_destroy will actually call the ActiveRecord destroy.
I have the following classes:
class Vigil < ActiveRecord::Base
after_update :do_something_cool
private
def do_something_cool
# Sweet code here
end
end
class NewsFeedObserver < ActionController::Caching::Sweeper
observe Vigil
def after_update
# Create a news feed entry
end
end
Everything works as expected; however, the after_update in the sweeper requires that the do_something_cool method in the model has finished before it can run properly. The problem is that the after_update in the sweeper is being called before (or perhaps at the same time as) the do_something_cool callback and it's causing problems.
Does anyone know how to force the after_update in the sweeper to fire after the model callback? Is there better way to achieve this?
Update/Fix: As it turns out, unlike the answer below states, the observer callbacks actually ARE firing in the correct order (after the model callbacks). When I discovered this, I realized something else must be wrong.
The do_something_cool method destroys all of a vigil's slots, and replaces them with the correct number of slots with the correct times. The observer relies on the number of slots to figure out how long the vigil should last. So, the underlying problem was that all of the vigil's slots were being destroyed, and that data was cached, so when I called vigil.slots from the observer, it was using the cached (destroyed slots) data. The solution: simply call vigil.slots(true) at the end of do_something_cool to reload/recache the newly created slots!
It's not going to be running at the same time but you're right, it looks like the Sweeper callback is being run before the Model one.
This post might be helpful : http://upstre.am/2007/10/27/using-and-testing-activerecordrails-observers/
About halfway down (search for 'callback :after_read') they have tried to create custom callbacks for their observers. You could use this to create a after_something_cool ARObserver method that gets called when the Model is done being cool e.g.
class Vigil < ActiveRecord::Base
after_update :do_something_cool
private
def do_something_cool
# Sweet code here
callback :after_something_cool
end
end
class NewsFeedObserver < ActionController::Caching::Sweeper
observe Vigil
def after_something_cool
# Create a news feed entry
end
end
Disclaimer: I've never done this and I've always found sweepers to be temperamental between rails versions so what worked for them might not work for you :(