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,7 +1,12 @@
1
1
  module PaperTrail
2
2
  module Rails
3
+ # See http://guides.rubyonrails.org/engines.html
3
4
  class Engine < ::Rails::Engine
4
- paths['app/models'] << 'lib/paper_trail/frameworks/active_record/models'
5
+ paths["app/models"] << "lib/paper_trail/frameworks/active_record/models"
6
+ config.paper_trail = ActiveSupport::OrderedOptions.new
7
+ initializer "paper_trail.initialisation" do |app|
8
+ PaperTrail.enabled = app.config.paper_trail.fetch(:enabled, true)
9
+ end
5
10
  end
6
11
  end
7
12
  end
@@ -1,6 +1,6 @@
1
- require 'rspec/core'
2
- require 'rspec/matchers'
3
- require 'paper_trail/frameworks/rspec/helpers'
1
+ require "rspec/core"
2
+ require "rspec/matchers"
3
+ require "paper_trail/frameworks/rspec/helpers"
4
4
 
5
5
  RSpec.configure do |config|
6
6
  config.include ::PaperTrail::RSpec::Helpers::InstanceMethods
@@ -13,17 +13,28 @@ RSpec.configure do |config|
13
13
  ::PaperTrail.controller_info = {} if defined?(::Rails) && defined?(::RSpec::Rails)
14
14
  end
15
15
 
16
- config.before(:each, :versioning => true) do
16
+ config.before(:each, versioning: true) do
17
17
  ::PaperTrail.enabled = true
18
18
  end
19
19
  end
20
20
 
21
21
  RSpec::Matchers.define :be_versioned do
22
22
  # check to see if the model has `has_paper_trail` declared on it
23
- match { |actual| actual.kind_of?(::PaperTrail::Model::InstanceMethods) }
23
+ match { |actual| actual.is_a?(::PaperTrail::Model::InstanceMethods) }
24
24
  end
25
25
 
26
26
  RSpec::Matchers.define :have_a_version_with do |attributes|
27
27
  # check if the model has a version with the specified attributes
28
- match { |actual| actual.versions.where_object(attributes).any? }
28
+ match do |actual|
29
+ versions_association = actual.class.versions_association_name
30
+ actual.send(versions_association).where_object(attributes).any?
31
+ end
32
+ end
33
+
34
+ RSpec::Matchers.define :have_a_version_with_changes do |attributes|
35
+ # check if the model has a version changes with the specified attributes
36
+ match do |actual|
37
+ versions_association = actual.class.versions_association_name
38
+ actual.send(versions_association).where_object_changes(attributes).any?
39
+ end
29
40
  end
@@ -1,6 +1,7 @@
1
1
  module PaperTrail
2
2
  module RSpec
3
3
  module Helpers
4
+ # Included in the RSpec configuration in `frameworks/rspec.rb`
4
5
  module InstanceMethods
5
6
  # enable versioning for specific blocks (at instance-level)
6
7
  def with_versioning
@@ -12,10 +13,11 @@ module PaperTrail
12
13
  end
13
14
  end
14
15
 
16
+ # Extended by the RSpec configuration in `frameworks/rspec.rb`
15
17
  module ClassMethods
16
18
  # enable versioning for specific blocks (at class-level)
17
19
  def with_versioning(&block)
18
- context 'with versioning', :versioning => true do
20
+ context "with versioning", versioning: true do
19
21
  class_exec(&block)
20
22
  end
21
23
  end
@@ -1,13 +1,21 @@
1
- require 'active_support/core_ext/object' # provides the `try` method
2
- require 'paper_trail/attributes_serialization'
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"
5
+ require "paper_trail/model_config"
6
+ require "paper_trail/record_trail"
3
7
 
4
8
  module PaperTrail
9
+ # Extensions to `ActiveRecord::Base`. See `frameworks/active_record.rb`.
10
+ # It is our goal to have the smallest possible footprint here, because
11
+ # `ActiveRecord::Base` is a very crowded namespace! That is why we introduced
12
+ # `.paper_trail` and `#paper_trail`.
5
13
  module Model
6
-
7
14
  def self.included(base)
8
15
  base.send :extend, ClassMethods
9
16
  end
10
17
 
18
+ # :nodoc:
11
19
  module ClassMethods
12
20
  # Declare this in your model to track every create, update, and destroy.
13
21
  # Each version of the model is available in the `versions` association.
@@ -45,516 +53,30 @@ module PaperTrail
45
53
  # the instance was reified from. Default is `:version`.
46
54
  # - :save_changes - Whether or not to save changes to the object_changes
47
55
  # column if it exists. Default is true
56
+ # - :join_tables - If the model has a has_and_belongs_to_many relation
57
+ # with an unpapertrailed model, passing the name of the association to
58
+ # the join_tables option will paper trail the join table but not save
59
+ # the other model, allowing reification of the association but with the
60
+ # other models latest state (if the other model is paper trailed, this
61
+ # option does nothing)
48
62
  #
63
+ # @api public
49
64
  def has_paper_trail(options = {})
50
- options[:on] ||= [:create, :update, :destroy]
51
-
52
- # Wrap the :on option in an array if necessary. This allows a single
53
- # symbol to be passed in.
54
- options[:on] = Array(options[:on])
55
-
56
- setup_model_for_paper_trail(options)
57
-
58
- setup_callbacks_from_options options[:on]
65
+ paper_trail.setup(options)
59
66
  end
60
67
 
61
- def setup_model_for_paper_trail(options = {})
62
- # Lazily include the instance methods so we don't clutter up
63
- # any more ActiveRecord models than we have to.
64
- send :include, InstanceMethods
65
- send :extend, AttributesSerialization
66
-
67
- class_attribute :version_association_name
68
- self.version_association_name = options[:version] || :version
69
-
70
- # The version this instance was reified from.
71
- attr_accessor self.version_association_name
72
-
73
- class_attribute :version_class_name
74
- self.version_class_name = options[:class_name] || 'PaperTrail::Version'
75
-
76
- class_attribute :paper_trail_options
77
-
78
- self.paper_trail_options = options.dup
79
-
80
- [:ignore, :skip, :only].each do |k|
81
- paper_trail_options[k] =
82
- [paper_trail_options[k]].flatten.compact.map { |attr| attr.is_a?(Hash) ? attr.stringify_keys : attr.to_s }
83
- end
84
-
85
- paper_trail_options[:meta] ||= {}
86
- paper_trail_options[:save_changes] = true if paper_trail_options[:save_changes].nil?
87
-
88
- class_attribute :versions_association_name
89
- self.versions_association_name = options[:versions] || :versions
90
-
91
- attr_accessor :paper_trail_event
92
-
93
- # `has_many` syntax for specifying order uses a lambda in Rails 4
94
- if ::ActiveRecord::VERSION::MAJOR >= 4
95
- has_many self.versions_association_name,
96
- lambda { order(model.timestamp_sort_order) },
97
- :class_name => self.version_class_name, :as => :item
98
- else
99
- has_many self.versions_association_name,
100
- :class_name => self.version_class_name,
101
- :as => :item,
102
- :order => self.paper_trail_version_class.timestamp_sort_order
103
- end
104
-
105
- # Reset the transaction id when the transaction is closed.
106
- after_commit :reset_transaction_id
107
- after_rollback :reset_transaction_id
108
- after_rollback :clear_rolled_back_versions
109
- end
110
-
111
- def setup_callbacks_from_options(options_on = [])
112
- options_on.each do |option|
113
- send "paper_trail_on_#{option}"
114
- end
115
- end
116
-
117
- # Record version before or after "destroy" event
118
- def paper_trail_on_destroy(recording_order = 'after')
119
- unless %w[after before].include?(recording_order.to_s)
120
- fail ArgumentError, 'recording order can only be "after" or "before"'
121
- end
122
-
123
- if recording_order.to_s == 'after' and
124
- Gem::Version.new(ActiveRecord::VERSION::STRING).release >= Gem::Version.new("5")
125
- if ::ActiveRecord::Base.belongs_to_required_by_default
126
- ::ActiveSupport::Deprecation.warn(
127
- "paper_trail_on_destroy(:after) is incompatible with ActiveRecord " +
128
- "belongs_to_required_by_default and has no effect. Please use :before " +
129
- "or disable belongs_to_required_by_default."
130
- )
131
- end
132
- end
133
-
134
- send "#{recording_order}_destroy", :record_destroy, :if => :save_version?
135
-
136
- return if paper_trail_options[:on].include?(:destroy)
137
- paper_trail_options[:on] << :destroy
138
- end
139
-
140
- # Record version after "update" event
141
- def paper_trail_on_update
142
- before_save :reset_timestamp_attrs_for_update_if_needed!,
143
- :on => :update
144
- after_update :record_update,
145
- :if => :save_version?
146
- after_update :clear_version_instance!
147
-
148
- return if paper_trail_options[:on].include?(:update)
149
- paper_trail_options[:on] << :update
150
- end
151
-
152
- # Record version after "create" event
153
- def paper_trail_on_create
154
- after_create :record_create,
155
- :if => :save_version?
156
-
157
- return if paper_trail_options[:on].include?(:create)
158
- paper_trail_options[:on] << :create
159
- end
160
-
161
- # Switches PaperTrail off for this class.
162
- def paper_trail_off!
163
- PaperTrail.enabled_for_model(self, false)
164
- end
165
-
166
- # Switches PaperTrail on for this class.
167
- def paper_trail_on!
168
- PaperTrail.enabled_for_model(self, true)
169
- end
170
-
171
- def paper_trail_enabled_for_model?
172
- return false unless self.include?(PaperTrail::Model::InstanceMethods)
173
- PaperTrail.enabled_for_model?(self)
174
- end
175
-
176
- def paper_trail_version_class
177
- @paper_trail_version_class ||= version_class_name.constantize
68
+ # @api public
69
+ def paper_trail
70
+ ::PaperTrail::ModelConfig.new(self)
178
71
  end
179
72
  end
180
73
 
181
74
  # Wrap the following methods in a module so we can include them only in the
182
75
  # ActiveRecord models that declare `has_paper_trail`.
183
76
  module InstanceMethods
184
- # Returns true if this instance is the current, live one;
185
- # returns false if this instance came from a previous version.
186
- def live?
187
- source_version.nil?
188
- end
189
-
190
- # Returns who put the object into its current state.
191
- def paper_trail_originator
192
- (source_version || send(self.class.versions_association_name).last).try(:whodunnit)
193
- end
194
-
195
- def originator
196
- ::ActiveSupport::Deprecation.warn "Use paper_trail_originator instead of originator."
197
- self.paper_trail_originator
198
- end
199
-
200
- # Invoked after rollbacks to ensure versions records are not created
201
- # for changes that never actually took place
202
- def clear_rolled_back_versions
203
- send(self.class.versions_association_name).reload
204
- end
205
-
206
- # Returns the object (not a Version) as it was at the given timestamp.
207
- def version_at(timestamp, reify_options={})
208
- # Because a version stores how its object looked *before* the change,
209
- # we need to look for the first version created *after* the timestamp.
210
- v = send(self.class.versions_association_name).subsequent(timestamp, true).first
211
- return v.reify(reify_options) if v
212
- self unless self.destroyed?
213
- end
214
-
215
- # Returns the objects (not Versions) as they were between the given times.
216
- def versions_between(start_time, end_time, reify_options={})
217
- versions = send(self.class.versions_association_name).between(start_time, end_time)
218
- versions.collect { |version| version_at(version.send PaperTrail.timestamp_field) }
219
- end
220
-
221
- # Returns the object (not a Version) as it was most recently.
222
- def previous_version
223
- preceding_version = source_version ? source_version.previous : send(self.class.versions_association_name).last
224
- preceding_version.reify if preceding_version
225
- end
226
-
227
- # Returns the object (not a Version) as it became next.
228
- # NOTE: if self (the item) was not reified from a version, i.e. it is the
229
- # "live" item, we return nil. Perhaps we should return self instead?
230
- def next_version
231
- subsequent_version = source_version.next
232
- subsequent_version ? subsequent_version.reify : self.class.find(self.id)
233
- rescue
234
- nil
235
- end
236
-
237
- def paper_trail_enabled_for_model?
238
- self.class.paper_trail_enabled_for_model?
239
- end
240
-
241
- # Executes the given method or block without creating a new version.
242
- def without_versioning(method = nil)
243
- paper_trail_was_enabled = self.paper_trail_enabled_for_model?
244
- self.class.paper_trail_off!
245
- method ? method.to_proc.call(self) : yield(self)
246
- ensure
247
- self.class.paper_trail_on! if paper_trail_was_enabled
248
- end
249
-
250
- # Utility method for reifying. Anything executed inside the block will
251
- # appear like a new record.
252
- def appear_as_new_record
253
- instance_eval {
254
- alias :old_new_record? :new_record?
255
- alias :new_record? :present?
256
- }
257
- yield
258
- instance_eval { alias :new_record? :old_new_record? }
259
- end
260
-
261
- # Temporarily overwrites the value of whodunnit and then executes the
262
- # provided block.
263
- def whodunnit(value)
264
- raise ArgumentError, 'expected to receive a block' unless block_given?
265
- current_whodunnit = PaperTrail.whodunnit
266
- PaperTrail.whodunnit = value
267
- yield self
268
- ensure
269
- PaperTrail.whodunnit = current_whodunnit
270
- end
271
-
272
- # Mimics the `touch` method from `ActiveRecord::Persistence`, but also
273
- # creates a version. A version is created regardless of options such as
274
- # `:on`, `:if`, or `:unless`.
275
- #
276
- # TODO: look into leveraging the `after_touch` callback from
277
- # `ActiveRecord` to allow the regular `touch` method to generate a version
278
- # as normal. May make sense to switch the `record_update` method to
279
- # leverage an `after_update` callback anyways (likely for v4.0.0)
280
- def touch_with_version(name = nil)
281
- raise ActiveRecordError, "can not touch on a new record object" unless persisted?
282
-
283
- attributes = timestamp_attributes_for_update_in_model
284
- attributes << name if name
285
- current_time = current_time_from_proper_timezone
286
-
287
- attributes.each { |column| write_attribute(column, current_time) }
288
-
289
- record_update(true) unless will_record_after_update?
290
- save!(:validate => false)
291
- end
292
-
293
- private
294
-
295
- # Returns true if `save` will cause `record_update`
296
- # to be called via the `after_update` callback.
297
- def will_record_after_update?
298
- on = paper_trail_options[:on]
299
- on.nil? || on.include?(:update)
300
- end
301
-
302
- def source_version
303
- send self.class.version_association_name
304
- end
305
-
306
- def record_create
307
- if paper_trail_switched_on?
308
- data = {
309
- :event => paper_trail_event || 'create',
310
- :whodunnit => PaperTrail.whodunnit
311
- }
312
- if respond_to?(:updated_at)
313
- data[PaperTrail.timestamp_field] = updated_at
314
- end
315
- if pt_record_object_changes? && changed_notably?
316
- data[:object_changes] = pt_recordable_object_changes
317
- end
318
- if self.class.paper_trail_version_class.column_names.include?('transaction_id')
319
- data[:transaction_id] = PaperTrail.transaction_id
320
- end
321
- version = send(self.class.versions_association_name).create! merge_metadata(data)
322
- set_transaction_id(version)
323
- save_associations(version)
324
- end
325
- end
326
-
327
- def record_update(force = nil)
328
- if paper_trail_switched_on? && (force || changed_notably?)
329
- data = {
330
- :event => paper_trail_event || 'update',
331
- :object => pt_recordable_object,
332
- :whodunnit => PaperTrail.whodunnit
333
- }
334
- if respond_to?(:updated_at)
335
- data[PaperTrail.timestamp_field] = updated_at
336
- end
337
- if pt_record_object_changes?
338
- data[:object_changes] = pt_recordable_object_changes
339
- end
340
- if self.class.paper_trail_version_class.column_names.include?('transaction_id')
341
- data[:transaction_id] = PaperTrail.transaction_id
342
- end
343
- version = send(self.class.versions_association_name).create merge_metadata(data)
344
- set_transaction_id(version)
345
- save_associations(version)
346
- end
347
- end
348
-
349
- # Returns a boolean indicating whether to store serialized version diffs
350
- # in the `object_changes` column of the version record.
351
- # @api private
352
- def pt_record_object_changes?
353
- paper_trail_options[:save_changes] &&
354
- self.class.paper_trail_version_class.column_names.include?('object_changes')
355
- end
356
-
357
- # Returns an object which can be assigned to the `object` attribute of a
358
- # nascent version record. If the `object` column is a postgres `json`
359
- # column, then a hash can be used in the assignment, otherwise the column
360
- # is a `text` column, and we must perform the serialization here, using
361
- # `PaperTrail.serializer`.
362
- # @api private
363
- def pt_recordable_object
364
- object_attrs = object_attrs_for_paper_trail(attributes_before_change)
365
- if self.class.paper_trail_version_class.object_col_is_json?
366
- object_attrs
367
- else
368
- PaperTrail.serializer.dump(object_attrs)
369
- end
370
- end
371
-
372
- # Returns an object which can be assigned to the `object_changes`
373
- # attribute of a nascent version record. If the `object_changes` column is
374
- # a postgres `json` column, then a hash can be used in the assignment,
375
- # otherwise the column is a `text` column, and we must perform the
376
- # serialization here, using `PaperTrail.serializer`.
377
- # @api private
378
- def pt_recordable_object_changes
379
- if self.class.paper_trail_version_class.object_changes_col_is_json?
380
- changes_for_paper_trail
381
- else
382
- PaperTrail.serializer.dump(changes_for_paper_trail)
383
- end
384
- end
385
-
386
- def changes_for_paper_trail
387
- _changes = changes.delete_if { |k,v| !notably_changed.include?(k) }
388
- self.class.serialize_attribute_changes_for_paper_trail!(_changes)
389
- _changes.to_hash
390
- end
391
-
392
- # Invoked via`after_update` callback for when a previous version is
393
- # reified and then saved.
394
- def clear_version_instance!
395
- send("#{self.class.version_association_name}=", nil)
396
- end
397
-
398
- # Invoked via callback when a user attempts to persist a reified
399
- # `Version`.
400
- def reset_timestamp_attrs_for_update_if_needed!
401
- return if self.live?
402
- timestamp_attributes_for_update_in_model.each do |column|
403
- # ActiveRecord 4.2 deprecated `reset_column!` in favor of
404
- # `restore_column!`.
405
- if respond_to?("restore_#{column}!")
406
- send("restore_#{column}!")
407
- else
408
- send("reset_#{column}!")
409
- end
410
- end
411
- end
412
-
413
- def record_destroy
414
- if paper_trail_switched_on? and not new_record?
415
- data = {
416
- :item_id => self.id,
417
- :item_type => self.class.base_class.name,
418
- :event => paper_trail_event || 'destroy',
419
- :object => pt_recordable_object,
420
- :whodunnit => PaperTrail.whodunnit
421
- }
422
- if self.class.paper_trail_version_class.column_names.include?('transaction_id')
423
- data[:transaction_id] = PaperTrail.transaction_id
424
- end
425
- version = self.class.paper_trail_version_class.create(merge_metadata(data))
426
- send("#{self.class.version_association_name}=", version)
427
- send(self.class.versions_association_name).send :load_target
428
- set_transaction_id(version)
429
- save_associations(version)
430
- end
431
- end
432
-
433
- # Saves associations if the join table for `VersionAssociation` exists.
434
- def save_associations(version)
435
- return unless PaperTrail.config.track_associations?
436
- self.class.reflect_on_all_associations(:belongs_to).each do |assoc|
437
- assoc_version_args = {
438
- :version_id => version.id,
439
- :foreign_key_name => assoc.foreign_key
440
- }
441
-
442
- if assoc.options[:polymorphic]
443
- associated_record = send(assoc.name) if send(assoc.foreign_type)
444
- if associated_record && associated_record.class.paper_trail_enabled_for_model?
445
- assoc_version_args.merge!(:foreign_key_id => associated_record.id)
446
- end
447
- elsif assoc.klass.paper_trail_enabled_for_model?
448
- assoc_version_args.merge!(:foreign_key_id => send(assoc.foreign_key))
449
- end
450
-
451
- PaperTrail::VersionAssociation.create(assoc_version_args) if assoc_version_args.has_key?(:foreign_key_id)
452
- end
453
- end
454
-
455
- def set_transaction_id(version)
456
- return unless self.class.paper_trail_version_class.column_names.include?('transaction_id')
457
- if PaperTrail.transaction? && PaperTrail.transaction_id.nil?
458
- PaperTrail.transaction_id = version.id
459
- version.transaction_id = version.id
460
- version.save
461
- end
462
- end
463
-
464
- def reset_transaction_id
465
- PaperTrail.transaction_id = nil
466
- end
467
-
468
- def merge_metadata(data)
469
- # First we merge the model-level metadata in `meta`.
470
- paper_trail_options[:meta].each do |k,v|
471
- data[k] =
472
- if v.respond_to?(:call)
473
- v.call(self)
474
- elsif v.is_a?(Symbol) && respond_to?(v)
475
- # If it is an attribute that is changing in an existing object,
476
- # be sure to grab the current version.
477
- if has_attribute?(v) && send("#{v}_changed?".to_sym) && data[:event] != 'create'
478
- send("#{v}_was".to_sym)
479
- else
480
- send(v)
481
- end
482
- else
483
- v
484
- end
485
- end
486
-
487
- # Second we merge any extra data from the controller (if available).
488
- data.merge(PaperTrail.controller_info || {})
489
- end
490
-
491
- def attributes_before_change
492
- attributes.tap do |prev|
493
- enums = self.respond_to?(:defined_enums) ? self.defined_enums : {}
494
- changed_attributes.select { |k,v| self.class.column_names.include?(k) }.each do |attr, before|
495
- before = enums[attr][before] if enums[attr]
496
- prev[attr] = before
497
- end
498
- end
499
- end
500
-
501
- # Returns hash of attributes (with appropriate attributes serialized),
502
- # ommitting attributes to be skipped.
503
- def object_attrs_for_paper_trail(attributes_hash)
504
- attrs = attributes_hash.except(*self.paper_trail_options[:skip])
505
- self.class.serialize_attributes_for_paper_trail!(attrs)
506
- attrs
507
- end
508
-
509
- # Determines whether it is appropriate to generate a new version
510
- # instance. A timestamp-only update (e.g. only `updated_at` changed) is
511
- # considered notable unless an ignored attribute was also changed.
512
- def changed_notably?
513
- if ignored_attr_has_changed?
514
- timestamps = timestamp_attributes_for_update_in_model.map(&:to_s)
515
- (notably_changed - timestamps).any?
516
- else
517
- notably_changed.any?
518
- end
519
- end
520
-
521
- # An attributed is "ignored" if it is listed in the `:ignore` option
522
- # and/or the `:skip` option. Returns true if an ignored attribute has
523
- # changed.
524
- def ignored_attr_has_changed?
525
- ignored = self.paper_trail_options[:ignore] + self.paper_trail_options[:skip]
526
- ignored.any? && (changed & ignored).any?
527
- end
528
-
529
- def notably_changed
530
- only = self.paper_trail_options[:only].dup
531
- # Remove Hash arguments and then evaluate whether the attributes (the
532
- # keys of the hash) should also get pushed into the collection.
533
- only.delete_if do |obj|
534
- obj.is_a?(Hash) && obj.each { |attr, condition| only << attr if condition.respond_to?(:call) && condition.call(self) }
535
- end
536
- only.empty? ? changed_and_not_ignored : (changed_and_not_ignored & only)
537
- end
538
-
539
- def changed_and_not_ignored
540
- ignore = self.paper_trail_options[:ignore].dup
541
- # Remove Hash arguments and then evaluate whether the attributes (the
542
- # keys of the hash) should also get pushed into the collection.
543
- ignore.delete_if do |obj|
544
- obj.is_a?(Hash) && obj.each { |attr, condition| ignore << attr if condition.respond_to?(:call) && condition.call(self) }
545
- end
546
- skip = self.paper_trail_options[:skip]
547
- changed - ignore - skip
548
- end
549
-
550
- def paper_trail_switched_on?
551
- PaperTrail.enabled? && PaperTrail.enabled_for_controller? && self.paper_trail_enabled_for_model?
552
- end
553
-
554
- def save_version?
555
- if_condition = self.paper_trail_options[:if]
556
- unless_condition = self.paper_trail_options[:unless]
557
- (if_condition.blank? || if_condition.call(self)) && !unless_condition.try(:call, self)
77
+ # @api public
78
+ def paper_trail
79
+ ::PaperTrail::RecordTrail.new(self)
558
80
  end
559
81
  end
560
82
  end