Using conditionals on callbacks rails - ruby-on-rails

I have a callback on my comment model that creates a notification that gets sent out to the appropriate members but I don't want it to create a notification if the current_member is commenting on his own commentable object. I've tried using the unless conditional like this:
after_create :create_notification, on: :create, unless: Proc.new { |commentable| commentable.member == current_member }
def create_notification
subject = "#{member.user_name}"
body = "wrote you a <b>Comment</b> <p><i>#{content}</i></p>"
commentable.member.notify(subject, body, self)
end
But I get this error: undefined local variable or method 'current_member' for #<Comment:0x746e008
How do I get this to work like I want?

It's pretty atypical to try to use current_user or things like that from the model layer. One problem is that you're really coupling your model layer to the current state of the controller layer, which will make unit testing your models much more difficult and error-prone.
What I would recommend is to not use an after_create hook to do this, and instead create the notifications at the controller layer. This will give you access to current_user without needing to jump through any hoops.

Related

How to check attribute change and trigger a callback in rails

I'm trying to send an email notification when the email address is changed. To do that i need to detect first if the attribute actually changed. I think the best place to add the code is in an after_filter in the controller.
# app/controllers/users_controller.rb
after_filter :email_change_notification, only: [:update]
def email_change_notification
UserMailer.notify_new_email if #user.email_changed?
end
My problem now is that email email_changed? does not return expected value when used in this context. It is always false. As an alternative, I can do it in the model after_save
# app/models/user.rb
after_save :email_change_notification
def email_change_notification
UserMailer.notify_new_email if email_changed?
end
This works but I think the former is a better approach since calling a mailer is not part of the model's responsibility.
My question would be:
(1) Where should I put such a callback (model or controller)?
(2) Is there a better way to make the controller approach work?
(3) Is there a better approach than the ones mentioned?
What you may want to implement is an ActiveRecord::Observer
http://api.rubyonrails.org/v3.2.0/classes/ActiveRecord/Observer.html
This is designed exactly for your requiements... a class that performs trigger-like behavour on changes to the original class, but outside the class.
Very easy to set up and use!

How do I test my callback

I have three models - contact, interactions and leads.
When an interactions occurs that generates a lead (is_lead), I am updating the lead status or creating a new lead (depending if it exists or not). A lead is captured against a contact.
To achieve this, I am using a callback after_commit which calls process_interaction. See below
class Interaction < ActiveRecord::Base
belongs_to :contact
has_many :leads
enum interaction_type: { file_download: 1, email: 2, telesale: 3, registration: 4 }
after_commit :process_interaction, on: [:create, :update]
private
def process_interaction
if file_download? || email? || telesale?
lead = Lead.find_or_initialize_by(contact_id: contact_id)
self.is_lead ? lead.active! : lead.stale!
end
end
end
The code is fairly straight forward and it works. My question is how should I go about testing this? I don't really know how to test the callback correctly. Or how to change my code so that it is more testable. I have read a lot of articles and just can't figure out how to do this. Also I am not really sure if this logic should sit in my interaction model. I am still trying to get to grips with the direction of my dependencies.
Note I did try injecting the interaction into lead by rather doing
def process_interaction
if file_download? || email? || telesale?
Lead.process_potential_lead(interaction)
end
end
This would achieve the same thing, but the processing would be done on the lead side. Not sure how to test this in my interaction spec and not sure which way is better.
I wouldn't test the callback directly, instead I'd test the effect of the callback, which in this case is to create a Lead object and put it in the appropriate state.
So basically something like the following:
it 'should create an active lead' do
... create the appropriate interaction object ...
lead = Lead.where(contact_id: contact_id).first
expect(lead.active?).to be_true
end
Personally I'd avoid the use of callbacks and look to create a service object given you're dealing with 3 different models. You'll most likely find it's simpler to test the service object.

Using current user in Rails in a model method

I'm currently trying to implement simple audit for users (just for destroy method). This way I know if the user has been deleted by an admin or user deleted itself. I wanted to add deleted_by_id column to my model.
I was thinking to use before_destroy, and to retrieve the user info like described in this post :
http://www.zorched.net/2007/05/29/making-session-data-available-to-models-in-ruby-on-rails/
module UserInfo
def current_user
Thread.current[:user]
end
def self.current_user=(user)
Thread.current[:user] = user
end
end
But this article is from 2007, I'm not sure will this work in multithreaded and is there something more up to date on this topic, has anyone done something like this lately to pass on the experience?
Using that technique would certainly work, but will violate the principle that wants the Model unaware of the controller state.
If you need to know who is responsible for a deletion, the correct approach is to pass such information as parameter.
Instead of using callbacks and threads (both represents unnecessary complexity in this case) simply define a new method in your model
class User
def delete_user(actor)
self.deleted_by_id = actor.id
# do what you need to do with the record
# such as .destroy or whatever
end
end
Then in your controller simply call
#user.delete_user(current_user)
This approach:
respects the MVC pattern
can be easily tested in isolation with minimal dependencies (it's a model method)
expose a custom API instead of coupling your app to ActiveRecord API
You can use paranoia gem to make soft deletes. And then I suggest destroying users through some kind of service. Check, really basic example below:
class UserDestroyService
def initialize(user, destroyer)
#user = user
#destroyer = destroyer
end
def perform
#user.deleted_by_id = #destroyer.id
#user.destroy
end
end
UserDestroyService.new(user, current_user).perform

Access previous value of association on record update

I have a "event" model that has many "invitations". Invitations are setup through checkboxes on the event form. When an event is updated, I wanted to compare the invitations before the update, to the invitations after the update. I want to do this as part of the validation for the event.
My problem is that I can't seem to access the old invitations in any model callback or validation. The transaction has already began at this point and since invitations are not an attribute of the event model, I can't use _was to get the old values.
I thought about trying to use a "after_initialize" callback to store this myself. These callbacks don't seem to respect the ":on" option though so I can't do this only :on :update. I don't want to run this every time a object is initialized.
Is there a better approach to this problem?
Here is the code in my update controller:
def update
params[:event][:invited_user_ids] ||= []
if #event.update_attributes(params[:event])
redirect_to #event
else
render action: "edit"
end
end
My primary goal is to make it so you can add users to an event, but you can't not remove users. I want to validate that the posted invited_user_ids contains all the users that currently are invited.
--Update
As a temporary solution I made use for the :before_remove option on the :has_many association. I set it such that it throws an ActiveRecord::RollBack exception which prevents users from being uninvited. Not exactly what I want because I can't display a validation error but it does prevent it.
Thank you,
Corsen
Could you use ActiveModel::Dirty? Something like this:
def Event < ActiveRecord::Base
validates :no_invitees_removed
def no_invitees_removed
if invitees.changed? && (invitees - invitees_was).present?
# ... add an error or re-add the missing invitees
end
end
end
Edit: I didn't notice that the OP already discounted ActiveModel::Dirty since it doesn't work on associations. My bad.
Another possibility is overriding the invited_user_ids= method to append the existing user IDs to the given array:
class Event < ActiveRecord::Base
# ...
def invited_user_ids_with_guard=(ids)
self.invited_user_ids_without_guard = self.invited_user_ids.concat(ids).uniq
end
alias_method_chain :invited_user_ids=, :guard
end
This should still work for you since update_attributes ultimately calls the individual attribute= methods.
Edit: #corsen asked in a comment why I used alias_method_chain instead of super in this example.
Calling super only works when you're overriding a method that's defined further up the inheritance chain. Mixing in a module or inheriting from another class provides a means to do this. That module or class doesn't directly "add" methods to the deriving class. Instead, it inserts itself in that class's inheritance chain. Then you can redefine methods in the deriving class without destroying the original definition of the methods (because they're still in the superclass/module).
In this case, invited_user_ids is not defined on any ancestor of Event. It's defined through metaprogramming directly on the Event class as a part of ActiveRecord. Calling super within invited_user_ids will result in a NoMethodError because it has no superclass definition, and redefining the method loses its original definition. So alias_method_chain is really the simplest way to acheive super-like behavior in this situation.
Sometimes alias_method_chain is overkill and pollutes your namespace and makes it hard to follow a stack trace. But sometimes it's the best way to change the behavior of a method without losing the original behavior. You just need to understand the difference in order to know which is appropriate.

Passing variables to Rails StateMachine gem transitions

Is it possible to send variables in the the transition? i.e.
#car.crash!(:crashed_by => current_user)
I have callbacks in my model but I need to send them the user who instigated the transition
after_crash do |car, transition|
# Log the car crashers name
end
I can't access current_user because I'm in the Model and not the Controller/View.
And before you say it... I know I know.
Don't try to access session variables in the model
I get it.
However, whenever you wish to create a callback that logs or audits something then it's quite likely you're going to want to know who caused it? Ordinarily I'd have something in my controller that did something like...
#foo.some_method(current_user)
and my Foo model would be expecting some user to instigate some_method but how do I do this with a transition with the StateMachine gem?
If you are referring to the state_machine gem - https://github.com/pluginaweek/state_machine - then it supports arguments to events
after_crash do |car, transition|
Log.crash(car: car, crashed_by: transition.args.first)
end
I was having trouble with all of the other answers, and then I found that you can simply override the event in the class.
class Car
state_machine do
...
event :crash do
transition any => :crashed
end
end
def crash(current_driver)
logger.debug(current_driver)
super
end
end
Just make sure to call "super" in your custom method
I don't think you can pass params to events with that gem, so maybe you could try storing the current_user on #car (temporarily) so that your audit callback can access it.
In controller
#car.driver = current_user
In callback
after_crash do |car, transition|
create_audit_log car.driver, transition
end
Or something along those lines.. :)
I used transactions, instead of updating the object and changing the state in one call. For example, in update action,
ActiveRecord::Base.transaction do
if #car.update_attribute!(:crashed_by => current_user)
if #car.crash!()
format.html { redirect_to #car }
else
raise ActiveRecord::Rollback
else
raise ActiveRecord::Rollback
end
end
Another common pattern (see the state_machine docs) that saves you from having to pass variables between the controller and model is to dynamically define a state-checking method within the callback method. This wouldn't be very elegant in the example given above, but might be preferable in cases where the model needs to handle the same variable(s) in different states. For example, if you have 'crashed', 'stolen', and 'borrowed' states in your Car model, all of which can be associated with a responsible Person, you could have:
state :crashed, :stolen, :borrowed do
def blameable?
true
end
state all - [:crashed, :stolen, :borrowed] do
def blameable?
false
end
Then in the controller, you can do something like:
car.blame_person(person) if car.blameable?

Resources