paper_trail 4.2.0 → 7.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (171) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/paper_trail/install_generator.rb +91 -17
  3. data/lib/generators/paper_trail/templates/add_object_changes_to_versions.rb.erb +12 -0
  4. data/lib/generators/paper_trail/templates/{add_transaction_id_column_to_versions.rb → add_transaction_id_column_to_versions.rb.erb} +3 -1
  5. data/lib/generators/paper_trail/templates/create_version_associations.rb.erb +22 -0
  6. data/lib/generators/paper_trail/templates/{create_versions.rb → create_versions.rb.erb} +9 -7
  7. data/lib/paper_trail.rb +180 -148
  8. data/lib/paper_trail/attribute_serializers/README.md +10 -0
  9. data/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +80 -0
  10. data/lib/paper_trail/attribute_serializers/legacy_active_record_shim.rb +48 -0
  11. data/lib/paper_trail/attribute_serializers/object_attribute.rb +39 -0
  12. data/lib/paper_trail/attribute_serializers/object_changes_attribute.rb +42 -0
  13. data/lib/paper_trail/cleaner.rb +16 -10
  14. data/lib/paper_trail/config.rb +28 -27
  15. data/lib/paper_trail/frameworks/active_record/models/paper_trail/version.rb +5 -1
  16. data/lib/paper_trail/frameworks/active_record/models/paper_trail/version_association.rb +6 -2
  17. data/lib/paper_trail/frameworks/cucumber.rb +1 -0
  18. data/lib/paper_trail/frameworks/rails.rb +2 -7
  19. data/lib/paper_trail/frameworks/rails/controller.rb +20 -18
  20. data/lib/paper_trail/frameworks/rails/engine.rb +6 -1
  21. data/lib/paper_trail/frameworks/rspec.rb +17 -6
  22. data/lib/paper_trail/frameworks/rspec/helpers.rb +3 -1
  23. data/lib/paper_trail/has_paper_trail.rb +25 -503
  24. data/lib/paper_trail/model_config.rb +207 -0
  25. data/lib/paper_trail/queries/versions/where_object.rb +60 -0
  26. data/lib/paper_trail/queries/versions/where_object_changes.rb +68 -0
  27. data/lib/paper_trail/record_history.rb +2 -12
  28. data/lib/paper_trail/record_trail.rb +573 -0
  29. data/lib/paper_trail/reifier.rb +164 -215
  30. data/lib/paper_trail/reifiers/belongs_to.rb +48 -0
  31. data/lib/paper_trail/reifiers/has_and_belongs_to_many.rb +50 -0
  32. data/lib/paper_trail/reifiers/has_many.rb +110 -0
  33. data/lib/paper_trail/reifiers/has_many_through.rb +90 -0
  34. data/lib/paper_trail/reifiers/has_one.rb +76 -0
  35. data/lib/paper_trail/serializers/json.rb +16 -7
  36. data/lib/paper_trail/serializers/yaml.rb +9 -13
  37. data/lib/paper_trail/version_association_concern.rb +3 -5
  38. data/lib/paper_trail/version_concern.rb +138 -111
  39. data/lib/paper_trail/version_number.rb +10 -9
  40. metadata +95 -327
  41. data/.gitignore +0 -22
  42. data/.rspec +0 -2
  43. data/.travis.yml +0 -41
  44. data/CHANGELOG.md +0 -362
  45. data/CONTRIBUTING.md +0 -84
  46. data/Gemfile +0 -2
  47. data/MIT-LICENSE +0 -20
  48. data/README.md +0 -1535
  49. data/Rakefile +0 -30
  50. data/doc/bug_report_template.rb +0 -65
  51. data/gemfiles/ar3.gemfile +0 -61
  52. data/lib/generators/paper_trail/templates/add_object_changes_to_versions.rb +0 -10
  53. data/lib/generators/paper_trail/templates/create_version_associations.rb +0 -17
  54. data/lib/paper_trail/attributes_serialization.rb +0 -89
  55. data/lib/paper_trail/frameworks/sinatra.rb +0 -38
  56. data/paper_trail.gemspec +0 -59
  57. data/spec/generators/install_generator_spec.rb +0 -67
  58. data/spec/models/animal_spec.rb +0 -36
  59. data/spec/models/boolit_spec.rb +0 -48
  60. data/spec/models/callback_modifier_spec.rb +0 -96
  61. data/spec/models/fluxor_spec.rb +0 -19
  62. data/spec/models/gadget_spec.rb +0 -70
  63. data/spec/models/joined_version_spec.rb +0 -47
  64. data/spec/models/json_version_spec.rb +0 -103
  65. data/spec/models/kitchen/banana_spec.rb +0 -14
  66. data/spec/models/not_on_update_spec.rb +0 -19
  67. data/spec/models/post_with_status_spec.rb +0 -17
  68. data/spec/models/skipper_spec.rb +0 -46
  69. data/spec/models/thing_spec.rb +0 -11
  70. data/spec/models/version_spec.rb +0 -239
  71. data/spec/models/widget_spec.rb +0 -298
  72. data/spec/modules/paper_trail_spec.rb +0 -27
  73. data/spec/modules/version_concern_spec.rb +0 -32
  74. data/spec/modules/version_number_spec.rb +0 -44
  75. data/spec/paper_trail/config_spec.rb +0 -52
  76. data/spec/paper_trail_spec.rb +0 -66
  77. data/spec/rails_helper.rb +0 -34
  78. data/spec/requests/articles_spec.rb +0 -30
  79. data/spec/spec_helper.rb +0 -95
  80. data/spec/support/alt_db_init.rb +0 -59
  81. data/test/custom_json_serializer.rb +0 -13
  82. data/test/dummy/Rakefile +0 -7
  83. data/test/dummy/app/controllers/application_controller.rb +0 -20
  84. data/test/dummy/app/controllers/articles_controller.rb +0 -17
  85. data/test/dummy/app/controllers/test_controller.rb +0 -5
  86. data/test/dummy/app/controllers/widgets_controller.rb +0 -31
  87. data/test/dummy/app/helpers/application_helper.rb +0 -2
  88. data/test/dummy/app/models/animal.rb +0 -6
  89. data/test/dummy/app/models/article.rb +0 -16
  90. data/test/dummy/app/models/authorship.rb +0 -5
  91. data/test/dummy/app/models/book.rb +0 -9
  92. data/test/dummy/app/models/boolit.rb +0 -4
  93. data/test/dummy/app/models/callback_modifier.rb +0 -45
  94. data/test/dummy/app/models/cat.rb +0 -2
  95. data/test/dummy/app/models/chapter.rb +0 -9
  96. data/test/dummy/app/models/citation.rb +0 -5
  97. data/test/dummy/app/models/customer.rb +0 -4
  98. data/test/dummy/app/models/document.rb +0 -4
  99. data/test/dummy/app/models/dog.rb +0 -2
  100. data/test/dummy/app/models/editor.rb +0 -4
  101. data/test/dummy/app/models/editorship.rb +0 -5
  102. data/test/dummy/app/models/elephant.rb +0 -3
  103. data/test/dummy/app/models/fluxor.rb +0 -3
  104. data/test/dummy/app/models/foo_widget.rb +0 -2
  105. data/test/dummy/app/models/fruit.rb +0 -5
  106. data/test/dummy/app/models/gadget.rb +0 -3
  107. data/test/dummy/app/models/kitchen/banana.rb +0 -5
  108. data/test/dummy/app/models/legacy_widget.rb +0 -4
  109. data/test/dummy/app/models/line_item.rb +0 -4
  110. data/test/dummy/app/models/not_on_update.rb +0 -4
  111. data/test/dummy/app/models/order.rb +0 -5
  112. data/test/dummy/app/models/paragraph.rb +0 -5
  113. data/test/dummy/app/models/person.rb +0 -38
  114. data/test/dummy/app/models/post.rb +0 -3
  115. data/test/dummy/app/models/post_with_status.rb +0 -8
  116. data/test/dummy/app/models/protected_widget.rb +0 -3
  117. data/test/dummy/app/models/quotation.rb +0 -5
  118. data/test/dummy/app/models/section.rb +0 -6
  119. data/test/dummy/app/models/skipper.rb +0 -6
  120. data/test/dummy/app/models/song.rb +0 -32
  121. data/test/dummy/app/models/thing.rb +0 -3
  122. data/test/dummy/app/models/translation.rb +0 -4
  123. data/test/dummy/app/models/whatchamajigger.rb +0 -4
  124. data/test/dummy/app/models/widget.rb +0 -15
  125. data/test/dummy/app/models/wotsit.rb +0 -8
  126. data/test/dummy/app/versions/joined_version.rb +0 -5
  127. data/test/dummy/app/versions/json_version.rb +0 -3
  128. data/test/dummy/app/versions/kitchen/banana_version.rb +0 -5
  129. data/test/dummy/app/versions/post_version.rb +0 -3
  130. data/test/dummy/app/views/layouts/application.html.erb +0 -14
  131. data/test/dummy/config.ru +0 -4
  132. data/test/dummy/config/application.rb +0 -69
  133. data/test/dummy/config/boot.rb +0 -10
  134. data/test/dummy/config/database.mysql.yml +0 -19
  135. data/test/dummy/config/database.postgres.yml +0 -15
  136. data/test/dummy/config/database.sqlite.yml +0 -15
  137. data/test/dummy/config/environment.rb +0 -5
  138. data/test/dummy/config/environments/development.rb +0 -40
  139. data/test/dummy/config/environments/production.rb +0 -73
  140. data/test/dummy/config/environments/test.rb +0 -41
  141. data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
  142. data/test/dummy/config/initializers/inflections.rb +0 -10
  143. data/test/dummy/config/initializers/mime_types.rb +0 -5
  144. data/test/dummy/config/initializers/paper_trail.rb +0 -10
  145. data/test/dummy/config/initializers/secret_token.rb +0 -7
  146. data/test/dummy/config/initializers/session_store.rb +0 -8
  147. data/test/dummy/config/locales/en.yml +0 -5
  148. data/test/dummy/config/routes.rb +0 -4
  149. data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +0 -287
  150. data/test/dummy/db/schema.rb +0 -246
  151. data/test/dummy/script/rails +0 -6
  152. data/test/functional/controller_test.rb +0 -91
  153. data/test/functional/enabled_for_controller_test.rb +0 -29
  154. data/test/functional/modular_sinatra_test.rb +0 -48
  155. data/test/functional/sinatra_test.rb +0 -49
  156. data/test/functional/thread_safety_test.rb +0 -48
  157. data/test/paper_trail_test.rb +0 -38
  158. data/test/test_helper.rb +0 -105
  159. data/test/time_travel_helper.rb +0 -15
  160. data/test/unit/associations_test.rb +0 -726
  161. data/test/unit/cleaner_test.rb +0 -182
  162. data/test/unit/inheritance_column_test.rb +0 -43
  163. data/test/unit/model_test.rb +0 -1373
  164. data/test/unit/protected_attrs_test.rb +0 -47
  165. data/test/unit/serializer_test.rb +0 -117
  166. data/test/unit/serializers/json_test.rb +0 -88
  167. data/test/unit/serializers/mixin_json_test.rb +0 -36
  168. data/test/unit/serializers/mixin_yaml_test.rb +0 -49
  169. data/test/unit/serializers/yaml_test.rb +0 -52
  170. data/test/unit/timestamp_test.rb +0 -43
  171. data/test/unit/version_test.rb +0 -119
@@ -1,269 +1,218 @@
1
- module PaperTrail
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
 
8
+ module PaperTrail
3
9
  # Given a version record and some options, builds a new model object.
4
10
  # @api private
5
11
  module Reifier
6
12
  class << self
7
-
8
13
  # See `VersionConcern#reify` for documentation.
9
14
  # @api private
10
15
  def reify(version, options)
11
- options = options.dup
16
+ options = apply_defaults_to(options, version)
17
+ attrs = version.object_deserialized
18
+ model = init_model(attrs, options, version)
19
+ reify_attributes(model, version, attrs)
20
+ model.send "#{model.class.version_association_name}=", version
21
+ reify_associations(model, options, version)
22
+ model
23
+ end
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
12
36
 
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
- )
37
+ private
20
38
 
21
- attrs = version.class.object_col_is_json? ?
22
- version.object :
23
- PaperTrail.serializer.load(version.object)
39
+ # Given a hash of `options` for `.reify`, return a new hash with default
40
+ # values applied.
41
+ # @api private
42
+ def apply_defaults_to(options, version)
43
+ {
44
+ version_at: version.created_at,
45
+ mark_for_destruction: false,
46
+ has_one: false,
47
+ has_many: false,
48
+ belongs_to: false,
49
+ has_and_belongs_to_many: false,
50
+ unversioned_attributes: :nil
51
+ }.merge(options)
52
+ end
24
53
 
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.
54
+ # @api private
55
+ def each_enabled_association(associations)
56
+ associations.each do |assoc|
57
+ next unless assoc.klass.paper_trail.enabled?
58
+ yield assoc
59
+ end
60
+ end
36
61
 
62
+ # Initialize a model object suitable for reifying `version` into. Does
63
+ # not perform reification, merely instantiates the appropriate model
64
+ # class and, if specified by `options[:unversioned_attributes]`, sets
65
+ # unversioned attributes to `nil`.
66
+ #
67
+ # Normally a polymorphic belongs_to relationship allows us to get the
68
+ # object we belong to by calling, in this case, `item`. However this
69
+ # returns nil if `item` has been destroyed, and we need to be able to
70
+ # retrieve destroyed objects.
71
+ #
72
+ # In this situation we constantize the `item_type` to get hold of the
73
+ # class...except when the stored object's attributes include a `type`
74
+ # key. If this is the case, the object we belong to is using single
75
+ # table inheritance (STI) and the `item_type` will be the base class,
76
+ # not the actual subclass. If `type` is present but empty, the class is
77
+ # the base class.
78
+ def init_model(attrs, options, version)
37
79
  if options[:dup] != true && version.item
38
80
  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
81
  if options[:unversioned_attributes] == :nil
42
- (model.attribute_names - attrs.keys).each { |k| attrs[k] = nil }
82
+ init_unversioned_attrs(attrs, model)
43
83
  end
44
84
  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
85
+ klass = version_reification_class(version, attrs)
50
86
  # The `dup` option always returns a new object, otherwise we should
51
87
  # 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?
88
+ find_cond = { klass.primary_key => version.item_id }
89
+ if options[:dup] || (item_found = klass.unscoped.where(find_cond).first).nil?
53
90
  model = klass.new
54
91
  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
- )
92
+ model = item_found
93
+ init_unversioned_attrs(attrs, model)
74
94
  end
75
95
  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
96
  model
88
97
  end
89
98
 
90
- private
91
-
92
- # Replaces each record in `array` with its reified version, if present
93
- # in `versions`.
94
- #
99
+ # Look for attributes that exist in `model` and not in this version.
100
+ # These attributes should be set to nil. Modifies `attrs`.
95
101
  # @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`
102
+ def init_unversioned_attrs(attrs, model)
103
+ (model.attribute_names - attrs.keys).each { |k| attrs[k] = nil }
104
+ end
105
+
106
+ # Reify onto `model` an attribute named `k` with value `v` from `version`.
100
107
  #
101
- # Once modified by this method, `array` will be assigned to the
102
- # AR association currently being reified.
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.
103
112
  #
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
113
+ # @api private
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})."
124
+ )
115
125
  end
126
+ end
116
127
 
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!
128
+ # Reify onto `model` all the attributes of `version`.
129
+ # @api private
130
+ def reify_attributes(model, version, attrs)
131
+ AttributeSerializers::ObjectAttribute.new(model.class).deserialize(attrs)
132
+ attrs.each do |k, v|
133
+ reify_attribute(k, v, model, version)
134
+ end
135
+ end
127
136
 
128
- nil
137
+ # @api private
138
+ def reify_associations(model, options, version)
139
+ if options[:has_one]
140
+ reify_has_one_associations(version.transaction_id, model, options)
141
+ end
142
+ if options[:belongs_to]
143
+ reify_belongs_to_associations(version.transaction_id, model, options)
144
+ end
145
+ if options[:has_many]
146
+ reify_has_manys(version.transaction_id, model, options)
147
+ end
148
+ if options[:has_and_belongs_to_many]
149
+ reify_habtm_associations version.transaction_id, model, options
150
+ end
129
151
  end
130
152
 
131
153
  # Restore the `model`'s has_one associations as they were when this
132
154
  # version was superseded by the next (because that's what the user was
133
155
  # 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
156
+ # @api private
157
+ def reify_has_one_associations(transaction_id, model, options = {})
158
+ associations = model.class.reflect_on_all_associations(:has_one)
159
+ each_enabled_association(associations) do |assoc|
160
+ Reifiers::HasOne.reify(assoc, model, options, transaction_id)
162
161
  end
163
162
  end
164
163
 
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)
164
+ # Reify all `belongs_to` associations of `model`.
165
+ # @api private
166
+ def reify_belongs_to_associations(transaction_id, model, options = {})
167
+ associations = model.class.reflect_on_all_associations(:belongs_to)
168
+ each_enabled_association(associations) do |assoc|
169
+ Reifiers::BelongsTo.reify(assoc, model, options, transaction_id)
170
+ end
174
171
  end
175
172
 
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
173
+ # Reify all direct (not `through`) `has_many` associations of `model`.
174
+ # @api private
175
+ def reify_has_many_associations(transaction_id, associations, model, options = {})
176
+ version_table_name = model.class.paper_trail.version_class.table_name
177
+ each_enabled_association(associations) do |assoc|
178
+ Reifiers::HasMany.reify(assoc, model, options, transaction_id, version_table_name)
195
179
  end
196
180
  end
197
181
 
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
182
+ # Reify all HMT associations of `model`. This must be called after the
183
+ # direct (non-`through`) has_manys have been reified.
184
+ # @api private
185
+ def reify_has_many_through_associations(transaction_id, associations, model, options = {})
186
+ each_enabled_association(associations) do |assoc|
187
+ Reifiers::HasManyThrough.reify(assoc, model, options, transaction_id)
188
+ end
189
+ end
246
190
 
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
191
+ # Reify all HABTM associations of `model`.
192
+ # @api private
193
+ def reify_habtm_associations(transaction_id, model, options = {})
194
+ model.class.reflect_on_all_associations(:has_and_belongs_to_many).each do |assoc|
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)
250
198
  end
251
199
  end
252
200
 
253
- # Given a SQL fragment that identifies the IDs of version records,
254
- # returns a `Hash` mapping those IDs to `Version`s.
201
+ # Given a `version`, return the class to reify. This method supports
202
+ # Single Table Inheritance (STI) with custom inheritance columns.
255
203
  #
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
204
+ # For example, imagine a `version` whose `item_type` is "Animal". The
205
+ # `animals` table is an STI table (it has cats and dogs) and it has a
206
+ # custom inheritance column, `species`. If `attrs["species"]` is "Dog",
207
+ # this method returns the constant `Dog`. If `attrs["species"]` is blank,
208
+ # this method returns the constant `Animal`. You can see this particular
209
+ # example in action in `spec/models/animal_spec.rb`.
261
210
  #
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) }
211
+ def version_reification_class(version, attrs)
212
+ inheritance_column_name = version.item_type.constantize.inheritance_column
213
+ inher_col_value = attrs[inheritance_column_name]
214
+ class_name = inher_col_value.blank? ? version.item_type : inher_col_value
215
+ class_name.constantize
267
216
  end
268
217
  end
269
218
  end
@@ -0,0 +1,48 @@
1
+ module PaperTrail
2
+ module Reifiers
3
+ # Reify a single `belongs_to` association of `model`.
4
+ # @api private
5
+ module BelongsTo
6
+ class << self
7
+ # @api private
8
+ def reify(assoc, model, options, transaction_id)
9
+ id = model.send(assoc.foreign_key)
10
+ version = load_version(assoc, id, transaction_id, options[:version_at])
11
+ record = load_record(assoc, id, options, version)
12
+ model.send("#{assoc.name}=".to_sym, record)
13
+ end
14
+
15
+ private
16
+
17
+ # Given a `belongs_to` association and a `version`, return a record that
18
+ # can be assigned in order to reify that association.
19
+ # @api private
20
+ def load_record(assoc, id, options, version)
21
+ if version.nil?
22
+ assoc.klass.where(assoc.klass.primary_key => id).first
23
+ else
24
+ version.reify(
25
+ options.merge(
26
+ has_many: false,
27
+ has_one: false,
28
+ belongs_to: false,
29
+ has_and_belongs_to_many: false
30
+ )
31
+ )
32
+ end
33
+ end
34
+
35
+ # Given a `belongs_to` association and an `id`, return a version record
36
+ # from the point in time identified by `transaction_id` or `version_at`.
37
+ # @api private
38
+ def load_version(assoc, id, transaction_id, version_at)
39
+ assoc.klass.paper_trail.version_class.
40
+ where("item_type = ?", assoc.class_name).
41
+ where("item_id = ?", id).
42
+ where("created_at >= ? OR transaction_id = ?", version_at, transaction_id).
43
+ order("id").limit(1).first
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end