mongoid-history 0.8.3 → 0.8.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/.coveralls.yml +1 -1
  3. data/.document +5 -5
  4. data/.github/workflows/test.yml +72 -0
  5. data/.gitignore +46 -46
  6. data/.rspec +2 -2
  7. data/.rubocop.yml +6 -6
  8. data/.rubocop_todo.yml +99 -99
  9. data/CHANGELOG.md +173 -163
  10. data/CONTRIBUTING.md +117 -118
  11. data/Dangerfile +1 -1
  12. data/Gemfile +49 -40
  13. data/LICENSE.txt +20 -20
  14. data/README.md +609 -608
  15. data/RELEASING.md +66 -67
  16. data/Rakefile +24 -24
  17. data/UPGRADING.md +53 -53
  18. data/lib/mongoid/history/attributes/base.rb +72 -72
  19. data/lib/mongoid/history/attributes/create.rb +45 -45
  20. data/lib/mongoid/history/attributes/destroy.rb +34 -34
  21. data/lib/mongoid/history/attributes/update.rb +104 -104
  22. data/lib/mongoid/history/options.rb +177 -177
  23. data/lib/mongoid/history/trackable.rb +588 -583
  24. data/lib/mongoid/history/tracker.rb +247 -247
  25. data/lib/mongoid/history/version.rb +5 -5
  26. data/lib/mongoid/history.rb +77 -77
  27. data/lib/mongoid-history.rb +1 -1
  28. data/mongoid-history.gemspec +25 -25
  29. data/perf/benchmark_modified_attributes_for_create.rb +65 -65
  30. data/perf/gc_suite.rb +21 -21
  31. data/spec/integration/embedded_in_polymorphic_spec.rb +112 -112
  32. data/spec/integration/integration_spec.rb +976 -976
  33. data/spec/integration/multi_relation_spec.rb +47 -47
  34. data/spec/integration/multiple_trackers_spec.rb +68 -68
  35. data/spec/integration/nested_embedded_documents_spec.rb +64 -64
  36. data/spec/integration/nested_embedded_documents_tracked_in_parent_spec.rb +124 -124
  37. data/spec/integration/nested_embedded_polymorphic_documents_spec.rb +115 -115
  38. data/spec/integration/subclasses_spec.rb +47 -47
  39. data/spec/integration/track_history_order_spec.rb +84 -84
  40. data/spec/integration/validation_failure_spec.rb +76 -76
  41. data/spec/spec_helper.rb +32 -30
  42. data/spec/support/error_helpers.rb +7 -0
  43. data/spec/support/mongoid.rb +11 -11
  44. data/spec/support/mongoid_history.rb +12 -12
  45. data/spec/unit/attributes/base_spec.rb +141 -141
  46. data/spec/unit/attributes/create_spec.rb +342 -342
  47. data/spec/unit/attributes/destroy_spec.rb +228 -228
  48. data/spec/unit/attributes/update_spec.rb +342 -342
  49. data/spec/unit/callback_options_spec.rb +165 -165
  50. data/spec/unit/embedded_methods_spec.rb +87 -87
  51. data/spec/unit/history_spec.rb +58 -58
  52. data/spec/unit/my_instance_methods_spec.rb +555 -555
  53. data/spec/unit/options_spec.rb +365 -365
  54. data/spec/unit/singleton_methods_spec.rb +406 -406
  55. data/spec/unit/store/default_store_spec.rb +11 -11
  56. data/spec/unit/store/request_store_spec.rb +13 -13
  57. data/spec/unit/trackable_spec.rb +1057 -987
  58. data/spec/unit/tracker_spec.rb +190 -190
  59. metadata +9 -7
  60. data/.travis.yml +0 -36
@@ -1,976 +1,976 @@
1
- require 'spec_helper'
2
-
3
- describe Mongoid::History do
4
- before :each do
5
- class Post
6
- include Mongoid::Document
7
- include Mongoid::Timestamps
8
- include Mongoid::History::Trackable
9
-
10
- field :title
11
- field :body
12
- field :rating
13
- field :views, type: Integer
14
-
15
- embeds_many :comments, store_as: :coms
16
- embeds_one :section, store_as: :sec
17
- embeds_many :tags, cascade_callbacks: true
18
-
19
- accepts_nested_attributes_for :tags, allow_destroy: true
20
-
21
- track_history on: %i[title body], track_destroy: true
22
- end
23
-
24
- class Comment
25
- include Mongoid::Document
26
- include Mongoid::Timestamps
27
- include Mongoid::History::Trackable
28
-
29
- field :t, as: :title
30
- field :body
31
- embedded_in :commentable, polymorphic: true
32
- # BUG: see https://github.com/mongoid/mongoid-history/issues/223, modifier_field_optional should not be necessary
33
- track_history on: %i[title body], scope: :post, track_create: true, track_destroy: true, modifier_field_optional: true
34
- end
35
-
36
- class Section
37
- include Mongoid::Document
38
- include Mongoid::Timestamps
39
- include Mongoid::History::Trackable
40
-
41
- field :t, as: :title
42
- embedded_in :post
43
- track_history on: [:title], scope: :post, track_create: true, track_destroy: true
44
- end
45
-
46
- class User
47
- include Mongoid::Document
48
- include Mongoid::Timestamps
49
- include Mongoid::History::Trackable
50
-
51
- field :n, as: :name
52
- field :em, as: :email
53
- field :phone
54
- field :address
55
- field :city
56
- field :country
57
- field :aliases, type: Array
58
- track_history except: %i[email updated_at], modifier_field_optional: true
59
- end
60
-
61
- class Tag
62
- include Mongoid::Document
63
- # include Mongoid::Timestamps (see: https://github.com/mongoid/mongoid/issues/3078)
64
- include Mongoid::History::Trackable
65
-
66
- belongs_to :updated_by, class_name: 'User'
67
-
68
- field :title
69
- track_history on: [:title], scope: :post, track_create: true, track_destroy: true, modifier_field: :updated_by
70
- end
71
-
72
- class Foo < Comment
73
- end
74
- end
75
-
76
- after :each do
77
- Object.send(:remove_const, :Post)
78
- Object.send(:remove_const, :Comment)
79
- Object.send(:remove_const, :Section)
80
- Object.send(:remove_const, :User)
81
- Object.send(:remove_const, :Tag)
82
- Object.send(:remove_const, :Foo)
83
- end
84
-
85
- let(:user) { User.create!(name: 'Aaron', email: 'aaron@randomemail.com', aliases: ['bob'], country: 'Canada', city: 'Toronto', address: '21 Jump Street') }
86
- let(:another_user) { User.create!(name: 'Another Guy', email: 'anotherguy@randomemail.com') }
87
- let(:post) { Post.create!(title: 'Test', body: 'Post', modifier: user, views: 100) }
88
- let(:comment) { post.comments.create!(title: 'test', body: 'comment', modifier: user) }
89
- let(:tag) { Tag.create!(title: 'test', updated_by: user) }
90
-
91
- describe 'track' do
92
- describe 'on creation' do
93
- it 'should have one history track in comment' do
94
- expect(comment.history_tracks.count).to eq(1)
95
- end
96
-
97
- it 'should assign title and body on modified' do
98
- expect(comment.history_tracks.first.modified).to eq('t' => 'test', 'body' => 'comment')
99
- end
100
-
101
- it 'should not assign title and body on original' do
102
- expect(comment.history_tracks.first.original).to eq({})
103
- end
104
-
105
- it 'should assign modifier' do
106
- expect(comment.history_tracks.first.modifier.id).to eq(user.id)
107
- end
108
-
109
- it 'should assign version' do
110
- expect(comment.history_tracks.first.version).to eq(1)
111
- end
112
-
113
- it 'should assign scope' do
114
- expect(comment.history_tracks.first.scope).to eq('post')
115
- end
116
-
117
- it 'should assign method' do
118
- expect(comment.history_tracks.first.action).to eq('create')
119
- end
120
-
121
- it 'should assign association_chain' do
122
- expected = [
123
- { 'id' => post.id, 'name' => 'Post' },
124
- { 'id' => comment.id, 'name' => 'coms' }
125
- ]
126
- expect(comment.history_tracks.first.association_chain).to eq(expected)
127
- end
128
- end
129
-
130
- describe 'on destruction' do
131
- it 'should have two history track records in post' do
132
- post # This will create history track records for creation
133
- expect do
134
- post.destroy
135
- end.to change(Tracker, :count).by(1)
136
- end
137
-
138
- it 'should assign destroy on track record' do
139
- post.destroy
140
- expect(post.history_tracks.last.action).to eq('destroy')
141
- end
142
-
143
- it 'should return affected attributes from track record' do
144
- post.destroy
145
- expect(post.history_tracks.last.affected['title']).to eq('Test')
146
- end
147
-
148
- it 'should no-op on repeated calls to destroy' do
149
- post.destroy
150
- expect do
151
- post.destroy
152
- end.not_to change(Tracker, :count)
153
- end
154
- end
155
-
156
- describe 'on update non-embedded' do
157
- it 'should create a history track if changed attributes match tracked attributes' do
158
- post # This will create history track records for creation
159
- expect do
160
- post.update_attributes!(title: 'Another Test')
161
- end.to change(Tracker, :count).by(1)
162
- end
163
-
164
- it 'should not create a history track if changed attributes do not match tracked attributes' do
165
- post # This will create history track records for creation
166
- expect do
167
- post.update_attributes!(rating: 'untracked')
168
- end.to change(Tracker, :count).by(0)
169
- end
170
-
171
- it 'should assign modified fields' do
172
- post.update_attributes!(title: 'Another Test')
173
- expect(post.history_tracks.last.modified).to eq(
174
- 'title' => 'Another Test'
175
- )
176
- end
177
-
178
- it 'should assign method field' do
179
- post.update_attributes!(title: 'Another Test')
180
- expect(post.history_tracks.last.action).to eq('update')
181
- end
182
-
183
- it 'should assign original fields' do
184
- post.update_attributes!(title: 'Another Test')
185
- expect(post.history_tracks.last.original).to eq(
186
- 'title' => 'Test'
187
- )
188
- end
189
-
190
- it 'should assign modifier' do
191
- post.update_attributes!(title: 'Another Test')
192
- expect(post.history_tracks.first.modifier.id).to eq(user.id)
193
- end
194
-
195
- it 'should assign version on history tracks' do
196
- post.update_attributes!(title: 'Another Test')
197
- expect(post.history_tracks.first.version).to eq(1)
198
- end
199
-
200
- it 'should assign version on post' do
201
- expect(post.version).to eq(1) # Created
202
- post.update_attributes!(title: 'Another Test')
203
- expect(post.version).to eq(2) # Updated
204
- end
205
-
206
- it 'should assign scope' do
207
- post.update_attributes!(title: 'Another Test')
208
- expect(post.history_tracks.first.scope).to eq('post')
209
- end
210
-
211
- it 'should assign association_chain' do
212
- post.update_attributes!(title: 'Another Test')
213
- expect(post.history_tracks.last.association_chain).to eq([{ 'id' => post.id, 'name' => 'Post' }])
214
- end
215
-
216
- it 'should exclude defined options' do
217
- name = user.name
218
- user.update_attributes!(name: 'Aaron2', email: 'aaronsnewemail@randomemail.com')
219
- expect(user.history_tracks.last.original.keys).to eq(['n'])
220
- expect(user.history_tracks.last.original['n']).to eq(name)
221
- expect(user.history_tracks.last.modified.keys).to eq(['n'])
222
- expect(user.history_tracks.last.modified['n']).to eq(user.name)
223
- end
224
-
225
- it 'should undo field changes' do
226
- name = user.name
227
- user.update_attributes!(name: 'Aaron2', email: 'aaronsnewemail@randomemail.com')
228
- user.history_tracks.last.undo! nil
229
- expect(user.reload.name).to eq(name)
230
- end
231
-
232
- it 'should undo non-existing field changes' do
233
- post = Post.create!(modifier: user, views: 100)
234
- expect(post.reload.title).to be_nil
235
- post.update_attributes!(title: 'Aaron2')
236
- expect(post.reload.title).to eq('Aaron2')
237
- post.history_tracks.last.undo! user
238
- expect(post.reload.title).to be_nil
239
- end
240
-
241
- it 'should track array changes' do
242
- aliases = user.aliases
243
- user.update_attributes!(aliases: %w[bob joe])
244
- expect(user.history_tracks.last.original['aliases']).to eq(aliases)
245
- expect(user.history_tracks.last.modified['aliases']).to eq(user.aliases)
246
- end
247
-
248
- it 'should undo array changes' do
249
- aliases = user.aliases
250
- user.update_attributes!(aliases: %w[bob joe])
251
- user.history_tracks.last.undo! nil
252
- expect(user.reload.aliases).to eq(aliases)
253
- end
254
- end
255
-
256
- describe '#tracked_changes' do
257
- context 'create action' do
258
- subject { tag.history_tracks.first.tracked_changes }
259
- it 'consider all fields values as :to' do
260
- expect(subject[:title]).to eq({ to: 'test' }.with_indifferent_access)
261
- end
262
- end
263
- context 'destroy action' do
264
- subject do
265
- tag.destroy
266
- tag.history_tracks.last.tracked_changes
267
- end
268
- it 'consider all fields values as :from' do
269
- expect(subject[:title]).to eq({ from: 'test' }.with_indifferent_access)
270
- end
271
- end
272
- context 'update action' do
273
- subject { user.history_tracks.last.tracked_changes }
274
- before do
275
- user.update_attributes!(name: 'Aaron2', email: nil, country: '', city: nil, phone: '867-5309', aliases: ['', 'bill', 'james'])
276
- end
277
- it { is_expected.to be_a HashWithIndifferentAccess }
278
- it 'should track changed field' do
279
- expect(subject[:n]).to eq({ from: 'Aaron', to: 'Aaron2' }.with_indifferent_access)
280
- end
281
- it 'should track added field' do
282
- expect(subject[:phone]).to eq({ to: '867-5309' }.with_indifferent_access)
283
- end
284
- it 'should track removed field' do
285
- expect(subject[:city]).to eq({ from: 'Toronto' }.with_indifferent_access)
286
- end
287
- it 'should not consider blank as removed' do
288
- expect(subject[:country]).to eq({ from: 'Canada', to: '' }.with_indifferent_access)
289
- end
290
- it 'should track changed array field' do
291
- expect(subject[:aliases]).to eq({ from: ['bob'], to: ['', 'bill', 'james'] }.with_indifferent_access)
292
- end
293
- it 'should not track unmodified field' do
294
- expect(subject[:address]).to be_nil
295
- end
296
- it 'should not track untracked fields' do
297
- expect(subject[:email]).to be_nil
298
- end
299
- end
300
- end
301
-
302
- describe '#tracked_edits' do
303
- context 'create action' do
304
- subject { tag.history_tracks.first.tracked_edits }
305
- it 'consider all edits as ;add' do
306
- expect(subject[:add]).to eq({ title: 'test' }.with_indifferent_access)
307
- end
308
- end
309
- context 'destroy action' do
310
- subject do
311
- tag.destroy
312
- tag.history_tracks.last.tracked_edits
313
- end
314
- it 'consider all edits as ;remove' do
315
- expect(subject[:remove]).to eq({ title: 'test' }.with_indifferent_access)
316
- end
317
- end
318
- context 'update action' do
319
- subject { user.history_tracks.last.tracked_edits }
320
- before do
321
- user.update_attributes!(name: 'Aaron2', email: nil, country: '', city: nil, phone: '867-5309', aliases: ['', 'bill', 'james'])
322
- end
323
- it { is_expected.to be_a HashWithIndifferentAccess }
324
- it 'should track changed field' do
325
- expect(subject[:modify]).to eq({ n: { from: 'Aaron', to: 'Aaron2' } }.with_indifferent_access)
326
- end
327
- it 'should track added field' do
328
- expect(subject[:add]).to eq({ phone: '867-5309' }.with_indifferent_access)
329
- end
330
- it 'should track removed field and consider blank as removed' do
331
- expect(subject[:remove]).to eq({ city: 'Toronto', country: 'Canada' }.with_indifferent_access)
332
- end
333
- it 'should track changed array field' do
334
- expect(subject[:array]).to eq({ aliases: { remove: ['bob'], add: ['', 'bill', 'james'] } }.with_indifferent_access)
335
- end
336
- it 'should not track unmodified field' do
337
- %w[add modify remove array].each do |edit|
338
- expect(subject[edit][:address]).to be_nil
339
- end
340
- end
341
- it 'should not track untracked fields' do
342
- %w[add modify remove array].each do |edit|
343
- expect(subject[edit][:email]).to be_nil
344
- end
345
- end
346
- end
347
- context 'with empty values' do
348
- before do
349
- allow(subject).to receive(:trackable_parent_class) { Tracker }
350
- allow(Tracker).to receive(:tracked_embeds_many?) { false }
351
- end
352
- subject { Tracker.new }
353
- it 'should skip empty values' do
354
- allow(subject).to receive(:tracked_changes) { { name: { to: '', from: [] }, city: { to: 'Toronto', from: '' } } }
355
- expect(subject.tracked_edits).to eq({ add: { city: 'Toronto' } }.with_indifferent_access)
356
- end
357
- end
358
- end
359
-
360
- describe 'on update non-embedded twice' do
361
- it 'should assign version on post' do
362
- expect(post.version).to eq(1)
363
- post.update_attributes!(title: 'Test2')
364
- post.update_attributes!(title: 'Test3')
365
- expect(post.version).to eq(3)
366
- end
367
-
368
- it 'should create a history track if changed attributes match tracked attributes' do
369
- post # Created
370
- expect do
371
- post.update_attributes!(title: 'Test2')
372
- post.update_attributes!(title: 'Test3')
373
- end.to change(Tracker, :count).by(2)
374
- end
375
-
376
- it 'should create a history track of version 2' do
377
- post.update_attributes!(title: 'Test2')
378
- post.update_attributes!(title: 'Test3')
379
- expect(post.history_tracks.where(version: 2).first).not_to be_nil
380
- end
381
-
382
- it 'should assign modified fields' do
383
- post.update_attributes!(title: 'Test2')
384
- post.update_attributes!(title: 'Test3')
385
- expect(post.history_tracks.where(version: 3).first.modified).to eq(
386
- 'title' => 'Test3'
387
- )
388
- end
389
-
390
- it 'should assign original fields' do
391
- post.update_attributes!(title: 'Test2')
392
- post.update_attributes!(title: 'Test3')
393
- expect(post.history_tracks.where(version: 3).first.original).to eq(
394
- 'title' => 'Test2'
395
- )
396
- end
397
-
398
- it 'should assign modifier' do
399
- post.update_attributes!(title: 'Another Test', modifier: another_user)
400
- expect(post.history_tracks.last.modifier.id).to eq(another_user.id)
401
- end
402
- end
403
-
404
- describe 'on update embedded 1..N (embeds_many)' do
405
- it 'should assign version on comment' do
406
- comment.update_attributes!(title: 'Test2')
407
- expect(comment.version).to eq(2) # first track generated on creation
408
- end
409
-
410
- it 'should create a history track of version 2' do
411
- comment.update_attributes!(title: 'Test2')
412
- expect(comment.history_tracks.where(version: 2).first).not_to be_nil
413
- end
414
-
415
- it 'should assign modified fields' do
416
- comment.update_attributes!(t: 'Test2')
417
- expect(comment.history_tracks.where(version: 2).first.modified).to eq(
418
- 't' => 'Test2'
419
- )
420
- end
421
-
422
- it 'should assign original fields' do
423
- comment.update_attributes!(title: 'Test2')
424
- expect(comment.history_tracks.where(version: 2).first.original).to eq(
425
- 't' => 'test'
426
- )
427
- end
428
-
429
- it 'should be possible to undo from parent' do
430
- comment.update_attributes!(title: 'Test 2')
431
- user
432
- post.history_tracks.last.undo!(user)
433
- comment.reload
434
- expect(comment.title).to eq('test')
435
- end
436
-
437
- it 'should assign modifier' do
438
- post.update_attributes!(title: 'Another Test', modifier: another_user)
439
- expect(post.history_tracks.last.modifier.id).to eq(another_user.id)
440
- end
441
- end
442
-
443
- describe 'on update embedded 1..1 (embeds_one)' do
444
- let(:section) { Section.new(title: 'Technology', modifier: user) }
445
-
446
- before(:each) do
447
- post.section = section
448
- post.modifier = user
449
- post.save!
450
- post.reload
451
- post.section
452
- end
453
-
454
- it 'should assign version on create section' do
455
- expect(section.version).to eq(1)
456
- end
457
-
458
- it 'should assign version on section' do
459
- section.update_attributes!(title: 'Technology 2')
460
- expect(section.version).to eq(2) # first track generated on creation
461
- end
462
-
463
- it 'should create a history track of version 2' do
464
- section.update_attributes!(title: 'Technology 2')
465
- expect(section.history_tracks.where(version: 2).first).not_to be_nil
466
- end
467
-
468
- it 'should assign modified fields' do
469
- section.update_attributes!(title: 'Technology 2')
470
- expect(section.history_tracks.where(version: 2).first.modified).to eq(
471
- 't' => 'Technology 2'
472
- )
473
- end
474
-
475
- it 'should assign original fields' do
476
- section.update_attributes!(title: 'Technology 2')
477
- expect(section.history_tracks.where(version: 2).first.original).to eq(
478
- 't' => 'Technology'
479
- )
480
- end
481
-
482
- it 'should be possible to undo from parent' do
483
- section.update_attributes!(title: 'Technology 2')
484
- post.history_tracks.last.undo!(user)
485
- section.reload
486
- expect(section.title).to eq('Technology')
487
- end
488
-
489
- it 'should assign modifier' do
490
- section.update_attributes!(title: 'Business', modifier: another_user)
491
- expect(post.history_tracks.last.modifier.id).to eq(another_user.id)
492
- end
493
- end
494
-
495
- describe 'on destroy embedded' do
496
- it 'should be possible to re-create destroyed embedded' do
497
- comment.destroy
498
- comment.history_tracks.last.undo!(user)
499
- post.reload
500
- expect(post.comments.first.title).to eq('test')
501
- end
502
-
503
- it 'should be possible to re-create destroyed embedded from parent' do
504
- comment.destroy
505
- post.history_tracks.last.undo!(user)
506
- post.reload
507
- expect(post.comments.first.title).to eq('test')
508
- end
509
-
510
- it 'should be possible to destroy after re-create embedded from parent' do
511
- comment.destroy
512
- post.history_tracks.last.undo!(user)
513
- post.history_tracks.last.undo!(user)
514
- post.reload
515
- expect(post.comments.count).to eq(0)
516
- end
517
-
518
- it 'should be possible to create with redo after undo create embedded from parent' do
519
- comment # initialize
520
- post.comments.create!(title: 'The second one', modifier: user)
521
- track = post.history_tracks[2]
522
- expect(post.reload.comments.count).to eq 2
523
- track.undo!(user)
524
- expect(post.reload.comments.count).to eq 1
525
- track.redo!(user)
526
- expect(post.reload.comments.count).to eq 2
527
- end
528
- end
529
-
530
- describe 'embedded with cascading callbacks' do
531
- let(:tag_foo) { post.tags.create!(title: 'foo', updated_by: user) }
532
- let(:tag_bar) { post.tags.create!(title: 'bar', updated_by: user) }
533
-
534
- it 'should allow an update through the parent model' do
535
- update_hash = { 'post' => { 'tags_attributes' => { '1234' => { 'id' => tag_bar.id, 'title' => 'baz' } } } }
536
- post.update_attributes!(update_hash['post'])
537
- expect(post.tags.last.title).to eq('baz')
538
- end
539
-
540
- it 'should be possible to destroy through parent model using canoncial _destroy macro' do
541
- tag_foo
542
- tag_bar # initialize
543
- expect(post.tags.count).to eq(2)
544
- update_hash = { 'post' => { 'tags_attributes' => { '1234' => { 'id' => tag_bar.id, 'title' => 'baz', '_destroy' => 'true' } } } }
545
- post.update_attributes!(update_hash['post'])
546
- expect(post.tags.count).to eq(1)
547
- expect(post.history_tracks.to_a.last.action).to eq('destroy')
548
- end
549
-
550
- it 'should write relationship name for association_chain hiearchy instead of class name when using _destroy macro' do
551
- update_hash = { 'tags_attributes' => { '1234' => { 'id' => tag_foo.id, '_destroy' => '1' } } }
552
- post.update_attributes!(update_hash)
553
-
554
- # historically this would have evaluated to 'Tags' and an error would be thrown
555
- # on any call that walked up the association_chain, e.g. 'trackable'
556
- expect(tag_foo.history_tracks.last.association_chain.last['name']).to eq('tags')
557
- expect { tag_foo.history_tracks.last.trackable }.not_to raise_error
558
- end
559
- end
560
-
561
- describe 'non-embedded' do
562
- it 'should undo changes' do
563
- post.update_attributes!(title: 'Test2')
564
- post.history_tracks.where(version: 2).last.undo!(user)
565
- post.reload
566
- expect(post.title).to eq('Test')
567
- end
568
-
569
- it 'should undo destruction' do
570
- post.destroy
571
- post.history_tracks.where(version: 2).last.undo!(user)
572
- expect(Post.find(post.id).title).to eq('Test')
573
- end
574
-
575
- it 'should create a new history track after undo' do
576
- comment # initialize
577
- post.update_attributes!(title: 'Test2')
578
- post.history_tracks.last.undo!(user)
579
- post.reload
580
- expect(post.history_tracks.count).to eq(4)
581
- end
582
-
583
- it 'should assign user as the modifier of the newly created history track' do
584
- post.update_attributes!(title: 'Test2')
585
- post.history_tracks.where(version: 2).last.undo!(user)
586
- post.reload
587
- expect(post.history_tracks.where(version: 2).last.modifier.id).to eq(user.id)
588
- end
589
-
590
- it 'should stay the same after undo and redo' do
591
- post.update_attributes!(title: 'Test2')
592
- track = post.history_tracks.last
593
- track.undo!(user)
594
- track.redo!(user)
595
- post2 = Post.where(_id: post.id).first
596
-
597
- expect(post.title).to eq(post2.title)
598
- end
599
-
600
- it 'should be destroyed after undo and redo' do
601
- post.destroy
602
- track = post.history_tracks.where(version: 2).last
603
- track.undo!(user)
604
- track.redo!(user)
605
- expect(Post.where(_id: post.id).first).to be_nil
606
- end
607
- end
608
-
609
- describe 'embedded' do
610
- it 'should undo changes' do
611
- comment.update_attributes!(title: 'Test2')
612
- comment.history_tracks.where(version: 2).first.undo!(user)
613
- comment.reload
614
- expect(comment.title).to eq('test')
615
- end
616
-
617
- it 'should create a new history track after undo' do
618
- comment.update_attributes!(title: 'Test2')
619
- comment.history_tracks.where(version: 2).first.undo!(user)
620
- comment.reload
621
- expect(comment.history_tracks.count).to eq(3)
622
- end
623
-
624
- it 'should assign user as the modifier of the newly created history track' do
625
- comment.update_attributes!(title: 'Test2')
626
- comment.history_tracks.where(version: 2).first.undo!(user)
627
- comment.reload
628
- expect(comment.history_tracks.where(version: 3).first.modifier.id).to eq(user.id)
629
- end
630
-
631
- it 'should stay the same after undo and redo' do
632
- comment.update_attributes!(title: 'Test2')
633
- track = comment.history_tracks.where(version: 2).first
634
- track.undo!(user)
635
- track.redo!(user)
636
- comment.reload
637
- expect(comment.title).to eq('Test2')
638
- end
639
- end
640
-
641
- describe 'trackables' do
642
- before :each do
643
- comment.update_attributes!(title: 'Test2') # version == 2
644
- comment.update_attributes!(title: 'Test3') # version == 3
645
- comment.update_attributes!(title: 'Test4') # version == 4
646
- end
647
-
648
- describe 'undo' do
649
- { 'undo' => [nil], 'undo!' => [nil, :reload] }.each do |test_method, methods|
650
- methods.each do |method|
651
- context (method || 'instance').to_s do
652
- it 'recognizes :from, :to options' do
653
- comment.send test_method, user, from: 4, to: 2
654
- comment.send(method) if method
655
- expect(comment.title).to eq('test')
656
- end
657
-
658
- it 'recognizes parameter as version number' do
659
- comment.send test_method, user, 3
660
- comment.send(method) if method
661
- expect(comment.title).to eq('Test2')
662
- end
663
-
664
- it 'should undo last version when no parameter is specified' do
665
- comment.send test_method, user
666
- comment.send(method) if method
667
- expect(comment.title).to eq('Test3')
668
- end
669
-
670
- it 'recognizes :last options' do
671
- comment.send test_method, user, last: 2
672
- comment.send(method) if method
673
- expect(comment.title).to eq('Test2')
674
- end
675
-
676
- if Mongoid::Compatibility::Version.mongoid3?
677
- context 'protected attributes' do
678
- before :each do
679
- Comment.attr_accessible(nil)
680
- end
681
-
682
- after :each do
683
- Comment.attr_protected(nil)
684
- end
685
-
686
- it 'should undo last version when no parameter is specified on protected attributes' do
687
- comment.send test_method, user
688
- comment.send(method) if method
689
- expect(comment.title).to eq('Test3')
690
- end
691
-
692
- it 'recognizes :last options on model with protected attributes' do
693
- comment.send test_method, user, last: 2
694
- comment.send(method) if method
695
- expect(comment.title).to eq('Test2')
696
- end
697
- end
698
- end
699
- end
700
- end
701
- end
702
- end
703
-
704
- describe 'redo' do
705
- [nil, :reload].each do |method|
706
- context (method || 'instance').to_s do
707
- before :each do
708
- comment.update_attributes!(title: 'Test5')
709
- end
710
-
711
- it 'should recognize :from, :to options' do
712
- comment.redo! user, from: 2, to: 4
713
- comment.send(method) if method
714
- expect(comment.title).to eq('Test4')
715
- end
716
-
717
- it 'should recognize parameter as version number' do
718
- comment.redo! user, 2
719
- comment.send(method) if method
720
- expect(comment.title).to eq('Test2')
721
- end
722
-
723
- it 'should redo last version when no parameter is specified' do
724
- comment.redo! user
725
- comment.send(method) if method
726
- expect(comment.title).to eq('Test5')
727
- end
728
-
729
- it 'should recognize :last options' do
730
- comment.redo! user, last: 1
731
- comment.send(method) if method
732
- expect(comment.title).to eq('Test5')
733
- end
734
-
735
- if Mongoid::Compatibility::Version.mongoid3?
736
- context 'protected attributes' do
737
- before :each do
738
- Comment.attr_accessible(nil)
739
- end
740
-
741
- after :each do
742
- Comment.attr_protected(nil)
743
- end
744
-
745
- it 'should recognize parameter as version number' do
746
- comment.redo! user, 2
747
- comment.send(method) if method
748
- expect(comment.title).to eq('Test2')
749
- end
750
-
751
- it 'should recognize :from, :to options' do
752
- comment.redo! user, from: 2, to: 4
753
- comment.send(method) if method
754
- expect(comment.title).to eq('Test4')
755
- end
756
- end
757
- end
758
- end
759
- end
760
- end
761
- end
762
-
763
- describe 'embedded with a polymorphic trackable' do
764
- let(:foo) { Foo.new(title: 'a title', body: 'a body', modifier: user) }
765
- before :each do
766
- post.comments << foo
767
- post.save!
768
- end
769
- it 'should assign interface name in association chain' do
770
- foo.update_attribute(:body, 'a changed body')
771
- expected_root = { 'name' => 'Post', 'id' => post.id }
772
- expected_node = { 'name' => 'coms', 'id' => foo.id }
773
- expect(foo.history_tracks.first.association_chain).to eq([expected_root, expected_node])
774
- end
775
- end
776
-
777
- describe '#trackable_parent_class' do
778
- context 'a non-embedded model' do
779
- it 'should return the trackable parent class' do
780
- expect(tag.history_tracks.first.trackable_parent_class).to eq(Tag)
781
- end
782
- it 'should return the parent class even if the trackable is deleted' do
783
- tracker = tag.history_tracks.first
784
- tag.destroy
785
- expect(tracker.trackable_parent_class).to eq(Tag)
786
- end
787
- end
788
- context 'an embedded model' do
789
- it 'should return the trackable parent class' do
790
- comment.update_attributes!(title: 'Foo')
791
- expect(comment.history_tracks.first.trackable_parent_class).to eq(Post)
792
- end
793
- it 'should return the parent class even if the trackable is deleted' do
794
- tracker = comment.history_tracks.first
795
- comment.destroy
796
- expect(tracker.trackable_parent_class).to eq(Post)
797
- end
798
- end
799
- end
800
-
801
- describe 'when default scope is present' do
802
- before :each do
803
- class Post
804
- default_scope -> { where(title: nil) }
805
- end
806
- class Comment
807
- default_scope -> { where(title: nil) }
808
- end
809
- class User
810
- default_scope -> { where(name: nil) }
811
- end
812
- class Tag
813
- default_scope -> { where(title: nil) }
814
- end
815
- end
816
-
817
- describe 'post' do
818
- it 'should correctly undo and redo' do
819
- post.update_attributes!(title: 'a new title')
820
- track = post.history_tracks.last
821
- track.undo! user
822
- expect(post.reload.title).to eq('Test')
823
- track.redo! user
824
- expect(post.reload.title).to eq('a new title')
825
- end
826
-
827
- it 'should stay the same after undo and redo' do
828
- post.update_attributes!(title: 'testing')
829
- track = post.history_tracks.last
830
- track.undo! user
831
- track.redo! user
832
- expect(post.reload.title).to eq('testing')
833
- end
834
- end
835
- describe 'comment' do
836
- it 'should correctly undo and redo' do
837
- comment.update_attributes!(title: 'a new title')
838
- track = comment.history_tracks.last
839
- track.undo! user
840
- expect(comment.reload.title).to eq('test')
841
- track.redo! user
842
- expect(comment.reload.title).to eq('a new title')
843
- end
844
-
845
- it 'should stay the same after undo and redo' do
846
- comment.update_attributes!(title: 'testing')
847
- track = comment.history_tracks.last
848
- track.undo! user
849
- track.redo! user
850
- expect(comment.reload.title).to eq('testing')
851
- end
852
- end
853
- describe 'user' do
854
- it 'should correctly undo and redo' do
855
- user.update_attributes!(name: 'a new name')
856
- track = user.history_tracks.last
857
- track.undo! user
858
- expect(user.reload.name).to eq('Aaron')
859
- track.redo! user
860
- expect(user.reload.name).to eq('a new name')
861
- end
862
-
863
- it 'should stay the same after undo and redo' do
864
- user.update_attributes!(name: 'testing')
865
- track = user.history_tracks.last
866
- track.undo! user
867
- track.redo! user
868
- expect(user.reload.name).to eq('testing')
869
- end
870
- end
871
- describe 'tag' do
872
- it 'should correctly undo and redo' do
873
- tag.update_attributes!(title: 'a new title')
874
- track = tag.history_tracks.last
875
- track.undo! user
876
- expect(tag.reload.title).to eq('test')
877
- track.redo! user
878
- expect(tag.reload.title).to eq('a new title')
879
- end
880
-
881
- it 'should stay the same after undo and redo' do
882
- tag.update_attributes!(title: 'testing')
883
- track = tag.history_tracks.last
884
- track.undo! user
885
- track.redo! user
886
- expect(tag.reload.title).to eq('testing')
887
- end
888
- end
889
- end
890
-
891
- describe 'overriden changes_method with additional fields' do
892
- before :each do
893
- class OverriddenChangesMethod
894
- include Mongoid::Document
895
- include Mongoid::History::Trackable
896
-
897
- track_history on: [:foo], changes_method: :my_changes
898
-
899
- def my_changes
900
- { foo: %w[bar baz] }
901
- end
902
- end
903
- end
904
-
905
- after :each do
906
- Object.send(:remove_const, :OverriddenChangesMethod)
907
- end
908
-
909
- it 'should add foo to the changes history' do
910
- o = OverriddenChangesMethod.create(modifier: user)
911
- o.save!
912
- track = o.history_tracks.last
913
- expect(track.modified).to eq('foo' => 'baz')
914
- expect(track.original).to eq('foo' => 'bar')
915
- end
916
- end
917
-
918
- describe 'localized fields' do
919
- before :each do
920
- class Sausage
921
- include Mongoid::Document
922
- include Mongoid::History::Trackable
923
-
924
- field :flavour, localize: true
925
- track_history on: [:flavour], track_destroy: true, modifier_field_optional: true
926
- end
927
- end
928
-
929
- after :each do
930
- Object.send(:remove_const, :Sausage)
931
- end
932
-
933
- it 'should correctly undo and redo' do
934
- pending unless Sausage.respond_to?(:localized_fields)
935
-
936
- sausage = Sausage.create!(flavour_translations: { 'en' => 'Apple', 'nl' => 'Appel' }, modifier: user)
937
- sausage.update_attributes!(flavour: 'Guinness')
938
-
939
- track = sausage.history_tracks.last
940
-
941
- track.undo! user
942
- expect(sausage.reload.flavour).to eq('Apple')
943
-
944
- track.redo! user
945
- expect(sausage.reload.flavour).to eq('Guinness')
946
-
947
- sausage.destroy
948
- expect(sausage.history_tracks.last.action).to eq('destroy')
949
- sausage.history_tracks.last.undo! user
950
- expect(sausage.reload.flavour).to eq('Guinness')
951
- end
952
- end
953
-
954
- describe 'changing collection' do
955
- before :each do
956
- class Fish
957
- include Mongoid::Document
958
- include Mongoid::History::Trackable
959
-
960
- track_history on: [:species], modifier_field_optional: true
961
- store_in collection: :animals
962
-
963
- field :species
964
- end
965
- end
966
-
967
- after :each do
968
- Object.send(:remove_const, :Fish)
969
- end
970
-
971
- it 'should track history' do
972
- Fish.new.save!
973
- end
974
- end
975
- end
976
- end
1
+ require 'spec_helper'
2
+
3
+ describe Mongoid::History do
4
+ before :each do
5
+ class Post
6
+ include Mongoid::Document
7
+ include Mongoid::Timestamps
8
+ include Mongoid::History::Trackable
9
+
10
+ field :title
11
+ field :body
12
+ field :rating
13
+ field :views, type: Integer
14
+
15
+ embeds_many :comments, store_as: :coms
16
+ embeds_one :section, store_as: :sec
17
+ embeds_many :tags, cascade_callbacks: true
18
+
19
+ accepts_nested_attributes_for :tags, allow_destroy: true
20
+
21
+ track_history on: %i[title body], track_destroy: true
22
+ end
23
+
24
+ class Comment
25
+ include Mongoid::Document
26
+ include Mongoid::Timestamps
27
+ include Mongoid::History::Trackable
28
+
29
+ field :t, as: :title
30
+ field :body
31
+ embedded_in :commentable, polymorphic: true
32
+ # BUG: see https://github.com/mongoid/mongoid-history/issues/223, modifier_field_optional should not be necessary
33
+ track_history on: %i[title body], scope: :post, track_create: true, track_destroy: true, modifier_field_optional: true
34
+ end
35
+
36
+ class Section
37
+ include Mongoid::Document
38
+ include Mongoid::Timestamps
39
+ include Mongoid::History::Trackable
40
+
41
+ field :t, as: :title
42
+ embedded_in :post
43
+ track_history on: [:title], scope: :post, track_create: true, track_destroy: true
44
+ end
45
+
46
+ class User
47
+ include Mongoid::Document
48
+ include Mongoid::Timestamps
49
+ include Mongoid::History::Trackable
50
+
51
+ field :n, as: :name
52
+ field :em, as: :email
53
+ field :phone
54
+ field :address
55
+ field :city
56
+ field :country
57
+ field :aliases, type: Array
58
+ track_history except: %i[email updated_at], modifier_field_optional: true
59
+ end
60
+
61
+ class Tag
62
+ include Mongoid::Document
63
+ # include Mongoid::Timestamps (see: https://github.com/mongoid/mongoid/issues/3078)
64
+ include Mongoid::History::Trackable
65
+
66
+ belongs_to :updated_by, class_name: 'User'
67
+
68
+ field :title
69
+ track_history on: [:title], scope: :post, track_create: true, track_destroy: true, modifier_field: :updated_by
70
+ end
71
+
72
+ class Foo < Comment
73
+ end
74
+ end
75
+
76
+ after :each do
77
+ Object.send(:remove_const, :Post)
78
+ Object.send(:remove_const, :Comment)
79
+ Object.send(:remove_const, :Section)
80
+ Object.send(:remove_const, :User)
81
+ Object.send(:remove_const, :Tag)
82
+ Object.send(:remove_const, :Foo)
83
+ end
84
+
85
+ let(:user) { User.create!(name: 'Aaron', email: 'aaron@randomemail.com', aliases: ['bob'], country: 'Canada', city: 'Toronto', address: '21 Jump Street') }
86
+ let(:another_user) { User.create!(name: 'Another Guy', email: 'anotherguy@randomemail.com') }
87
+ let(:post) { Post.create!(title: 'Test', body: 'Post', modifier: user, views: 100) }
88
+ let(:comment) { post.comments.create!(title: 'test', body: 'comment', modifier: user) }
89
+ let(:tag) { Tag.create!(title: 'test', updated_by: user) }
90
+
91
+ describe 'track' do
92
+ describe 'on creation' do
93
+ it 'should have one history track in comment' do
94
+ expect(comment.history_tracks.count).to eq(1)
95
+ end
96
+
97
+ it 'should assign title and body on modified' do
98
+ expect(comment.history_tracks.first.modified).to eq('t' => 'test', 'body' => 'comment')
99
+ end
100
+
101
+ it 'should not assign title and body on original' do
102
+ expect(comment.history_tracks.first.original).to eq({})
103
+ end
104
+
105
+ it 'should assign modifier' do
106
+ expect(comment.history_tracks.first.modifier.id).to eq(user.id)
107
+ end
108
+
109
+ it 'should assign version' do
110
+ expect(comment.history_tracks.first.version).to eq(1)
111
+ end
112
+
113
+ it 'should assign scope' do
114
+ expect(comment.history_tracks.first.scope).to eq('post')
115
+ end
116
+
117
+ it 'should assign method' do
118
+ expect(comment.history_tracks.first.action).to eq('create')
119
+ end
120
+
121
+ it 'should assign association_chain' do
122
+ expected = [
123
+ { 'id' => post.id, 'name' => 'Post' },
124
+ { 'id' => comment.id, 'name' => 'coms' }
125
+ ]
126
+ expect(comment.history_tracks.first.association_chain).to eq(expected)
127
+ end
128
+ end
129
+
130
+ describe 'on destruction' do
131
+ it 'should have two history track records in post' do
132
+ post # This will create history track records for creation
133
+ expect do
134
+ post.destroy
135
+ end.to change(Tracker, :count).by(1)
136
+ end
137
+
138
+ it 'should assign destroy on track record' do
139
+ post.destroy
140
+ expect(post.history_tracks.last.action).to eq('destroy')
141
+ end
142
+
143
+ it 'should return affected attributes from track record' do
144
+ post.destroy
145
+ expect(post.history_tracks.last.affected['title']).to eq('Test')
146
+ end
147
+
148
+ it 'should no-op on repeated calls to destroy' do
149
+ post.destroy
150
+ expect do
151
+ post.destroy
152
+ end.not_to change(Tracker, :count)
153
+ end
154
+ end
155
+
156
+ describe 'on update non-embedded' do
157
+ it 'should create a history track if changed attributes match tracked attributes' do
158
+ post # This will create history track records for creation
159
+ expect do
160
+ post.update_attributes!(title: 'Another Test')
161
+ end.to change(Tracker, :count).by(1)
162
+ end
163
+
164
+ it 'should not create a history track if changed attributes do not match tracked attributes' do
165
+ post # This will create history track records for creation
166
+ expect do
167
+ post.update_attributes!(rating: 'untracked')
168
+ end.to change(Tracker, :count).by(0)
169
+ end
170
+
171
+ it 'should assign modified fields' do
172
+ post.update_attributes!(title: 'Another Test')
173
+ expect(post.history_tracks.last.modified).to eq(
174
+ 'title' => 'Another Test'
175
+ )
176
+ end
177
+
178
+ it 'should assign method field' do
179
+ post.update_attributes!(title: 'Another Test')
180
+ expect(post.history_tracks.last.action).to eq('update')
181
+ end
182
+
183
+ it 'should assign original fields' do
184
+ post.update_attributes!(title: 'Another Test')
185
+ expect(post.history_tracks.last.original).to eq(
186
+ 'title' => 'Test'
187
+ )
188
+ end
189
+
190
+ it 'should assign modifier' do
191
+ post.update_attributes!(title: 'Another Test')
192
+ expect(post.history_tracks.first.modifier.id).to eq(user.id)
193
+ end
194
+
195
+ it 'should assign version on history tracks' do
196
+ post.update_attributes!(title: 'Another Test')
197
+ expect(post.history_tracks.first.version).to eq(1)
198
+ end
199
+
200
+ it 'should assign version on post' do
201
+ expect(post.version).to eq(1) # Created
202
+ post.update_attributes!(title: 'Another Test')
203
+ expect(post.version).to eq(2) # Updated
204
+ end
205
+
206
+ it 'should assign scope' do
207
+ post.update_attributes!(title: 'Another Test')
208
+ expect(post.history_tracks.first.scope).to eq('post')
209
+ end
210
+
211
+ it 'should assign association_chain' do
212
+ post.update_attributes!(title: 'Another Test')
213
+ expect(post.history_tracks.last.association_chain).to eq([{ 'id' => post.id, 'name' => 'Post' }])
214
+ end
215
+
216
+ it 'should exclude defined options' do
217
+ name = user.name
218
+ user.update_attributes!(name: 'Aaron2', email: 'aaronsnewemail@randomemail.com')
219
+ expect(user.history_tracks.last.original.keys).to eq(['n'])
220
+ expect(user.history_tracks.last.original['n']).to eq(name)
221
+ expect(user.history_tracks.last.modified.keys).to eq(['n'])
222
+ expect(user.history_tracks.last.modified['n']).to eq(user.name)
223
+ end
224
+
225
+ it 'should undo field changes' do
226
+ name = user.name
227
+ user.update_attributes!(name: 'Aaron2', email: 'aaronsnewemail@randomemail.com')
228
+ user.history_tracks.last.undo! nil
229
+ expect(user.reload.name).to eq(name)
230
+ end
231
+
232
+ it 'should undo non-existing field changes' do
233
+ post = Post.create!(modifier: user, views: 100)
234
+ expect(post.reload.title).to be_nil
235
+ post.update_attributes!(title: 'Aaron2')
236
+ expect(post.reload.title).to eq('Aaron2')
237
+ post.history_tracks.last.undo! user
238
+ expect(post.reload.title).to be_nil
239
+ end
240
+
241
+ it 'should track array changes' do
242
+ aliases = user.aliases
243
+ user.update_attributes!(aliases: %w[bob joe])
244
+ expect(user.history_tracks.last.original['aliases']).to eq(aliases)
245
+ expect(user.history_tracks.last.modified['aliases']).to eq(user.aliases)
246
+ end
247
+
248
+ it 'should undo array changes' do
249
+ aliases = user.aliases
250
+ user.update_attributes!(aliases: %w[bob joe])
251
+ user.history_tracks.last.undo! nil
252
+ expect(user.reload.aliases).to eq(aliases)
253
+ end
254
+ end
255
+
256
+ describe '#tracked_changes' do
257
+ context 'create action' do
258
+ subject { tag.history_tracks.first.tracked_changes }
259
+ it 'consider all fields values as :to' do
260
+ expect(subject[:title]).to eq({ to: 'test' }.with_indifferent_access)
261
+ end
262
+ end
263
+ context 'destroy action' do
264
+ subject do
265
+ tag.destroy
266
+ tag.history_tracks.last.tracked_changes
267
+ end
268
+ it 'consider all fields values as :from' do
269
+ expect(subject[:title]).to eq({ from: 'test' }.with_indifferent_access)
270
+ end
271
+ end
272
+ context 'update action' do
273
+ subject { user.history_tracks.last.tracked_changes }
274
+ before do
275
+ user.update_attributes!(name: 'Aaron2', email: nil, country: '', city: nil, phone: '867-5309', aliases: ['', 'bill', 'james'])
276
+ end
277
+ it { is_expected.to be_a HashWithIndifferentAccess }
278
+ it 'should track changed field' do
279
+ expect(subject[:n]).to eq({ from: 'Aaron', to: 'Aaron2' }.with_indifferent_access)
280
+ end
281
+ it 'should track added field' do
282
+ expect(subject[:phone]).to eq({ to: '867-5309' }.with_indifferent_access)
283
+ end
284
+ it 'should track removed field' do
285
+ expect(subject[:city]).to eq({ from: 'Toronto' }.with_indifferent_access)
286
+ end
287
+ it 'should not consider blank as removed' do
288
+ expect(subject[:country]).to eq({ from: 'Canada', to: '' }.with_indifferent_access)
289
+ end
290
+ it 'should track changed array field' do
291
+ expect(subject[:aliases]).to eq({ from: ['bob'], to: ['', 'bill', 'james'] }.with_indifferent_access)
292
+ end
293
+ it 'should not track unmodified field' do
294
+ expect(subject[:address]).to be_nil
295
+ end
296
+ it 'should not track untracked fields' do
297
+ expect(subject[:email]).to be_nil
298
+ end
299
+ end
300
+ end
301
+
302
+ describe '#tracked_edits' do
303
+ context 'create action' do
304
+ subject { tag.history_tracks.first.tracked_edits }
305
+ it 'consider all edits as ;add' do
306
+ expect(subject[:add]).to eq({ title: 'test' }.with_indifferent_access)
307
+ end
308
+ end
309
+ context 'destroy action' do
310
+ subject do
311
+ tag.destroy
312
+ tag.history_tracks.last.tracked_edits
313
+ end
314
+ it 'consider all edits as ;remove' do
315
+ expect(subject[:remove]).to eq({ title: 'test' }.with_indifferent_access)
316
+ end
317
+ end
318
+ context 'update action' do
319
+ subject { user.history_tracks.last.tracked_edits }
320
+ before do
321
+ user.update_attributes!(name: 'Aaron2', email: nil, country: '', city: nil, phone: '867-5309', aliases: ['', 'bill', 'james'])
322
+ end
323
+ it { is_expected.to be_a HashWithIndifferentAccess }
324
+ it 'should track changed field' do
325
+ expect(subject[:modify]).to eq({ n: { from: 'Aaron', to: 'Aaron2' } }.with_indifferent_access)
326
+ end
327
+ it 'should track added field' do
328
+ expect(subject[:add]).to eq({ phone: '867-5309' }.with_indifferent_access)
329
+ end
330
+ it 'should track removed field and consider blank as removed' do
331
+ expect(subject[:remove]).to eq({ city: 'Toronto', country: 'Canada' }.with_indifferent_access)
332
+ end
333
+ it 'should track changed array field' do
334
+ expect(subject[:array]).to eq({ aliases: { remove: ['bob'], add: ['', 'bill', 'james'] } }.with_indifferent_access)
335
+ end
336
+ it 'should not track unmodified field' do
337
+ %w[add modify remove array].each do |edit|
338
+ expect(subject[edit][:address]).to be_nil
339
+ end
340
+ end
341
+ it 'should not track untracked fields' do
342
+ %w[add modify remove array].each do |edit|
343
+ expect(subject[edit][:email]).to be_nil
344
+ end
345
+ end
346
+ end
347
+ context 'with empty values' do
348
+ before do
349
+ allow(subject).to receive(:trackable_parent_class) { Tracker }
350
+ allow(Tracker).to receive(:tracked_embeds_many?) { false }
351
+ end
352
+ subject { Tracker.new }
353
+ it 'should skip empty values' do
354
+ allow(subject).to receive(:tracked_changes) { { name: { to: '', from: [] }, city: { to: 'Toronto', from: '' } } }
355
+ expect(subject.tracked_edits).to eq({ add: { city: 'Toronto' } }.with_indifferent_access)
356
+ end
357
+ end
358
+ end
359
+
360
+ describe 'on update non-embedded twice' do
361
+ it 'should assign version on post' do
362
+ expect(post.version).to eq(1)
363
+ post.update_attributes!(title: 'Test2')
364
+ post.update_attributes!(title: 'Test3')
365
+ expect(post.version).to eq(3)
366
+ end
367
+
368
+ it 'should create a history track if changed attributes match tracked attributes' do
369
+ post # Created
370
+ expect do
371
+ post.update_attributes!(title: 'Test2')
372
+ post.update_attributes!(title: 'Test3')
373
+ end.to change(Tracker, :count).by(2)
374
+ end
375
+
376
+ it 'should create a history track of version 2' do
377
+ post.update_attributes!(title: 'Test2')
378
+ post.update_attributes!(title: 'Test3')
379
+ expect(post.history_tracks.where(version: 2).first).not_to be_nil
380
+ end
381
+
382
+ it 'should assign modified fields' do
383
+ post.update_attributes!(title: 'Test2')
384
+ post.update_attributes!(title: 'Test3')
385
+ expect(post.history_tracks.where(version: 3).first.modified).to eq(
386
+ 'title' => 'Test3'
387
+ )
388
+ end
389
+
390
+ it 'should assign original fields' do
391
+ post.update_attributes!(title: 'Test2')
392
+ post.update_attributes!(title: 'Test3')
393
+ expect(post.history_tracks.where(version: 3).first.original).to eq(
394
+ 'title' => 'Test2'
395
+ )
396
+ end
397
+
398
+ it 'should assign modifier' do
399
+ post.update_attributes!(title: 'Another Test', modifier: another_user)
400
+ expect(post.history_tracks.last.modifier.id).to eq(another_user.id)
401
+ end
402
+ end
403
+
404
+ describe 'on update embedded 1..N (embeds_many)' do
405
+ it 'should assign version on comment' do
406
+ comment.update_attributes!(title: 'Test2')
407
+ expect(comment.version).to eq(2) # first track generated on creation
408
+ end
409
+
410
+ it 'should create a history track of version 2' do
411
+ comment.update_attributes!(title: 'Test2')
412
+ expect(comment.history_tracks.where(version: 2).first).not_to be_nil
413
+ end
414
+
415
+ it 'should assign modified fields' do
416
+ comment.update_attributes!(t: 'Test2')
417
+ expect(comment.history_tracks.where(version: 2).first.modified).to eq(
418
+ 't' => 'Test2'
419
+ )
420
+ end
421
+
422
+ it 'should assign original fields' do
423
+ comment.update_attributes!(title: 'Test2')
424
+ expect(comment.history_tracks.where(version: 2).first.original).to eq(
425
+ 't' => 'test'
426
+ )
427
+ end
428
+
429
+ it 'should be possible to undo from parent' do
430
+ comment.update_attributes!(title: 'Test 2')
431
+ user
432
+ post.history_tracks.last.undo!(user)
433
+ comment.reload
434
+ expect(comment.title).to eq('test')
435
+ end
436
+
437
+ it 'should assign modifier' do
438
+ post.update_attributes!(title: 'Another Test', modifier: another_user)
439
+ expect(post.history_tracks.last.modifier.id).to eq(another_user.id)
440
+ end
441
+ end
442
+
443
+ describe 'on update embedded 1..1 (embeds_one)' do
444
+ let(:section) { Section.new(title: 'Technology', modifier: user) }
445
+
446
+ before(:each) do
447
+ post.section = section
448
+ post.modifier = user
449
+ post.save!
450
+ post.reload
451
+ post.section
452
+ end
453
+
454
+ it 'should assign version on create section' do
455
+ expect(section.version).to eq(1)
456
+ end
457
+
458
+ it 'should assign version on section' do
459
+ section.update_attributes!(title: 'Technology 2')
460
+ expect(section.version).to eq(2) # first track generated on creation
461
+ end
462
+
463
+ it 'should create a history track of version 2' do
464
+ section.update_attributes!(title: 'Technology 2')
465
+ expect(section.history_tracks.where(version: 2).first).not_to be_nil
466
+ end
467
+
468
+ it 'should assign modified fields' do
469
+ section.update_attributes!(title: 'Technology 2')
470
+ expect(section.history_tracks.where(version: 2).first.modified).to eq(
471
+ 't' => 'Technology 2'
472
+ )
473
+ end
474
+
475
+ it 'should assign original fields' do
476
+ section.update_attributes!(title: 'Technology 2')
477
+ expect(section.history_tracks.where(version: 2).first.original).to eq(
478
+ 't' => 'Technology'
479
+ )
480
+ end
481
+
482
+ it 'should be possible to undo from parent' do
483
+ section.update_attributes!(title: 'Technology 2')
484
+ post.history_tracks.last.undo!(user)
485
+ section.reload
486
+ expect(section.title).to eq('Technology')
487
+ end
488
+
489
+ it 'should assign modifier' do
490
+ section.update_attributes!(title: 'Business', modifier: another_user)
491
+ expect(post.history_tracks.last.modifier.id).to eq(another_user.id)
492
+ end
493
+ end
494
+
495
+ describe 'on destroy embedded' do
496
+ it 'should be possible to re-create destroyed embedded' do
497
+ comment.destroy
498
+ comment.history_tracks.last.undo!(user)
499
+ post.reload
500
+ expect(post.comments.first.title).to eq('test')
501
+ end
502
+
503
+ it 'should be possible to re-create destroyed embedded from parent' do
504
+ comment.destroy
505
+ post.history_tracks.last.undo!(user)
506
+ post.reload
507
+ expect(post.comments.first.title).to eq('test')
508
+ end
509
+
510
+ it 'should be possible to destroy after re-create embedded from parent' do
511
+ comment.destroy
512
+ post.history_tracks[-1].undo!(user)
513
+ post.history_tracks[-1].undo!(user)
514
+ post.reload
515
+ expect(post.comments.count).to eq(0)
516
+ end
517
+
518
+ it 'should be possible to create with redo after undo create embedded from parent' do
519
+ comment # initialize
520
+ post.comments.create!(title: 'The second one', modifier: user)
521
+ track = post.history_tracks[2]
522
+ expect(post.reload.comments.count).to eq 2
523
+ track.undo!(user)
524
+ expect(post.reload.comments.count).to eq 1
525
+ track.redo!(user)
526
+ expect(post.reload.comments.count).to eq 2
527
+ end
528
+ end
529
+
530
+ describe 'embedded with cascading callbacks' do
531
+ let(:tag_foo) { post.tags.create!(title: 'foo', updated_by: user) }
532
+ let(:tag_bar) { post.tags.create!(title: 'bar', updated_by: user) }
533
+
534
+ it 'should allow an update through the parent model' do
535
+ update_hash = { 'post' => { 'tags_attributes' => { '1234' => { 'id' => tag_bar.id, 'title' => 'baz' } } } }
536
+ post.update_attributes!(update_hash['post'])
537
+ expect(post.tags.last.title).to eq('baz')
538
+ end
539
+
540
+ it 'should be possible to destroy through parent model using canoncial _destroy macro' do
541
+ tag_foo
542
+ tag_bar # initialize
543
+ expect(post.tags.count).to eq(2)
544
+ update_hash = { 'post' => { 'tags_attributes' => { '1234' => { 'id' => tag_bar.id, 'title' => 'baz', '_destroy' => 'true' } } } }
545
+ post.update_attributes!(update_hash['post'])
546
+ expect(post.tags.count).to eq(1)
547
+ expect(post.history_tracks.to_a.last.action).to eq('destroy')
548
+ end
549
+
550
+ it 'should write relationship name for association_chain hiearchy instead of class name when using _destroy macro' do
551
+ update_hash = { 'tags_attributes' => { '1234' => { 'id' => tag_foo.id, '_destroy' => '1' } } }
552
+ post.update_attributes!(update_hash)
553
+
554
+ # historically this would have evaluated to 'Tags' and an error would be thrown
555
+ # on any call that walked up the association_chain, e.g. 'trackable'
556
+ expect(tag_foo.history_tracks.last.association_chain.last['name']).to eq('tags')
557
+ expect { tag_foo.history_tracks.last.trackable }.not_to raise_error
558
+ end
559
+ end
560
+
561
+ describe 'non-embedded' do
562
+ it 'should undo changes' do
563
+ post.update_attributes!(title: 'Test2')
564
+ post.history_tracks.where(version: 2).last.undo!(user)
565
+ post.reload
566
+ expect(post.title).to eq('Test')
567
+ end
568
+
569
+ it 'should undo destruction' do
570
+ post.destroy
571
+ post.history_tracks.where(version: 2).last.undo!(user)
572
+ expect(Post.find(post.id).title).to eq('Test')
573
+ end
574
+
575
+ it 'should create a new history track after undo' do
576
+ comment # initialize
577
+ post.update_attributes!(title: 'Test2')
578
+ post.history_tracks.last.undo!(user)
579
+ post.reload
580
+ expect(post.history_tracks.count).to eq(4)
581
+ end
582
+
583
+ it 'should assign user as the modifier of the newly created history track' do
584
+ post.update_attributes!(title: 'Test2')
585
+ post.history_tracks.where(version: 2).last.undo!(user)
586
+ post.reload
587
+ expect(post.history_tracks.where(version: 2).last.modifier.id).to eq(user.id)
588
+ end
589
+
590
+ it 'should stay the same after undo and redo' do
591
+ post.update_attributes!(title: 'Test2')
592
+ track = post.history_tracks.last
593
+ track.undo!(user)
594
+ track.redo!(user)
595
+ post2 = Post.where(_id: post.id).first
596
+
597
+ expect(post.title).to eq(post2.title)
598
+ end
599
+
600
+ it 'should be destroyed after undo and redo' do
601
+ post.destroy
602
+ track = post.history_tracks.where(version: 2).last
603
+ track.undo!(user)
604
+ track.redo!(user)
605
+ expect(Post.where(_id: post.id).first).to be_nil
606
+ end
607
+ end
608
+
609
+ describe 'embedded' do
610
+ it 'should undo changes' do
611
+ comment.update_attributes!(title: 'Test2')
612
+ comment.history_tracks.where(version: 2).first.undo!(user)
613
+ comment.reload
614
+ expect(comment.title).to eq('test')
615
+ end
616
+
617
+ it 'should create a new history track after undo' do
618
+ comment.update_attributes!(title: 'Test2')
619
+ comment.history_tracks.where(version: 2).first.undo!(user)
620
+ comment.reload
621
+ expect(comment.history_tracks.count).to eq(3)
622
+ end
623
+
624
+ it 'should assign user as the modifier of the newly created history track' do
625
+ comment.update_attributes!(title: 'Test2')
626
+ comment.history_tracks.where(version: 2).first.undo!(user)
627
+ comment.reload
628
+ expect(comment.history_tracks.where(version: 3).first.modifier.id).to eq(user.id)
629
+ end
630
+
631
+ it 'should stay the same after undo and redo' do
632
+ comment.update_attributes!(title: 'Test2')
633
+ track = comment.history_tracks.where(version: 2).first
634
+ track.undo!(user)
635
+ track.redo!(user)
636
+ comment.reload
637
+ expect(comment.title).to eq('Test2')
638
+ end
639
+ end
640
+
641
+ describe 'trackables' do
642
+ before :each do
643
+ comment.update_attributes!(title: 'Test2') # version == 2
644
+ comment.update_attributes!(title: 'Test3') # version == 3
645
+ comment.update_attributes!(title: 'Test4') # version == 4
646
+ end
647
+
648
+ describe 'undo' do
649
+ { 'undo' => [nil], 'undo!' => [nil, :reload] }.each do |test_method, methods|
650
+ methods.each do |method|
651
+ context (method || 'instance').to_s do
652
+ it 'recognizes :from, :to options' do
653
+ comment.send test_method, user, from: 4, to: 2
654
+ comment.send(method) if method
655
+ expect(comment.title).to eq('test')
656
+ end
657
+
658
+ it 'recognizes parameter as version number' do
659
+ comment.send test_method, user, 3
660
+ comment.send(method) if method
661
+ expect(comment.title).to eq('Test2')
662
+ end
663
+
664
+ it 'should undo last version when no parameter is specified' do
665
+ comment.send test_method, user
666
+ comment.send(method) if method
667
+ expect(comment.title).to eq('Test3')
668
+ end
669
+
670
+ it 'recognizes :last options' do
671
+ comment.send test_method, user, last: 2
672
+ comment.send(method) if method
673
+ expect(comment.title).to eq('Test2')
674
+ end
675
+
676
+ if Mongoid::Compatibility::Version.mongoid3?
677
+ context 'protected attributes' do
678
+ before :each do
679
+ Comment.attr_accessible(nil)
680
+ end
681
+
682
+ after :each do
683
+ Comment.attr_protected(nil)
684
+ end
685
+
686
+ it 'should undo last version when no parameter is specified on protected attributes' do
687
+ comment.send test_method, user
688
+ comment.send(method) if method
689
+ expect(comment.title).to eq('Test3')
690
+ end
691
+
692
+ it 'recognizes :last options on model with protected attributes' do
693
+ comment.send test_method, user, last: 2
694
+ comment.send(method) if method
695
+ expect(comment.title).to eq('Test2')
696
+ end
697
+ end
698
+ end
699
+ end
700
+ end
701
+ end
702
+ end
703
+
704
+ describe 'redo' do
705
+ [nil, :reload].each do |method|
706
+ context (method || 'instance').to_s do
707
+ before :each do
708
+ comment.update_attributes!(title: 'Test5')
709
+ end
710
+
711
+ it 'should recognize :from, :to options' do
712
+ comment.redo! user, from: 2, to: 4
713
+ comment.send(method) if method
714
+ expect(comment.title).to eq('Test4')
715
+ end
716
+
717
+ it 'should recognize parameter as version number' do
718
+ comment.redo! user, 2
719
+ comment.send(method) if method
720
+ expect(comment.title).to eq('Test2')
721
+ end
722
+
723
+ it 'should redo last version when no parameter is specified' do
724
+ comment.redo! user
725
+ comment.send(method) if method
726
+ expect(comment.title).to eq('Test5')
727
+ end
728
+
729
+ it 'should recognize :last options' do
730
+ comment.redo! user, last: 1
731
+ comment.send(method) if method
732
+ expect(comment.title).to eq('Test5')
733
+ end
734
+
735
+ if Mongoid::Compatibility::Version.mongoid3?
736
+ context 'protected attributes' do
737
+ before :each do
738
+ Comment.attr_accessible(nil)
739
+ end
740
+
741
+ after :each do
742
+ Comment.attr_protected(nil)
743
+ end
744
+
745
+ it 'should recognize parameter as version number' do
746
+ comment.redo! user, 2
747
+ comment.send(method) if method
748
+ expect(comment.title).to eq('Test2')
749
+ end
750
+
751
+ it 'should recognize :from, :to options' do
752
+ comment.redo! user, from: 2, to: 4
753
+ comment.send(method) if method
754
+ expect(comment.title).to eq('Test4')
755
+ end
756
+ end
757
+ end
758
+ end
759
+ end
760
+ end
761
+ end
762
+
763
+ describe 'embedded with a polymorphic trackable' do
764
+ let(:foo) { Foo.new(title: 'a title', body: 'a body', modifier: user) }
765
+ before :each do
766
+ post.comments << foo
767
+ post.save!
768
+ end
769
+ it 'should assign interface name in association chain' do
770
+ foo.update_attribute(:body, 'a changed body')
771
+ expected_root = { 'name' => 'Post', 'id' => post.id }
772
+ expected_node = { 'name' => 'coms', 'id' => foo.id }
773
+ expect(foo.history_tracks.first.association_chain).to eq([expected_root, expected_node])
774
+ end
775
+ end
776
+
777
+ describe '#trackable_parent_class' do
778
+ context 'a non-embedded model' do
779
+ it 'should return the trackable parent class' do
780
+ expect(tag.history_tracks.first.trackable_parent_class).to eq(Tag)
781
+ end
782
+ it 'should return the parent class even if the trackable is deleted' do
783
+ tracker = tag.history_tracks.first
784
+ tag.destroy
785
+ expect(tracker.trackable_parent_class).to eq(Tag)
786
+ end
787
+ end
788
+ context 'an embedded model' do
789
+ it 'should return the trackable parent class' do
790
+ comment.update_attributes!(title: 'Foo')
791
+ expect(comment.history_tracks.first.trackable_parent_class).to eq(Post)
792
+ end
793
+ it 'should return the parent class even if the trackable is deleted' do
794
+ tracker = comment.history_tracks.first
795
+ comment.destroy
796
+ expect(tracker.trackable_parent_class).to eq(Post)
797
+ end
798
+ end
799
+ end
800
+
801
+ describe 'when default scope is present' do
802
+ before :each do
803
+ class Post
804
+ default_scope -> { where(title: nil) }
805
+ end
806
+ class Comment
807
+ default_scope -> { where(title: nil) }
808
+ end
809
+ class User
810
+ default_scope -> { where(name: nil) }
811
+ end
812
+ class Tag
813
+ default_scope -> { where(title: nil) }
814
+ end
815
+ end
816
+
817
+ describe 'post' do
818
+ it 'should correctly undo and redo' do
819
+ post.update_attributes!(title: 'a new title')
820
+ track = post.history_tracks.last
821
+ track.undo! user
822
+ expect(post.reload.title).to eq('Test')
823
+ track.redo! user
824
+ expect(post.reload.title).to eq('a new title')
825
+ end
826
+
827
+ it 'should stay the same after undo and redo' do
828
+ post.update_attributes!(title: 'testing')
829
+ track = post.history_tracks.last
830
+ track.undo! user
831
+ track.redo! user
832
+ expect(post.reload.title).to eq('testing')
833
+ end
834
+ end
835
+ describe 'comment' do
836
+ it 'should correctly undo and redo' do
837
+ comment.update_attributes!(title: 'a new title')
838
+ track = comment.history_tracks.last
839
+ track.undo! user
840
+ expect(comment.reload.title).to eq('test')
841
+ track.redo! user
842
+ expect(comment.reload.title).to eq('a new title')
843
+ end
844
+
845
+ it 'should stay the same after undo and redo' do
846
+ comment.update_attributes!(title: 'testing')
847
+ track = comment.history_tracks.last
848
+ track.undo! user
849
+ track.redo! user
850
+ expect(comment.reload.title).to eq('testing')
851
+ end
852
+ end
853
+ describe 'user' do
854
+ it 'should correctly undo and redo' do
855
+ user.update_attributes!(name: 'a new name')
856
+ track = user.history_tracks.last
857
+ track.undo! user
858
+ expect(user.reload.name).to eq('Aaron')
859
+ track.redo! user
860
+ expect(user.reload.name).to eq('a new name')
861
+ end
862
+
863
+ it 'should stay the same after undo and redo' do
864
+ user.update_attributes!(name: 'testing')
865
+ track = user.history_tracks.last
866
+ track.undo! user
867
+ track.redo! user
868
+ expect(user.reload.name).to eq('testing')
869
+ end
870
+ end
871
+ describe 'tag' do
872
+ it 'should correctly undo and redo' do
873
+ tag.update_attributes!(title: 'a new title')
874
+ track = tag.history_tracks.last
875
+ track.undo! user
876
+ expect(tag.reload.title).to eq('test')
877
+ track.redo! user
878
+ expect(tag.reload.title).to eq('a new title')
879
+ end
880
+
881
+ it 'should stay the same after undo and redo' do
882
+ tag.update_attributes!(title: 'testing')
883
+ track = tag.history_tracks.last
884
+ track.undo! user
885
+ track.redo! user
886
+ expect(tag.reload.title).to eq('testing')
887
+ end
888
+ end
889
+ end
890
+
891
+ describe 'overriden changes_method with additional fields' do
892
+ before :each do
893
+ class OverriddenChangesMethod
894
+ include Mongoid::Document
895
+ include Mongoid::History::Trackable
896
+
897
+ track_history on: [:foo], changes_method: :my_changes
898
+
899
+ def my_changes
900
+ { foo: %w[bar baz] }
901
+ end
902
+ end
903
+ end
904
+
905
+ after :each do
906
+ Object.send(:remove_const, :OverriddenChangesMethod)
907
+ end
908
+
909
+ it 'should add foo to the changes history' do
910
+ o = OverriddenChangesMethod.create(modifier: user)
911
+ o.save!
912
+ track = o.history_tracks.last
913
+ expect(track.modified).to eq('foo' => 'baz')
914
+ expect(track.original).to eq('foo' => 'bar')
915
+ end
916
+ end
917
+
918
+ describe 'localized fields' do
919
+ before :each do
920
+ class Sausage
921
+ include Mongoid::Document
922
+ include Mongoid::History::Trackable
923
+
924
+ field :flavour, localize: true
925
+ track_history on: [:flavour], track_destroy: true, modifier_field_optional: true
926
+ end
927
+ end
928
+
929
+ after :each do
930
+ Object.send(:remove_const, :Sausage)
931
+ end
932
+
933
+ it 'should correctly undo and redo' do
934
+ pending unless Sausage.respond_to?(:localized_fields)
935
+
936
+ sausage = Sausage.create!(flavour_translations: { 'en' => 'Apple', 'nl' => 'Appel' }, modifier: user)
937
+ sausage.update_attributes!(flavour: 'Guinness')
938
+
939
+ track = sausage.history_tracks.last
940
+
941
+ track.undo! user
942
+ expect(sausage.reload.flavour).to eq('Apple')
943
+
944
+ track.redo! user
945
+ expect(sausage.reload.flavour).to eq('Guinness')
946
+
947
+ sausage.destroy
948
+ expect(sausage.history_tracks.last.action).to eq('destroy')
949
+ sausage.history_tracks.last.undo! user
950
+ expect(sausage.reload.flavour).to eq('Guinness')
951
+ end
952
+ end
953
+
954
+ describe 'changing collection' do
955
+ before :each do
956
+ class Fish
957
+ include Mongoid::Document
958
+ include Mongoid::History::Trackable
959
+
960
+ track_history on: [:species], modifier_field_optional: true
961
+ store_in collection: :animals
962
+
963
+ field :species
964
+ end
965
+ end
966
+
967
+ after :each do
968
+ Object.send(:remove_const, :Fish)
969
+ end
970
+
971
+ it 'should track history' do
972
+ Fish.new.save!
973
+ end
974
+ end
975
+ end
976
+ end