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
Related
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
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.
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!
Basically I have a blog type application that can have posts created from an rss source feed, for which I'm using the feedzirra gem:
def self.update_from_feed
feed_url = ENV['FEED_URL']
feed = Feedzirra::Feed.fetch_and_parse(feed_url)
add_entries(feed.entries)
end
Thus, one of the methods in my post model handles creating a new post from the parsed feed, and looks something like this:
def self.add_entries(entries)
entries.each do |entry|
unless exists? :guid => entry.id
create!(guid: entry.id, name: entry.title, author: entry.author, summary: entry.summary, published_at: entry.published)
end
end
Feedzirra also has an update method available (I've attempted to implement but haven't been able to get it working) in which it can check a feed for changes, and if changes are present it will download the changes in the feed and create new posts as necessary. As you can see in my above method, using unless exists? if a post has already been created (determined using the guid) compared to the source it skips it and does not make any changes.
However, in a blog type application, ideally it would instead of checking to see if exists, check to see if it changed (namely the :summary portion is what I'm after), maybe using the last_modified accessor and then update the already created record. Is there something similar to exists? that I could use to let an entry be updated like this/ am I on the right track or do I need to dig much deeper?
You can try changed?.
Idea is following (for existed records): set attributes without saving and check if entry.changed?
So I've done a couple of days worth of research on the matter, and the general consensus is that there isn't one. So I was hoping for an answer more specific to my situation...
I'm using Rails to import a file into a database. Everything is working regarding the import, but I'm wanting to give the database itself an attribute, not just every entry. I'm creating a hash of the file, and I figured it'd be easiest to just assign it to the database (or the class).
I've created a class called Issue (and thus an 'issues' database) with each entry having a couple of attributes. I was wanting to figure out a way to add a class variable (at least, that's what I think is the best option) to Issue to simply store the hash. I've written a rake to import the file, iff the new file is different than the previous file imported (read, if the hash's are different).
desc "Parses a CSV file into the 'issues' database"
task :issues, [:file] => :environment do |t, args|
md5 = Digest::MD5.hexdigest(args[:file])
puts "1: Issue.md5 = #{Issue.md5}"
if md5 != Issue.md5
Issue.destroy_all()
#import new csv file
CSV.foreach(args[:file]) do |row|
issue = {
#various attributes to be columns...
}
Issue.create(issue)
end #end foreach loop
Issue.md5 = md5
puts "2: Issue.md5 = #{Issue.md5}"
end #end if statement
end #end task
And my model is as follows:
class Issue < ActiveRecord::Base
attr_accessible :md5
##md5 = 5
def self.md5
##md5
end
def self.md5=(newmd5)
##md5 = newmd5
end
attr_accessible #various database-entry attributes
end
I've tried various different ways to write my model, but it all comes down to this. Whatever I set the ##md5 in my model, becomes a permanent change, almost like a constant. If I change this value here, and refresh my database, the change is noted immediately. If I go into rails console and do:
Issue.md5 # => 5
Issue.md5 = 123 # => 123
Issue.md5 # => 123
But this change isn't committed to anything. As soon as I exit the console, things return to "5" again. It's almost like I need a .save method for my class.
Also, in the rake file, you see I have two print statements, printing out Issue.md5 before and after the parse. The first prints out "5" and the second prints out the new, correct hash. So Ruby is recognizing the fact that I'm changing this variable, it's just never saved anywhere.
Ruby 1.9.3, Rails 3.2.6, SQLite3 3.6.20.
tl;dr I need a way to create a class variable, and be able to access it, modify it, and re-store it.
Fixes please? Thanks!
There are a couple solutions here. Essentially, you need to persist that one variable: Postgres provides a key/value store in the database, which would be most ideal, but you're using SQLite so that isn't an option for you. Instead, you'll probably need to use either redis or memcached to persist this information into your database.
Either one allows you to persist values into a schema-less datastore and query them again later. Redis has the advantage of being saved to disk, so if the server craps out on you you can get the value of md5 again when it restarts. Data saved into memcached is never persisted, so if the memcached instance goes away, when it comes back md5 will be 5 once again.
Both redis and memcached enjoy a lot of support in the Ruby community. It will complicate your stack slightly installing one, but I think it's the best solution available to you. That said, if you just can't use either one, you could also write the value of md5 to a temporary file on your server and access it again later. The issue there is that the value then won't be shared among all your server processes.