Simon Willison linked to an article which argues:

Warnings cause us to lose our work, to mistrust our computers, and to blame ourselves. A simple but foolproof design methodology solves the problem: Never use a warning when you mean undo. And when a user is deleting their work, you always mean undo.

The post spawned a discussion on undo techniques for Django. I decided to implement one method and post the results here. It only offers undo for deleting, and not for editing. Other than that, I like it.

How it works

It’s a pretty simple concept: add a trashed_at field to your model, with the default value of None . When delete() is called on an object, if trashed_at is None , set it to the current time but don’t delete it. If it’s not None , actually delete it from the database.

Model

Here’s what I did:

from datetime import datetime from django.db import models class SomeModel ( models . Model ): # ... other fields ... trashed_at = models . DateTimeField ( blank = True , null = True ) objects = NonTrashManager () trash = TrashManager () def __str__ ( self ): trashed = ( self . trashed_at and 'trashed' or 'not trashed' ) return ' %d ( %s )' % ( self . id , trashed ) def delete ( self , trash = True ): if not self . trashed_at and trash : self . trashed_at = datetime . now () self . save () else : super ( SomeModel , self ) . delete () def restore ( self , commit = True ): self . trashed_at = None if commit : self . save ()

The custom managers are used to make it so SomeModel.objects and SomeModel.trash only query against the appropriate rows:

class NonTrashManager ( models . Manager ): ''' Query only objects which have not been trashed. ''' def get_query_set ( self ): query_set = super ( NonTrashManager , self ) . get_query_set () return query_set . filter ( trashed_at__isnull = True ) class TrashManager ( models . Manager ): ''' Query only objects which have been trashed. ''' def get_query_set ( self ): query_set = super ( TrashManager , self ) . get_query_set () return query_set . filter ( trashed_at__isnull = False )

Usage

Here are some examples:

# use the managers to see what's what >>> SomeModel . objects . count () 5L >>> SomeModel . trash . count () 0L # grab a non-trashed object >>> object = SomeModel . objects . get ( id = 1 ) >>> object < Item : 1 ( not trashed ) > # now delete it (move it to the trash) >>> object . delete () >>> object < Item : 1 ( trashed ) > >>> SomeModel . objects . count () 4L >>> SomeModel . trash . count () 1L # undo the delete >>> object . restore () >>> object < Item : 1 ( not trashed ) > # trash it again >>> object . delete () # calling delete again will *really* delete it >>> object . delete () # you could also force it to skip the trash >>> object = SomeModel . objects . get ( id = 2 ) >>> object . delete ( trash = False ) # you could use a date range filter to delete # everything trashed over a month ago >>> from datetime import datetime >>> from dateutil.relativedelta import relativedelta >>> month_ago = datetime . now () - relativedelta ( months = 1 ) >>> objects = SomeModel . trashed . filter ( trashed_at__lte = month_ago ) >>> for object in objects : ... object . delete () >>>

Code

Here’s a zip of the source code for my test. It ain’t perfect. To keep it simple, I didn’t use AJAX for the deleting and undoing, but it wouldn’t be hard to add it yourself.