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!
Related
I'm trying to update a parent object (Client) and associated child objects (License). It is possible that either of these objects is new, that's why I have to check for existing objects to update and if there aren't any I have to create them.
My code currently looks like this:
return nil unless params.has_key?('client_uid')
client = Client.find_by_uid(params['client_uid'])
if client.nil?
client = Client.new(uid: params['client_uid'])
end
client.app = params['application']
client.app_version = params['application_version']
...
licenses = params['licenses']
licenses.each do |licenseInfo|
next unless licenseInfo.has_key?('application')
license = client.licenses.find_by_application(licenseInfo['application'])
if license.nil?
license = License.new
# add license without instantly saving
client.association(:licenses).add_to_target(license)
license.application = licenseInfo['application']
end
license.options = licenseInfo['options']
...
end
client.last_seen = Time.zone.now
client.save
This is able to create new clients and update existing ones from the values in the params, as well as adding new licenses for the client (I will implement deleting old licenses later). The problem is that changes to existing licenses aren't saved when I call client.save.
I'm not sure if I'm doing something wrong, or if ActiveRecord really isn't able to handle a situation like this. I already searched for this problem and tried to explicitly specify autosave: true for the License class' belongs_to as recommended here, but this didn't have any effect.
In conclusion, my question is: What do I have to change to be able to save my Client together with all changes (new, changed, deleted) to his licenses?
EDIT:
I tried manually saving all licenses and putting this together in a transaction:
Client.transaction do
client.save
client.licenses.each do |license|
license.save
end
end
But this didn't work, either. Now I'm confused....
I think you need to add
accepts_nested_attributes_for :licenses
in Client model. This would help you to save the child objects
as mentioned in comment. Please refer this link
http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
I try to update 2 model with following steps.
Article
id
current_version
status
ArticleHistory
id
article_id
title
content
version
These models have relationship with article_id and current_version = version.
First, we made one record like this.
article.id:1
article.current_version:1
article.status:public
article_history.id:1
article_history.title:"test title"
article_history.content:"test content"
article_history.version:1
And I'm going to update like this. Before that, I'd like to copy existing ArticleHistory record with new id. I mean, it's like renewing ArticleHistory.
article.id:1
article.current_version:2
article.status:public
(copied)article_history.id:2
(copied)article_history.title:"updated test title"
(copied)article_history.content:"updated test content"
(copied)article_history.version:2
But now, I can't figure out how to express by RoR ActiveRecord.
After this modification, Article has got multiple record.
Give me advise, please.
class Article
has_many :article_histories
should do the trick. If you need more, the doco for has_many is here:
http://apidock.com/rails/ActiveRecord/Associations/ClassMethods/has_many
If this is not suitable - then please tell us why it doesn't work for you :)
To copy...
# first find the article_history with the highest version for this article
latest_history = article.article_histories.order(:version).last
# if there isn't one, create a new one
if latest_history.blank?
new_history = article.article_histories.new(params[:article_history])
new_history.version = 1
else
# Otherwise... merge params and the old attributes
combined_attributes = latest_history.attributes.merge(params[:article_history])
# and use that to create the newer article_history version
new_history = article.article_histories.build(combined_attributes)
new_history.version = latest_history.version + 1
end
new_history.save!
Note: this code is just to give you an idea of how it could be done.
You will need to bugfix it and make it actually work yourself.
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 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
Basically I have this User model which has certain attributes say 'health' and another Battle model which records all the fight between Users. Users can fight with one another and some probability will determine who wins. Both will lose health after a fight.
So in the Battle controller, 'CREATE' action I did,
#battle = Battle.attempt current_user.id, opponent.id
In the Battle model,
def self.attempt current_user.id, opponent_id
battle = Battle.new({:user_id => current_user.id, :opponent_id => opponent_id})
# all the math calculation here
...
# Update Health
...
battle.User.health = new_health
battle.User.save
battle.save
return battle
end
Back to the Battle controller, I did ...
new_user_health = current_user.health
to get the new health value after the Battle. However the value I got is the old health value (the health value before the Battle).
Has anyone face this kind of problem before ???
UPDATE
I just add
current_user.reload
before the line
new_user_health = current_user.health
and that works. Problem solved. Thanks!
It appears that you are getting current_user, then updating battle.user and then expecting current_user to automatically have the updated values. This type of thing is possible using Rails' Identity Map but there are some caveats that you'll want to read up on first.
The problem is that even though the two objects are backed by the same data in the database, you have two objects in memory. To refresh the information, you can call current_user.reload.
As a side note, this wouldn't be classified a race condition because you aren't using more than one process to modify/read the data. In this example, you are reading the data, then updating the data on a different object in memory. A race condition could happen if you were using two threads to access the same information at the same time.
Also, you should use battle.user, not battle.User like Wayne mentioned in the comments.