Feed Sign in with OpenID OpenID

Simon Willison’s Weblog

Never use a warning when you mean undo. The abundance of “undo” is one of my favourite things about Gmail. I wonder if there’s anything Django could do to make implementing undo functionality easier...

Tagged , , , , ,

15 comments

  1. I thought exactly the same when I read the article. Did you have any idea about this implementation in Django? (or how this can be done in Gmail?).

    It's not evident to be generic on that part. For the moment I thought about a 'status' field in each model (maybe associated with a datetime field?) which is regularly checked by a cron job which purge the database. Any thoughts/pointers?

    David, biologeek - 17th July 2007 11:41 - #

  2. We're currently implementing undo and redo incrementally for Resolver.

    The basic problem is 'trivial' (maintain an undo and redo stack of operations - adding to and clearing them appropriately).

    Unfortunately the undo operations themselves are often 'non-trivial' and different for every operation.

    We almost ended up with a solution for some operations that looked like 'store the entire document state and restore that' - which obviously isn't very memory friendly...

    Michael Foord - 17th July 2007 11:42 - #

  3. I was thinking along the lines of doing it in the session - maybe adding enough information to the session to be able to reverse the effect of the previous operation. Could get messy pretty fast though. There would also have to be a process by which undo-data older than 5 minutes or so was deleted from the session again.

    Simon Willison - 17th July 2007 11:44 - #

  4. Apple's Core Data framework is supposed to give you "undo" functionality for free. Maybe that's a first place to look? Never tried it myself though.

    Jay Parlar - 17th July 2007 12:49 - #

  5. We have undo in Tabblo. It took a day to implement, and was the best value for effort in the whole product.

    We chose to simply pickle the state of the object before a modifying operation. Undo is simply an unpickling operation. As Michael points out above, this is not memory-friendly (the table of undo pickles is a significant fraction of our total data size), but it has the advantage that as the application grows more operations, nothing needs to be done to support undo, except to call the save-an-undo-pickle function before making the change. The size of the pickle warehouse can be controlled by deleting old pickles after an hour/day/week.

    With a "reverse the operation" style of undo, each new feature in the application needs to have undo/redo logic written to go with it. This brings bugs, and under time pressure, the temptation to make some operations not undoable.

    With the stored pickle model, you chew up disk space. Which is more expensive: disk or developer attention?

    Ned Batchelder - 17th July 2007 12:59 - #

  6. Thanks Ned; that was really useful. Are the objects you're pickling simply regular Django ORM model objects?

    Simon Willison - 17th July 2007 13:23 - #

  7. David's idea seems the most natural to me. You could create a 'deleted' datetime field, NULL by default. When delete() is called on the model, if self.deleted is None, set it to the current time. If not None, actually delete it.

    Then use a cron job to cleanup the items after they've been marked for deletion. Setting self.deleted to None again would undo the operation. This would allow Django to handle FileFields normally, too.

    Pro? Deleted objects can be queried and displayed like they would be normally. Con? Having to filter out deleted objects in your normal queries.

    Ned's idea is really cool, especially for tables where you don't want to leave the data in the table after it's been deleted.

    Nathan Ostgard - 17th July 2007 22:17 - #

  8. @Nathan: I like that idea. On top of that, you could use a default manager which overrides the core get_query_set method to filter out the deleted items automatically.

    SmileyChris - 18th July 2007 10:50 - #

  9. The objects we're saving are regular ORM objects. In particular, they are actually a set of related objects. For example, saving a tabblo involves saving the story-photo relationships, the story-textblocks, the photo-thoughtbubbles, etc.

    The one complexity in saving objects like this is that the pickling and unpickling code has to be updated when the data model changes. In our case at least, this was much less frequently than how often the verb set changed, so saving the whole object was definitely a win for us.

    Ned Batchelder - 18th July 2007 19:19 - #

  10. @Nathan: This sounds like a pretty good idea for an app. Then you could just link any models you wanted to be undoable to it. I guess this will have to wait for the mysterious new One-to-One system.

    Sam Bull - 19th July 2007 17:04 - #

  11. The method of creating the backups is the easy part. In Django it would be a simple manager function on your model that would pickle or serialize a record before any updates.

    Storing those, as has been pointed out, is the bigger problem.

    One implementation I was considering was generating a session folder on a disk where the records would be serialized to json files. The file naming convention would be simple:

    <model_name>_<primary_key>.<version_num>

    A middleware manager could set up the folder and destroy it for each session.

    It seems pretty heavy weight, but in read times, you can load the json into javascript and show the user the effect of the undo in the client before they commit to it without hitting the database.

    Just a thought.

    Neat thread though. :)

    James - 19th July 2007 23:07 - #

  12. Wow, thanks for all your feedback! And for your implementation Nathan. We had exactly the same approach on #django-fr when we thought about that.

    Anyway, there are a lot of useful thoughts here :-).

    David, biologeek - 20th July 2007 08:19 - #

  13. I've implemented Nathan's idea, but run into a problem with Admin, where I see the trashed items in the list, but I can't edit them since they're not part of 'objects' model property anymore.

    I'm experimenting with a solution at the moment. It would create 'trash' & 'no_trash' model properties, which would return querysets from related NonTrash and Trash model managers, so it plays nicely with Admin.

    Patrick Anderson - 20th July 2007 18:13 - #

  14. "With the stored pickle model, you chew up disk space. Which is more expensive: disk or developer attention?"

    We have multi-megabyte documents. To store the state of the whole document (or even a single worksheet) on each operation doesn't just chew memory and disk space - it is sloooow....

    This leaves us with little choice in our strategy I think.

    Michael Foord - 20th July 2007 21:04 - #

  15. What about using the python difflib to generate diffs? This should keep the size of the undos under control.

    Erik - 18th March 2008 14:27 - #

Sign in with OpenID

Auto-HTML: Line breaks are preserved; URLs will be converted in to links.

Manual XHTML: Enter your own, valid XHTML. Allowed tags are a, p, blockquote, ul, ol, li, dl, dt, dd, em, strong, dfn, code, q, samp, kbd, var, cite, abbr, acronym, sub, sup, br, pre

A django site