bkwld-paper_trail 2.3.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.
Files changed (78) hide show
  1. data/.gitignore +10 -0
  2. data/.travis.yml +5 -0
  3. data/Gemfile +2 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.md +663 -0
  6. data/Rakefile +15 -0
  7. data/lib/generators/paper_trail/USAGE +2 -0
  8. data/lib/generators/paper_trail/install_generator.rb +20 -0
  9. data/lib/generators/paper_trail/templates/add_object_changes_column_to_versions.rb +9 -0
  10. data/lib/generators/paper_trail/templates/create_versions.rb +19 -0
  11. data/lib/paper_trail.rb +87 -0
  12. data/lib/paper_trail/config.rb +11 -0
  13. data/lib/paper_trail/controller.rb +76 -0
  14. data/lib/paper_trail/has_paper_trail.rb +228 -0
  15. data/lib/paper_trail/version.rb +159 -0
  16. data/lib/paper_trail/version_number.rb +3 -0
  17. data/paper_trail.gemspec +24 -0
  18. data/test/dummy/Rakefile +7 -0
  19. data/test/dummy/app/controllers/application_controller.rb +17 -0
  20. data/test/dummy/app/controllers/test_controller.rb +5 -0
  21. data/test/dummy/app/controllers/widgets_controller.rb +23 -0
  22. data/test/dummy/app/helpers/application_helper.rb +2 -0
  23. data/test/dummy/app/models/animal.rb +4 -0
  24. data/test/dummy/app/models/article.rb +12 -0
  25. data/test/dummy/app/models/authorship.rb +5 -0
  26. data/test/dummy/app/models/book.rb +5 -0
  27. data/test/dummy/app/models/cat.rb +2 -0
  28. data/test/dummy/app/models/document.rb +4 -0
  29. data/test/dummy/app/models/dog.rb +2 -0
  30. data/test/dummy/app/models/elephant.rb +3 -0
  31. data/test/dummy/app/models/fluxor.rb +3 -0
  32. data/test/dummy/app/models/foo_widget.rb +2 -0
  33. data/test/dummy/app/models/person.rb +5 -0
  34. data/test/dummy/app/models/post.rb +4 -0
  35. data/test/dummy/app/models/song.rb +12 -0
  36. data/test/dummy/app/models/widget.rb +5 -0
  37. data/test/dummy/app/models/wotsit.rb +4 -0
  38. data/test/dummy/app/versions/post_version.rb +3 -0
  39. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  40. data/test/dummy/config.ru +4 -0
  41. data/test/dummy/config/application.rb +45 -0
  42. data/test/dummy/config/boot.rb +10 -0
  43. data/test/dummy/config/database.yml +22 -0
  44. data/test/dummy/config/environment.rb +5 -0
  45. data/test/dummy/config/environments/development.rb +26 -0
  46. data/test/dummy/config/environments/production.rb +49 -0
  47. data/test/dummy/config/environments/test.rb +35 -0
  48. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  49. data/test/dummy/config/initializers/inflections.rb +10 -0
  50. data/test/dummy/config/initializers/mime_types.rb +5 -0
  51. data/test/dummy/config/initializers/secret_token.rb +7 -0
  52. data/test/dummy/config/initializers/session_store.rb +8 -0
  53. data/test/dummy/config/locales/en.yml +5 -0
  54. data/test/dummy/config/routes.rb +3 -0
  55. data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +120 -0
  56. data/test/dummy/db/schema.rb +103 -0
  57. data/test/dummy/db/test.sqlite3 +0 -0
  58. data/test/dummy/public/404.html +26 -0
  59. data/test/dummy/public/422.html +26 -0
  60. data/test/dummy/public/500.html +26 -0
  61. data/test/dummy/public/favicon.ico +0 -0
  62. data/test/dummy/public/javascripts/application.js +2 -0
  63. data/test/dummy/public/javascripts/controls.js +965 -0
  64. data/test/dummy/public/javascripts/dragdrop.js +974 -0
  65. data/test/dummy/public/javascripts/effects.js +1123 -0
  66. data/test/dummy/public/javascripts/prototype.js +6001 -0
  67. data/test/dummy/public/javascripts/rails.js +175 -0
  68. data/test/dummy/public/stylesheets/.gitkeep +0 -0
  69. data/test/dummy/script/rails +6 -0
  70. data/test/functional/controller_test.rb +71 -0
  71. data/test/functional/thread_safety_test.rb +26 -0
  72. data/test/integration/navigation_test.rb +7 -0
  73. data/test/paper_trail_test.rb +27 -0
  74. data/test/support/integration_case.rb +5 -0
  75. data/test/test_helper.rb +49 -0
  76. data/test/unit/inheritance_column_test.rb +43 -0
  77. data/test/unit/model_test.rb +925 -0
  78. metadata +236 -0
@@ -0,0 +1,175 @@
1
+ (function() {
2
+ // Technique from Juriy Zaytsev
3
+ // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/
4
+ function isEventSupported(eventName) {
5
+ var el = document.createElement('div');
6
+ eventName = 'on' + eventName;
7
+ var isSupported = (eventName in el);
8
+ if (!isSupported) {
9
+ el.setAttribute(eventName, 'return;');
10
+ isSupported = typeof el[eventName] == 'function';
11
+ }
12
+ el = null;
13
+ return isSupported;
14
+ }
15
+
16
+ function isForm(element) {
17
+ return Object.isElement(element) && element.nodeName.toUpperCase() == 'FORM'
18
+ }
19
+
20
+ function isInput(element) {
21
+ if (Object.isElement(element)) {
22
+ var name = element.nodeName.toUpperCase()
23
+ return name == 'INPUT' || name == 'SELECT' || name == 'TEXTAREA'
24
+ }
25
+ else return false
26
+ }
27
+
28
+ var submitBubbles = isEventSupported('submit'),
29
+ changeBubbles = isEventSupported('change')
30
+
31
+ if (!submitBubbles || !changeBubbles) {
32
+ // augment the Event.Handler class to observe custom events when needed
33
+ Event.Handler.prototype.initialize = Event.Handler.prototype.initialize.wrap(
34
+ function(init, element, eventName, selector, callback) {
35
+ init(element, eventName, selector, callback)
36
+ // is the handler being attached to an element that doesn't support this event?
37
+ if ( (!submitBubbles && this.eventName == 'submit' && !isForm(this.element)) ||
38
+ (!changeBubbles && this.eventName == 'change' && !isInput(this.element)) ) {
39
+ // "submit" => "emulated:submit"
40
+ this.eventName = 'emulated:' + this.eventName
41
+ }
42
+ }
43
+ )
44
+ }
45
+
46
+ if (!submitBubbles) {
47
+ // discover forms on the page by observing focus events which always bubble
48
+ document.on('focusin', 'form', function(focusEvent, form) {
49
+ // special handler for the real "submit" event (one-time operation)
50
+ if (!form.retrieve('emulated:submit')) {
51
+ form.on('submit', function(submitEvent) {
52
+ var emulated = form.fire('emulated:submit', submitEvent, true)
53
+ // if custom event received preventDefault, cancel the real one too
54
+ if (emulated.returnValue === false) submitEvent.preventDefault()
55
+ })
56
+ form.store('emulated:submit', true)
57
+ }
58
+ })
59
+ }
60
+
61
+ if (!changeBubbles) {
62
+ // discover form inputs on the page
63
+ document.on('focusin', 'input, select, texarea', function(focusEvent, input) {
64
+ // special handler for real "change" events
65
+ if (!input.retrieve('emulated:change')) {
66
+ input.on('change', function(changeEvent) {
67
+ input.fire('emulated:change', changeEvent, true)
68
+ })
69
+ input.store('emulated:change', true)
70
+ }
71
+ })
72
+ }
73
+
74
+ function handleRemote(element) {
75
+ var method, url, params;
76
+
77
+ var event = element.fire("ajax:before");
78
+ if (event.stopped) return false;
79
+
80
+ if (element.tagName.toLowerCase() === 'form') {
81
+ method = element.readAttribute('method') || 'post';
82
+ url = element.readAttribute('action');
83
+ params = element.serialize();
84
+ } else {
85
+ method = element.readAttribute('data-method') || 'get';
86
+ url = element.readAttribute('href');
87
+ params = {};
88
+ }
89
+
90
+ new Ajax.Request(url, {
91
+ method: method,
92
+ parameters: params,
93
+ evalScripts: true,
94
+
95
+ onComplete: function(request) { element.fire("ajax:complete", request); },
96
+ onSuccess: function(request) { element.fire("ajax:success", request); },
97
+ onFailure: function(request) { element.fire("ajax:failure", request); }
98
+ });
99
+
100
+ element.fire("ajax:after");
101
+ }
102
+
103
+ function handleMethod(element) {
104
+ var method = element.readAttribute('data-method'),
105
+ url = element.readAttribute('href'),
106
+ csrf_param = $$('meta[name=csrf-param]')[0],
107
+ csrf_token = $$('meta[name=csrf-token]')[0];
108
+
109
+ var form = new Element('form', { method: "POST", action: url, style: "display: none;" });
110
+ element.parentNode.insert(form);
111
+
112
+ if (method !== 'post') {
113
+ var field = new Element('input', { type: 'hidden', name: '_method', value: method });
114
+ form.insert(field);
115
+ }
116
+
117
+ if (csrf_param) {
118
+ var param = csrf_param.readAttribute('content'),
119
+ token = csrf_token.readAttribute('content'),
120
+ field = new Element('input', { type: 'hidden', name: param, value: token });
121
+ form.insert(field);
122
+ }
123
+
124
+ form.submit();
125
+ }
126
+
127
+
128
+ document.on("click", "*[data-confirm]", function(event, element) {
129
+ var message = element.readAttribute('data-confirm');
130
+ if (!confirm(message)) event.stop();
131
+ });
132
+
133
+ document.on("click", "a[data-remote]", function(event, element) {
134
+ if (event.stopped) return;
135
+ handleRemote(element);
136
+ event.stop();
137
+ });
138
+
139
+ document.on("click", "a[data-method]", function(event, element) {
140
+ if (event.stopped) return;
141
+ handleMethod(element);
142
+ event.stop();
143
+ });
144
+
145
+ document.on("submit", function(event) {
146
+ var element = event.findElement(),
147
+ message = element.readAttribute('data-confirm');
148
+ if (message && !confirm(message)) {
149
+ event.stop();
150
+ return false;
151
+ }
152
+
153
+ var inputs = element.select("input[type=submit][data-disable-with]");
154
+ inputs.each(function(input) {
155
+ input.disabled = true;
156
+ input.writeAttribute('data-original-value', input.value);
157
+ input.value = input.readAttribute('data-disable-with');
158
+ });
159
+
160
+ var element = event.findElement("form[data-remote]");
161
+ if (element) {
162
+ handleRemote(element);
163
+ event.stop();
164
+ }
165
+ });
166
+
167
+ document.on("ajax:after", "form", function(event, element) {
168
+ var inputs = element.select("input[type=submit][disabled=true][data-disable-with]");
169
+ inputs.each(function(input) {
170
+ input.value = input.readAttribute('data-original-value');
171
+ input.removeAttribute('data-original-value');
172
+ input.disabled = false;
173
+ });
174
+ });
175
+ })();
File without changes
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
3
+
4
+ APP_PATH = File.expand_path('../../config/application', __FILE__)
5
+ require File.expand_path('../../config/boot', __FILE__)
6
+ require 'rails/commands'
@@ -0,0 +1,71 @@
1
+ require 'test_helper'
2
+
3
+ class ControllerTest < ActionController::TestCase
4
+ tests WidgetsController
5
+
6
+ setup do
7
+ @request.env['REMOTE_ADDR'] = '127.0.0.1'
8
+ end
9
+
10
+ teardown do
11
+ PaperTrail.enabled_for_controller = true
12
+ end
13
+
14
+ test 'disable on create' do
15
+ @request.env['HTTP_USER_AGENT'] = 'Disable User-Agent'
16
+ post :create, :widget => { :name => 'Flugel' }
17
+ assert_equal 0, assigns(:widget).versions.length
18
+ end
19
+
20
+ test 'disable on update' do
21
+ @request.env['HTTP_USER_AGENT'] = 'Disable User-Agent'
22
+ post :create, :widget => { :name => 'Flugel' }
23
+ w = assigns(:widget)
24
+ assert_equal 0, w.versions.length
25
+ put :update, :id => w.id, :widget => { :name => 'Bugle' }
26
+ widget = assigns(:widget)
27
+ assert_equal 0, widget.versions.length
28
+ end
29
+
30
+ test 'disable on destroy' do
31
+ @request.env['HTTP_USER_AGENT'] = 'Disable User-Agent'
32
+ post :create, :widget => { :name => 'Flugel' }
33
+ w = assigns(:widget)
34
+ assert_equal 0, w.versions.length
35
+ delete :destroy, :id => w.id
36
+ widget = assigns(:widget)
37
+ assert_equal 0, Version.with_item_keys('Widget', w.id).size
38
+ end
39
+
40
+ test 'create' do
41
+ post :create, :widget => { :name => 'Flugel' }
42
+ widget = assigns(:widget)
43
+ assert_equal 1, widget.versions.length
44
+ assert_equal 153, widget.versions.last.whodunnit.to_i
45
+ assert_equal '127.0.0.1', widget.versions.last.ip
46
+ assert_equal 'Rails Testing', widget.versions.last.user_agent
47
+ end
48
+
49
+ test 'update' do
50
+ w = Widget.create :name => 'Duvel'
51
+ assert_equal 1, w.versions.length
52
+ put :update, :id => w.id, :widget => { :name => 'Bugle' }
53
+ widget = assigns(:widget)
54
+ assert_equal 2, widget.versions.length
55
+ assert_equal 153, widget.versions.last.whodunnit.to_i
56
+ assert_equal '127.0.0.1', widget.versions.last.ip
57
+ assert_equal 'Rails Testing', widget.versions.last.user_agent
58
+ end
59
+
60
+ test 'destroy' do
61
+ w = Widget.create :name => 'Roundel'
62
+ assert_equal 1, w.versions.length
63
+ delete :destroy, :id => w.id
64
+ widget = assigns(:widget)
65
+ versions_for_widget = Version.with_item_keys('Widget', w.id)
66
+ assert_equal 2, versions_for_widget.length
67
+ assert_equal 153, versions_for_widget.last.whodunnit.to_i
68
+ assert_equal '127.0.0.1', versions_for_widget.last.ip
69
+ assert_equal 'Rails Testing', versions_for_widget.last.user_agent
70
+ end
71
+ end
@@ -0,0 +1,26 @@
1
+ require 'test_helper'
2
+
3
+ class ThreadSafetyTest < ActionController::TestCase
4
+ should "be thread safe" do
5
+ blocked = true
6
+
7
+ slow_thread = Thread.new do
8
+ controller = TestController.new
9
+ controller.send :set_whodunnit
10
+ begin
11
+ sleep 0.001
12
+ end while blocked
13
+ PaperTrail.whodunnit
14
+ end
15
+
16
+ fast_thread = Thread.new do
17
+ controller = TestController.new
18
+ controller.send :set_whodunnit
19
+ who = PaperTrail.whodunnit
20
+ blocked = false
21
+ who
22
+ end
23
+
24
+ assert_not_equal slow_thread.value, fast_thread.value
25
+ end
26
+ end
@@ -0,0 +1,7 @@
1
+ require 'test_helper'
2
+
3
+ class NavigationTest < ActiveSupport::IntegrationCase
4
+ test 'Sanity test' do
5
+ assert_kind_of Dummy::Application, Rails.application
6
+ end
7
+ end
@@ -0,0 +1,27 @@
1
+ require 'test_helper'
2
+
3
+ class PaperTrailTest < ActiveSupport::TestCase
4
+ test 'Sanity test' do
5
+ assert_kind_of Module, PaperTrail
6
+ end
7
+
8
+ test 'create with plain model class' do
9
+ widget = Widget.create
10
+ assert_equal 1, widget.versions.length
11
+ end
12
+
13
+ test 'update with plain model class' do
14
+ widget = Widget.create
15
+ assert_equal 1, widget.versions.length
16
+ widget.update_attributes(:name => 'Bugle')
17
+ assert_equal 2, widget.versions.length
18
+ end
19
+
20
+ test 'destroy with plain model class' do
21
+ widget = Widget.create
22
+ assert_equal 1, widget.versions.length
23
+ widget.destroy
24
+ versions_for_widget = Version.with_item_keys('Widget', widget.id)
25
+ assert_equal 2, versions_for_widget.length
26
+ end
27
+ end
@@ -0,0 +1,5 @@
1
+ # Define a bare test case to use with Capybara
2
+ class ActiveSupport::IntegrationCase < ActiveSupport::TestCase
3
+ include Capybara
4
+ include Rails.application.routes.url_helpers
5
+ end
@@ -0,0 +1,49 @@
1
+ # Configure Rails Envinronment
2
+ ENV["RAILS_ENV"] = "test"
3
+
4
+ require File.expand_path("../dummy/config/environment.rb", __FILE__)
5
+ require "rails/test_help"
6
+
7
+ begin
8
+ require 'turn'
9
+ rescue LoadError
10
+ # noop
11
+ end
12
+
13
+ #ActionMailer::Base.delivery_method = :test
14
+ #ActionMailer::Base.perform_deliveries = true
15
+ #ActionMailer::Base.default_url_options[:host] = "test.com"
16
+
17
+ Rails.backtrace_cleaner.remove_silencers!
18
+
19
+ require 'shoulda'
20
+
21
+ # Configure capybara for integration testing
22
+ require "capybara/rails"
23
+ Capybara.default_driver = :rack_test
24
+ Capybara.default_selector = :css
25
+
26
+ # Run any available migration
27
+ ActiveRecord::Migrator.migrate File.expand_path("../dummy/db/migrate/", __FILE__)
28
+
29
+ # Load support files
30
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
31
+
32
+ # global setup block resetting Thread.current
33
+ class ActiveSupport::TestCase
34
+ teardown do
35
+ Thread.current[:paper_trail] = nil
36
+ end
37
+ end
38
+
39
+ #
40
+ # Helpers
41
+ #
42
+
43
+ def change_schema
44
+ ActiveRecord::Migration.verbose = false
45
+ ActiveRecord::Schema.define do
46
+ remove_column :widgets, :sacrificial_column
47
+ end
48
+ ActiveRecord::Migration.verbose = true
49
+ end
@@ -0,0 +1,43 @@
1
+ require 'test_helper'
2
+
3
+ class InheritanceColumnTest < ActiveSupport::TestCase
4
+
5
+ context 'STI models' do
6
+ setup do
7
+ @animal = Animal.create :name => 'Animal'
8
+ @animal.update_attributes :name => 'Animal from the Muppets'
9
+ @animal.update_attributes :name => 'Animal Muppet'
10
+ @animal.destroy
11
+
12
+ @dog = Dog.create :name => 'Snoopy'
13
+ @dog.update_attributes :name => 'Scooby'
14
+ @dog.update_attributes :name => 'Scooby Doo'
15
+ @dog.destroy
16
+
17
+ @cat = Cat.create :name => 'Garfield'
18
+ @cat.update_attributes :name => 'Garfield (I hate Mondays)'
19
+ @cat.update_attributes :name => 'Garfield The Cat'
20
+ @cat.destroy
21
+ end
22
+
23
+ should 'work with custom STI inheritance column' do
24
+ assert_equal 12, Version.count
25
+ assert_equal 4, @animal.versions.count
26
+ assert @animal.versions.first.reify.nil?
27
+ @animal.versions[1..-1].each { |v| assert_equal 'Animal', v.reify.class.name }
28
+
29
+ # For some reason `@dog.versions` doesn't include the final `destroy` version.
30
+ # Neither do `@dog.versions.scoped` nor `@dog.versions(true)` nor `@dog.versions.reload`.
31
+ dog_versions = Version.where(:item_id => @dog.id)
32
+ assert_equal 4, dog_versions.count
33
+ assert dog_versions.first.reify.nil?
34
+ dog_versions[1..-1].each { |v| assert_equal 'Dog', v.reify.class.name }
35
+
36
+ cat_versions = Version.where(:item_id => @cat.id)
37
+ assert_equal 4, cat_versions.count
38
+ assert cat_versions.first.reify.nil?
39
+ cat_versions[1..-1].each { |v| assert_equal 'Cat', v.reify.class.name }
40
+ end
41
+ end
42
+
43
+ end
@@ -0,0 +1,925 @@
1
+ require 'test_helper'
2
+
3
+ class HasPaperTrailModelTest < ActiveSupport::TestCase
4
+
5
+ context 'A record' do
6
+ setup { @article = Article.create }
7
+
8
+ context 'which updates an ignored column' do
9
+ setup { @article.update_attributes :title => 'My first title' }
10
+ should_not_change('the number of versions') { Version.count }
11
+ end
12
+
13
+ context 'which updates an ignored column and a selected column' do
14
+ setup { @article.update_attributes :title => 'My first title', :content => 'Some text here.' }
15
+ should_change('the number of versions', :by => 1) { Version.count }
16
+
17
+ should 'have stored only non-ignored attributes' do
18
+ assert_equal ({'content' => [nil, 'Some text here.']}), @article.versions.last.changeset
19
+ end
20
+ end
21
+
22
+ context 'which updates a selected column' do
23
+ setup { @article.update_attributes :content => 'Some text here.' }
24
+ should_change('the number of versions', :by => 1) { Version.count }
25
+ end
26
+
27
+ context 'which updates a non-ignored and non-selected column' do
28
+ setup { @article.update_attributes :abstract => 'Other abstract'}
29
+ should_not_change('the number of versions') { Version.count }
30
+ end
31
+
32
+ end
33
+
34
+
35
+ context 'A new record' do
36
+ setup { @widget = Widget.new }
37
+
38
+ should 'not have any previous versions' do
39
+ assert_equal [], @widget.versions
40
+ end
41
+
42
+ should 'be live' do
43
+ assert @widget.live?
44
+ end
45
+
46
+
47
+ context 'which is then created' do
48
+ setup { @widget.update_attributes :name => 'Henry' }
49
+
50
+ should 'have one previous version' do
51
+ assert_equal 1, @widget.versions.length
52
+ end
53
+
54
+ should 'be nil in its previous version' do
55
+ assert_nil @widget.versions.first.object
56
+ assert_nil @widget.versions.first.reify
57
+ end
58
+
59
+ should 'record the correct event' do
60
+ assert_match /create/i, @widget.versions.first.event
61
+ end
62
+
63
+ should 'be live' do
64
+ assert @widget.live?
65
+ end
66
+
67
+ should 'not have changes' do
68
+ assert_equal Hash.new, @widget.versions.last.changeset
69
+ end
70
+
71
+ context 'and then updated without any changes' do
72
+ setup { @widget.save }
73
+
74
+ should 'not have a new version' do
75
+ assert_equal 1, @widget.versions.length
76
+ end
77
+ end
78
+
79
+
80
+ context 'and then updated with changes' do
81
+ setup { @widget.update_attributes :name => 'Harry' }
82
+
83
+ should 'have two previous versions' do
84
+ assert_equal 2, @widget.versions.length
85
+ end
86
+
87
+ should 'be available in its previous version' do
88
+ assert_equal 'Harry', @widget.name
89
+ assert_not_nil @widget.versions.last.object
90
+ widget = @widget.versions.last.reify
91
+ assert_equal 'Henry', widget.name
92
+ assert_equal 'Harry', @widget.name
93
+ end
94
+
95
+ should 'have the same ID in its previous version' do
96
+ assert_equal @widget.id, @widget.versions.last.reify.id
97
+ end
98
+
99
+ should 'record the correct event' do
100
+ assert_match /update/i, @widget.versions.last.event
101
+ end
102
+
103
+ should 'have versions that are not live' do
104
+ assert @widget.versions.map(&:reify).compact.all? { |w| !w.live? }
105
+ end
106
+
107
+ should 'have stored changes' do
108
+ assert_equal ({'name' => ['Henry', 'Harry']}), YAML::load(@widget.versions.last.object_changes)
109
+ assert_equal ({'name' => ['Henry', 'Harry']}), @widget.versions.last.changeset
110
+ end
111
+
112
+ should 'return changes with indifferent access' do
113
+ assert_equal ['Henry', 'Harry'], @widget.versions.last.changeset[:name]
114
+ assert_equal ['Henry', 'Harry'], @widget.versions.last.changeset['name']
115
+ end
116
+
117
+ if defined?(ActiveRecord::IdentityMap) && ActiveRecord::IdentityMap.respond_to?(:without)
118
+ should 'not clobber the IdentityMap when reifying' do
119
+ module ActiveRecord::IdentityMap
120
+ class << self
121
+ alias :__without :without
122
+ def without(&block)
123
+ @unclobbered = true
124
+ __without(&block)
125
+ end
126
+ end
127
+ end
128
+
129
+ @widget.versions.last.reify
130
+ assert ActiveRecord::IdentityMap.instance_variable_get("@unclobbered")
131
+ end
132
+ end
133
+
134
+ context 'and has one associated object' do
135
+ setup do
136
+ @wotsit = @widget.create_wotsit :name => 'John'
137
+ end
138
+
139
+ should 'not copy the has_one association by default when reifying' do
140
+ reified_widget = @widget.versions.last.reify
141
+ assert_equal @wotsit, reified_widget.wotsit # association hasn't been affected by reifying
142
+ assert_equal @wotsit, @widget.wotsit # confirm that the association is correct
143
+ end
144
+
145
+ should 'copy the has_one association when reifying with :has_one => true' do
146
+ reified_widget = @widget.versions.last.reify(:has_one => true)
147
+ assert_nil reified_widget.wotsit # wotsit wasn't there at the last version
148
+ assert_equal @wotsit, @widget.wotsit # wotsit came into being on the live object
149
+ end
150
+ end
151
+
152
+
153
+ context 'and has many associated objects' do
154
+ setup do
155
+ @f0 = @widget.fluxors.create :name => 'f-zero'
156
+ @f1 = @widget.fluxors.create :name => 'f-one'
157
+ @reified_widget = @widget.versions.last.reify
158
+ end
159
+
160
+ should 'copy the has_many associations when reifying' do
161
+ assert_equal @widget.fluxors.length, @reified_widget.fluxors.length
162
+ assert_same_elements @widget.fluxors, @reified_widget.fluxors
163
+
164
+ assert_equal @widget.versions.length, @reified_widget.versions.length
165
+ assert_same_elements @widget.versions, @reified_widget.versions
166
+ end
167
+ end
168
+
169
+
170
+ context 'and then destroyed' do
171
+ setup do
172
+ @fluxor = @widget.fluxors.create :name => 'flux'
173
+ @widget.destroy
174
+ @reified_widget = Version.last.reify
175
+ end
176
+
177
+ should 'record the correct event' do
178
+ assert_match /destroy/i, Version.last.event
179
+ end
180
+
181
+ should 'have three previous versions' do
182
+ assert_equal 3, Version.with_item_keys('Widget', @widget.id).length
183
+ end
184
+
185
+ should 'be available in its previous version' do
186
+ assert_equal @widget.id, @reified_widget.id
187
+ assert_equal @widget.attributes, @reified_widget.attributes
188
+ end
189
+
190
+ should 'be re-creatable from its previous version' do
191
+ assert @reified_widget.save
192
+ end
193
+
194
+ should 'restore its associations on its previous version' do
195
+ @reified_widget.save
196
+ assert_equal 1, @reified_widget.fluxors.length
197
+ end
198
+
199
+ should 'not have changes' do
200
+ assert_equal Hash.new, @widget.versions.last.changeset
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end
206
+
207
+
208
+ # Test the serialisation and deserialisation.
209
+ # TODO: binary
210
+ context "A record's papertrail" do
211
+ setup do
212
+ @date_time = DateTime.now.utc
213
+ @time = Time.now
214
+ @date = Date.new 2009, 5, 29
215
+ @widget = Widget.create :name => 'Warble',
216
+ :a_text => 'The quick brown fox',
217
+ :an_integer => 42,
218
+ :a_float => 153.01,
219
+ :a_decimal => 2.71828,
220
+ :a_datetime => @date_time,
221
+ :a_time => @time,
222
+ :a_date => @date,
223
+ :a_boolean => true
224
+
225
+ @widget.update_attributes :name => nil,
226
+ :a_text => nil,
227
+ :an_integer => nil,
228
+ :a_float => nil,
229
+ :a_decimal => nil,
230
+ :a_datetime => nil,
231
+ :a_time => nil,
232
+ :a_date => nil,
233
+ :a_boolean => false
234
+ @previous = @widget.versions.last.reify
235
+ end
236
+
237
+ should 'handle strings' do
238
+ assert_equal 'Warble', @previous.name
239
+ end
240
+
241
+ should 'handle text' do
242
+ assert_equal 'The quick brown fox', @previous.a_text
243
+ end
244
+
245
+ should 'handle integers' do
246
+ assert_equal 42, @previous.an_integer
247
+ end
248
+
249
+ should 'handle floats' do
250
+ assert_in_delta 153.01, @previous.a_float, 0.001
251
+ end
252
+
253
+ should 'handle decimals' do
254
+ assert_in_delta 2.71828, @previous.a_decimal, 0.00001
255
+ end
256
+
257
+ should 'handle datetimes' do
258
+ assert_equal @date_time.to_time.utc.to_i, @previous.a_datetime.to_time.utc.to_i
259
+ end
260
+
261
+ should 'handle times' do
262
+ assert_equal @time.utc.to_i, @previous.a_time.utc.to_i
263
+ end
264
+
265
+ should 'handle dates' do
266
+ assert_equal @date, @previous.a_date
267
+ end
268
+
269
+ should 'handle booleans' do
270
+ assert @previous.a_boolean
271
+ end
272
+
273
+
274
+ context "after a column is removed from the record's schema" do
275
+ setup do
276
+ change_schema
277
+ Widget.reset_column_information
278
+ assert_raise(NoMethodError) { Widget.new.sacrificial_column }
279
+ @last = @widget.versions.last
280
+ end
281
+
282
+ should 'reify previous version' do
283
+ assert_kind_of Widget, @last.reify
284
+ end
285
+
286
+ should 'restore all forward-compatible attributes' do
287
+ assert_equal 'Warble', @last.reify.name
288
+ assert_equal 'The quick brown fox', @last.reify.a_text
289
+ assert_equal 42, @last.reify.an_integer
290
+ assert_in_delta 153.01, @last.reify.a_float, 0.001
291
+ assert_in_delta 2.71828, @last.reify.a_decimal, 0.00001
292
+ assert_equal @date_time.to_time.utc.to_i, @last.reify.a_datetime.to_time.utc.to_i
293
+ assert_equal @time.utc.to_i, @last.reify.a_time.utc.to_i
294
+ assert_equal @date, @last.reify.a_date
295
+ assert @last.reify.a_boolean
296
+ end
297
+ end
298
+ end
299
+
300
+
301
+ context 'A record' do
302
+ setup { @widget = Widget.create :name => 'Zaphod' }
303
+
304
+ context 'with PaperTrail globally disabled' do
305
+ setup do
306
+ PaperTrail.enabled = false
307
+ @count = @widget.versions.length
308
+ end
309
+
310
+ teardown { PaperTrail.enabled = true }
311
+
312
+ context 'when updated' do
313
+ setup { @widget.update_attributes :name => 'Beeblebrox' }
314
+
315
+ should 'not add to its trail' do
316
+ assert_equal @count, @widget.versions.length
317
+ end
318
+ end
319
+ end
320
+
321
+ context 'with its paper trail turned off' do
322
+ setup do
323
+ Widget.paper_trail_off
324
+ @count = @widget.versions.length
325
+ end
326
+
327
+ teardown { Widget.paper_trail_on }
328
+
329
+ context 'when updated' do
330
+ setup { @widget.update_attributes :name => 'Beeblebrox' }
331
+
332
+ should 'not add to its trail' do
333
+ assert_equal @count, @widget.versions.length
334
+ end
335
+ end
336
+
337
+ context 'when destroyed "without versioning"' do
338
+ should 'leave paper trail off after call' do
339
+ @widget.without_versioning :destroy
340
+ assert !Widget.paper_trail_enabled_for_model
341
+ end
342
+ end
343
+
344
+ context 'and then its paper trail turned on' do
345
+ setup { Widget.paper_trail_on }
346
+
347
+ context 'when updated' do
348
+ setup { @widget.update_attributes :name => 'Ford' }
349
+
350
+ should 'add to its trail' do
351
+ assert_equal @count + 1, @widget.versions.length
352
+ end
353
+ end
354
+
355
+ context 'when updated "without versioning"' do
356
+ setup do
357
+ @widget.without_versioning do
358
+ @widget.update_attributes :name => 'Ford'
359
+ end
360
+ end
361
+
362
+ should 'not create new version' do
363
+ assert_equal 1, @widget.versions.length
364
+ end
365
+
366
+ should 'enable paper trail after call' do
367
+ assert Widget.paper_trail_enabled_for_model
368
+ end
369
+ end
370
+ end
371
+ end
372
+ end
373
+
374
+
375
+ context 'A papertrail with somebody making changes' do
376
+ setup do
377
+ @widget = Widget.new :name => 'Fidget'
378
+ end
379
+
380
+ context 'when a record is created' do
381
+ setup do
382
+ PaperTrail.whodunnit = 'Alice'
383
+ @widget.save
384
+ @version = @widget.versions.last # only 1 version
385
+ end
386
+
387
+ should 'track who made the change' do
388
+ assert_equal 'Alice', @version.whodunnit
389
+ assert_nil @version.originator
390
+ assert_equal 'Alice', @version.terminator
391
+ assert_equal 'Alice', @widget.originator
392
+ end
393
+
394
+ context 'when a record is updated' do
395
+ setup do
396
+ PaperTrail.whodunnit = 'Bob'
397
+ @widget.update_attributes :name => 'Rivet'
398
+ @version = @widget.versions.last
399
+ end
400
+
401
+ should 'track who made the change' do
402
+ assert_equal 'Bob', @version.whodunnit
403
+ assert_equal 'Alice', @version.originator
404
+ assert_equal 'Bob', @version.terminator
405
+ assert_equal 'Bob', @widget.originator
406
+ end
407
+
408
+ context 'when a record is destroyed' do
409
+ setup do
410
+ PaperTrail.whodunnit = 'Charlie'
411
+ @widget.destroy
412
+ @version = Version.last
413
+ end
414
+
415
+ should 'track who made the change' do
416
+ assert_equal 'Charlie', @version.whodunnit
417
+ assert_equal 'Bob', @version.originator
418
+ assert_equal 'Charlie', @version.terminator
419
+ assert_equal 'Charlie', @widget.originator
420
+ end
421
+ end
422
+ end
423
+ end
424
+ end
425
+
426
+
427
+ context 'A subclass' do
428
+ setup do
429
+ @foo = FooWidget.create
430
+ @foo.update_attributes :name => 'Fooey'
431
+ end
432
+
433
+ should 'reify with the correct type' do
434
+ thing = Version.last.reify
435
+ assert_kind_of FooWidget, thing
436
+ assert_equal @foo.versions.first, Version.last.previous
437
+ assert_nil Version.last.next
438
+ end
439
+
440
+ context 'when destroyed' do
441
+ setup { @foo.destroy }
442
+
443
+ should 'reify with the correct type' do
444
+ thing = Version.last.reify
445
+ assert_kind_of FooWidget, thing
446
+ assert_equal @foo.versions[1], Version.last.previous
447
+ assert_nil Version.last.next
448
+ end
449
+ end
450
+ end
451
+
452
+
453
+ context 'An item with versions' do
454
+ setup do
455
+ @widget = Widget.create :name => 'Widget'
456
+ @widget.update_attributes :name => 'Fidget'
457
+ @widget.update_attributes :name => 'Digit'
458
+ end
459
+
460
+ context 'which were created over time' do
461
+ setup do
462
+ @created = 2.days.ago
463
+ @first_update = 1.day.ago
464
+ @second_update = 1.hour.ago
465
+ @widget.versions[0].update_attributes :created_at => @created
466
+ @widget.versions[1].update_attributes :created_at => @first_update
467
+ @widget.versions[2].update_attributes :created_at => @second_update
468
+ @widget.update_attribute :updated_at, @second_update
469
+ end
470
+
471
+ should 'return nil for version_at before it was created' do
472
+ assert_nil @widget.version_at(@created - 1)
473
+ end
474
+
475
+ should 'return how it looked when created for version_at its creation' do
476
+ assert_equal 'Widget', @widget.version_at(@created).name
477
+ end
478
+
479
+ should "return how it looked when created for version_at just before its first update" do
480
+ assert_equal 'Widget', @widget.version_at(@first_update - 1).name
481
+ end
482
+
483
+ should "return how it looked when first updated for version_at its first update" do
484
+ assert_equal 'Fidget', @widget.version_at(@first_update).name
485
+ end
486
+
487
+ should 'return how it looked when first updated for version_at just before its second update' do
488
+ assert_equal 'Fidget', @widget.version_at(@second_update - 1).name
489
+ end
490
+
491
+ should 'return how it looked when subsequently updated for version_at its second update' do
492
+ assert_equal 'Digit', @widget.version_at(@second_update).name
493
+ end
494
+
495
+ should 'return the current object for version_at after latest update' do
496
+ assert_equal 'Digit', @widget.version_at(1.day.from_now).name
497
+ end
498
+ end
499
+
500
+
501
+ context 'on the first version' do
502
+ setup { @version = @widget.versions.first }
503
+
504
+ should 'have a nil previous version' do
505
+ assert_nil @version.previous
506
+ end
507
+
508
+ should 'return the next version' do
509
+ assert_equal @widget.versions[1], @version.next
510
+ end
511
+
512
+ should 'return the correct index' do
513
+ assert_equal 0, @version.index
514
+ end
515
+ end
516
+
517
+ context 'on the last version' do
518
+ setup { @version = @widget.versions.last }
519
+
520
+ should 'return the previous version' do
521
+ assert_equal @widget.versions[@widget.versions.length - 2], @version.previous
522
+ end
523
+
524
+ should 'have a nil next version' do
525
+ assert_nil @version.next
526
+ end
527
+
528
+ should 'return the correct index' do
529
+ assert_equal @widget.versions.length - 1, @version.index
530
+ end
531
+ end
532
+ end
533
+
534
+
535
+ context 'An item' do
536
+ setup { @article = Article.new }
537
+
538
+ context 'which is created' do
539
+ setup { @article.save }
540
+
541
+ should 'store fixed meta data' do
542
+ assert_equal 42, @article.versions.last.answer
543
+ end
544
+
545
+ should 'store dynamic meta data which is independent of the item' do
546
+ assert_equal '31 + 11 = 42', @article.versions.last.question
547
+ end
548
+
549
+ should 'store dynamic meta data which depends on the item' do
550
+ assert_equal @article.id, @article.versions.last.article_id
551
+ end
552
+
553
+ should 'store dynamic meta data based on a method of the item' do
554
+ assert_equal @article.action_data_provider_method, @article.versions.last.action
555
+ end
556
+
557
+
558
+ context 'and updated' do
559
+ setup { @article.update_attributes! :content => 'Better text.' }
560
+
561
+ should 'store fixed meta data' do
562
+ assert_equal 42, @article.versions.last.answer
563
+ end
564
+
565
+ should 'store dynamic meta data which is independent of the item' do
566
+ assert_equal '31 + 11 = 42', @article.versions.last.question
567
+ end
568
+
569
+ should 'store dynamic meta data which depends on the item' do
570
+ assert_equal @article.id, @article.versions.last.article_id
571
+ end
572
+ end
573
+
574
+
575
+ context 'and destroyed' do
576
+ setup { @article.destroy }
577
+
578
+ should 'store fixed meta data' do
579
+ assert_equal 42, @article.versions.last.answer
580
+ end
581
+
582
+ should 'store dynamic meta data which is independent of the item' do
583
+ assert_equal '31 + 11 = 42', @article.versions.last.question
584
+ end
585
+
586
+ should 'store dynamic meta data which depends on the item' do
587
+ assert_equal @article.id, @article.versions.last.article_id
588
+ end
589
+
590
+ end
591
+ end
592
+ end
593
+
594
+ context 'A reified item' do
595
+ setup do
596
+ widget = Widget.create :name => 'Bob'
597
+ %w( Tom Dick Jane ).each { |name| widget.update_attributes :name => name }
598
+ @version = widget.versions.last
599
+ @widget = @version.reify
600
+ end
601
+
602
+ should 'know which version it came from' do
603
+ assert_equal @version, @widget.version
604
+ end
605
+
606
+ should 'return its previous self' do
607
+ assert_equal @widget.versions[-2].reify, @widget.previous_version
608
+ end
609
+
610
+ end
611
+
612
+
613
+ context 'A non-reified item' do
614
+ setup { @widget = Widget.new }
615
+
616
+ should 'not have a previous version' do
617
+ assert_nil @widget.previous_version
618
+ end
619
+
620
+ should 'not have a next version' do
621
+ assert_nil @widget.next_version
622
+ end
623
+
624
+ context 'with versions' do
625
+ setup do
626
+ @widget.save
627
+ %w( Tom Dick Jane ).each { |name| @widget.update_attributes :name => name }
628
+ end
629
+
630
+ should 'have a previous version' do
631
+ assert_equal @widget.versions.last.reify, @widget.previous_version
632
+ end
633
+
634
+ should 'have a next version' do
635
+ assert_nil @widget.next_version
636
+ end
637
+ end
638
+ end
639
+
640
+ context 'A reified item' do
641
+ setup do
642
+ widget = Widget.create :name => 'Bob'
643
+ %w( Tom Dick Jane ).each { |name| widget.update_attributes :name => name }
644
+ @versions = widget.versions
645
+ @second_widget = @versions[1].reify # first widget is null
646
+ @last_widget = @versions.last.reify
647
+ end
648
+
649
+ should 'have a previous version' do
650
+ assert_nil @second_widget.previous_version
651
+ assert_equal @versions[-2].reify, @last_widget.previous_version
652
+ end
653
+
654
+ should 'have a next version' do
655
+ assert_equal @versions[2].reify, @second_widget.next_version
656
+ assert_nil @last_widget.next_version
657
+ end
658
+ end
659
+
660
+ context ":has_many :through" do
661
+ setup do
662
+ @book = Book.create :title => 'War and Peace'
663
+ @dostoyevsky = Person.create :name => 'Dostoyevsky'
664
+ @solzhenitsyn = Person.create :name => 'Solzhenitsyn'
665
+ end
666
+
667
+ should 'store version on source <<' do
668
+ count = Version.count
669
+ @book.authors << @dostoyevsky
670
+ assert_equal 1, Version.count - count
671
+ assert_equal Version.last, @book.authorships.first.versions.first
672
+ end
673
+
674
+ should 'store version on source create' do
675
+ count = Version.count
676
+ @book.authors.create :name => 'Tolstoy'
677
+ assert_equal 2, Version.count - count
678
+ assert_same_elements [Person.last, Authorship.last], [Version.all[-2].item, Version.last.item]
679
+ end
680
+
681
+ should 'store version on join destroy' do
682
+ @book.authors << @dostoyevsky
683
+ count = Version.count
684
+ @book.authorships(true).last.destroy
685
+ assert_equal 1, Version.count - count
686
+ assert_equal @book, Version.last.reify.book
687
+ assert_equal @dostoyevsky, Version.last.reify.person
688
+ end
689
+
690
+ should 'store version on join clear' do
691
+ @book.authors << @dostoyevsky
692
+ count = Version.count
693
+ @book.authorships(true).clear
694
+ assert_equal 1, Version.count - count
695
+ assert_equal @book, Version.last.reify.book
696
+ assert_equal @dostoyevsky, Version.last.reify.person
697
+ end
698
+ end
699
+
700
+
701
+ context 'A model with a has_one association' do
702
+ setup { @widget = Widget.create :name => 'widget_0' }
703
+
704
+ context 'before the associated was created' do
705
+ setup do
706
+ @widget.update_attributes :name => 'widget_1'
707
+ @wotsit = @widget.create_wotsit :name => 'wotsit_0'
708
+ end
709
+
710
+ context 'when reified' do
711
+ setup { @widget_0 = @widget.versions.last.reify(:has_one => 1) }
712
+
713
+ should 'see the associated as it was at the time' do
714
+ assert_nil @widget_0.wotsit
715
+ end
716
+ end
717
+ end
718
+
719
+ context 'where the associated is created between model versions' do
720
+ setup do
721
+ @wotsit = @widget.create_wotsit :name => 'wotsit_0'
722
+ make_last_version_earlier @wotsit
723
+
724
+ @widget.update_attributes :name => 'widget_1'
725
+ end
726
+
727
+ context 'when reified' do
728
+ setup { @widget_0 = @widget.versions.last.reify(:has_one => 1) }
729
+
730
+ should 'see the associated as it was at the time' do
731
+ assert_equal 'wotsit_0', @widget_0.wotsit.name
732
+ end
733
+ end
734
+
735
+ context 'and then the associated is updated between model versions' do
736
+ setup do
737
+ @wotsit.update_attributes :name => 'wotsit_1'
738
+ make_last_version_earlier @wotsit
739
+ @wotsit.update_attributes :name => 'wotsit_2'
740
+ make_last_version_earlier @wotsit
741
+
742
+ @widget.update_attributes :name => 'widget_2'
743
+ @wotsit.update_attributes :name => 'wotsit_3'
744
+ end
745
+
746
+ context 'when reified' do
747
+ setup { @widget_1 = @widget.versions.last.reify(:has_one => 1) }
748
+
749
+ should 'see the associated as it was at the time' do
750
+ assert_equal 'wotsit_2', @widget_1.wotsit.name
751
+ end
752
+ end
753
+
754
+ context 'when reified opting out of has_one reification' do
755
+ setup { @widget_1 = @widget.versions.last.reify(:has_one => false) }
756
+
757
+ should 'see the associated as it is live' do
758
+ assert_equal 'wotsit_3', @widget_1.wotsit.name
759
+ end
760
+ end
761
+ end
762
+
763
+ context 'and then the associated is destroyed between model versions' do
764
+ setup do
765
+ @wotsit.destroy
766
+ make_last_version_earlier @wotsit
767
+
768
+ @widget.update_attributes :name => 'widget_3'
769
+ end
770
+
771
+ context 'when reified' do
772
+ setup { @widget_2 = @widget.versions.last.reify(:has_one => 1) }
773
+
774
+ should 'see the associated as it was at the time' do
775
+ assert_nil @widget_2.wotsit
776
+ end
777
+ end
778
+ end
779
+ end
780
+ end
781
+
782
+ context 'A new model instance which uses a custom Version class' do
783
+ setup { @post = Post.new }
784
+
785
+ context 'which is then saved' do
786
+ setup { @post.save }
787
+ should_change('the number of post versions') { PostVersion.count }
788
+ should_not_change('the number of versions') { Version.count }
789
+ end
790
+ end
791
+
792
+ context 'An existing model instance which uses a custom Version class' do
793
+ setup { @post = Post.create }
794
+
795
+ context 'on the first version' do
796
+ setup { @version = @post.versions.first }
797
+
798
+ should 'have the correct index' do
799
+ assert_equal 0, @version.index
800
+ end
801
+ end
802
+
803
+ should 'have versions of the custom class' do
804
+ assert_equal "PostVersion", @post.versions.first.class.name
805
+ end
806
+
807
+ context 'which is modified' do
808
+ setup { @post.update_attributes({ :content => "Some new content" }) }
809
+ should_change('the number of post versions') { PostVersion.count }
810
+ should_not_change('the number of versions') { Version.count }
811
+ should "not have stored changes when object_changes column doesn't exist" do
812
+ assert_nil @post.versions.last.changeset
813
+ end
814
+ end
815
+ end
816
+
817
+
818
+ context 'An overwritten default accessor' do
819
+ setup do
820
+ @song = Song.create :length => 4
821
+ @song.update_attributes :length => 5
822
+ end
823
+
824
+ should 'return "overwritten" value on live instance' do
825
+ assert_equal 5, @song.length
826
+ end
827
+ should 'return "overwritten" value on reified instance' do
828
+ assert_equal 4, @song.versions.last.reify.length
829
+ end
830
+ end
831
+
832
+
833
+ context 'An unsaved record' do
834
+ setup do
835
+ @widget = Widget.new
836
+ @widget.destroy
837
+ end
838
+ should 'not have a version created on destroy' do
839
+ assert @widget.versions.empty?
840
+ end
841
+ end
842
+
843
+ context 'A model with a custom association' do
844
+ setup do
845
+ @doc = Document.create
846
+ @doc.update_attributes :name => 'Doc 1'
847
+ end
848
+
849
+ should 'not respond to versions method' do
850
+ assert !@doc.respond_to?(:versions)
851
+ end
852
+
853
+ should 'create a new version record' do
854
+ assert_equal 2, @doc.paper_trail_versions.length
855
+ end
856
+
857
+ should 'respond to previous_version as normal' do
858
+ @doc.update_attributes :name => 'Doc 2'
859
+ assert_equal 3, @doc.paper_trail_versions.length
860
+ assert_equal 'Doc 1', @doc.previous_version.name
861
+ end
862
+ end
863
+
864
+ context 'The `on` option' do
865
+ context 'on create' do
866
+ setup do
867
+ Fluxor.instance_eval <<-END
868
+ has_paper_trail :on => [:create]
869
+ END
870
+ @fluxor = Fluxor.create
871
+ @fluxor.update_attributes :name => 'blah'
872
+ @fluxor.destroy
873
+ end
874
+ should 'only have a version for the create event' do
875
+ assert_equal 1, @fluxor.versions.length
876
+ assert_equal 'create', @fluxor.versions.last.event
877
+ end
878
+ end
879
+ context 'on update' do
880
+ setup do
881
+ Fluxor.reset_callbacks :create
882
+ Fluxor.reset_callbacks :update
883
+ Fluxor.reset_callbacks :destroy
884
+ Fluxor.instance_eval <<-END
885
+ has_paper_trail :on => [:update]
886
+ END
887
+ @fluxor = Fluxor.create
888
+ @fluxor.update_attributes :name => 'blah'
889
+ @fluxor.destroy
890
+ end
891
+ should 'only have a version for the update event' do
892
+ assert_equal 1, @fluxor.versions.length
893
+ assert_equal 'update', @fluxor.versions.last.event
894
+ end
895
+ end
896
+ context 'on destroy' do
897
+ setup do
898
+ Fluxor.reset_callbacks :create
899
+ Fluxor.reset_callbacks :update
900
+ Fluxor.reset_callbacks :destroy
901
+ Fluxor.instance_eval <<-END
902
+ has_paper_trail :on => [:destroy]
903
+ END
904
+ @fluxor = Fluxor.create
905
+ @fluxor.update_attributes :name => 'blah'
906
+ @fluxor.destroy
907
+ end
908
+ should 'only have a version for the destroy event' do
909
+ assert_equal 1, @fluxor.versions.length
910
+ assert_equal 'destroy', @fluxor.versions.last.event
911
+ end
912
+ end
913
+ end
914
+
915
+ private
916
+
917
+ # Updates `model`'s last version so it looks like the version was
918
+ # created 2 seconds ago.
919
+ def make_last_version_earlier(model)
920
+ Version.record_timestamps = false
921
+ model.versions.last.update_attributes :created_at => 2.seconds.ago
922
+ Version.record_timestamps = true
923
+ end
924
+
925
+ end