Skip to content

Commit 71b9062

Browse files
committed
Added support for has_and_belongs_to_many association tracking.
1 parent c2ff8d2 commit 71b9062

File tree

8 files changed

+209
-79
lines changed

8 files changed

+209
-79
lines changed

.rubocop_todo.yml

+28-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# This configuration was generated by
22
# `rubocop --auto-gen-config`
3-
# on 2018-01-16 15:37:33 -0500 using RuboCop version 0.48.1.
3+
# on 2018-01-17 21:47:30 -0500 using RuboCop version 0.48.1.
44
# The point is for the user to remove these configuration records
55
# one by one as the offenses are removed from the code base.
66
# Note that changes in the inspected code, or installation of new
@@ -31,9 +31,9 @@ Lint/ParenthesesAsGroupedExpression:
3131
Exclude:
3232
- 'spec/integration/integration_spec.rb'
3333

34-
# Offense count: 21
34+
# Offense count: 22
3535
Metrics/AbcSize:
36-
Max: 45
36+
Max: 62
3737

3838
# Offense count: 114
3939
# Configuration parameters: CountComments, ExcludedMethods.
@@ -43,29 +43,29 @@ Metrics/BlockLength:
4343
# Offense count: 1
4444
# Configuration parameters: CountComments.
4545
Metrics/ClassLength:
46-
Max: 114
46+
Max: 123
4747

48-
# Offense count: 5
48+
# Offense count: 6
4949
Metrics/CyclomaticComplexity:
5050
Max: 10
5151

52-
# Offense count: 457
52+
# Offense count: 461
5353
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
5454
# URISchemes: http, https
5555
Metrics/LineLength:
5656
Max: 688
5757

58-
# Offense count: 15
58+
# Offense count: 16
5959
# Configuration parameters: CountComments.
6060
Metrics/MethodLength:
61-
Max: 23
61+
Max: 25
6262

6363
# Offense count: 2
6464
# Configuration parameters: CountComments.
6565
Metrics/ModuleLength:
66-
Max: 180
66+
Max: 200
6767

68-
# Offense count: 5
68+
# Offense count: 6
6969
Metrics/PerceivedComplexity:
7070
Max: 12
7171

@@ -107,3 +107,21 @@ Style/IfInsideElse:
107107
Style/MultilineBlockChain:
108108
Exclude:
109109
- 'lib/mongoid/history/tracker.rb'
110+
111+
# Offense count: 1
112+
# Cop supports --auto-correct.
113+
# Configuration parameters: EnforcedStyle, SupportedStyles.
114+
# SupportedStyles: symmetrical, new_line, same_line
115+
Style/MultilineMethodCallBraceLayout:
116+
Exclude:
117+
- 'spec/unit/options_spec.rb'
118+
119+
# Offense count: 2
120+
# Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist.
121+
# NamePrefix: is_, has_, have_
122+
# NamePrefixBlacklist: is_, has_, have_
123+
# NameWhitelist: is_a?
124+
Style/PredicateName:
125+
Exclude:
126+
- 'spec/**/*'
127+
- 'lib/mongoid/history/trackable.rb'

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
### 0.8.1 (Next)
22

3+
* [#217](https://github.com/mongoid/mongoid-history/pull/217): Support for tracking `has_and_belongs_to_many` associations - [@dblock](https://github.com/dblock).
34
* Your contribution here.
45

56
### 0.8.0 (2018/01/16)

README.md

+6-3
Original file line numberDiff line numberDiff line change
@@ -168,11 +168,11 @@ class Post
168168
field :body
169169
field :rating
170170

171-
track_history :on => [:fields] # all fields will be tracked
171+
track_history :on => [ :fields ] # only fields will be tracked
172172
end
173173
```
174174

175-
You can also track changes on all embedded relations.
175+
You can also track changes on all embedded (`embeds_one` and `embeds_many`) or referenced (`has_and_belongs_to_many`) relations.
176176

177177
```ruby
178178
class Post
@@ -182,7 +182,10 @@ class Post
182182
embeds_many :comments
183183
embeds_one :content
184184

185-
track_history :on => [:embedded_relations] # all embedded relations will be tracked
185+
track_history :on => [
186+
:embedded_relations,
187+
:rereferenced_relations
188+
] # only embedded and references relations will be tracked
186189
end
187190
```
188191

lib/mongoid/history/options.rb

+12-2
Original file line numberDiff line numberDiff line change
@@ -82,17 +82,25 @@ def parse_tracked_fields_and_relations
8282

8383
if options[:on].include?(:fields)
8484
@options[:on] = options[:on].reject { |opt| opt == :fields }
85-
@options[:on] = options[:on] | trackable.fields.keys.map(&:to_sym) - reserved_fields.map(&:to_sym)
85+
@options[:on] = options[:on] |
86+
trackable.fields.keys.map(&:to_sym) -
87+
reserved_fields.map(&:to_sym)
8688
end
8789

8890
if options[:on].include?(:embedded_relations)
8991
@options[:on] = options[:on].reject { |opt| opt == :embedded_relations }
92+
p trackable.embedded_relations.keys
9093
@options[:on] = options[:on] | trackable.embedded_relations.keys
9194
end
9295

96+
if options[:on].include?(:referenced_relations)
97+
@options[:on] = options[:on].reject { |opt| opt == :referenced_relations }
98+
@options[:on] = options[:on] | trackable.referenced_relations.keys
99+
end
100+
93101
@options[:fields] = []
94102
@options[:dynamic] = []
95-
@options[:relations] = { embeds_one: {}, embeds_many: {} }
103+
@options[:relations] = { embeds_one: {}, embeds_many: {}, has_and_belongs_to_many: {} }
96104

97105
options[:on].each do |option|
98106
field = get_database_field_name(option)
@@ -146,6 +154,8 @@ def categorize_tracked_option(field, field_options = nil)
146154
track_relation(field, :embeds_one, field_options)
147155
elsif trackable.embeds_many?(field)
148156
track_relation(field, :embeds_many, field_options)
157+
elsif trackable.has_and_belongs_to_many?(field)
158+
track_relation(field, :has_and_belongs_to_many, field_options)
149159
elsif trackable.fields.keys.include?(field)
150160
@options[:fields] << field
151161
else

lib/mongoid/history/trackable.rb

+50-39
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ module Trackable
44
extend ActiveSupport::Concern
55

66
module ClassMethods
7+
def has_and_belongs_to_many(field, opts = {})
8+
super field, {
9+
before_add: :track_references,
10+
before_remove: :track_references
11+
}.merge(opts)
12+
end
13+
714
def track_history(options = {})
815
extend RelationMethods
916

@@ -19,7 +26,6 @@ def track_history(options = {})
1926
end
2027

2128
include MyInstanceMethods
22-
include HasAndBelongsToManyMethods
2329
extend SingletonMethods
2430

2531
delegate :history_trackable_options, to: 'self.class'
@@ -249,6 +255,34 @@ def track_destroy(&block)
249255
track_history_for_action(:destroy, &block) unless destroyed?
250256
end
251257

258+
def track_references(related)
259+
# skip for new records (track_create will capture assignment) and when track updates disabled
260+
return true if new_record? || !track_history? || !history_trackable_options[:track_update]
261+
metadata = reflect_on_all_associations(:has_and_belongs_to_many).find { |m| m.class_name == related.class.name }
262+
263+
related_id = related.id
264+
original_ids = send(metadata.key.to_sym)
265+
modified_ids = if original_ids.include?(related_id)
266+
original_ids.reject { |id| id == related_id }
267+
else
268+
original_ids + [related_id]
269+
end
270+
271+
modified = { metadata.key => modified_ids }
272+
original = { metadata.key => original_ids }
273+
action = :update
274+
current_version = increment_current_version
275+
self.class.tracker_class.create!(
276+
history_tracker_attributes(action.to_sym).merge(
277+
version: current_version,
278+
action: action.to_s,
279+
original: original,
280+
modified: modified,
281+
trackable: self
282+
)
283+
)
284+
end
285+
252286
def clear_trackable_memoization
253287
@history_tracker_attributes = nil
254288
@modified_attributes_for_create = nil
@@ -300,44 +334,6 @@ def track_history_for_action(action)
300334
end
301335
end
302336

303-
module HasAndBelongsToManyMethods
304-
def track_has_and_belongs_to_many(related)
305-
metadata = reflect_on_all_associations(:has_and_belongs_to_many).find { |m| m.class_name == related.class.name }
306-
307-
related_id = related.id
308-
original_ids = send(metadata.key.to_sym)
309-
modified_ids = if original_ids.include?(related_id)
310-
original_ids.reject { |id| id == related_id }
311-
else
312-
original_ids + [related_id]
313-
end
314-
315-
modified = { metadata.key => modified_ids }
316-
original = { metadata.key => original_ids }
317-
action = :update
318-
self.class.tracker_class.create!(
319-
history_tracker_attributes(action.to_sym)
320-
.merge(version: increment_and_set_version,
321-
action: action.to_s,
322-
original: original,
323-
modified: modified,
324-
trackable: self)
325-
)
326-
end
327-
328-
private
329-
330-
def increment_and_set_version
331-
if Mongoid::Compatibility::Version.mongoid3?
332-
inc(:version, 1)
333-
else
334-
current_version = (version || 0) + 1
335-
set(version: current_version)
336-
current_version
337-
end
338-
end
339-
end
340-
341337
module RelationMethods
342338
# Returns a relation class for the given field.
343339
#
@@ -367,6 +363,15 @@ def embeds_many?(field)
367363
relation_of(field) == Mongoid::Relations::Embedded::Many
368364
end
369365

366+
# Indicates whether there is an Referenced::ManyToMany relation for the given embedded field.
367+
#
368+
# @param [ String | Symbol ] field The name of the referenced field.
369+
#
370+
# @return [ Boolean ] true if there is an Referenced::ManyToMany relation for the given referenced field.
371+
def has_and_belongs_to_many?(field)
372+
relation_of(field) == Mongoid::Relations::Referenced::ManyToMany
373+
end
374+
370375
# Retrieves the database representation of an embedded field name, in case the :store_as option is used.
371376
#
372377
# @param [ String | Symbol ] embed The name or alias of the embedded field.
@@ -477,6 +482,12 @@ def reserved_tracked_fields
477482
end
478483
end
479484

485+
def referenced_relations
486+
relations.select do |_, r|
487+
r.relation == Mongoid::Relations::Referenced::ManyToMany
488+
end
489+
end
490+
480491
def field_formats
481492
@field_formats ||= history_trackable_options[:format]
482493
end

spec/integration/has_and_belongs_to_many_spec.rb

+73-14
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,6 @@
22

33
describe Mongoid::History do
44
before :all 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-
has_and_belongs_to_many :tags, before_add: :track_has_and_belongs_to_many, before_remove: :track_has_and_belongs_to_many
13-
track_history on: %i[all], track_create: false, track_update: false
14-
end
15-
165
class Tag
176
include Mongoid::Document
187

@@ -22,6 +11,19 @@ class Tag
2211
end
2312

2413
describe 'track' do
14+
before :all do
15+
class Post
16+
include Mongoid::Document
17+
include Mongoid::Timestamps
18+
include Mongoid::History::Trackable
19+
20+
field :title
21+
field :body
22+
has_and_belongs_to_many :tags
23+
track_history on: %i[fields]
24+
end
25+
end
26+
2527
let(:tag) { Tag.create! }
2628

2729
describe 'on creation' do
@@ -35,8 +37,8 @@ class Tag
3537
expect(post.history_tracks.first.modified).to include('tag_ids' => [tag.id])
3638
end
3739

38-
it 'should assign empty tag_ids on original' do
39-
expect(post.history_tracks.first.original).to include('tag_ids' => [])
40+
it 'should be empty on original' do
41+
expect(post.history_tracks.first.original).to eq({})
4042
end
4143
end
4244

@@ -110,10 +112,67 @@ class Tag
110112
expect(post.history_tracks.last.original).to include('tag_ids' => [])
111113
end
112114
end
115+
116+
after :all do
117+
Object.send(:remove_const, :Post)
118+
end
119+
end
120+
121+
describe 'not track' do
122+
let!(:post) { Post.create! }
123+
124+
context 'track_update: false' do
125+
before :all do
126+
class Post
127+
include Mongoid::Document
128+
include Mongoid::Timestamps
129+
include Mongoid::History::Trackable
130+
131+
field :title
132+
field :body
133+
has_and_belongs_to_many :tags
134+
track_history on: %i[fields], track_update: false
135+
end
136+
end
137+
138+
it 'should not create track' do
139+
expect { post.tags = [Tag.create!] }.not_to change(Tracker, :count)
140+
end
141+
142+
after :all do
143+
Object.send(:remove_const, :Post)
144+
end
145+
end
146+
147+
context '#disable_tracking' do
148+
before :all do
149+
class Post
150+
include Mongoid::Document
151+
include Mongoid::Timestamps
152+
include Mongoid::History::Trackable
153+
154+
field :title
155+
field :body
156+
has_and_belongs_to_many :tags
157+
track_history on: %i[fields]
158+
end
159+
end
160+
161+
it 'should not create track' do
162+
expect do
163+
Post.disable_tracking do
164+
post.tags = [Tag.create!]
165+
end
166+
end.not_to change(Tracker, :count)
167+
end
168+
169+
after :all do
170+
Object.send(:remove_const, :Post)
171+
end
172+
end
113173
end
114174

115175
after :all do
116-
Object.send(:remove_const, :Post)
117176
Object.send(:remove_const, :Tag)
118177
end
119178
end

0 commit comments

Comments
 (0)