paper_trail 6.0.2 → 7.0.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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CONTRIBUTING.md +20 -0
  3. data/.rubocop.yml +30 -2
  4. data/.rubocop_todo.yml +20 -0
  5. data/.travis.yml +3 -5
  6. data/Appraisals +5 -6
  7. data/CHANGELOG.md +33 -0
  8. data/README.md +43 -81
  9. data/Rakefile +1 -1
  10. data/doc/bug_report_template.rb +4 -2
  11. data/gemfiles/ar_4.0.gemfile +7 -0
  12. data/gemfiles/ar_4.2.gemfile +0 -1
  13. data/lib/generators/paper_trail/templates/create_version_associations.rb +1 -1
  14. data/lib/generators/paper_trail/templates/create_versions.rb +1 -1
  15. data/lib/paper_trail.rb +7 -9
  16. data/lib/paper_trail/config.rb +0 -15
  17. data/lib/paper_trail/frameworks/rspec.rb +8 -2
  18. data/lib/paper_trail/model_config.rb +6 -2
  19. data/lib/paper_trail/record_trail.rb +3 -1
  20. data/lib/paper_trail/reifier.rb +43 -354
  21. data/lib/paper_trail/reifiers/belongs_to.rb +48 -0
  22. data/lib/paper_trail/reifiers/has_and_belongs_to_many.rb +50 -0
  23. data/lib/paper_trail/reifiers/has_many.rb +110 -0
  24. data/lib/paper_trail/reifiers/has_many_through.rb +90 -0
  25. data/lib/paper_trail/reifiers/has_one.rb +76 -0
  26. data/lib/paper_trail/serializers/yaml.rb +2 -25
  27. data/lib/paper_trail/version_concern.rb +5 -5
  28. data/lib/paper_trail/version_number.rb +7 -3
  29. data/paper_trail.gemspec +7 -34
  30. data/spec/controllers/articles_controller_spec.rb +1 -1
  31. data/spec/generators/install_generator_spec.rb +40 -34
  32. data/spec/models/animal_spec.rb +50 -25
  33. data/spec/models/boolit_spec.rb +8 -7
  34. data/spec/models/callback_modifier_spec.rb +13 -13
  35. data/spec/models/document_spec.rb +21 -0
  36. data/spec/models/gadget_spec.rb +35 -39
  37. data/spec/models/joined_version_spec.rb +4 -4
  38. data/spec/models/json_version_spec.rb +14 -15
  39. data/spec/models/not_on_update_spec.rb +1 -1
  40. data/spec/models/post_with_status_spec.rb +2 -2
  41. data/spec/models/skipper_spec.rb +4 -4
  42. data/spec/models/thing_spec.rb +1 -1
  43. data/spec/models/truck_spec.rb +1 -1
  44. data/spec/models/vehicle_spec.rb +1 -1
  45. data/spec/models/version_spec.rb +152 -168
  46. data/spec/models/widget_spec.rb +170 -196
  47. data/spec/modules/paper_trail_spec.rb +3 -3
  48. data/spec/modules/version_concern_spec.rb +5 -8
  49. data/spec/modules/version_number_spec.rb +11 -36
  50. data/spec/paper_trail/cleaner_spec.rb +152 -0
  51. data/spec/paper_trail/config_spec.rb +1 -1
  52. data/spec/paper_trail/serializers/custom_yaml_serializer_spec.rb +45 -0
  53. data/spec/paper_trail/serializers/json_spec.rb +57 -0
  54. data/spec/paper_trail/version_limit_spec.rb +55 -0
  55. data/spec/paper_trail_spec.rb +45 -32
  56. data/spec/requests/articles_spec.rb +4 -4
  57. data/test/dummy/app/models/custom_primary_key_record.rb +4 -2
  58. data/test/dummy/app/models/document.rb +1 -1
  59. data/test/dummy/app/models/not_on_update.rb +1 -1
  60. data/test/dummy/app/models/on/create.rb +6 -0
  61. data/test/dummy/app/models/on/destroy.rb +6 -0
  62. data/test/dummy/app/models/on/empty_array.rb +6 -0
  63. data/test/dummy/app/models/on/update.rb +6 -0
  64. data/test/dummy/app/models/person.rb +1 -0
  65. data/test/dummy/app/models/song.rb +19 -28
  66. data/test/dummy/config/application.rb +10 -43
  67. data/test/dummy/config/routes.rb +1 -1
  68. data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +25 -51
  69. data/test/dummy/db/schema.rb +29 -19
  70. data/test/test_helper.rb +0 -16
  71. data/test/unit/associations_test.rb +81 -81
  72. data/test/unit/model_test.rb +48 -131
  73. data/test/unit/serializer_test.rb +34 -45
  74. data/test/unit/serializers/mixin_json_test.rb +3 -1
  75. data/test/unit/serializers/yaml_test.rb +1 -5
  76. metadata +44 -19
  77. data/lib/paper_trail/frameworks/sinatra.rb +0 -40
  78. data/test/functional/modular_sinatra_test.rb +0 -46
  79. data/test/functional/sinatra_test.rb +0 -51
  80. data/test/unit/cleaner_test.rb +0 -151
  81. data/test/unit/inheritance_column_test.rb +0 -41
  82. data/test/unit/serializers/json_test.rb +0 -95
  83. data/test/unit/serializers/mixin_yaml_test.rb +0 -53
data/Rakefile CHANGED
@@ -27,4 +27,4 @@ require "rubocop/rake_task"
27
27
  RuboCop::RakeTask.new
28
28
 
29
29
  desc "Default: run all available test suites"
30
- task default: [:rubocop, :prepare, :test, :spec]
30
+ task default: %i(rubocop prepare test spec)
@@ -34,7 +34,7 @@ ActiveRecord::Schema.define do
34
34
  t.integer :transaction_id
35
35
  t.datetime :created_at
36
36
  end
37
- add_index :versions, [:item_type, :item_id]
37
+ add_index :versions, %i(item_type item_id)
38
38
  add_index :versions, [:transaction_id]
39
39
 
40
40
  create_table :version_associations do |t|
@@ -43,7 +43,7 @@ ActiveRecord::Schema.define do
43
43
  t.integer :foreign_key_id
44
44
  end
45
45
  add_index :version_associations, [:version_id]
46
- add_index :version_associations, [:foreign_key_name, :foreign_key_id],
46
+ add_index :version_associations, %i(foreign_key_name foreign_key_id),
47
47
  name: "index_version_associations_on_foreign_key"
48
48
  end
49
49
  ActiveRecord::Base.logger = Logger.new(STDOUT)
@@ -67,3 +67,5 @@ class BugTest < ActiveSupport::TestCase
67
67
  }
68
68
  end
69
69
  end
70
+
71
+ # STEP SIX: Run this script using `ruby my_bug_report.rb`
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 4.0"
6
+
7
+ gemspec :path => "../"
@@ -3,6 +3,5 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "activerecord", "~> 4.2"
6
- gem "sinatra", "~> 1.4.6"
7
6
 
8
7
  gemspec :path => "../"
@@ -9,7 +9,7 @@ class CreateVersionAssociations < ActiveRecord::Migration
9
9
  end
10
10
  add_index :version_associations, [:version_id]
11
11
  add_index :version_associations,
12
- [:foreign_key_name, :foreign_key_id],
12
+ %i(foreign_key_name foreign_key_id),
13
13
  name: "index_version_associations_on_foreign_key"
14
14
  end
15
15
 
@@ -38,7 +38,7 @@ class CreateVersions < ActiveRecord::Migration
38
38
  #
39
39
  t.datetime :created_at
40
40
  end
41
- add_index :versions, [:item_type, :item_id]
41
+ add_index :versions, %i(item_type item_id)
42
42
  end
43
43
 
44
44
  private
@@ -34,14 +34,6 @@ module PaperTrail
34
34
  !!PaperTrail.config.enabled
35
35
  end
36
36
 
37
- def serialized_attributes?
38
- ActiveSupport::Deprecation.warn(
39
- "PaperTrail.serialized_attributes? is deprecated without replacement " +
40
- "and always returns false."
41
- )
42
- false
43
- end
44
-
45
37
  # Sets whether PaperTrail is enabled or disabled for the current request.
46
38
  # @api public
47
39
  def enabled_for_controller=(value)
@@ -70,6 +62,13 @@ module PaperTrail
70
62
  !!paper_trail_store.fetch(:"enabled_for_#{model}", true)
71
63
  end
72
64
 
65
+ # Returns a `::Gem::Version`, convenient for comparisons. This is
66
+ # recommended over `::PaperTrail::VERSION::STRING`.
67
+ # @api public
68
+ def gem_version
69
+ ::Gem::Version.new(VERSION::STRING)
70
+ end
71
+
73
72
  # Set the field which records when a version was created.
74
73
  # @api public
75
74
  def timestamp_field=(_field_name)
@@ -163,7 +162,6 @@ ActiveSupport.on_load(:active_record) do
163
162
  end
164
163
 
165
164
  # Require frameworks
166
- require "paper_trail/frameworks/sinatra"
167
165
  if defined?(::Rails) && ActiveRecord::VERSION::STRING >= "3.2"
168
166
  require "paper_trail/frameworks/rails"
169
167
  else
@@ -18,21 +18,6 @@ module PaperTrail
18
18
  @serializer = PaperTrail::Serializers::YAML
19
19
  end
20
20
 
21
- def serialized_attributes
22
- ActiveSupport::Deprecation.warn(
23
- "PaperTrail.config.serialized_attributes is deprecated without " +
24
- "replacement and always returns false."
25
- )
26
- false
27
- end
28
-
29
- def serialized_attributes=(_)
30
- ActiveSupport::Deprecation.warn(
31
- "PaperTrail.config.serialized_attributes= is deprecated without " +
32
- "replacement and no longer has any effect."
33
- )
34
- end
35
-
36
21
  # Previously, we checked `PaperTrail::VersionAssociation.table_exists?`
37
22
  # here, but that proved to be problematic in situations when the database
38
23
  # connection had not been established, or when the database does not exist
@@ -25,10 +25,16 @@ end
25
25
 
26
26
  RSpec::Matchers.define :have_a_version_with do |attributes|
27
27
  # check if the model has a version with the specified attributes
28
- match { |actual| actual.versions.where_object(attributes).any? }
28
+ match do |actual|
29
+ versions_association = actual.class.versions_association_name
30
+ actual.send(versions_association).where_object(attributes).any?
31
+ end
29
32
  end
30
33
 
31
34
  RSpec::Matchers.define :have_a_version_with_changes do |attributes|
32
35
  # check if the model has a version changes with the specified attributes
33
- match { |actual| actual.versions.where_object_changes(attributes).any? }
36
+ match do |actual|
37
+ versions_association = actual.class.versions_association_name
38
+ actual.send(versions_association).where_object_changes(attributes).any?
39
+ end
34
40
  end
@@ -76,10 +76,14 @@ module PaperTrail
76
76
  # "class attributes", instance methods, and more.
77
77
  # @api private
78
78
  def setup(options = {})
79
- options[:on] ||= [:create, :update, :destroy]
79
+ options[:on] ||= %i(create update destroy)
80
80
  options[:on] = Array(options[:on]) # Support single symbol
81
81
  @model_class.send :include, ::PaperTrail::Model::InstanceMethods
82
82
  if ::ActiveRecord::VERSION::STRING < "4.2"
83
+ ::ActiveSupport::Deprecation.warn(
84
+ "Your version of ActiveRecord (< 4.2) has reached EOL. PaperTrail " \
85
+ "will soon drop support. Please upgrade ActiveRecord ASAP."
86
+ )
83
87
  @model_class.send :extend, AttributeSerializers::LegacyActiveRecordShim
84
88
  end
85
89
  setup_options(options)
@@ -163,7 +167,7 @@ module PaperTrail
163
167
  @model_class.class_attribute :paper_trail_options
164
168
  @model_class.paper_trail_options = options.dup
165
169
 
166
- [:ignore, :skip, :only].each do |k|
170
+ %i(ignore skip only).each do |k|
167
171
  @model_class.paper_trail_options[k] = [@model_class.paper_trail_options[k]].
168
172
  flatten.
169
173
  compact.
@@ -474,7 +474,9 @@ module PaperTrail
474
474
  if @in_after_callback && RAILS_GTE_5_1
475
475
  @record.attribute_before_last_save(attr_name.to_s)
476
476
  else
477
- @record.attribute_was(attr_name.to_s)
477
+ # TODO: after dropping support for rails 4.0, remove send, because
478
+ # attribute_was is no longer private.
479
+ @record.send(:attribute_was, attr_name.to_s)
478
480
  end
479
481
  end
480
482
 
@@ -1,4 +1,9 @@
1
1
  require "paper_trail/attribute_serializers/object_attribute"
2
+ require "paper_trail/reifiers/belongs_to"
3
+ require "paper_trail/reifiers/has_and_belongs_to_many"
4
+ require "paper_trail/reifiers/has_many"
5
+ require "paper_trail/reifiers/has_many_through"
6
+ require "paper_trail/reifiers/has_one"
2
7
 
3
8
  module PaperTrail
4
9
  # Given a version record and some options, builds a new model object.
@@ -17,6 +22,18 @@ module PaperTrail
17
22
  model
18
23
  end
19
24
 
25
+ # Restore the `model`'s has_many associations as they were at version_at
26
+ # timestamp We lookup the first child versions after version_at timestamp or
27
+ # in same transaction.
28
+ # @api private
29
+ def reify_has_manys(transaction_id, model, options = {})
30
+ assoc_has_many_through, assoc_has_many_directly =
31
+ model.class.reflect_on_all_associations(:has_many).
32
+ partition { |assoc| assoc.options[:through] }
33
+ reify_has_many_associations(transaction_id, assoc_has_many_directly, model, options)
34
+ reify_has_many_through_associations(transaction_id, assoc_has_many_through, model, options)
35
+ end
36
+
20
37
  private
21
38
 
22
39
  # Given a hash of `options` for `.reify`, return a new hash with default
@@ -79,54 +96,6 @@ module PaperTrail
79
96
  model
80
97
  end
81
98
 
82
- # Examine the `source_reflection`, i.e. the "source" of `assoc` the
83
- # `ThroughReflection`. The source can be a `BelongsToReflection`
84
- # or a `HasManyReflection`.
85
- #
86
- # If the association is a has_many association again, then call
87
- # reify_has_manys for each record in `through_collection`.
88
- #
89
- # @api private
90
- def hmt_collection(through_collection, assoc, options, transaction_id)
91
- if !assoc.source_reflection.belongs_to? && through_collection.present?
92
- hmt_collection_through_has_many(
93
- through_collection, assoc, options, transaction_id
94
- )
95
- else
96
- hmt_collection_through_belongs_to(
97
- through_collection, assoc, options, transaction_id
98
- )
99
- end
100
- end
101
-
102
- # @api private
103
- def hmt_collection_through_has_many(through_collection, assoc, options, transaction_id)
104
- through_collection.each do |through_model|
105
- reify_has_manys(transaction_id, through_model, options)
106
- end
107
-
108
- # At this point, the "through" part of the association chain has
109
- # been reified, but not the final, "target" part. To continue our
110
- # example, `model.sections` (including `model.sections.paragraphs`)
111
- # has been loaded. However, the final "target" part of the
112
- # association, that is, `model.paragraphs`, has not been loaded. So,
113
- # we do that now.
114
- through_collection.flat_map { |through_model|
115
- through_model.public_send(assoc.name.to_sym).to_a
116
- }
117
- end
118
-
119
- # @api private
120
- def hmt_collection_through_belongs_to(through_collection, assoc, options, tx_id)
121
- ids = through_collection.map { |through_model|
122
- through_model.send(assoc.source_reflection.foreign_key)
123
- }
124
- versions = load_versions_for_hmt_association(assoc, ids, tx_id, options[:version_at])
125
- collection = Array.new assoc.klass.where(assoc.klass.primary_key => ids)
126
- prepare_array_for_has_many(collection, options, versions)
127
- collection
128
- end
129
-
130
99
  # Look for attributes that exist in `model` and not in this version.
131
100
  # These attributes should be set to nil. Modifies `attrs`.
132
101
  # @api private
@@ -134,171 +103,35 @@ module PaperTrail
134
103
  (model.attribute_names - attrs.keys).each { |k| attrs[k] = nil }
135
104
  end
136
105
 
137
- # Given a `belongs_to` association and a `version`, return a record that
138
- # can be assigned in order to reify that association.
106
+ # Reify onto `model` an attribute named `k` with value `v` from `version`.
107
+ #
108
+ # `ObjectAttribute#deserialize` will return the mapped enum value and in
109
+ # Rails < 5, the []= uses the integer type caster from the column
110
+ # definition (in general) and thus will turn a (usually) string to 0
111
+ # instead of the correct value.
112
+ #
139
113
  # @api private
140
- def load_record_for_bt_association(assoc, id, options, version)
141
- if version.nil?
142
- assoc.klass.where(assoc.klass.primary_key => id).first
143
- else
144
- version.reify(
145
- options.merge(
146
- has_many: false,
147
- has_one: false,
148
- belongs_to: false,
149
- has_and_belongs_to_many: false
150
- )
114
+ def reify_attribute(k, v, model, version)
115
+ enums = model.class.respond_to?(:defined_enums) ? model.class.defined_enums : {}
116
+ is_enum_without_type_caster = ::ActiveRecord::VERSION::MAJOR < 5 && enums.key?(k)
117
+ if model.has_attribute?(k) && !is_enum_without_type_caster
118
+ model[k.to_sym] = v
119
+ elsif model.respond_to?("#{k}=")
120
+ model.send("#{k}=", v)
121
+ elsif version.logger
122
+ version.logger.warn(
123
+ "Attribute #{k} does not exist on #{version.item_type} (Version id: #{version.id})."
151
124
  )
152
125
  end
153
126
  end
154
127
 
155
- # Given a `belongs_to` association and an `id`, return a version record
156
- # from the point in time identified by `transaction_id` or `version_at`.
157
- # @api private
158
- def load_version_for_bt_association(assoc, id, transaction_id, version_at)
159
- assoc.klass.paper_trail.version_class.
160
- where("item_type = ?", assoc.class_name).
161
- where("item_id = ?", id).
162
- where("created_at >= ? OR transaction_id = ?", version_at, transaction_id).
163
- order("id").limit(1).first
164
- end
165
-
166
- # Given a HABTM association `assoc` and an `id`, return a version record
167
- # from the point in time identified by `transaction_id` or `version_at`.
168
- # @api private
169
- def load_version_for_habtm(assoc, id, transaction_id, version_at)
170
- assoc.klass.paper_trail.version_class.
171
- where("item_type = ?", assoc.klass.name).
172
- where("item_id = ?", id).
173
- where("created_at >= ? OR transaction_id = ?", version_at, transaction_id).
174
- order("id").
175
- limit(1).
176
- first
177
- end
178
-
179
- # Given a has-one association `assoc` on `model`, return the version
180
- # record from the point in time identified by `transaction_id` or `version_at`.
181
- # @api private
182
- def load_version_for_has_one(assoc, model, transaction_id, version_at)
183
- version_table_name = model.class.paper_trail.version_class.table_name
184
- model.class.paper_trail.version_class.joins(:version_associations).
185
- where("version_associations.foreign_key_name = ?", assoc.foreign_key).
186
- where("version_associations.foreign_key_id = ?", model.id).
187
- where("#{version_table_name}.item_type = ?", assoc.class_name).
188
- where("created_at >= ? OR transaction_id = ?", version_at, transaction_id).
189
- order("#{version_table_name}.id ASC").
190
- first
191
- end
192
-
193
- # Given a `has_many` association on `model`, return the version records
194
- # from the point in time identified by `tx_id` or `version_at`.
128
+ # Reify onto `model` all the attributes of `version`.
195
129
  # @api private
196
- def load_versions_for_hm_association(assoc, model, version_table, tx_id, version_at)
197
- version_id_subquery = ::PaperTrail::VersionAssociation.
198
- joins(model.class.version_association_name).
199
- select("MIN(version_id)").
200
- where("foreign_key_name = ?", assoc.foreign_key).
201
- where("foreign_key_id = ?", model.id).
202
- where("#{version_table}.item_type = ?", assoc.class_name).
203
- where("created_at >= ? OR transaction_id = ?", version_at, tx_id).
204
- group("item_id").
205
- to_sql
206
- versions_by_id(model.class, version_id_subquery)
207
- end
208
-
209
- # Given a `has_many(through:)` association and an array of `ids`, return
210
- # the version records from the point in time identified by `tx_id` or
211
- # `version_at`.
212
- # @api private
213
- def load_versions_for_hmt_association(assoc, ids, tx_id, version_at)
214
- version_id_subquery = assoc.klass.paper_trail.version_class.
215
- select("MIN(id)").
216
- where("item_type = ?", assoc.class_name).
217
- where("item_id IN (?)", ids).
218
- where(
219
- "created_at >= ? OR transaction_id = ?",
220
- version_at,
221
- tx_id
222
- ).
223
- group("item_id").
224
- to_sql
225
- versions_by_id(assoc.klass, version_id_subquery)
226
- end
227
-
228
- # Set all the attributes in this version on the model.
229
130
  def reify_attributes(model, version, attrs)
230
- enums = model.class.respond_to?(:defined_enums) ? model.class.defined_enums : {}
231
131
  AttributeSerializers::ObjectAttribute.new(model.class).deserialize(attrs)
232
132
  attrs.each do |k, v|
233
- # `ObjectAttribute#deserialize` will return the mapped enum value
234
- # and in Rails < 5, the []= uses the integer type caster from the column
235
- # definition (in general) and thus will turn a (usually) string to 0 instead
236
- # of the correct value
237
- is_enum_without_type_caster = ::ActiveRecord::VERSION::MAJOR < 5 && enums.key?(k)
238
-
239
- if model.has_attribute?(k) && !is_enum_without_type_caster
240
- model[k.to_sym] = v
241
- elsif model.respond_to?("#{k}=")
242
- model.send("#{k}=", v)
243
- elsif version.logger
244
- version.logger.warn(
245
- "Attribute #{k} does not exist on #{version.item_type} (Version id: #{version.id})."
246
- )
247
- end
248
- end
249
- end
250
-
251
- # Replaces each record in `array` with its reified version, if present
252
- # in `versions`.
253
- #
254
- # @api private
255
- # @param array - The collection to be modified.
256
- # @param options
257
- # @param versions - A `Hash` mapping IDs to `Version`s
258
- # @return nil - Always returns `nil`
259
- #
260
- # Once modified by this method, `array` will be assigned to the
261
- # AR association currently being reified.
262
- #
263
- def prepare_array_for_has_many(array, options, versions)
264
- # Iterate each child to replace it with the previous value if there is
265
- # a version after the timestamp.
266
- array.map! do |record|
267
- if (version = versions.delete(record.id)).nil?
268
- record
269
- elsif version.event == "create"
270
- options[:mark_for_destruction] ? record.tap(&:mark_for_destruction) : nil
271
- else
272
- version.reify(
273
- options.merge(
274
- has_many: false,
275
- has_one: false,
276
- belongs_to: false,
277
- has_and_belongs_to_many: false
278
- )
279
- )
280
- end
133
+ reify_attribute(k, v, model, version)
281
134
  end
282
-
283
- # Reify the rest of the versions and add them to the collection, these
284
- # versions are for those that have been removed from the live
285
- # associations.
286
- array.concat(
287
- versions.values.map { |v|
288
- v.reify(
289
- options.merge(
290
- has_many: false,
291
- has_one: false,
292
- belongs_to: false,
293
- has_and_belongs_to_many: false
294
- )
295
- )
296
- }
297
- )
298
-
299
- array.compact!
300
-
301
- nil
302
135
  end
303
136
 
304
137
  # @api private
@@ -317,36 +150,6 @@ module PaperTrail
317
150
  end
318
151
  end
319
152
 
320
- # Reify a single `has_one` association of `model`.
321
- # @api private
322
- def reify_has_one_association(assoc, model, options, transaction_id)
323
- version = load_version_for_has_one(assoc, model, transaction_id, options[:version_at])
324
- return unless version
325
- if version.event == "create"
326
- if options[:mark_for_destruction]
327
- model.send(assoc.name).mark_for_destruction if model.send(assoc.name, true)
328
- else
329
- model.paper_trail.appear_as_new_record do
330
- model.send "#{assoc.name}=", nil
331
- end
332
- end
333
- else
334
- child = version.reify(
335
- options.merge(
336
- has_many: false,
337
- has_one: false,
338
- belongs_to: false,
339
- has_and_belongs_to_many: false
340
- )
341
- )
342
- model.paper_trail.appear_as_new_record do
343
- without_persisting(child) do
344
- model.send "#{assoc.name}=", child
345
- end
346
- end
347
- end
348
- end
349
-
350
153
  # Restore the `model`'s has_one associations as they were when this
351
154
  # version was superseded by the next (because that's what the user was
352
155
  # looking at when they made the change).
@@ -354,130 +157,44 @@ module PaperTrail
354
157
  def reify_has_one_associations(transaction_id, model, options = {})
355
158
  associations = model.class.reflect_on_all_associations(:has_one)
356
159
  each_enabled_association(associations) do |assoc|
357
- reify_has_one_association(assoc, model, options, transaction_id)
160
+ Reifiers::HasOne.reify(assoc, model, options, transaction_id)
358
161
  end
359
162
  end
360
163
 
361
- # Reify a single `belongs_to` association of `model`.
362
- # @api private
363
- def reify_belongs_to_association(assoc, model, options, transaction_id)
364
- id = model.send(assoc.association_foreign_key)
365
- version = load_version_for_bt_association(assoc, id, transaction_id, options[:version_at])
366
- record = load_record_for_bt_association(assoc, id, options, version)
367
- model.send("#{assoc.name}=".to_sym, record)
368
- end
369
-
370
164
  # Reify all `belongs_to` associations of `model`.
371
165
  # @api private
372
166
  def reify_belongs_to_associations(transaction_id, model, options = {})
373
167
  associations = model.class.reflect_on_all_associations(:belongs_to)
374
168
  each_enabled_association(associations) do |assoc|
375
- reify_belongs_to_association(assoc, model, options, transaction_id)
169
+ Reifiers::BelongsTo.reify(assoc, model, options, transaction_id)
376
170
  end
377
171
  end
378
172
 
379
- # Restore the `model`'s has_many associations as they were at version_at
380
- # timestamp We lookup the first child versions after version_at timestamp or
381
- # in same transaction.
382
- def reify_has_manys(transaction_id, model, options = {})
383
- assoc_has_many_through, assoc_has_many_directly =
384
- model.class.reflect_on_all_associations(:has_many).
385
- partition { |assoc| assoc.options[:through] }
386
- reify_has_many_associations(transaction_id, assoc_has_many_directly, model, options)
387
- reify_has_many_through_associations(transaction_id, assoc_has_many_through, model, options)
388
- end
389
-
390
- # Reify a single, direct (not `through`) `has_many` association of `model`.
391
- # @api private
392
- def reify_has_many_association(assoc, model, options, transaction_id, version_table_name)
393
- versions = load_versions_for_hm_association(
394
- assoc,
395
- model,
396
- version_table_name,
397
- transaction_id,
398
- options[:version_at]
399
- )
400
- collection = Array.new model.send(assoc.name).reload # to avoid cache
401
- prepare_array_for_has_many(collection, options, versions)
402
- model.send(assoc.name).proxy_association.target = collection
403
- end
404
-
405
173
  # Reify all direct (not `through`) `has_many` associations of `model`.
406
174
  # @api private
407
175
  def reify_has_many_associations(transaction_id, associations, model, options = {})
408
176
  version_table_name = model.class.paper_trail.version_class.table_name
409
177
  each_enabled_association(associations) do |assoc|
410
- reify_has_many_association(assoc, model, options, transaction_id, version_table_name)
178
+ Reifiers::HasMany.reify(assoc, model, options, transaction_id, version_table_name)
411
179
  end
412
180
  end
413
181
 
414
- # Reify a single HMT association of `model`.
415
- # @api private
416
- def reify_has_many_through_association(assoc, model, options, transaction_id)
417
- # Load the collection of through-models. For example, if `model` is a
418
- # Chapter, having many Paragraphs through Sections, then
419
- # `through_collection` will contain Sections.
420
- through_collection = model.send(assoc.options[:through])
421
-
422
- # Now, given the collection of "through" models (e.g. sections), load
423
- # the collection of "target" models (e.g. paragraphs)
424
- collection = hmt_collection(through_collection, assoc, options, transaction_id)
425
-
426
- # Finally, assign the `collection` of "target" models, e.g. to
427
- # `model.paragraphs`.
428
- model.send(assoc.name).proxy_association.target = collection
429
- end
430
-
431
182
  # Reify all HMT associations of `model`. This must be called after the
432
183
  # direct (non-`through`) has_manys have been reified.
433
184
  # @api private
434
185
  def reify_has_many_through_associations(transaction_id, associations, model, options = {})
435
186
  each_enabled_association(associations) do |assoc|
436
- reify_has_many_through_association(assoc, model, options, transaction_id)
187
+ Reifiers::HasManyThrough.reify(assoc, model, options, transaction_id)
437
188
  end
438
189
  end
439
190
 
440
- # Reify a single HABTM association of `model`.
441
- # @api private
442
- def reify_habtm_association(assoc, model, options, papertrail_enabled, transaction_id)
443
- version_ids = PaperTrail::VersionAssociation.
444
- where("foreign_key_name = ?", assoc.name).
445
- where("version_id = ?", transaction_id).
446
- pluck(:foreign_key_id)
447
-
448
- model.send(assoc.name).proxy_association.target =
449
- version_ids.map do |id|
450
- if papertrail_enabled
451
- version = load_version_for_habtm(
452
- assoc,
453
- id,
454
- transaction_id,
455
- options[:version_at]
456
- )
457
- if version
458
- next version.reify(
459
- options.merge(
460
- has_many: false,
461
- has_one: false,
462
- belongs_to: false,
463
- has_and_belongs_to_many: false
464
- )
465
- )
466
- end
467
- end
468
- assoc.klass.where(assoc.klass.primary_key => id).first
469
- end
470
- end
471
-
472
191
  # Reify all HABTM associations of `model`.
473
192
  # @api private
474
193
  def reify_habtm_associations(transaction_id, model, options = {})
475
194
  model.class.reflect_on_all_associations(:has_and_belongs_to_many).each do |assoc|
476
- papertrail_enabled = assoc.klass.paper_trail.enabled?
477
- next unless
478
- model.class.paper_trail_save_join_tables.include?(assoc.name) ||
479
- papertrail_enabled
480
- reify_habtm_association(assoc, model, options, papertrail_enabled, transaction_id)
195
+ pt_enabled = assoc.klass.paper_trail.enabled?
196
+ next unless model.class.paper_trail_save_join_tables.include?(assoc.name) || pt_enabled
197
+ Reifiers::HasAndBelongsToMany.reify(pt_enabled, assoc, model, options, transaction_id)
481
198
  end
482
199
  end
483
200
 
@@ -497,34 +214,6 @@ module PaperTrail
497
214
  class_name = inher_col_value.blank? ? version.item_type : inher_col_value
498
215
  class_name.constantize
499
216
  end
500
-
501
- # Given a SQL fragment that identifies the IDs of version records,
502
- # returns a `Hash` mapping those IDs to `Version`s.
503
- #
504
- # @api private
505
- # @param klass - An ActiveRecord class.
506
- # @param version_id_subquery - String. A SQL subquery that selects
507
- # the IDs of version records.
508
- # @return A `Hash` mapping IDs to `Version`s
509
- #
510
- def versions_by_id(klass, version_id_subquery)
511
- klass.
512
- paper_trail.version_class.
513
- where("id IN (#{version_id_subquery})").
514
- inject({}) { |a, e| a.merge!(e.item_id => e) }
515
- end
516
-
517
- # Temporarily suppress #save so we can reassociate with the reified
518
- # master of a has_one relationship. Since ActiveRecord 5 the related
519
- # object is saved when it is assigned to the association. ActiveRecord
520
- # 5 also happens to be the first version that provides #suppress.
521
- def without_persisting(record)
522
- if record.class.respond_to? :suppress
523
- record.class.suppress { yield }
524
- else
525
- yield
526
- end
527
- end
528
217
  end
529
218
  end
530
219
  end