How to get non-model data in ActiveRecord Callbacks

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Post < ActiveRecord::Base
  belongs_to :user
  belongs_to :group
  has_one    :alert, :as => 'alertable'

  after_update :note_activity

  private
  def note_activity
    group.admins.each do |admin|
      # What user is doing this update?
      admin.notifications.create(:user => "Someone",
                                 :action => 'did something')
    end
  end
end
1
2
3
4
5
6
7
8
9
10
11
12
class Admin::PostsController < ApplicationController

  def update
    @post = Post.find(params[:id])

    if @post.update_attributes(params[:post])
      redirect_to admin_posts_path
    else
      render :action => 'edit'
    end
  end
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
2
3
4
5
6
7
class ApplicationController < ActionController::Base
  before_filter :store_user

  def store_user
    User.current = current_user
  end
end

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

1
2
3
4
5
6
7
8
9
10
class User < ActiveRecord::Base

  def self.current=(user)
    Thread.current['user'] = user.try(:id)
  end

  def self.current
    @@current ||= User.find_by_id(Thread.current['user'])
  end
end

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

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

Comments

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
Irish October 28, 2011 at 12:44 am
Sentient_user looks cool, thanks for the link :)