mongoid-history 0.4.0 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,220 +1,215 @@
1
- module Mongoid::History
2
- module Tracker
3
- extend ActiveSupport::Concern
4
-
5
- included do
6
- include Mongoid::Document
7
- include Mongoid::Timestamps
8
- attr_writer :trackable
9
-
10
- field :association_chain, :type => Array, :default => []
11
- field :modified, :type => Hash
12
- field :original, :type => Hash
13
- field :version, :type => Integer
14
- field :action, :type => String
15
- field :scope, :type => String
16
- belongs_to :modifier, :class_name => Mongoid::History.modifier_class_name
17
-
18
- index(:scope => 1)
19
- index(:association_chain => 1)
20
-
21
- Mongoid::History.tracker_class_name = self.name.tableize.singularize.to_sym
22
-
23
- if defined?(ActionController) and defined?(ActionController::Base)
24
- ActionController::Base.class_eval do
25
- around_filter Mongoid::History::Sweeper.instance
26
- end
27
- end
28
- end
29
-
30
- def undo!(modifier)
31
- if action.to_sym == :destroy
32
- re_create
33
- elsif action.to_sym == :create
34
- re_destroy
35
- else
36
- trackable.update_attributes!(undo_attr(modifier))
37
- end
38
- end
39
-
40
- def redo!(modifier)
41
- if action.to_sym == :destroy
42
- re_destroy
43
- elsif action.to_sym == :create
44
- re_create
45
- else
46
- trackable.update_attributes!(redo_attr(modifier))
47
- end
48
- end
49
-
50
- def undo_attr(modifier)
51
- undo_hash = affected.easy_unmerge(modified)
52
- undo_hash.easy_merge!(original)
53
- modifier_field = trackable.history_trackable_options[:modifier_field]
54
- undo_hash[modifier_field] = modifier
55
- (modified.keys - undo_hash.keys).each do |k|
56
- undo_hash[k] = nil
57
- end
58
- localize_keys(undo_hash)
59
- end
60
-
61
- def redo_attr(modifier)
62
- redo_hash = affected.easy_unmerge(original)
63
- redo_hash.easy_merge!(modified)
64
- modifier_field = trackable.history_trackable_options[:modifier_field]
65
- redo_hash[modifier_field] = modifier
66
- localize_keys(redo_hash)
67
- end
68
-
69
- def trackable_root
70
- @trackable_root ||= trackable_parents_and_trackable.first
71
- end
72
-
73
- def trackable
74
- @trackable ||= trackable_parents_and_trackable.last
75
- end
76
-
77
- def trackable_parents
78
- @trackable_parents ||= trackable_parents_and_trackable[0, -1]
79
- end
80
-
81
- def trackable_parent
82
- @trackable_parent ||= trackable_parents_and_trackable[-2]
83
- end
84
-
85
- # Outputs a :from, :to hash for each affected field. Intentionally excludes fields
86
- # which are not tracked, even if there are tracked values for such fields
87
- # present in the database.
88
- #
89
- # @return [ HashWithIndifferentAccess ] a change set in the format:
90
- # { field_1: {to: new_val}, field_2: {from: old_val, to: new_val} }
91
- def tracked_changes
92
- @tracked_changes ||= (modified.keys | original.keys).inject(HashWithIndifferentAccess.new) do |h,k|
93
- h[k] = {from: original[k], to: modified[k]}.delete_if{|k,v| v.nil?}
94
- h
95
- end.delete_if{|k,v| v.blank? || !trackable_parent_class.tracked_field?(k)}
96
- end
97
-
98
- # Outputs summary of edit actions performed: :add, :modify, :remove, or :array.
99
- # Does deep comparison of arrays. Useful for creating human-readable representations
100
- # of the history tracker. Considers changing a value to 'blank' to be a removal.
101
- #
102
- # @return [ HashWithIndifferentAccess ] a change set in the format:
103
- # { add: { field_1: new_val, ... },
104
- # modify: { field_2: {from: old_val, to: new_val}, ... },
105
- # remove: { field_3: old_val },
106
- # array: { field_4: {add: ['foo', 'bar'], remove: ['baz']} } }
107
- def tracked_edits
108
- @tracked_edits ||= tracked_changes.inject(HashWithIndifferentAccess.new) do |h,(k,v)|
109
- unless v[:from].blank? && v[:to].blank?
110
- if v[:from].blank?
111
- h[:add] ||={}
112
- h[:add][k] = v[:to]
113
- elsif v[:to].blank?
114
- h[:remove] ||={}
115
- h[:remove][k] = v[:from]
116
- else
117
- if v[:from].is_a?(Array) && v[:to].is_a?(Array)
118
- h[:array] ||={}
119
- old_values = v[:from] - v[:to]
120
- new_values = v[:to] - v[:from]
121
- h[:array][k] = {add: new_values, remove: old_values}.delete_if{|k,v| v.blank?}
122
- else
123
- h[:modify] ||={}
124
- h[:modify][k] = v
125
- end
126
- end
127
- end
128
- h
129
- end
130
- end
131
-
132
- # Similar to #tracked_changes, but contains only a single value for each
133
- # affected field:
134
- # - :create and :update return the modified values
135
- # - :destroy returns original values
136
- # Included for legacy compatibility.
137
- #
138
- # @deprecated
139
- #
140
- # @return [ HashWithIndifferentAccess ] a change set in the format:
141
- # { field_1: value, field_2: value }
142
- def affected
143
- target = action.to_sym == :destroy ? :from : :to
144
- @affected ||= tracked_changes.inject(HashWithIndifferentAccess.new){|h,(k,v)| h[k] = v[target]; h}
145
- end
146
-
147
- # Returns the class of the trackable, irrespective of whether the trackable object
148
- # has been destroyed.
149
- #
150
- # @return [ Class ] the class of the trackable
151
- def trackable_parent_class
152
- association_chain.first["name"].constantize
153
- end
154
-
155
- private
156
-
157
- def re_create
158
- association_chain.length > 1 ? create_on_parent : create_standalone
159
- end
160
-
161
- def re_destroy
162
- trackable.destroy
163
- end
164
-
165
- def create_standalone
166
- restored = trackable_parent_class.new(localize_keys(original))
167
- restored.id = original["_id"]
168
- restored.save!
169
- end
170
-
171
- def create_on_parent
172
- name = association_chain.last["name"]
173
- if trackable_parent.class.embeds_one?(name)
174
- trackable_parent.create_embedded(name, localize_keys(original))
175
- elsif trackable_parent.class.embeds_many?(name)
176
- trackable_parent.get_embedded(name).create!(localize_keys(original))
177
- else
178
- raise "This should never happen. Please report bug!"
179
- end
180
- end
181
-
182
- def trackable_parents_and_trackable
183
- @trackable_parents_and_trackable ||= traverse_association_chain
184
- end
185
-
186
- def traverse_association_chain
187
- chain = association_chain.dup
188
- doc = nil
189
- documents = []
190
-
191
- begin
192
- node = chain.shift
193
- name = node['name']
194
-
195
- doc = if doc.nil?
196
- # root association. First element of the association chain
197
- klass = name.classify.constantize
198
- klass.where(:_id => node['id']).first
199
- elsif doc.class.embeds_one?(name)
200
- doc.get_embedded(name)
201
- elsif doc.class.embeds_many?(name)
202
- doc.get_embedded(name).where(:_id => node['id']).first
203
- else
204
- raise "This should never happen. Please report bug."
205
- end
206
- documents << doc
207
- end while( !chain.empty? )
208
- documents
209
- end
210
-
211
- def localize_keys(hash)
212
- klass = association_chain.first["name"].constantize
213
- klass.localized_fields.keys.each do |name|
214
- hash["#{name}_translations"] = hash.delete(name) if hash[name].present?
215
- end if klass.respond_to?(:localized_fields)
216
- hash
217
- end
218
-
219
- end
220
- end
1
+ module Mongoid::History
2
+ module Tracker
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ include Mongoid::Document
7
+ include Mongoid::Timestamps
8
+ attr_writer :trackable
9
+
10
+ field :association_chain, type: Array, default: []
11
+ field :modified, type: Hash
12
+ field :original, type: Hash
13
+ field :version, type: Integer
14
+ field :action, type: String
15
+ field :scope, type: String
16
+ belongs_to :modifier, class_name: Mongoid::History.modifier_class_name
17
+
18
+ index(scope: 1)
19
+ index(association_chain: 1)
20
+
21
+ Mongoid::History.tracker_class_name = name.tableize.singularize.to_sym
22
+ end
23
+
24
+ def undo!(modifier = nil)
25
+ if action.to_sym == :destroy
26
+ re_create
27
+ elsif action.to_sym == :create
28
+ re_destroy
29
+ else
30
+ trackable.update_attributes!(undo_attr(modifier), without_protection: true)
31
+ end
32
+ end
33
+
34
+ def redo!(modifier = nil)
35
+ if action.to_sym == :destroy
36
+ re_destroy
37
+ elsif action.to_sym == :create
38
+ re_create
39
+ else
40
+ trackable.update_attributes!(redo_attr(modifier), without_protection: true)
41
+ end
42
+ end
43
+
44
+ def undo_attr(modifier)
45
+ undo_hash = affected.easy_unmerge(modified)
46
+ undo_hash.easy_merge!(original)
47
+ modifier_field = trackable.history_trackable_options[:modifier_field]
48
+ undo_hash[modifier_field] = modifier
49
+ (modified.keys - undo_hash.keys).each do |k|
50
+ undo_hash[k] = nil
51
+ end
52
+ localize_keys(undo_hash)
53
+ end
54
+
55
+ def redo_attr(modifier)
56
+ redo_hash = affected.easy_unmerge(original)
57
+ redo_hash.easy_merge!(modified)
58
+ modifier_field = trackable.history_trackable_options[:modifier_field]
59
+ redo_hash[modifier_field] = modifier
60
+ localize_keys(redo_hash)
61
+ end
62
+
63
+ def trackable_root
64
+ @trackable_root ||= trackable_parents_and_trackable.first
65
+ end
66
+
67
+ def trackable
68
+ @trackable ||= trackable_parents_and_trackable.last
69
+ end
70
+
71
+ def trackable_parents
72
+ @trackable_parents ||= trackable_parents_and_trackable[0, -1]
73
+ end
74
+
75
+ def trackable_parent
76
+ @trackable_parent ||= trackable_parents_and_trackable[-2]
77
+ end
78
+
79
+ # Outputs a :from, :to hash for each affected field. Intentionally excludes fields
80
+ # which are not tracked, even if there are tracked values for such fields
81
+ # present in the database.
82
+ #
83
+ # @return [ HashWithIndifferentAccess ] a change set in the format:
84
+ # { field_1: {to: new_val}, field_2: {from: old_val, to: new_val} }
85
+ def tracked_changes
86
+ @tracked_changes ||= (modified.keys | original.keys).inject(HashWithIndifferentAccess.new) do |h, k|
87
+ h[k] = { from: original[k], to: modified[k] }.delete_if { |_, vv| vv.nil? }
88
+ h
89
+ end.delete_if { |k, v| v.blank? || !trackable_parent_class.tracked_field?(k) }
90
+ end
91
+
92
+ # Outputs summary of edit actions performed: :add, :modify, :remove, or :array.
93
+ # Does deep comparison of arrays. Useful for creating human-readable representations
94
+ # of the history tracker. Considers changing a value to 'blank' to be a removal.
95
+ #
96
+ # @return [ HashWithIndifferentAccess ] a change set in the format:
97
+ # { add: { field_1: new_val, ... },
98
+ # modify: { field_2: {from: old_val, to: new_val}, ... },
99
+ # remove: { field_3: old_val },
100
+ # array: { field_4: {add: ['foo', 'bar'], remove: ['baz']} } }
101
+ def tracked_edits
102
+ @tracked_edits ||= tracked_changes.inject(HashWithIndifferentAccess.new) do |h, (k, v)|
103
+ unless v[:from].blank? && v[:to].blank?
104
+ if v[:from].blank?
105
+ h[:add] ||= {}
106
+ h[:add][k] = v[:to]
107
+ elsif v[:to].blank?
108
+ h[:remove] ||= {}
109
+ h[:remove][k] = v[:from]
110
+ else
111
+ if v[:from].is_a?(Array) && v[:to].is_a?(Array)
112
+ h[:array] ||= {}
113
+ old_values = v[:from] - v[:to]
114
+ new_values = v[:to] - v[:from]
115
+ h[:array][k] = { add: new_values, remove: old_values }.delete_if { |_, vv| vv.blank? }
116
+ else
117
+ h[:modify] ||= {}
118
+ h[:modify][k] = v
119
+ end
120
+ end
121
+ end
122
+ h
123
+ end
124
+ end
125
+
126
+ # Similar to #tracked_changes, but contains only a single value for each
127
+ # affected field:
128
+ # - :create and :update return the modified values
129
+ # - :destroy returns original values
130
+ # Included for legacy compatibility.
131
+ #
132
+ # @deprecated
133
+ #
134
+ # @return [ HashWithIndifferentAccess ] a change set in the format:
135
+ # { field_1: value, field_2: value }
136
+ def affected
137
+ target = action.to_sym == :destroy ? :from : :to
138
+ @affected ||= tracked_changes.inject(HashWithIndifferentAccess.new) { |h, (k, v)|
139
+ h[k] = v[target]
140
+ h
141
+ }
142
+ end
143
+
144
+ # Returns the class of the trackable, irrespective of whether the trackable object
145
+ # has been destroyed.
146
+ #
147
+ # @return [ Class ] the class of the trackable
148
+ def trackable_parent_class
149
+ association_chain.first["name"].constantize
150
+ end
151
+
152
+ private
153
+
154
+ def re_create
155
+ association_chain.length > 1 ? create_on_parent : create_standalone
156
+ end
157
+
158
+ def re_destroy
159
+ trackable.destroy
160
+ end
161
+
162
+ def create_standalone
163
+ restored = trackable_parent_class.new(localize_keys(original))
164
+ restored.id = original["_id"]
165
+ restored.save!
166
+ end
167
+
168
+ def create_on_parent
169
+ name = association_chain.last["name"]
170
+ if trackable_parent.class.embeds_one?(name)
171
+ trackable_parent.create_embedded(name, localize_keys(original))
172
+ elsif trackable_parent.class.embeds_many?(name)
173
+ trackable_parent.get_embedded(name).create!(localize_keys(original))
174
+ else
175
+ raise "This should never happen. Please report bug!"
176
+ end
177
+ end
178
+
179
+ def trackable_parents_and_trackable
180
+ @trackable_parents_and_trackable ||= traverse_association_chain
181
+ end
182
+
183
+ def traverse_association_chain
184
+ chain = association_chain.dup
185
+ doc = nil
186
+ documents = []
187
+ loop do
188
+ node = chain.shift
189
+ name = node['name']
190
+ doc = if doc.nil?
191
+ # root association. First element of the association chain
192
+ klass = name.classify.constantize
193
+ klass.where(_id: node['id']).first
194
+ elsif doc.class.embeds_one?(name)
195
+ doc.get_embedded(name)
196
+ elsif doc.class.embeds_many?(name)
197
+ doc.get_embedded(name).where(_id: node['id']).first
198
+ else
199
+ raise "This should never happen. Please report bug."
200
+ end
201
+ documents << doc
202
+ break if chain.empty?
203
+ end
204
+ documents
205
+ end
206
+
207
+ def localize_keys(hash)
208
+ klass = association_chain.first["name"].constantize
209
+ klass.localized_fields.keys.each do |name|
210
+ hash["#{name}_translations"] = hash.delete(name) if hash[name].present?
211
+ end if klass.respond_to?(:localized_fields)
212
+ hash
213
+ end
214
+ end
215
+ end
@@ -1,27 +1,25 @@
1
- module Mongoid
2
- module History
3
- GLOBAL_TRACK_HISTORY_FLAG = "mongoid_history_trackable_enabled"
4
-
5
- mattr_accessor :tracker_class_name
6
- mattr_accessor :trackable_class_options
7
- mattr_accessor :modifier_class_name
8
- mattr_accessor :current_user_method
9
-
10
- def self.tracker_class
11
- @tracker_class ||= tracker_class_name.to_s.classify.constantize
12
- end
13
-
14
- def self.disable(&block)
15
- begin
16
- Thread.current[GLOBAL_TRACK_HISTORY_FLAG] = false
17
- yield
18
- ensure
19
- Thread.current[GLOBAL_TRACK_HISTORY_FLAG] = true
20
- end
21
- end
22
-
23
- def self.enabled?
24
- Thread.current[GLOBAL_TRACK_HISTORY_FLAG] != false
25
- end
26
- end
27
- end
1
+ module Mongoid
2
+ module History
3
+ GLOBAL_TRACK_HISTORY_FLAG = "mongoid_history_trackable_enabled"
4
+
5
+ mattr_accessor :tracker_class_name
6
+ mattr_accessor :trackable_class_options
7
+ mattr_accessor :modifier_class_name
8
+ mattr_accessor :current_user_method
9
+
10
+ def self.tracker_class
11
+ @tracker_class ||= tracker_class_name.to_s.classify.constantize
12
+ end
13
+
14
+ def self.disable(&block)
15
+ Thread.current[GLOBAL_TRACK_HISTORY_FLAG] = false
16
+ yield
17
+ ensure
18
+ Thread.current[GLOBAL_TRACK_HISTORY_FLAG] = true
19
+ end
20
+
21
+ def self.enabled?
22
+ Thread.current[GLOBAL_TRACK_HISTORY_FLAG] != false
23
+ end
24
+ end
25
+ end
@@ -1,10 +1,9 @@
1
- require 'easy_diff'
2
-
3
- require File.expand_path(File.dirname(__FILE__) + '/mongoid/history')
4
- require File.expand_path(File.dirname(__FILE__) + '/mongoid/history/tracker')
5
- require File.expand_path(File.dirname(__FILE__) + '/mongoid/history/trackable')
6
- require File.expand_path(File.dirname(__FILE__) + '/mongoid/history/sweeper')
7
-
8
- Mongoid::History.modifier_class_name = "User"
9
- Mongoid::History.trackable_class_options = {}
10
- Mongoid::History.current_user_method ||= :current_user
1
+ require 'easy_diff'
2
+
3
+ require File.expand_path(File.dirname(__FILE__) + '/mongoid/history')
4
+ require File.expand_path(File.dirname(__FILE__) + '/mongoid/history/tracker')
5
+ require File.expand_path(File.dirname(__FILE__) + '/mongoid/history/trackable')
6
+
7
+ Mongoid::History.modifier_class_name = "User"
8
+ Mongoid::History.trackable_class_options = {}
9
+ Mongoid::History.current_user_method ||= :current_user