paper_trail 4.0.0 → 5.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (158) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CONTRIBUTING.md +105 -0
  3. data/.github/ISSUE_TEMPLATE.md +13 -0
  4. data/.gitignore +2 -0
  5. data/.rubocop.yml +100 -0
  6. data/.rubocop_todo.yml +14 -0
  7. data/.travis.yml +11 -10
  8. data/Appraisals +37 -0
  9. data/CHANGELOG.md +173 -8
  10. data/Gemfile +1 -1
  11. data/README.md +641 -470
  12. data/Rakefile +19 -19
  13. data/doc/bug_report_template.rb +71 -0
  14. data/doc/warning_about_not_setting_whodunnit.md +32 -0
  15. data/gemfiles/ar3.gemfile +18 -0
  16. data/gemfiles/ar4.gemfile +7 -0
  17. data/gemfiles/ar5.gemfile +13 -0
  18. data/lib/generators/paper_trail/install_generator.rb +26 -18
  19. data/lib/generators/paper_trail/templates/add_object_changes_to_versions.rb +3 -1
  20. data/lib/generators/paper_trail/templates/add_transaction_id_column_to_versions.rb +2 -0
  21. data/lib/generators/paper_trail/templates/create_version_associations.rb +9 -4
  22. data/lib/generators/paper_trail/templates/create_versions.rb +53 -5
  23. data/lib/paper_trail/attribute_serializers/README.md +10 -0
  24. data/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +58 -0
  25. data/lib/paper_trail/attribute_serializers/legacy_active_record_shim.rb +48 -0
  26. data/lib/paper_trail/attribute_serializers/object_attribute.rb +39 -0
  27. data/lib/paper_trail/attribute_serializers/object_changes_attribute.rb +42 -0
  28. data/lib/paper_trail/cleaner.rb +41 -18
  29. data/lib/paper_trail/config.rb +42 -26
  30. data/lib/paper_trail/frameworks/active_record/models/paper_trail/version.rb +5 -1
  31. data/lib/paper_trail/frameworks/active_record/models/paper_trail/version_association.rb +6 -2
  32. data/lib/paper_trail/frameworks/active_record.rb +2 -2
  33. data/lib/paper_trail/frameworks/cucumber.rb +1 -0
  34. data/lib/paper_trail/frameworks/rails/controller.rb +50 -14
  35. data/lib/paper_trail/frameworks/rails/engine.rb +6 -1
  36. data/lib/paper_trail/frameworks/rails.rb +2 -7
  37. data/lib/paper_trail/frameworks/rspec/helpers.rb +3 -1
  38. data/lib/paper_trail/frameworks/rspec.rb +5 -5
  39. data/lib/paper_trail/frameworks/sinatra.rb +8 -5
  40. data/lib/paper_trail/has_paper_trail.rb +381 -221
  41. data/lib/paper_trail/record_history.rb +57 -0
  42. data/lib/paper_trail/reifier.rb +450 -0
  43. data/lib/paper_trail/serializers/json.rb +7 -7
  44. data/lib/paper_trail/serializers/yaml.rb +31 -12
  45. data/lib/paper_trail/version_association_concern.rb +6 -2
  46. data/lib/paper_trail/version_concern.rb +200 -287
  47. data/lib/paper_trail/version_number.rb +6 -9
  48. data/lib/paper_trail.rb +169 -137
  49. data/paper_trail.gemspec +41 -43
  50. data/spec/generators/install_generator_spec.rb +24 -25
  51. data/spec/generators/paper_trail/templates/create_versions_spec.rb +51 -0
  52. data/spec/models/animal_spec.rb +23 -6
  53. data/spec/models/boolit_spec.rb +8 -8
  54. data/spec/models/callback_modifier_spec.rb +96 -0
  55. data/spec/models/car_spec.rb +13 -0
  56. data/spec/models/fluxor_spec.rb +3 -3
  57. data/spec/models/gadget_spec.rb +19 -19
  58. data/spec/models/joined_version_spec.rb +3 -3
  59. data/spec/models/json_version_spec.rb +50 -28
  60. data/spec/models/kitchen/banana_spec.rb +3 -3
  61. data/spec/models/not_on_update_spec.rb +7 -4
  62. data/spec/models/post_with_status_spec.rb +13 -3
  63. data/spec/models/skipper_spec.rb +40 -11
  64. data/spec/models/thing_spec.rb +4 -4
  65. data/spec/models/truck_spec.rb +5 -0
  66. data/spec/models/vehicle_spec.rb +5 -0
  67. data/spec/models/version_spec.rb +103 -59
  68. data/spec/models/widget_spec.rb +86 -55
  69. data/spec/modules/paper_trail_spec.rb +2 -2
  70. data/spec/modules/version_concern_spec.rb +11 -12
  71. data/spec/modules/version_number_spec.rb +3 -4
  72. data/spec/paper_trail/config_spec.rb +33 -0
  73. data/spec/paper_trail_spec.rb +16 -14
  74. data/spec/rails_helper.rb +10 -9
  75. data/spec/requests/articles_spec.rb +11 -7
  76. data/spec/spec_helper.rb +42 -17
  77. data/spec/support/alt_db_init.rb +8 -13
  78. data/test/custom_json_serializer.rb +3 -3
  79. data/test/dummy/Rakefile +2 -2
  80. data/test/dummy/app/controllers/application_controller.rb +21 -8
  81. data/test/dummy/app/controllers/articles_controller.rb +11 -8
  82. data/test/dummy/app/controllers/widgets_controller.rb +13 -12
  83. data/test/dummy/app/models/animal.rb +1 -1
  84. data/test/dummy/app/models/article.rb +19 -11
  85. data/test/dummy/app/models/authorship.rb +1 -1
  86. data/test/dummy/app/models/bar_habtm.rb +4 -0
  87. data/test/dummy/app/models/book.rb +4 -4
  88. data/test/dummy/app/models/boolit.rb +1 -1
  89. data/test/dummy/app/models/callback_modifier.rb +45 -0
  90. data/test/dummy/app/models/car.rb +3 -0
  91. data/test/dummy/app/models/chapter.rb +9 -0
  92. data/test/dummy/app/models/citation.rb +5 -0
  93. data/test/dummy/app/models/customer.rb +1 -1
  94. data/test/dummy/app/models/document.rb +2 -2
  95. data/test/dummy/app/models/editor.rb +1 -1
  96. data/test/dummy/app/models/foo_habtm.rb +5 -0
  97. data/test/dummy/app/models/fruit.rb +2 -2
  98. data/test/dummy/app/models/gadget.rb +1 -1
  99. data/test/dummy/app/models/kitchen/banana.rb +1 -1
  100. data/test/dummy/app/models/legacy_widget.rb +2 -2
  101. data/test/dummy/app/models/line_item.rb +1 -1
  102. data/test/dummy/app/models/not_on_update.rb +1 -1
  103. data/test/dummy/app/models/paragraph.rb +5 -0
  104. data/test/dummy/app/models/person.rb +6 -6
  105. data/test/dummy/app/models/post.rb +1 -1
  106. data/test/dummy/app/models/post_with_status.rb +1 -1
  107. data/test/dummy/app/models/quotation.rb +5 -0
  108. data/test/dummy/app/models/section.rb +6 -0
  109. data/test/dummy/app/models/skipper.rb +2 -2
  110. data/test/dummy/app/models/song.rb +13 -4
  111. data/test/dummy/app/models/thing.rb +2 -2
  112. data/test/dummy/app/models/translation.rb +2 -2
  113. data/test/dummy/app/models/truck.rb +4 -0
  114. data/test/dummy/app/models/vehicle.rb +4 -0
  115. data/test/dummy/app/models/whatchamajigger.rb +1 -1
  116. data/test/dummy/app/models/widget.rb +7 -6
  117. data/test/dummy/app/versions/joined_version.rb +4 -3
  118. data/test/dummy/app/versions/json_version.rb +1 -1
  119. data/test/dummy/app/versions/kitchen/banana_version.rb +1 -1
  120. data/test/dummy/app/versions/post_version.rb +2 -2
  121. data/test/dummy/config/application.rb +20 -9
  122. data/test/dummy/config/boot.rb +5 -5
  123. data/test/dummy/config/database.postgres.yml +1 -1
  124. data/test/dummy/config/environment.rb +1 -1
  125. data/test/dummy/config/environments/development.rb +4 -3
  126. data/test/dummy/config/environments/production.rb +3 -2
  127. data/test/dummy/config/environments/test.rb +15 -5
  128. data/test/dummy/config/initializers/backtrace_silencers.rb +4 -2
  129. data/test/dummy/config/initializers/paper_trail.rb +4 -3
  130. data/test/dummy/config/initializers/secret_token.rb +3 -1
  131. data/test/dummy/config/initializers/session_store.rb +1 -1
  132. data/test/dummy/config/routes.rb +2 -2
  133. data/test/dummy/config.ru +1 -1
  134. data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +148 -68
  135. data/test/dummy/db/schema.rb +119 -31
  136. data/test/dummy/script/rails +6 -4
  137. data/test/functional/controller_test.rb +34 -35
  138. data/test/functional/enabled_for_controller_test.rb +6 -7
  139. data/test/functional/modular_sinatra_test.rb +43 -38
  140. data/test/functional/sinatra_test.rb +49 -40
  141. data/test/functional/thread_safety_test.rb +4 -6
  142. data/test/paper_trail_test.rb +15 -14
  143. data/test/test_helper.rb +78 -18
  144. data/test/time_travel_helper.rb +1 -15
  145. data/test/unit/associations_test.rb +1016 -0
  146. data/test/unit/cleaner_test.rb +66 -60
  147. data/test/unit/inheritance_column_test.rb +19 -19
  148. data/test/unit/model_test.rb +646 -1071
  149. data/test/unit/protected_attrs_test.rb +19 -14
  150. data/test/unit/serializer_test.rb +44 -43
  151. data/test/unit/serializers/json_test.rb +28 -21
  152. data/test/unit/serializers/mixin_json_test.rb +15 -14
  153. data/test/unit/serializers/mixin_yaml_test.rb +20 -16
  154. data/test/unit/serializers/yaml_test.rb +16 -14
  155. data/test/unit/timestamp_test.rb +10 -12
  156. data/test/unit/version_test.rb +88 -70
  157. metadata +166 -72
  158. data/gemfiles/3.0.gemfile +0 -52
@@ -1,134 +1,171 @@
1
- require 'active_support/concern'
1
+ require "active_support/concern"
2
+ require "paper_trail/attribute_serializers/object_changes_attribute"
2
3
 
3
4
  module PaperTrail
5
+ # Originally, PaperTrail did not provide this module, and all of this
6
+ # functionality was in `PaperTrail::Version`. That model still exists (and is
7
+ # used by most apps) but by moving the functionality to this module, people
8
+ # can include this concern instead of sub-classing the `Version` model.
4
9
  module VersionConcern
5
10
  extend ::ActiveSupport::Concern
6
11
 
7
12
  included do
8
- belongs_to :item, :polymorphic => true
13
+ belongs_to :item, polymorphic: true
9
14
 
10
- # Since the test suite has test coverage for this, we want to declare the
11
- # association when the test suite is running. This makes it pass
12
- # when DB is not initialized prior to test runs such as when we run on
13
- # Travis CI (there won't be a db in `test/dummy/db/`)
15
+ # Since the test suite has test coverage for this, we want to declare
16
+ # the association when the test suite is running. This makes it pass when
17
+ # DB is not initialized prior to test runs such as when we run on Travis
18
+ # CI (there won't be a db in `test/dummy/db/`).
14
19
  if PaperTrail.config.track_associations?
15
- has_many :version_associations, :dependent => :destroy
20
+ has_many :version_associations, dependent: :destroy
16
21
  end
17
22
 
18
23
  validates_presence_of :event
19
24
 
20
25
  if PaperTrail.active_record_protected_attributes?
21
- attr_accessible :item_type, :item_id, :event, :whodunnit, :object, :object_changes, :transaction_id, :created_at
26
+ attr_accessible(
27
+ :item_type,
28
+ :item_id,
29
+ :event,
30
+ :whodunnit,
31
+ :object,
32
+ :object_changes,
33
+ :transaction_id,
34
+ :created_at
35
+ )
22
36
  end
23
37
 
24
38
  after_create :enforce_version_limit!
25
39
 
26
- scope :within_transaction, lambda { |id| where :transaction_id => id }
40
+ scope :within_transaction, ->(id) { where transaction_id: id }
27
41
  end
28
42
 
43
+ # :nodoc:
29
44
  module ClassMethods
30
45
  def with_item_keys(item_type, item_id)
31
- where :item_type => item_type, :item_id => item_id
46
+ where item_type: item_type, item_id: item_id
32
47
  end
33
48
 
34
49
  def creates
35
- where :event => 'create'
50
+ where event: "create"
36
51
  end
37
52
 
38
53
  def updates
39
- where :event => 'update'
54
+ where event: "update"
40
55
  end
41
56
 
42
57
  def destroys
43
- where :event => 'destroy'
58
+ where event: "destroy"
44
59
  end
45
60
 
46
61
  def not_creates
47
- where 'event <> ?', 'create'
62
+ where "event <> ?", "create"
48
63
  end
49
64
 
50
- # Expects `obj` to be an instance of `PaperTrail::Version` by default, but can accept a timestamp if
51
- # `timestamp_arg` receives `true`
65
+ # Returns versions after `obj`.
66
+ #
67
+ # @param obj - a `Version` or a timestamp
68
+ # @param timestamp_arg - boolean - When true, `obj` is a timestamp.
69
+ # Default: false.
70
+ # @return `ActiveRecord::Relation`
71
+ # @api public
52
72
  def subsequent(obj, timestamp_arg = false)
53
- if timestamp_arg != true && self.primary_key_is_int?
73
+ if timestamp_arg != true && primary_key_is_int?
54
74
  return where(arel_table[primary_key].gt(obj.id)).order(arel_table[primary_key].asc)
55
75
  end
56
76
 
57
77
  obj = obj.send(PaperTrail.timestamp_field) if obj.is_a?(self)
58
- where(arel_table[PaperTrail.timestamp_field].gt(obj)).order(self.timestamp_sort_order)
78
+ where(arel_table[PaperTrail.timestamp_field].gt(obj)).order(timestamp_sort_order)
59
79
  end
60
80
 
81
+ # Returns versions before `obj`.
82
+ #
83
+ # @param obj - a `Version` or a timestamp
84
+ # @param timestamp_arg - boolean - When true, `obj` is a timestamp.
85
+ # Default: false.
86
+ # @return `ActiveRecord::Relation`
87
+ # @api public
61
88
  def preceding(obj, timestamp_arg = false)
62
- if timestamp_arg != true && self.primary_key_is_int?
89
+ if timestamp_arg != true && primary_key_is_int?
63
90
  return where(arel_table[primary_key].lt(obj.id)).order(arel_table[primary_key].desc)
64
91
  end
65
92
 
66
93
  obj = obj.send(PaperTrail.timestamp_field) if obj.is_a?(self)
67
- where(arel_table[PaperTrail.timestamp_field].lt(obj)).order(self.timestamp_sort_order('desc'))
94
+ where(arel_table[PaperTrail.timestamp_field].lt(obj)).
95
+ order(timestamp_sort_order("desc"))
68
96
  end
69
97
 
70
-
71
98
  def between(start_time, end_time)
72
99
  where(
73
100
  arel_table[PaperTrail.timestamp_field].gt(start_time).
74
101
  and(arel_table[PaperTrail.timestamp_field].lt(end_time))
75
- ).order(self.timestamp_sort_order)
102
+ ).order(timestamp_sort_order)
76
103
  end
77
104
 
78
- # defaults to using the primary key as the secondary sort order if possible
79
- def timestamp_sort_order(direction = 'asc')
105
+ # Defaults to using the primary key as the secondary sort order if
106
+ # possible.
107
+ def timestamp_sort_order(direction = "asc")
80
108
  [arel_table[PaperTrail.timestamp_field].send(direction.downcase)].tap do |array|
81
- array << arel_table[primary_key].send(direction.downcase) if self.primary_key_is_int?
109
+ array << arel_table[primary_key].send(direction.downcase) if primary_key_is_int?
82
110
  end
83
111
  end
84
112
 
113
+ # Query the `versions.objects` column using the SQL LIKE operator.
85
114
  # Performs an attribute search on the serialized object by invoking the
86
115
  # identically-named method in the serializer being used.
116
+ # @api public
87
117
  def where_object(args = {})
88
- raise ArgumentError, 'expected to receive a Hash' unless args.is_a?(Hash)
89
-
90
- if columns_hash['object'].type == :jsonb
91
- where_conditions = "object @> '#{args.to_json}'::jsonb"
92
- elsif columns_hash['object'].type == :json
93
- where_conditions = args.map do |field, value|
94
- "object->>'#{field}' = '#{value}'"
118
+ raise ArgumentError, "expected to receive a Hash" unless args.is_a?(Hash)
119
+
120
+ if columns_hash["object"].type == :jsonb
121
+ where("object @> ?", args.to_json)
122
+ elsif columns_hash["object"].type == :json
123
+ predicates = []
124
+ values = []
125
+ args.each do |field, value|
126
+ predicates.push "object->>? = ?"
127
+ values.concat([field, value.to_s])
95
128
  end
96
- where_conditions = where_conditions.join(" AND ")
129
+ sql = predicates.join(" and ")
130
+ where(sql, *values)
97
131
  else
98
132
  arel_field = arel_table[:object]
99
-
100
- where_conditions = args.map do |field, value|
133
+ where_conditions = args.map { |field, value|
101
134
  PaperTrail.serializer.where_object_condition(arel_field, field, value)
102
- end.reduce do |condition1, condition2|
103
- condition1.and(condition2)
104
- end
135
+ }
136
+ where_conditions = where_conditions.reduce { |a, e| a.and(e) }
137
+ where(where_conditions)
105
138
  end
106
-
107
- where(where_conditions)
108
139
  end
109
140
 
141
+ # Query the `versions.object_changes` column by attributes, using the
142
+ # SQL LIKE operator.
143
+ # @api public
110
144
  def where_object_changes(args = {})
111
- raise ArgumentError, 'expected to receive a Hash' unless args.is_a?(Hash)
145
+ raise ArgumentError, "expected to receive a Hash" unless args.is_a?(Hash)
112
146
 
113
- if columns_hash['object_changes'].type == :jsonb
147
+ if columns_hash["object_changes"].type == :jsonb
114
148
  args.each { |field, value| args[field] = [value] }
115
- where_conditions = "object_changes @> '#{args.to_json}'::jsonb"
116
- elsif columns_hash['object'].type == :json
117
- where_conditions = args.map do |field, value|
118
- "((object_changes->>'#{field}' ILIKE '[#{value.to_json},%') OR (object_changes->>'#{field}' ILIKE '[%,#{value.to_json}]%'))"
149
+ where("object_changes @> ?", args.to_json)
150
+ elsif columns_hash["object"].type == :json
151
+ predicates = []
152
+ values = []
153
+ args.each do |field, value|
154
+ predicates.push(
155
+ "((object_changes->>? ILIKE ?) OR (object_changes->>? ILIKE ?))"
156
+ )
157
+ values.concat([field, "[#{value.to_json},%", field, "[%,#{value.to_json}]%"])
119
158
  end
120
- where_conditions = where_conditions.join(" AND ")
159
+ sql = predicates.join(" and ")
160
+ where(sql, *values)
121
161
  else
122
162
  arel_field = arel_table[:object_changes]
123
-
124
- where_conditions = args.map do |field, value|
163
+ where_conditions = args.map { |field, value|
125
164
  PaperTrail.serializer.where_object_changes_condition(arel_field, field, value)
126
- end.reduce do |condition1, condition2|
127
- condition1.and(condition2)
128
- end
165
+ }
166
+ where_conditions = where_conditions.reduce { |a, e| a.and(e) }
167
+ where(where_conditions)
129
168
  end
130
-
131
- where(where_conditions)
132
169
  end
133
170
 
134
171
  def primary_key_is_int?
@@ -137,140 +174,92 @@ module PaperTrail
137
174
  true
138
175
  end
139
176
 
140
- # Returns whether the `object` column is using the `json` type supported by PostgreSQL
177
+ # Returns whether the `object` column is using the `json` type supported
178
+ # by PostgreSQL.
141
179
  def object_col_is_json?
142
- [:json, :jsonb].include?(columns_hash['object'].type)
180
+ [:json, :jsonb].include?(columns_hash["object"].type)
143
181
  end
144
182
 
145
- # Returns whether the `object_changes` column is using the `json` type supported by PostgreSQL
183
+ # Returns whether the `object_changes` column is using the `json` type
184
+ # supported by PostgreSQL.
146
185
  def object_changes_col_is_json?
147
- [:json, :jsonb].include?(columns_hash['object_changes'].try(:type))
186
+ [:json, :jsonb].include?(columns_hash["object_changes"].try(:type))
187
+ end
188
+ end
189
+
190
+ # @api private
191
+ def object_deserialized
192
+ if self.class.object_col_is_json?
193
+ object
194
+ else
195
+ PaperTrail.serializer.load(object)
148
196
  end
149
197
  end
150
198
 
151
199
  # Restore the item from this version.
152
200
  #
153
- # Optionally this can also restore all :has_one and :has_many (including has_many :through) associations as
154
- # they were "at the time", if they are also being versioned by PaperTrail.
201
+ # Optionally this can also restore all :has_one and :has_many (including
202
+ # has_many :through) associations as they were "at the time", if they are
203
+ # also being versioned by PaperTrail.
155
204
  #
156
205
  # Options:
157
- # :has_one set to `true` to also reify has_one associations. Default is `false`.
158
- # :has_many set to `true` to also reify has_many and has_many :through associations.
159
- # Default is `false`.
160
- # :mark_for_destruction set to `true` to mark the has_one/has_many associations that did not exist in the
161
- # reified version for destruction, instead of remove them. Default is `false`.
162
- # This option is handy for people who want to persist the reified version.
163
- # :dup `false` default behavior
164
- # `true` it always create a new object instance. It is useful for comparing two versions of the same object
206
+ #
207
+ # - :has_one
208
+ # - `true` - Also reify has_one associations.
209
+ # - `false - Default.
210
+ # - :has_many
211
+ # - `true` - Also reify has_many and has_many :through associations.
212
+ # - `false` - Default.
213
+ # - :mark_for_destruction
214
+ # - `true` - Mark the has_one/has_many associations that did not exist in
215
+ # the reified version for destruction, instead of removing them.
216
+ # - `false` - Default. Useful for persisting the reified version.
217
+ # - :dup
218
+ # - `false` - Default.
219
+ # - `true` - Always create a new object instance. Useful for
220
+ # comparing two versions of the same object.
221
+ # - :unversioned_attributes
222
+ # - `:nil` - Default. Attributes undefined in version record are set to
223
+ # nil in reified record.
224
+ # - `:preserve` - Attributes undefined in version record are not modified.
225
+ #
165
226
  def reify(options = {})
166
227
  return nil if object.nil?
167
-
168
228
  without_identity_map do
169
- options.reverse_merge!(
170
- :version_at => created_at,
171
- :mark_for_destruction => false,
172
- :has_one => false,
173
- :has_many => false
174
- )
175
-
176
- attrs = self.class.object_col_is_json? ? object : PaperTrail.serializer.load(object)
177
-
178
- # Normally a polymorphic belongs_to relationship allows us
179
- # to get the object we belong to by calling, in this case,
180
- # `item`. However this returns nil if `item` has been
181
- # destroyed, and we need to be able to retrieve destroyed
182
- # objects.
183
- #
184
- # In this situation we constantize the `item_type` to get hold of
185
- # the class...except when the stored object's attributes
186
- # include a `type` key. If this is the case, the object
187
- # we belong to is using single table inheritance and the
188
- # `item_type` will be the base class, not the actual subclass.
189
- # If `type` is present but empty, the class is the base class.
190
-
191
- if options[:dup] != true && item
192
- model = item
193
- # Look for attributes that exist in the model and not in this version. These attributes should be set to nil.
194
- (model.attribute_names - attrs.keys).each { |k| attrs[k] = nil }
195
- else
196
- inheritance_column_name = item_type.constantize.inheritance_column
197
- class_name = attrs[inheritance_column_name].blank? ? item_type : attrs[inheritance_column_name]
198
- klass = class_name.constantize
199
- # the `dup` option always returns a new object, otherwise we should attempt
200
- # to look for the item outside of default scope(s)
201
- if options[:dup] || (_item = klass.unscoped.find_by_id(item_id)).nil?
202
- model = klass.new
203
- else
204
- model = _item
205
- # Look for attributes that exist in the model and not in this version. These attributes should be set to nil.
206
- (model.attribute_names - attrs.keys).each { |k| attrs[k] = nil }
207
- end
208
- end
209
-
210
- if PaperTrail.serialized_attributes?
211
- model.class.unserialize_attributes_for_paper_trail! attrs
212
- end
213
-
214
- # Set all the attributes in this version on the model
215
- attrs.each do |k, v|
216
- if model.has_attribute?(k)
217
- model[k.to_sym] = v
218
- elsif model.respond_to?("#{k}=")
219
- model.send("#{k}=", v)
220
- else
221
- logger.warn "Attribute #{k} does not exist on #{item_type} (Version id: #{id})."
222
- end
223
- end
224
-
225
- model.send "#{model.class.version_association_name}=", self
226
-
227
- unless options[:has_one] == false
228
- reify_has_ones model, options
229
- end
230
-
231
- unless options[:has_many] == false
232
- reify_has_manys model, options
233
- end
234
-
235
- model
229
+ ::PaperTrail::Reifier.reify(self, options)
236
230
  end
237
231
  end
238
232
 
239
- # Returns what changed in this version of the item. `ActiveModel::Dirty#changes`.
240
- # returns `nil` if your `versions` table does not have an `object_changes` text column.
233
+ # Returns what changed in this version of the item.
234
+ # `ActiveModel::Dirty#changes`. returns `nil` if your `versions` table does
235
+ # not have an `object_changes` text column.
241
236
  def changeset
242
- return nil unless self.class.column_names.include? 'object_changes'
243
-
244
- _changes = self.class.object_changes_col_is_json? ? object_changes : PaperTrail.serializer.load(object_changes)
245
- @changeset ||= HashWithIndifferentAccess.new(_changes).tap do |changes|
246
- if PaperTrail.serialized_attributes?
247
- item_type.constantize.unserialize_attribute_changes_for_paper_trail!(changes)
248
- end
249
- end
250
- rescue
251
- {}
237
+ return nil unless self.class.column_names.include? "object_changes"
238
+ @changeset ||= load_changeset
252
239
  end
253
240
 
254
241
  # Returns who put the item into the state stored in this version.
255
242
  def paper_trail_originator
256
- @paper_trail_originator ||= previous.whodunnit rescue nil
243
+ @paper_trail_originator ||= previous.try(:whodunnit)
257
244
  end
258
245
 
259
246
  def originator
260
247
  ::ActiveSupport::Deprecation.warn "Use paper_trail_originator instead of originator."
261
- self.paper_trail_originator
248
+ paper_trail_originator
262
249
  end
263
250
 
264
- # Returns who changed the item from the state it had in this version.
265
- # This is an alias for `whodunnit`.
251
+ # Returns who changed the item from the state it had in this version. This
252
+ # is an alias for `whodunnit`.
266
253
  def terminator
267
254
  @terminator ||= whodunnit
268
255
  end
269
- alias_method :version_author, :terminator
256
+ alias version_author terminator
270
257
 
271
258
  def sibling_versions(reload = false)
272
- @sibling_versions = nil if reload == true
273
- @sibling_versions ||= self.class.with_item_keys(item_type, item_id)
259
+ if reload || @sibling_versions.nil?
260
+ @sibling_versions = self.class.with_item_keys(item_type, item_id)
261
+ end
262
+ @sibling_versions
274
263
  end
275
264
 
276
265
  def next
@@ -281,156 +270,80 @@ module PaperTrail
281
270
  @previous ||= sibling_versions.preceding(self).first
282
271
  end
283
272
 
273
+ # Returns an integer representing the chronological position of the
274
+ # version among its siblings (see `sibling_versions`). The "create" event,
275
+ # for example, has an index of 0.
276
+ # @api public
284
277
  def index
285
- table = self.class.arel_table unless @index
286
- @index ||=
287
- if self.class.primary_key_is_int?
288
- sibling_versions.select(table[self.class.primary_key]).order(table[self.class.primary_key].asc).index(self)
289
- else
290
- sibling_versions.select([table[PaperTrail.timestamp_field], table[self.class.primary_key]]).
291
- order(self.class.timestamp_sort_order).index(self)
292
- end
278
+ @index ||= RecordHistory.new(sibling_versions, self.class).index(self)
293
279
  end
294
280
 
295
281
  private
296
282
 
297
- # In Rails 3.1+, calling reify on a previous version confuses the
298
- # IdentityMap, if enabled. This prevents insertion into the map.
299
- def without_identity_map(&block)
300
- if defined?(::ActiveRecord::IdentityMap) && ::ActiveRecord::IdentityMap.respond_to?(:without)
301
- ::ActiveRecord::IdentityMap.without(&block)
302
- else
303
- block.call
283
+ # @api private
284
+ def load_changeset
285
+ # First, deserialize the `object_changes` column.
286
+ changes = HashWithIndifferentAccess.new(object_changes_deserialized)
287
+
288
+ # The next step is, perhaps unfortunately, called "de-serialization",
289
+ # and appears to be responsible for custom attribute serializers. For an
290
+ # example of a custom attribute serializer, see
291
+ # `Person::TimeZoneSerializer` in the test suite.
292
+ #
293
+ # Is `item.class` good enough? Does it handle `inheritance_column`
294
+ # as well as `Reifier#version_reification_class`? We were using
295
+ # `item_type.constantize`, but that is problematic when the STI parent
296
+ # is not versioned. (See `Vehicle` and `Car` in the test suite).
297
+ #
298
+ # Note: `item` returns nil if `event` is "destroy".
299
+ unless item.nil?
300
+ AttributeSerializers::ObjectChangesAttribute.
301
+ new(item.class).
302
+ deserialize(changes)
304
303
  end
305
- end
306
-
307
- # Restore the `model`'s has_one associations as they were when this version was
308
- # superseded by the next (because that's what the user was looking at when they
309
- # made the change).
310
- def reify_has_ones(model, options = {})
311
- version_table_name = model.class.paper_trail_version_class.table_name
312
- model.class.reflect_on_all_associations(:has_one).each do |assoc|
313
- if assoc.klass.paper_trail_enabled_for_model?
314
- version = model.class.paper_trail_version_class.joins(:version_associations).
315
- where("version_associations.foreign_key_name = ?", assoc.foreign_key).
316
- where("version_associations.foreign_key_id = ?", model.id).
317
- where("#{version_table_name}.item_type = ?", assoc.class_name).
318
- where("created_at >= ? OR transaction_id = ?", options[:version_at], transaction_id).
319
- order("#{version_table_name}.id ASC").first
320
- if version
321
- if version.event == 'create'
322
- if options[:mark_for_destruction]
323
- model.send(assoc.name).mark_for_destruction if model.send(assoc.name, true)
324
- else
325
- model.appear_as_new_record do
326
- model.send "#{assoc.name}=", nil
327
- end
328
- end
329
- else
330
- child = version.reify(options.merge(:has_many => false, :has_one => false))
331
- model.appear_as_new_record do
332
- model.send "#{assoc.name}=", child
333
- end
334
- end
335
- end
336
- end
337
- end
338
- end
339
304
 
340
- # Restore the `model`'s has_many associations as they were at version_at timestamp
341
- # We lookup the first child versions after version_at timestamp or in same transaction.
342
- def reify_has_manys(model, options = {})
343
- assoc_has_many_through, assoc_has_many_directly =
344
- model.class.reflect_on_all_associations(:has_many).
345
- partition { |assoc| assoc.options[:through] }
346
- reify_has_many_directly(assoc_has_many_directly, model, options)
347
- reify_has_many_through(assoc_has_many_through, model, options)
305
+ # Finally, return a Hash mapping each attribute name to
306
+ # a two-element array representing before and after.
307
+ changes
348
308
  end
349
309
 
350
- # Restore the `model`'s has_many associations not associated through another association
351
- def reify_has_many_directly(associations, model, options = {})
352
- version_table_name = model.class.paper_trail_version_class.table_name
353
- associations.each do |assoc|
354
- next unless assoc.klass.paper_trail_enabled_for_model?
355
- version_id_subquery = PaperTrail::VersionAssociation.joins(model.class.version_association_name).
356
- select("MIN(version_id)").
357
- where("foreign_key_name = ?", assoc.foreign_key).
358
- where("foreign_key_id = ?", model.id).
359
- where("#{version_table_name}.item_type = ?", assoc.class_name).
360
- where("created_at >= ? OR transaction_id = ?", options[:version_at], transaction_id).
361
- group("item_id").to_sql
362
- versions = model.class.paper_trail_version_class.where("id IN (#{version_id_subquery})").inject({}) do |acc, v|
363
- acc.merge!(v.item_id => v)
364
- end
365
-
366
- # Pass true to force the model to load
367
- collection = Array.new model.send(assoc.name, true)
368
-
369
- # Iterate each child to replace it with the previous value if there is a version after the timestamp
370
- collection.map! do |c|
371
- if (version = versions.delete(c.id)).nil?
372
- c
373
- elsif version.event == 'create'
374
- options[:mark_for_destruction] ? c.tap { |r| r.mark_for_destruction } : nil
375
- else
376
- version.reify(options.merge(:has_many => false, :has_one => false))
377
- end
310
+ # If the `object_changes` column is a Postgres JSON column, then
311
+ # ActiveRecord will deserialize it for us. Otherwise, it's a string column
312
+ # and we must deserialize it ourselves.
313
+ # @api private
314
+ def object_changes_deserialized
315
+ if self.class.object_changes_col_is_json?
316
+ object_changes
317
+ else
318
+ begin
319
+ PaperTrail.serializer.load(object_changes)
320
+ rescue # TODO: Rescue something specific
321
+ {}
378
322
  end
379
-
380
- # Reify the rest of the versions and add them to the collection, these versions are for those that
381
- # have been removed from the live associations
382
- collection += versions.values.map { |version| version.reify(options.merge(:has_many => false, :has_one => false)) }
383
-
384
- model.send(assoc.name).proxy_association.target = collection.compact
385
323
  end
386
324
  end
387
325
 
388
- # Restore the `model`'s has_many associations through another association
389
- # This must be called after the direct has_manys have been reified (reify_has_many_directly)
390
- def reify_has_many_through(associations, model, options = {})
391
- associations.each do |assoc|
392
- next unless assoc.klass.paper_trail_enabled_for_model?
393
- through_collection = model.send(assoc.options[:through])
394
- collection_keys = through_collection.map { |through_model| through_model.send(assoc.foreign_key) }
395
-
396
- version_id_subquery = assoc.klass.paper_trail_version_class.
397
- select("MIN(id)").
398
- where("item_type = ?", assoc.class_name).
399
- where("item_id IN (?)", collection_keys).
400
- where("created_at >= ? OR transaction_id = ?", options[:version_at], transaction_id).
401
- group("item_id").to_sql
402
- versions = assoc.klass.paper_trail_version_class.where("id IN (#{version_id_subquery})").inject({}) do |acc, v|
403
- acc.merge!(v.item_id => v)
404
- end
405
-
406
- collection = Array.new assoc.klass.where(assoc.klass.primary_key => collection_keys)
407
-
408
- # Iterate each child to replace it with the previous value if there is a version after the timestamp
409
- collection.map! do |c|
410
- if (version = versions.delete(c.id)).nil?
411
- c
412
- elsif version.event == 'create'
413
- options[:mark_for_destruction] ? c.tap { |r| r.mark_for_destruction } : nil
414
- else
415
- version.reify(options.merge(:has_many => false, :has_one => false))
416
- end
417
- end
418
-
419
- # Reify the rest of the versions and add them to the collection, these versions are for those that
420
- # have been removed from the live associations
421
- collection += versions.values.map { |version| version.reify(options.merge(:has_many => false, :has_one => false)) }
422
-
423
- model.send(assoc.name).proxy_association.target = collection.compact
326
+ # In Rails 3.1+, calling reify on a previous version confuses the
327
+ # IdentityMap, if enabled. This prevents insertion into the map.
328
+ # @api private
329
+ def without_identity_map(&block)
330
+ if defined?(::ActiveRecord::IdentityMap) && ::ActiveRecord::IdentityMap.respond_to?(:without)
331
+ ::ActiveRecord::IdentityMap.without(&block)
332
+ else
333
+ yield
424
334
  end
425
335
  end
426
336
 
427
- # checks to see if a value has been set for the `version_limit` config option, and if so enforces it
337
+ # Checks that a value has been set for the `version_limit` config
338
+ # option, and if so enforces it.
339
+ # @api private
428
340
  def enforce_version_limit!
429
- return unless PaperTrail.config.version_limit.is_a? Numeric
341
+ limit = PaperTrail.config.version_limit
342
+ return unless limit.is_a? Numeric
430
343
  previous_versions = sibling_versions.not_creates
431
- return unless previous_versions.size > PaperTrail.config.version_limit
432
- excess_previous_versions = previous_versions - previous_versions.last(PaperTrail.config.version_limit)
433
- excess_previous_versions.map(&:destroy)
344
+ return unless previous_versions.size > limit
345
+ excess_versions = previous_versions - previous_versions.last(limit)
346
+ excess_versions.map(&:destroy)
434
347
  end
435
348
  end
436
349
  end
@@ -1,18 +1,15 @@
1
1
  module PaperTrail
2
+ # :nodoc:
2
3
  module VERSION
3
- MAJOR = 4
4
- MINOR = 0
5
- TINY = 0
6
- PRE = nil
4
+ MAJOR = 5
5
+ MINOR = 1
6
+ TINY = 0
7
+ PRE = nil
7
8
 
8
- STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
9
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".").freeze
9
10
 
10
11
  def self.to_s
11
12
  STRING
12
13
  end
13
14
  end
14
-
15
- def self.version
16
- VERSION::STRING
17
- end
18
15
  end