Users and the admin
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.