paper_trail 1.2.13

Sign up to get free protection for your applications and to get access to all the features.
@@ -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