Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add paper_trail.update_columns #1037

Merged
merged 5 commits into from
Jan 23, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
58 changes: 54 additions & 4 deletions lib/paper_trail/record_trail.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 = {
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down
9 changes: 9 additions & 0 deletions spec/models/on/empty_array_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just FYI, there is a nice way to "expect a change" in RSpec. You don't have to use it, what you have is fine, just thought I'd share.

expect {
  widget.paper_trail.update_columns(name: "Bugle")
}.to(change { widget.versions.length }).from(1).to(2)

end
end

describe "#update_attributes" do
it "does not create any version records" do
record = described_class.create(name: "Alice")
Expand Down
14 changes: 14 additions & 0 deletions spec/models/widget_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down