mongoid-history 0.3.3 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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