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,169 +1,236 @@
1
- require 'active_support/core_ext/object' # provides the `try` method
1
+ require "active_support/core_ext/object" # provides the `try` method
2
+ require "paper_trail/attribute_serializers/legacy_active_record_shim"
3
+ require "paper_trail/attribute_serializers/object_attribute"
4
+ require "paper_trail/attribute_serializers/object_changes_attribute"
2
5
 
3
6
  module PaperTrail
7
+ # Extensions to `ActiveRecord::Base`. See `frameworks/active_record.rb`.
4
8
  module Model
5
-
6
9
  def self.included(base)
7
10
  base.send :extend, ClassMethods
11
+ base.send :attr_accessor, :paper_trail_habtm
8
12
  end
9
13
 
14
+ # :nodoc:
10
15
  module ClassMethods
11
- # Declare this in your model to track every create, update, and destroy. Each version of
12
- # the model is available in the `versions` association.
16
+ # Declare this in your model to track every create, update, and destroy.
17
+ # Each version of the model is available in the `versions` association.
13
18
  #
14
19
  # Options:
15
- # :on the events to track (optional; defaults to all of them). Set to an array of
16
- # `:create`, `:update`, `:destroy` as desired.
17
- # :class_name the name of a custom Version class. This class should inherit from `PaperTrail::Version`.
18
- # :ignore an array of attributes for which a new `Version` will not be created if only they change.
19
- # it can also aceept a Hash as an argument where the key is the attribute to ignore (a `String` or `Symbol`),
20
- # which will only be ignored if the value is a `Proc` which returns truthily.
21
- # :if, :unless Procs that allow to specify conditions when to save versions for an object
22
- # :only inverse of `ignore` - a new `Version` will be created only for these attributes if supplied
23
- # it can also aceept a Hash as an argument where the key is the attribute to track (a `String` or `Symbol`),
24
- # which will only be counted if the value is a `Proc` which returns truthily.
25
- # :skip fields to ignore completely. As with `ignore`, updates to these fields will not create
26
- # a new `Version`. In addition, these fields will not be included in the serialized versions
27
- # of the object whenever a new `Version` is created.
28
- # :meta a hash of extra data to store. You must add a column to the `versions` table for each key.
29
- # Values are objects or procs (which are called with `self`, i.e. the model with the paper
30
- # trail). See `PaperTrail::Controller.info_for_paper_trail` for how to store data from
31
- # the controller.
32
- # :versions the name to use for the versions association. Default is `:versions`.
33
- # :version the name to use for the method which returns the version the instance was reified from.
34
- # Default is `:version`.
35
- # :save_changes whether or not to save changes to the object_changes column if it exists. Default is true
36
20
  #
21
+ # - :on - The events to track (optional; defaults to all of them). Set
22
+ # to an array of `:create`, `:update`, `:destroy` as desired.
23
+ # - :class_name - The name of a custom Version class. This class should
24
+ # inherit from `PaperTrail::Version`.
25
+ # - :ignore - An array of attributes for which a new `Version` will not be
26
+ # created if only they change. It can also aceept a Hash as an
27
+ # argument where the key is the attribute to ignore (a `String` or
28
+ # `Symbol`), which will only be ignored if the value is a `Proc` which
29
+ # returns truthily.
30
+ # - :if, :unless - Procs that allow to specify conditions when to save
31
+ # versions for an object.
32
+ # - :only - Inverse of `ignore`. A new `Version` will be created only
33
+ # for these attributes if supplied it can also aceept a Hash as an
34
+ # argument where the key is the attribute to track (a `String` or
35
+ # `Symbol`), which will only be counted if the value is a `Proc` which
36
+ # returns truthily.
37
+ # - :skip - Fields to ignore completely. As with `ignore`, updates to
38
+ # these fields will not create a new `Version`. In addition, these
39
+ # fields will not be included in the serialized versions of the object
40
+ # whenever a new `Version` is created.
41
+ # - :meta - A hash of extra data to store. You must add a column to the
42
+ # `versions` table for each key. Values are objects or procs (which
43
+ # are called with `self`, i.e. the model with the paper trail). See
44
+ # `PaperTrail::Controller.info_for_paper_trail` for how to store data
45
+ # from the controller.
46
+ # - :versions - The name to use for the versions association. Default
47
+ # is `:versions`.
48
+ # - :version - The name to use for the method which returns the version
49
+ # the instance was reified from. Default is `:version`.
50
+ # - :save_changes - Whether or not to save changes to the object_changes
51
+ # column if it exists. Default is true
52
+ # - :join_tables - If the model has a has_and_belongs_to_many relation
53
+ # with an unpapertrailed model, passing the name of the association to
54
+ # the join_tables option will paper trail the join table but not save
55
+ # the other model, allowing reification of the association but with the
56
+ # other models latest state (if the other model is paper trailed, this
57
+ # option does nothing)
58
+ #
59
+ # @api public
37
60
  def has_paper_trail(options = {})
61
+ options[:on] ||= [:create, :update, :destroy]
62
+
63
+ # Wrap the :on option in an array if necessary. This allows a single
64
+ # symbol to be passed in.
65
+ options[:on] = Array(options[:on])
66
+
67
+ setup_model_for_paper_trail(options)
68
+
69
+ setup_callbacks_from_options options[:on]
70
+
71
+ setup_callbacks_for_habtm options[:join_tables]
72
+ end
73
+
74
+ def update_for_callback(name, callback, model, assoc)
75
+ model.paper_trail_habtm ||= {}
76
+ model.paper_trail_habtm.reverse_merge!(name => { removed: [], added: [] })
77
+ case callback
78
+ when :before_add
79
+ model.paper_trail_habtm[name][:added] |= [assoc.id]
80
+ model.paper_trail_habtm[name][:removed] -= [assoc.id]
81
+ when :before_remove
82
+ model.paper_trail_habtm[name][:removed] |= [assoc.id]
83
+ model.paper_trail_habtm[name][:added] -= [assoc.id]
84
+ end
85
+ end
86
+
87
+ attr_reader :paper_trail_save_join_tables
88
+
89
+ def setup_callbacks_for_habtm(join_tables)
90
+ @paper_trail_save_join_tables = Array.wrap(join_tables)
91
+ # Adds callbacks to record changes to habtm associations such that on
92
+ # save the previous version of the association (if changed) can be
93
+ # interpreted
94
+ reflect_on_all_associations(:has_and_belongs_to_many).
95
+ reject { |a| paper_trail_options[:skip].include?(a.name.to_s) }.
96
+ each do |a|
97
+ added_callback = lambda do |*args|
98
+ update_for_callback(a.name, :before_add, args[-2], args.last)
99
+ end
100
+ removed_callback = lambda do |*args|
101
+ update_for_callback(a.name, :before_remove, args[-2], args.last)
102
+ end
103
+ send(:"before_add_for_#{a.name}").send(:<<, added_callback)
104
+ send(:"before_remove_for_#{a.name}").send(:<<, removed_callback)
105
+ end
106
+ end
107
+
108
+ # Installs callbacks, associations, "class attributes", and more.
109
+ # For details of how "class attributes" work, see the activesupport docs.
110
+ # @api private
111
+ def setup_model_for_paper_trail(options = {})
38
112
  # Lazily include the instance methods so we don't clutter up
39
113
  # any more ActiveRecord models than we have to.
40
114
  send :include, InstanceMethods
41
115
 
116
+ if ::ActiveRecord::VERSION::STRING < "4.2"
117
+ send :extend, AttributeSerializers::LegacyActiveRecordShim
118
+ end
119
+
42
120
  class_attribute :version_association_name
43
121
  self.version_association_name = options[:version] || :version
44
122
 
45
123
  # The version this instance was reified from.
46
- attr_accessor self.version_association_name
124
+ attr_accessor version_association_name
47
125
 
48
126
  class_attribute :version_class_name
49
- self.version_class_name = options[:class_name] || 'PaperTrail::Version'
127
+ self.version_class_name = options[:class_name] || "PaperTrail::Version"
50
128
 
51
- class_attribute :paper_trail_options
52
- self.paper_trail_options = options.dup
53
-
54
- [:ignore, :skip, :only].each do |k|
55
- paper_trail_options[k] =
56
- [paper_trail_options[k]].flatten.compact.map { |attr| attr.is_a?(Hash) ? attr.stringify_keys : attr.to_s }
57
- end
58
-
59
- paper_trail_options[:meta] ||= {}
60
- paper_trail_options[:save_changes] = true if paper_trail_options[:save_changes].nil?
129
+ setup_paper_trail_options(options)
61
130
 
62
131
  class_attribute :versions_association_name
63
132
  self.versions_association_name = options[:versions] || :versions
64
133
 
65
134
  attr_accessor :paper_trail_event
66
135
 
67
- if ::ActiveRecord::VERSION::MAJOR >= 4 # `has_many` syntax for specifying order uses a lambda in Rails 4
68
- has_many self.versions_association_name,
69
- lambda { order(model.timestamp_sort_order) },
70
- :class_name => self.version_class_name, :as => :item
136
+ # `has_many` syntax for specifying order uses a lambda in Rails 4
137
+ if ::ActiveRecord::VERSION::MAJOR >= 4
138
+ has_many versions_association_name,
139
+ -> { order(model.timestamp_sort_order) },
140
+ class_name: version_class_name, as: :item
71
141
  else
72
- has_many self.versions_association_name,
73
- :class_name => self.version_class_name,
74
- :as => :item,
75
- :order => self.paper_trail_version_class.timestamp_sort_order
76
- end
77
-
78
- options[:on] ||= [:create, :update, :destroy]
79
- options_on = Array(options[:on]) # so that a single symbol can be passed in without wrapping it in an `Array`
80
- after_create :record_create, :if => :save_version? if options_on.include?(:create)
81
- if options_on.include?(:update)
82
- before_save :reset_timestamp_attrs_for_update_if_needed!, :on => :update
83
- after_update :record_update, :if => :save_version?
84
- after_update :clear_version_instance!
142
+ has_many versions_association_name,
143
+ class_name: version_class_name,
144
+ as: :item,
145
+ order: paper_trail_version_class.timestamp_sort_order
85
146
  end
86
- after_destroy :record_destroy, :if => :save_version? if options_on.include?(:destroy)
87
147
 
88
- # Reset the transaction id when the transaction is closed
148
+ # Reset the transaction id when the transaction is closed.
89
149
  after_commit :reset_transaction_id
90
150
  after_rollback :reset_transaction_id
91
151
  after_rollback :clear_rolled_back_versions
92
152
  end
93
153
 
94
- # Switches PaperTrail off for this class.
95
- def paper_trail_off!
96
- PaperTrail.enabled_for_model(self, false)
154
+ # Given `options`, populates `paper_trail_options`.
155
+ # @api private
156
+ def setup_paper_trail_options(options)
157
+ class_attribute :paper_trail_options
158
+ self.paper_trail_options = options.dup
159
+ [:ignore, :skip, :only].each do |k|
160
+ paper_trail_options[k] = [paper_trail_options[k]].flatten.compact.map { |attr|
161
+ attr.is_a?(Hash) ? attr.stringify_keys : attr.to_s
162
+ }
163
+ end
164
+ paper_trail_options[:meta] ||= {}
165
+ if paper_trail_options[:save_changes].nil?
166
+ paper_trail_options[:save_changes] = true
167
+ end
97
168
  end
98
169
 
99
- # Switches PaperTrail on for this class.
100
- def paper_trail_on!
101
- PaperTrail.enabled_for_model(self, true)
170
+ def setup_callbacks_from_options(options_on = [])
171
+ options_on.each do |option|
172
+ send "paper_trail_on_#{option}"
173
+ end
102
174
  end
103
175
 
104
- def paper_trail_enabled_for_model?
105
- return false unless self.include?(PaperTrail::Model::InstanceMethods)
106
- PaperTrail.enabled_for_model?(self)
107
- end
176
+ # Record version before or after "destroy" event
177
+ def paper_trail_on_destroy(recording_order = "before")
178
+ unless %w(after before).include?(recording_order.to_s)
179
+ raise ArgumentError, 'recording order can only be "after" or "before"'
180
+ end
108
181
 
109
- def paper_trail_version_class
110
- @paper_trail_version_class ||= version_class_name.constantize
182
+ if recording_order == "after" &&
183
+ Gem::Version.new(ActiveRecord::VERSION::STRING) >= Gem::Version.new("5")
184
+ if ::ActiveRecord::Base.belongs_to_required_by_default
185
+ ::ActiveSupport::Deprecation.warn(
186
+ "paper_trail_on_destroy(:after) is incompatible with ActiveRecord " +
187
+ "belongs_to_required_by_default and has no effect. Please use :before " +
188
+ "or disable belongs_to_required_by_default."
189
+ )
190
+ end
191
+ end
192
+
193
+ send "#{recording_order}_destroy", :record_destroy, if: :save_version?
194
+
195
+ return if paper_trail_options[:on].include?(:destroy)
196
+ paper_trail_options[:on] << :destroy
111
197
  end
112
198
 
113
- # Used for Version#object attribute
114
- def serialize_attributes_for_paper_trail!(attributes)
115
- # don't serialize before values before inserting into columns of type `JSON` on `PostgreSQL` databases
116
- return attributes if self.paper_trail_version_class.object_col_is_json?
199
+ # Record version after "update" event
200
+ def paper_trail_on_update
201
+ before_save :reset_timestamp_attrs_for_update_if_needed!, on: :update
202
+ after_update :record_update, if: :save_version?
203
+ after_update :clear_version_instance!
117
204
 
118
- serialized_attributes.each do |key, coder|
119
- if attributes.key?(key)
120
- # Fall back to current serializer if `coder` has no `dump` method
121
- coder = PaperTrail.serializer unless coder.respond_to?(:dump)
122
- attributes[key] = coder.dump(attributes[key])
123
- end
124
- end
205
+ return if paper_trail_options[:on].include?(:update)
206
+ paper_trail_options[:on] << :update
125
207
  end
126
208
 
127
- def unserialize_attributes_for_paper_trail!(attributes)
128
- # don't serialize before values before inserting into columns of type `JSON` on `PostgreSQL` databases
129
- return attributes if self.paper_trail_version_class.object_col_is_json?
209
+ # Record version after "create" event
210
+ def paper_trail_on_create
211
+ after_create :record_create, if: :save_version?
130
212
 
131
- serialized_attributes.each do |key, coder|
132
- if attributes.key?(key)
133
- coder = PaperTrail.serializer unless coder.respond_to?(:dump)
134
- attributes[key] = coder.load(attributes[key])
135
- end
136
- end
213
+ return if paper_trail_options[:on].include?(:create)
214
+ paper_trail_options[:on] << :create
137
215
  end
138
216
 
139
- # Used for Version#object_changes attribute
140
- def serialize_attribute_changes_for_paper_trail!(changes)
141
- # don't serialize before values before inserting into columns of type `JSON` on `PostgreSQL` databases
142
- return changes if self.paper_trail_version_class.object_changes_col_is_json?
217
+ # Switches PaperTrail off for this class.
218
+ def paper_trail_off!
219
+ PaperTrail.enabled_for_model(self, false)
220
+ end
143
221
 
144
- serialized_attributes.each do |key, coder|
145
- if changes.key?(key)
146
- # Fall back to current serializer if `coder` has no `dump` method
147
- coder = PaperTrail.serializer unless coder.respond_to?(:dump)
148
- old_value, new_value = changes[key]
149
- changes[key] = [coder.dump(old_value),
150
- coder.dump(new_value)]
151
- end
152
- end
222
+ # Switches PaperTrail on for this class.
223
+ def paper_trail_on!
224
+ PaperTrail.enabled_for_model(self, true)
153
225
  end
154
226
 
155
- def unserialize_attribute_changes_for_paper_trail!(changes)
156
- # don't serialize before values before inserting into columns of type `JSON` on `PostgreSQL` databases
157
- return changes if self.paper_trail_version_class.object_changes_col_is_json?
227
+ def paper_trail_enabled_for_model?
228
+ return false unless include?(PaperTrail::Model::InstanceMethods)
229
+ PaperTrail.enabled_for_model?(self)
230
+ end
158
231
 
159
- serialized_attributes.each do |key, coder|
160
- if changes.key?(key)
161
- coder = PaperTrail.serializer unless coder.respond_to?(:dump)
162
- old_value, new_value = changes[key]
163
- changes[key] = [coder.load(old_value),
164
- coder.load(new_value)]
165
- end
166
- end
232
+ def paper_trail_version_class
233
+ @paper_trail_version_class ||= version_class_name.constantize
167
234
  end
168
235
  end
169
236
 
@@ -183,34 +250,43 @@ module PaperTrail
183
250
 
184
251
  def originator
185
252
  ::ActiveSupport::Deprecation.warn "Use paper_trail_originator instead of originator."
186
- self.paper_trail_originator
253
+ paper_trail_originator
187
254
  end
188
255
 
189
256
  # Invoked after rollbacks to ensure versions records are not created
190
- # for changes that never actually took place
257
+ # for changes that never actually took place.
258
+ # Optimization: Use lazy `reset` instead of eager `reload` because, in
259
+ # many use cases, the association will not be used.
191
260
  def clear_rolled_back_versions
192
- send(self.class.versions_association_name).reload
261
+ send(self.class.versions_association_name).reset
193
262
  end
194
263
 
195
264
  # Returns the object (not a Version) as it was at the given timestamp.
196
- def version_at(timestamp, reify_options={})
265
+ def version_at(timestamp, reify_options = {})
197
266
  # Because a version stores how its object looked *before* the change,
198
267
  # we need to look for the first version created *after* the timestamp.
199
268
  v = send(self.class.versions_association_name).subsequent(timestamp, true).first
200
269
  return v.reify(reify_options) if v
201
- self unless self.destroyed?
270
+ self unless destroyed?
202
271
  end
203
272
 
204
273
  # Returns the objects (not Versions) as they were between the given times.
205
- def versions_between(start_time, end_time, reify_options={})
274
+ # TODO: Either add support for the third argument, `_reify_options`, or
275
+ # add a deprecation warning if someone tries to use it.
276
+ def versions_between(start_time, end_time, _reify_options = {})
206
277
  versions = send(self.class.versions_association_name).between(start_time, end_time)
207
- versions.collect { |version| version_at(version.send PaperTrail.timestamp_field) }
278
+ versions.collect { |version| version_at(version.send(PaperTrail.timestamp_field)) }
208
279
  end
209
280
 
210
281
  # Returns the object (not a Version) as it was most recently.
211
282
  def previous_version
212
- preceding_version = source_version ? source_version.previous : send(self.class.versions_association_name).last
213
- preceding_version.reify if preceding_version
283
+ previous =
284
+ if source_version
285
+ source_version.previous
286
+ else
287
+ send(self.class.versions_association_name).last
288
+ end
289
+ previous.try(:reify)
214
290
  end
215
291
 
216
292
  # Returns the object (not a Version) as it became next.
@@ -218,7 +294,7 @@ module PaperTrail
218
294
  # "live" item, we return nil. Perhaps we should return self instead?
219
295
  def next_version
220
296
  subsequent_version = source_version.next
221
- subsequent_version ? subsequent_version.reify : self.class.find(self.id)
297
+ subsequent_version ? subsequent_version.reify : self.class.find(id)
222
298
  rescue
223
299
  nil
224
300
  end
@@ -229,14 +305,15 @@ module PaperTrail
229
305
 
230
306
  # Executes the given method or block without creating a new version.
231
307
  def without_versioning(method = nil)
232
- paper_trail_was_enabled = self.paper_trail_enabled_for_model?
308
+ paper_trail_was_enabled = paper_trail_enabled_for_model?
233
309
  self.class.paper_trail_off!
234
310
  method ? method.to_proc.call(self) : yield(self)
235
311
  ensure
236
312
  self.class.paper_trail_on! if paper_trail_was_enabled
237
313
  end
238
314
 
239
- # Utility method for reifying. Anything executed inside the block will appear like a new record
315
+ # Utility method for reifying. Anything executed inside the block will
316
+ # appear like a new record.
240
317
  def appear_as_new_record
241
318
  instance_eval {
242
319
  alias :old_new_record? :new_record?
@@ -246,9 +323,10 @@ module PaperTrail
246
323
  instance_eval { alias :new_record? :old_new_record? }
247
324
  end
248
325
 
249
- # Temporarily overwrites the value of whodunnit and then executes the provided block.
326
+ # Temporarily overwrites the value of whodunnit and then executes the
327
+ # provided block.
250
328
  def whodunnit(value)
251
- raise ArgumentError, 'expected to receive a block' unless block_given?
329
+ raise ArgumentError, "expected to receive a block" unless block_given?
252
330
  current_whodunnit = PaperTrail.whodunnit
253
331
  PaperTrail.whodunnit = value
254
332
  yield self
@@ -261,7 +339,7 @@ module PaperTrail
261
339
  # `:on`, `:if`, or `:unless`.
262
340
  #
263
341
  # TODO: look into leveraging the `after_touch` callback from
264
- # `ActiveRecord` to allow the regular `touch` method go generate a version
342
+ # `ActiveRecord` to allow the regular `touch` method to generate a version
265
343
  # as normal. May make sense to switch the `record_update` method to
266
344
  # leverage an `after_update` callback anyways (likely for v4.0.0)
267
345
  def touch_with_version(name = nil)
@@ -274,7 +352,7 @@ module PaperTrail
274
352
  attributes.each { |column| write_attribute(column, current_time) }
275
353
 
276
354
  record_update(true) unless will_record_after_update?
277
- save!(:validate => false)
355
+ save!(validate: false)
278
356
  end
279
357
 
280
358
  private
@@ -293,66 +371,103 @@ module PaperTrail
293
371
  def record_create
294
372
  if paper_trail_switched_on?
295
373
  data = {
296
- :event => paper_trail_event || 'create',
297
- :whodunnit => PaperTrail.whodunnit
374
+ event: paper_trail_event || "create",
375
+ whodunnit: PaperTrail.whodunnit
298
376
  }
299
- if respond_to?(:created_at)
300
- data[PaperTrail.timestamp_field] = created_at
301
- end
302
- if paper_trail_options[:save_changes] && changed_notably? && self.class.paper_trail_version_class.column_names.include?('object_changes')
303
- data[:object_changes] = self.class.paper_trail_version_class.object_changes_col_is_json? ? changes_for_paper_trail :
304
- PaperTrail.serializer.dump(changes_for_paper_trail)
377
+ if respond_to?(:updated_at)
378
+ data[PaperTrail.timestamp_field] = updated_at
305
379
  end
306
- if self.class.paper_trail_version_class.column_names.include?('transaction_id')
307
- data[:transaction_id] = PaperTrail.transaction_id
380
+ if pt_record_object_changes? && changed_notably?
381
+ data[:object_changes] = pt_recordable_object_changes
308
382
  end
383
+ add_transaction_id_to(data)
309
384
  version = send(self.class.versions_association_name).create! merge_metadata(data)
310
- set_transaction_id(version)
385
+ update_transaction_id(version)
311
386
  save_associations(version)
312
387
  end
313
388
  end
314
389
 
315
390
  def record_update(force = nil)
316
391
  if paper_trail_switched_on? && (force || changed_notably?)
317
- object_attrs = object_attrs_for_paper_trail(attributes_before_change)
318
392
  data = {
319
- :event => paper_trail_event || 'update',
320
- :object => self.class.paper_trail_version_class.object_col_is_json? ? object_attrs : PaperTrail.serializer.dump(object_attrs),
321
- :whodunnit => PaperTrail.whodunnit
393
+ event: paper_trail_event || "update",
394
+ object: pt_recordable_object,
395
+ whodunnit: PaperTrail.whodunnit
322
396
  }
323
397
  if respond_to?(:updated_at)
324
398
  data[PaperTrail.timestamp_field] = updated_at
325
399
  end
326
- if paper_trail_options[:save_changes] && self.class.paper_trail_version_class.column_names.include?('object_changes')
327
- data[:object_changes] = self.class.paper_trail_version_class.object_changes_col_is_json? ? changes_for_paper_trail :
328
- PaperTrail.serializer.dump(changes_for_paper_trail)
329
- end
330
- if self.class.paper_trail_version_class.column_names.include?('transaction_id')
331
- data[:transaction_id] = PaperTrail.transaction_id
400
+ if pt_record_object_changes?
401
+ data[:object_changes] = pt_recordable_object_changes
332
402
  end
403
+ add_transaction_id_to(data)
333
404
  version = send(self.class.versions_association_name).create merge_metadata(data)
334
- set_transaction_id(version)
335
- save_associations(version)
405
+ if version.errors.any?
406
+ log_version_errors(version, :update)
407
+ else
408
+ update_transaction_id(version)
409
+ save_associations(version)
410
+ end
336
411
  end
337
412
  end
338
413
 
339
- def changes_for_paper_trail
340
- _changes = changes.delete_if { |k,v| !notably_changed.include?(k) }
341
- if PaperTrail.serialized_attributes?
342
- self.class.serialize_attribute_changes_for_paper_trail!(_changes)
414
+ # Returns a boolean indicating whether to store serialized version diffs
415
+ # in the `object_changes` column of the version record.
416
+ # @api private
417
+ def pt_record_object_changes?
418
+ paper_trail_options[:save_changes] &&
419
+ self.class.paper_trail_version_class.column_names.include?("object_changes")
420
+ end
421
+
422
+ # Returns an object which can be assigned to the `object` attribute of a
423
+ # nascent version record. If the `object` column is a postgres `json`
424
+ # column, then a hash can be used in the assignment, otherwise the column
425
+ # is a `text` column, and we must perform the serialization here, using
426
+ # `PaperTrail.serializer`.
427
+ # @api private
428
+ def pt_recordable_object
429
+ if self.class.paper_trail_version_class.object_col_is_json?
430
+ object_attrs_for_paper_trail
431
+ else
432
+ PaperTrail.serializer.dump(object_attrs_for_paper_trail)
343
433
  end
344
- _changes.to_hash
345
434
  end
346
435
 
347
- # Invoked via`after_update` callback for when a previous version is reified and then saved
436
+ # Returns an object which can be assigned to the `object_changes`
437
+ # attribute of a nascent version record. If the `object_changes` column is
438
+ # a postgres `json` column, then a hash can be used in the assignment,
439
+ # otherwise the column is a `text` column, and we must perform the
440
+ # serialization here, using `PaperTrail.serializer`.
441
+ # @api private
442
+ def pt_recordable_object_changes
443
+ if self.class.paper_trail_version_class.object_changes_col_is_json?
444
+ changes_for_paper_trail
445
+ else
446
+ PaperTrail.serializer.dump(changes_for_paper_trail)
447
+ end
448
+ end
449
+
450
+ def changes_for_paper_trail
451
+ notable_changes = changes.delete_if { |k, _v| !notably_changed.include?(k) }
452
+ AttributeSerializers::ObjectChangesAttribute.
453
+ new(self.class).
454
+ serialize(notable_changes)
455
+ notable_changes.to_hash
456
+ end
457
+
458
+ # Invoked via`after_update` callback for when a previous version is
459
+ # reified and then saved.
348
460
  def clear_version_instance!
349
461
  send("#{self.class.version_association_name}=", nil)
350
462
  end
351
463
 
464
+ # Invoked via callback when a user attempts to persist a reified
465
+ # `Version`.
352
466
  def reset_timestamp_attrs_for_update_if_needed!
353
- return if self.live? # invoked via callback when a user attempts to persist a reified `Version`
467
+ return if live?
354
468
  timestamp_attributes_for_update_in_model.each do |column|
355
- # ActiveRecord 4.2 deprecated `reset_column!` in favor of `restore_column!`
469
+ # ActiveRecord 4.2 deprecated `reset_column!` in favor of
470
+ # `restore_column!`.
356
471
  if respond_to?("restore_#{column}!")
357
472
  send("restore_#{column}!")
358
473
  else
@@ -362,54 +477,74 @@ module PaperTrail
362
477
  end
363
478
 
364
479
  def record_destroy
365
- if paper_trail_switched_on? and not new_record?
366
- object_attrs = object_attrs_for_paper_trail(attributes_before_change)
480
+ if paper_trail_switched_on? && !new_record?
367
481
  data = {
368
- :item_id => self.id,
369
- :item_type => self.class.base_class.name,
370
- :event => paper_trail_event || 'destroy',
371
- :object => self.class.paper_trail_version_class.object_col_is_json? ? object_attrs : PaperTrail.serializer.dump(object_attrs),
372
- :whodunnit => PaperTrail.whodunnit
482
+ item_id: id,
483
+ item_type: self.class.base_class.name,
484
+ event: paper_trail_event || "destroy",
485
+ object: pt_recordable_object,
486
+ whodunnit: PaperTrail.whodunnit
373
487
  }
374
- if self.class.paper_trail_version_class.column_names.include?('transaction_id')
375
- data[:transaction_id] = PaperTrail.transaction_id
376
- end
488
+ add_transaction_id_to(data)
377
489
  version = self.class.paper_trail_version_class.create(merge_metadata(data))
378
- send("#{self.class.version_association_name}=", version)
379
- send(self.class.versions_association_name).send :load_target
380
- set_transaction_id(version)
381
- save_associations(version)
490
+ if version.errors.any?
491
+ log_version_errors(version, :destroy)
492
+ else
493
+ send("#{self.class.version_association_name}=", version)
494
+ send(self.class.versions_association_name).reset
495
+ update_transaction_id(version)
496
+ save_associations(version)
497
+ end
382
498
  end
383
499
  end
384
500
 
385
- # saves associations if the join table for `VersionAssociation` exists
501
+ # Saves associations if the join table for `VersionAssociation` exists.
386
502
  def save_associations(version)
387
503
  return unless PaperTrail.config.track_associations?
504
+ save_associations_belongs_to(version)
505
+ save_associations_has_and_belongs_to_many(version)
506
+ end
507
+
508
+ def save_associations_belongs_to(version)
388
509
  self.class.reflect_on_all_associations(:belongs_to).each do |assoc|
389
510
  assoc_version_args = {
390
- :version_id => version.id,
391
- :foreign_key_name => assoc.foreign_key
511
+ version_id: version.id,
512
+ foreign_key_name: assoc.foreign_key
392
513
  }
393
514
 
394
515
  if assoc.options[:polymorphic]
395
516
  associated_record = send(assoc.name) if send(assoc.foreign_type)
396
517
  if associated_record && associated_record.class.paper_trail_enabled_for_model?
397
- assoc_version_args.merge!(:foreign_key_id => associated_record.id)
518
+ assoc_version_args[:foreign_key_id] = associated_record.id
398
519
  end
399
520
  elsif assoc.klass.paper_trail_enabled_for_model?
400
- assoc_version_args.merge!(:foreign_key_id => send(assoc.foreign_key))
521
+ assoc_version_args[:foreign_key_id] = send(assoc.foreign_key)
401
522
  end
402
523
 
403
- PaperTrail::VersionAssociation.create(assoc_version_args) if assoc_version_args.has_key?(:foreign_key_id)
524
+ if assoc_version_args.key?(:foreign_key_id)
525
+ PaperTrail::VersionAssociation.create(assoc_version_args)
526
+ end
404
527
  end
405
528
  end
406
529
 
407
- def set_transaction_id(version)
408
- return unless self.class.paper_trail_version_class.column_names.include?('transaction_id')
409
- if PaperTrail.transaction? && PaperTrail.transaction_id.nil?
410
- PaperTrail.transaction_id = version.id
411
- version.transaction_id = version.id
412
- version.save
530
+ def save_associations_has_and_belongs_to_many(version)
531
+ # Use the :added and :removed keys to extrapolate the HABTM associations
532
+ # to before any changes were made
533
+ self.class.reflect_on_all_associations(:has_and_belongs_to_many).each do |a|
534
+ next unless
535
+ self.class.paper_trail_save_join_tables.include?(a.name) ||
536
+ a.klass.paper_trail_enabled_for_model?
537
+ assoc_version_args = {
538
+ version_id: version.transaction_id,
539
+ foreign_key_name: a.name
540
+ }
541
+ assoc_ids =
542
+ send(a.name).to_a.map(&:id) +
543
+ (@paper_trail_habtm.try(:[], a.name).try(:[], :removed) || []) -
544
+ (@paper_trail_habtm.try(:[], a.name).try(:[], :added) || [])
545
+ assoc_ids.each do |id|
546
+ PaperTrail::VersionAssociation.create(assoc_version_args.merge(foreign_key_id: id))
547
+ end
413
548
  end
414
549
  end
415
550
 
@@ -419,14 +554,14 @@ module PaperTrail
419
554
 
420
555
  def merge_metadata(data)
421
556
  # First we merge the model-level metadata in `meta`.
422
- paper_trail_options[:meta].each do |k,v|
557
+ paper_trail_options[:meta].each do |k, v|
423
558
  data[k] =
424
559
  if v.respond_to?(:call)
425
560
  v.call(self)
426
- elsif v.is_a?(Symbol) && respond_to?(v)
427
- # if it is an attribute that is changing in an existing object,
428
- # be sure to grab the current version
429
- if has_attribute?(v) && send("#{v}_changed?".to_sym) && data[:event] != 'create'
561
+ elsif v.is_a?(Symbol) && respond_to?(v, true)
562
+ # If it is an attribute that is changing in an existing object,
563
+ # be sure to grab the current version.
564
+ if has_attribute?(v) && send("#{v}_changed?".to_sym) && data[:event] != "create"
430
565
  send("#{v}_was".to_sym)
431
566
  else
432
567
  send(v)
@@ -435,34 +570,27 @@ module PaperTrail
435
570
  v
436
571
  end
437
572
  end
573
+
438
574
  # Second we merge any extra data from the controller (if available).
439
575
  data.merge(PaperTrail.controller_info || {})
440
576
  end
441
577
 
442
578
  def attributes_before_change
443
- attributes.tap do |prev|
444
- enums = self.respond_to?(:defined_enums) ? self.defined_enums : {}
445
- changed_attributes.select { |k,v| self.class.column_names.include?(k) }.each do |attr, before|
446
- before = enums[attr][before] if enums[attr]
447
- prev[attr] = before
448
- end
449
- end
579
+ changed = changed_attributes.select { |k, _v| self.class.column_names.include?(k) }
580
+ attributes.merge(changed)
450
581
  end
451
582
 
452
- # returns hash of attributes (with appropriate attributes serialized),
453
- # ommitting attributes to be skipped
454
- def object_attrs_for_paper_trail(attributes_hash)
455
- attrs = attributes_hash.except(*self.paper_trail_options[:skip])
456
- if PaperTrail.serialized_attributes?
457
- self.class.serialize_attributes_for_paper_trail!(attrs)
458
- end
583
+ # Returns hash of attributes (with appropriate attributes serialized),
584
+ # ommitting attributes to be skipped.
585
+ def object_attrs_for_paper_trail
586
+ attrs = attributes_before_change.except(*paper_trail_options[:skip])
587
+ AttributeSerializers::ObjectAttribute.new(self.class).serialize(attrs)
459
588
  attrs
460
589
  end
461
590
 
462
- # This method determines whether it is appropriate to generate a new
463
- # version instance. A timestamp-only update (e.g. only `updated_at`
464
- # changed) is considered notable unless an ignored attribute was also
465
- # changed.
591
+ # Determines whether it is appropriate to generate a new version
592
+ # instance. A timestamp-only update (e.g. only `updated_at` changed) is
593
+ # considered notable unless an ignored attribute was also changed.
466
594
  def changed_notably?
467
595
  if ignored_attr_has_changed?
468
596
  timestamps = timestamp_attributes_for_update_in_model.map(&:to_s)
@@ -473,41 +601,73 @@ module PaperTrail
473
601
  end
474
602
 
475
603
  # An attributed is "ignored" if it is listed in the `:ignore` option
476
- # and/or the `:skip` option. Returns true if an ignored attribute
477
- # has changed.
604
+ # and/or the `:skip` option. Returns true if an ignored attribute has
605
+ # changed.
478
606
  def ignored_attr_has_changed?
479
- ignored = self.paper_trail_options[:ignore] + self.paper_trail_options[:skip]
607
+ ignored = paper_trail_options[:ignore] + paper_trail_options[:skip]
480
608
  ignored.any? && (changed & ignored).any?
481
609
  end
482
610
 
483
611
  def notably_changed
484
- only = self.paper_trail_options[:only].dup
485
- # remove Hash arguments and then evaluate whether the attributes (the keys of the hash) should also get pushed into the collection
612
+ only = paper_trail_options[:only].dup
613
+ # Remove Hash arguments and then evaluate whether the attributes (the
614
+ # keys of the hash) should also get pushed into the collection.
486
615
  only.delete_if do |obj|
487
- obj.is_a?(Hash) && obj.each { |attr, condition| only << attr if condition.respond_to?(:call) && condition.call(self) }
616
+ obj.is_a?(Hash) &&
617
+ obj.each { |attr, condition|
618
+ only << attr if condition.respond_to?(:call) && condition.call(self)
619
+ }
488
620
  end
489
621
  only.empty? ? changed_and_not_ignored : (changed_and_not_ignored & only)
490
622
  end
491
623
 
492
624
  def changed_and_not_ignored
493
- ignore = self.paper_trail_options[:ignore].dup
494
- # remove Hash arguments and then evaluate whether the attributes (the keys of the hash) should also get pushed into the collection
625
+ ignore = paper_trail_options[:ignore].dup
626
+ # Remove Hash arguments and then evaluate whether the attributes (the
627
+ # keys of the hash) should also get pushed into the collection.
495
628
  ignore.delete_if do |obj|
496
- obj.is_a?(Hash) && obj.each { |attr, condition| ignore << attr if condition.respond_to?(:call) && condition.call(self) }
629
+ obj.is_a?(Hash) &&
630
+ obj.each { |attr, condition|
631
+ ignore << attr if condition.respond_to?(:call) && condition.call(self)
632
+ }
497
633
  end
498
- skip = self.paper_trail_options[:skip]
634
+ skip = paper_trail_options[:skip]
499
635
  changed - ignore - skip
500
636
  end
501
637
 
502
638
  def paper_trail_switched_on?
503
- PaperTrail.enabled? && PaperTrail.enabled_for_controller? && self.paper_trail_enabled_for_model?
639
+ PaperTrail.enabled? &&
640
+ PaperTrail.enabled_for_controller? &&
641
+ paper_trail_enabled_for_model?
504
642
  end
505
643
 
506
644
  def save_version?
507
- if_condition = self.paper_trail_options[:if]
508
- unless_condition = self.paper_trail_options[:unless]
645
+ if_condition = paper_trail_options[:if]
646
+ unless_condition = paper_trail_options[:unless]
509
647
  (if_condition.blank? || if_condition.call(self)) && !unless_condition.try(:call, self)
510
648
  end
649
+
650
+ def add_transaction_id_to(data)
651
+ return unless self.class.paper_trail_version_class.column_names.include?("transaction_id")
652
+ data[:transaction_id] = PaperTrail.transaction_id
653
+ end
654
+
655
+ # @api private
656
+ def update_transaction_id(version)
657
+ return unless self.class.paper_trail_version_class.column_names.include?("transaction_id")
658
+ if PaperTrail.transaction? && PaperTrail.transaction_id.nil?
659
+ PaperTrail.transaction_id = version.id
660
+ version.transaction_id = version.id
661
+ version.save
662
+ end
663
+ end
664
+
665
+ def log_version_errors(version, action)
666
+ version.logger.warn(
667
+ "Unable to create version for #{action} of #{self.class.name}##{id}: " +
668
+ version.errors.full_messages.join(", ")
669
+ )
670
+ end
511
671
  end
512
672
  end
513
673
  end