Class: PaperTrail::RecordTrail
- Inherits:
-
Object
- Object
- PaperTrail::RecordTrail
- Defined in:
- lib/paper_trail/record_trail.rb
Overview
Represents the “paper trail” for a single record.
Instance Method Summary collapse
-
#appear_as_new_record ⇒ Object
Utility method for reifying.
- #attributes_before_change ⇒ Object
- #changed_and_not_ignored ⇒ Object
-
#changed_notably? ⇒ Boolean
Determines whether it is appropriate to generate a new version instance.
- #changes ⇒ Object private
-
#clear_rolled_back_versions ⇒ Object
Invoked after rollbacks to ensure versions records are not created for changes that never actually took place.
-
#clear_version_instance ⇒ Object
Invoked via`after_update` callback for when a previous version is reified and then saved.
- #enabled? ⇒ Boolean
- #enabled_for_model? ⇒ Boolean
-
#ignored_attr_has_changed? ⇒ Boolean
An attributed is “ignored” if it is listed in the ‘:ignore` option and/or the `:skip` option.
-
#initialize(record) ⇒ RecordTrail
constructor
A new instance of RecordTrail.
-
#live? ⇒ Boolean
Returns true if this instance is the current, live one; returns false if this instance came from a previous version.
- #merge_metadata(data) ⇒ Object private
-
#next_version ⇒ Object
Returns the object (not a Version) as it became next.
- #notably_changed ⇒ Object
-
#object_attrs_for_paper_trail ⇒ Object
Returns hash of attributes (with appropriate attributes serialized), omitting attributes to be skipped.
-
#originator ⇒ Object
Returns who put ‘@record` into its current state.
-
#previous_version ⇒ Object
Returns the object (not a Version) as it was most recently.
- #record_create ⇒ Object
- #record_destroy ⇒ Object
-
#record_object_changes? ⇒ Boolean
private
Returns a boolean indicating whether to store serialized version diffs in the ‘object_changes` column of the version record.
- #record_update(force) ⇒ Object
-
#recordable_object ⇒ Object
private
Returns an object which can be assigned to the ‘object` attribute of a nascent version record.
-
#recordable_object_changes ⇒ Object
private
Returns an object which can be assigned to the ‘object_changes` attribute of a nascent version record.
-
#reset_timestamp_attrs_for_update_if_needed ⇒ Object
Invoked via callback when a user attempts to persist a reified ‘Version`.
-
#save_associations(version) ⇒ Object
Saves associations if the join table for ‘VersionAssociation` exists.
- #save_associations_belongs_to(version) ⇒ Object
- #save_associations_habtm(version) ⇒ Object
-
#save_version? ⇒ Boolean
private
AR callback.
- #source_version ⇒ Object
-
#touch_with_version(name = nil) ⇒ Object
Mimics the ‘touch` method from `ActiveRecord::Persistence`, but also creates a version.
-
#version_at(timestamp, reify_options = {}) ⇒ Object
Returns the object (not a Version) as it was at the given timestamp.
-
#versions_between(start_time, end_time) ⇒ Object
Returns the objects (not Versions) as they were between the given times.
-
#whodunnit(value) ⇒ Object
Temporarily overwrites the value of whodunnit and then executes the provided block.
-
#without_versioning(method = nil) ⇒ Object
Executes the given method or block without creating a new version.
Constructor Details
#initialize(record) ⇒ RecordTrail
Returns a new instance of RecordTrail.
4 5 6 |
# File 'lib/paper_trail/record_trail.rb', line 4 def initialize(record) @record = record end |
Instance Method Details
#appear_as_new_record ⇒ Object
Utility method for reifying. Anything executed inside the block will appear like a new record.
10 11 12 13 14 15 16 17 |
# File 'lib/paper_trail/record_trail.rb', line 10 def appear_as_new_record @record.instance_eval { alias :old_new_record? :new_record? alias :new_record? :present? } yield @record.instance_eval { alias :new_record? :old_new_record? } end |
#attributes_before_change ⇒ Object
19 20 21 22 23 24 |
# File 'lib/paper_trail/record_trail.rb', line 19 def attributes_before_change changed = @record.changed_attributes.select { |k, _v| @record.class.column_names.include?(k) } @record.attributes.merge(changed) end |
#changed_and_not_ignored ⇒ Object
26 27 28 29 30 31 32 33 34 35 36 37 38 |
# File 'lib/paper_trail/record_trail.rb', line 26 def changed_and_not_ignored ignore = @record.[:ignore].dup # Remove Hash arguments and then evaluate whether the attributes (the # keys of the hash) should also get pushed into the collection. ignore.delete_if do |obj| obj.is_a?(Hash) && obj.each { |attr, condition| ignore << attr if condition.respond_to?(:call) && condition.call(@record) } end skip = @record.[:skip] @record.changed - ignore - skip end |
#changed_notably? ⇒ Boolean
Determines whether it is appropriate to generate a new version instance. A timestamp-only update (e.g. only ‘updated_at` changed) is considered notable unless an ignored attribute was also changed.
57 58 59 60 61 62 63 64 |
# File 'lib/paper_trail/record_trail.rb', line 57 def changed_notably? if ignored_attr_has_changed? = @record.send(:timestamp_attributes_for_update_in_model).map(&:to_s) (notably_changed - ).any? else notably_changed.any? end end |
#changes ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
67 68 69 70 71 72 73 74 75 |
# File 'lib/paper_trail/record_trail.rb', line 67 def changes notable_changes = @record.changes.delete_if { |k, _v| !notably_changed.include?(k) } AttributeSerializers::ObjectChangesAttribute. new(@record.class). serialize(notable_changes) notable_changes.to_hash end |
#clear_rolled_back_versions ⇒ Object
Invoked after rollbacks to ensure versions records are not created for changes that never actually took place. Optimization: Use lazy ‘reset` instead of eager `reload` because, in many use cases, the association will not be used.
44 45 46 |
# File 'lib/paper_trail/record_trail.rb', line 44 def clear_rolled_back_versions versions.reset end |
#clear_version_instance ⇒ Object
Invoked via`after_update` callback for when a previous version is reified and then saved.
50 51 52 |
# File 'lib/paper_trail/record_trail.rb', line 50 def clear_version_instance @record.send("#{@record.class.version_association_name}=", nil) end |
#enabled? ⇒ Boolean
77 78 79 |
# File 'lib/paper_trail/record_trail.rb', line 77 def enabled? PaperTrail.enabled? && PaperTrail.enabled_for_controller? && enabled_for_model? end |
#enabled_for_model? ⇒ Boolean
81 82 83 |
# File 'lib/paper_trail/record_trail.rb', line 81 def enabled_for_model? @record.class.paper_trail.enabled? end |
#ignored_attr_has_changed? ⇒ Boolean
An attributed is “ignored” if it is listed in the ‘:ignore` option and/or the `:skip` option. Returns true if an ignored attribute has changed.
88 89 90 91 |
# File 'lib/paper_trail/record_trail.rb', line 88 def ignored_attr_has_changed? ignored = @record.[:ignore] + @record.[:skip] ignored.any? && (@record.changed & ignored).any? end |
#live? ⇒ Boolean
Returns true if this instance is the current, live one; returns false if this instance came from a previous version.
95 96 97 |
# File 'lib/paper_trail/record_trail.rb', line 95 def live? source_version.nil? end |
#merge_metadata(data) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
# File 'lib/paper_trail/record_trail.rb', line 100 def (data) # First we merge the model-level metadata in `meta`. @record.[:meta].each do |k, v| data[k] = if v.respond_to?(:call) v.call(@record) elsif v.is_a?(Symbol) && @record.respond_to?(v, true) # If it is an attribute that is changing in an existing object, # be sure to grab the current version. if @record.has_attribute?(v) && @record.send("#{v}_changed?".to_sym) && data[:event] != "create" @record.send("#{v}_was".to_sym) else @record.send(v) end else v end end # Second we merge any extra data from the controller (if available). data.merge(PaperTrail.controller_info || {}) end |
#next_version ⇒ Object
Returns the object (not a Version) as it became next. NOTE: if self (the item) was not reified from a version, i.e. it is the
"live" item, we return nil. Perhaps we should return self instead?
128 129 130 131 132 133 |
# File 'lib/paper_trail/record_trail.rb', line 128 def next_version subsequent_version = source_version.next subsequent_version ? subsequent_version.reify : @record.class.find(@record.id) rescue # TODO: Rescue something more specific nil end |
#notably_changed ⇒ Object
135 136 137 138 139 140 141 142 143 144 145 146 |
# File 'lib/paper_trail/record_trail.rb', line 135 def notably_changed only = @record.[:only].dup # Remove Hash arguments and then evaluate whether the attributes (the # keys of the hash) should also get pushed into the collection. only.delete_if do |obj| obj.is_a?(Hash) && obj.each { |attr, condition| only << attr if condition.respond_to?(:call) && condition.call(@record) } end only.empty? ? changed_and_not_ignored : (changed_and_not_ignored & only) end |
#object_attrs_for_paper_trail ⇒ Object
Returns hash of attributes (with appropriate attributes serialized), omitting attributes to be skipped.
150 151 152 153 154 |
# File 'lib/paper_trail/record_trail.rb', line 150 def object_attrs_for_paper_trail attrs = attributes_before_change.except(*@record.[:skip]) AttributeSerializers::ObjectAttribute.new(@record.class).serialize(attrs) attrs end |
#originator ⇒ Object
Returns who put ‘@record` into its current state.
157 158 159 |
# File 'lib/paper_trail/record_trail.rb', line 157 def originator (source_version || versions.last).try(:whodunnit) end |
#previous_version ⇒ Object
Returns the object (not a Version) as it was most recently.
162 163 164 |
# File 'lib/paper_trail/record_trail.rb', line 162 def previous_version (source_version ? source_version.previous : versions.last).try(:reify) end |
#record_create ⇒ Object
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 |
# File 'lib/paper_trail/record_trail.rb', line 166 def record_create return unless enabled? data = { event: @record.paper_trail_event || "create", whodunnit: PaperTrail.whodunnit } if @record.respond_to?(:updated_at) data[PaperTrail.] = @record.updated_at end if record_object_changes? && changed_notably? data[:object_changes] = recordable_object_changes end add_transaction_id_to(data) versions_assoc = @record.send(@record.class.versions_association_name) version = versions_assoc.create! (data) update_transaction_id(version) save_associations(version) end |
#record_destroy ⇒ Object
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 |
# File 'lib/paper_trail/record_trail.rb', line 185 def record_destroy if enabled? && !@record.new_record? data = { item_id: @record.id, item_type: @record.class.base_class.name, event: @record.paper_trail_event || "destroy", object: recordable_object, whodunnit: PaperTrail.whodunnit } add_transaction_id_to(data) version = @record.class.paper_trail.version_class.create((data)) if version.errors.any? log_version_errors(version, :destroy) else @record.send("#{@record.class.version_association_name}=", version) @record.send(@record.class.versions_association_name).reset update_transaction_id(version) save_associations(version) end end end |
#record_object_changes? ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns a boolean indicating whether to store serialized version diffs in the ‘object_changes` column of the version record.
210 211 212 213 |
# File 'lib/paper_trail/record_trail.rb', line 210 def record_object_changes? @record.[:save_changes] && @record.class.paper_trail.version_class.column_names.include?("object_changes") end |
#record_update(force) ⇒ Object
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 |
# File 'lib/paper_trail/record_trail.rb', line 215 def record_update(force) if enabled? && (force || changed_notably?) data = { event: @record.paper_trail_event || "update", object: recordable_object, whodunnit: PaperTrail.whodunnit } if @record.respond_to?(:updated_at) data[PaperTrail.] = @record.updated_at end if record_object_changes? data[:object_changes] = recordable_object_changes end add_transaction_id_to(data) versions_assoc = @record.send(@record.class.versions_association_name) version = versions_assoc.create((data)) if version.errors.any? log_version_errors(version, :update) else update_transaction_id(version) save_associations(version) end end end |
#recordable_object ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns an object which can be assigned to the ‘object` attribute of a nascent version record. If the `object` column is a postgres `json` column, then a hash can be used in the assignment, otherwise the column is a `text` column, and we must perform the serialization here, using `PaperTrail.serializer`.
246 247 248 249 250 251 252 |
# File 'lib/paper_trail/record_trail.rb', line 246 def recordable_object if @record.class.paper_trail.version_class.object_col_is_json? object_attrs_for_paper_trail else PaperTrail.serializer.dump(object_attrs_for_paper_trail) end end |
#recordable_object_changes ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns an object which can be assigned to the ‘object_changes` attribute of a nascent version record. If the `object_changes` column is a postgres `json` column, then a hash can be used in the assignment, otherwise the column is a `text` column, and we must perform the serialization here, using `PaperTrail.serializer`.
260 261 262 263 264 265 266 |
# File 'lib/paper_trail/record_trail.rb', line 260 def recordable_object_changes if @record.class.paper_trail.version_class.object_changes_col_is_json? changes else PaperTrail.serializer.dump(changes) end end |
#reset_timestamp_attrs_for_update_if_needed ⇒ Object
Invoked via callback when a user attempts to persist a reified ‘Version`.
270 271 272 273 274 275 276 277 278 279 280 281 |
# File 'lib/paper_trail/record_trail.rb', line 270 def return if live? @record.send(:timestamp_attributes_for_update_in_model).each do |column| # ActiveRecord 4.2 deprecated `reset_column!` in favor of # `restore_column!`. if @record.respond_to?("restore_#{column}!") @record.send("restore_#{column}!") else @record.send("reset_#{column}!") end end end |
#save_associations(version) ⇒ Object
Saves associations if the join table for ‘VersionAssociation` exists.
284 285 286 287 288 |
# File 'lib/paper_trail/record_trail.rb', line 284 def save_associations(version) return unless PaperTrail.config.track_associations? save_associations_belongs_to(version) save_associations_habtm(version) end |
#save_associations_belongs_to(version) ⇒ Object
290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 |
# File 'lib/paper_trail/record_trail.rb', line 290 def save_associations_belongs_to(version) @record.class.reflect_on_all_associations(:belongs_to).each do |assoc| assoc_version_args = { version_id: version.id, foreign_key_name: assoc.foreign_key } if assoc.[:polymorphic] associated_record = @record.send(assoc.name) if @record.send(assoc.foreign_type) if associated_record && associated_record.class.paper_trail.enabled? assoc_version_args[:foreign_key_id] = associated_record.id end elsif assoc.klass.paper_trail.enabled? assoc_version_args[:foreign_key_id] = @record.send(assoc.foreign_key) end if assoc_version_args.key?(:foreign_key_id) PaperTrail::VersionAssociation.create(assoc_version_args) end end end |
#save_associations_habtm(version) ⇒ Object
312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 |
# File 'lib/paper_trail/record_trail.rb', line 312 def save_associations_habtm(version) # Use the :added and :removed keys to extrapolate the HABTM associations # to before any changes were made @record.class.reflect_on_all_associations(:has_and_belongs_to_many).each do |a| next unless @record.class.paper_trail_save_join_tables.include?(a.name) || a.klass.paper_trail.enabled? assoc_version_args = { version_id: version.transaction_id, foreign_key_name: a.name } assoc_ids = @record.send(a.name).to_a.map(&:id) + (@record.paper_trail_habtm.try(:[], a.name).try(:[], :removed) || []) - (@record.paper_trail_habtm.try(:[], a.name).try(:[], :added) || []) assoc_ids.each do |id| PaperTrail::VersionAssociation.create(assoc_version_args.merge(foreign_key_id: id)) end end end |
#save_version? ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
AR callback.
335 336 337 338 339 |
# File 'lib/paper_trail/record_trail.rb', line 335 def save_version? if_condition = @record.[:if] unless_condition = @record.[:unless] (if_condition.blank? || if_condition.call(@record)) && !unless_condition.try(:call, @record) end |
#source_version ⇒ Object
341 342 343 |
# File 'lib/paper_trail/record_trail.rb', line 341 def source_version version end |
#touch_with_version(name = nil) ⇒ Object
Mimics the ‘touch` method from `ActiveRecord::Persistence`, but also creates a version. A version is created regardless of options such as `:on`, `:if`, or `:unless`.
TODO: look into leveraging the ‘after_touch` callback from `ActiveRecord` to allow the regular `touch` method to generate a version as normal. May make sense to switch the `record_update` method to leverage an `after_update` callback anyways (likely for v4.0.0)
353 354 355 356 357 358 359 360 361 362 363 364 365 |
# File 'lib/paper_trail/record_trail.rb', line 353 def touch_with_version(name = nil) unless @record.persisted? raise ActiveRecordError, "can not touch on a new record object" end attributes = @record.send :timestamp_attributes_for_update_in_model attributes << name if name current_time = @record.send :current_time_from_proper_timezone attributes.each { |column| @record.send(:write_attribute, column, current_time) } @record.record_update(true) unless will_record_after_update? @record.save!(validate: false) end |
#version_at(timestamp, reify_options = {}) ⇒ Object
Returns the object (not a Version) as it was at the given timestamp.
368 369 370 371 372 373 374 |
# File 'lib/paper_trail/record_trail.rb', line 368 def version_at(, = {}) # Because a version stores how its object looked *before* the change, # we need to look for the first version created *after* the timestamp. v = versions.subsequent(, true).first return v.reify() if v @record unless @record.destroyed? end |
#versions_between(start_time, end_time) ⇒ Object
Returns the objects (not Versions) as they were between the given times.
377 378 379 380 381 382 |
# File 'lib/paper_trail/record_trail.rb', line 377 def versions_between(start_time, end_time) versions = send(@record.class.versions_association_name).between(start_time, end_time) versions.collect { |version| version_at(version.send(PaperTrail.)) } end |
#whodunnit(value) ⇒ Object
Temporarily overwrites the value of whodunnit and then executes the provided block.
403 404 405 406 407 408 409 410 |
# File 'lib/paper_trail/record_trail.rb', line 403 def whodunnit(value) raise ArgumentError, "expected to receive a block" unless block_given? current_whodunnit = PaperTrail.whodunnit PaperTrail.whodunnit = value yield @record ensure PaperTrail.whodunnit = current_whodunnit end |
#without_versioning(method = nil) ⇒ Object
Executes the given method or block without creating a new version.
385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 |
# File 'lib/paper_trail/record_trail.rb', line 385 def without_versioning(method = nil) paper_trail_was_enabled = enabled_for_model? @record.class.paper_trail.disable if method if respond_to?(method) public_send(method) else @record.send(method) end else yield @record end ensure @record.class.paper_trail.enable if paper_trail_was_enabled end |