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.
- data/.gitignore +10 -0
- data/.travis.yml +5 -0
- data/Gemfile +2 -0
- data/MIT-LICENSE +20 -0
- data/README.md +663 -0
- data/Rakefile +15 -0
- data/lib/generators/paper_trail/USAGE +2 -0
- data/lib/generators/paper_trail/install_generator.rb +20 -0
- data/lib/generators/paper_trail/templates/add_object_changes_column_to_versions.rb +9 -0
- data/lib/generators/paper_trail/templates/create_versions.rb +19 -0
- data/lib/paper_trail.rb +87 -0
- data/lib/paper_trail/config.rb +11 -0
- data/lib/paper_trail/controller.rb +76 -0
- data/lib/paper_trail/has_paper_trail.rb +228 -0
- data/lib/paper_trail/version.rb +159 -0
- data/lib/paper_trail/version_number.rb +3 -0
- data/paper_trail.gemspec +24 -0
- data/test/dummy/Rakefile +7 -0
- data/test/dummy/app/controllers/application_controller.rb +17 -0
- data/test/dummy/app/controllers/test_controller.rb +5 -0
- data/test/dummy/app/controllers/widgets_controller.rb +23 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/models/animal.rb +4 -0
- data/test/dummy/app/models/article.rb +12 -0
- data/test/dummy/app/models/authorship.rb +5 -0
- data/test/dummy/app/models/book.rb +5 -0
- data/test/dummy/app/models/cat.rb +2 -0
- data/test/dummy/app/models/document.rb +4 -0
- data/test/dummy/app/models/dog.rb +2 -0
- data/test/dummy/app/models/elephant.rb +3 -0
- data/test/dummy/app/models/fluxor.rb +3 -0
- data/test/dummy/app/models/foo_widget.rb +2 -0
- data/test/dummy/app/models/person.rb +5 -0
- data/test/dummy/app/models/post.rb +4 -0
- data/test/dummy/app/models/song.rb +12 -0
- data/test/dummy/app/models/widget.rb +5 -0
- data/test/dummy/app/models/wotsit.rb +4 -0
- data/test/dummy/app/versions/post_version.rb +3 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +45 -0
- data/test/dummy/config/boot.rb +10 -0
- data/test/dummy/config/database.yml +22 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +26 -0
- data/test/dummy/config/environments/production.rb +49 -0
- data/test/dummy/config/environments/test.rb +35 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/inflections.rb +10 -0
- data/test/dummy/config/initializers/mime_types.rb +5 -0
- data/test/dummy/config/initializers/secret_token.rb +7 -0
- data/test/dummy/config/initializers/session_store.rb +8 -0
- data/test/dummy/config/locales/en.yml +5 -0
- data/test/dummy/config/routes.rb +3 -0
- data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +120 -0
- data/test/dummy/db/schema.rb +103 -0
- data/test/dummy/db/test.sqlite3 +0 -0
- data/test/dummy/public/404.html +26 -0
- data/test/dummy/public/422.html +26 -0
- data/test/dummy/public/500.html +26 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/public/javascripts/application.js +2 -0
- data/test/dummy/public/javascripts/controls.js +965 -0
- data/test/dummy/public/javascripts/dragdrop.js +974 -0
- data/test/dummy/public/javascripts/effects.js +1123 -0
- data/test/dummy/public/javascripts/prototype.js +6001 -0
- data/test/dummy/public/javascripts/rails.js +175 -0
- data/test/dummy/public/stylesheets/.gitkeep +0 -0
- data/test/dummy/script/rails +6 -0
- data/test/functional/controller_test.rb +71 -0
- data/test/functional/thread_safety_test.rb +26 -0
- data/test/integration/navigation_test.rb +7 -0
- data/test/paper_trail_test.rb +27 -0
- data/test/support/integration_case.rb +5 -0
- data/test/test_helper.rb +49 -0
- data/test/unit/inheritance_column_test.rb +43 -0
- data/test/unit/model_test.rb +925 -0
- 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,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
|
data/test/test_helper.rb
ADDED
|
@@ -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
|