paper_trail 1.2.13

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.
@@ -0,0 +1,2 @@
1
+ test/debug.log
2
+ test/paper_trail_plugin.sqlite3.db
@@ -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.
@@ -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.
@@ -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.2.13
@@ -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,20 @@
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
+ t.integer :user_id, :null => true
11
+ end
12
+ add_index :versions, [:item_type, :item_id]
13
+ add_index :versions, :user_id
14
+ end
15
+
16
+ def self.down
17
+ remove_index :versions, [:item_type, :item_id]
18
+ drop_table :versions
19
+ end
20
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ # Include hook code here
@@ -0,0 +1 @@
1
+ # Install hook code here
@@ -0,0 +1,43 @@
1
+ require 'yaml'
2
+ require 'paper_trail/config'
3
+ require 'paper_trail/has_paper_trail'
4
+ require 'paper_trail/version'
5
+
6
+ module PaperTrail
7
+ @@whodunnit = nil
8
+
9
+ def self.config
10
+ @@config ||= PaperTrail::Config.instance
11
+ end
12
+
13
+ def self.included(base)
14
+ base.before_filter :set_whodunnit
15
+ end
16
+
17
+ def self.enabled=(value)
18
+ PaperTrail.config.enabled = value
19
+ end
20
+
21
+ def self.enabled?
22
+ !!PaperTrail.config.enabled
23
+ end
24
+
25
+ def self.whodunnit
26
+ @@whodunnit.respond_to?(:call) ? @@whodunnit.call : @@whodunnit
27
+ end
28
+
29
+ def self.whodunnit=(value)
30
+ @@whodunnit = value
31
+ end
32
+
33
+ private
34
+
35
+ def set_whodunnit
36
+ @@whodunnit = lambda {
37
+ self.send :current_user rescue nil
38
+ }
39
+ end
40
+
41
+ end
42
+
43
+ ActionController::Base.send :include, PaperTrail
@@ -0,0 +1,10 @@
1
+ module PaperTrail
2
+ class Config
3
+ include Singleton
4
+ attr_accessor :enabled
5
+
6
+ def initialize
7
+ @enabled = true
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,142 @@
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 && PaperTrail.enabled?
36
+ end
37
+
38
+ def record_update
39
+ if changed? and self.class.paper_trail_active and PaperTrail.enabled?
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 && PaperTrail.enabled?
50
+ end
51
+
52
+ # Returns the object at the version that was valid at the given timestamp.
53
+ def version_at timestamp
54
+ # short-circuit if the current state is valid
55
+ return self if self.updated_at < timestamp
56
+
57
+ version = versions.first(
58
+ :conditions => ['created_at < ?', timestamp],
59
+ :order => 'created_at DESC')
60
+ version.reify if version
61
+ end
62
+
63
+ # Walk the versions to construct an audit trail of the edits made
64
+ # over time, and by whom.
65
+ def audit_trail options={}
66
+ options[:attributes_to_ignore] ||= %w(updated_at)
67
+
68
+ audit_trail = []
69
+
70
+ versions_desc = versions_including_current_in_descending_order
71
+
72
+ versions_desc.each_with_index do |version, index|
73
+ previous_version = versions_desc[index + 1]
74
+ break if previous_version.nil?
75
+
76
+ attributes_after = yaml_to_hash(version.object)
77
+ attributes_before = yaml_to_hash(previous_version.object)
78
+
79
+ # remove some attributes that we don't need to report
80
+ [attributes_before, attributes_after].each do |hash|
81
+ hash.reject! { |k,v| k.in? Array(options[:attributes_to_ignore]) }
82
+ end
83
+
84
+ audit_trail << {
85
+ :event => previous_version.event,
86
+ :changed_by => transform_whodunnit(previous_version.whodunnit),
87
+ :changed_at => previous_version.created_at,
88
+ :changes => differences(attributes_before, attributes_after)
89
+ }
90
+ end
91
+
92
+ audit_trail
93
+ end
94
+
95
+ protected
96
+
97
+ def transform_whodunnit(whodunnit)
98
+ whodunnit
99
+ end
100
+
101
+
102
+ private
103
+
104
+ def previous_version
105
+ previous = self.clone
106
+ previous.id = id
107
+ changes.each do |attr, ary|
108
+ previous.send "#{attr}=", ary.first
109
+ end
110
+ previous
111
+ end
112
+
113
+ def object_to_string(object)
114
+ object.attributes.to_yaml
115
+ end
116
+
117
+ def yaml_to_hash(yaml)
118
+ return {} if yaml.nil?
119
+ YAML::load(yaml).to_hash
120
+ end
121
+
122
+ # Returns an array of hashes, where each hash specifies the +:attribute+,
123
+ # value +:before+ the change, and value +:after+ the change.
124
+ def differences(before, after)
125
+ before.diff(after).keys.sort.inject([]) do |diffs, k|
126
+ diff = { :attribute => k, :before => before[k], :after => after[k] }
127
+ diffs << diff; diffs
128
+ end
129
+ end
130
+
131
+ def versions_including_current_in_descending_order
132
+ v = self.versions.dup
133
+ v << Version.new(:event => 'update',
134
+ :object => object_to_string(self),
135
+ :created_at => self.updated_at)
136
+ v.reverse # newest first
137
+ end
138
+ end
139
+
140
+ end
141
+
142
+ ActiveRecord::Base.send :include, PaperTrail
@@ -0,0 +1,77 @@
1
+ class Version < ActiveRecord::Base
2
+ belongs_to :item, :polymorphic => true
3
+ belongs_to :user
4
+ before_save :set_user_id
5
+ validates_presence_of :event
6
+
7
+ named_scope :for_item_type, lambda { |item_types|
8
+ { :conditions => { :item_type => item_types } }
9
+ }
10
+
11
+ named_scope :created_after, lambda { |time|
12
+ { :conditions => ['versions.created_at > ?', time] }
13
+ }
14
+
15
+ named_scope :by_created_at_ascending, :order => 'versions.created_at asc'
16
+
17
+ def reify
18
+ unless object.nil?
19
+ # Attributes
20
+
21
+ attrs = YAML::load object
22
+
23
+ # Normally a polymorphic belongs_to relationship allows us
24
+ # to get the object we belong to by calling, in this case,
25
+ # +item+. However this returns nil if +item+ has been
26
+ # destroyed, and we need to be able to retrieve destroyed
27
+ # objects.
28
+ #
29
+ # In this situation we constantize the +item_type+ to get hold of
30
+ # the class...except when the stored object's attributes
31
+ # include a +type+ key. If this is the case, the object
32
+ # we belong to is using single table inheritance and the
33
+ # +item_type+ will be the base class, not the actual subclass.
34
+ # If +type+ is present but empty, the class is the base class.
35
+
36
+ if item
37
+ model = item
38
+ else
39
+ class_name = attrs['type'].blank? ? item_type : attrs['type']
40
+ klass = class_name.constantize
41
+ model = klass.new
42
+ end
43
+
44
+ attrs.each do |k, v|
45
+ begin
46
+ model.send "#{k}=", v
47
+ rescue NoMethodError
48
+ RAILS_DEFAULT_LOGGER.warn "Attribute #{k} does not exist on #{item_type} (Version id: #{id})."
49
+ end
50
+ end
51
+
52
+ model
53
+ end
54
+ end
55
+
56
+ def next
57
+ Version.first :conditions => ["id > ? AND item_type = ? AND item_id = ?", id, item_type, item_id],
58
+ :order => 'id ASC'
59
+ end
60
+
61
+ def previous
62
+ Version.first :conditions => ["id < ? AND item_type = ? AND item_id = ?", id, item_type, item_id],
63
+ :order => 'id DESC'
64
+ end
65
+
66
+ def index
67
+ Version.all(:conditions => ["item_type = ? AND item_id = ?", item_type, item_id],
68
+ :order => 'id ASC').index(self)
69
+ end
70
+
71
+ private
72
+
73
+ def set_user_id
74
+ self.user_id = self.whodunnit.id if self.whodunnit.is_a?(ActiveRecord::Base)
75
+ return true
76
+ end
77
+ end
@@ -0,0 +1,65 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{paper_trail}
5
+ s.version = "1.2.13"
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-10-02}
10
+ s.email = %q{jeremy@weiskotten.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/config.rb",
27
+ "lib/paper_trail/has_paper_trail.rb",
28
+ "lib/paper_trail/version.rb",
29
+ "paper_trail.gemspec",
30
+ "rails/init.rb",
31
+ "tasks/paper_trail_tasks.rake",
32
+ "test/database.yml",
33
+ "test/paper_trail_controller_test.rb",
34
+ "test/paper_trail_model_test.rb",
35
+ "test/paper_trail_schema_test.rb",
36
+ "test/schema.rb",
37
+ "test/schema_change.rb",
38
+ "test/test_helper.rb",
39
+ "uninstall.rb"
40
+ ]
41
+ s.has_rdoc = true
42
+ s.homepage = %q{http://github.com/jeremyw/paper_trail}
43
+ s.rdoc_options = ["--charset=UTF-8"]
44
+ s.require_paths = ["lib"]
45
+ s.rubygems_version = %q{1.3.5}
46
+ s.summary = %q{Track changes to your models' data. Good for auditing or versioning.}
47
+ s.test_files = [
48
+ "test/paper_trail_controller_test.rb",
49
+ "test/paper_trail_model_test.rb",
50
+ "test/paper_trail_schema_test.rb",
51
+ "test/schema.rb",
52
+ "test/schema_change.rb",
53
+ "test/test_helper.rb"
54
+ ]
55
+
56
+ # if s.respond_to? :specification_version then
57
+ # current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
58
+ # s.specification_version = 2
59
+ #
60
+ # if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
61
+ # else
62
+ # end
63
+ # else
64
+ # end
65
+ end
@@ -0,0 +1 @@
1
+ require 'paper_trail'
File without changes
@@ -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
@@ -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
@@ -0,0 +1 @@
1
+ # Uninstall hook code here
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: paper_trail
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.2.13
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-10-02 00:00:00 -04:00
15
+ default_executable:
16
+ dependencies: []
17
+
18
+ description:
19
+ email: jeremy@weiskotten.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/config.rb
39
+ - lib/paper_trail/has_paper_trail.rb
40
+ - lib/paper_trail/version.rb
41
+ - paper_trail.gemspec
42
+ - rails/init.rb
43
+ - tasks/paper_trail_tasks.rake
44
+ - test/database.yml
45
+ - test/paper_trail_controller_test.rb
46
+ - test/paper_trail_model_test.rb
47
+ - test/paper_trail_schema_test.rb
48
+ - test/schema.rb
49
+ - test/schema_change.rb
50
+ - test/test_helper.rb
51
+ - uninstall.rb
52
+ has_rdoc: true
53
+ homepage: http://github.com/jeremyw/paper_trail
54
+ licenses: []
55
+
56
+ post_install_message:
57
+ rdoc_options:
58
+ - --charset=UTF-8
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: "0"
66
+ version:
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: "0"
72
+ version:
73
+ requirements: []
74
+
75
+ rubyforge_project:
76
+ rubygems_version: 1.3.5
77
+ signing_key:
78
+ specification_version: 3
79
+ summary: Track changes to your models' data. Good for auditing or versioning.
80
+ test_files:
81
+ - test/paper_trail_controller_test.rb
82
+ - test/paper_trail_model_test.rb
83
+ - test/paper_trail_schema_test.rb
84
+ - test/schema.rb
85
+ - test/schema_change.rb
86
+ - test/test_helper.rb