How to check attribute change and trigger a callback in rails - ruby-on-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!

Related

How to validate presense of a value based on an API call

I've got a model #record_request that takes in a unique identifier and saves it to the database - the model can be later used to fire a bunch of API requests off to an external database to do it's work.
Thing is, I don't want the user to be able to save the value in the Rails app, if it can't be queried in the remote database.
I've tried creating validations and creating Rspec tests, but all of them require my #current_user variable with all the api_token and referesh_token and other OmniAuth goodies needed to make an autenticated API call...
So I'm kind of stumped. Where is the best practice place to put a validation rule of this kind.
I'm thinking I'll have to put something in my controller on :create that makes the call there and raises errors/flashes that way... but then it seems like I've got a load of code on my controller, and I've fallen for a fat controller anti-pattern, no?
What is the best practice?
Here is an example from a project I'm working on, this code is written in the model
after_validation :should_validate_match?, on: [:create, :update]
def should_validate_match?
if self[:type] == 'Match'
self.answers.each{|ans| self.errors.add(:answer, "match cannot be null") if ans.match.blank?}
end
self.errors.empty?
end
You could test the authentication in custom method triggered by a before_save callback in your model.
Something like:
before_save :do_this
def do_this
if name_of_attribute.blank?
return
else
# api call to test authentication
end
end
If a record request has a reference to user you shouldn't need access to the current_user helper.

Using conditionals on callbacks 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.

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.

What is the best way of accessing routes in ActiveRecord models and observers

I have a situation where I want to make a request to third-party API(url shortening service) after creating a record in the database (updates a column in the table which stores the short url), in order to decouple the API request from the Model, I have set up an ActiveRecord Observer which kicks in every time a record is created, using after_create callback hook, here is the relevant code:
class Article < ActiveRecord::Base
has_many :comments
end
class ArticleObserver < ActiveRecord::Observer
def after_create(model)
url = article_url(model)
# Make api request...
end
end
The problem in the above code is article_url because Rails Routes are not available in either Model or ModelObservers, same as ActionMailer (similar problem exists in Mails where if we need to put an URL we have to configure "ActionMailer::default_options_url"). In theory accessing routes/request object in Model is considered a bad design. To circumvent the above issue I could include the url_helpers module as described in the following URL:
http://slaive-prog.tumblr.com/post/7618787555/using-routes-in-your-model-in-rails-3-0-x
But this does not seem to me a clean solution, does anybody have a pointer on this issue or any advice on how it should be done?
Thanks in advance.
I would definitely not let your models know about your routes. Instead, add something like attr_accessor :unshortened_url on your Article class. Set that field in your controller, and then use it from your observer. This has the added benefit of continuing to work if you later decide to set your shortened URL asynchronously via a background task.
Edit
A couple of things, first of all.
Let's get the knowledge of creating a short_url out of the model
entirely.
We could nitpick and say that the short_url itself doesn't belong in the model at all, but to remain practical let's leave it in there.
So let's move the trigger of this soon-to-be-background task into the controller.
class ArticlesController < ApplicationController
after_filter :short_url_job, :only => [:create]
# ...
protected
def short_url_job
begin
#article.short_url = "I have a short URL"
#article.save!
rescue Exception => e
# Log thy exception here
end
end
end
Now, obviously, this version of short_url_job is stupid, but it illustrates the point. You could trigger a DelayedJob, some sort of resque task, or whatever at this point, and your controller will carry on from here.

Rails3 - Permission Model Before_Save Check?

I have a permission model in my app, that ties (Users, Roles, Projects) together.
What I'm looking to learn how to do is prevent a user for removing himself for their project...
Can you give me feedback on the following?
class Permission < ActiveRecord::Base
.
.
.
#admin_lock makes sure the user who created the project, is always the admin
before_save :admin_lock
def before_save
#Get the Project Object
project = Find(self.project_id)
if project.creator_id == current_user.id
# SOME HOW ABORT OR SEND BACK Not Allowed?
else
#continue, do nothing
end
end
end
Is that look like the right approach?
Also, I'm not sure how to do the following two things above:
How to abort prevent the save, and send back an error msg?
Get the devise, current_user.id in the model, that doesn't seem possible, so how do Rails gurus do stuff like the above?
Thanks for reading through
How to abort prevent the save, and send back an error msg?
return false during the callback chain tells activemodel to stop (similar to how adding errors to the model during a validation tells it to stop at that point)
self.errors.add_to_base "msg" will add an error to the model, which can then be rendered on the view.
Get the devise, current_user.id in the model, that doesn't seem possible, so how do Rails gurus do stuff like the above?
Models shouldn't really know about things like the current request, if at all possible, you should be locking things down at the controller/action level.
EDIT:
So, the role of controllers is to deal with everything involved in getting the correct information together based on the request, and passing it to the view (which becomes the response). People often say "make your models fat and your controllers skinny", but that could be said of any system that embraces object oriented design -- your logic should be in objects when possible.
That being said, the whole point of controllers is to deal with routing the right things to the right places, and authentication is definitely a concern of routing.
You could easily move the line comparing creator_id to user id in the action, and react based on that.
Now, sometimes you genuinely need that stuff in the model and there is no way around it. That becomes a problem, because you need to fight rails to get it there. One way would be to attr_accessor a current_user field on your model, and pass that in on initialize. Another would be to remove the fields from the params hash that a user is not allowed to change in the action. Neither is really that nice though.
Agreed with Matt that you should try to use the controller for the redirect. The model should have the logic to determine if the redirect is appropriate. Maybe something like
class ProjectsController < ApplicationController
def update
redirect_to(projects_url, :alert => "You can't remove yourself from this project.") and return if Role.unauthorized_action?(:update, params[:project])
#project = Project.find(params[:id])
if #project.update_attributes(params[:project])
...
end
class Role
def self.unauthorized_action?(action, params)
# your logic here
end
You should check out CanCan for some ideas.
In permission model take one field project_creater as boolean
In project modelbefore_create :set_project_ownership
def set_project_ownership
self.permissions.build(user_id: User.current.id, project_creater: true)
end
In project controllerbefore_filter :set_current_user
In Application controllerdef set_current_user
User.current = current_user
end

Resources