mongoid-audit 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.rspec +1 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +191 -0
- data/Rakefile +1 -0
- data/config/mongoid.yml +8 -0
- data/lib/mongoid-audit.rb +24 -0
- data/lib/mongoid-audit/sweeper.rb +40 -0
- data/lib/mongoid-audit/trackable.rb +262 -0
- data/lib/mongoid-audit/tracker.rb +159 -0
- data/lib/mongoid-audit/version.rb +5 -0
- data/mongoid-audit.gemspec +27 -0
- data/spec/integration/integration_spec.rb +553 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/support/database_cleaner.rb +9 -0
- data/spec/support/mongoid.rb +17 -0
- data/spec/trackable_spec.rb +121 -0
- data/spec/tracker_spec.rb +17 -0
- metadata +168 -0
@@ -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,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
|