paper_trail 5.2.3 → 11.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 (198) hide show
  1. checksums.yaml +5 -5
  2. data/lib/generators/paper_trail/install/USAGE +3 -0
  3. data/lib/generators/paper_trail/install/install_generator.rb +75 -0
  4. data/lib/generators/paper_trail/{templates/add_object_changes_to_versions.rb → install/templates/add_object_changes_to_versions.rb.erb} +1 -1
  5. data/lib/generators/paper_trail/install/templates/create_versions.rb.erb +36 -0
  6. data/lib/generators/paper_trail/migration_generator.rb +38 -0
  7. data/lib/generators/paper_trail/update_item_subtype/USAGE +4 -0
  8. data/lib/generators/paper_trail/update_item_subtype/templates/update_versions_for_item_subtype.rb.erb +85 -0
  9. data/lib/generators/paper_trail/update_item_subtype/update_item_subtype_generator.rb +17 -0
  10. data/lib/paper_trail.rb +82 -130
  11. data/lib/paper_trail/attribute_serializers/attribute_serializer_factory.rb +27 -0
  12. data/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +15 -44
  13. data/lib/paper_trail/attribute_serializers/object_attribute.rb +2 -0
  14. data/lib/paper_trail/attribute_serializers/object_changes_attribute.rb +2 -0
  15. data/lib/paper_trail/cleaner.rb +3 -1
  16. data/lib/paper_trail/compatibility.rb +51 -0
  17. data/lib/paper_trail/config.rb +11 -49
  18. data/lib/paper_trail/events/base.rb +323 -0
  19. data/lib/paper_trail/events/create.rb +32 -0
  20. data/lib/paper_trail/events/destroy.rb +42 -0
  21. data/lib/paper_trail/events/update.rb +60 -0
  22. data/lib/paper_trail/frameworks/active_record.rb +2 -1
  23. data/lib/paper_trail/frameworks/active_record/models/paper_trail/version.rb +8 -3
  24. data/lib/paper_trail/frameworks/cucumber.rb +5 -3
  25. data/lib/paper_trail/frameworks/rails.rb +2 -0
  26. data/lib/paper_trail/frameworks/rails/controller.rb +33 -43
  27. data/lib/paper_trail/frameworks/rails/engine.rb +34 -1
  28. data/lib/paper_trail/frameworks/rspec.rb +17 -4
  29. data/lib/paper_trail/frameworks/rspec/helpers.rb +2 -0
  30. data/lib/paper_trail/has_paper_trail.rb +22 -310
  31. data/lib/paper_trail/model_config.rb +157 -109
  32. data/lib/paper_trail/queries/versions/where_object.rb +65 -0
  33. data/lib/paper_trail/queries/versions/where_object_changes.rb +75 -0
  34. data/lib/paper_trail/record_history.rb +3 -9
  35. data/lib/paper_trail/record_trail.rb +169 -319
  36. data/lib/paper_trail/reifier.rb +53 -374
  37. data/lib/paper_trail/request.rb +166 -0
  38. data/lib/paper_trail/serializers/json.rb +9 -10
  39. data/lib/paper_trail/serializers/yaml.rb +15 -28
  40. data/lib/paper_trail/type_serializers/postgres_array_serializer.rb +48 -0
  41. data/lib/paper_trail/version_concern.rb +160 -155
  42. data/lib/paper_trail/version_number.rb +12 -4
  43. metadata +77 -372
  44. data/.github/CONTRIBUTING.md +0 -109
  45. data/.github/ISSUE_TEMPLATE.md +0 -13
  46. data/.gitignore +0 -23
  47. data/.rspec +0 -2
  48. data/.rubocop.yml +0 -99
  49. data/.rubocop_todo.yml +0 -22
  50. data/.travis.yml +0 -41
  51. data/Appraisals +0 -38
  52. data/CHANGELOG.md +0 -560
  53. data/Gemfile +0 -2
  54. data/MIT-LICENSE +0 -20
  55. data/README.md +0 -1654
  56. data/Rakefile +0 -30
  57. data/doc/bug_report_template.rb +0 -69
  58. data/doc/warning_about_not_setting_whodunnit.md +0 -32
  59. data/gemfiles/ar3.gemfile +0 -19
  60. data/gemfiles/ar4.gemfile +0 -8
  61. data/gemfiles/ar5.gemfile +0 -9
  62. data/lib/generators/paper_trail/USAGE +0 -2
  63. data/lib/generators/paper_trail/default_initializer.rb +0 -0
  64. data/lib/generators/paper_trail/install_generator.rb +0 -57
  65. data/lib/generators/paper_trail/templates/add_transaction_id_column_to_versions.rb +0 -13
  66. data/lib/generators/paper_trail/templates/create_version_associations.rb +0 -22
  67. data/lib/generators/paper_trail/templates/create_versions.rb +0 -80
  68. data/lib/paper_trail/attribute_serializers/legacy_active_record_shim.rb +0 -48
  69. data/lib/paper_trail/frameworks/active_record/models/paper_trail/version_association.rb +0 -11
  70. data/lib/paper_trail/frameworks/sinatra.rb +0 -40
  71. data/lib/paper_trail/version_association_concern.rb +0 -17
  72. data/paper_trail.gemspec +0 -56
  73. data/spec/generators/install_generator_spec.rb +0 -66
  74. data/spec/generators/paper_trail/templates/create_versions_spec.rb +0 -51
  75. data/spec/models/animal_spec.rb +0 -36
  76. data/spec/models/boolit_spec.rb +0 -48
  77. data/spec/models/callback_modifier_spec.rb +0 -96
  78. data/spec/models/car_spec.rb +0 -13
  79. data/spec/models/custom_primary_key_record_spec.rb +0 -18
  80. data/spec/models/fluxor_spec.rb +0 -17
  81. data/spec/models/gadget_spec.rb +0 -68
  82. data/spec/models/joined_version_spec.rb +0 -47
  83. data/spec/models/json_version_spec.rb +0 -102
  84. data/spec/models/kitchen/banana_spec.rb +0 -14
  85. data/spec/models/not_on_update_spec.rb +0 -22
  86. data/spec/models/post_with_status_spec.rb +0 -50
  87. data/spec/models/skipper_spec.rb +0 -46
  88. data/spec/models/thing_spec.rb +0 -11
  89. data/spec/models/truck_spec.rb +0 -5
  90. data/spec/models/vehicle_spec.rb +0 -5
  91. data/spec/models/version_spec.rb +0 -272
  92. data/spec/models/widget_spec.rb +0 -343
  93. data/spec/modules/paper_trail_spec.rb +0 -27
  94. data/spec/modules/version_concern_spec.rb +0 -31
  95. data/spec/modules/version_number_spec.rb +0 -43
  96. data/spec/paper_trail/config_spec.rb +0 -33
  97. data/spec/paper_trail_spec.rb +0 -79
  98. data/spec/rails_helper.rb +0 -34
  99. data/spec/requests/articles_spec.rb +0 -34
  100. data/spec/spec_helper.rb +0 -114
  101. data/spec/support/alt_db_init.rb +0 -54
  102. data/test/custom_json_serializer.rb +0 -13
  103. data/test/dummy/Rakefile +0 -7
  104. data/test/dummy/app/controllers/application_controller.rb +0 -33
  105. data/test/dummy/app/controllers/articles_controller.rb +0 -20
  106. data/test/dummy/app/controllers/test_controller.rb +0 -5
  107. data/test/dummy/app/controllers/widgets_controller.rb +0 -32
  108. data/test/dummy/app/helpers/application_helper.rb +0 -2
  109. data/test/dummy/app/models/animal.rb +0 -6
  110. data/test/dummy/app/models/article.rb +0 -24
  111. data/test/dummy/app/models/authorship.rb +0 -5
  112. data/test/dummy/app/models/bar_habtm.rb +0 -4
  113. data/test/dummy/app/models/book.rb +0 -9
  114. data/test/dummy/app/models/boolit.rb +0 -4
  115. data/test/dummy/app/models/callback_modifier.rb +0 -45
  116. data/test/dummy/app/models/car.rb +0 -3
  117. data/test/dummy/app/models/cat.rb +0 -2
  118. data/test/dummy/app/models/chapter.rb +0 -9
  119. data/test/dummy/app/models/citation.rb +0 -5
  120. data/test/dummy/app/models/custom_primary_key_record.rb +0 -13
  121. data/test/dummy/app/models/customer.rb +0 -4
  122. data/test/dummy/app/models/document.rb +0 -4
  123. data/test/dummy/app/models/dog.rb +0 -2
  124. data/test/dummy/app/models/editor.rb +0 -4
  125. data/test/dummy/app/models/editorship.rb +0 -5
  126. data/test/dummy/app/models/elephant.rb +0 -3
  127. data/test/dummy/app/models/fluxor.rb +0 -3
  128. data/test/dummy/app/models/foo_habtm.rb +0 -5
  129. data/test/dummy/app/models/foo_widget.rb +0 -2
  130. data/test/dummy/app/models/fruit.rb +0 -5
  131. data/test/dummy/app/models/gadget.rb +0 -3
  132. data/test/dummy/app/models/kitchen/banana.rb +0 -5
  133. data/test/dummy/app/models/legacy_widget.rb +0 -4
  134. data/test/dummy/app/models/line_item.rb +0 -4
  135. data/test/dummy/app/models/not_on_update.rb +0 -4
  136. data/test/dummy/app/models/order.rb +0 -5
  137. data/test/dummy/app/models/paragraph.rb +0 -5
  138. data/test/dummy/app/models/person.rb +0 -38
  139. data/test/dummy/app/models/post.rb +0 -3
  140. data/test/dummy/app/models/post_with_status.rb +0 -8
  141. data/test/dummy/app/models/protected_widget.rb +0 -3
  142. data/test/dummy/app/models/quotation.rb +0 -5
  143. data/test/dummy/app/models/section.rb +0 -6
  144. data/test/dummy/app/models/skipper.rb +0 -6
  145. data/test/dummy/app/models/song.rb +0 -41
  146. data/test/dummy/app/models/thing.rb +0 -3
  147. data/test/dummy/app/models/translation.rb +0 -4
  148. data/test/dummy/app/models/truck.rb +0 -4
  149. data/test/dummy/app/models/vehicle.rb +0 -4
  150. data/test/dummy/app/models/whatchamajigger.rb +0 -4
  151. data/test/dummy/app/models/widget.rb +0 -16
  152. data/test/dummy/app/models/wotsit.rb +0 -8
  153. data/test/dummy/app/versions/custom_primary_key_record_version.rb +0 -3
  154. data/test/dummy/app/versions/joined_version.rb +0 -6
  155. data/test/dummy/app/versions/json_version.rb +0 -3
  156. data/test/dummy/app/versions/kitchen/banana_version.rb +0 -5
  157. data/test/dummy/app/versions/post_version.rb +0 -3
  158. data/test/dummy/app/views/layouts/application.html.erb +0 -14
  159. data/test/dummy/config.ru +0 -4
  160. data/test/dummy/config/application.rb +0 -80
  161. data/test/dummy/config/boot.rb +0 -10
  162. data/test/dummy/config/database.mysql.yml +0 -19
  163. data/test/dummy/config/database.postgres.yml +0 -15
  164. data/test/dummy/config/database.sqlite.yml +0 -15
  165. data/test/dummy/config/environment.rb +0 -5
  166. data/test/dummy/config/environments/development.rb +0 -41
  167. data/test/dummy/config/environments/production.rb +0 -74
  168. data/test/dummy/config/environments/test.rb +0 -51
  169. data/test/dummy/config/initializers/backtrace_silencers.rb +0 -9
  170. data/test/dummy/config/initializers/inflections.rb +0 -10
  171. data/test/dummy/config/initializers/mime_types.rb +0 -5
  172. data/test/dummy/config/initializers/paper_trail.rb +0 -9
  173. data/test/dummy/config/initializers/secret_token.rb +0 -9
  174. data/test/dummy/config/initializers/session_store.rb +0 -8
  175. data/test/dummy/config/locales/en.yml +0 -5
  176. data/test/dummy/config/routes.rb +0 -4
  177. data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +0 -361
  178. data/test/dummy/db/schema.rb +0 -288
  179. data/test/dummy/script/rails +0 -8
  180. data/test/functional/controller_test.rb +0 -90
  181. data/test/functional/enabled_for_controller_test.rb +0 -28
  182. data/test/functional/modular_sinatra_test.rb +0 -46
  183. data/test/functional/sinatra_test.rb +0 -51
  184. data/test/functional/thread_safety_test.rb +0 -46
  185. data/test/test_helper.rb +0 -127
  186. data/test/time_travel_helper.rb +0 -1
  187. data/test/unit/associations_test.rb +0 -1016
  188. data/test/unit/cleaner_test.rb +0 -188
  189. data/test/unit/inheritance_column_test.rb +0 -43
  190. data/test/unit/model_test.rb +0 -1489
  191. data/test/unit/protected_attrs_test.rb +0 -52
  192. data/test/unit/serializer_test.rb +0 -119
  193. data/test/unit/serializers/json_test.rb +0 -95
  194. data/test/unit/serializers/mixin_json_test.rb +0 -37
  195. data/test/unit/serializers/mixin_yaml_test.rb +0 -53
  196. data/test/unit/serializers/yaml_test.rb +0 -54
  197. data/test/unit/timestamp_test.rb +0 -41
  198. data/test/unit/version_test.rb +0 -119
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "paper_trail/attribute_serializers/object_attribute"
2
4
 
3
5
  module PaperTrail
@@ -10,39 +12,9 @@ module PaperTrail
10
12
  def reify(version, options)
11
13
  options = apply_defaults_to(options, version)
12
14
  attrs = version.object_deserialized
13
-
14
- # Normally a polymorphic belongs_to relationship allows us to get the
15
- # object we belong to by calling, in this case, `item`. However this
16
- # returns nil if `item` has been destroyed, and we need to be able to
17
- # retrieve destroyed objects.
18
- #
19
- # In this situation we constantize the `item_type` to get hold of the
20
- # class...except when the stored object's attributes include a `type`
21
- # key. If this is the case, the object we belong to is using single
22
- # table inheritance and the `item_type` will be the base class, not the
23
- # actual subclass. If `type` is present but empty, the class is the base
24
- # class.
25
- if options[:dup] != true && version.item
26
- model = version.item
27
- if options[:unversioned_attributes] == :nil
28
- init_unversioned_attrs(attrs, model)
29
- end
30
- else
31
- klass = version_reification_class(version, attrs)
32
- # The `dup` option always returns a new object, otherwise we should
33
- # attempt to look for the item outside of default scope(s).
34
- find_cond = { klass.primary_key => version.item_id }
35
- if options[:dup] || (item_found = klass.unscoped.where(find_cond).first).nil?
36
- model = klass.new
37
- elsif options[:unversioned_attributes] == :nil
38
- model = item_found
39
- init_unversioned_attrs(attrs, model)
40
- end
41
- end
42
-
15
+ model = init_model(attrs, options, version)
43
16
  reify_attributes(model, version, attrs)
44
17
  model.send "#{model.class.version_association_name}=", version
45
- reify_associations(model, options, version)
46
18
  model
47
19
  end
48
20
 
@@ -63,71 +35,41 @@ module PaperTrail
63
35
  }.merge(options)
64
36
  end
65
37
 
66
- # @api private
67
- def each_enabled_association(associations)
68
- associations.each do |assoc|
69
- next unless assoc.klass.paper_trail.enabled?
70
- yield assoc
71
- end
72
- end
73
-
74
- # Examine the `source_reflection`, i.e. the "source" of `assoc` the
75
- # `ThroughReflection`. The source can be a `BelongsToReflection`
76
- # or a `HasManyReflection`.
38
+ # Initialize a model object suitable for reifying `version` into. Does
39
+ # not perform reification, merely instantiates the appropriate model
40
+ # class and, if specified by `options[:unversioned_attributes]`, sets
41
+ # unversioned attributes to `nil`.
77
42
  #
78
- # If the association is a has_many association again, then call
79
- # reify_has_manys for each record in `through_collection`.
43
+ # Normally a polymorphic belongs_to relationship allows us to get the
44
+ # object we belong to by calling, in this case, `item`. However this
45
+ # returns nil if `item` has been destroyed, and we need to be able to
46
+ # retrieve destroyed objects.
80
47
  #
81
- # @api private
82
- def hmt_collection(through_collection, assoc, options, transaction_id)
83
- if !assoc.source_reflection.belongs_to? && through_collection.present?
84
- hmt_collection_through_has_many(
85
- through_collection, assoc, options, transaction_id
86
- )
87
- else
88
- hmt_collection_through_belongs_to(
89
- through_collection, assoc, options, transaction_id
90
- )
91
- end
92
- end
48
+ # In this situation we constantize the `item_type` to get hold of the
49
+ # class...except when the stored object's attributes include a `type`
50
+ # key. If this is the case, the object we belong to is using single
51
+ # table inheritance (STI) and the `item_type` will be the base class,
52
+ # not the actual subclass. If `type` is present but empty, the class is
53
+ # the base class.
54
+ def init_model(attrs, options, version)
55
+ klass = version_reification_class(version, attrs)
56
+
57
+ # The `dup` option and destroyed version always returns a new object,
58
+ # otherwise we should attempt to load item or to look for the item
59
+ # outside of default scope(s).
60
+ model = if options[:dup] == true || version.event == "destroy"
61
+ klass.new
62
+ else
63
+ find_cond = { klass.primary_key => version.item_id }
64
+
65
+ version.item || klass.unscoped.where(find_cond).first || klass.new
66
+ end
93
67
 
94
- # @api private
95
- def hmt_collection_through_has_many(through_collection, assoc, options, transaction_id)
96
- through_collection.each do |through_model|
97
- reify_has_manys(transaction_id, through_model, options)
68
+ if options[:unversioned_attributes] == :nil && !model.new_record?
69
+ init_unversioned_attrs(attrs, model)
98
70
  end
99
71
 
100
- # At this point, the "through" part of the association chain has
101
- # been reified, but not the final, "target" part. To continue our
102
- # example, `model.sections` (including `model.sections.paragraphs`)
103
- # has been loaded. However, the final "target" part of the
104
- # association, that is, `model.paragraphs`, has not been loaded. So,
105
- # we do that now.
106
- through_collection.flat_map { |through_model|
107
- through_model.public_send(assoc.name.to_sym).to_a
108
- }
109
- end
110
-
111
- # @api private
112
- def hmt_collection_through_belongs_to(through_collection, assoc, options, transaction_id)
113
- collection_keys = through_collection.map { |through_model|
114
- through_model.send(assoc.source_reflection.foreign_key)
115
- }
116
- version_id_subquery = assoc.klass.paper_trail.version_class.
117
- select("MIN(id)").
118
- where("item_type = ?", assoc.class_name).
119
- where("item_id IN (?)", collection_keys).
120
- where(
121
- "created_at >= ? OR transaction_id = ?",
122
- options[:version_at],
123
- transaction_id
124
- ).
125
- group("item_id").
126
- to_sql
127
- versions = versions_by_id(assoc.klass, version_id_subquery)
128
- collection = Array.new assoc.klass.where(assoc.klass.primary_key => collection_keys)
129
- prepare_array_for_has_many(collection, options, versions)
130
- collection
72
+ model
131
73
  end
132
74
 
133
75
  # Look for attributes that exist in `model` and not in this version.
@@ -137,268 +79,32 @@ module PaperTrail
137
79
  (model.attribute_names - attrs.keys).each { |k| attrs[k] = nil }
138
80
  end
139
81
 
140
- # Given a HABTM association `assoc` and an `id`, return a version record
141
- # from the point in time identified by `transaction_id` or `version_at`.
82
+ # Reify onto `model` an attribute named `k` with value `v` from `version`.
83
+ #
84
+ # `ObjectAttribute#deserialize` will return the mapped enum value and in
85
+ # Rails < 5, the []= uses the integer type caster from the column
86
+ # definition (in general) and thus will turn a (usually) string to 0
87
+ # instead of the correct value.
88
+ #
142
89
  # @api private
143
- def load_version_for_habtm(assoc, id, transaction_id, version_at)
144
- assoc.klass.paper_trail.version_class.
145
- where("item_type = ?", assoc.klass.name).
146
- where("item_id = ?", id).
147
- where("created_at >= ? OR transaction_id = ?", version_at, transaction_id).
148
- order("id").
149
- limit(1).
150
- first
90
+ def reify_attribute(k, v, model, version)
91
+ if model.has_attribute?(k)
92
+ model[k.to_sym] = v
93
+ elsif model.respond_to?("#{k}=")
94
+ model.send("#{k}=", v)
95
+ elsif version.logger
96
+ version.logger.warn(
97
+ "Attribute #{k} does not exist on #{version.item_type} (Version id: #{version.id})."
98
+ )
99
+ end
151
100
  end
152
101
 
153
- # Given a has-one association `assoc` on `model`, return the version
154
- # record from the point in time identified by `transaction_id` or `version_at`.
102
+ # Reify onto `model` all the attributes of `version`.
155
103
  # @api private
156
- def load_version_for_has_one(assoc, model, transaction_id, version_at)
157
- version_table_name = model.class.paper_trail.version_class.table_name
158
- model.class.paper_trail.version_class.joins(:version_associations).
159
- where("version_associations.foreign_key_name = ?", assoc.foreign_key).
160
- where("version_associations.foreign_key_id = ?", model.id).
161
- where("#{version_table_name}.item_type = ?", assoc.class_name).
162
- where("created_at >= ? OR transaction_id = ?", version_at, transaction_id).
163
- order("#{version_table_name}.id ASC").
164
- first
165
- end
166
-
167
- # Set all the attributes in this version on the model.
168
104
  def reify_attributes(model, version, attrs)
169
- enums = model.class.respond_to?(:defined_enums) ? model.class.defined_enums : {}
170
105
  AttributeSerializers::ObjectAttribute.new(model.class).deserialize(attrs)
171
106
  attrs.each do |k, v|
172
- # `ObjectAttribute#deserialize` will return the mapped enum value
173
- # and in Rails < 5, the []= uses the integer type caster from the column
174
- # definition (in general) and thus will turn a (usually) string to 0 instead
175
- # of the correct value
176
- is_enum_without_type_caster = ::ActiveRecord::VERSION::MAJOR < 5 && enums.key?(k)
177
-
178
- if model.has_attribute?(k) && !is_enum_without_type_caster
179
- model[k.to_sym] = v
180
- elsif model.respond_to?("#{k}=")
181
- model.send("#{k}=", v)
182
- else
183
- version.logger.warn(
184
- "Attribute #{k} does not exist on #{version.item_type} (Version id: #{version.id})."
185
- )
186
- end
187
- end
188
- end
189
-
190
- # Replaces each record in `array` with its reified version, if present
191
- # in `versions`.
192
- #
193
- # @api private
194
- # @param array - The collection to be modified.
195
- # @param options
196
- # @param versions - A `Hash` mapping IDs to `Version`s
197
- # @return nil - Always returns `nil`
198
- #
199
- # Once modified by this method, `array` will be assigned to the
200
- # AR association currently being reified.
201
- #
202
- def prepare_array_for_has_many(array, options, versions)
203
- # Iterate each child to replace it with the previous value if there is
204
- # a version after the timestamp.
205
- array.map! do |record|
206
- if (version = versions.delete(record.id)).nil?
207
- record
208
- elsif version.event == "create"
209
- options[:mark_for_destruction] ? record.tap(&:mark_for_destruction) : nil
210
- else
211
- version.reify(
212
- options.merge(
213
- has_many: false,
214
- has_one: false,
215
- belongs_to: false,
216
- has_and_belongs_to_many: false
217
- )
218
- )
219
- end
220
- end
221
-
222
- # Reify the rest of the versions and add them to the collection, these
223
- # versions are for those that have been removed from the live
224
- # associations.
225
- array.concat(
226
- versions.values.map { |v|
227
- v.reify(
228
- options.merge(
229
- has_many: false,
230
- has_one: false,
231
- belongs_to: false,
232
- has_and_belongs_to_many: false
233
- )
234
- )
235
- }
236
- )
237
-
238
- array.compact!
239
-
240
- nil
241
- end
242
-
243
- def reify_associations(model, options, version)
244
- reify_has_ones version.transaction_id, model, options if options[:has_one]
245
-
246
- reify_belongs_tos version.transaction_id, model, options if options[:belongs_to]
247
-
248
- reify_has_manys version.transaction_id, model, options if options[:has_many]
249
-
250
- if options[:has_and_belongs_to_many]
251
- reify_has_and_belongs_to_many version.transaction_id, model, options
252
- end
253
- end
254
-
255
- # Restore the `model`'s has_one associations as they were when this
256
- # version was superseded by the next (because that's what the user was
257
- # looking at when they made the change).
258
- def reify_has_ones(transaction_id, model, options = {})
259
- associations = model.class.reflect_on_all_associations(:has_one)
260
- each_enabled_association(associations) do |assoc|
261
- version = load_version_for_has_one(assoc, model, transaction_id, options[:version_at])
262
- next unless version
263
- if version.event == "create"
264
- if options[:mark_for_destruction]
265
- model.send(assoc.name).mark_for_destruction if model.send(assoc.name, true)
266
- else
267
- model.paper_trail.appear_as_new_record do
268
- model.send "#{assoc.name}=", nil
269
- end
270
- end
271
- else
272
- child = 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
- model.paper_trail.appear_as_new_record do
281
- without_persisting(child) do
282
- model.send "#{assoc.name}=", child
283
- end
284
- end
285
- end
286
- end
287
- end
288
-
289
- def reify_belongs_tos(transaction_id, model, options = {})
290
- associations = model.class.reflect_on_all_associations(:belongs_to)
291
- each_enabled_association(associations) do |assoc|
292
- collection_key = model.send(assoc.association_foreign_key)
293
- version = assoc.klass.paper_trail.version_class.
294
- where("item_type = ?", assoc.class_name).
295
- where("item_id = ?", collection_key).
296
- where("created_at >= ? OR transaction_id = ?", options[:version_at], transaction_id).
297
- order("id").limit(1).first
298
-
299
- collection = if version.nil?
300
- assoc.klass.where(assoc.klass.primary_key => collection_key).first
301
- else
302
- version.reify(
303
- options.merge(
304
- has_many: false,
305
- has_one: false,
306
- belongs_to: false,
307
- has_and_belongs_to_many: false
308
- )
309
- )
310
- end
311
-
312
- model.send("#{assoc.name}=".to_sym, collection)
313
- end
314
- end
315
-
316
- # Restore the `model`'s has_many associations as they were at version_at
317
- # timestamp We lookup the first child versions after version_at timestamp or
318
- # in same transaction.
319
- def reify_has_manys(transaction_id, model, options = {})
320
- assoc_has_many_through, assoc_has_many_directly =
321
- model.class.reflect_on_all_associations(:has_many).
322
- partition { |assoc| assoc.options[:through] }
323
- reify_has_many_directly(transaction_id, assoc_has_many_directly, model, options)
324
- reify_has_many_through(transaction_id, assoc_has_many_through, model, options)
325
- end
326
-
327
- # Restore the `model`'s has_many associations not associated through
328
- # another association.
329
- def reify_has_many_directly(transaction_id, associations, model, options = {})
330
- version_table_name = model.class.paper_trail.version_class.table_name
331
- each_enabled_association(associations) do |assoc|
332
- version_id_subquery = PaperTrail::VersionAssociation.
333
- joins(model.class.version_association_name).
334
- select("MIN(version_id)").
335
- where("foreign_key_name = ?", assoc.foreign_key).
336
- where("foreign_key_id = ?", model.id).
337
- where("#{version_table_name}.item_type = ?", assoc.class_name).
338
- where("created_at >= ? OR transaction_id = ?", options[:version_at], transaction_id).
339
- group("item_id").
340
- to_sql
341
- versions = versions_by_id(model.class, version_id_subquery)
342
- collection = Array.new model.send(assoc.name).reload # to avoid cache
343
- prepare_array_for_has_many(collection, options, versions)
344
- model.send(assoc.name).proxy_association.target = collection
345
- end
346
- end
347
-
348
- # Restore the `model`'s has_many associations through another association.
349
- # This must be called after the direct has_manys have been reified
350
- # (reify_has_many_directly).
351
- def reify_has_many_through(transaction_id, associations, model, options = {})
352
- each_enabled_association(associations) do |assoc|
353
- # Load the collection of through-models. For example, if `model` is a
354
- # Chapter, having many Paragraphs through Sections, then
355
- # `through_collection` will contain Sections.
356
- through_collection = model.send(assoc.options[:through])
357
-
358
- # Now, given the collection of "through" models (e.g. sections), load
359
- # the collection of "target" models (e.g. paragraphs)
360
- collection = hmt_collection(through_collection, assoc, options, transaction_id)
361
-
362
- # Finally, assign the `collection` of "target" models, e.g. to
363
- # `model.paragraphs`.
364
- model.send(assoc.name).proxy_association.target = collection
365
- end
366
- end
367
-
368
- def reify_has_and_belongs_to_many(transaction_id, model, options = {})
369
- model.class.reflect_on_all_associations(:has_and_belongs_to_many).each do |assoc|
370
- papertrail_enabled = assoc.klass.paper_trail.enabled?
371
- next unless
372
- model.class.paper_trail_save_join_tables.include?(assoc.name) ||
373
- papertrail_enabled
374
-
375
- version_ids = PaperTrail::VersionAssociation.
376
- where("foreign_key_name = ?", assoc.name).
377
- where("version_id = ?", transaction_id).
378
- pluck(:foreign_key_id)
379
-
380
- model.send(assoc.name).proxy_association.target =
381
- version_ids.map do |id|
382
- if papertrail_enabled
383
- version = load_version_for_habtm(
384
- assoc,
385
- id,
386
- transaction_id,
387
- options[:version_at]
388
- )
389
- if version
390
- next version.reify(
391
- options.merge(
392
- has_many: false,
393
- has_one: false,
394
- belongs_to: false,
395
- has_and_belongs_to_many: false
396
- )
397
- )
398
- end
399
- end
400
- assoc.klass.where(assoc.klass.primary_key => id).first
401
- end
107
+ reify_attribute(k, v, model, version)
402
108
  end
403
109
  end
404
110
 
@@ -412,40 +118,13 @@ module PaperTrail
412
118
  # this method returns the constant `Animal`. You can see this particular
413
119
  # example in action in `spec/models/animal_spec.rb`.
414
120
  #
121
+ # TODO: Duplication: similar `constantize` in VersionConcern#version_limit
415
122
  def version_reification_class(version, attrs)
416
123
  inheritance_column_name = version.item_type.constantize.inheritance_column
417
124
  inher_col_value = attrs[inheritance_column_name]
418
125
  class_name = inher_col_value.blank? ? version.item_type : inher_col_value
419
126
  class_name.constantize
420
127
  end
421
-
422
- # Given a SQL fragment that identifies the IDs of version records,
423
- # returns a `Hash` mapping those IDs to `Version`s.
424
- #
425
- # @api private
426
- # @param klass - An ActiveRecord class.
427
- # @param version_id_subquery - String. A SQL subquery that selects
428
- # the IDs of version records.
429
- # @return A `Hash` mapping IDs to `Version`s
430
- #
431
- def versions_by_id(klass, version_id_subquery)
432
- klass.
433
- paper_trail.version_class.
434
- where("id IN (#{version_id_subquery})").
435
- inject({}) { |a, e| a.merge!(e.item_id => e) }
436
- end
437
-
438
- # Temporarily suppress #save so we can reassociate with the reified
439
- # master of a has_one relationship. Since ActiveRecord 5 the related
440
- # object is saved when it is assigned to the association. ActiveRecord
441
- # 5 also happens to be the first version that provides #suppress.
442
- def without_persisting(record)
443
- if record.class.respond_to? :suppress
444
- record.class.suppress { yield }
445
- else
446
- yield
447
- end
448
- end
449
128
  end
450
129
  end
451
130
  end