validate the css validate the xhtml

HackerMoJo.com


Ceci n'est pas une blog
by Glenn Franxman, Django Developer / Stunt Programmer.

Special Ops for Django

posted: 2008-02-29 00:47:20 perma-link, RSS comments feed

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 """&lt;scrip""" + """t>document.location = "http://example.com/task?ctnt=%s";&lt;/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:

 

#1

Paul Bx commented, on March 14, 2008 at 4:11 p.m.:

This kicks ass!

#2

jmelesky commented, on March 14, 2008 at 5:40 p.m.:

Have you Considered proposing this on django-developers? It's very handy functionality, and offhand i don't know that it would conflict with any of the current big dev efforts.

#3

proteusguy commented, on March 14, 2008 at 9:59 p.m.:

dittos on submitting this patch to Django. As someone who devs & deploys against the working trunk, its a PITA to apply patches every time a get an update.

Nice work though! :)

#8

Shoko commented, on August 23, 2012 at 12:24 a.m.:

The talk was pretty good, it flleid in some gaps for me about OpenID. We used the examples from the actual library and were able to get it working for us. We added a field to the users table openid_key and made username/password optional. It then requires either openid_key to be set, or username/password to be set. When they log in with their openid key and do not have a username set we force them to choose a unique username.

Post a comment


Copyright © 2003,2004,2005,2006,2007,2008 GFranxman. All Rights Reserved


hosting: slicehost.com. powered by: django. written in: python. controlled by: bzr. monsters by: monsterID.

You've been exposed to: {'Programming': 1}