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.
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.
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.
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)
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.
Comments for this entry are closed. If you'd like to share your thoughts on this entry with me, please contact me directly.
Awesome blog entry!
Its been some time since I’ve actively done any Django work (0.95) and the new admin interface as u describe it here looks great. It sounds like the admin abstractions were pulled out model definition and are now citizens in their own right living in admin.py
I’ve been using the thread local hack, which works for me currently, but i am waiting for mysterious infrastructure issues to pop up. Also it has been a pain to get working on different distro’s under xen/uml etc.
I’m definitely switching to the new approach as soon as i have time.
Thanks ! and Happy Christmas :)
since nobody reads the docs, you should just post one interesting section from them every week.
this post has been most useful, thanks.
Thanks for the tips… hope you get on the plane soon!
Great article. Can I make a suggestion for another topic that falls in the same category of “question asked every other day”?
It might be good to talk about the “right” way to extend the user model. I know this question gets asked quite a bit and a definitive article would be nice to point people to.
This is very useful, thanks for writing it up. I’m really impressed by the amount of flexibility and careful thought put into the design of Django. Hope you get to where you want to go soon.
Shouldn’t exclude be a list. Making it a tuple causes a TypeError when Django tries to concatenate it with a default list. (Using Django-1.0)
Great article, I was one of those “How do I do it?” persons for this too!
*Harsh J*
“”” since nobody reads the docs, you should just post one interesting section from them every week. ”“”
Hey. I came from the TurboGears world where the perennial policies on documentation are:
Gee, I guess we need to do better there.
It’s the responsibility of the user community to write docs; We devs are too busy.
I read the excellent Django docs for the sheer delight of reveling in their existence and quality. ;-)
No, it can be either a list or a tuple.
There have been occasional bugs which prevented that working, but so far as we know they’re fixed. And the code above worked just fine for me on Django 1.0.2, so perhaps you should follow the advice of the release notes and upgrade.
James if threadlocal is a Very Bad thing .. how would I alter the list of columns in list_display based on request.user? Or the exclude list for that matter? Isn’t something like threadlocal the only way I could do this? I’d much prefer a supported and standard way of doing it but it’s not clear the preferred way works across the board. New to Django and Python so go easy :)
Off the top of my head I can’t give you an answer for
list_display; forexcludejust look at theget_form()method ofModelAdmin. Override that to determine what form you want to use.As a general rule, if you just look at the source code you’ll see where all the methods are and what they do, and from there it’s usually pretty obvious what to override.
Thanks for the pointer James …I’ll look into the get_form() method.
How can we do the following in an inline? def save_model(self, request, obj, form, change): if not change: obj.author = request.user obj.save()
My inline looks like this:
class rentalInline(admin.TabularInline): model= Rent extra = 3 raw_id_fields = (‘movie’,) exclude = [‘rented_by’]
But I keep getting: “null value in column “rented_by_id” violates not-null constraint”
Thanks for the great post!
Great post. I had figured out other less clean work arounds to both of those issues on a project, but this is so much elegant.
Might also be worth mentioning the has_delete_permission method which you could override to only let only authors to delete their own posts.
this seems to work for the admin, but what if I want to write a custom manager for my model which only returns the rows for the currently logged-in user?
somethin like:
how is this possible without using threadlocals?
thanks.
Great article, unfortunately it doesn’t work for ForeignKey fields: suppose I extend the Entry model by a “project” field. If user A creates a project a, and user B creates a project b, both user A & B will see both projects in the drop down list presented on the Entry page. How can we make sure that the drop-down lists are also filtered?
class Entry(models.Model): title = models.CharField(max_length=255) body = models.TextField() created = models.DateTimeField(auto_now_add = True) author = models.ForeignKey(User) project = models.ForeignKey(Project)
class Project(models.Model): title = models.CharField(max_length=255) body = models.TextField() created = models.DateTimeField(auto_now_add = True) author = models.ForeignKey(User)