How to get non-model data in ActiveRecord Callbacks

by Irish on August 16, 2010

On a project I’m currently working on, we use ActiveRecord callbacks heavily. From simplying creating other associated objects, to recording the historic activity about the object in question, and more.

The drawback I find from the scope of AR callbacks though is that you only have direct access to data from the database. And since you can’t pass arguments to a AR callback, you can’t easily get the request params for example or know which controller action fired off the the AR chain that triggered the callback you’re in, or know who the current user logged in is.

But it turns out that there is a way, however it is neither straight forward or obvious. Say for example you have the following model and controller:

    1 class Post < ActiveRecord::Base
    2   belongs_to :user
    3   belongs_to :group
    4   has_one    :alert, :as => 'alertable'
    5
    6   after_update :note_activity
    7
    8   private
    9   def note_activity
   10     group.admins.each do |admin|
   11       # What user is doing this update?
   12       admin.notifications.create(:user => "Someone",
   13                                  :action => 'did something')
   14     end
   15   end
   16 end
    1 class Admin::PostsController < ApplicationController
    2
    3   def update
    4     @post = Post.find(params[:id])
    5
    6     if @post.update_attributes(params[:post])
    7       redirect_to admin_posts_path
    8     else
    9       render :action => 'edit'
   10     end
   11   end
   12 end

What we want to accomplish here is that all admins of a group get a notification when another admin user edits a post. The post in question here has no context in it’s callback of which admin was logged in at the time the update happened. So what we’re gonna do is put the admin user’s id onto the current thread of the request in a before filter. Then, we’ll pull that id off in the callback to look up the admin and create the notifications.

So we’ll add a simple before filter:

    1 class ApplicationController < ActionController::Base
    2   before_filter :store_user
    3
    4   def store_user
    5     User.current = current_user
    6   end
    7 end

And create a couple class methods on User to store the currently logged in admin for the duration of the request:

    1 class User < ActiveRecord::Base
    2
    3   def self.current=(user)
    4     Thread.current['user'] = user.try(:id)
    5   end
    6
    7   def self.current
    8     @@current ||= User.find_by_id(Thread.current['user'])
    9   end
   10 end

Now we can alter our note_activity method in Post to get the admin who did the edit:

    1 def note_activity
    2   group.admins.each do |admin|
    3     admin.notifications.create(:user => User.current,
    4                                :action => 'did something')
    5   end
    6 end

This works well in my project. I’d like to know if there is a less “hacky” way of doing this but I’m not currently aware of one.

{ 2 comments… read them below or add one }

Gam October 27, 2011 at 3:10 am

It feels more sane for me to use in the model class:
attr_accessor :current_user
And assign it before saving, the callback can access it. You must however assign it in your controller.
For a similar global solution:
https://github.com/bokmann/sentient_user

Reply

Irish October 28, 2011 at 12:44 am

Sentient_user looks cool, thanks for the link :)

Reply

Leave a Comment

Previous post:

Next post: