Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement an "undo" feature using Python/Django

I have a Django application where I allow a user to import a CSV file with contact data (membership #, first name, last name, etc).

When they import the file, the application checks the database for a matching record and either: 1) inserts a new record if no match exists, or 2) updates the existing data with the new data.

My question is: what is the best way to implement an undo feature, using Django or straight Python, so that a user can undo the import operation and revert multiple records back to their original state?

My initial thoughts are to create a table like this (pseudo code):

Table HISTORY
   unique_id
   record_affected_id
   old_value
   new_value

Then if the user clicks "Undo" I can look up the unique_id that's associated with their transaction and set every record affected by that transaction to the old_value.

I'm wondering if there's a simpler way to do this that I'm missing, or if anyone has experience with something like this.

like image 435
Ben S Avatar asked Dec 18 '10 19:12

Ben S


People also ask

How do you implement undo/redo in Python?

Most Python commands which affect the underlying state of an object create an undo/redo action which can then be undone or redone through Python or through the Edit > Undo and Redo menu items.

What is Django reversion?

django-reversion is an extension to the Django web framework that provides version control for model instances.


1 Answers

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.

like image 104
Anton Strogonoff Avatar answered Oct 03 '22 17:10

Anton Strogonoff