revo-paper_trail 1.4.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ test/debug.log
2
+ test/paper_trail_plugin.sqlite3.db
3
+ coverage
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Andy Stewart, AirBlade Software Ltd.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,226 @@
1
+ # PaperTrail
2
+
3
+ PaperTrail lets you track changes to your models' data. It's good for auditing or versioning. You can see how a model looked at any stage in its lifecycle, revert it to any version, and even undelete it after it's been destroyed.
4
+
5
+
6
+ ## Features
7
+
8
+ * Stores every create, update and destroy.
9
+ * Does not store updates which don't change anything.
10
+ * Does not store updates which only change attributes you are ignoring.
11
+ * Allows you to get at every version, including the original, even once destroyed.
12
+ * Allows you to get at every version even if the schema has since changed.
13
+ * Automatically records who was responsible if your controller has a `current_user` method.
14
+ * Allows you to set who is responsible at model-level (useful for migrations).
15
+ * Allows you to store arbitrary metadata with each version (useful for filtering versions).
16
+ * Can be turned off/on (useful for migrations).
17
+ * No configuration necessary.
18
+ * Stores everything in a single database table (generates migration for you).
19
+ * Thoroughly tested.
20
+ * Thread safe
21
+
22
+
23
+ ## Rails Version
24
+
25
+ Known to work on Rails 2.3. Probably works on Rails 2.2 and 2.1.
26
+
27
+
28
+ ## Basic Usage
29
+
30
+ PaperTrail is simple to use. Just add 15 characters to a model to get a paper trail of every `create`, `update`, and `destroy`.
31
+
32
+ class Widget < ActiveRecord::Base
33
+ has_paper_trail
34
+ end
35
+
36
+ This gives you a `versions` method which returns the paper trail of changes to your model.
37
+
38
+ >> widget = Widget.find 42
39
+ >> widget.versions # [<Version>, <Version>, ...]
40
+
41
+ Once you have a version, you can find out what happened:
42
+
43
+ >> v = widget.versions.last
44
+ >> v.event # 'update' (or 'create' or 'destroy')
45
+ >> v.whodunnit # '153' (if the update was via a controller and
46
+ # the controller has a current_user method,
47
+ # here returning the id of the current user)
48
+ >> v.created_at # when the update occurred
49
+ >> widget = v.reify # the widget as it was before the update;
50
+ # would be nil for a create event
51
+
52
+ PaperTrail stores the pre-change version of the model, unlike some other auditing/versioning plugins, so you can retrieve the original version. This is useful when you start keeping a paper trail for models that already have records in the database.
53
+
54
+ >> widget = Widget.find 153
55
+ >> widget.name # 'Doobly'
56
+
57
+ # Add has_paper_trail to Widget model.
58
+
59
+ >> widget.versions # []
60
+ >> widget.update_attributes :name => 'Wotsit'
61
+ >> widget.versions.first.reify.name # 'Doobly'
62
+ >> widget.versions.first.event # 'update'
63
+
64
+ This also means that PaperTrail does not waste space storing a version of the object as it currently stands. The `versions` method gives you previous versions; to get the current one just call a finder on your `Widget` model as usual.
65
+
66
+ Here's a helpful table showing what PaperTrail stores:
67
+
68
+ <table>
69
+ <tr>
70
+ <th>Event</th>
71
+ <th>Model Before</th>
72
+ <th>Model After</th>
73
+ </tr>
74
+ <tr>
75
+ <td>create</td>
76
+ <td>nil</td>
77
+ <td>widget</td>
78
+ </tr>
79
+ <tr>
80
+ <td>update</td>
81
+ <td>widget</td>
82
+ <td>widget'</td>
83
+ <tr>
84
+ <td>destroy</td>
85
+ <td>widget</td>
86
+ <td>nil</td>
87
+ </tr>
88
+ </table>
89
+
90
+ PaperTrail stores the values in the Model Before column. Most other auditing/versioning plugins store the After column.
91
+
92
+
93
+ ## Ignoring changes to certain attributes
94
+
95
+ You can ignore changes to certain attributes like this:
96
+
97
+ class Article < ActiveRecord::Base
98
+ has_paper_trail :ignore => [:title, :rating]
99
+ end
100
+
101
+ This means that changes to just the `title` or `rating` will not store another version of the article. It does not mean that the `title` and `rating` attributes will be ignored if some other change causes a new `Version` to be crated. For example:
102
+
103
+ >> a = Article.create
104
+ >> a.versions.length # 1
105
+ >> a.update_attributes :title => 'My Title', :rating => 3
106
+ >> a.versions.length # 1
107
+ >> a.update_attributes :content => 'Hello'
108
+ >> a.versions.length # 2
109
+ >> a.versions.last.reify.title # 'My Title'
110
+
111
+
112
+ ## Reverting And Undeleting A Model
113
+
114
+ PaperTrail makes reverting to a previous version easy:
115
+
116
+ >> widget = Widget.find 42
117
+ >> widget.update_attributes :name => 'Blah blah'
118
+ # Time passes....
119
+ >> widget = widget.versions.last.reify # the widget as it was before the update
120
+ >> widget.save # reverted
121
+
122
+ Undeleting is just as simple:
123
+
124
+ >> widget = Widget.find 42
125
+ >> widget.destroy
126
+ # Time passes....
127
+ >> widget = Version.find(153).reify # the widget as it was before it was destroyed
128
+ >> widget.save # the widget lives!
129
+
130
+ In fact you could use PaperTrail to implement an undo system, though I haven't had the opportunity yet to do it myself.
131
+
132
+
133
+ ## Finding Out Who Was Responsible For A Change
134
+
135
+ If your `ApplicationController` has a `current_user` method, PaperTrail will store the value it returns in the `version`'s `whodunnit` column. Note that this column is a string so you will have to convert it to an integer if it's an id and you want to look up the user later on:
136
+
137
+ >> last_change = Widget.versions.last
138
+ >> user_who_made_the_change = User.find last_change.whodunnit.to_i
139
+
140
+ In a migration or in `script/console` you can set who is responsible like this:
141
+
142
+ >> PaperTrail.whodunnit = 'Andy Stewart'
143
+ >> widget.update_attributes :name => 'Wibble'
144
+ >> widget.versions.last.whodunnit # Andy Stewart
145
+
146
+
147
+ ## Storing metadata
148
+
149
+ You can store arbitrary metadata alongside each version like this:
150
+
151
+ class Article < ActiveRecord::Base
152
+ belongs_to :author
153
+ has_paper_trail :meta => { :author_id => Proc.new { |article| article.author_id },
154
+ :answer => 42 }
155
+ end
156
+
157
+ 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.)
158
+
159
+ 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:
160
+
161
+ Version.all(:conditions => ['author_id = ?', author_id])
162
+
163
+
164
+ ## Turning PaperTrail Off/On
165
+
166
+ Sometimes you don't want to store changes. Perhaps you are only interested in changes made
167
+ by your users and don't need to store changes you make yourself in, say, a migration.
168
+
169
+ If you are about change some widgets and you don't want a paper trail of your changes, you can
170
+ turn PaperTrail off like this:
171
+
172
+ >> Widget.paper_trail_off
173
+
174
+ And on again like this:
175
+
176
+ >> Widget.paper_trail_on
177
+
178
+
179
+ ## Installation
180
+
181
+ 1. Install PaperTrail either as a gem (from Gemcutter; the ones on GitHub are obsolete) or as a plugin:
182
+
183
+ `config.gem 'paper_trail', :source => 'http://gemcutter.org'`
184
+
185
+ or:
186
+
187
+ `script/plugin install git://github.com/airblade/paper_trail.git`
188
+
189
+ 2. Generate a migration which will add a `versions` table to your database.
190
+
191
+ `script/generate paper_trail`
192
+
193
+ 3. Run the migration.
194
+
195
+ `rake db:migrate`
196
+
197
+ 4. Add `has_paper_trail` to the models you want to track.
198
+
199
+
200
+ ## Testing
201
+
202
+ PaperTrail has a thorough suite of tests. Thanks to [Zachery Hostens](http://github.com/zacheryph) for making them able to run standalone, i.e. without needing PaperTrail to be sitting in a Rails app.
203
+
204
+
205
+ ## Problems
206
+
207
+ Please use GitHub's [issue tracker](http://github.com/airblade/paper_trail/issues).
208
+
209
+
210
+ ## Contributors
211
+
212
+ Many thanks to:
213
+
214
+ * [Zachery Hostens](http://github.com/zacheryph)
215
+
216
+
217
+ ## Inspirations
218
+
219
+ * [Simply Versioned](http://github.com/github/simply_versioned)
220
+ * [Acts As Audited](http://github.com/collectiveidea/acts_as_audited)
221
+
222
+
223
+ ## Intellectual Property
224
+
225
+ Copyright (c) 2009 Andy Stewart (boss@airbladesoftware.com).
226
+ Released under the MIT licence.
data/Rakefile ADDED
@@ -0,0 +1,50 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ begin
6
+ require 'jeweler'
7
+ Jeweler::Tasks.new do |gemspec|
8
+ gemspec.name = 'revo-paper_trail'
9
+ gemspec.summary = "Track changes to your models' data. Good for auditing or versioning."
10
+ gemspec.email = 'boss@airbladesoftware.com, ge_developers@playup.com'
11
+ gemspec.homepage = 'http://github.com/airblade/paper_trail'
12
+ gemspec.authors = ['Andy Stewart', 'Revo Developers']
13
+ end
14
+ Jeweler::GemcutterTasks.new
15
+ rescue LoadError
16
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
17
+ end
18
+
19
+ desc 'Test the paper_trail plugin.'
20
+ Rake::TestTask.new(:test) do |t|
21
+ t.libs << 'lib'
22
+ t.libs << 'test'
23
+ t.pattern = 'test/**/*_test.rb'
24
+ t.verbose = true
25
+ end
26
+
27
+ begin
28
+ require 'rcov/rcovtask'
29
+ Rcov::RcovTask.new do |test|
30
+ test.libs << 'test'
31
+ test.pattern = 'test/**/*_test.rb'
32
+ test.verbose = true
33
+ end
34
+ rescue LoadError
35
+ task :rcov do
36
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
37
+ end
38
+ end
39
+
40
+ desc 'Generate documentation for the paper_trail plugin.'
41
+ Rake::RDocTask.new(:rdoc) do |rdoc|
42
+ rdoc.rdoc_dir = 'rdoc'
43
+ rdoc.title = 'PaperTrail'
44
+ rdoc.options << '--line-numbers' << '--inline-source'
45
+ rdoc.rdoc_files.include('README')
46
+ rdoc.rdoc_files.include('lib/**/*.rb')
47
+ end
48
+
49
+ desc 'Default: run unit tests.'
50
+ task :default => :test
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.4.2
@@ -0,0 +1,2 @@
1
+ Description:
2
+ Generates (but does not run) a migration to add a versions table.
@@ -0,0 +1,9 @@
1
+ class PaperTrailGenerator < Rails::Generator::Base
2
+
3
+ def manifest
4
+ record do |m|
5
+ m.migration_template 'create_versions.rb', 'db/migrate', :migration_file_name => 'create_versions'
6
+ end
7
+ end
8
+
9
+ end
@@ -0,0 +1,18 @@
1
+ class CreateVersions < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :versions do |t|
4
+ t.string :item_type, :null => false
5
+ t.integer :item_id, :null => false
6
+ t.string :event, :null => false
7
+ t.string :whodunnit
8
+ t.text :object
9
+ t.datetime :created_at
10
+ end
11
+ add_index :versions, [:item_type, :item_id]
12
+ end
13
+
14
+ def self.down
15
+ remove_index :versions, [:item_type, :item_id]
16
+ drop_table :versions
17
+ end
18
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ # Include hook code here
data/install.rb ADDED
@@ -0,0 +1 @@
1
+ # Install hook code here
@@ -0,0 +1,21 @@
1
+ require 'yaml'
2
+ require 'paper_trail/has_paper_trail'
3
+ require 'paper_trail/version'
4
+
5
+ module PaperTrail
6
+
7
+ def self.included(base)
8
+ base.before_filter :set_whodunnit
9
+ end
10
+
11
+ def self.whodunnit
12
+ Thread.current[:whodunnit]
13
+ end
14
+
15
+ private
16
+ def set_whodunnit
17
+ Thread.current[:whodunnit] = self.send :current_user rescue nil
18
+ end
19
+ end
20
+
21
+ ActionController::Base.send :include, PaperTrail
@@ -0,0 +1,95 @@
1
+ module PaperTrail
2
+
3
+ def self.included(base)
4
+ base.send :extend, ClassMethods
5
+ end
6
+
7
+
8
+ module ClassMethods
9
+ # Options:
10
+ # :ignore an array of attributes for which a new +Version+ will not be created if only they change.
11
+ # :meta a hash of extra data to store. You must add a column to the versions table for each key.
12
+ # Values are objects or procs (which are called with +self+, i.e. the model with the paper
13
+ # trail).
14
+ def has_paper_trail(options = {})
15
+ send :include, InstanceMethods
16
+
17
+ cattr_accessor :ignore
18
+ self.ignore = (options[:ignore] || []).map &:to_s
19
+
20
+ cattr_accessor :meta
21
+ self.meta = options[:meta] || {}
22
+
23
+ cattr_accessor :paper_trail_active
24
+ self.paper_trail_active = true
25
+
26
+ has_many :versions, :as => :item, :order => 'created_at ASC, id ASC'
27
+
28
+ after_create :record_create
29
+ before_update :record_update
30
+ after_destroy :record_destroy
31
+ end
32
+
33
+ def paper_trail_off
34
+ self.paper_trail_active = false
35
+ end
36
+
37
+ def paper_trail_on
38
+ self.paper_trail_active = true
39
+ end
40
+ end
41
+
42
+
43
+ module InstanceMethods
44
+ def record_create
45
+ if self.class.paper_trail_active
46
+ versions.create merge_metadata(:event => 'create', :whodunnit => PaperTrail.whodunnit)
47
+ end
48
+ end
49
+
50
+ def record_update
51
+ if changed_and_we_care? and self.class.paper_trail_active
52
+ versions.build merge_metadata(:event => 'update',
53
+ :object => object_to_string(previous_version),
54
+ :whodunnit => PaperTrail.whodunnit)
55
+ end
56
+ end
57
+
58
+ def record_destroy
59
+ if self.class.paper_trail_active
60
+ versions.create merge_metadata(:event => 'destroy',
61
+ :object => object_to_string(previous_version),
62
+ :whodunnit => PaperTrail.whodunnit)
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ def merge_metadata(data)
69
+ meta.each do |k,v|
70
+ data[k] = v.respond_to?(:call) ? v.call(self) : v
71
+ end
72
+ data
73
+ end
74
+
75
+ def previous_version
76
+ previous = self.clone
77
+ previous.id = id
78
+ changes.each do |attr, ary|
79
+ previous.send "#{attr}=", ary.first
80
+ end
81
+ previous
82
+ end
83
+
84
+ def object_to_string(object)
85
+ object.attributes.to_yaml
86
+ end
87
+
88
+ def changed_and_we_care?
89
+ changed? and !(changed - self.class.ignore).empty?
90
+ end
91
+ end
92
+
93
+ end
94
+
95
+ ActiveRecord::Base.send :include, PaperTrail