I do a lot of hacking on django both at work and in my spare time. One of the things I like most about django is the built in admin. It's cheap, fast and reliable. But it doesn't always have the functionality you might want to add, and extending it isn't always the easiest thing to do.
So much of what I do ends up being done in an ad hoc mode. It's only after performing a task a few times that you can really begin to get to the bottom of what is common and belongs in a framework, and what isn't. So when it came time to add some features to handle blog spam last week, I guess I finally had enough context to extend django's admin in a hopefully intelligent manner.
What I've done is add what I'm calling admin actions to the standard admin mechanisms as defined in the models. The gist of it is that I've added a few new views and a single function decorator that you can use to unleash the additional functionality within the admin.
( BTW: you can get the diff to patch your instance from here. Just apply it to your django/contrib/admin folder. I'm using an older version from the .96 days. )
So once you've applied that patch, you'll have the new @admin_action decorator. It's time to play!
I've already started adding all sorts of functionality to my models. Here's an example of its use:
Suppose you've got a lot of blog spam that you want to delete. Choosing the comments individually and deleting them would be pretty tedious. We can solve this by creating a bulk deleter function in the manager class for the comments. Here's how we add multiple delete using the admin_action decorator.
In django.contrib.comments.models I add the following to the CommentManager
from django.contrib.admin.decorators import admin_action @admin_action( name='Delete selected posts' ) def slaughter( self, id_list ): res = "" for id in id_list: obj = self.get( pk=id ) res += repr( obj ) + "<" + "b" + "r" + " /> " obj.delete() return "Deleted %s" % res
When you use the admin_action decorator on a manager function it gets turned into an available action on the object listing page. You'll get check boxes next to all of the items and a dropdown control at the bottom of the list detailing the available actions. The function signature is important here. It should expect a list of integers that represent primary keys. At this point true composite keys are not supported.
Here's what you'll see:
After selecting the offending comments and choosing 'Delete selected posts' from the drop down, a quick smack on the submit button takes us to the results page where the slaughter method has been called and the results are presented.
These bulk operations are pretty useful. Especially on comments. I've added actions to ban ip addresses, re-check them against akismet, bulk approve/disapprove and send warning notices.
This type of extension is so useful, I immediately extended it to work for model methods too. When you decorate a method on model, the method shows up in a new section called 'Special Ops' just above the save/delete bar.
The method should only take 'self' as an argument in this case. Here's a lazy example for the comment model:
from django.contrib.admin.decorators import admin_action @admin_action( name='Send a warning message to this user' ) def harass( self ): """ Send a note to comment.email telling them to chill out. """ return "warning sent to %s" % ( self.person_name, )
You'll note it didn't actually do anything. It doesn't have to. It's just an example. One additional thing to note is that the 'name' parameter being passed in to the decorator is optional. Without it, the method name will be used.
Here's some screenshot love showing the appearance of the 'harass' action:
and the page resulting from clicking on the action:
Your actions don't have to do anything directly either. They can return instructions that link to a full external view if you'd like, or even just redirect out to an external app. For example:
from django.contrib.admin.decorators import admin_action @admin_action( name= 'Destroy for all time after formatting the system drive' ) def burninate( self ): return """ ********** Are you sure? Are you absolutely certain? Really? *************** <strong><a href="/some/dangerous/view/%s/">YES, I hate persistent storage</a></strong> <b> <a href="../../">No, I'm good. l8r...</a> """ % self.id @admin_action( name="Vendor tasklet" ) def outsourced( self ): return """<scrip""" + """t>document.location = "http://example.com/task?ctnt=%s";</sc""" + """ript>""" % urlencode( self.content ) </b>
Now you can easily hook in views for spell checking, signaling your edge-caching service, advanced lookups, entity extraction, sending to a mailing list, etc.
( BTW: I know the results pages are ugly. Suggest a fix. If there's interest, I'll submit this to django proper, but I know they're working on a newforms admin anyway. If you find this useful or have questions just drop me note here. )
UPDATE: I've pulled down the latest 0.96.1 stable release, applied the patches to that and replaced the pathfile linked to above with something more people can use. To apply it to your own django, try:
$ cd django/contrib/admin $ wget http://franxman.com/browsable/django-special-ops/specialops.diff $ patch --strip 0 < specialops.diff
And you're off to the races, ready to add multiple delete, etc to your models.
If you'd like to receive or contrib improvements, I'm using bzr, so you can stay in sync with the whole thing by :
$ bzr branch http://bzr.franxman.com/Django-0.96.1-SpecialOps/
If you'd like to keep track of the comments on this article, you can use this rss feed.
Based upon your reading habits, might I recommend:
Or, you might like: