#255 Undo with Paper Trail
Feb 28, 2011 | 13 minutes |
Plugins
Undo and redo behavior is easy to add with a versioning gem such as Paper Trail. Learn how to add an undo link to any model's flash message.
- Download:
- source codeProject Files in Zip (124 KB)
- mp4Full Size H.264 Video (21.6 MB)
- m4vSmaller H.264 Video (14.4 MB)
- webmFull Size VP8 Video (37 MB)
- ogvFull Size Theora Video (31 MB)
That was great Ryan. I will implement this gradually to all my projects starting today.
Only watching your casts is enough for using Rails :). Thanks to your effort. For me destroy link has to be changed on Rails. Because records must not be destroyable as default.
Does anyone see a problem integrating this gem with mongoid?
Thanks Ryan, excellent screencast on a very useful topic.
Love it! Only one question: how will this work for database records that are deleted through :dependent => :destroy?
So if a post :has_many comments, and I delete a post, wouldn't the .last parament get the last comment of that post and allow me to revert that only?
I guess I'd need to add more logic to the revert method for this then.
Great screencast as always.
Confirmation boxes are still useful, though. Undoing changes on a model with a Paperclip attachment is less trivial.
it seems as if another (malicious) user could revert an action by poking around using different (guessed) :ids, couldn't they?
I've been using both Paper Trail and Vestal Versions for this kind of things.
I've found Paper Trail to be more complete, and with many convenient methods such as "next", as you show. Another point is that it seems to be more actively mantained.
But I prefer Vestal Versions philosophy of storing the current version when you update a record (Paper Trail stores the previous one on an update). That way the current version can easily be managed with all the rest in features such as wiki-like history and comparison.
And you actually can delete old versions with Vestal Versions. Vestal Versions recovers previous versions based on differences with the current one, so deleting old ones doesn't hurt a bit either.
@4ware, Paper Trail is only available for Active Record, there is mention of adding Mongoid support here: https://github.com/airblade/paper_trail/issues#issue/35
@Steve, I haven't tested this with associations yet, but try adding has_paper_trail to both models and see if it brings back the association. If that doesn't work, I know Vestal Versions has a ":tracking" option for this, so you may want to use that if you need this functionality.
@otagi, right, you would only want to add this where it works well.
@/users/1021, yes, but they can also edit other models by guessing their ids. If you are restricting permissions in the resource controller, you will also need to restrict them in the versions controller. You may want to use CanCan for this so the permissions are all in one place.
@Javi, thanks for the info. I didn't realize Vestal Versions allowed deleting of older records.
@Ryan, thanks for this -- a far neater implementation of undo than in my own apps! Also, using `scoped` is a good trick.
@Steve, automatically bringing back dependent associations is something I haven't cracked yet. I've tried a couple of times but so far haven't come up with code I like. All patches welcome.
@Steve Castaneda, I bet the best way to revert recursive removals fromoutside thegem would be to store the class name and the ids of the children into the metadata of the destroyed object, and recall them on restore.
Check out my fork here: https://github.com/maletor/paper_trail
Also, vestal_versions is better.
Using "html_safe" works for flash :notice messages and it is better than a blanket rendering of all :notice messages using raw()
So, that'll become:
redirect_to :back,
:notice=>"Undid #{@version.event}. #{link}".html_safe
This is a beautiful thing. As more and more experts in user design are bringing web interfaces into the 21st century, its a gem that our tools are starting to catch up.
Aza Raskin (Jef's son) recently wrote on this subject, explaining its virtues in http://www.alistapart.com/articles/neveruseawarning/
and Ryan comes up with this pretty ditty just days after I started thinking about it. I got excited and started messing around it at once.
A few areas for refactoring:
1) REST. I'm not sanguine about a versions controller delivering a reversion command in modern-day rails. Though its not a meaningful difference, it feels more right to implement this as a reversion subresource of a a version resource, so that versions#reversion becomes reversion#new. Ok, I'm anal after drinking the kool-aid, but it was cherry kool-aid! So, I prefer
resources versions do
resource reversion
end
even though the urls are completely the same.
2. MVC. I really don't like making links in controllers, particularly with all that logic there. To me, this stuff belongs in the view and/or model layers. And rails provides for communication with flash, so why not use it? I would suggest:
redirect_to url, :flash => {:notice => msg, :reversion => x.versions.etc.id}
and then build your interface in the view layer, where you can custom-tailer the presentation as you always should.
3) There is a bug. Things blow up if you are updating a product that does not already have at least one version (at least the create version). This can (and is likely) to happen when you are building an undo structure over an existing database. Many ways to address this.
I, too, lament the limitations of paper_trail in terms of losing associations for a deleted resource. I wonder if model layer CRUD is too low-level for a higher-level concept such as an undo, which is generally more transactional in nature?
Great stuff, Ryan. You tweaked my mind with an elegant and straightforward spike at a non-trivial problem. Next step, how to integrate this undo functionality more generally, ultimately for inclusion in the framework itself... Aza thinks we should, and when has he ever been wrong?
I agree
@Andrew Greenberg, a small correction: Paper Trail doesn't lose a deleted resource's assocations. They're all present and correct in its versions table.
The hassle (for now) is that you have to restore the assocations from that information yourself, rather than Paper Trail doing it for you. If you think of a good way to do this, please let me know.
@Andy Stewart, correction granted. I have been looking at the problem, and it seems like -- at least for purposes of an undelete function -- it ought to be straightforward-ish, at least for certain types of associations.
Clearly, if the programmer is setting up a complicated transaction in which he is "manually" manipulating associations, she should be preparing to undo the transaction herself, either by embedding the information in the paper-trail records or so forth.
But because Rails permits associations to be manipulated using the :dependent => :foo options, a bunch of records can be deleted without the programmer under the metal. But in this case, it seems to me that it ought to be straightforward, using the Activerecord reflection capabilities to identify the tables that will contain autodeleted records, and then to set up a query on the versions of that table "pointing" back to the id of the principal deleted record to find the contemporaneous delete versions. and then to undelete those records, and so on.
I think reflection can automate this to some extent, at least for has_many and belongs_to types of associations with :dependent options. I don't need to look at harder questions for the undelete function, thankfully.
Or am I missing stuff?
hey,
is it possible to track files ? like images ? that would be nice :)
Not using this, but I believe paperclip supports S3 versioning
Awesome episode Ryan. I needed this in an existing Rails 2.3.8 project, so I made a few changes to suit.
If anyone is interested, my fork is at: https://github.com/MeetDom/railscasts-episodes/tree/rails238/episode-255
Includes the sqlite3 db with products and has fixes to deal with undoing a create which was throwing an ActiveRecord::RecordNotFound error. I substituted @template for view_context since it's not available in until Rails 3.
Hope this helps someone else still using Rails 2.3.8.
Awesome, as usual.
The only thing I'd say is that the action is misnamed. You're not reverting the version in the parameter - you're reverting TO that version.
Does anyone have any thoughts for implementing this on an edit or destroy multiple action?
As usual great work. Thanks. :-)
FYI Ryan, rather than doing product.versions.scoped, I think you can use product.versions.reload (I think the passing true to the scope/relation is getting phased out in favor of reload, which I think is better).
Thanks for the view_context tip! Was looking for a solution for that problem!
Thanks for very good introduction to Papertrail.
I had a query regarding the "undo" link. What will happen if multiple users are performing updates on various products, and some of them starts undoing their updates. Wouldn't the sample app in your demo always undo the last update on the product table, and hence, many users will never be able to really undo?
Regards,
Srikanth
In the code !params[:redo] is going to evaluate the truthyness of an instance of String. Even though the contents of that string is 'true' or 'false' !params[:redo] will evaluate to false every time. This will cause your users to get stuck in a loop where 'redo' link is always displayed.
I would make the following changes to the rever action.
Sorry for the content-free post but just had to say, that was an amazing episode, thank you Ryan :)
I had this working perfectly following this screencast and then some weeks later I went back to check on it and it was borked. Now I'm getting this error whenever I hit the undo button:
uninitialized constant VersionsController::Version
I have it set up exactly as in this screencast, but I have no clue what might have broken it.
Problem is on line 3 apparently:
Any suggestions?
Hi here's the answer to my own question:
The latest versions of Papertrail actually namespace the Version class as PaperTrail::Version.
Problem solved.
Does this work with optimistic locking and a lock_version column set up? I'm getting:
ActiveRecord::StaleObjectError
Problem is on line 4
Thanks a lot for the episode.
I am not one for commenting on videos, but you tutorials are short and to the point, its makeing my unversity project much more advanced with very little pain.
You one of best for showing off rails, but i hate it when i try some code out and it doesnot work cause rails 4 has change somthing, i guess that what learning programmer is ment for. :)
Thanks
I have one problem. I am using PaperTrail with Carrierwave gem.The model for which paper trail is enabled also has some images lets say avatar. So when PaperTrail is creating a version for that model it is storing the image as an image object not the image URL as String. so when I reify that version it breaks with the following error.
TypeError: allocator undefined for Proc
from /home/webonise/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/psych/visitors/to_ruby.rb:281:in
allocate'
revive'from /home/webonise/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/psych/visitors/to_ruby.rb:281:in
from /home/webonise/.rvm/rubies/ruby-1.9.3-p551/lib/ruby/1.9.1/psych/visitors/to_ruby.rb:219:in `visit_Psych_Nodes_Mapping'
............
Can you please help me with this?
It would be great if you add similar example.
Thanks..