All Unkept
Posted in: Django, Python, Web development  —  25 March 2006

Django Admin Hack - Fields varying with user permissions

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 §

§ On 27 March 2006, derek wrote:
57 I had to add 'null=True' into the user line in the model to get this to work.

Very useful, thanks.

§ On 27 March 2006, Joseph Kocherhans wrote:
58 Thanks for that hack Luke! I'd resigned myself to adding all sorts of incredibly ugly nested ifs in my admin templates to get things like this to work. This involves much less code and in some ways safer. There really needs to be a "blessed" way to do this in Django.

/ME goes digging to find a clean way to instantiate new AdminOptions instances and swap them out when a view is called.

§ On 28 March 2006, luke wrote:
59 Joseph,

perhaps having a 'class_' attribute for both the 'Meta' and 'Admin' classes could do this? If present, the class specified by that attribute would be instantiated instead of Options and AdminOptions. I might bring this up on django-dev.

Luke

§ On 29 March 2006, Joseph Kocherhans wrote:
60 I was thinking that if you want to override the default behavior, maybe you could just have the inner Admin class inherit from AdminOptions. Then, if Admin is a subclass of AdminOptions, the metaclass machinery would just instantiate your AdminOptions subclass with no arguments. Otherwise, it would do the normal AdminOptions instantiation dance... I might toy around with this next week, but for now I have projects to finish. Bringing it up on django-dev would probably be a good idea. I bet Jacob has some ideas.

§ On 29 March 2006, Jay Graves wrote:
61 It's great to know that this will finaly be available once "magic removal" hits the trunk.

§ On 29 March 2006, luke wrote:
62 Derek - thanks for the catch. I've found that in a later version of this code (that is now actually live), I've changed this:

if self.officer_id is None:

to this:

if not hasattr(self, 'officer_id') or self.officer_id is None:
# etc

I guess that is an alternative way to get around the same problem.

§ On 30 March 2006, akaihola wrote:
63 luke, this is even prettier:

if getattr(self, 'officer_id', None) is None:
# etc

§ On 30 March 2006, luke wrote:
64 akaihola: thanks, I missed the obvious!

§ On 1 April 2006, Joseph Kocherhans wrote:
65 Note that this only works for python 2.4 because 2.3 does not have threading.local(). In magic-removal, the pure python version of threading.local() from 2.4 is now included in django.utils, so something like this will work (hopefully the correct indentation is obvious):

try:
from threading import local
except ImportError:
from django.utils._threading_local import local

_thread_locals = local()

§ On 30 August 2006, ibson wrote:
91 Hi,
I have a problem using this hack in my models while using limit_choices_to. The returned user doesn't change sometimes, so the result of limit_choices_to is not allways the good values.
I explain that a little here ( http://groups.google.com/group/django-users/browse_thread/thread/8006b7e493ef877d )
Some help?

§ On 30 August 2006, ibson wrote:
92 I think I see what I'm doing bad!
I didn't notice that every where I use the hack I have to call the method in order to create a new local thread!!
Now it seems a little stable!

§ On 9 November 2006, Picio wrote:
159 Hello I used your trick in my app too, thanks for It! I saw in one ticket that you point out a way to have a custom filter for records in Admin. I'm talking about:
---------
05/16/06 13:15:30: Modified by lukeplant:

"My overridden get_query_set() method then accesses the User and/or Member object to decide what filters to add e.g. 'posts' that are 'hidden' are not visible to 'Members', only 'Users', private 'Messages' are only visible to the Member they are to etc......"
---------

This is something I really searching for, because my app miss only this thing.
Can you tell me more about It?
Thanks a lot.

§ On 21 August 2007, gustavo wrote:
244 luke, thanks a lot !
you safe my life!

§ On 17 September 2007, Andy Baker wrote:
270 I am wondering if some of this is obsolete now? Has newforms-admin introduced a more elegant way to implement this?

Also is this patch relevant?: http://code.djangoproject.com/ticket/3987

§ On 20 September 2007, luke wrote:
271 I think all of it is obsolete even without newforms-admin -- some changes to the way the metaclasses worked provided a much better way of implementing this. I haven't looked at newforms-admin though, it might bring more improvements.

§ On 4 October 2007, Andy Baker wrote:
272 What you said about Metaclasses sounds interesting. Could you elaborate?

§ On 4 October 2007, Andy Baker wrote:
273 Sorry - to be more specific in my original enquiry - is the threadlocals middleware obselete? I'm particularly interested in accessing the user object from the 'save' method of a model.

§ On 4 October 2007, luke wrote:
274 Andy: sorry, the threadlocals middleware would not be obsolete, as this technique would be the only way of getting hold of the 'current user' in the 'save()' method. That's because the ORM is not coupled to any concept of a web request, so there is not necessarily any 'current user' at all.

§ On 19 March 2008, igor wrote:
306 Please give an example how to use it.
Thanks.

§ On 15 April 2008, dimrub wrote:
318 Thank you! It works great, and it allowed me to go on using the admin views, with is a great relief really.

§ On 24 November 2008, vincent wrote:
380 Now it's way more simple!

http://docs.djangoproject.com/en/dev/ref/contrib/admin/#save-model-self-request-obj-form-change

Don't forget to do admin.site.register(Article, ArticleAdmin) after creating the ArticleAdmin class

§ On 24 November 2008, vincent wrote:
381 Well actually, the solution given here http://docs.djangoproject.com/en/dev/ref/contrib/admin/#save-model-self-request-obj-form-change is the new and more simple way of getting hold of the 'current user' in the 'save()' method, but I don't think it allows you to hide/show the field depending of the user

§ On 24 November 2008, luke wrote:
382 Yes, it's very different now - I blogged about this here:

http://lukeplant.me.uk/blog.php?id=1107301686

Add comment

Format:

  • Javascript has to be on to get past my spam protection, and cookies, and there is a delay, sorry for any inconvenience!
  • I reserve the right to moderate comments.