From 84368ea289d3d24be71a091110fc152a71711790 Mon Sep 17 00:00:00 2001 From: Jared Beck Date: Sun, 22 Jul 2018 00:10:37 -0400 Subject: [PATCH] Extract `Events` module Introduces event classes, like Events::Create, which wrap the AR instance and have one public method, `#data`, which returns the attributes of nascent `Version` records. See comments in Events::Base. This extraction greatly simplifies RecordTrail. Not only have we moved a lot of code out of RecordTrail, but we've also moved the `@in_after_callback` variable. In its new location, this variable is much easier to understand, and we no longer need the `ensure` blocks to reset it. Preserving compatibility with PT-AT caused some minor difficulties, and we should follow up with them to see if we can simplify the API they are using. Specifically, we would like to delete the `data_for_*` methods. --- lib/paper_trail/events/base.rb | 273 ++++++++++++++++++++ lib/paper_trail/events/create.rb | 29 +++ lib/paper_trail/events/destroy.rb | 26 ++ lib/paper_trail/events/update.rb | 43 ++++ lib/paper_trail/record_trail.rb | 357 +++++---------------------- spec/models/gadget_spec.rb | 39 --- spec/paper_trail/events/base_spec.rb | 53 ++++ spec/paper_trail/serializer_spec.rb | 14 +- 8 files changed, 494 insertions(+), 340 deletions(-) create mode 100644 lib/paper_trail/events/base.rb create mode 100644 lib/paper_trail/events/create.rb create mode 100644 lib/paper_trail/events/destroy.rb create mode 100644 lib/paper_trail/events/update.rb create mode 100644 spec/paper_trail/events/base_spec.rb diff --git a/lib/paper_trail/events/base.rb b/lib/paper_trail/events/base.rb new file mode 100644 index 000000000..461c2cf40 --- /dev/null +++ b/lib/paper_trail/events/base.rb @@ -0,0 +1,273 @@ +# frozen_string_literal: true + +module PaperTrail + module Events + # We refer to times in the lifecycle of a record as "events". There are + # three events: + # + # - create + # - `after_create` we call `RecordTrail#record_create` + # - update + # - `after_update` we call `RecordTrail#record_update` + # - `after_touch` we call `RecordTrail#record_update` + # - `RecordTrail#save_with_version` calls `RecordTrail#record_update` + # - `RecordTrail#touch_with_version` (deprecated) calls `RecordTrail#record_update` + # - `RecordTrail#update_columns` is also referred to as an update, though + # it uses `RecordTrail#record_update_columns` rather than + # `RecordTrail#record_update` + # - destroy + # - `before_destroy` or `after_destroy` we call `RecordTrail#record_destroy` + # + # The value inserted into the `event` column of the versions table can also + # be overridden by the user, with `paper_trail_event`. + # + # @api private + class Base + RAILS_GTE_5_1 = ::ActiveRecord.gem_version >= ::Gem::Version.new("5.1.0.beta1") + + # @api private + def initialize(record, in_after_callback) + @record = record + @in_after_callback = in_after_callback + end + + # 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. + # + # @api private + def changed_notably? + if ignored_attr_has_changed? + timestamps = @record.send(:timestamp_attributes_for_update_in_model).map(&:to_s) + (notably_changed - timestamps).any? + else + notably_changed.any? + end + end + + private + + # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See + # https://github.com/paper-trail-gem/paper_trail/pull/899 + # + # @api private + def attribute_changed_in_latest_version?(attr_name) + if @in_after_callback && RAILS_GTE_5_1 + @record.saved_change_to_attribute?(attr_name.to_s) + else + @record.attribute_changed?(attr_name.to_s) + end + end + + # @api private + def attributes_before_change(is_touch) + Hash[@record.attributes.map do |k, v| + if @record.class.column_names.include?(k) + [k, attribute_in_previous_version(k, is_touch)] + else + [k, v] + end + end] + end + + # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See + # https://github.com/paper-trail-gem/paper_trail/pull/899 + # + # Event can be any of the three (create, update, destroy). + # + # @api private + def attribute_in_previous_version(attr_name, is_touch) + if RAILS_GTE_5_1 + if @in_after_callback && !is_touch + # For most events, we want the original value of the attribute, before + # the last save. + @record.attribute_before_last_save(attr_name.to_s) + else + # We are either performing a `record_destroy` or a + # `record_update(is_touch: true)`. + @record.attribute_in_database(attr_name.to_s) + end + else + @record.attribute_was(attr_name.to_s) + end + end + + # @api private + def changed_and_not_ignored + ignore = @record.paper_trail_options[: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.paper_trail_options[:skip] + changed_in_latest_version - ignore - skip + end + + # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See + # https://github.com/paper-trail-gem/paper_trail/pull/899 + # + # @api private + def changed_in_latest_version + if @in_after_callback && RAILS_GTE_5_1 + @record.saved_changes.keys + else + @record.changed + end + end + + # @api private + def changes + notable_changes = changes_in_latest_version.delete_if { |k, _v| + !notably_changed.include?(k) + } + AttributeSerializers::ObjectChangesAttribute. + new(@record.class). + serialize(notable_changes) + notable_changes.to_hash + end + + # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See + # https://github.com/paper-trail-gem/paper_trail/pull/899 + # + # @api private + def changes_in_latest_version + if @in_after_callback && RAILS_GTE_5_1 + @record.saved_changes + else + @record.changes + end + end + + # 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. + # + # @api private + def ignored_attr_has_changed? + ignored = @record.paper_trail_options[:ignore] + @record.paper_trail_options[:skip] + ignored.any? && (changed_in_latest_version & ignored).any? + end + + # Updates `data` from the model's `meta` option and from `controller_info`. + # Metadata is always recorded; that means all three events (create, update, + # destroy) and `update_columns`. + # + # @api private + def merge_metadata_into(data) + merge_metadata_from_model_into(data) + merge_metadata_from_controller_into(data) + end + + # Updates `data` from `controller_info`. + # + # @api private + def merge_metadata_from_controller_into(data) + data.merge(PaperTrail.request.controller_info || {}) + end + + # Updates `data` from the model's `meta` option. + # + # @api private + def merge_metadata_from_model_into(data) + @record.paper_trail_options[:meta].each do |k, v| + data[k] = model_metadatum(v, data[:event]) + end + end + + # Given a `value` from the model's `meta` option, returns an object to be + # persisted. The `value` can be a simple scalar value, but it can also + # be a symbol that names a model method, or even a Proc. + # + # @api private + def model_metadatum(value, event) + if value.respond_to?(:call) + value.call(@record) + elsif value.is_a?(Symbol) && @record.respond_to?(value, true) + # If it is an attribute that is changing in an existing object, + # be sure to grab the current version. + if event != "create" && + @record.has_attribute?(value) && + attribute_changed_in_latest_version?(value) + attribute_in_previous_version(value, false) + else + @record.send(value) + end + else + value + end + end + + # @api private + def notably_changed + only = @record.paper_trail_options[: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 + + # Returns hash of attributes (with appropriate attributes serialized), + # omitting attributes to be skipped. + # + # @api private + def object_attrs_for_paper_trail(is_touch) + attrs = attributes_before_change(is_touch). + except(*@record.paper_trail_options[:skip]) + AttributeSerializers::ObjectAttribute.new(@record.class).serialize(attrs) + attrs + end + + # 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`. + # + # @api private + def recordable_object_changes(changes) + if PaperTrail.config.object_changes_adapter + changes = PaperTrail.config.object_changes_adapter.diff(changes) + end + + if @record.class.paper_trail.version_class.object_changes_col_is_json? + changes + else + PaperTrail.serializer.dump(changes) + end + end + + # Returns a boolean indicating whether to store serialized version diffs + # in the `object_changes` column of the version record. + # + # @api private + def record_object_changes? + @record.paper_trail_options[:save_changes] && + @record.class.paper_trail.version_class.column_names.include?("object_changes") + end + + # 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`. + # + # @api private + def recordable_object(is_touch) + if @record.class.paper_trail.version_class.object_col_is_json? + object_attrs_for_paper_trail(is_touch) + else + PaperTrail.serializer.dump(object_attrs_for_paper_trail(is_touch)) + end + end + end + end +end diff --git a/lib/paper_trail/events/create.rb b/lib/paper_trail/events/create.rb new file mode 100644 index 000000000..e9a637a26 --- /dev/null +++ b/lib/paper_trail/events/create.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require "paper_trail/events/base" + +module PaperTrail + module Events + # See docs in `Base`. + # + # @api private + class Create < Base + # Return attributes of nascent `Version` record. + # + # @api private + def data + data = { + event: @record.paper_trail_event || "create", + whodunnit: PaperTrail.request.whodunnit + } + if @record.respond_to?(:updated_at) + data[:created_at] = @record.updated_at + end + if record_object_changes? && changed_notably? + data[:object_changes] = recordable_object_changes(changes) + end + merge_metadata_into(data) + end + end + end +end diff --git a/lib/paper_trail/events/destroy.rb b/lib/paper_trail/events/destroy.rb new file mode 100644 index 000000000..4becbae5c --- /dev/null +++ b/lib/paper_trail/events/destroy.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require "paper_trail/events/base" + +module PaperTrail + module Events + # See docs in `Base`. + # + # @api private + class Destroy < Base + # Return attributes of nascent `Version` record. + # + # @api private + def data + data = { + item_id: @record.id, + item_type: @record.class.base_class.name, + event: @record.paper_trail_event || "destroy", + object: recordable_object(false), + whodunnit: PaperTrail.request.whodunnit + } + merge_metadata_into(data) + end + end + end +end diff --git a/lib/paper_trail/events/update.rb b/lib/paper_trail/events/update.rb new file mode 100644 index 000000000..4a3a99c26 --- /dev/null +++ b/lib/paper_trail/events/update.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require "paper_trail/events/base" + +module PaperTrail + module Events + # See docs in `Base`. + # + # @api private + class Update < Base + # - is_touch - [boolean] - Used in the two situations that are touch-like: + # - `after_touch` we call `RecordTrail#record_update` + # - `RecordTrail#touch_with_version` (deprecated) calls `RecordTrail#record_update` + # - force_changes - [Hash] - Only used by `RecordTrail#update_columns`, + # because there dirty tracking is off, so it has to track its own changes. + # + # @api private + def initialize(record, in_after_callback, is_touch, force_changes) + super(record, in_after_callback) + @is_touch = is_touch + @changes = force_changes.nil? ? changes : force_changes + end + + # Return attributes of nascent `Version` record. + # + # @api private + def data + data = { + event: @record.paper_trail_event || "update", + object: recordable_object(@is_touch), + whodunnit: PaperTrail.request.whodunnit + } + if @record.respond_to?(:updated_at) + data[:created_at] = @record.updated_at + end + if record_object_changes? + data[:object_changes] = recordable_object_changes(@changes) + end + merge_metadata_into(data) + end + end + end +end diff --git a/lib/paper_trail/record_trail.rb b/lib/paper_trail/record_trail.rb index 316ee15e8..44cd5d5b3 100644 --- a/lib/paper_trail/record_trail.rb +++ b/lib/paper_trail/record_trail.rb @@ -1,5 +1,9 @@ # frozen_string_literal: true +require "paper_trail/events/create" +require "paper_trail/events/destroy" +require "paper_trail/events/update" + module PaperTrail # Represents the "paper trail" for a single record. class RecordTrail @@ -42,31 +46,6 @@ class RecordTrail def initialize(record) @record = record - @in_after_callback = false - end - - def attributes_before_change(is_touch) - Hash[@record.attributes.map do |k, v| - if @record.class.column_names.include?(k) - [k, attribute_in_previous_version(k, is_touch)] - else - [k, v] - end - end] - end - - def changed_and_not_ignored - ignore = @record.paper_trail_options[: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.paper_trail_options[:skip] - changed_in_latest_version - ignore - skip end # Invoked after rollbacks to ensure versions records are not created for @@ -83,29 +62,6 @@ def clear_version_instance @record.send("#{@record.class.version_association_name}=", nil) end - # 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. - def changed_notably? - if ignored_attr_has_changed? - timestamps = @record.send(:timestamp_attributes_for_update_in_model).map(&:to_s) - (notably_changed - timestamps).any? - else - notably_changed.any? - end - end - - # @api private - def changes - notable_changes = changes_in_latest_version.delete_if { |k, _v| - !notably_changed.include?(k) - } - AttributeSerializers::ObjectChangesAttribute. - new(@record.class). - serialize(notable_changes) - notable_changes.to_hash - end - # Is PT enabled for this particular record? # @api private def enabled? @@ -126,65 +82,12 @@ def enabled_for_model? PaperTrail.request.enabled_for_model?(@record.class) end - # 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. - def ignored_attr_has_changed? - ignored = @record.paper_trail_options[:ignore] + @record.paper_trail_options[:skip] - ignored.any? && (changed_in_latest_version & ignored).any? - end - # Returns true if this instance is the current, live one; # returns false if this instance came from a previous version. def live? source_version.nil? end - # Updates `data` from the model's `meta` option and from `controller_info`. - # Metadata is always recorded; that means all three events (create, update, - # destroy) and `update_columns`. - # @api private - def merge_metadata_into(data) - merge_metadata_from_model_into(data) - merge_metadata_from_controller_into(data) - end - - # Updates `data` from `controller_info`. - # @api private - def merge_metadata_from_controller_into(data) - data.merge(PaperTrail.request.controller_info || {}) - end - - # Updates `data` from the model's `meta` option. - # @api private - def merge_metadata_from_model_into(data) - @record.paper_trail_options[:meta].each do |k, v| - data[k] = model_metadatum(v, data[:event]) - end - end - - # Given a `value` from the model's `meta` option, returns an object to be - # persisted. The `value` can be a simple scalar value, but it can also - # be a symbol that names a model method, or even a Proc. - # @api private - def model_metadatum(value, event) - if value.respond_to?(:call) - value.call(@record) - elsif value.is_a?(Symbol) && @record.respond_to?(value, true) - # If it is an attribute that is changing in an existing object, - # be sure to grab the current version. - if event != "create" && - @record.has_attribute?(value) && - attribute_changed_in_latest_version?(value) - attribute_in_previous_version(value, false) - else - @record.send(value) - end - else - value - end - end - # 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? @@ -195,30 +98,6 @@ def next_version nil end - def notably_changed - only = @record.paper_trail_options[: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 - - # Returns hash of attributes (with appropriate attributes serialized), - # omitting attributes to be skipped. - # - # @api private - def object_attrs_for_paper_trail(is_touch) - attrs = attributes_before_change(is_touch). - except(*@record.paper_trail_options[:skip]) - AttributeSerializers::ObjectAttribute.new(@record.class).serialize(attrs) - attrs - end - # Returns who put `@record` into its current state. # # @api public @@ -234,28 +113,22 @@ def previous_version end def record_create - @in_after_callback = true return unless enabled? + event = Events::Create.new(@record, true) + + # Merge data from `Event` with data from PT-AT. We no longer use + # `data_for_create` but PT-AT still does. + data = event.data.merge(data_for_create) + versions_assoc = @record.send(@record.class.versions_association_name) - versions_assoc.create! data_for_create - ensure - @in_after_callback = false + versions_assoc.create!(data) end - # Returns data for record create + # PT-AT extends this method to add its transaction id. + # # @api private def data_for_create - data = { - event: @record.paper_trail_event || "create", - whodunnit: PaperTrail.request.whodunnit - } - if @record.respond_to?(:updated_at) - data[:created_at] = @record.updated_at - end - if record_object_changes? && changed_notably? - data[:object_changes] = recordable_object_changes(changes) - end - merge_metadata_into(data) + {} end # `recording_order` is "after" or "before". See ModelConfig#on_destroy. @@ -264,77 +137,56 @@ def data_for_create # @return - The created version object, so that plugins can use it, e.g. # paper_trail-association_tracking def record_destroy(recording_order) - @in_after_callback = recording_order == "after" - if enabled? && !@record.new_record? - version = @record.class.paper_trail.version_class.create(data_for_destroy) - 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 - version - end + return unless enabled? && !@record.new_record? + in_after_callback = recording_order == "after" + event = Events::Destroy.new(@record, in_after_callback) + + # Merge data from `Event` with data from PT-AT. We no longer use + # `data_for_destroy` but PT-AT still does. + data = event.data.merge(data_for_destroy) + + version = @record.class.paper_trail.version_class.create(data) + if version.errors.any? + log_version_errors(version, :destroy) + else + assign_and_reset_version_association(version) + version end - ensure - @in_after_callback = false end - # Returns data for record destroy + # PT-AT extends this method to add its transaction id. + # # @api private def data_for_destroy - data = { - item_id: @record.id, - item_type: @record.class.base_class.name, - event: @record.paper_trail_event || "destroy", - object: recordable_object(false), - whodunnit: PaperTrail.request.whodunnit - } - merge_metadata_into(data) - end - - # Returns a boolean indicating whether to store serialized version diffs - # in the `object_changes` column of the version record. - # @api private - def record_object_changes? - @record.paper_trail_options[:save_changes] && - @record.class.paper_trail.version_class.column_names.include?("object_changes") + {} end # @api private # @return - The created version object, so that plugins can use it, e.g. # paper_trail-association_tracking def record_update(force:, in_after_callback:, is_touch:) - @in_after_callback = in_after_callback - if enabled? && (force || changed_notably?) - versions_assoc = @record.send(@record.class.versions_association_name) - version = versions_assoc.create(data_for_update(is_touch)) - if version.errors.any? - log_version_errors(version, :update) - else - version - end + return unless enabled? + event = Events::Update.new(@record, in_after_callback, is_touch, nil) + return unless force || event.changed_notably? + + # Merge data from `Event` with data from PT-AT. We no longer use + # `data_for_update` but PT-AT still does. + data = event.data.merge(data_for_update) + + versions_assoc = @record.send(@record.class.versions_association_name) + version = versions_assoc.create(data) + if version.errors.any? + log_version_errors(version, :update) + else + version end - ensure - @in_after_callback = false end - # Used during `record_update`, returns a hash of data suitable for an AR - # `create`. That is, all the attributes of the nascent `Version` record. + # PT-AT extends this method to add its transaction id. # # @api private - def data_for_update(is_touch) - data = { - event: @record.paper_trail_event || "update", - object: recordable_object(is_touch), - whodunnit: PaperTrail.request.whodunnit - } - if @record.respond_to?(:updated_at) - data[:created_at] = @record.updated_at - end - if record_object_changes? - data[:object_changes] = recordable_object_changes(changes) - end - merge_metadata_into(data) + def data_for_update + {} end # @api private @@ -342,8 +194,14 @@ def data_for_update(is_touch) # paper_trail-association_tracking def record_update_columns(changes) return unless enabled? + event = Events::Update.new(@record, false, false, changes) + + # Merge data from `Event` with data from PT-AT. We no longer use + # `data_for_update_columns` but PT-AT still does. + data = event.data.merge(data_for_update_columns) + versions_assoc = @record.send(@record.class.versions_association_name) - version = versions_assoc.create(data_for_update_columns(changes)) + version = versions_assoc.create(data) if version.errors.any? log_version_errors(version, :update) else @@ -351,52 +209,11 @@ def record_update_columns(changes) end end - # Returns data for record_update_columns - # @api private - def data_for_update_columns(changes) - data = { - event: @record.paper_trail_event || "update", - object: recordable_object(false), - whodunnit: PaperTrail.request.whodunnit - } - if record_object_changes? - data[:object_changes] = recordable_object_changes(changes) - end - merge_metadata_into(data) - end - - # 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`. + # PT-AT extends this method to add its transaction id. # # @api private - def recordable_object(is_touch) - if @record.class.paper_trail.version_class.object_col_is_json? - object_attrs_for_paper_trail(is_touch) - else - PaperTrail.serializer.dump(object_attrs_for_paper_trail(is_touch)) - end - end - - # 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`. - # - # @api private - def recordable_object_changes(changes) - if PaperTrail.config.object_changes_adapter - changes = PaperTrail.config.object_changes_adapter.diff(changes) - end - - if @record.class.paper_trail.version_class.object_changes_col_is_json? - changes - else - PaperTrail.serializer.dump(changes) - end + def data_for_update_columns + {} end # Invoked via callback when a user attempts to persist a reified @@ -539,62 +356,10 @@ def whodunnit(value) private - # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See - # https://github.com/paper-trail-gem/paper_trail/pull/899 - # - # @api private - def attribute_changed_in_latest_version?(attr_name) - if @in_after_callback && RAILS_GTE_5_1 - @record.saved_change_to_attribute?(attr_name.to_s) - else - @record.attribute_changed?(attr_name.to_s) - end - end - - # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See - # https://github.com/paper-trail-gem/paper_trail/pull/899 - # - # Event can be any of the three (create, update, destroy). - # - # @api private - def attribute_in_previous_version(attr_name, is_touch) - if RAILS_GTE_5_1 - if @in_after_callback && !is_touch - # For most events, we want the original value of the attribute, before - # the last save. - @record.attribute_before_last_save(attr_name.to_s) - else - # We are either performing a `record_destroy` or a - # `record_update(is_touch: true)`. - @record.attribute_in_database(attr_name.to_s) - end - else - @record.attribute_was(attr_name.to_s) - end - end - - # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See - # https://github.com/paper-trail-gem/paper_trail/pull/899 - # - # @api private - def changed_in_latest_version - if @in_after_callback && RAILS_GTE_5_1 - @record.saved_changes.keys - else - @record.changed - end - end - - # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See - # https://github.com/paper-trail-gem/paper_trail/pull/899 - # # @api private - def changes_in_latest_version - if @in_after_callback && RAILS_GTE_5_1 - @record.saved_changes - else - @record.changes - end + def assign_and_reset_version_association(version) + @record.send("#{@record.class.version_association_name}=", version) + @record.send(@record.class.versions_association_name).reset end def log_version_errors(version, action) diff --git a/spec/models/gadget_spec.rb b/spec/models/gadget_spec.rb index 7f3c2842f..31ff8082a 100644 --- a/spec/models/gadget_spec.rb +++ b/spec/models/gadget_spec.rb @@ -23,43 +23,4 @@ }.to(change { gadget.versions.size }.by(1)) end end - - describe "#changed_notably?", versioning: true do - context "new record" do - it "returns true" do - g = Gadget.new(created_at: Time.now) - expect(g.paper_trail.changed_notably?).to eq(true) - end - end - - context "persisted record without update timestamps" do - it "only acknowledges non-ignored attrs" do - gadget = Gadget.create!(created_at: Time.now) - gadget.name = "Wrench" - expect(gadget.paper_trail.changed_notably?).to be true - end - - it "does not acknowledge ignored attr (brand)" do - gadget = Gadget.create!(created_at: Time.now) - gadget.brand = "Acme" - expect(gadget.paper_trail.changed_notably?).to be false - end - end - - context "persisted record with update timestamps" do - it "only acknowledges non-ignored attrs" do - gadget = Gadget.create!(created_at: Time.now) - gadget.name = "Wrench" - gadget.updated_at = Time.now - expect(gadget.paper_trail.changed_notably?).to be true - end - - it "does not acknowledge ignored attrs and timestamps only" do - gadget = Gadget.create!(created_at: Time.now) - gadget.brand = "Acme" - gadget.updated_at = Time.now - expect(gadget.paper_trail.changed_notably?).to be false - end - end - end end diff --git a/spec/paper_trail/events/base_spec.rb b/spec/paper_trail/events/base_spec.rb new file mode 100644 index 000000000..431fde016 --- /dev/null +++ b/spec/paper_trail/events/base_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require "spec_helper" + +module PaperTrail + module Events + ::RSpec.describe Base do + describe "#changed_notably?", versioning: true do + context "new record" do + it "returns true" do + g = Gadget.new(created_at: Time.now) + event = PaperTrail::Events::Base.new(g, false) + expect(event.changed_notably?).to eq(true) + end + end + + context "persisted record without update timestamps" do + it "only acknowledges non-ignored attrs" do + gadget = Gadget.create!(created_at: Time.now) + gadget.name = "Wrench" + event = PaperTrail::Events::Base.new(gadget, false) + expect(event.changed_notably?).to eq(true) + end + + it "does not acknowledge ignored attr (brand)" do + gadget = Gadget.create!(created_at: Time.now) + gadget.brand = "Acme" + event = PaperTrail::Events::Base.new(gadget, false) + expect(event.changed_notably?).to eq(false) + end + end + + context "persisted record with update timestamps" do + it "only acknowledges non-ignored attrs" do + gadget = Gadget.create!(created_at: Time.now) + gadget.name = "Wrench" + gadget.updated_at = Time.now + event = PaperTrail::Events::Base.new(gadget, false) + expect(event.changed_notably?).to eq(true) + end + + it "does not acknowledge ignored attrs and timestamps only" do + gadget = Gadget.create!(created_at: Time.now) + gadget.brand = "Acme" + gadget.updated_at = Time.now + event = PaperTrail::Events::Base.new(gadget, false) + expect(event.changed_notably?).to eq(false) + end + end + end + end + end +end diff --git a/spec/paper_trail/serializer_spec.rb b/spec/paper_trail/serializer_spec.rb index 5405421c7..814d41d67 100644 --- a/spec/paper_trail/serializer_spec.rb +++ b/spec/paper_trail/serializer_spec.rb @@ -7,7 +7,9 @@ context "YAML serializer" do it "saves the expected YAML in the object column" do customer = Customer.create(name: "Some text.") - original_attributes = customer.paper_trail.attributes_before_change(false) + original_attributes = PaperTrail::Events::Base. + new(customer, false). + send(:attributes_before_change, false) customer.update(name: "Some more text.") expect(customer.versions.length).to(eq(2)) expect(customer.versions[0].reify).to(be_nil) @@ -30,7 +32,9 @@ it "reify with JSON serializer" do customer = Customer.create(name: "Some text.") - original_attributes = customer.paper_trail.attributes_before_change(false) + original_attributes = PaperTrail::Events::Base. + new(customer, false). + send(:attributes_before_change, false) customer.update(name: "Some more text.") expect(customer.versions.length).to(eq(2)) expect(customer.versions[0].reify).to(be_nil) @@ -62,9 +66,9 @@ it "reify with custom serializer" do customer = Customer.create - original_attributes = customer. - paper_trail. - attributes_before_change(false). + original_attributes = PaperTrail::Events::Base. + new(customer, false). + send(:attributes_before_change, false). reject { |_k, v| v.nil? } customer.update(name: "Some more text.") expect(customer.versions.length).to(eq(2))