mongoid-history 0.3.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,22 @@
1
- 0.3.3 (1/4/2013)
1
+ 0.4.0 (6/12/2013)
2
+ -----------------
3
+
4
+ * Add Mongoid::History.disable and Mongoid::History.enabled? methods for global tracking disablement - [@johnnyshields](https://github.com/johnnyshields)
5
+ * Add `:changes_method` that optionally overrides which method to call to collect changes - [@joelnordel](https://github.com/joelnordell).
6
+ * [API Change] The `:destroy` action now stores trackers in the format `original=value, modified=nil` (previously it was the reverse) - [@johnnyshields](https://github.com/johnnyshields)
7
+ * Support for polymorphic embedded classes - [@tstepp](https://github.com/tstepp)
8
+ * Support for Mongoid field aliases, e.g. `field :n, as: :name` - [@johnnyshields](https://github.com/johnnyshields)
9
+ * Support for Mongoid embedded aliases, e.g. `embeds_many :comments, store_as: :coms` - [@johnnyshields](https://github.com/johnnyshields)
10
+ * Add `#tracked_changes` and `#tracked_edits` methods to `Tracker` class for nicer change summaries - [@johnnyshields](https://github.com/johnnyshields) and [@tstepp](https://github.com/tstepp)
11
+ * Refactored and exposed `#trackable_parent_class` in `Tracker`, which returns the class of the trackable regardless of whether the trackable itself has been destroyed - [@johnnyshields](https://github.com/johnnyshields)
12
+ * Add class-level `#tracked_field?` and `#tracked_fields` methods; refactor logic to determine whether a field is tracked - [@johnnyshields](https://github.com/johnnyshields)
13
+ * Fix bug in Trackable#track_update where `return` condition at beginning of method caused a short-circuit where memoization would not be cleared properly. - [@johnnyshields](https://github.com/johnnyshields)
14
+ * Tests: Added spec for nested embedded documents - [@matekb](https://github.com/matekb)
15
+ * Tests: Test run time cut in half (~2.5s versus ~5s) by using `#let` helper and removing class initialization before each test - [@johnnyshields](https://github.com/johnnyshields)
16
+ * Tests: Remove `database_cleaner` gem in favor of `Mongoid.purge!` - [@johnnyshields](https://github.com/johnnyshields)
17
+ * Tests: Remove dependency on non-committed file `mongoid.yml` and hardcode collection to `mongoid_history_test` - [@johnnyshields](https://github.com/johnnyshields)
18
+
19
+ 0.3.3 (4/1/2013)
2
20
  ----------------
3
21
 
4
22
  * [#42](https://github.com/aq1018/mongoid-history/issues/42) Fix: corrected creation of association chain when using nested embedded documents - [@matekb](https://github.com/matekb).
data/Gemfile CHANGED
@@ -5,9 +5,8 @@ gem "mongoid", "~> 3.0"
5
5
  gem "activesupport"
6
6
 
7
7
  group :test do
8
- gem "rspec", "~> 2.11.0"
8
+ gem "rspec", ">= 2.11.0"
9
9
  gem "yard"
10
10
  gem "bundler", ">= 1.0.0"
11
11
  gem "jeweler"
12
- gem "database_cleaner", ">= 0.8.0"
13
12
  end
data/README.md CHANGED
@@ -2,6 +2,7 @@ mongoid-history
2
2
  ===============
3
3
 
4
4
  [![Build Status](https://secure.travis-ci.org/aq1018/mongoid-history.png?branch=master)](http://travis-ci.org/aq1018/mongoid-history)
5
+ [![Code Climate](https://codeclimate.com/github/aq1018/mongoid-history.png)](https://codeclimate.com/github/aq1018/mongoid-history)
5
6
 
6
7
  Mongoid-history tracks historical changes for any document, including embedded ones. It achieves this by storing all history tracks in a single collection that you define. Embedded documents are referenced by storing an association path, which is an array of `document_name` and `document_id` fields starting from the top most parent document and down to the embedded document that should track history.
7
8
 
@@ -10,7 +11,7 @@ This gem also implements multi-user undo, which allows users to undo any history
10
11
  Stable Release
11
12
  --------------
12
13
 
13
- You're reading the documentation the 0.3.x release that supports Mongoid 3.x. For 2.x compatible mongoid-history, please use a 0.2.x version from the [2.x-stable branch](https://github.com/aq1018/mongoid-history/tree/2.4-stable).
14
+ You're reading the documentation the 0.4.x release that supports Mongoid 3.x. For 2.x compatible mongoid-history, please use a 0.2.x version from the [2.x-stable branch](https://github.com/aq1018/mongoid-history/tree/2.4-stable).
14
15
 
15
16
  Install
16
17
  -------
@@ -160,7 +161,128 @@ post.undo! user
160
161
  Comment.disable_tracking do
161
162
  comment.update_attributes(:title => "Test 3")
162
163
  end
164
+
165
+ # globally disable all history tracking
166
+ Mongoid::History.disable do
167
+ comment.update_attributes(:title => "Test 3")
168
+ user.update_attributes(:name => "Eddie Van Halen")
169
+ end
170
+ ```
171
+
172
+ **Retrieving the list of tracked fields**
173
+
174
+ ```ruby
175
+ class Book
176
+ ...
177
+ field :title
178
+ field :author
179
+ field :price
180
+ track_history :on => [:title, :price]
181
+ end
182
+
183
+ Book.tracked_fields #=> ["title", "price"]
184
+ Book.tracked_field?(:title) #=> true
185
+ Book.tracked_field?(:author) #=> false
163
186
  ```
187
+
188
+ **Displaying history trackers as an audit trail**
189
+
190
+ In your Controller:
191
+
192
+ ```ruby
193
+ # Fetch history trackers
194
+ @trackers = HistoryTracker.limit(25)
195
+
196
+ # get change set for the first tracker
197
+ @changes = @trackers.first.tracked_changes
198
+ #=> {field: {to: val1, from: val2}}
199
+
200
+ # get edit set for the first tracker
201
+ @edits = @trackers.first.tracked_changes
202
+ #=> { add: {field: val},
203
+ # remove: {field: val},
204
+ # modify: { to: val1, from: val2 },
205
+ # array: { add: [val2], remove: [val1] } }
206
+ ```
207
+
208
+ In your View, you might do something like (example in HAML format):
209
+
210
+ ```haml
211
+ %ul.changes
212
+ - (@edits[:add]||[]).each do |k,v|
213
+ %li.remove Added field #{k} value #{v}
214
+
215
+ - (@edits[:modify]||[]).each do |k,v|
216
+ %li.modify Changed field #{k} from #{v[:from]} to #{v[:to]}
217
+
218
+ - (@edits[:array]||[]).each do |k,v|
219
+ %li.modify
220
+ - if v[:remove].nil?
221
+ Changed field #{k} by adding #{v[:add]}
222
+ - elsif v[:add].nil?
223
+ Changed field #{k} by removing #{v[:remove]}
224
+ - else
225
+ Changed field #{k} by adding #{v[:add]} and removing #{v[:remove]}
226
+
227
+ - (@edits[:remove]||[]).each do |k,v|
228
+ %li.remove Removed field #{k} (was previously #{v})
229
+ ```
230
+
231
+ **Using an alternate changes method**
232
+
233
+ Sometimes you may wish to provide an alternate method for determining which changes should be tracked. For example, if you are using embedded documents
234
+ and nested attributes, you may wish to write your own changes method that includes changes from the embedded documents.
235
+
236
+ Mongoid::History provides an option named `:changes_method` which allows you to do this. It defaults to `:changes`, which is the standard changes method.
237
+
238
+ Example:
239
+
240
+ ```ruby
241
+ class Foo
242
+ include Mongoid::Document
243
+ include Mongoid::Timestamps
244
+ include Mongoid::History::Trackable
245
+
246
+ field :bar
247
+ embeds_one :baz
248
+ accepts_nested_attributes_for :baz
249
+
250
+ # use changes_with_baz to include baz's changes in this document's
251
+ # history.
252
+ track_history :changes_method => :changes_with_baz
253
+
254
+ def changes_with_baz
255
+ if baz.changed?
256
+ changes.merge( :baz => summarized_changes(baz) )
257
+ else
258
+ changes
259
+ end
260
+ end
261
+
262
+ private
263
+ # This method takes the changes from an embedded doc and formats them
264
+ # in a summarized way, similar to how the embedded doc appears in the
265
+ # parent document's attributes
266
+ def summarized_changes obj
267
+ obj.changes.keys.map do |field|
268
+ next unless obj.respond_to?("#{field}_change")
269
+ [ { field => obj.send("#{field}_change")[0] },
270
+ { field => obj.send("#{field}_change")[1] } ]
271
+ end.compact.transpose.map do |fields|
272
+ fields.inject({}) {|map,f| map.merge(f)}
273
+ end
274
+ end
275
+ end
276
+
277
+ class Baz
278
+ include Mongoid::Document
279
+ include Mongoid::Timestamps
280
+
281
+ embedded_in :foo
282
+ field :value
283
+ end
284
+ ```
285
+
164
286
  For more examples, check out [spec/integration/integration_spec.rb](https://github.com/aq1018/mongoid-history/blob/master/spec/integration/integration_spec.rb).
165
287
 
166
288
  Contributing to mongoid-history
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.3
1
+ 0.4.0
@@ -1,5 +1,7 @@
1
1
  module Mongoid
2
2
  module History
3
+ GLOBAL_TRACK_HISTORY_FLAG = "mongoid_history_trackable_enabled"
4
+
3
5
  mattr_accessor :tracker_class_name
4
6
  mattr_accessor :trackable_class_options
5
7
  mattr_accessor :modifier_class_name
@@ -9,5 +11,17 @@ module Mongoid
9
11
  @tracker_class ||= tracker_class_name.to_s.classify.constantize
10
12
  end
11
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
12
26
  end
13
27
  end
@@ -10,6 +10,7 @@ module Mongoid::History
10
10
  :except => [:created_at, :updated_at],
11
11
  :modifier_field => :modifier,
12
12
  :version_field => :version,
13
+ :changes_method => :changes,
13
14
  :scope => scope_name,
14
15
  :track_create => false,
15
16
  :track_update => true,
@@ -18,19 +19,14 @@ module Mongoid::History
18
19
 
19
20
  options = default_options.merge(options)
20
21
 
21
- # normalize except fields
22
- # manually ensure _id, id, version will not be tracked in history
22
+ # normalize :except fields to an array of database field strings
23
23
  options[:except] = [options[:except]] unless options[:except].is_a? Array
24
- options[:except] << options[:version_field]
25
- options[:except] << "#{options[:modifier_field]}_id".to_sym
26
- options[:except] += [:_id, :id]
27
- options[:except] = options[:except].map(&:to_s).flatten.compact.uniq
28
- options[:except].map(&:to_s)
24
+ options[:except] = options[:except].map{|field| database_field_name(field)}.compact.uniq
29
25
 
30
- # normalize fields to track to either :all or an array of strings
26
+ # normalize :on fields to either :all or an array of database field strings
31
27
  if options[:on] != :all
32
28
  options[:on] = [options[:on]] unless options[:on].is_a? Array
33
- options[:on] = options[:on].map(&:to_s).flatten.uniq
29
+ options[:on] = options[:on].map{|field| database_field_name(field)}.compact.uniq
34
30
  end
35
31
 
36
32
  field options[:version_field].to_sym, :type => Integer
@@ -54,8 +50,7 @@ module Mongoid::History
54
50
  end
55
51
 
56
52
  def track_history?
57
- enabled = Thread.current[track_history_flag]
58
- enabled.nil? ? true : enabled
53
+ Mongoid::History.enabled? && Thread.current[track_history_flag] != false
59
54
  end
60
55
 
61
56
  def disable_tracking(&block)
@@ -101,6 +96,14 @@ module Mongoid::History
101
96
  save!
102
97
  end
103
98
 
99
+ def get_embedded(name)
100
+ self.send(self.class.embedded_alias(name))
101
+ end
102
+
103
+ def create_embedded(name, value)
104
+ self.send("create_#{self.class.embedded_alias(name)}!", value)
105
+ end
106
+
104
107
  private
105
108
  def get_versions_criteria(options_or_version)
106
109
  if options_or_version.is_a? Hash
@@ -124,10 +127,6 @@ module Mongoid::History
124
127
  versions.desc(:version)
125
128
  end
126
129
 
127
- def should_track_update?
128
- track_history? && !modified_attributes_for_update.blank?
129
- end
130
-
131
130
  def traverse_association_chain(node=self)
132
131
  list = node._parent ? traverse_association_chain(node._parent) : []
133
132
  list << association_hash(node)
@@ -141,7 +140,7 @@ module Mongoid::History
141
140
  # the child to parent (embedded_in, belongs_to) relation will be defined
142
141
  if node._parent
143
142
  meta = node._parent.relations.values.select do |relation|
144
- relation.class_name == node.class.to_s
143
+ relation.class_name == node.metadata.class_name.to_s
145
144
  end.first
146
145
  end
147
146
 
@@ -151,81 +150,68 @@ module Mongoid::History
151
150
  ActiveSupport::OrderedHash['name', name, 'id', node.id]
152
151
  end
153
152
 
154
- def modified_attributes_for_update
155
- @modified_attributes_for_update ||= if history_trackable_options[:on] == :all
156
- changes.reject do |k, v|
157
- history_trackable_options[:except].include?(k)
158
- end
159
- else
160
- changes.reject do |k, v|
161
- !history_trackable_options[:on].include?(k)
162
- end
163
-
153
+ # Returns a Hash of field name to pairs of original and modified values
154
+ # for each tracked field for a given action.
155
+ #
156
+ # @param [ String | Symbol ] action The modification action (:create, :update, :destroy)
157
+ #
158
+ # @return [ Hash<String, Array<Object>> ] the pairs of original and modified
159
+ # values for each field
160
+ def modified_attributes_for_action(action)
161
+ case action.to_sym
162
+ when :destroy then modified_attributes_for_destroy
163
+ when :create then modified_attributes_for_create
164
+ else modified_attributes_for_update
164
165
  end
165
166
  end
166
167
 
168
+ def modified_attributes_for_update
169
+ @modified_attributes_for_update ||= self.send(history_trackable_options[:changes_method]).select{|k, v| self.class.tracked_field?(k, :update)}
170
+ end
171
+
167
172
  def modified_attributes_for_create
168
- @modified_attributes_for_create ||= attributes.inject({}) do |h, pair|
169
- k,v = pair
173
+ @modified_attributes_for_create ||= attributes.inject({}) do |h,(k,v)|
170
174
  h[k] = [nil, v]
171
175
  h
172
- end.reject do |k, v|
173
- history_trackable_options[:except].include?(k)
174
- end
176
+ end.select{|k, v| self.class.tracked_field?(k, :create)}
175
177
  end
176
178
 
177
179
  def modified_attributes_for_destroy
178
- @modified_attributes_for_destroy ||= attributes.inject({}) do |h, pair|
179
- k,v = pair
180
- h[k] = [nil, v]
180
+ @modified_attributes_for_destroy ||= attributes.inject({}) do |h,(k,v)|
181
+ h[k] = [v, nil]
181
182
  h
182
- end
183
+ end.select{|k, v| self.class.tracked_field?(k, :destroy)}
183
184
  end
184
185
 
185
- def history_tracker_attributes(method)
186
+ def history_tracker_attributes(action)
186
187
  return @history_tracker_attributes if @history_tracker_attributes
187
188
 
188
189
  @history_tracker_attributes = {
189
190
  :association_chain => traverse_association_chain,
190
191
  :scope => history_trackable_options[:scope],
191
- :modifier => send(history_trackable_options[:modifier_field])
192
+ :modifier => send(history_trackable_options[:modifier_field])
192
193
  }
193
194
 
194
- original, modified = transform_changes(case method
195
- when :destroy then modified_attributes_for_destroy
196
- when :create then modified_attributes_for_create
197
- else modified_attributes_for_update
198
- end)
195
+ original, modified = transform_changes(modified_attributes_for_action(action))
199
196
 
200
197
  @history_tracker_attributes[:original] = original
201
198
  @history_tracker_attributes[:modified] = modified
202
199
  @history_tracker_attributes
203
200
  end
204
201
 
205
- def track_update
206
- return unless should_track_update?
207
- current_version = (self.send(history_trackable_options[:version_field]) || 0 ) + 1
208
- self.send("#{history_trackable_options[:version_field]}=", current_version)
209
- Mongoid::History.tracker_class.create!(history_tracker_attributes(:update).merge(:version => current_version, :action => "update", :trackable => self))
210
- clear_memoization
202
+ def track_create
203
+ track_history_for_action(:create)
211
204
  end
212
205
 
213
- def track_create
214
- return unless track_history?
215
- current_version = (self.send(history_trackable_options[:version_field]) || 0 ) + 1
216
- self.send("#{history_trackable_options[:version_field]}=", current_version)
217
- Mongoid::History.tracker_class.create!(history_tracker_attributes(:create).merge(:version => current_version, :action => "create", :trackable => self))
218
- clear_memoization
206
+ def track_update
207
+ track_history_for_action(:update)
219
208
  end
220
209
 
221
210
  def track_destroy
222
- return unless track_history?
223
- current_version = (self.send(history_trackable_options[:version_field]) || 0 ) + 1
224
- Mongoid::History.tracker_class.create!(history_tracker_attributes(:destroy).merge(:version => current_version, :action => "destroy", :trackable => self))
225
- clear_memoization
211
+ track_history_for_action(:destroy)
226
212
  end
227
213
 
228
- def clear_memoization
214
+ def clear_trackable_memoization
229
215
  @history_tracker_attributes = nil
230
216
  @modified_attributes_for_create = nil
231
217
  @modified_attributes_for_update = nil
@@ -244,12 +230,109 @@ module Mongoid::History
244
230
  [ original, modified ]
245
231
  end
246
232
 
233
+ protected
234
+
235
+ def track_history_for_action?(action)
236
+ track_history? && !(action.to_sym == :update && modified_attributes_for_update.blank?)
237
+ end
238
+
239
+ def track_history_for_action(action)
240
+ if track_history_for_action?(action)
241
+ current_version = (self.send(history_trackable_options[:version_field]) || 0 ) + 1
242
+ self.send("#{history_trackable_options[:version_field]}=", current_version)
243
+ Mongoid::History.tracker_class.create!(history_tracker_attributes(action.to_sym).merge(version: current_version, action: action.to_s, trackable: self))
244
+ end
245
+ clear_trackable_memoization
246
+ end
247
247
  end
248
248
 
249
249
  module SingletonMethods
250
+
251
+ # Whether or not the field should be tracked.
252
+ #
253
+ # @param [ String | Symbol ] field The name or alias of the field
254
+ # @param [ String | Symbol ] action The optional action name (:create, :update, or :destroy)
255
+ #
256
+ # @return [ Boolean ] whether or not the field is tracked for the given action
257
+ def tracked_field?(field, action = :update)
258
+ tracked_fields_for_action(action).include? database_field_name(field)
259
+ end
260
+
261
+ # Retrieves the list of tracked fields for a given action.
262
+ #
263
+ # @param [ String | Symbol ] action The action name (:create, :update, or :destroy)
264
+ #
265
+ # @return [ Array < String > ] the list of tracked fields for the given action
266
+ def tracked_fields_for_action(action)
267
+ case action.to_sym
268
+ when :destroy then tracked_fields + reserved_tracked_fields
269
+ else tracked_fields
270
+ end
271
+ end
272
+
273
+ # Retrieves the memoized base list of tracked fields, excluding reserved fields.
274
+ #
275
+ # @return [ Array < String > ] the base list of tracked database field names
276
+ def tracked_fields
277
+ @tracked_fields ||= self.fields.keys.select do |field|
278
+ h = history_trackable_options
279
+ (h[:on]==:all || h[:on].include?(field)) && !h[:except].include?(field)
280
+ end - reserved_tracked_fields
281
+ end
282
+
283
+ # Retrieves the memoized list of reserved tracked fields, which are only included for certain actions.
284
+ #
285
+ # @return [ Array < String > ] the list of reserved database field names
286
+ def reserved_tracked_fields
287
+ @reserved_tracked_fields ||= ["_id", history_trackable_options[:version_field].to_s, "#{history_trackable_options[:modifier_field]}_id"]
288
+ end
289
+
250
290
  def history_trackable_options
251
291
  @history_trackable_options ||= Mongoid::History.trackable_class_options[self.collection_name.to_s.singularize.to_sym]
252
292
  end
293
+
294
+ # Indicates whether there is an Embedded::One relation for the given embedded field.
295
+ #
296
+ # @param [ String | Symbol ] embed The name of the embedded field
297
+ #
298
+ # @return [ Boolean ] true if there is an Embedded::One relation for the given embedded field
299
+ def embeds_one?(embed)
300
+ relation_of(embed) == Mongoid::Relations::Embedded::One
301
+ end
302
+
303
+ # Indicates whether there is an Embedded::Many relation for the given embedded field.
304
+ #
305
+ # @param [ String | Symbol ] embed The name of the embedded field
306
+ #
307
+ # @return [ Boolean ] true if there is an Embedded::Many relation for the given embedded field
308
+ def embeds_many?(embed)
309
+ relation_of(embed) == Mongoid::Relations::Embedded::Many
310
+ end
311
+
312
+ # Retrieves the database representation of an embedded field name, in case the :store_as option is used.
313
+ #
314
+ # @param [ String | Symbol ] embed The name or alias of the embedded field
315
+ #
316
+ # @return [ String ] the database name of the embedded field
317
+ def embedded_alias(embed)
318
+ embedded_aliases[embed]
319
+ end
320
+
321
+ protected
322
+
323
+ # Retrieves the memoized hash of embedded aliases and their associated database representations.
324
+ #
325
+ # @return [ Hash < String, String > ] hash of embedded aliases (keys) to database representations (values)
326
+ def embedded_aliases
327
+ @embedded_aliases ||= relations.inject(HashWithIndifferentAccess.new) do |h,(k,v)|
328
+ h[v[:store_as]||k]=k; h
329
+ end
330
+ end
331
+
332
+ def relation_of(embed)
333
+ meta = reflect_on_association(embedded_alias(embed))
334
+ meta ? meta.relation : nil
335
+ end
253
336
  end
254
337
  end
255
338
  end