paper_trail 4.0.2 → 4.1.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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -0
  3. data/CHANGELOG.md +27 -0
  4. data/CONTRIBUTING.md +78 -5
  5. data/README.md +328 -268
  6. data/doc/bug_report_template.rb +65 -0
  7. data/gemfiles/3.0.gemfile +7 -4
  8. data/lib/generators/paper_trail/templates/add_object_changes_to_versions.rb +1 -1
  9. data/lib/generators/paper_trail/templates/create_versions.rb +14 -0
  10. data/lib/paper_trail.rb +11 -9
  11. data/lib/paper_trail/attributes_serialization.rb +89 -0
  12. data/lib/paper_trail/cleaner.rb +8 -1
  13. data/lib/paper_trail/config.rb +15 -18
  14. data/lib/paper_trail/frameworks/rails/controller.rb +16 -2
  15. data/lib/paper_trail/has_paper_trail.rb +102 -99
  16. data/lib/paper_trail/record_history.rb +59 -0
  17. data/lib/paper_trail/reifier.rb +270 -0
  18. data/lib/paper_trail/version_association_concern.rb +3 -1
  19. data/lib/paper_trail/version_concern.rb +60 -226
  20. data/lib/paper_trail/version_number.rb +2 -2
  21. data/paper_trail.gemspec +7 -10
  22. data/spec/models/animal_spec.rb +17 -0
  23. data/spec/models/callback_modifier_spec.rb +96 -0
  24. data/spec/models/json_version_spec.rb +20 -17
  25. data/spec/paper_trail/config_spec.rb +52 -0
  26. data/spec/spec_helper.rb +6 -0
  27. data/test/dummy/app/models/callback_modifier.rb +45 -0
  28. data/test/dummy/app/models/chapter.rb +9 -0
  29. data/test/dummy/app/models/citation.rb +5 -0
  30. data/test/dummy/app/models/paragraph.rb +5 -0
  31. data/test/dummy/app/models/quotation.rb +5 -0
  32. data/test/dummy/app/models/section.rb +6 -0
  33. data/test/dummy/config/database.postgres.yml +1 -1
  34. data/test/dummy/config/initializers/paper_trail.rb +3 -1
  35. data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +33 -0
  36. data/test/dummy/db/schema.rb +27 -0
  37. data/test/test_helper.rb +36 -0
  38. data/test/unit/associations_test.rb +726 -0
  39. data/test/unit/inheritance_column_test.rb +6 -6
  40. data/test/unit/model_test.rb +62 -594
  41. data/test/unit/protected_attrs_test.rb +3 -2
  42. data/test/unit/version_test.rb +87 -69
  43. metadata +38 -2
@@ -0,0 +1,59 @@
1
+ module PaperTrail
2
+
3
+ # Represents the history of a single record.
4
+ # @api private
5
+ class RecordHistory
6
+
7
+ # @param versions - ActiveRecord::Relation - All versions of the record.
8
+ # @param version_class - Class - Usually PaperTrail::Version,
9
+ # but it could also be a custom version class.
10
+ # @api private
11
+ def initialize(versions, version_class)
12
+ @versions = versions
13
+ @version_class = version_class
14
+ end
15
+
16
+ # Returns ordinal position of `version` in `sequence`.
17
+ # @api private
18
+ def index(version)
19
+ sequence.index(version)
20
+ end
21
+
22
+ private
23
+
24
+ # Returns `@versions` in chronological order.
25
+ # @api private
26
+ def sequence
27
+ if @version_class.primary_key_is_int?
28
+ @versions.select(primary_key).order(primary_key.asc)
29
+ else
30
+ @versions.
31
+ select([timestamp, primary_key]).
32
+ order(@version_class.timestamp_sort_order)
33
+ end
34
+ end
35
+
36
+ # @return - Arel::Attribute - Attribute representing the primary key
37
+ # of the version table. The column's data type is usually a serial
38
+ # integer (the rails convention) but not always.
39
+ # @api private
40
+ def primary_key
41
+ table[@version_class.primary_key]
42
+ end
43
+
44
+ # @return - Arel::Table - The version table, usually named `versions`, but
45
+ # not always.
46
+ # @api private
47
+ def table
48
+ @version_class.arel_table
49
+ end
50
+
51
+ # @return - Arel::Attribute - Attribute representing the timestamp column
52
+ # of the version table, usually named `created_at` (the rails convention)
53
+ # but not always.
54
+ # @api private
55
+ def timestamp
56
+ table[PaperTrail.timestamp_field]
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,270 @@
1
+ module PaperTrail
2
+
3
+ # Given a version record and some options, builds a new model object.
4
+ # @api private
5
+ module Reifier
6
+ class << self
7
+
8
+ # See `VersionConcern#reify` for documentation.
9
+ # @api private
10
+ def reify(version, options)
11
+ options = options.dup
12
+
13
+ options.reverse_merge!(
14
+ :version_at => version.created_at,
15
+ :mark_for_destruction => false,
16
+ :has_one => false,
17
+ :has_many => false,
18
+ :unversioned_attributes => :nil
19
+ )
20
+
21
+ attrs = version.class.object_col_is_json? ?
22
+ version.object :
23
+ PaperTrail.serializer.load(version.object)
24
+
25
+ # Normally a polymorphic belongs_to relationship allows us to get the
26
+ # object we belong to by calling, in this case, `item`. However this
27
+ # returns nil if `item` has been destroyed, and we need to be able to
28
+ # retrieve destroyed objects.
29
+ #
30
+ # In this situation we constantize the `item_type` to get hold of the
31
+ # class...except when the stored object's attributes include a `type`
32
+ # key. If this is the case, the object we belong to is using single
33
+ # table inheritance and the `item_type` will be the base class, not the
34
+ # actual subclass. If `type` is present but empty, the class is the base
35
+ # class.
36
+
37
+ if options[:dup] != true && version.item
38
+ model = version.item
39
+ # Look for attributes that exist in the model and not in this
40
+ # version. These attributes should be set to nil.
41
+ if options[:unversioned_attributes] == :nil
42
+ (model.attribute_names - attrs.keys).each { |k| attrs[k] = nil }
43
+ end
44
+ else
45
+ inheritance_column_name = version.item_type.constantize.inheritance_column
46
+ class_name = attrs[inheritance_column_name].blank? ?
47
+ version.item_type :
48
+ attrs[inheritance_column_name]
49
+ klass = class_name.constantize
50
+ # The `dup` option always returns a new object, otherwise we should
51
+ # attempt to look for the item outside of default scope(s).
52
+ if options[:dup] || (_item = klass.unscoped.find_by_id(version.item_id)).nil?
53
+ model = klass.new
54
+ elsif options[:unversioned_attributes] == :nil
55
+ model = _item
56
+ # Look for attributes that exist in the model and not in this
57
+ # version. These attributes should be set to nil.
58
+ (model.attribute_names - attrs.keys).each { |k| attrs[k] = nil }
59
+ end
60
+ end
61
+
62
+ model.class.unserialize_attributes_for_paper_trail! attrs
63
+
64
+ # Set all the attributes in this version on the model.
65
+ attrs.each do |k, v|
66
+ if model.has_attribute?(k)
67
+ model[k.to_sym] = v
68
+ elsif model.respond_to?("#{k}=")
69
+ model.send("#{k}=", v)
70
+ else
71
+ version.logger.warn(
72
+ "Attribute #{k} does not exist on #{version.item_type} (Version id: #{version.id})."
73
+ )
74
+ end
75
+ end
76
+
77
+ model.send "#{model.class.version_association_name}=", version
78
+
79
+ unless options[:has_one] == false
80
+ reify_has_ones version.transaction_id, model, options
81
+ end
82
+
83
+ unless options[:has_many] == false
84
+ reify_has_manys version.transaction_id, model, options
85
+ end
86
+
87
+ model
88
+ end
89
+
90
+ private
91
+
92
+ # Replaces each record in `array` with its reified version, if present
93
+ # in `versions`.
94
+ #
95
+ # @api private
96
+ # @param array - The collection to be modified.
97
+ # @param options
98
+ # @param versions - A `Hash` mapping IDs to `Version`s
99
+ # @return nil - Always returns `nil`
100
+ #
101
+ # Once modified by this method, `array` will be assigned to the
102
+ # AR association currently being reified.
103
+ #
104
+ def prepare_array_for_has_many(array, options, versions)
105
+ # Iterate each child to replace it with the previous value if there is
106
+ # a version after the timestamp.
107
+ array.map! do |record|
108
+ if (version = versions.delete(record.id)).nil?
109
+ record
110
+ elsif version.event == 'create'
111
+ options[:mark_for_destruction] ? record.tap { |r| r.mark_for_destruction } : nil
112
+ else
113
+ version.reify(options.merge(:has_many => false, :has_one => false))
114
+ end
115
+ end
116
+
117
+ # Reify the rest of the versions and add them to the collection, these
118
+ # versions are for those that have been removed from the live
119
+ # associations.
120
+ array.concat(
121
+ versions.values.map { |v|
122
+ v.reify(options.merge(:has_many => false, :has_one => false))
123
+ }
124
+ )
125
+
126
+ array.compact!
127
+
128
+ nil
129
+ end
130
+
131
+ # Restore the `model`'s has_one associations as they were when this
132
+ # version was superseded by the next (because that's what the user was
133
+ # looking at when they made the change).
134
+ def reify_has_ones(transaction_id, model, options = {})
135
+ version_table_name = model.class.paper_trail_version_class.table_name
136
+ model.class.reflect_on_all_associations(:has_one).each do |assoc|
137
+ if assoc.klass.paper_trail_enabled_for_model?
138
+ version = model.class.paper_trail_version_class.joins(:version_associations).
139
+ where("version_associations.foreign_key_name = ?", assoc.foreign_key).
140
+ where("version_associations.foreign_key_id = ?", model.id).
141
+ where("#{version_table_name}.item_type = ?", assoc.class_name).
142
+ where("created_at >= ? OR transaction_id = ?", options[:version_at], transaction_id).
143
+ order("#{version_table_name}.id ASC").
144
+ first
145
+ if version
146
+ if version.event == 'create'
147
+ if options[:mark_for_destruction]
148
+ model.send(assoc.name).mark_for_destruction if model.send(assoc.name, true)
149
+ else
150
+ model.appear_as_new_record do
151
+ model.send "#{assoc.name}=", nil
152
+ end
153
+ end
154
+ else
155
+ child = version.reify(options.merge(:has_many => false, :has_one => false))
156
+ model.appear_as_new_record do
157
+ model.send "#{assoc.name}=", child
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
164
+
165
+ # Restore the `model`'s has_many associations as they were at version_at
166
+ # timestamp We lookup the first child versions after version_at timestamp or
167
+ # in same transaction.
168
+ def reify_has_manys(transaction_id, model, options = {})
169
+ assoc_has_many_through, assoc_has_many_directly =
170
+ model.class.reflect_on_all_associations(:has_many).
171
+ partition { |assoc| assoc.options[:through] }
172
+ reify_has_many_directly(transaction_id, assoc_has_many_directly, model, options)
173
+ reify_has_many_through(transaction_id, assoc_has_many_through, model, options)
174
+ end
175
+
176
+ # Restore the `model`'s has_many associations not associated through
177
+ # another association.
178
+ def reify_has_many_directly(transaction_id, associations, model, options = {})
179
+ version_table_name = model.class.paper_trail_version_class.table_name
180
+ associations.each do |assoc|
181
+ next unless assoc.klass.paper_trail_enabled_for_model?
182
+ version_id_subquery = PaperTrail::VersionAssociation.
183
+ joins(model.class.version_association_name).
184
+ select("MIN(version_id)").
185
+ where("foreign_key_name = ?", assoc.foreign_key).
186
+ where("foreign_key_id = ?", model.id).
187
+ where("#{version_table_name}.item_type = ?", assoc.class_name).
188
+ where("created_at >= ? OR transaction_id = ?", options[:version_at], transaction_id).
189
+ group("item_id").
190
+ to_sql
191
+ versions = versions_by_id(model.class, version_id_subquery)
192
+ collection = Array.new model.send(assoc.name, true) # pass true to avoid cache
193
+ prepare_array_for_has_many(collection, options, versions)
194
+ model.send(assoc.name).proxy_association.target = collection
195
+ end
196
+ end
197
+
198
+ # Restore the `model`'s has_many associations through another association.
199
+ # This must be called after the direct has_manys have been reified
200
+ # (reify_has_many_directly).
201
+ def reify_has_many_through(transaction_id, associations, model, options = {})
202
+ associations.each do |assoc|
203
+ next unless assoc.klass.paper_trail_enabled_for_model?
204
+
205
+ # Load the collection of through-models. For example, if `model` is a
206
+ # Chapter, having many Paragraphs through Sections, then
207
+ # `through_collection` will contain Sections.
208
+ through_collection = model.send(assoc.options[:through])
209
+
210
+ # Examine the `source_reflection`, i.e. the "source" of `assoc` the
211
+ # `ThroughReflection`. The source can be a `BelongsToReflection`
212
+ # or a `HasManyReflection`.
213
+ #
214
+ # If the association is a has_many association again, then call
215
+ # reify_has_manys for each record in `through_collection`.
216
+ if !assoc.source_reflection.belongs_to? && through_collection.present?
217
+ through_collection.each do |through_model|
218
+ reify_has_manys(transaction_id, through_model, options)
219
+ end
220
+
221
+ # At this point, the "through" part of the association chain has
222
+ # been reified, but not the final, "target" part. To continue our
223
+ # example, `model.sections` (including `model.sections.paragraphs`)
224
+ # has been loaded. However, the final "target" part of the
225
+ # association, that is, `model.paragraphs`, has not been loaded. So,
226
+ # we do that now.
227
+ collection = through_collection.map { |through_model|
228
+ through_model.send(assoc.name.to_sym).to_a
229
+ }.flatten
230
+ else
231
+ collection_keys = through_collection.map { |through_model|
232
+ through_model.send(assoc.association_foreign_key)
233
+ }
234
+
235
+ version_id_subquery = assoc.klass.paper_trail_version_class.
236
+ select("MIN(id)").
237
+ where("item_type = ?", assoc.class_name).
238
+ where("item_id IN (?)", collection_keys).
239
+ where("created_at >= ? OR transaction_id = ?", options[:version_at], transaction_id).
240
+ group("item_id").
241
+ to_sql
242
+ versions = versions_by_id(assoc.klass, version_id_subquery)
243
+ collection = Array.new assoc.klass.where(assoc.klass.primary_key => collection_keys)
244
+ prepare_array_for_has_many(collection, options, versions)
245
+ end
246
+
247
+ # To continue our example above, assign to `model.paragraphs` the
248
+ # `collection` (an array of `Paragraph`s).
249
+ model.send(assoc.name).proxy_association.target = collection
250
+ end
251
+ end
252
+
253
+ # Given a SQL fragment that identifies the IDs of version records,
254
+ # returns a `Hash` mapping those IDs to `Version`s.
255
+ #
256
+ # @api private
257
+ # @param klass - An ActiveRecord class.
258
+ # @param version_id_subquery - String. A SQL subquery that selects
259
+ # the IDs of version records.
260
+ # @return A `Hash` mapping IDs to `Version`s
261
+ #
262
+ def versions_by_id(klass, version_id_subquery)
263
+ klass.
264
+ paper_trail_version_class.
265
+ where("id IN (#{version_id_subquery})").
266
+ inject({}) { |acc, v| acc.merge!(v.item_id => v) }
267
+ end
268
+ end
269
+ end
270
+ end
@@ -7,7 +7,9 @@ module PaperTrail
7
7
  included do
8
8
  belongs_to :version
9
9
 
10
- attr_accessible :version_id, :foreign_key_name, :foreign_key_id if PaperTrail.active_record_protected_attributes?
10
+ if PaperTrail.active_record_protected_attributes?
11
+ attr_accessible :version_id, :foreign_key_name, :foreign_key_id
12
+ end
11
13
  end
12
14
  end
13
15
  end
@@ -18,7 +18,16 @@ module PaperTrail
18
18
  validates_presence_of :event
19
19
 
20
20
  if PaperTrail.active_record_protected_attributes?
21
- attr_accessible :item_type, :item_id, :event, :whodunnit, :object, :object_changes, :transaction_id, :created_at
21
+ attr_accessible(
22
+ :item_type,
23
+ :item_id,
24
+ :event,
25
+ :whodunnit,
26
+ :object,
27
+ :object_changes,
28
+ :transaction_id,
29
+ :created_at
30
+ )
22
31
  end
23
32
 
24
33
  after_create :enforce_version_limit!
@@ -47,8 +56,13 @@ module PaperTrail
47
56
  where 'event <> ?', 'create'
48
57
  end
49
58
 
50
- # Expects `obj` to be an instance of `PaperTrail::Version` by default,
51
- # but can accept a timestamp if `timestamp_arg` receives `true`
59
+ # Returns versions after `obj`.
60
+ #
61
+ # @param obj - a `Version` or a timestamp
62
+ # @param timestamp_arg - boolean - When true, `obj` is a timestamp.
63
+ # Default: false.
64
+ # @return `ActiveRecord::Relation`
65
+ # @api public
52
66
  def subsequent(obj, timestamp_arg = false)
53
67
  if timestamp_arg != true && self.primary_key_is_int?
54
68
  return where(arel_table[primary_key].gt(obj.id)).order(arel_table[primary_key].asc)
@@ -58,6 +72,13 @@ module PaperTrail
58
72
  where(arel_table[PaperTrail.timestamp_field].gt(obj)).order(self.timestamp_sort_order)
59
73
  end
60
74
 
75
+ # Returns versions before `obj`.
76
+ #
77
+ # @param obj - a `Version` or a timestamp
78
+ # @param timestamp_arg - boolean - When true, `obj` is a timestamp.
79
+ # Default: false.
80
+ # @return `ActiveRecord::Relation`
81
+ # @api public
61
82
  def preceding(obj, timestamp_arg = false)
62
83
  if timestamp_arg != true && self.primary_key_is_int?
63
84
  return where(arel_table[primary_key].lt(obj.id)).order(arel_table[primary_key].desc)
@@ -67,7 +88,6 @@ module PaperTrail
67
88
  where(arel_table[PaperTrail.timestamp_field].lt(obj)).order(self.timestamp_sort_order('desc'))
68
89
  end
69
90
 
70
-
71
91
  def between(start_time, end_time)
72
92
  where(
73
93
  arel_table[PaperTrail.timestamp_field].gt(start_time).
@@ -182,79 +202,8 @@ module PaperTrail
182
202
  #
183
203
  def reify(options = {})
184
204
  return nil if object.nil?
185
-
186
205
  without_identity_map do
187
- options.reverse_merge!(
188
- :version_at => created_at,
189
- :mark_for_destruction => false,
190
- :has_one => false,
191
- :has_many => false,
192
- :unversioned_attributes => :nil
193
- )
194
-
195
- attrs = self.class.object_col_is_json? ? object : PaperTrail.serializer.load(object)
196
-
197
- # Normally a polymorphic belongs_to relationship allows us to get the
198
- # object we belong to by calling, in this case, `item`. However this
199
- # returns nil if `item` has been destroyed, and we need to be able to
200
- # retrieve destroyed objects.
201
- #
202
- # In this situation we constantize the `item_type` to get hold of the
203
- # class...except when the stored object's attributes include a `type`
204
- # key. If this is the case, the object we belong to is using single
205
- # table inheritance and the `item_type` will be the base class, not the
206
- # actual subclass. If `type` is present but empty, the class is the base
207
- # class.
208
-
209
- if options[:dup] != true && item
210
- model = item
211
- # Look for attributes that exist in the model and not in this
212
- # version. These attributes should be set to nil.
213
- if options[:unversioned_attributes] == :nil
214
- (model.attribute_names - attrs.keys).each { |k| attrs[k] = nil }
215
- end
216
- else
217
- inheritance_column_name = item_type.constantize.inheritance_column
218
- class_name = attrs[inheritance_column_name].blank? ? item_type : attrs[inheritance_column_name]
219
- klass = class_name.constantize
220
- # The `dup` option always returns a new object, otherwise we should
221
- # attempt to look for the item outside of default scope(s).
222
- if options[:dup] || (_item = klass.unscoped.find_by_id(item_id)).nil?
223
- model = klass.new
224
- elsif options[:unversioned_attributes] == :nil
225
- model = _item
226
- # Look for attributes that exist in the model and not in this
227
- # version. These attributes should be set to nil.
228
- (model.attribute_names - attrs.keys).each { |k| attrs[k] = nil }
229
- end
230
- end
231
-
232
- if PaperTrail.serialized_attributes?
233
- model.class.unserialize_attributes_for_paper_trail! attrs
234
- end
235
-
236
- # Set all the attributes in this version on the model.
237
- attrs.each do |k, v|
238
- if model.has_attribute?(k)
239
- model[k.to_sym] = v
240
- elsif model.respond_to?("#{k}=")
241
- model.send("#{k}=", v)
242
- else
243
- logger.warn "Attribute #{k} does not exist on #{item_type} (Version id: #{id})."
244
- end
245
- end
246
-
247
- model.send "#{model.class.version_association_name}=", self
248
-
249
- unless options[:has_one] == false
250
- reify_has_ones model, options
251
- end
252
-
253
- unless options[:has_many] == false
254
- reify_has_manys model, options
255
- end
256
-
257
- model
206
+ ::PaperTrail::Reifier.reify(self, options)
258
207
  end
259
208
  end
260
209
 
@@ -263,15 +212,7 @@ module PaperTrail
263
212
  # not have an `object_changes` text column.
264
213
  def changeset
265
214
  return nil unless self.class.column_names.include? 'object_changes'
266
-
267
- _changes = self.class.object_changes_col_is_json? ? object_changes : PaperTrail.serializer.load(object_changes)
268
- @changeset ||= HashWithIndifferentAccess.new(_changes).tap do |changes|
269
- if PaperTrail.serialized_attributes?
270
- item_type.constantize.unserialize_attribute_changes_for_paper_trail!(changes)
271
- end
272
- end
273
- rescue
274
- {}
215
+ @changeset ||= load_changeset
275
216
  end
276
217
 
277
218
  # Returns who put the item into the state stored in this version.
@@ -292,8 +233,10 @@ module PaperTrail
292
233
  alias_method :version_author, :terminator
293
234
 
294
235
  def sibling_versions(reload = false)
295
- @sibling_versions = nil if reload == true
296
- @sibling_versions ||= self.class.with_item_keys(item_type, item_id)
236
+ if reload || @sibling_versions.nil?
237
+ @sibling_versions = self.class.with_item_keys(item_type, item_id)
238
+ end
239
+ @sibling_versions
297
240
  end
298
241
 
299
242
  def next
@@ -304,21 +247,37 @@ module PaperTrail
304
247
  @previous ||= sibling_versions.preceding(self).first
305
248
  end
306
249
 
250
+ # Returns an integer representing the chronological position of the
251
+ # version among its siblings (see `sibling_versions`). The "create" event,
252
+ # for example, has an index of 0.
253
+ # @api public
307
254
  def index
308
- table = self.class.arel_table unless @index
309
- @index ||=
310
- if self.class.primary_key_is_int?
311
- sibling_versions.select(table[self.class.primary_key]).order(table[self.class.primary_key].asc).index(self)
312
- else
313
- sibling_versions.select([table[PaperTrail.timestamp_field], table[self.class.primary_key]]).
314
- order(self.class.timestamp_sort_order).index(self)
315
- end
255
+ @index ||= RecordHistory.new(sibling_versions, self.class).index(self)
316
256
  end
317
257
 
318
258
  private
319
259
 
260
+ # @api private
261
+ def load_changeset
262
+ changes = HashWithIndifferentAccess.new(object_changes_deserialized)
263
+ item_type.constantize.unserialize_attribute_changes_for_paper_trail!(changes)
264
+ changes
265
+ rescue # TODO: Rescue something specific
266
+ {}
267
+ end
268
+
269
+ # @api private
270
+ def object_changes_deserialized
271
+ if self.class.object_changes_col_is_json?
272
+ object_changes
273
+ else
274
+ PaperTrail.serializer.load(object_changes)
275
+ end
276
+ end
277
+
320
278
  # In Rails 3.1+, calling reify on a previous version confuses the
321
279
  # IdentityMap, if enabled. This prevents insertion into the map.
280
+ # @api private
322
281
  def without_identity_map(&block)
323
282
  if defined?(::ActiveRecord::IdentityMap) && ::ActiveRecord::IdentityMap.respond_to?(:without)
324
283
  ::ActiveRecord::IdentityMap.without(&block)
@@ -327,141 +286,16 @@ module PaperTrail
327
286
  end
328
287
  end
329
288
 
330
- # Restore the `model`'s has_one associations as they were when this
331
- # version was superseded by the next (because that's what the user was
332
- # looking at when they made the change).
333
- def reify_has_ones(model, options = {})
334
- version_table_name = model.class.paper_trail_version_class.table_name
335
- model.class.reflect_on_all_associations(:has_one).each do |assoc|
336
- if assoc.klass.paper_trail_enabled_for_model?
337
- version = model.class.paper_trail_version_class.joins(:version_associations).
338
- where("version_associations.foreign_key_name = ?", assoc.foreign_key).
339
- where("version_associations.foreign_key_id = ?", model.id).
340
- where("#{version_table_name}.item_type = ?", assoc.class_name).
341
- where("created_at >= ? OR transaction_id = ?", options[:version_at], transaction_id).
342
- order("#{version_table_name}.id ASC").first
343
- if version
344
- if version.event == 'create'
345
- if options[:mark_for_destruction]
346
- model.send(assoc.name).mark_for_destruction if model.send(assoc.name, true)
347
- else
348
- model.appear_as_new_record do
349
- model.send "#{assoc.name}=", nil
350
- end
351
- end
352
- else
353
- child = version.reify(options.merge(:has_many => false, :has_one => false))
354
- model.appear_as_new_record do
355
- model.send "#{assoc.name}=", child
356
- end
357
- end
358
- end
359
- end
360
- end
361
- end
362
-
363
- # Restore the `model`'s has_many associations as they were at version_at
364
- # timestamp We lookup the first child versions after version_at timestamp or
365
- # in same transaction.
366
- def reify_has_manys(model, options = {})
367
- assoc_has_many_through, assoc_has_many_directly =
368
- model.class.reflect_on_all_associations(:has_many).
369
- partition { |assoc| assoc.options[:through] }
370
- reify_has_many_directly(assoc_has_many_directly, model, options)
371
- reify_has_many_through(assoc_has_many_through, model, options)
372
- end
373
-
374
- # Restore the `model`'s has_many associations not associated through
375
- # another association.
376
- def reify_has_many_directly(associations, model, options = {})
377
- version_table_name = model.class.paper_trail_version_class.table_name
378
- associations.each do |assoc|
379
- next unless assoc.klass.paper_trail_enabled_for_model?
380
- version_id_subquery = PaperTrail::VersionAssociation.joins(model.class.version_association_name).
381
- select("MIN(version_id)").
382
- where("foreign_key_name = ?", assoc.foreign_key).
383
- where("foreign_key_id = ?", model.id).
384
- where("#{version_table_name}.item_type = ?", assoc.class_name).
385
- where("created_at >= ? OR transaction_id = ?", options[:version_at], transaction_id).
386
- group("item_id").to_sql
387
- versions = model.class.paper_trail_version_class.where("id IN (#{version_id_subquery})").inject({}) do |acc, v|
388
- acc.merge!(v.item_id => v)
389
- end
390
-
391
- # Pass true to force the model to load.
392
- collection = Array.new model.send(assoc.name, true)
393
-
394
- # Iterate each child to replace it with the previous value if there is
395
- # a version after the timestamp.
396
- collection.map! do |c|
397
- if (version = versions.delete(c.id)).nil?
398
- c
399
- elsif version.event == 'create'
400
- options[:mark_for_destruction] ? c.tap { |r| r.mark_for_destruction } : nil
401
- else
402
- version.reify(options.merge(:has_many => false, :has_one => false))
403
- end
404
- end
405
-
406
- # Reify the rest of the versions and add them to the collection, these
407
- # versions are for those that have been removed from the live
408
- # associations.
409
- collection += versions.values.map { |version| version.reify(options.merge(:has_many => false, :has_one => false)) }
410
-
411
- model.send(assoc.name).proxy_association.target = collection.compact
412
- end
413
- end
414
-
415
- # Restore the `model`'s has_many associations through another association.
416
- # This must be called after the direct has_manys have been reified
417
- # (reify_has_many_directly).
418
- def reify_has_many_through(associations, model, options = {})
419
- associations.each do |assoc|
420
- next unless assoc.klass.paper_trail_enabled_for_model?
421
- through_collection = model.send(assoc.options[:through])
422
- collection_keys = through_collection.map { |through_model| through_model.send(assoc.foreign_key) }
423
-
424
- version_id_subquery = assoc.klass.paper_trail_version_class.
425
- select("MIN(id)").
426
- where("item_type = ?", assoc.class_name).
427
- where("item_id IN (?)", collection_keys).
428
- where("created_at >= ? OR transaction_id = ?", options[:version_at], transaction_id).
429
- group("item_id").to_sql
430
- versions = assoc.klass.paper_trail_version_class.where("id IN (#{version_id_subquery})").inject({}) do |acc, v|
431
- acc.merge!(v.item_id => v)
432
- end
433
-
434
- collection = Array.new assoc.klass.where(assoc.klass.primary_key => collection_keys)
435
-
436
- # Iterate each child to replace it with the previous value if there is
437
- # a version after the timestamp.
438
- collection.map! do |c|
439
- if (version = versions.delete(c.id)).nil?
440
- c
441
- elsif version.event == 'create'
442
- options[:mark_for_destruction] ? c.tap { |r| r.mark_for_destruction } : nil
443
- else
444
- version.reify(options.merge(:has_many => false, :has_one => false))
445
- end
446
- end
447
-
448
- # Reify the rest of the versions and add them to the collection, these
449
- # versions are for those that have been removed from the live
450
- # associations.
451
- collection += versions.values.map { |version| version.reify(options.merge(:has_many => false, :has_one => false)) }
452
-
453
- model.send(assoc.name).proxy_association.target = collection.compact
454
- end
455
- end
456
-
457
289
  # Checks that a value has been set for the `version_limit` config
458
290
  # option, and if so enforces it.
291
+ # @api private
459
292
  def enforce_version_limit!
460
- return unless PaperTrail.config.version_limit.is_a? Numeric
293
+ limit = PaperTrail.config.version_limit
294
+ return unless limit.is_a? Numeric
461
295
  previous_versions = sibling_versions.not_creates
462
- return unless previous_versions.size > PaperTrail.config.version_limit
463
- excess_previous_versions = previous_versions - previous_versions.last(PaperTrail.config.version_limit)
464
- excess_previous_versions.map(&:destroy)
296
+ return unless previous_versions.size > limit
297
+ excess_versions = previous_versions - previous_versions.last(limit)
298
+ excess_versions.map(&:destroy)
465
299
  end
466
300
  end
467
301
  end