mongoid-history 0.8.0 → 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 +5 -5
  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 -101
  9. data/CHANGELOG.md +173 -144
  10. data/CONTRIBUTING.md +117 -118
  11. data/Dangerfile +1 -1
  12. data/Gemfile +49 -37
  13. data/LICENSE.txt +20 -20
  14. data/README.md +609 -595
  15. data/RELEASING.md +66 -67
  16. data/Rakefile +24 -24
  17. data/UPGRADING.md +53 -34
  18. data/lib/mongoid/history/attributes/base.rb +72 -72
  19. data/lib/mongoid/history/attributes/create.rb +45 -50
  20. data/lib/mongoid/history/attributes/destroy.rb +34 -34
  21. data/lib/mongoid/history/attributes/update.rb +104 -45
  22. data/lib/mongoid/history/options.rb +177 -179
  23. data/lib/mongoid/history/trackable.rb +588 -521
  24. data/lib/mongoid/history/tracker.rb +247 -244
  25. data/lib/mongoid/history/version.rb +5 -5
  26. data/lib/mongoid/history.rb +77 -52
  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 -0
  30. data/perf/gc_suite.rb +21 -0
  31. data/spec/integration/embedded_in_polymorphic_spec.rb +112 -135
  32. data/spec/integration/integration_spec.rb +976 -942
  33. data/spec/integration/multi_relation_spec.rb +47 -53
  34. data/spec/integration/multiple_trackers_spec.rb +68 -71
  35. data/spec/integration/nested_embedded_documents_spec.rb +64 -84
  36. data/spec/integration/nested_embedded_documents_tracked_in_parent_spec.rb +124 -0
  37. data/spec/integration/nested_embedded_polymorphic_documents_spec.rb +115 -127
  38. data/spec/integration/subclasses_spec.rb +47 -29
  39. data/spec/integration/track_history_order_spec.rb +84 -52
  40. data/spec/integration/validation_failure_spec.rb +76 -63
  41. data/spec/spec_helper.rb +32 -25
  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 -13
  45. data/spec/unit/attributes/base_spec.rb +141 -150
  46. data/spec/unit/attributes/create_spec.rb +342 -315
  47. data/spec/unit/attributes/destroy_spec.rb +228 -218
  48. data/spec/unit/attributes/update_spec.rb +342 -321
  49. data/spec/unit/callback_options_spec.rb +165 -159
  50. data/spec/unit/embedded_methods_spec.rb +87 -69
  51. data/spec/unit/history_spec.rb +58 -35
  52. data/spec/unit/my_instance_methods_spec.rb +555 -485
  53. data/spec/unit/options_spec.rb +365 -327
  54. data/spec/unit/singleton_methods_spec.rb +406 -338
  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 -689
  58. data/spec/unit/tracker_spec.rb +190 -163
  59. metadata +13 -8
  60. data/.travis.yml +0 -35
@@ -1,45 +1,104 @@
1
- module Mongoid
2
- module History
3
- module Attributes
4
- class Update < ::Mongoid::History::Attributes::Base
5
- def attributes
6
- @attributes = {}
7
- changes.each do |k, v|
8
- if trackable_class.tracked_embeds_one?(k)
9
- insert_embeds_one_changes(k, v)
10
- elsif trackable_class.tracked_embeds_many?(k)
11
- insert_embeds_many_changes(k, v)
12
- elsif trackable_class.tracked?(k, :update)
13
- @attributes[k] = format_field(k, v) unless v.all?(&:blank?)
14
- end
15
- end
16
- @attributes
17
- end
18
-
19
- private
20
-
21
- def insert_embeds_one_changes(relation, value)
22
- relation = trackable_class.database_field_name(relation)
23
- relation_class = trackable_class.embeds_one_class(relation)
24
- paranoia_field = Mongoid::History.trackable_class_settings(relation_class)[:paranoia_field]
25
- original_value = value[0][paranoia_field].present? ? {} : format_embeds_one_relation(relation, value[0])
26
- modified_value = value[1][paranoia_field].present? ? {} : format_embeds_one_relation(relation, value[1])
27
- return if original_value == modified_value
28
- @attributes[relation] = [original_value, modified_value]
29
- end
30
-
31
- def insert_embeds_many_changes(relation, value)
32
- relation = trackable_class.database_field_name(relation)
33
- relation_class = trackable_class.embeds_many_class(relation)
34
- paranoia_field = Mongoid::History.trackable_class_settings(relation_class)[:paranoia_field]
35
- original_value = value[0].reject { |rel| rel[paranoia_field].present? }
36
- .map { |v_attrs| format_embeds_many_relation(relation, v_attrs) }
37
- modified_value = value[1].reject { |rel| rel[paranoia_field].present? }
38
- .map { |v_attrs| format_embeds_many_relation(relation, v_attrs) }
39
- return if original_value == modified_value
40
- @attributes[relation] = [original_value, modified_value]
41
- end
42
- end
43
- end
44
- end
45
- end
1
+ module Mongoid
2
+ module History
3
+ module Attributes
4
+ class Update < ::Mongoid::History::Attributes::Base
5
+ # @example when both an attribute `foo` and a child's attribute `nested_bar.baz` are changed
6
+ #
7
+ # {
8
+ # 'foo' => ['foo_before_changes', 'foo_after_changes']
9
+ # 'nested_bar.baz' => ['nested_bar_baz_before_changes', 'nested_bar_baz_after_changes']
10
+ # }
11
+ # }
12
+ #
13
+ # @return [Hash<String, Array<(?,?)>>] Hash of changes
14
+ def attributes
15
+ changes_from_parent.deep_merge(changes_from_children)
16
+ end
17
+
18
+ private
19
+
20
+ def changes_from_parent
21
+ parent_changes = {}
22
+ changes.each do |k, v|
23
+ change_value = begin
24
+ if trackable_class.tracked_embeds_one?(k)
25
+ embeds_one_changes_from_parent(k, v)
26
+ elsif trackable_class.tracked_embeds_many?(k)
27
+ embeds_many_changes_from_parent(k, v)
28
+ elsif trackable_class.tracked?(k, :update)
29
+ { k => format_field(k, v) } unless v.all?(&:blank?)
30
+ end
31
+ end
32
+ parent_changes.merge!(change_value) if change_value.present?
33
+ end
34
+ parent_changes
35
+ end
36
+
37
+ def changes_from_children
38
+ embeds_one_changes_from_embedded_documents
39
+ end
40
+
41
+ # Retrieve the list of changes applied directly to the nested documents
42
+ #
43
+ # @example when a child's name is changed from "todd" to "mario"
44
+ #
45
+ # child = Child.new(name: 'todd')
46
+ # Parent.create(child: child)
47
+ # child.name = "Mario"
48
+ #
49
+ # embeds_one_changes_from_embedded_documents # when called from "Parent"
50
+ # # => { "child.name"=>["todd", "mario"] }
51
+ #
52
+ # @return [Hash<String, Array<(?,?)>] changes of embeds_ones from embedded documents
53
+ def embeds_one_changes_from_embedded_documents
54
+ embedded_doc_changes = {}
55
+ trackable_class.tracked_embeds_one.each do |rel|
56
+ rel_class = trackable_class.relation_class_of(rel)
57
+ paranoia_field = Mongoid::History.trackable_class_settings(rel_class)[:paranoia_field]
58
+ paranoia_field = rel_class.aliased_fields.key(paranoia_field) || paranoia_field
59
+ rel = aliased_fields.key(rel) || rel
60
+ obj = trackable.send(rel)
61
+ next if !obj || (obj.respond_to?(paranoia_field) && obj.public_send(paranoia_field).present?)
62
+
63
+ obj.changes.each do |k, v|
64
+ embedded_doc_changes["#{rel}.#{k}"] = [v.first, v.last]
65
+ end
66
+ end
67
+ embedded_doc_changes
68
+ end
69
+
70
+ # @param [String] relation
71
+ # @param [String] value
72
+ #
73
+ # @return [Hash<String, Array<(?,?)>>]
74
+ def embeds_one_changes_from_parent(relation, value)
75
+ relation = trackable_class.database_field_name(relation)
76
+ relation_class = trackable_class.relation_class_of(relation)
77
+ paranoia_field = Mongoid::History.trackable_class_settings(relation_class)[:paranoia_field]
78
+ original_value = value[0][paranoia_field].present? ? {} : format_embeds_one_relation(relation, value[0])
79
+ modified_value = value[1][paranoia_field].present? ? {} : format_embeds_one_relation(relation, value[1])
80
+ return if original_value == modified_value
81
+
82
+ { relation => [original_value, modified_value] }
83
+ end
84
+
85
+ # @param [String] relation
86
+ # @param [String] value
87
+ #
88
+ # @return [Hash<Array<(?,?)>>]
89
+ def embeds_many_changes_from_parent(relation, value)
90
+ relation = trackable_class.database_field_name(relation)
91
+ relation_class = trackable_class.relation_class_of(relation)
92
+ paranoia_field = Mongoid::History.trackable_class_settings(relation_class)[:paranoia_field]
93
+ original_value = value[0].reject { |rel| rel[paranoia_field].present? }
94
+ .map { |v_attrs| format_embeds_many_relation(relation, v_attrs) }
95
+ modified_value = value[1].reject { |rel| rel[paranoia_field].present? }
96
+ .map { |v_attrs| format_embeds_many_relation(relation, v_attrs) }
97
+ return if original_value == modified_value
98
+
99
+ { relation => [original_value, modified_value] }
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -1,179 +1,177 @@
1
- module Mongoid
2
- module History
3
- class Options
4
- attr_reader :trackable, :options
5
-
6
- def initialize(trackable, opts = {})
7
- @trackable = trackable
8
- @options = default_options.merge(opts)
9
- end
10
-
11
- def scope
12
- trackable.collection_name.to_s.singularize.to_sym
13
- end
14
-
15
- def prepared
16
- @prepared ||= begin
17
- prepare_skipped_fields
18
- prepare_formatted_fields
19
- parse_tracked_fields_and_relations
20
- options
21
- end
22
- end
23
-
24
- private
25
-
26
- def default_options
27
- @default_options ||=
28
- { on: :all,
29
- except: %i[created_at updated_at],
30
- tracker_class_name: nil,
31
- modifier_field: :modifier,
32
- version_field: :version,
33
- changes_method: :changes,
34
- scope: scope,
35
- track_create: true,
36
- track_update: true,
37
- track_destroy: true,
38
- format: nil }
39
- end
40
-
41
- # Sets the :except attributes and relations in `options` to be an [ Array <String> ]
42
- # The attribute names and relations are stored by their `database_field_name`s
43
- # Removes the `nil` and duplicate entries from skipped attributes/relations list
44
- def prepare_skipped_fields
45
- # normalize :except fields to an array of database field strings
46
- @options[:except] = Array(options[:except])
47
- @options[:except] = options[:except].map { |field| trackable.database_field_name(field) }.compact.uniq
48
- end
49
-
50
- def prepare_formatted_fields
51
- formats = {}
52
-
53
- if options[:format].class == Hash
54
- options[:format].each do |field, format|
55
- next if field.nil?
56
-
57
- field = trackable.database_field_name(field)
58
-
59
- if format.class == Hash && trackable.embeds_many?(field)
60
- relation_class = trackable.embeds_many_class(field)
61
- formats[field] = format.inject({}) { |a, e| a.merge(relation_class.database_field_name(e.first) => e.last) }
62
- elsif format.class == Hash && trackable.embeds_one?(field)
63
- relation_class = trackable.embeds_one_class(field)
64
- formats[field] = format.inject({}) { |a, e| a.merge(relation_class.database_field_name(e.first) => e.last) }
65
- else
66
- formats[field] = format
67
- end
68
- end
69
- end
70
-
71
- options[:format] = formats
72
- end
73
-
74
- def parse_tracked_fields_and_relations
75
- # case `options[:on]`
76
- # when `posts: [:id, :title]`, then it will convert it to `[[:posts, [:id, :title]]]`
77
- # when `:foo`, then `[:foo]`
78
- # when `[:foo, { posts: [:id, :title] }]`, then return as is
79
- @options[:on] = Array(options[:on])
80
-
81
- @options[:on] = options[:on].map { |opt| opt == :all ? :fields : opt }
82
-
83
- if options[:on].include?(:fields)
84
- @options[:on] = options[:on].reject { |opt| opt == :fields }
85
- @options[:on] = options[:on] | trackable.fields.keys.map(&:to_sym) - reserved_fields.map(&:to_sym)
86
- end
87
-
88
- if options[:on].include?(:embedded_relations)
89
- @options[:on] = options[:on].reject { |opt| opt == :embedded_relations }
90
- @options[:on] = options[:on] | trackable.embedded_relations.keys
91
- end
92
-
93
- @options[:fields] = []
94
- @options[:dynamic] = []
95
- @options[:relations] = { embeds_one: {}, embeds_many: {} }
96
-
97
- options[:on].each do |option|
98
- field = get_database_field_name(option)
99
- field_options = get_field_options(option)
100
- categorize_tracked_option(field, field_options)
101
- end
102
- end
103
-
104
- # Returns the database_field_name key for tracked option
105
- #
106
- # @param [ String | Symbol | Array | Hash ] option The field or relation name to track
107
- #
108
- # @return [ String ] the database field name for tracked option
109
- def get_database_field_name(option)
110
- key = if option.is_a?(Hash)
111
- option.keys.first
112
- elsif option.is_a?(Array)
113
- option.first
114
- end
115
- trackable.database_field_name(key || option)
116
- end
117
-
118
- # Returns the tracked attributes for embedded relations, otherwise `nil`
119
- #
120
- # @param [ String | Symbol | Array | Hash ] option The field or relation name to track
121
- #
122
- # @return [ nil | Array <String | Symbol> ] the list of tracked fields for embedded relation
123
- def get_field_options(option)
124
- if option.is_a?(Hash)
125
- option.values.first
126
- elsif option.is_a?(Array)
127
- option.last
128
- end
129
- end
130
-
131
- # Tracks the passed option under:
132
- # `fields`
133
- # `dynamic`
134
- # `relations -> embeds_one` or
135
- # `relations -> embeds_many`
136
- #
137
- # @param [ String ] field The database field name of field or relation to track
138
- # @param [ nil | Array <String | Symbol> ] field_options The tracked fields for embedded relations
139
- def categorize_tracked_option(field, field_options = nil)
140
- return if options[:except].include?(field)
141
- return if reserved_fields.include?(field)
142
-
143
- field_options = Array(field_options)
144
-
145
- if trackable.embeds_one?(field)
146
- track_embeds_one(field, field_options)
147
- elsif trackable.embeds_many?(field)
148
- track_embeds_many(field, field_options)
149
- elsif trackable.fields.keys.include?(field)
150
- @options[:fields] << field
151
- else
152
- @options[:dynamic] << field
153
- end
154
- end
155
-
156
- def track_embeds_one(field, field_options)
157
- relation_class = trackable.embeds_one_class(field)
158
- @options[:relations][:embeds_one][field] = if field_options.blank?
159
- relation_class.fields.keys
160
- else
161
- %w[_id] | field_options.map { |opt| relation_class.database_field_name(opt) }
162
- end
163
- end
164
-
165
- def track_embeds_many(field, field_options)
166
- relation_class = trackable.embeds_many_class(field)
167
- @options[:relations][:embeds_many][field] = if field_options.blank?
168
- relation_class.fields.keys
169
- else
170
- %w[_id] | field_options.map { |opt| relation_class.database_field_name(opt) }
171
- end
172
- end
173
-
174
- def reserved_fields
175
- @reserved_fields ||= ['_id', '_type', options[:version_field].to_s, "#{options[:modifier_field]}_id"]
176
- end
177
- end
178
- end
179
- end
1
+ module Mongoid
2
+ module History
3
+ class Options
4
+ attr_reader :trackable, :options
5
+
6
+ def initialize(trackable, opts = {})
7
+ @trackable = trackable
8
+ @options = default_options.merge(opts)
9
+ end
10
+
11
+ def scope
12
+ trackable.collection_name.to_s.singularize.to_sym
13
+ end
14
+
15
+ def prepared
16
+ return @prepared if @prepared
17
+ @prepared = options.dup
18
+ prepare_skipped_fields
19
+ prepare_formatted_fields
20
+ parse_tracked_fields_and_relations
21
+ @prepared
22
+ end
23
+
24
+ private
25
+
26
+ def default_options
27
+ { on: :all,
28
+ except: %i[created_at updated_at],
29
+ tracker_class_name: nil,
30
+ modifier_field: :modifier,
31
+ version_field: :version,
32
+ changes_method: :changes,
33
+ scope: scope,
34
+ track_create: true,
35
+ track_update: true,
36
+ track_destroy: true,
37
+ format: nil }
38
+ end
39
+
40
+ # Sets the :except attributes and relations in `options` to be an [ Array <String> ]
41
+ # The attribute names and relations are stored by their `database_field_name`s
42
+ # Removes the `nil` and duplicate entries from skipped attributes/relations list
43
+ def prepare_skipped_fields
44
+ # normalize :except fields to an array of database field strings
45
+ @prepared[:except] = Array(@prepared[:except])
46
+ @prepared[:except] = @prepared[:except].map { |field| trackable.database_field_name(field) }.compact.uniq
47
+ end
48
+
49
+ def prepare_formatted_fields
50
+ formats = {}
51
+
52
+ if @prepared[:format].class == Hash
53
+ @prepared[:format].each do |field, format|
54
+ next if field.nil?
55
+
56
+ field = trackable.database_field_name(field)
57
+
58
+ if format.class == Hash && trackable.embeds_many?(field)
59
+ relation_class = trackable.relation_class_of(field)
60
+ formats[field] = format.inject({}) { |a, e| a.merge(relation_class.database_field_name(e.first) => e.last) }
61
+ elsif format.class == Hash && trackable.embeds_one?(field)
62
+ relation_class = trackable.relation_class_of(field)
63
+ formats[field] = format.inject({}) { |a, e| a.merge(relation_class.database_field_name(e.first) => e.last) }
64
+ else
65
+ formats[field] = format
66
+ end
67
+ end
68
+ end
69
+
70
+ @prepared[:format] = formats
71
+ end
72
+
73
+ def parse_tracked_fields_and_relations
74
+ # case `options[:on]`
75
+ # when `posts: [:id, :title]`, then it will convert it to `[[:posts, [:id, :title]]]`
76
+ # when `:foo`, then `[:foo]`
77
+ # when `[:foo, { posts: [:id, :title] }]`, then return as is
78
+ @prepared[:on] = Array(@prepared[:on])
79
+
80
+ @prepared[:on] = @prepared[:on].map { |opt| opt == :all ? :fields : opt }
81
+
82
+ if @prepared[:on].include?(:fields)
83
+ @prepared[:on] = @prepared[:on].reject { |opt| opt == :fields }
84
+ @prepared[:on] = @prepared[:on] | trackable.fields.keys.map(&:to_sym) - reserved_fields.map(&:to_sym)
85
+ end
86
+
87
+ if @prepared[:on].include?(:embedded_relations)
88
+ @prepared[:on] = @prepared[:on].reject { |opt| opt == :embedded_relations }
89
+ @prepared[:on] = @prepared[:on] | trackable.embedded_relations.keys
90
+ end
91
+
92
+ @prepared[:fields] = []
93
+ @prepared[:dynamic] = []
94
+ @prepared[:relations] = { embeds_one: {}, embeds_many: {} }
95
+
96
+ @prepared[:on].each do |option|
97
+ if option.is_a?(Hash)
98
+ option.each { |k, v| split_and_categorize(k => v) }
99
+ else
100
+ split_and_categorize(option)
101
+ end
102
+ end
103
+ end
104
+
105
+ def split_and_categorize(field_and_options)
106
+ field = get_database_field_name(field_and_options)
107
+ field_options = get_field_options(field_and_options)
108
+ categorize_tracked_option(field, field_options)
109
+ end
110
+
111
+ # Returns the database_field_name key for tracked option
112
+ #
113
+ # @param [ String | Symbol | Array | Hash ] option The field or relation name to track
114
+ #
115
+ # @return [ String ] the database field name for tracked option
116
+ def get_database_field_name(option)
117
+ key = if option.is_a?(Hash)
118
+ option.keys.first
119
+ elsif option.is_a?(Array)
120
+ option.first
121
+ end
122
+ trackable.database_field_name(key || option)
123
+ end
124
+
125
+ # Returns the tracked attributes for embedded relations, otherwise `nil`
126
+ #
127
+ # @param [ String | Symbol | Array | Hash ] option The field or relation name to track
128
+ #
129
+ # @return [ nil | Array <String | Symbol> ] the list of tracked fields for embedded relation
130
+ def get_field_options(option)
131
+ if option.is_a?(Hash)
132
+ option.values.first
133
+ elsif option.is_a?(Array)
134
+ option.last
135
+ end
136
+ end
137
+
138
+ # Tracks the passed option under:
139
+ # `fields`
140
+ # `dynamic`
141
+ # `relations -> embeds_one` or
142
+ # `relations -> embeds_many`
143
+ #
144
+ # @param [ String ] field The database field name of field or relation to track
145
+ # @param [ nil | Array <String | Symbol> ] field_options The tracked fields for embedded relations
146
+ def categorize_tracked_option(field, field_options = nil)
147
+ return if @prepared[:except].include?(field)
148
+ return if reserved_fields.include?(field)
149
+
150
+ field_options = Array(field_options)
151
+
152
+ if trackable.embeds_one?(field)
153
+ track_relation(field, :embeds_one, field_options)
154
+ elsif trackable.embeds_many?(field)
155
+ track_relation(field, :embeds_many, field_options)
156
+ elsif trackable.fields.keys.include?(field)
157
+ @prepared[:fields] << field
158
+ else
159
+ @prepared[:dynamic] << field
160
+ end
161
+ end
162
+
163
+ def track_relation(field, kind, field_options)
164
+ relation_class = trackable.relation_class_of(field)
165
+ @prepared[:relations][kind][field] = if field_options.blank?
166
+ relation_class.fields.keys
167
+ else
168
+ %w[_id] | field_options.map { |opt| relation_class.database_field_name(opt) }
169
+ end
170
+ end
171
+
172
+ def reserved_fields
173
+ @reserved_fields ||= ['_id', '_type', @prepared[:version_field].to_s, "#{@prepared[:modifier_field]}_id"]
174
+ end
175
+ end
176
+ end
177
+ end