diff --git a/README.md b/README.md index 37d4f877..a96690d6 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,10 @@ PaperTrail lets you track changes to your models' data. It's good for auditing * Allows you to get at every version, including the original, even once destroyed. * Allows you to get at every version even if the schema has since changed. * Allows you to get at the version as of a particular time. -* Automatically records who was responsible if your controller has a `current_user` method. +* Automatically records who was responsible via your controller. PaperTrail calls `current_user` by default, if it exists, but you can have it call any method you like. * Allows you to set who is responsible at model-level (useful for migrations). -* Allows you to store arbitrary metadata with each version (useful for filtering versions). +* Allows you to store arbitrary model-level metadata with each version (useful for filtering versions). +* Allows you to store arbitrary controller-level information with each version, e.g. remote IP. * Can be turned off/on per class (useful for migrations). * Can be turned off/on globally (useful for testing). * No configuration necessary. @@ -146,6 +147,14 @@ If your `ApplicationController` has a `current_user` method, PaperTrail will sto >> last_change = Widget.versions.last >> user_who_made_the_change = User.find last_change.whodunnit.to_i +You may want PaperTrail to call a different method to find out who is responsible. To do so, override the `user_for_paper_trail` method in your controller like this: + + class ApplicationController + def user_for_paper_trail + logged_in? ? current_member : 'Public user' # or whatever + end + end + In a migration or in `script/console` you can set who is responsible like this: >> PaperTrail.whodunnit = 'Andy Stewart' @@ -155,7 +164,7 @@ In a migration or in `script/console` you can set who is responsible like this: ## Storing metadata -You can store arbitrary metadata alongside each version like this: +You can store arbitrary model-level metadata alongside each version like this: class Article < ActiveRecord::Base belongs_to :author @@ -169,6 +178,16 @@ Why would you do this? In this example, `author_id` is an attribute of `Article Version.all(:conditions => ['author_id = ?', author_id]) +You can also store any information you like from your controller. Just override the `info_for_paper_trail` method in your controller to return a hash whose keys correspond to columns in your `versions` table. E.g.: + + class ApplicationController + def info_for_paper_trail + { :ip => request.remote_ip, :user_agent => request.user_agent } + end + end + +Remember to add those extra columns to your `versions` table ;) + ## Turning PaperTrail Off/On diff --git a/lib/paper_trail.rb b/lib/paper_trail.rb index b7f15c7b..0d3aa48b 100644 --- a/lib/paper_trail.rb +++ b/lib/paper_trail.rb @@ -18,14 +18,10 @@ module PaperTrail !!PaperTrail.config.enabled end - # Returns PaperTrail's configuration object. - def self.config - @@config ||= PaperTrail::Config.instance - end # Returns who is reponsible for any changes that occur. def self.whodunnit - Thread.current[:whodunnit] + paper_trail_store[:whodunnit] end # Sets who is responsible for any changes that occur. @@ -33,7 +29,36 @@ module PaperTrail # when working with models directly. In a controller it is set # automatically to the `current_user`. def self.whodunnit=(value) - Thread.current[:whodunnit] = value + paper_trail_store[:whodunnit] = value + end + + # Returns any information from the controller that you want + # PaperTrail to store. + # + # See `PaperTrail::Controller#info_for_paper_trail`. + def self.controller_info + paper_trail_store[:controller_info] + end + + # Sets any information from the controller that you want PaperTrail + # to store. By default this is set automatically by a before filter. + def self.controller_info=(value) + paper_trail_store[:controller_info] = value + end + + + private + + # Thread-safe hash to hold PaperTrail's data. + # + # TODO: is this a memory leak? + def self.paper_trail_store + Thread.current[:paper_trail] ||= {} + end + + # Returns PaperTrail's configuration object. + def self.config + @@config ||= PaperTrail::Config.instance end end diff --git a/lib/paper_trail/controller.rb b/lib/paper_trail/controller.rb index b86a97ef..2c84729f 100644 --- a/lib/paper_trail/controller.rb +++ b/lib/paper_trail/controller.rb @@ -2,15 +2,59 @@ module PaperTrail module Controller def self.included(base) - base.before_filter :set_whodunnit + base.before_filter :set_paper_trail_whodunnit + base.before_filter :set_paper_trail_controller_info end protected - # Sets who is responsible for any changes that occur: the controller's - # `current_user`. + # Returns the user who is responsible for any changes that occur. + # By default this calls `current_user` and returns the result. + # + # Override this method in your controller to call a different + # method, e.g. `current_person`, or anything you like. + def user_for_paper_trail + current_user rescue nil + end + + # Returns any information about the controller or request that you + # want PaperTrail to store alongside any changes that occur. By + # default this returns an empty hash. + # + # Override this method in your controller to return a hash of any + # information you need. The hash's keys must correspond to columns + # in your `versions` table, so don't forget to add any new columns + # you need. + # + # For example: + # + # {:ip => request.remote_ip, :user_agent => request.user_agent} + # + # The columns `ip` and `user_agent` must exist in your `versions` # table. + # + # Use the `:meta` option to `PaperTrail::Model::ClassMethods.has_paper_trail` + # to store any extra model-level data you need. + def info_for_paper_trail + {} + end + + private + + # Tells PaperTrail who is responsible for any changes that occur. + def set_paper_trail_whodunnit + ::PaperTrail.whodunnit = user_for_paper_trail + end + + # DEPRECATED: please use `set_paper_trail_whodunnit` instead. def set_whodunnit - ::PaperTrail.whodunnit = self.send :current_user rescue nil + logger.warn '[PaperTrail]: the `set_whodunnit` controller method has been deprecated. Please rename to `set_paper_trail_whodunnit`.' + set_paper_trail_whodunnit + end + + # Tells PaperTrail any information from the controller you want + # to store alongside any changes that occur. + def set_paper_trail_controller_info + ::PaperTrail.controller_info = info_for_paper_trail end end diff --git a/lib/paper_trail/has_paper_trail.rb b/lib/paper_trail/has_paper_trail.rb index f5ea9a6e..34aadafe 100644 --- a/lib/paper_trail/has_paper_trail.rb +++ b/lib/paper_trail/has_paper_trail.rb @@ -7,11 +7,16 @@ module PaperTrail module ClassMethods + + # Declare this in your model to track every create, update, and destroy. Each version of + # the model is available in the `versions` association. + # # Options: - # :ignore an array of attributes for which a new +Version+ will not be created if only they change. - # :meta a hash of extra data to store. You must add a column to the versions table for each key. - # Values are objects or procs (which are called with +self+, i.e. the model with the paper - # trail). + # :ignore an array of attributes for which a new `Version` will not be created if only they change. + # :meta a hash of extra data to store. You must add a column to the `versions` table for each key. + # Values are objects or procs (which are called with `self`, i.e. the model with the paper + # trail). See `PaperTrail::Controller.info_for_paper_trail` for how to store data from + # the controller. def has_paper_trail(options = {}) send :include, InstanceMethods @@ -83,10 +88,12 @@ module PaperTrail private def merge_metadata(data) + # First we merge the model-level metadata in `meta`. meta.each do |k,v| data[k] = v.respond_to?(:call) ? v.call(self) : v end - data + # Second we merge any extra data from the controller (if available). + data.merge(PaperTrail.controller_info || {}) end def previous_version diff --git a/test/paper_trail_controller_test.rb b/test/paper_trail_controller_test.rb index b4d5d83f..0a2584b0 100644 --- a/test/paper_trail_controller_test.rb +++ b/test/paper_trail_controller_test.rb @@ -13,6 +13,10 @@ class ApplicationController < ActionController::Base def current_user 153 end + + def info_for_paper_trail + {:ip => request.remote_ip, :user_agent => request.user_agent} + end end class WidgetsController < ApplicationController @@ -37,11 +41,17 @@ end class PaperTrailControllerTest < ActionController::TestCase tests WidgetsController + def setup + @request.env['REMOTE_ADDR'] = '127.0.0.1' + end + test 'create' do post :create, :widget => { :name => 'Flugel' } widget = assigns(:widget) assert_equal 1, widget.versions.length assert_equal 153, widget.versions.last.whodunnit.to_i + assert_equal '127.0.0.1', widget.versions.last.ip + assert_equal 'Rails Testing', widget.versions.last.user_agent end test 'update' do @@ -51,6 +61,8 @@ class PaperTrailControllerTest < ActionController::TestCase widget = assigns(:widget) assert_equal 2, widget.versions.length assert_equal 153, widget.versions.last.whodunnit.to_i + assert_equal '127.0.0.1', widget.versions.last.ip + assert_equal 'Rails Testing', widget.versions.last.user_agent end test 'destroy' do @@ -60,5 +72,7 @@ class PaperTrailControllerTest < ActionController::TestCase widget = assigns(:widget) assert_equal 2, widget.versions.length assert_equal 153, widget.versions.last.whodunnit.to_i + assert_equal '127.0.0.1', widget.versions.last.ip + assert_equal 'Rails Testing', widget.versions.last.user_agent end end diff --git a/test/schema.rb b/test/schema.rb index ceac194c..1495f6c0 100644 --- a/test/schema.rb +++ b/test/schema.rb @@ -27,6 +27,10 @@ ActiveRecord::Schema.define(:version => 0) do t.integer :answer t.string :question t.integer :article_id + + # Controller info columns. + t.string :ip + t.string :user_agent end add_index :versions, [:item_type, :item_id] diff --git a/test/test_helper.rb b/test/test_helper.rb index 849866bd..63cbce2d 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -34,4 +34,10 @@ class ActiveRecord::Base end end +class ActionController::Base + def logger + @logger ||= Logger.new(nil) + end +end + load_schema