jeremyw-paper_trail 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ test/debug.log
2
+ test/paper_trail_plugin.sqlite3.db
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,180 @@
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
+ * Allows you to get at every version, including the original, even once destroyed.
11
+ * Allows you to get at every version even if the schema has since changed.
12
+ * Automatically records who was responsible if your controller has a `current_user` method.
13
+ * Allows you to set who is responsible at model-level (useful for migrations).
14
+ * Can be turned off/on (useful for migrations).
15
+ * No configuration necessary.
16
+ * Stores everything in a single database table (generates migration for you).
17
+ * Thoroughly tested.
18
+
19
+
20
+ ## Rails Version
21
+
22
+ Known to work on Rails 2.3. Probably works on Rails 2.2 and 2.1.
23
+
24
+
25
+ ## Basic Usage
26
+
27
+ PaperTrail is simple to use. Just add 15 characters to a model to get a paper trail of every `create`, `update`, and `destroy`.
28
+
29
+ class Widget < ActiveRecord::Base
30
+ has_paper_trail
31
+ end
32
+
33
+ This gives you a `versions` method which returns the paper trail of changes to your model.
34
+
35
+ >> widget = Widget.find 42
36
+ >> widget.versions # [<Version>, <Version>, ...]
37
+
38
+ Once you have a version, you can find out what happened:
39
+
40
+ >> v = widget.versions.last
41
+ >> v.event # 'update' (or 'create' or 'destroy')
42
+ >> v.whodunnit # '153' (if the update was via a controller and
43
+ # the controller has a current_user method,
44
+ # here returning the id of the current user)
45
+ >> v.created_at # when the update occurred
46
+ >> widget = v.reify # the widget as it was before the update;
47
+ # would be nil for a create event
48
+
49
+ 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.
50
+
51
+ >> widget = Widget.find 153
52
+ >> widget.name # 'Doobly'
53
+
54
+ # Add has_paper_trail to Widget model.
55
+
56
+ >> widget.versions # []
57
+ >> widget.update_attributes :name => 'Wotsit'
58
+ >> widget.versions.first.reify.name # 'Doobly'
59
+ >> widget.versions.first.event # 'update'
60
+
61
+ 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.
62
+
63
+ Here's a helpful table showing what PaperTrail stores:
64
+
65
+ <table>
66
+ <tr>
67
+ <th>Event</th>
68
+ <th>Model Before</th>
69
+ <th>Model After</th>
70
+ </tr>
71
+ <tr>
72
+ <td>create</td>
73
+ <td>nil</td>
74
+ <td>widget</td>
75
+ </tr>
76
+ <tr>
77
+ <td>update</td>
78
+ <td>widget</td>
79
+ <td>widget'</td>
80
+ <tr>
81
+ <td>destroy</td>
82
+ <td>widget</td>
83
+ <td>nil</td>
84
+ </tr>
85
+ </table>
86
+
87
+ PaperTrail stores the values in the Model Before column. Most other auditing/versioning plugins store the After column.
88
+
89
+
90
+ ## Reverting And Undeleting A Model
91
+
92
+ PaperTrail makes reverting to a previous version easy:
93
+
94
+ >> widget = Widget.find 42
95
+ >> widget.update_attributes :name => 'Blah blah'
96
+ # Time passes....
97
+ >> widget = widget.versions.last.reify # the widget as it was before the update
98
+ >> widget.save # reverted
99
+
100
+ Undeleting is just as simple:
101
+
102
+ >> widget = Widget.find 42
103
+ >> widget.destroy
104
+ # Time passes....
105
+ >> widget = Version.find(153).reify # the widget as it was before it was destroyed
106
+ >> widget.save # the widget lives!
107
+
108
+ In fact you could use PaperTrail to implement an undo system, though I haven't had the opportunity yet to do it myself.
109
+
110
+
111
+ ## Finding Out Who Was Responsible For A Change
112
+
113
+ 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:
114
+
115
+ >> last_change = Widget.versions.last
116
+ >> user_who_made_the_change = User.find last_change.whodunnit.to_i
117
+
118
+ In a migration or in `script/console` you can set who is responsible like this:
119
+
120
+ >> PaperTrail.whodunnit = 'Andy Stewart'
121
+ >> widget.update_attributes :name => 'Wibble'
122
+ >> widget.versions.last.whodunnit # Andy Stewart
123
+
124
+
125
+ ## Turning PaperTrail Off/On
126
+
127
+ Sometimes you don't want to store changes. Perhaps you are only interested in changes made
128
+ by your users and don't need to store changes you make yourself in, say, a migration.
129
+
130
+ If you are about change some widgets and you don't want a paper trail of your changes, you can
131
+ turn PaperTrail off like this:
132
+
133
+ >> Widget.paper_trail_off
134
+
135
+ And on again like this:
136
+
137
+ >> Widget.paper_trail_on
138
+
139
+
140
+ ## Installation
141
+
142
+ 1. Install PaperTrail either as a gem or as a plugin:
143
+
144
+ `config.gem 'airblade-paper_trail', :lib => 'paper_trail', :source => 'http://gems.github.com'`
145
+
146
+ or:
147
+
148
+ `script/plugin install git://github.com/airblade/paper_trail.git`
149
+
150
+ 2. Generate a migration which will add a `versions` table to your database.
151
+
152
+ `script/generate paper_trail`
153
+
154
+ 3. Run the migration.
155
+
156
+ `rake db:migrate`
157
+
158
+ 4. Add `has_paper_trail` to the models you want to track.
159
+
160
+
161
+ ## Testing
162
+
163
+ PaperTrail has a thorough suite of tests. However they only run when PaperTrail is sitting in a Rails app's `vendor/plugins` directory. If anyone can tell me how to get them to run outside of a Rails app, I'd love to hear it.
164
+
165
+
166
+ ## Problems
167
+
168
+ Please use GitHub's [issue tracker](http://github.com/airblade/paper_trail/issues).
169
+
170
+
171
+ ## Inspirations
172
+
173
+ * [Simply Versioned](http://github.com/github/simply_versioned)
174
+ * [Acts As Audited](http://github.com/collectiveidea/acts_as_audited)
175
+
176
+
177
+ ## Intellectual Property
178
+
179
+ Copyright (c) 2009 Andy Stewart (boss@airbladesoftware.com).
180
+ Released under the MIT licence.
data/Rakefile ADDED
@@ -0,0 +1,49 @@
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 = 'paper_trail'
9
+ gemspec.summary = "Track changes to your models' data. Good for auditing or versioning."
10
+ gemspec.email = 'boss@airbladesoftware.com'
11
+ gemspec.homepage = 'http://github.com/airblade/paper_trail'
12
+ gemspec.authors = ['Andy Stewart']
13
+ end
14
+ rescue LoadError
15
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
16
+ end
17
+
18
+ desc 'Test the paper_trail plugin.'
19
+ Rake::TestTask.new(:test) do |t|
20
+ t.libs << 'lib'
21
+ t.libs << 'test'
22
+ t.pattern = 'test/**/*_test.rb'
23
+ t.verbose = true
24
+ end
25
+
26
+ begin
27
+ require 'rcov/rcovtask'
28
+ Rcov::RcovTask.new do |test|
29
+ test.libs << 'test'
30
+ test.pattern = 'test/**/*_test.rb'
31
+ test.verbose = true
32
+ end
33
+ rescue LoadError
34
+ task :rcov do
35
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
36
+ end
37
+ end
38
+
39
+ desc 'Generate documentation for the paper_trail plugin.'
40
+ Rake::RDocTask.new(:rdoc) do |rdoc|
41
+ rdoc.rdoc_dir = 'rdoc'
42
+ rdoc.title = 'PaperTrail'
43
+ rdoc.options << '--line-numbers' << '--inline-source'
44
+ rdoc.rdoc_files.include('README')
45
+ rdoc.rdoc_files.include('lib/**/*.rb')
46
+ end
47
+
48
+ desc 'Default: run unit tests.'
49
+ task :default => :test
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.1.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,89 @@
1
+ require 'yaml'
2
+ require 'paper_trail/has_paper_trail'
3
+ require 'paper_trail/version'
4
+
5
+ module PaperTrail
6
+ @@whodunnit = nil
7
+
8
+ def self.included(base)
9
+ base.before_filter :set_whodunnit
10
+ end
11
+
12
+ def self.whodunnit
13
+ @@whodunnit.respond_to?(:call) ? @@whodunnit.call : @@whodunnit
14
+ end
15
+
16
+ def self.whodunnit=(value)
17
+ @@whodunnit = value
18
+ end
19
+
20
+ # Walk the versions to construct an audit trail of the edits made
21
+ # over time, and by whom.
22
+ def audit_trail options={}
23
+ options[:attributes_to_ignore] ||= %w(updated_at)
24
+
25
+ audit_trail = []
26
+
27
+ versions_desc = versions_including_current_in_descending_order
28
+
29
+ versions_desc.each_with_index do |version, index|
30
+ previous_version = versions_desc[index + 1]
31
+ break if previous_version.nil?
32
+
33
+ attributes_after = yaml_to_hash(version.object)
34
+ attributes_before = yaml_to_hash(previous_version.object)
35
+
36
+ # remove some attributes that we don't need to report
37
+ [attributes_before, attributes_after].each do |hash|
38
+ hash.reject! { |k,v| k.in? Array(options[:attributes_to_ignore]) }
39
+ end
40
+
41
+ audit_trail << {
42
+ :event => previous_version.event,
43
+ :changed_by => transform_whodunnit(previous_version.whodunnit),
44
+ :changed_at => previous_version.created_at,
45
+ :changes => differences(attributes_before, attributes_after)
46
+ }
47
+ end
48
+
49
+ audit_trail
50
+ end
51
+
52
+ protected
53
+
54
+ def transform_whodunnit(whodunnit)
55
+ whodunnit
56
+ end
57
+
58
+ private
59
+
60
+ def set_whodunnit
61
+ @@whodunnit = lambda {
62
+ self.send :current_user rescue nil
63
+ }
64
+ end
65
+
66
+ def yaml_to_hash(yaml)
67
+ return {} if yaml.nil?
68
+ YAML::load(yaml).to_hash
69
+ end
70
+
71
+ # Returns an array of hashes, where each hash specifies the +:attribute+,
72
+ # value +:before+ the change, and value +:after+ the change.
73
+ def differences(before, after)
74
+ before.diff(after).keys.sort.inject([]) do |diffs, k|
75
+ diff = { :attribute => k, :before => before[k], :after => after[k] }
76
+ diffs << diff; diffs
77
+ end
78
+ end
79
+
80
+ def versions_including_current_in_descending_order
81
+ v = self.versions.dup
82
+ v << Version.new(:event => 'update',
83
+ :object => object_to_string(self),
84
+ :created_at => self.updated_at)
85
+ v.reverse # newest first
86
+ end
87
+ end
88
+
89
+ ActionController::Base.send :include, PaperTrail
@@ -0,0 +1,70 @@
1
+ module PaperTrail
2
+
3
+ def self.included(base)
4
+ base.send :extend, ClassMethods
5
+ end
6
+
7
+
8
+ module ClassMethods
9
+ def has_paper_trail
10
+ send :include, InstanceMethods
11
+
12
+ cattr_accessor :paper_trail_active
13
+ self.paper_trail_active = true
14
+
15
+ has_many :versions, :as => :item, :order => 'created_at ASC, id ASC'
16
+
17
+ after_create :record_create
18
+ before_update :record_update
19
+ after_destroy :record_destroy
20
+ end
21
+
22
+ def paper_trail_off
23
+ self.paper_trail_active = false
24
+ end
25
+
26
+ def paper_trail_on
27
+ self.paper_trail_active = true
28
+ end
29
+ end
30
+
31
+
32
+ module InstanceMethods
33
+ def record_create
34
+ versions.create(:event => 'create',
35
+ :whodunnit => PaperTrail.whodunnit) if self.class.paper_trail_active
36
+ end
37
+
38
+ def record_update
39
+ if changed? and self.class.paper_trail_active
40
+ versions.build :event => 'update',
41
+ :object => object_to_string(previous_version),
42
+ :whodunnit => PaperTrail.whodunnit
43
+ end
44
+ end
45
+
46
+ def record_destroy
47
+ versions.create(:event => 'destroy',
48
+ :object => object_to_string(previous_version),
49
+ :whodunnit => PaperTrail.whodunnit) if self.class.paper_trail_active
50
+ end
51
+
52
+ private
53
+
54
+ def previous_version
55
+ previous = self.clone
56
+ previous.id = id
57
+ changes.each do |attr, ary|
58
+ previous.send "#{attr}=", ary.first
59
+ end
60
+ previous
61
+ end
62
+
63
+ def object_to_string(object)
64
+ object.attributes.to_yaml
65
+ end
66
+ end
67
+
68
+ end
69
+
70
+ ActiveRecord::Base.send :include, PaperTrail
@@ -0,0 +1,59 @@
1
+ class Version < ActiveRecord::Base
2
+ belongs_to :item, :polymorphic => true
3
+ validates_presence_of :event
4
+
5
+ def reify
6
+ unless object.nil?
7
+ # Attributes
8
+
9
+ attrs = YAML::load object
10
+
11
+ # Normally a polymorphic belongs_to relationship allows us
12
+ # to get the object we belong to by calling, in this case,
13
+ # +item+. However this returns nil if +item+ has been
14
+ # destroyed, and we need to be able to retrieve destroyed
15
+ # objects.
16
+ #
17
+ # In this situation we constantize the +item_type+ to get hold of
18
+ # the class...except when the stored object's attributes
19
+ # include a +type+ key. If this is the case, the object
20
+ # we belong to is using single table inheritance and the
21
+ # +item_type+ will be the base class, not the actual subclass.
22
+ # If +type+ is present but empty, the class is the base class.
23
+
24
+ if item
25
+ model = item
26
+ else
27
+ class_name = attrs['type'].blank? ? item_type : attrs['type']
28
+ klass = class_name.constantize
29
+ model = klass.new
30
+ end
31
+
32
+ attrs.each do |k, v|
33
+ begin
34
+ model.send "#{k}=", v
35
+ rescue NoMethodError
36
+ RAILS_DEFAULT_LOGGER.warn "Attribute #{k} does not exist on #{item_type} (Version id: #{id})."
37
+ end
38
+ end
39
+
40
+ model
41
+ end
42
+ end
43
+
44
+ def next
45
+ Version.first :conditions => ["id > ? AND item_type = ? AND item_id = ?", id, item_type, item_id],
46
+ :order => 'id ASC'
47
+ end
48
+
49
+ def previous
50
+ Version.first :conditions => ["id < ? AND item_type = ? AND item_id = ?", id, item_type, item_id],
51
+ :order => 'id DESC'
52
+ end
53
+
54
+ def index
55
+ Version.all(:conditions => ["item_type = ? AND item_id = ?", item_type, item_id],
56
+ :order => 'id ASC').index(self)
57
+ end
58
+
59
+ end
@@ -0,0 +1,64 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{paper_trail}
5
+ s.version = "1.2.2"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Andy Stewart", "Jeremy Weiskotten", "Joe Lind"]
9
+ s.date = %q{2009-07-31}
10
+ s.email = %q{boss@airbladesoftware.com}
11
+ s.extra_rdoc_files = [
12
+ "README.md"
13
+ ]
14
+ s.files = [
15
+ ".gitignore",
16
+ "MIT-LICENSE",
17
+ "README.md",
18
+ "Rakefile",
19
+ "VERSION",
20
+ "generators/paper_trail/USAGE",
21
+ "generators/paper_trail/paper_trail_generator.rb",
22
+ "generators/paper_trail/templates/create_versions.rb",
23
+ "init.rb",
24
+ "install.rb",
25
+ "lib/paper_trail.rb",
26
+ "lib/paper_trail/has_paper_trail.rb",
27
+ "lib/paper_trail/version.rb",
28
+ "paper_trail.gemspec",
29
+ "rails/init.rb",
30
+ "tasks/paper_trail_tasks.rake",
31
+ "test/database.yml",
32
+ "test/paper_trail_controller_test.rb",
33
+ "test/paper_trail_model_test.rb",
34
+ "test/paper_trail_schema_test.rb",
35
+ "test/schema.rb",
36
+ "test/schema_change.rb",
37
+ "test/test_helper.rb",
38
+ "uninstall.rb"
39
+ ]
40
+ s.has_rdoc = true
41
+ s.homepage = %q{http://github.com/airblade/paper_trail}
42
+ s.rdoc_options = ["--charset=UTF-8"]
43
+ s.require_paths = ["lib"]
44
+ s.rubygems_version = %q{1.3.1}
45
+ s.summary = %q{Track changes to your models' data. Good for auditing or versioning.}
46
+ s.test_files = [
47
+ "test/paper_trail_controller_test.rb",
48
+ "test/paper_trail_model_test.rb",
49
+ "test/paper_trail_schema_test.rb",
50
+ "test/schema.rb",
51
+ "test/schema_change.rb",
52
+ "test/test_helper.rb"
53
+ ]
54
+
55
+ if s.respond_to? :specification_version then
56
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
57
+ s.specification_version = 2
58
+
59
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
60
+ else
61
+ end
62
+ else
63
+ end
64
+ end
data/rails/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'paper_trail'
File without changes
data/test/database.yml ADDED
@@ -0,0 +1,22 @@
1
+ sqlite:
2
+ :adapter: sqlite
3
+ :dbfile: vendor/plugins/paper_trail/test/paper_trail_plugin.sqlite.db
4
+
5
+ sqlite3:
6
+ :adapter: sqlite3
7
+ :dbfile: vendor/plugins/paper_trail/test/paper_trail_plugin.sqlite3.db
8
+
9
+ postgresql:
10
+ :adapter: postgresql
11
+ :username: postgres
12
+ :password: postgres
13
+ :database: paper_trail_plugin_test
14
+ :min_messages: ERROR
15
+
16
+ mysql:
17
+ :adapter: mysql
18
+ :host: localhost
19
+ :username: andy
20
+ :password:
21
+ :database: paper_trail_plugin_test
22
+ :socket: /tmp/mysql.sock
@@ -0,0 +1,72 @@
1
+ require File.dirname(__FILE__) + '/test_helper.rb'
2
+ require 'application_controller'
3
+ require 'action_controller/test_process'
4
+
5
+ class ApplicationController
6
+ def rescue_action(e)
7
+ raise e
8
+ end
9
+
10
+ # Returns id of hypothetical current user
11
+ def current_user
12
+ 153
13
+ end
14
+ end
15
+
16
+ class WidgetsController < ApplicationController
17
+ def create
18
+ @widget = Widget.create params[:widget]
19
+ head :ok
20
+ end
21
+
22
+ def update
23
+ @widget = Widget.find params[:id]
24
+ @widget.update_attributes params[:widget]
25
+ head :ok
26
+ end
27
+
28
+ def destroy
29
+ @widget = Widget.find params[:id]
30
+ @widget.destroy
31
+ head :ok
32
+ end
33
+ end
34
+
35
+
36
+ class PaperTrailControllerTest < ActionController::TestCase #Test::Unit::TestCase
37
+ def setup
38
+ @controller = WidgetsController.new
39
+ @request = ActionController::TestRequest.new
40
+ @response = ActionController::TestResponse.new
41
+
42
+ ActionController::Routing::Routes.draw do |map|
43
+ map.resources :widgets
44
+ end
45
+ end
46
+
47
+ test 'create' do
48
+ post :create, :widget => { :name => 'Flugel' }
49
+ widget = assigns(:widget)
50
+ assert_equal 1, widget.versions.length
51
+ assert_equal 153, widget.versions.last.whodunnit.to_i
52
+ end
53
+
54
+ test 'update' do
55
+ w = Widget.create :name => 'Duvel'
56
+ assert_equal 1, w.versions.length
57
+ put :update, :id => w.id, :widget => { :name => 'Bugle' }
58
+ widget = assigns(:widget)
59
+ assert_equal 2, widget.versions.length
60
+ assert_equal 153, widget.versions.last.whodunnit.to_i
61
+ end
62
+
63
+ test 'destroy' do
64
+ w = Widget.create :name => 'Roundel'
65
+ assert_equal 1, w.versions.length
66
+ delete :destroy, :id => w.id
67
+ widget = assigns(:widget)
68
+ assert_equal 2, widget.versions.length
69
+ assert_equal 153, widget.versions.last.whodunnit.to_i
70
+ end
71
+ end
72
+
@@ -0,0 +1,370 @@
1
+ require File.dirname(__FILE__) + '/test_helper.rb'
2
+
3
+ class Widget < ActiveRecord::Base
4
+ has_paper_trail
5
+ has_one :wotsit
6
+ has_many :fluxors, :order => :name
7
+ end
8
+
9
+ class FooWidget < Widget
10
+ end
11
+
12
+ class Wotsit < ActiveRecord::Base
13
+ belongs_to :widget
14
+ end
15
+
16
+ class Fluxor < ActiveRecord::Base
17
+ belongs_to :widget
18
+ end
19
+
20
+
21
+ class HasPaperTrailModelTest < Test::Unit::TestCase
22
+ load_schema
23
+
24
+ context 'A new record' do
25
+ setup { @widget = Widget.new }
26
+
27
+ should 'not have any previous versions' do
28
+ assert_equal [], @widget.versions
29
+ end
30
+
31
+
32
+ context 'which is then created' do
33
+ setup { @widget.update_attributes :name => 'Henry' }
34
+
35
+ should 'have one previous version' do
36
+ assert_equal 1, @widget.versions.length
37
+ end
38
+
39
+ should 'be nil in its previous version' do
40
+ assert_nil @widget.versions.first.object
41
+ assert_nil @widget.versions.first.reify
42
+ end
43
+
44
+ should 'record the correct event' do
45
+ assert_match /create/i, @widget.versions.first.event
46
+ end
47
+
48
+
49
+ context 'and then updated without any changes' do
50
+ setup { @widget.save }
51
+
52
+ should 'not have a new version' do
53
+ assert_equal 1, @widget.versions.length
54
+ end
55
+ end
56
+
57
+
58
+ context 'and then updated with changes' do
59
+ setup { @widget.update_attributes :name => 'Harry' }
60
+
61
+ should 'have two previous versions' do
62
+ assert_equal 2, @widget.versions.length
63
+ end
64
+
65
+ should 'be available in its previous version' do
66
+ assert_equal 'Harry', @widget.name
67
+ assert_not_nil @widget.versions.last.object
68
+ widget = @widget.versions.last.reify
69
+ assert_equal 'Henry', widget.name
70
+ assert_equal 'Harry', @widget.name
71
+ end
72
+
73
+ should 'have the same ID in its previous version' do
74
+ assert_equal @widget.id, @widget.versions.last.reify.id
75
+ end
76
+
77
+ should 'record the correct event' do
78
+ assert_match /update/i, @widget.versions.last.event
79
+ end
80
+
81
+
82
+ context 'and has one associated object' do
83
+ setup do
84
+ @wotsit = @widget.create_wotsit :name => 'John'
85
+ @reified_widget = @widget.versions.last.reify
86
+ end
87
+
88
+ should 'copy the has_one association when reifying' do
89
+ assert_equal @wotsit, @reified_widget.wotsit
90
+ end
91
+ end
92
+
93
+
94
+ context 'and has many associated objects' do
95
+ setup do
96
+ @f0 = @widget.fluxors.create :name => 'f-zero'
97
+ @f1 = @widget.fluxors.create :name => 'f-one'
98
+ @reified_widget = @widget.versions.last.reify
99
+ end
100
+
101
+ should 'copy the has_many associations when reifying' do
102
+ assert_equal @widget.fluxors.length, @reified_widget.fluxors.length
103
+ assert_same_elements @widget.fluxors, @reified_widget.fluxors
104
+
105
+ assert_equal @widget.versions.length, @reified_widget.versions.length
106
+ assert_same_elements @widget.versions, @reified_widget.versions
107
+ end
108
+ end
109
+
110
+
111
+ context 'and then destroyed' do
112
+ setup do
113
+ @fluxor = @widget.fluxors.create :name => 'flux'
114
+ @widget.destroy
115
+ @reified_widget = @widget.versions.last.reify
116
+ end
117
+
118
+ should 'record the correct event' do
119
+ assert_match /destroy/i, @widget.versions.last.event
120
+ end
121
+
122
+ should 'have three previous versions' do
123
+ assert_equal 3, @widget.versions.length
124
+ end
125
+
126
+ should 'be available in its previous version' do
127
+ assert_equal @widget.id, @reified_widget.id
128
+ assert_equal @widget.attributes, @reified_widget.attributes
129
+ end
130
+
131
+ should 'be re-creatable from its previous version' do
132
+ assert @reified_widget.save
133
+ end
134
+
135
+ should 'restore its associations on its previous version' do
136
+ @reified_widget.save
137
+ assert_equal 1, @reified_widget.fluxors.length
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
143
+
144
+
145
+ # Test the serialisation and deserialisation.
146
+ # TODO: binary
147
+ context "A record's papertrail" do
148
+ setup do
149
+ @date_time = DateTime.now.utc
150
+ @time = Time.now
151
+ @date = Date.new 2009, 5, 29
152
+ @widget = Widget.create :name => 'Warble',
153
+ :a_text => 'The quick brown fox',
154
+ :an_integer => 42,
155
+ :a_float => 153.01,
156
+ :a_decimal => 2.71828,
157
+ :a_datetime => @date_time,
158
+ :a_time => @time,
159
+ :a_date => @date,
160
+ :a_boolean => true
161
+
162
+ @widget.update_attributes :name => nil,
163
+ :a_text => nil,
164
+ :an_integer => nil,
165
+ :a_float => nil,
166
+ :a_decimal => nil,
167
+ :a_datetime => nil,
168
+ :a_time => nil,
169
+ :a_date => nil,
170
+ :a_boolean => false
171
+ @previous = @widget.versions.last.reify
172
+ end
173
+
174
+ should 'handle strings' do
175
+ assert_equal 'Warble', @previous.name
176
+ end
177
+
178
+ should 'handle text' do
179
+ assert_equal 'The quick brown fox', @previous.a_text
180
+ end
181
+
182
+ should 'handle integers' do
183
+ assert_equal 42, @previous.an_integer
184
+ end
185
+
186
+ should 'handle floats' do
187
+ assert_in_delta 153.01, @previous.a_float, 0.001
188
+ end
189
+
190
+ should 'handle decimals' do
191
+ assert_in_delta 2.71828, @previous.a_decimal, 0.00001
192
+ end
193
+
194
+ should 'handle datetimes' do
195
+ # Is there a better way to test equality of two datetimes?
196
+ format = '%a, %d %b %Y %H:%M:%S %z' # :rfc822
197
+ assert_equal @date_time.strftime(format), @previous.a_datetime.strftime(format)
198
+ end
199
+
200
+ should 'handle times' do
201
+ assert_equal @time, @previous.a_time
202
+ end
203
+
204
+ should 'handle dates' do
205
+ assert_equal @date, @previous.a_date
206
+ end
207
+
208
+ should 'handle booleans' do
209
+ assert @previous.a_boolean
210
+ end
211
+
212
+
213
+ context "after a column is removed from the record's schema" do
214
+ setup do
215
+ change_schema
216
+ Widget.reset_column_information
217
+ assert_raise(NoMethodError) { Widget.new.sacrificial_column }
218
+ @last = @widget.versions.last
219
+ end
220
+
221
+ should 'reify previous version' do
222
+ assert_kind_of Widget, @last.reify
223
+ end
224
+
225
+ should 'restore all forward-compatible attributes' do
226
+ format = '%a, %d %b %Y %H:%M:%S %z' # :rfc822
227
+ assert_equal 'Warble', @last.reify.name
228
+ assert_equal 'The quick brown fox', @last.reify.a_text
229
+ assert_equal 42, @last.reify.an_integer
230
+ assert_in_delta 153.01, @last.reify.a_float, 0.001
231
+ assert_in_delta 2.71828, @last.reify.a_decimal, 0.00001
232
+ assert_equal @date_time.strftime(format), @last.reify.a_datetime.strftime(format)
233
+ assert_equal @time, @last.reify.a_time
234
+ assert_equal @date, @last.reify.a_date
235
+ assert @last.reify.a_boolean
236
+ end
237
+ end
238
+ end
239
+
240
+
241
+ context 'A record' do
242
+ setup { @widget = Widget.create :name => 'Zaphod' }
243
+
244
+ context 'with its paper trail turned off' do
245
+ setup do
246
+ Widget.paper_trail_off
247
+ @count = @widget.versions.length
248
+ end
249
+
250
+ teardown { Widget.paper_trail_on }
251
+
252
+ context 'when updated' do
253
+ setup { @widget.update_attributes :name => 'Beeblebrox' }
254
+
255
+ should 'not add to its trail' do
256
+ assert_equal @count, @widget.versions.length
257
+ end
258
+ end
259
+
260
+ context 'and then its paper trail turned on' do
261
+ setup { Widget.paper_trail_on }
262
+
263
+ context 'when updated' do
264
+ setup { @widget.update_attributes :name => 'Ford' }
265
+
266
+ should 'add to its trail' do
267
+ assert_equal @count + 1, @widget.versions.length
268
+ end
269
+ end
270
+ end
271
+ end
272
+ end
273
+
274
+
275
+ context 'A papertrail with somebody making changes' do
276
+ setup do
277
+ PaperTrail.whodunnit = 'Colonel Mustard'
278
+ @widget = Widget.new :name => 'Fidget'
279
+ end
280
+
281
+ context 'when a record is created' do
282
+ setup { @widget.save }
283
+
284
+ should 'track who made the change' do
285
+ assert_equal 'Colonel Mustard', @widget.versions.last.whodunnit
286
+ end
287
+
288
+ context 'when a record is updated' do
289
+ setup { @widget.update_attributes :name => 'Rivet' }
290
+
291
+ should 'track who made the change' do
292
+ assert_equal 'Colonel Mustard', @widget.versions.last.whodunnit
293
+ end
294
+
295
+ context 'when a record is destroyed' do
296
+ setup { @widget.destroy }
297
+
298
+ should 'track who made the change' do
299
+ assert_equal 'Colonel Mustard', @widget.versions.last.whodunnit
300
+ end
301
+ end
302
+ end
303
+ end
304
+ end
305
+
306
+
307
+ context 'A subclass' do
308
+ setup do
309
+ @foo = FooWidget.create
310
+ @foo.update_attributes :name => 'Fooey'
311
+ end
312
+
313
+ should 'reify with the correct type' do
314
+ thing = @foo.versions.last.reify
315
+ assert_kind_of FooWidget, thing
316
+ end
317
+
318
+
319
+ context 'when destroyed' do
320
+ setup { @foo.destroy }
321
+
322
+ should 'reify with the correct type' do
323
+ thing = @foo.versions.last.reify
324
+ assert_kind_of FooWidget, thing
325
+ end
326
+ end
327
+ end
328
+
329
+
330
+ context 'An item with versions' do
331
+ setup do
332
+ @widget = Widget.create :name => 'Widget'
333
+ @widget.update_attributes :name => 'Fidget'
334
+ @widget.update_attributes :name => 'Digit'
335
+ end
336
+
337
+ context 'on the first version' do
338
+ setup { @version = @widget.versions.first }
339
+
340
+ should 'have a nil previous version' do
341
+ assert_nil @version.previous
342
+ end
343
+
344
+ should 'return the next version' do
345
+ assert_equal @widget.versions[1], @version.next
346
+ end
347
+
348
+ should 'return the correct index' do
349
+ assert_equal 0, @version.index
350
+ end
351
+ end
352
+
353
+ context 'on the last version' do
354
+ setup { @version = @widget.versions.last }
355
+
356
+ should 'return the previous version' do
357
+ assert_equal @widget.versions[@widget.versions.length - 2], @version.previous
358
+ end
359
+
360
+ should 'have a nil next version' do
361
+ assert_nil @version.next
362
+ end
363
+
364
+ should 'return the correct index' do
365
+ assert_equal @widget.versions.length - 1, @version.index
366
+ end
367
+ end
368
+ end
369
+
370
+ end
@@ -0,0 +1,14 @@
1
+ require 'test_helper'
2
+
3
+ class PaperTrailSchemaTest < ActiveSupport::TestCase
4
+ def setup
5
+ load_schema
6
+ end
7
+
8
+ def test_schema_has_loaded_correctly
9
+ assert_equal [], Widget.all
10
+ assert_equal [], Version.all
11
+ assert_equal [], Wotsit.all
12
+ assert_equal [], Fluxor.all
13
+ end
14
+ end
data/test/schema.rb ADDED
@@ -0,0 +1,38 @@
1
+ ActiveRecord::Schema.define(:version => 0) do
2
+
3
+ create_table :widgets, :force => true do |t|
4
+ t.string :name
5
+ t.text :a_text
6
+ t.integer :an_integer
7
+ t.float :a_float
8
+ t.decimal :a_decimal
9
+ t.datetime :a_datetime
10
+ t.time :a_time
11
+ t.date :a_date
12
+ t.boolean :a_boolean
13
+ t.datetime :created_at, :updated_at
14
+ t.string :sacrificial_column
15
+ t.string :type
16
+ end
17
+
18
+ create_table :versions, :force => true do |t|
19
+ t.string :item_type, :null => false
20
+ t.integer :item_id, :null => false
21
+ t.string :event, :null => false
22
+ t.string :whodunnit
23
+ t.text :object
24
+ t.datetime :created_at
25
+ end
26
+ add_index :versions, [:item_type, :item_id]
27
+
28
+ create_table :wotsits, :force => true do |t|
29
+ t.integer :widget_id
30
+ t.string :name
31
+ end
32
+
33
+ create_table :fluxors, :force => true do |t|
34
+ t.integer :widget_id
35
+ t.string :name
36
+ end
37
+
38
+ end
@@ -0,0 +1,3 @@
1
+ ActiveRecord::Schema.define do
2
+ remove_column :widgets, :sacrificial_column
3
+ end
@@ -0,0 +1,47 @@
1
+ require 'rubygems'
2
+ require 'active_support'
3
+ require 'active_support/test_case'
4
+ require 'shoulda'
5
+
6
+ ENV['RAILS_ENV'] = 'test'
7
+ ENV['RAILS_ROOT'] ||= File.dirname(__FILE__) + '/../../../..'
8
+
9
+ require 'test/unit'
10
+ require File.expand_path(File.join(ENV['RAILS_ROOT'], 'config/environment.rb'))
11
+
12
+ def connect_to_database
13
+ config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
14
+ ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
15
+
16
+ db_adapter = ENV['DB']
17
+
18
+ # no db passed, try one of these fine config-free DBs before bombing.
19
+ db_adapter ||=
20
+ begin
21
+ require 'rubygems'
22
+ require 'sqlite'
23
+ 'sqlite'
24
+ rescue MissingSourceFile
25
+ begin
26
+ require 'sqlite3'
27
+ 'sqlite3'
28
+ rescue MissingSourceFile
29
+ end
30
+ end
31
+
32
+ if db_adapter.nil?
33
+ raise "No DB Adapter selected. Pass the DB= option to pick one, or install Sqlite or Sqlite3."
34
+ end
35
+
36
+ ActiveRecord::Base.establish_connection(config[db_adapter])
37
+ end
38
+
39
+ def load_schema
40
+ connect_to_database
41
+ load(File.dirname(__FILE__) + "/schema.rb")
42
+ require File.dirname(__FILE__) + '/../rails/init.rb'
43
+ end
44
+
45
+ def change_schema
46
+ load(File.dirname(__FILE__) + "/schema_change.rb")
47
+ end
data/uninstall.rb ADDED
@@ -0,0 +1 @@
1
+ # Uninstall hook code here
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jeremyw-paper_trail
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.2.2
5
+ platform: ruby
6
+ authors:
7
+ - Andy Stewart
8
+ - Jeremy Weiskotten
9
+ - Joe Lind
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+
14
+ date: 2009-07-31 00:00:00 -07:00
15
+ default_executable:
16
+ dependencies: []
17
+
18
+ description:
19
+ email: boss@airbladesoftware.com
20
+ executables: []
21
+
22
+ extensions: []
23
+
24
+ extra_rdoc_files:
25
+ - README.md
26
+ files:
27
+ - .gitignore
28
+ - MIT-LICENSE
29
+ - README.md
30
+ - Rakefile
31
+ - VERSION
32
+ - generators/paper_trail/USAGE
33
+ - generators/paper_trail/paper_trail_generator.rb
34
+ - generators/paper_trail/templates/create_versions.rb
35
+ - init.rb
36
+ - install.rb
37
+ - lib/paper_trail.rb
38
+ - lib/paper_trail/has_paper_trail.rb
39
+ - lib/paper_trail/version.rb
40
+ - paper_trail.gemspec
41
+ - rails/init.rb
42
+ - tasks/paper_trail_tasks.rake
43
+ - test/database.yml
44
+ - test/paper_trail_controller_test.rb
45
+ - test/paper_trail_model_test.rb
46
+ - test/paper_trail_schema_test.rb
47
+ - test/schema.rb
48
+ - test/schema_change.rb
49
+ - test/test_helper.rb
50
+ - uninstall.rb
51
+ has_rdoc: true
52
+ homepage: http://github.com/airblade/paper_trail
53
+ licenses:
54
+ post_install_message:
55
+ rdoc_options:
56
+ - --charset=UTF-8
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: "0"
64
+ version:
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: "0"
70
+ version:
71
+ requirements: []
72
+
73
+ rubyforge_project:
74
+ rubygems_version: 1.3.5
75
+ signing_key:
76
+ specification_version: 2
77
+ summary: Track changes to your models' data. Good for auditing or versioning.
78
+ test_files:
79
+ - test/paper_trail_controller_test.rb
80
+ - test/paper_trail_model_test.rb
81
+ - test/paper_trail_schema_test.rb
82
+ - test/schema.rb
83
+ - test/schema_change.rb
84
+ - test/test_helper.rb