paper_trail 6.0.2 → 7.0.0

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