Django Admin Hack - Fields varying with user permissions

Posted in:

NOTE: this article is based on a very old version of Django. Everything achieved here is much easier with recent Django, and works quite differently. The rest is here only for historic reasons.


Using Django's admin, I've been able to rapidly create a form for collecting 'Applications' for CCIW officers -- instead of the long paper one that they have to fill out every year, the data can be collected in a database so that next year a few clicks will do. To use it, officers will have to be 'staff' users of the Django admin application, which is fine, as long as I limit their permissions to simply creating new 'Application' objects (sorry about the confusion between 'Application' and 'application' here – English needs Python's import mechanism!). But I came across two problems:

  • A common one: the 'Application' model has a 'User' foreign key -- I need to be able to set this automatically, as otherwise officers will be able to submit applications for other people. Not good.

  • When someone with more rights browses through the applications, he or she should be able to view the 'User' field and explicitly set it if required. This means that the 'fields' list in the 'Application.Admin' class needs to be dynamic somehow.

I've achieved both these now without patching Django at all. It only works in magic-removal (there may be a way to do it on trunk, but it will likely be harder).

The first one is easy: create a middleware that stashes the 'user' object in thread local storage, and a function for getting it out again. It looks like this:

# cciw/middleware/threadlocals.py
import threading

_thread_locals = threading.local()

def get_current_user():
    return getattr(_thread_locals, 'user', None)

class ThreadLocals(object):
    """Middleware that gets various objects from the
    request object and saves them in thread local storage."""
    def process_request(self, request):
        _thread_locals.user = getattr(request, 'user', None)

Then add the ThreadLocals middleware to the end of your middleware setting, and you can access the current user from any python code by importing and using the get_current_user() function. In my case, I need the current user to be set before saving an Application object, so I just overrode the save() method of the Application class.

[As it happens, we use essentially the same thing at my day job in a multi-(logical)-tier application to avoid having to pass user information right the way through all the stacks. There are different front-ends to set the thread variable (e.g. web or console), so you haven't tied yourself to a web environment. We also ought to do the same thing with locale information -- strangely, Django currently treats these two things the other way around!]

The second is slightly harder. I tried using a descriptor in the Application.Admin inner class. However, this doesn't work -- the 'Model' metaclass doesn't leave the 'Admin' inner class as an inner class and then instantiate it -- instead it creates an 'AdminOptions' instance with everything that the 'Admin' class had. Thankfully, due to some of the magic that has been removed, and due to way that Python lets you mess with an object's class after it is created, I found the following code worked perfectly (I've drastically reduced the number of fields on the 'Application' model, but you get the point):

# cciw/officers/models.py
from django.db import models
from django.contrib.auth.models import User
from django.db.models.options import AdminOptions
import cciw.middleware.threadlocals


# Application model - in reality has more fields than this
class Application(models.Model):
    # blank=True to get the admin to work when the
    # officer field isn't there:
    officer = models.ForeignKey(User, blank = True, default = None)
    full_name = models.CharField(maxlength = 30)
    address = models.TextField()

    def save(self):
        if getattr(self, 'officer_id', None) is None:
            self.officer_id = threadlocals.get_current_user().id
        super(Application, self).save()

    class Admin:
        fields = ()# we override this later
        list_display = ('full_name', )

# At this point, the Application class and some other
# objects on the class such as Application._meta
# and Application._meta.admin have been completely set up.

class ApplicationAdminOptions(AdminOptions):
    """Class used to replace AdminOptions for the Application model"""
    def _fields(self):
        user = threadlocals.get_current_user()
        if user is None or user.is_anonymous():
            # should never get here
            return ()
        else:
            if user.has_perm('officers.change_application'):
                # Fields for a user with more privileges
                # to be able to modify existing Applications
                return (
                   (None, {'fields':
                      ('officer', 'full_name', 'address')}
                   ),
                 )
            else:
                # Fields for a normal officer
                return (
                   (None, {'fields':
                      ('full_name', 'address')}
                   ),
                )
    fields = property(_fields)

# remove the 'fields' instance variable
del Application._meta.admin.fields
# Change 'admin' class to insert our behaviour
Application._meta.admin.__class__ = ApplicationAdminOptions

And that's it. Given the amount of work that saves me, I've decided I can stomach the hackiness of those last two lines very easily!

Comments §

Comments should load when you scroll to here...