mongoid-audit 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,159 @@
1
+ module Mongoid::Audit
2
+ module Tracker
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ include Mongoid::Document
7
+ include Mongoid::Timestamps
8
+ attr_writer :trackable
9
+
10
+ field :association_chain, :type => Array, :default => []
11
+ field :modified, :type => Hash
12
+ field :original, :type => Hash
13
+ field :version, :type => Integer
14
+ field :action, :type => String
15
+ field :scope, :type => String
16
+ belongs_to :modifier, :class_name => Mongoid::Audit.modifier_class_name
17
+
18
+ Mongoid::Audit.tracker_class_name = self.name.tableize.singularize.to_sym
19
+
20
+ # install model observer and action controller filter
21
+ Mongoid::Audit::Sweeper.send(:observe, Mongoid::Audit.tracker_class_name)
22
+ if defined?(ActionController) and defined?(ActionController::Base)
23
+ ActionController::Base.class_eval do
24
+ before_filter { |controller| Mongoid::Audit::Sweeper.instance.before(controller) }
25
+ after_filter { |controller| Mongoid::Audit::Sweeper.instance.after(controller) }
26
+ end
27
+ end
28
+ end
29
+
30
+ def undo!(modifier)
31
+ if action.to_sym == :destroy
32
+ re_create
33
+ elsif action.to_sym == :create
34
+ re_destroy
35
+ else
36
+ trackable.update_attributes!(undo_attr(modifier))
37
+ end
38
+ end
39
+
40
+ def redo!(modifier)
41
+ if action.to_sym == :destroy
42
+ re_destroy
43
+ elsif action.to_sym == :create
44
+ re_create
45
+ else
46
+ trackable.update_attributes!(redo_attr(modifier))
47
+ end
48
+ end
49
+
50
+ def undo_attr(modifier)
51
+ undo_hash = affected.easy_unmerge(modified)
52
+ undo_hash.easy_merge!(original)
53
+ modifier_field = trackable.history_trackable_options[:modifier_field]
54
+ undo_hash[modifier_field] = modifier
55
+ undo_hash
56
+ end
57
+
58
+ def redo_attr(modifier)
59
+ redo_hash = affected.easy_unmerge(original)
60
+ redo_hash.easy_merge!(modified)
61
+ modifier_field = trackable.history_trackable_options[:modifier_field]
62
+ redo_hash[modifier_field] = modifier
63
+ redo_hash
64
+ end
65
+
66
+ def trackable_root
67
+ @trackable_root ||= trackable_parents_and_trackable.first
68
+ end
69
+
70
+ def trackable
71
+ @trackable ||= trackable_parents_and_trackable.last
72
+ end
73
+
74
+ def trackable_parents
75
+ @trackable_parents ||= trackable_parents_and_trackable[0, -1]
76
+ end
77
+
78
+ def trackable_parent
79
+ @trackable_parent ||= trackable_parents_and_trackable[-2]
80
+ end
81
+
82
+
83
+ def affected
84
+ @affected ||= (modified.keys | original.keys).inject({}){ |h,k| h[k] =
85
+ trackable ? trackable.attributes[k] : modified[k]; h}
86
+ end
87
+
88
+ private
89
+
90
+ def re_create
91
+ association_chain.length > 1 ? create_on_parent : create_standalone
92
+ end
93
+
94
+ def re_destroy
95
+ trackable.destroy
96
+ end
97
+
98
+ def create_standalone
99
+ class_name = association_chain.first["name"]
100
+ restored = class_name.constantize.new(modified)
101
+ restored.id = modified["_id"]
102
+ restored.save!
103
+ end
104
+
105
+ def create_on_parent
106
+ name = association_chain.last["name"]
107
+ if embeds_one?(trackable_parent, name)
108
+ trackable_parent.send("create_#{name}!", modified)
109
+ elsif embeds_many?(trackable_parent, name)
110
+ trackable_parent.send(name).create!(modified)
111
+ else
112
+ raise "This should never happen. Please report bug!"
113
+ end
114
+ end
115
+
116
+ def trackable_parents_and_trackable
117
+ @trackable_parents_and_trackable ||= traverse_association_chain
118
+ end
119
+
120
+ def relation_of(doc, name)
121
+ meta = doc.reflect_on_association(name)
122
+ meta ? meta.relation : nil
123
+ end
124
+
125
+ def embeds_one?(doc, name)
126
+ relation_of(doc, name) == Mongoid::Relations::Embedded::One
127
+ end
128
+
129
+ def embeds_many?(doc, name)
130
+ relation_of(doc, name) == Mongoid::Relations::Embedded::Many
131
+ end
132
+
133
+ def traverse_association_chain
134
+ chain = association_chain.dup
135
+ doc = nil
136
+ documents = []
137
+
138
+ begin
139
+ node = chain.shift
140
+ name = node['name']
141
+
142
+ doc = if doc.nil?
143
+ # root association. First element of the association chain
144
+ klass = name.classify.constantize
145
+ klass.where(:_id => node['id']).first
146
+ elsif embeds_one?(doc, name)
147
+ doc.send(name)
148
+ elsif embeds_many?(doc, name)
149
+ doc.send(name).where(:_id => node['id']).first
150
+ else
151
+ raise "This should never happen. Please report bug."
152
+ end
153
+ documents << doc
154
+ end while( !chain.empty? )
155
+ documents
156
+ end
157
+
158
+ end
159
+ end
@@ -0,0 +1,5 @@
1
+ module Mongoid
2
+ module Audit
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'mongoid-audit/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "mongoid-audit"
8
+ gem.version = Mongoid::Audit::VERSION
9
+ gem.authors = ["Gleb Tv"]
10
+ gem.email = ["glebtv@gmail.com"]
11
+ gem.description = %q{Mongoid Audit}
12
+ gem.summary = %q{Easily track model change history and mantain audit log with mongoid}
13
+ gem.homepage = "https://github.com/rs-pro/mongoid-audit"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_runtime_dependency('easy_diff', ">= 0")
21
+ gem.add_runtime_dependency('mongoid', ">= 3.0.4")
22
+
23
+ gem.add_development_dependency('rspec', ["~> 2.12.0"])
24
+ gem.add_development_dependency('bundler', ['>= 1.0.0'])
25
+ gem.add_development_dependency('database_cleaner', [">= 0.8.0"])
26
+ gem.add_development_dependency('activesupport', '~> 3.2.11')
27
+ end
@@ -0,0 +1,553 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Mongoid::Audit do
4
+ before :all do
5
+ class HistoryTracker
6
+ include Mongoid::Audit::Tracker
7
+ end
8
+
9
+ class Post
10
+ include Mongoid::Document
11
+ include Mongoid::Timestamps
12
+ include Mongoid::Audit::Trackable
13
+
14
+ field :title
15
+ field :body
16
+ field :rating
17
+
18
+ embeds_many :comments
19
+ embeds_one :section
20
+ embeds_many :tags
21
+
22
+ accepts_nested_attributes_for :tags, :allow_destroy => true
23
+
24
+ track_history :on => [:title, :body], :track_destroy => true
25
+ end
26
+
27
+ class Comment
28
+ include Mongoid::Document
29
+ include Mongoid::Timestamps
30
+ include Mongoid::Audit::Trackable
31
+
32
+ field :title
33
+ field :body
34
+ embedded_in :post
35
+ track_history :on => [:title, :body], :scope => :post, :track_create => true, :track_destroy => true
36
+ end
37
+
38
+ class Section
39
+ include Mongoid::Document
40
+ include Mongoid::Timestamps
41
+ include Mongoid::Audit::Trackable
42
+
43
+ field :title
44
+ embedded_in :post
45
+ track_history :on => [:title], :scope => :post, :track_create => true, :track_destroy => true
46
+ end
47
+
48
+ class User
49
+ include Mongoid::Document
50
+ include Mongoid::Timestamps
51
+ include Mongoid::Audit::Trackable
52
+
53
+ field :email
54
+ field :name
55
+ track_history :except => [:email]
56
+ end
57
+
58
+ class Tag
59
+ include Mongoid::Document
60
+ include Mongoid::Timestamps
61
+ include Mongoid::Audit::Trackable
62
+
63
+ belongs_to :updated_by, :class_name => "User"
64
+
65
+ field :title
66
+ track_history :on => [:title], :scope => :post, :track_create => true, :track_destroy => true, :modifier_field => :updated_by
67
+ end
68
+ end
69
+
70
+ before :each do
71
+ @user = User.create(:name => "Aaron", :email => "aaron@randomemail.com")
72
+ @another_user = User.create(:name => "Another Guy", :email => "anotherguy@randomemail.com")
73
+ @post = Post.create(:title => "Test", :body => "Post", :modifier => @user, :views => 100)
74
+ @comment = @post.comments.create(:title => "test", :body => "comment", :modifier => @user)
75
+ end
76
+
77
+ describe "track" do
78
+ describe "on creation" do
79
+ it "should have one history track in comment" do
80
+ @comment.history_tracks.count.should == 1
81
+ end
82
+
83
+ it "should assign title and body on modified" do
84
+ @comment.history_tracks.first.modified.should == {'title' => "test", 'body' => "comment"}
85
+ end
86
+
87
+ it "should not assign title and body on original" do
88
+ @comment.history_tracks.first.original.should == {}
89
+ end
90
+
91
+ it "should assign modifier" do
92
+ @comment.history_tracks.first.modifier.should == @user
93
+ end
94
+
95
+ it "should assign version" do
96
+ @comment.history_tracks.first.version.should == 1
97
+ end
98
+
99
+ it "should assign scope" do
100
+ @comment.history_tracks.first.scope.should == "post"
101
+ end
102
+
103
+ it "should assign method" do
104
+ @comment.history_tracks.first.action.should == "create"
105
+ end
106
+
107
+ it "should assign association_chain" do
108
+ expected = [
109
+ {'id' => @post.id, 'name' => "Post"},
110
+ {'id' => @comment.id, 'name' => "comments"}
111
+ ]
112
+ @comment.history_tracks.first.association_chain.should == expected
113
+ end
114
+ end
115
+
116
+ describe "on destruction" do
117
+ it "should have two history track records in post" do
118
+ lambda {
119
+ @post.destroy
120
+ }.should change(HistoryTracker, :count).by(1)
121
+ end
122
+
123
+ it "should assign destroy on track record" do
124
+ @post.destroy
125
+ @post.history_tracks.last.action.should == "destroy"
126
+ end
127
+
128
+ it "should return affected attributes from track record" do
129
+ @post.destroy
130
+ @post.history_tracks.last.affected["title"].should == "Test"
131
+ end
132
+ end
133
+
134
+ describe "on update non-embedded" do
135
+ it "should create a history track if changed attributes match tracked attributes" do
136
+ lambda {
137
+ @post.update_attributes(:title => "Another Test")
138
+ }.should change(HistoryTracker, :count).by(1)
139
+ end
140
+
141
+ it "should not create a history track if changed attributes do not match tracked attributes" do
142
+ lambda {
143
+ @post.update_attributes(:rating => "untracked")
144
+ }.should change(HistoryTracker, :count).by(0)
145
+ end
146
+
147
+ it "should assign modified fields" do
148
+ @post.update_attributes(:title => "Another Test")
149
+ @post.history_tracks.last.modified.should == {
150
+ "title" => "Another Test"
151
+ }
152
+ end
153
+
154
+ it "should assign method field" do
155
+ @post.update_attributes(:title => "Another Test")
156
+ @post.history_tracks.last.action.should == "update"
157
+ end
158
+
159
+ it "should assign original fields" do
160
+ @post.update_attributes(:title => "Another Test")
161
+ @post.history_tracks.last.original.should == {
162
+ "title" => "Test"
163
+ }
164
+ end
165
+
166
+ it "should assign modifier" do
167
+ @post.update_attributes(:title => "Another Test")
168
+ @post.history_tracks.first.modifier.should == @user
169
+ end
170
+
171
+ it "should assign version on history tracks" do
172
+ @post.update_attributes(:title => "Another Test")
173
+ @post.history_tracks.first.version.should == 1
174
+ end
175
+
176
+ it "should assign version on post" do
177
+ @post.update_attributes(:title => "Another Test")
178
+ @post.version.should == 1
179
+ end
180
+
181
+ it "should assign scope" do
182
+ @post.update_attributes(:title => "Another Test")
183
+ @post.history_tracks.first.scope.should == "post"
184
+ end
185
+
186
+ it "should assign association_chain" do
187
+ @post.update_attributes(:title => "Another Test")
188
+ @post.history_tracks.last.association_chain.should == [{'id' => @post.id, 'name' => "Post"}]
189
+ end
190
+
191
+ it "should exclude defined options" do
192
+ @user.update_attributes(:name => "Aaron2", :email => "aaronsnewemail@randomemail.com")
193
+ @user.history_tracks.first.modified.keys.should include "name"
194
+ @user.history_tracks.first.modified.keys.should_not include "email"
195
+ end
196
+ end
197
+
198
+ describe "on update non-embedded twice" do
199
+ it "should assign version on post" do
200
+ @post.update_attributes(:title => "Test2")
201
+ @post.update_attributes(:title => "Test3")
202
+ @post.version.should == 2
203
+ end
204
+
205
+ it "should create a history track if changed attributes match tracked attributes" do
206
+ lambda {
207
+ @post.update_attributes(:title => "Test2")
208
+ @post.update_attributes(:title => "Test3")
209
+ }.should change(HistoryTracker, :count).by(2)
210
+ end
211
+
212
+ it "should create a history track of version 2" do
213
+ @post.update_attributes(:title => "Test2")
214
+ @post.update_attributes(:title => "Test3")
215
+ @post.history_tracks.where(:version => 2).first.should_not be_nil
216
+ end
217
+
218
+ it "should assign modified fields" do
219
+ @post.update_attributes(:title => "Test2")
220
+ @post.update_attributes(:title => "Test3")
221
+ @post.history_tracks.where(:version => 2).first.modified.should == {
222
+ "title" => "Test3"
223
+ }
224
+ end
225
+
226
+ it "should assign original fields" do
227
+ @post.update_attributes(:title => "Test2")
228
+ @post.update_attributes(:title => "Test3")
229
+ @post.history_tracks.where(:version => 2).first.original.should == {
230
+ "title" => "Test2"
231
+ }
232
+ end
233
+
234
+
235
+ it "should assign modifier" do
236
+ @post.update_attributes(:title => "Another Test", :modifier => @another_user)
237
+ @post.history_tracks.last.modifier.should == @another_user
238
+ end
239
+ end
240
+
241
+ describe "on update embedded 1..N (embeds_many)" do
242
+ it "should assign version on comment" do
243
+ @comment.update_attributes(:title => "Test2")
244
+ @comment.version.should == 2 # first track generated on creation
245
+ end
246
+
247
+ it "should create a history track of version 2" do
248
+ @comment.update_attributes(:title => "Test2")
249
+ @comment.history_tracks.where(:version => 2).first.should_not be_nil
250
+ end
251
+
252
+ it "should assign modified fields" do
253
+ @comment.update_attributes(:title => "Test2")
254
+ @comment.history_tracks.where(:version => 2).first.modified.should == {
255
+ "title" => "Test2"
256
+ }
257
+ end
258
+
259
+ it "should assign original fields" do
260
+ @comment.update_attributes(:title => "Test2")
261
+ @comment.history_tracks.where(:version => 2).first.original.should == {
262
+ "title" => "test"
263
+ }
264
+ end
265
+
266
+ it "should be possible to undo from parent" do
267
+ @comment.update_attributes(:title => "Test 2")
268
+ @post.history_tracks.last.undo!(@user)
269
+ @comment.reload
270
+ @comment.title.should == "test"
271
+ end
272
+
273
+ it "should assign modifier" do
274
+ @post.update_attributes(:title => "Another Test", :modifier => @another_user)
275
+ @post.history_tracks.last.modifier.should == @another_user
276
+ end
277
+ end
278
+
279
+ describe "on update embedded 1..1 (embeds_one)" do
280
+ before(:each) do
281
+ @section = Section.new(:title => 'Technology')
282
+ @post.section = @section
283
+ @post.save!
284
+ @post.reload
285
+ @section = @post.section
286
+ end
287
+
288
+ it "should assign version on create section" do
289
+ @section.version.should == 1
290
+ end
291
+
292
+ it "should assign version on section" do
293
+ @section.update_attributes(:title => 'Technology 2')
294
+ @section.version.should == 2 # first track generated on creation
295
+ end
296
+
297
+ it "should create a history track of version 2" do
298
+ @section.update_attributes(:title => 'Technology 2')
299
+ @section.history_tracks.where(:version => 2).first.should_not be_nil
300
+ end
301
+
302
+ it "should assign modified fields" do
303
+ @section.update_attributes(:title => 'Technology 2')
304
+ @section.history_tracks.where(:version => 2).first.modified.should == {
305
+ "title" => "Technology 2"
306
+ }
307
+ end
308
+
309
+ it "should assign original fields" do
310
+ @section.update_attributes(:title => 'Technology 2')
311
+ @section.history_tracks.where(:version => 2).first.original.should == {
312
+ "title" => "Technology"
313
+ }
314
+ end
315
+
316
+ it "should be possible to undo from parent" do
317
+ @section.update_attributes(:title => 'Technology 2')
318
+ @post.history_tracks.last.undo!(@user)
319
+ @section.reload
320
+ @section.title.should == "Technology"
321
+ end
322
+
323
+ it "should assign modifier" do
324
+ @section.update_attributes(:title => "Business", :modifier => @another_user)
325
+ @post.history_tracks.last.modifier.should == @another_user
326
+ end
327
+ end
328
+
329
+ describe "on destroy embedded" do
330
+ it "should be possible to re-create destroyed embedded" do
331
+ @comment.destroy
332
+ @comment.history_tracks.last.undo!(@user)
333
+ @post.reload
334
+ @post.comments.first.title.should == "test"
335
+ end
336
+
337
+ it "should be possible to re-create destroyed embedded from parent" do
338
+ @comment.destroy
339
+ @post.history_tracks.last.undo!(@user)
340
+ @post.reload
341
+ @post.comments.first.title.should == "test"
342
+ end
343
+
344
+ it "should be possible to destroy after re-create embedded from parent" do
345
+ @comment.destroy
346
+ @post.history_tracks.last.undo!(@user)
347
+ @post.history_tracks.last.undo!(@user)
348
+ @post.reload
349
+ @post.comments.count.should == 0
350
+ end
351
+
352
+ it "should be possible to create with redo after undo create embedded from parent" do
353
+ @post.comments.create!(:title => "The second one")
354
+ @track = @post.history_tracks.last
355
+ @track.undo!(@user)
356
+ @track.redo!(@user)
357
+ @post.reload
358
+ @post.comments.count.should == 2
359
+ end
360
+ end
361
+
362
+ describe "embedded with cascading callbacks" do
363
+ before(:each) do
364
+ Mongoid.instantiate_observers
365
+ Thread.current[:mongoid_history_sweeper_controller] = self
366
+ self.stub!(:current_user).and_return @user
367
+ @tag_foo = @post.tags.create(:title => "foo", :updated_by => @user)
368
+ @tag_bar = @post.tags.create(:title => "bar")
369
+ end
370
+
371
+ after(:each) do
372
+ Thread.current[:mongoid_history_sweeper_controller] = nil
373
+ end
374
+
375
+ it "should have cascaded the creation callbacks and set timestamps" do
376
+ @tag_foo.created_at.should_not be_nil
377
+ @tag_foo.updated_at.should_not be_nil
378
+ end
379
+
380
+ it "should allow an update through the parent model" do
381
+ update_hash = { "post" => { "tags_attributes" => { "1234" => { "id" => @tag_bar.id, "title" => "baz" } } } }
382
+ @post.update_attributes(update_hash["post"])
383
+ @post.tags.last.title.should == "baz"
384
+ end
385
+
386
+ it "should be possible to destroy through parent model using canoncial _destroy macro" do
387
+ @post.tags.count.should == 2
388
+ update_hash = { "post" => { "tags_attributes" => { "1234" => { "id" => @tag_bar.id, "title" => "baz", "_destroy" => "true"} } } }
389
+ @post.update_attributes(update_hash["post"])
390
+ @post.tags.count.should == 1
391
+ @post.history_tracks.last.action.should == "destroy"
392
+ end
393
+
394
+ it "should write relationship name for association_chain hiearchy instead of class name when using _destroy macro" do
395
+ update_hash = {"tags_attributes" => { "1234" => { "id" => @tag_foo.id, "_destroy" => "1"} } }
396
+ @post.update_attributes(update_hash)
397
+
398
+ # historically this would have evaluated to 'Tags' and an error would be thrown
399
+ # on any call that walked up the association_chain, e.g. 'trackable'
400
+ @tag_foo.history_tracks.last.association_chain.last["name"].should == "tags"
401
+ lambda{ @tag_foo.history_tracks.last.trackable }.should_not raise_error
402
+ end
403
+
404
+ it "should save modifier" do
405
+ @tag_foo.history_tracks.last.modifier.should eq @user
406
+ @tag_bar.history_tracks.last.modifier.should eq @user
407
+ end
408
+ end
409
+
410
+ describe "non-embedded" do
411
+ it "should undo changes" do
412
+ @post.update_attributes(:title => "Test2")
413
+ @post.history_tracks.where(:version => 1).last.undo!(@user)
414
+ @post.reload
415
+ @post.title.should == "Test"
416
+ end
417
+
418
+ it "should undo destruction" do
419
+ @post.destroy
420
+ @post.history_tracks.where(:version => 1).last.undo!(@user)
421
+ Post.find(@post.id).title.should == "Test"
422
+ end
423
+
424
+ it "should create a new history track after undo" do
425
+ @post.update_attributes(:title => "Test2")
426
+ @post.history_tracks.last.undo!(@user)
427
+ @post.reload
428
+ @post.history_tracks.count.should == 3
429
+ end
430
+
431
+ it "should assign @user as the modifier of the newly created history track" do
432
+ @post.update_attributes(:title => "Test2")
433
+ @post.history_tracks.where(:version => 1).last.undo!(@user)
434
+ @post.reload
435
+ @post.history_tracks.where(:version => 2).last.modifier.should == @user
436
+ end
437
+
438
+ it "should stay the same after undo and redo" do
439
+ @post.update_attributes(:title => "Test2")
440
+ @track = @post.history_tracks.last
441
+ @track.undo!(@user)
442
+ @track.redo!(@user)
443
+ @post2 = Post.where(:_id => @post.id).first
444
+
445
+ @post.title.should == @post2.title
446
+ end
447
+
448
+ it "should be destroyed after undo and redo" do
449
+ @post.destroy
450
+ @track = @post.history_tracks.where(:version => 1).last
451
+ @track.undo!(@user)
452
+ @track.redo!(@user)
453
+ Post.where(:_id => @post.id).first.should == nil
454
+ end
455
+ end
456
+
457
+ describe "embedded" do
458
+ it "should undo changes" do
459
+ @comment.update_attributes(:title => "Test2")
460
+ @comment.history_tracks.where(:version => 2).first.undo!(@user)
461
+ # reloading an embedded document === KAMIKAZE
462
+ # at least for the current release of mongoid...
463
+ @post.reload
464
+ @comment = @post.comments.first
465
+ @comment.title.should == "test"
466
+ end
467
+
468
+ it "should create a new history track after undo" do
469
+ @comment.update_attributes(:title => "Test2")
470
+ @comment.history_tracks.where(:version => 2).first.undo!(@user)
471
+ @post.reload
472
+ @comment = @post.comments.first
473
+ @comment.history_tracks.count.should == 3
474
+ end
475
+
476
+ it "should assign @user as the modifier of the newly created history track" do
477
+ @comment.update_attributes(:title => "Test2")
478
+ @comment.history_tracks.where(:version => 2).first.undo!(@user)
479
+ @post.reload
480
+ @comment = @post.comments.first
481
+ @comment.history_tracks.where(:version => 3).first.modifier.should == @user
482
+ end
483
+
484
+ it "should stay the same after undo and redo" do
485
+ @comment.update_attributes(:title => "Test2")
486
+ @track = @comment.history_tracks.where(:version => 2).first
487
+ @track.undo!(@user)
488
+ @track.redo!(@user)
489
+ @post2 = Post.where(:_id => @post.id).first
490
+ @comment2 = @post2.comments.first
491
+
492
+ @comment.title.should == @comment2.title
493
+ end
494
+ end
495
+
496
+ describe "trackables" do
497
+ before :each do
498
+ @comment.update_attributes(:title => "Test2") # version == 2
499
+ @comment.update_attributes(:title => "Test3") # version == 3
500
+ @comment.update_attributes(:title => "Test4") # version == 4
501
+ end
502
+
503
+ describe "undo" do
504
+ it "should recognize :from, :to options" do
505
+ @comment.undo! @user, :from => 4, :to => 2
506
+ @comment.title.should == "test"
507
+ end
508
+
509
+ it "should recognize parameter as version number" do
510
+ @comment.undo! @user, 3
511
+ @comment.title.should == "Test2"
512
+ end
513
+
514
+ it "should undo last version when no parameter is specified" do
515
+ @comment.undo! @user
516
+ @comment.title.should == "Test3"
517
+ end
518
+
519
+ it "should recognize :last options" do
520
+ @comment.undo! @user, :last => 2
521
+ @comment.title.should == "Test2"
522
+ end
523
+ end
524
+
525
+ describe "redo" do
526
+ before :each do
527
+ @comment.update_attributes(:title => "Test5")
528
+ end
529
+
530
+ it "should recognize :from, :to options" do
531
+ @comment.redo! @user, :from => 2, :to => 4
532
+ @comment.title.should == "Test4"
533
+ end
534
+
535
+ it "should recognize parameter as version number" do
536
+ @comment.redo! @user, 2
537
+ @comment.title.should == "Test2"
538
+ end
539
+
540
+ it "should redo last version when no parameter is specified" do
541
+ @comment.redo! @user
542
+ @comment.title.should == "Test5"
543
+ end
544
+
545
+ it "should recognize :last options" do
546
+ @comment.redo! @user, :last => 1
547
+ @comment.title.should == "Test5"
548
+ end
549
+
550
+ end
551
+ end
552
+ end
553
+ end