Ruby on Rails
Howto Add created_by and updated_by

Much like how Timestamping callbacks are set up by default for columns named created_at/created_on and updated_at/updated_on, (See http://rails.rubyonrails.com/classes/ActiveRecord/Timestamp.html) this will automaticaly populate created_by and updated_by fields with the user who created/updated items.

Warning
This code does not work in rails 2

If you are having trouble look at
http://dev.rubyonrails.org/changeset/7749

The instance_method_already_implemented? changes break this module.

Reverting to the previous version in green fixes the issue.

Also see ExtendingActiveRecordExample for more examples on how to extend ActiveRecord to your needs.

This code is from this blog post

The Code

Place this in /lib/usermonitor.rb

module ActiveRecord

  module UserMonitor

    def self.included(base)

      base.class_eval do
        alias_method :create_without_user, :create
        alias_method :create, :create_with_user

        alias_method :update_without_user, :update
        alias_method :update, :update_with_user
      end
    end

    def create_with_user
        user = user_model.current_user
        self[:created_by] = user.id if respond_to?(:created_by) && created_by.nil?
        self[:updated_by] = user.id if respond_to?(:updated_by)
        create_without_user
    end

    def update_with_user
        user = user_model.current_user
        self[:updated_by] = user.id if respond_to?(:updated_by)
        update_without_user
    end

    def created_by
      begin
        user_model.find(self[:created_by])
      rescue ActiveRecord::RecordNotFound
        nil
      end
    end

    def updated_by
      begin
        user_model.find(self[:updated_by])
      rescue ActiveRecord::RecordNotFound
        nil
      end
    end
  end

  class Base

    @@default_user_model = :users
    cattr_accessor :user_model_name

    def user_model_name
      @@user_model_name || @@default_user_model
    end

    def self.relates_to_user_in(model)
      self.user_model_name = model
    end

    def user_model
      Object.const_get(user_model_name.to_s.singularize.humanize)
    end
  end
end

Then add the following to the bottom of /config/environment.rb

require 'usermonitor'
ActiveRecord::Base.class_eval do
  include ActiveRecord::UserMonitor
end

Now if you add created_by or updated_by to your database tables, you will be able to access the user who created/updated the objects by calling object.created_by and object.updated_by respectively. Likewise you can set the user by assigning a user object to either one.

This defaults to assuming you have a user model called User, which can be overriden by calling relates_to_user_in in your model like so:


class WebPage < ActiveRecord::Base
  relates_to_user_in :monkeys
end

The user model must contain “current_user” that references the currently logged in user.

An example of setting up a “current_user” method in your User model is as follows

In my model I have an accessor for current_user:
——user.rb——
cattr_accessor :current_user
————————-

Then in my application controller I have a before_filter which grabs the user ID from the session (if there is one) and sets the current_user in the model.
——application.rb——
before_filter do |c|

User.current_user = User.find(c.session[:user]) unless c.session[:user].nil?
end
————————-

So now you can call User.current_user to get the current logged in user.


Note: an update to the blog post mentioned below says that thread safety is not actually an issue here …

I’m afraid that current_user method is not thread-safe, so I use Thread.current instead to retrieve currently logged-in user.

Find out more info at this blog post.

Here is the updated version with Thread.current

/app/controllers/application.rb

class ApplicationController < ActionController::Base
  ....

  before_filter :set_current_user

  def set_current_user
    Thread.current['user'] = session[:user]
  end
end

/lib/usermonitor.rb

module ActiveRecord

  module UserMonitor
    def self.included(base)
      base.class_eval do
        alias_method_chain :create, :user
        alias_method_chain :update, :user

        def current_user
          Thread.current['user']
        end
      end
    end

    def create_with_user
      user = current_user
      if !user.nil?
        self[:created_by] = user.id if respond_to?(:created_by) && created_by.nil?
        self[:updated_by] = user.id if respond_to?(:updated_by)
      end
      create_without_user
    end

    def update_with_user
      user = current_user
      self[:updated_by] = user.id if respond_to?(:updated_by) && !user.nil?
      update_without_user
    end

    def created_by
      begin
        current_user.class.find(self[:created_by]) if current_user
      rescue ActiveRecord::RecordNotFound
        nil
      end
    end

    def updated_by
      begin
        current_user.class.find(self[:updated_by]) if current_user
      rescue ActiveRecord::RecordNotFound
        nil
      end
    end
  end
end

Note alias_method_chain is currently only available if you are using Rails on Edge.

If you are using the LoginGenerator, neither of the two beforementioned codes work! After working on a fix for several hours, I decided to post it here, so that others can possibly use this very usefull feature. I’ve merged code here and there from the two and am using the thread-safe Thread.current to store the current_user. There also was a typo (or maybe it works, but only on edge!) that prevented the current_user to be determined. The following code works with 1.1.6 and the newest LoginGenerator. As alias_method_chain is only supported on Edge I’m not using it.

application.rb (inside the class)

  before_filter :set_current_user

  def set_current_user
    Thread.current['user'] = @session['user']
  end

@environment.rb (insert before end of file)

require 'usermonitor'
 ActiveRecord::Base.class_eval do
   include ActiveRecord::UserMonitor
end

/lib/usermonitor.rb

module ActiveRecord

  module UserMonitor

    def self.included(base)

      base.class_eval do
        alias_method :create_without_user, :create
        alias_method :create, :create_with_user

        alias_method :update_without_user, :update
        alias_method :update, :update_with_user

        def current_user
          Thread.current['user']
        end
      end
    end

    def create_with_user
          user = current_user
          if !user.nil?
            self[:created_by] = user.id if respond_to?(:created_by) && created_by.nil?
            self[:updated_by] = user.id if respond_to?(:updated_by)
          end
          create_without_user
        end

        def update_with_user
          user = current_user
          self[:updated_by] = user.id if respond_to?(:updated_by) && !user.nil?
          update_without_user
        end

        def created_by
          begin
            current_user.class.find(self[:created_by]) if current_user
          rescue ActiveRecord::RecordNotFound
            nil
          end
        end

        def updated_by
          begin
            current_user.class.find(self[:updated_by]) if current_user
          rescue ActiveRecord::RecordNotFound
            nil
          end
        end
  end
end

That’s it!

category:Howto, Example