Users and the admin

Published December 24, 2008. Filed under: Django.

So, for as long as I can remember the single most-frequently-asked question about the Django admin has been some variation of “how do I set a foreign key to User to automatically be filled in with request.user?” And for a while the answer was that you couldn’t do that, really; it was and still is easy to do with a custom form in your own view, but up until a few months back it wasn’t really something you could do in the admin. Now, as of the merge of newforms-admin back before Django 1.0, you can and it’s really really easy; in fact, it’s even documented. But still it’s probably the #1 most-frequently-asked question about the admin.

So as I’m stuck in a hotel overnight with nothing better to do (thanks to the gross incompetence of Delta Airlines), consider this my attempt to give the Django community an early Christmas present by walking through the process step-by-step and demonstrating just how trivially simple it is to do this. And next time you see someone asking how to accomplish this, please point them at this article.

For a free bonus Christmas present, I’ll also explain another frequently-requested item: how to ensure that people can only see/edit things they “own” (i.e., that they created) in the admin.

But first…

A big fat disclaimer: there are lots and lots of potential uses for these types of features. Many of them are wrong and stupid and you shouldn’t be trying them.

I say this because a huge number of the proposed use cases for this type of automatic filling-in of users and automatic filtering of objects boil down to “I’ve let these people into my admin, but I still don’t trust them”. And that’s a problem no amount of technology will solve for you: if you can’t rely on your site administrators to do the right thing, then they shouldn’t be your site administrators.

Also, you will occasionally see someone suggest that these features can be obtained by what’s known as the “threadlocal hack”; this basically involves sticking request.user into a sort of magical globally-available variable, and is a Very Bad Thing to use if you don’t know what you’re doing. It’s also generally a Very Bad Thing to use even if you do know what you’re doing, since you’re probably just doing it because you’re lazy and don’t feel like ensuring you pass information around properly. So if you see someone suggesting that you do this using a “threadlocal”, ignore that person.

Now. Let’s get to work.

Automatically filling in a user

Consider a weblog, with an Entry model which looks like this:

import datetime

from django.contrib.auth.models import User
from django.db import models


class Entry(models.Model):
    title = models.CharField(max_length=250)
    slug = models.SlugField()
    pub_date = models.DateTimeField(default=datetime.datetime.now)
    author = models.ForeignKey(User, related_name='entries')
    summary = models.TextField(blank=True)
    body = models.TextField()

    class Meta:
        get_latest_by = 'pub_date'
        ordering = ('-pub_date',)
        verbose_name_plural = 'entries'

    def __unicode__(self):
        return self.title

    def get_absolute_url(self):
        return "/weblog/%s/%s/" % (self.pub_date.strftime("%Y/%b/%d"),
                                   self.slug)

Our first goal is to write a ModelAdmin class for the Entry model, such that each new Entry being saved will automatically fill in the “author” field with the User who created the Entry. We can start by filling out the normal options for the ModelAdmin in the blog application’s admin.py file:

from django.contrib import admin

from blog.models import Entry


class EntryAdmin(admin.ModelAdmin):
    list_display = ('title', 'pub_date', 'author')
    prepopulated_fields = { 'slug': ['title'] }

admin.site.register(Entry, EntryAdmin)

And since we’re going to automatically fill in the author field, let’s go ahead and leave it out of the form:

class EntryAdmin(admin.ModelAdmin):
    exclude = ('author',)
    list_display = ('title', 'pub_date', 'author')
    prepopulated_fields = { 'slug': ['title'] }

All that’s left now is to override one method on EntryAdmin. This method is called save_model, and it receives as arguments the current HttpRequest, the object that’s about to be saved, the form being used to validate its data and a boolean flag indicating whether the object is about to be saved for the first time, or already exists and is being edited. Since we only need to fill in the author field on creation, we can just look at that flag. The code ends up looking like this:

def save_model(self, request, obj, form, change):
    if not change:
        obj.author = request.user
    obj.save()

That’s it: all this method has to do is save the object, and it’s allowed to do anything it wants prior to saving. And since it receives the HttpRequest as an argument, it has access to request.user and can fill that in on the Entry before saving it. This is documented, by the way, and the documentation is your friend.

Showing only entries someone “owns”

The other half of the puzzle, for most folks, is limiting the list of viewable/editable objects in the admin to only those “owned by” the current user; in our example blog application, that would mean limiting the list of entries in the admin changelist to only those posted by the current user. To accomplish this we’ll need to make two changes to the EntryAdmin class: one to handle the main list of objects, and another to make sure a malicious user can’t get around this and edit an object by knowing its ID and jumping straight to its edit page.

First, we override the method queryset on EntryAdmin; this generates the QuerySet used on the main list of Entry objects, and it gets access to the HttpRequest object so we can filter the entries based on request.user:

def queryset(self, request):
    return Entry.objects.filter(author=request.user)

Of course, there’s a problem with this: it completely hides every entry except the ones you yourself have written, we probably want an ability for a few extremely trusted people to still see and edit other folks’ entries. Most likely, these people will have the is_superuser flag set to True on their user accounts, so we can show them the full list and only filter for everybody else:

def queryset(self, request):
    if request.user.is_superuser:
        return Entry.objects.all()
    return Entry.objects.filter(author=request.user)

This only affects the entries shown in the list view, however; a different method — has_change_permission — is called from the individual object editing page, to ensure the user is allowed to edit that object. And that method, by default, returns True if the user has the “change” permission for the model class in question, so we’ll need to change it to check for the class-level permission, then check to see if the user is a superuser or the author of the entry. Here’s the code (this method receives both the HttpRequest and the object as arguments):

def has_change_permission(self, request, obj=None):
    has_class_permission = super(EntryAdmin, self).has_change_permission(request, obj)
    if not has_class_permission:
        return False
    if obj is not None and not request.user.is_superuser and request.user.id != obj.author.id:
        return False
    return True

And finally, here’s the completed admin.py file containing the full class:

from django.contrib import admin

from blog.models import Entry


class EntryAdmin(admin.ModelAdmin):
    exclude = ('author',)
    list_display = ('title', 'pub_date', 'author')
    prepopulated_fields = { 'slug': ['title'] }

    def has_change_permission(self, request, obj=None):
        has_class_permission = super(EntryAdmin, self).has_change_permission(request, obj)
        if not has_class_permission:
            return False
        if obj is not None and not request.user.is_superuser and request.user.id != obj.author.id:
            return False
        return True

    def queryset(self, request):
        if request.user.is_superuser:
            return Entry.objects.all()
        return Entry.objects.filter(author=request.user)

    def save_model(self, request, obj, form, change):
        if not change:
            obj.author = request.user
        obj.save()


admin.site.register(Entry, EntryAdmin)

And that’s it

No, really. Although this ends up generating huge numbers of “how do I do it” questions, it really is that easy.

And if you’re curious, there are plenty more interesting methods on ModelAdmin you can tinker with to get even more interesting and useful behavior out of the admin; you can do a lot worse than to sit down some night with the source code (django/contrib/admin/options.py) and read through it to get a feel for what’s easily overridden.