diff --git a/CHANGELOG.md b/CHANGELOG.md index a159aae8f..7a98849b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ recommendations of [keepachangelog.com](http://keepachangelog.com/). ### Added +- [#1037](https://github.com/airblade/paper_trail/pull/1037) Add `paper_trail.update_columns` - [#961](https://github.com/airblade/paper_trail/issues/961) - Instead of crashing when misconfigured Custom Version Classes are used, an error will be raised earlier, with a much more helpful message. diff --git a/lib/paper_trail/record_trail.rb b/lib/paper_trail/record_trail.rb index 581c72186..e579627da 100644 --- a/lib/paper_trail/record_trail.rb +++ b/lib/paper_trail/record_trail.rb @@ -226,7 +226,7 @@ def data_for_create data[:created_at] = @record.updated_at end if record_object_changes? && changed_notably? - data[:object_changes] = recordable_object_changes + data[:object_changes] = recordable_object_changes(changes) end add_transaction_id_to(data) merge_metadata_into(data) @@ -284,7 +284,7 @@ def record_update(force) @in_after_callback = false end - # Returns data for record update + # Returns data for record_update # @api private def data_for_update data = { @@ -296,7 +296,35 @@ def data_for_update data[:created_at] = @record.updated_at end if record_object_changes? - data[:object_changes] = recordable_object_changes + data[:object_changes] = recordable_object_changes(changes) + end + add_transaction_id_to(data) + merge_metadata_into(data) + end + + # @api private + def record_update_columns(changes) + return unless enabled? + versions_assoc = @record.send(@record.class.versions_association_name) + version = versions_assoc.create(data_for_update_columns(changes)) + if version.errors.any? + log_version_errors(version, :update) + else + update_transaction_id(version) + save_associations(version) + 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, + whodunnit: PaperTrail.whodunnit + } + if record_object_changes? + data[:object_changes] = recordable_object_changes(changes) end add_transaction_id_to(data) merge_metadata_into(data) @@ -322,7 +350,7 @@ def recordable_object # otherwise the column is a `text` column, and we must perform the # serialization here, using `PaperTrail.serializer`. # @api private - def recordable_object_changes + def recordable_object_changes(changes) if @record.class.paper_trail.version_class.object_changes_col_is_json? changes else @@ -406,6 +434,28 @@ def touch_with_version(name = nil) @record.save!(validate: false) end + # Like the `update_column` method from `ActiveRecord::Persistence`, but also + # creates a version to record those changes. + # @api public + def update_column(name, value) + update_columns(name => value) + end + + # Like the `update_columns` method from `ActiveRecord::Persistence`, but also + # creates a version to record those changes. + # @api public + def update_columns(attributes) + # `@record.update_columns` skips dirty tracking, so we can't just use `@record.changes` or + # @record.saved_changes` from `ActiveModel::Dirty`. We need to build our own hash with the + # changes that will be made directly to the database. + changes = {} + attributes.each do |k, v| + changes[k] = [@record[k], v] + end + @record.update_columns(attributes) + record_update_columns(changes) + end + # Returns the object (not a Version) as it was at the given timestamp. def version_at(timestamp, reify_options = {}) # Because a version stores how its object looked *before* the change, diff --git a/spec/models/on/empty_array_spec.rb b/spec/models/on/empty_array_spec.rb index 2b2ea2da9..9d1b30cbb 100644 --- a/spec/models/on/empty_array_spec.rb +++ b/spec/models/on/empty_array_spec.rb @@ -21,6 +21,15 @@ module On end end + describe ".paper_trail.update_columns" do + it "creates a version record" do + widget = Widget.create + assert_equal 1, widget.versions.length + widget.paper_trail.update_columns(name: "Bugle") + assert_equal 2, widget.versions.length + end + end + describe "#update_attributes" do it "does not create any version records" do record = described_class.create(name: "Alice") diff --git a/spec/models/widget_spec.rb b/spec/models/widget_spec.rb index ccb99bbd0..65da52de7 100644 --- a/spec/models/widget_spec.rb +++ b/spec/models/widget_spec.rb @@ -285,6 +285,20 @@ end end + describe ".paper_trail.update_columns", versioning: true do + it "creates a version record" do + widget = Widget.create + expect(widget.versions.count).to eq(1) + Timecop.freeze Time.now do + widget.paper_trail.update_columns(name: "Bugle") + expect(widget.versions.count).to eq(2) + expect(widget.versions.last.event).to(eq("update")) + expect(widget.versions.last.changeset[:name]).to eq([nil, "Bugle"]) + expect(widget.versions.last.created_at.to_i).to eq(Time.now.to_i) + end + end + end + describe "#update", versioning: true do it "creates a version record" do widget = Widget.create