Store user-defined metadata.

This commit is contained in:
Andy Stewart 2010-01-06 12:57:54 +00:00
parent e71252ce29
commit 55185cd448
4 changed files with 110 additions and 11 deletions

View File

@ -6,11 +6,13 @@ PaperTrail lets you track changes to your models' data. It's good for auditing
## Features ## Features
* Stores every create, update and destroy. * Stores every create, update and destroy.
* Does not store updates which don't change anything (or which only change attributes you are ignoring). * Does not store updates which don't change anything.
* Does not store updates which only change attributes you are ignoring.
* Allows you to get at every version, including the original, even once destroyed. * 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 every version even if the schema has since changed.
* Automatically records who was responsible if your controller has a `current_user` method. * Automatically records who was responsible if your controller has a `current_user` method.
* Allows you to set who is responsible at model-level (useful for migrations). * 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).
* Can be turned off/on (useful for migrations). * Can be turned off/on (useful for migrations).
* No configuration necessary. * No configuration necessary.
* Stores everything in a single database table (generates migration for you). * Stores everything in a single database table (generates migration for you).
@ -141,6 +143,23 @@ In a migration or in `script/console` you can set who is responsible like this:
>> widget.versions.last.whodunnit # Andy Stewart >> widget.versions.last.whodunnit # Andy Stewart
## Storing metadata
You can store arbitrary metadata alongside each version like this:
class Article < ActiveRecord::Base
belongs_to :author
has_paper_trail :meta => { :author_id => Proc.new { |article| article.author_id },
:answer => 42 }
end
PaperTrail will call your proc with the current article and store the result in the `author_id` column of the `versions` table. (Remember to add your metadata columns to the table.)
Why would you do this? In this example, `author_id` is an attribute of `Article` and PaperTrail will store it anyway in serialized (YAML) form in the `object` column of the `version` record. But let's say you wanted to pull out all versions for a particular author; without the metadata you would have to deserialize (reify) each `version` object to see if belonged to the author in question. Clearly this is inefficient. Using the metadata you can find just those versions you want:
Version.all(:conditions => ['author_id = ?', author_id])
## Turning PaperTrail Off/On ## Turning PaperTrail Off/On
Sometimes you don't want to store changes. Perhaps you are only interested in changes made Sometimes you don't want to store changes. Perhaps you are only interested in changes made

View File

@ -8,12 +8,18 @@ module PaperTrail
module ClassMethods module ClassMethods
# Options: # Options:
# :ignore an array of attributes for which a new +Version+ will not be created if only they change. # :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).
def has_paper_trail(options = {}) def has_paper_trail(options = {})
send :include, InstanceMethods send :include, InstanceMethods
cattr_accessor :ignore cattr_accessor :ignore
self.ignore = (options[:ignore] || []).map &:to_s self.ignore = (options[:ignore] || []).map &:to_s
cattr_accessor :meta
self.meta = options[:meta] || {}
cattr_accessor :paper_trail_active cattr_accessor :paper_trail_active
self.paper_trail_active = true self.paper_trail_active = true
@ -36,26 +42,36 @@ module PaperTrail
module InstanceMethods module InstanceMethods
def record_create def record_create
versions.create(:event => 'create', if self.class.paper_trail_active
:whodunnit => PaperTrail.whodunnit) if self.class.paper_trail_active versions.create merge_metadata(:event => 'create', :whodunnit => PaperTrail.whodunnit)
end
end end
def record_update def record_update
if changed_and_we_care? and self.class.paper_trail_active if changed_and_we_care? and self.class.paper_trail_active
versions.build :event => 'update', versions.build merge_metadata(:event => 'update',
:object => object_to_string(previous_version), :object => object_to_string(previous_version),
:whodunnit => PaperTrail.whodunnit :whodunnit => PaperTrail.whodunnit)
end end
end end
def record_destroy def record_destroy
versions.create(:event => 'destroy', if self.class.paper_trail_active
:object => object_to_string(previous_version), versions.create merge_metadata(:event => 'destroy',
:whodunnit => PaperTrail.whodunnit) if self.class.paper_trail_active :object => object_to_string(previous_version),
:whodunnit => PaperTrail.whodunnit)
end
end end
private private
def merge_metadata(data)
meta.each do |k,v|
data[k] = v.respond_to?(:call) ? v.call(self) : v
end
data
end
def previous_version def previous_version
previous = self.clone previous = self.clone
previous.id = id previous.id = id

View File

@ -18,7 +18,10 @@ class Fluxor < ActiveRecord::Base
end end
class Article < ActiveRecord::Base class Article < ActiveRecord::Base
has_paper_trail :ignore => [:title] has_paper_trail :ignore => [:title],
:meta => {:answer => 42,
:question => Proc.new { "31 + 11 = #{31 + 11}" },
:article_id => Proc.new { |article| article.id } }
end end
@ -37,9 +40,9 @@ class HasPaperTrailModelTest < Test::Unit::TestCase
setup { @article.update_attributes :title => 'My first title', :content => 'Some text here.' } setup { @article.update_attributes :title => 'My first title', :content => 'Some text here.' }
should_change('the number of versions', :by => 1) { Version.count } should_change('the number of versions', :by => 1) { Version.count }
end end
end end
context 'A new record' do context 'A new record' do
setup { @widget = Widget.new } setup { @widget = Widget.new }
@ -386,4 +389,60 @@ class HasPaperTrailModelTest < Test::Unit::TestCase
end end
end end
context 'An item' do
setup { @article = Article.new }
context 'which is created' do
setup { @article.save }
should 'store fixed meta data' do
assert_equal 42, @article.versions.last.answer
end
should 'store dynamic meta data which is independent of the item' do
assert_equal '31 + 11 = 42', @article.versions.last.question
end
should 'store dynamic meta data which depends on the item' do
assert_equal @article.id, @article.versions.last.article_id
end
context 'and updated' do
setup { @article.update_attributes! :content => 'Better text.' }
should 'store fixed meta data' do
assert_equal 42, @article.versions.last.answer
end
should 'store dynamic meta data which is independent of the item' do
assert_equal '31 + 11 = 42', @article.versions.last.question
end
should 'store dynamic meta data which depends on the item' do
assert_equal @article.id, @article.versions.last.article_id
end
end
context 'and destroyd' do
setup { @article.destroy }
should 'store fixed meta data' do
assert_equal 42, @article.versions.last.answer
end
should 'store dynamic meta data which is independent of the item' do
assert_equal '31 + 11 = 42', @article.versions.last.question
end
should 'store dynamic meta data which depends on the item' do
assert_equal @article.id, @article.versions.last.article_id
end
end
end
end
end end

View File

@ -22,6 +22,11 @@ ActiveRecord::Schema.define(:version => 0) do
t.string :whodunnit t.string :whodunnit
t.text :object t.text :object
t.datetime :created_at t.datetime :created_at
# Metadata columns.
t.integer :answer
t.string :question
t.integer :article_id
end end
add_index :versions, [:item_type, :item_id] add_index :versions, [:item_type, :item_id]