views:

441

answers:

6

In my rails app I would like to track who changes my model and update a field on the model's table to reflect.

So, for example we have:

class Foo < ActiveRecord::Base
  before_create :set_creator
  belongs_to :creator, :class_name => "User" 

  protected 

  def set_creator
    # no access to session[:user_id] here... 
  end
end

What's a good testable way for me to get at the user_id from my model? Should I be wacking this data in Thread.current ?

Is it a better practice to hand this information from the controller?

A: 

I'd create new save, update, etc methods that take the user_id from everything that calls them (mainly the controller).

I'd probably extend ActiveRecord:Base into a new class that handles this for all the models that need this behaviour.

RichH
A: 

I wouldn't trust Thread.current, seems a bit hackish. I would always call a custom method which takes an argument:

def create_with_creator(creator, attributes={})
  r = new(attributes)
  r.creator = creator
  r.save
end

As it follows the MVC pattern. The obviously inherient problem with this is that you're going to be calling create_with_creator everywhere.

Ryan Bigg
A: 

You might find PaperTrail useful.

floehopper
it avoids doing the Thread.current, but does use global vars, and central module to track the current user.
Sam Saffron
+2  A: 

Yeah, something like that would work, or having a class variable on your User model

cattr_accessor :current_user

Then in your controller you could have something like:

User.current_user = current_user

inside a before filter (assuming current_user is the logged in user).

You could then extend AR:Base's create/update methods to check for the existence of a created_by/updated_by field on models and set the value to User.current_user.

Omar Qureshi
I guess if you don't care at all about thread safety.
Michael Sofaer
A: 

Probably you could check out usertamp plugins, found two in github

http://github.com/delynn/userstamp/tree/master http://github.com/jnunemaker/user_stamp/tree/master

Ed
+1  A: 

Best practice in MVC is to have your Models be stateless, the controller gets to handle state. If you want the information to get to your models, you need to pass it from the controller. Using a creation hook here isn't really the right way to go, because you are trying to add stateful data, and those hooks are really for stateless behavior.

You can pass the info in from the controller:

Foo.new(params[:foo].merge {:creator_id => current_user.id})

Or you can create methods on User to handle these operations:

class User
  def create_foo(params)
    Foo.new(params.merge! {:creator_id => self.id})
  end
end

If you find yourself writing a lot of permissions code in the controller, I'd go with option 2, since it will let you refactor that code to the model. Otherwise option 1 is cleaner.

Omar points out that it's trickier to automate, but it can still be done. Here's one way, using the create_something instance method on user:

def method_missing(method_sym, *arguments, &block)
  meth = method_sym.to_s
  if meth[0..6] == "create_"
      obj = meth[7..-1].classify.constantize.new(*arguments)
      obj.creator_id = self.id
  else
    super
  end
end

You could also override the constructor to require user_ids on construction, or create a method inside ApplicationController that wraps new.

There's probably a more elegant way to do things, but I definitely don't like trying to read state from inside Model code, it breaks MVC encapsulation. I much prefer to pass it in explicitly, one way or another.

Michael Sofaer
you know what, I'm kind of leaning towards this, the side effect solution seems a bit to ... ahm ... side effecty for my liking.
Sam Saffron
Thread safe? yes. Automated? not very. Problem is that you have to track all the models with a creator_id column. What happens if you additionally want to add an updater_id column? You have to manage the logic additionally. Also, I would probably forget that i need to merge the creator_id attributes in on occasion. Too many things to track for my liking.
Omar Qureshi
Added some automation, but there is definitely room for a better solution to this.
Michael Sofaer
Could I just do Foo.new(params[:foo].merge {:user_id => current_user.id}) and take care of dispatching to creator/modifier before create and after save?
Sam Saffron
You can just do Foo.new(params[:foo].merge {:user_id => current_user.id}) in all your controllers where you need a user_id set. If you add a validator to ensure its existence, you will be protected from forgetting it somewhere.
Michael Sofaer
Can't have validator. What if current_user == nil ?
Omar Qureshi
If you want to allow non-authenticated users to add objects, you could denote that case with user_id => 0
Michael Sofaer