Store user-defined metadata.
This commit is contained in:
parent
e71252ce29
commit
55185cd448
21
README.md
21
README.md
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue