[Fixed]-How to implement an "undo" feature using Python/Django

12đź‘Ť

âś…

Take a look at django-reversion. It provides version control for Django models. Can be easily added to existing project.

It doesn’t employ “current” pointer approach. Instead, it serializes object each time it’s being saved and stores it in a separate Version model with generic foreign key pointing to this object. (Relationship fields are serialized as primary keys by default.) Also, it allows to group Versions into Revisions in a flexible way.

So you can do something like that:

  • When user uploads CSV, just save changes as usual, but add @revision.create_on_success decorator to the function which does the import—so that any changes to records made by that function will be stored under a single revision.
  • When user hits “Undo”, you just revert the latest revision.

Here’s how it could be done::

@revision.create_on_success
def import_csv(request, csv):
    # Old versions of all objects save()d here will
    # belong to single revision.

def undo_last_csv_import(request):
    # First, get latest revision saved by this user.
    # (Assuming you create revisions only when user imports a CSV
    # and do not version control other data.)
    revision = Revision.objects.filter(user=request.user)\
        .order_by('-date_created')[0]
    # And revert it, delete=True means we want to delete
    # any newly added records as well
    revision.revert(delete=True)

It relies on the fact that you create revisions only when user imports CSVs. That means, if you plan to also version control other data, then you’ll need to implement some kind of a flag by which you can get records affected by the latest import. Then you can get a record by this flag, get it latest saved version, and revert the whole revision that version belongs to. Like this::

def undo_last_csv_import(request):
    some_record = Record.objects.by_user(request.user).from_the_last_import()[0]
    latest_saved_version_of_some_record = Version.objects.get_for_date(
        some_record,
        datetime.now(), # The latest saved Version at the moment.
        )
    # Revert all versions that belong to the same revision
    # as the version we got above.
    latest_saved_version_of_some_record.revision.revert()

It’s not a beautiful solution, there most certainly are ways to do it better with this app. I recommend to take a look at the code to understand better how does django-reversion work—very well documented, couldn’t find a function without a docstring. ^_^d

(Documentation is also good, but turned out to be a bit misleading for me, i.e. they write Version.objects.get_for_date(your_model, date), where your_model is actually a model instance.)

Update: django-reversion is actively maintained, so don’t rely on the code above much, and better check their wiki on how to manage versions & revisions outside django’s admin. For instance, revision comments are already supported, that may simplify things a bit.

3đź‘Ť

You need to have version control, and the issue there is not Python or Django, but rather how to design the database to do this. One common way is to store documents with unique IDs and keep track of which is the “current”. Undo is then just matter of putting the “current” pointer back to an older revision. This is, as far as I can see, what you are doing.

Although this is the common way of doing it, I don’t know if it’s the best. I’ve never seen any other way, which might mean it’s the best, or that the best way is unobvious. 🙂

Doing this in a generic way in Django is probably A Hard Problem, but will be easier if you make your Django app support it in a custom way.

Then you get into Fun (not) Issues, like how to edit things in a “future” revision and then publish a whole set of documents at once, in a staging kind of way of content. But hopefully you don’t need that. 🙂

1đź‘Ť

Your history table looks fine, except that you don’t need the new_value field to perform the undo. And yes, that’ how “undo” is often implemented (the other alternative being Lennart’s approach of putting a version number into all records). The advantage of a separate journal table is that you don’t need to deal with the version number in regular queries.

Leave a comment