Edit a paper_trail version without creating a new version - ruby-on-rails

I'm using paper_trail 3.0.8 on a Rails 3.2 app and I've got a model called 'levels' and I keep versions of these levels. Each level has a from_date and a cost relating to it. Whenever someone changes the date a new version is created.
I allow people to remove old versions if they want and this works well. I would like the ability to modify an old paper_trail version and save it without creating a new version.
class Level < ActiveRecord::Base
has_paper_trail :only => [:from_date],
:if => Proc.new { |l|
l.versions.count == 0 || l.versions.first.item != nil && (l.versions.first.item.from_date.nil? || l.from_date > l.versions.first.item.from_date)
}
<snip code>
end
If I do the following it only updates the current level and not the version
level = Level.find 1
version=level.versions[1].reify
version.cost_cents = 1000
version.save
Is there anyway to update the cost_cents for an old version?
Also is there a way to update the from_date of an old version without creating a new version on the save?

Is there anyway to update the cost_cents for an old version?
Yes, but the only way I know is a bit awkward.
PaperTrail::Version is a normal ActiveRecord object, so it's easy to work with in that sense, but the data is serialized (in YAML, by default) so you'll have to de-serialize, make your change, and re-serialize.
v = PaperTrail::Version.last
hash = YAML.load(v.object)
hash[:my_attribute] = "my new value"
v.object = YAML.dump(hash)
v.save
There may be a better way to do this with ActiveRecord's automatic-serialization features (like ActiveRecord::AttributeMethods::Serialization).
PS: I see you were trying to use reify, which returns an instance of your model, not an instance of PaperTrail::Version.

Related

How can I customise AttributeSerializerFactory in papertrail?

I am having an issue creating versions for a model that contains datetime columns.
I do not know why. I've already asked a question. See Error on reify a version using ruby PaperTrail
Because I do not find a solution and I've verified that everything else works if I just skip these columns, I would like to solve the issue implementing my own serializer/deserializer.
I can see that papertrails relies on a class called CastAttributeSerializer and that this class uses a factory as shown in the following snippet:
def deserialize(attr, val)
if defined_enums[attr] && val.is_a?(::String)
# Because PT 4 used to save the string version of enums to `object_changes`
val
elsif PaperTrail::RAILS_GTE_7_0 && val.is_a?(ActiveRecord::Type::Time::Value)
# Because Rails 7 time attribute throws a delegation error when you deserialize
# it with the factory.
# See ActiveRecord::Type::Time::Value crashes when loaded from YAML on rails 7.0
# https://github.com/rails/rails/issues/43966
val.instance_variable_get(:#time)
else
AttributeSerializerFactory.for(#klass, attr).deserialize(val)
end
def serialize(attr, val)
AttributeSerializerFactory.for(#klass, attr).serialize(val)
end
If I understand correctly, I should somehow modify the factory AttributeSerializerFactory to introduce my own serializer/deserializer for the class Time (that is failing).
What's the correct way to do it?

Modify or Create paper_trail version with alternate attribute value

I have a model with a start_date and end_date.
When a version is created, if the old end_date is after the new start_date then the end_date in the paper_trail version should be set to start_date of the updated record
In sudo code this might look like...
(doesn't work and I'd prefer to not modify the version after creating it)
record.save
version = paper_trail.previous_version
if (version.end_date > record.start_date)
version.end_date = record.start_date
version.save
end
The sequence would look like this...
Create record
record: start_date=2021, end_date=nil
Update record
record: start_date=2022, end_date=2023
version1: start_date=2021, end_date=2022
Update record
record: start_date=2024, end_date=2037
version2: start_date=2022, end_date=2024
version1: start_date=2021, end_date=2022
rails 6.1, paper_trail 12
I would suggest keeping the Papertrail behavior as-is, meaning it would keep track of value changes for each field without any additional custom logic that would overwrite the values, thus breaking interface for rolling back to a version etc.
Instead, I would use meta to store and retrieve any custom data you need for the model https://github.com/paper-trail-gem/paper_trail#4c-storing-metadata

PaperTrail Manually create version

I have a spreadsheet of items which I convert to CSV and import using a custom import script into my Rails based application.
The spreadsheet contains a row for each record but some rows hold different versions of previous rows.
When importing the CSV I currently mark the second row using a "past_version" field but I am now thinking that implementing a full versioning gem would be a much nicer way of going about it.
I have been reading through the docs for PaperTrail and it looks perfect for what I am after, however, I need the versions of some rows to be created as part of my import script. Can this be done with PaperTrail?
Basically I need to start an import, say record 1, 2, 3, 4, 5 are added normally, then record 6 is actually a newer version of record 2 and so I now need to manually create a PaperTrail version.
Is this possible?
#Flori's touch_with_version approach worked, but paper_trail deprecated this method when they made touch a recordable event.
On the latest version of paper_trail, you can just do:
model.paper_trail.save_with_version
If you are on an older version, and this is not available, you can use the record_update method (this is what save_with_version uses internally):
model.paper_trail.record_update(force: true, in_after_callback: false)
There is no way to customize the event... you can do create, restore, update and touch but can't do something like record it as 'manual' at least not without a lot of monkey patching or something.
Even doing this is a little treacherous, since you're calling some internals that could change in future versions. It has some required params, also, that have changed from version to version.
In my case, I am using paper_trail and paranoia together and wanted to record an explicit version on restore which seems to bypass paper_trail unless you do something like the above.
In case anyone stumble upon this issue: It's possible! You can call touch_with_version on any PaperTrail model like this: mymodel.paper_trail.touch_with_version. It will create a version for you.
For PaperTrail 4.0.0 and Rails 4.2.0
I had to manually create my own version so that I could use the update_column method (which would otherwise not trigger PaperTrail.
#instance method in model to generate new version and create object_changes
def generate_version!(object_attrs, changed_attrs)
object_changes_attrs = {}
changed_attrs.each do |k, v|
object_changes_attrs[k] = v
end
object_value = self.class.paper_trail_version_class.object_col_is_json? ? object_attrs : PaperTrail.serializer.dump(object_attrs)
object_changes_value = self.class.paper_trail_version_class.object_col_is_json? ? object_changes_attrs : PaperTrail.serializer.dump(object_changes_attrs)
data = {
event: 'update', # or any custom name you want
whodunnit: PaperTrail.whodunnit,
object: object_value,
object_changes: object_changes_value
}
send(self.class.versions_association_name).create! merge_metadata(data)
end
Then in your model you can call it wherever you want passing in: (1) a hash of the current object attributes (before the update); and (2) a hash of the attributes and changes made
#some method where you do not otherwise trigger PaperTrail
def my_method
old_updated_at = self.updated_at
new_updated_at = DateTime.now.utc
object_attrs = self.attributes
self.update_columns(prioritized: true, updated_at: new_updated_at)
self.generate_version!(object_attrs, { 'prioritized': [false, true], 'updated_at': [old_updated_at, new_updated_at] })
end

Can I use papertrail to store "pending-approval" changes?

I need to allow less-privileged users to propose changes to a record but have them sit somewhere until an administrator approves them. It should be similar to the way Stack Overflow allows users with lower reputation to propose an edit to a question or answer which must be reviewed by someone with higher reputation.
In papertrail terms, I'd like to allow users to create versions of a record without actually committing those changes to the record itself—future versions, rather than past versions. Then I'd like to allow another user to "revert" ("prevert"?) to the new version.
Is this something papertrail supports? Or is there another gem that can do this?
I know that this question is very old but let me explain how I managed to solve it:
Suppose that I have a model Post, two users: A and B, that A is authorized to update posts but he needs B approval before committing the changes, B is the monitor who can approve updates and can update posts as well.
I added a method to revert the record to a particular version of it so we can update it with whatever version we want:
def revert_to(version)
raise 'not version of this model' unless self == version.item
changes = version.changeset.select{ |k, v| not SKIP_FIELDS.include?(k) }.map{ |k,v| [k.to_sym, v[1]] }.to_h
self.update_attributes(changes)
end
I got that method from this Stackoverflow answer but I modified it a bit.
The trick is to not update the record itself if the current user A hasn't authorized to commit changing, rather than updating, a new Paper Trail version will be created, then the monitor B can accept the new changes by reverting the original record to that version.
For that I used a function from Paper Trail core paper_trail.record_update().
A:
p = Post.find(1)
p.title = "A new pending version"
p.paper_trail.record_update(nil)
B as a monitor:
p = Publication.find(1)
p.revert_to(p.versions.last)
B as an editor:
p = Publication.find(1)
p.title = "p will be updated to this version immediately"
p.paper_trail.record_update(nil)
p.save
I added has_paper_trail to Post model but I restricted it on create and destroy actions because as I said above I don't want a new version to be created after updating, I want it to be created before.
has_paper_trail :on => [:create, :destroy]
I'm facing the same problem right now.
No, it's not supported by paper_trail, but maybe we can achieve it adding an approved attribute to our Record. It should default to false so that, when the Record object is saved, a new paper_trail version is created with that attribute set to false.
Later an AdminUser can approve the record setting approved to true and paper_trail will create the new approved version.
It's not the cleanest solution but it should work. And we could also add other attributes to your Record model such as approved_by and approved_at, should you we them.
Mmm.. I'm thinking about it..
Please let me know if you found a better solution!

rails build or update method?

I'm currently using the find or create method to update an associated record which i use to store cached information, but I'm wondering if there's some simpler alternative method similar to build since the object is a has_one relation. The problem with just using build_ is in most cases the object will exist and needs to be updated. I could use a bunch of ifs the check but wondered if there was some better rails-fu than I'm using currently.
def self.update_caches(data)
cache = SpaceCache.find_or_create_by_space_id(#space.id)
cache.rating = ((data.ratings.sum / data.ratings.size)/20).round
cache.price_min = #space.availables.recent.average(:minimum)
cache.price_avg = #space.availables.recent.average(:price)
cache.save
end
Bonus:
I also read here:
http://m.onkey.org/active-record-query-interface
That the rails calculation methods average, sum, etc will be depreciated in 3.1, so should I'm unsure if I should be replacing them?
count(column, options)
average(column, options)
minimum(column, options)
maximum(column, options)
sum(column, options)
calculate(operation, column, options)
I wrote one for my has_one :event.
def build_or_update_event(params)
result = new_record? ? build_event(params) : event.update_attributes(params)
result != false
end

Resources