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
* 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 even if the schema has since changed.
* 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 store arbitrary metadata with each version (useful for filtering versions).
* Can be turned off/on (useful for migrations).
* No configuration necessary.
* 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
## 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
Sometimes you don't want to store changes. Perhaps you are only interested in changes made

View File

@ -8,11 +8,17 @@ module PaperTrail
module ClassMethods
# 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).
def has_paper_trail(options = {})
send :include, InstanceMethods
cattr_accessor :ignore
self.ignore = (options[:ignore] || []).map &:to_s
cattr_accessor :meta
self.meta = options[:meta] || {}
cattr_accessor :paper_trail_active
self.paper_trail_active = true
@ -36,26 +42,36 @@ module PaperTrail
module InstanceMethods
def record_create
versions.create(:event => 'create',
:whodunnit => PaperTrail.whodunnit) if self.class.paper_trail_active
if self.class.paper_trail_active
versions.create merge_metadata(:event => 'create', :whodunnit => PaperTrail.whodunnit)
end
end
def record_update
if changed_and_we_care? and self.class.paper_trail_active
versions.build :event => 'update',
:object => object_to_string(previous_version),
:whodunnit => PaperTrail.whodunnit
versions.build merge_metadata(:event => 'update',
:object => object_to_string(previous_version),
:whodunnit => PaperTrail.whodunnit)
end
end
def record_destroy
versions.create(:event => 'destroy',
:object => object_to_string(previous_version),
:whodunnit => PaperTrail.whodunnit) if self.class.paper_trail_active
if self.class.paper_trail_active
versions.create merge_metadata(:event => 'destroy',
:object => object_to_string(previous_version),
:whodunnit => PaperTrail.whodunnit)
end
end
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
previous = self.clone
previous.id = id

View File

@ -18,7 +18,10 @@ class Fluxor < ActiveRecord::Base
end
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
@ -37,9 +40,9 @@ class HasPaperTrailModelTest < Test::Unit::TestCase
setup { @article.update_attributes :title => 'My first title', :content => 'Some text here.' }
should_change('the number of versions', :by => 1) { Version.count }
end
end
context 'A new record' do
setup { @widget = Widget.new }
@ -386,4 +389,60 @@ class HasPaperTrailModelTest < Test::Unit::TestCase
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

View File

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