Blog Separating staff and user accounts in Django's admin

Django's user application allows you to store user accounts for staff members (User who can access the admin application) and users without this status, lets call these "Customers" here. The problem is that both user groups are displayed in a combined table.

http://static.mahner.org/assets/weblog/django/proxyadmin1.png

This can raise some questions, especially for a client who is not so familiar with the auth backend structure. Why do staff member and customers belong together? Have customers admin access too? Is this a security issue?

So it would be cool if staff members and customers are in separate admin areas. This is quickly done by overwriting the queryset of each ModelAdmin to return only staff member or customers:

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User
from django.db.models import Q

class StaffAdmin(UserAdmin):

    def queryset(self, request):
        qs = super(UserAdmin, self).queryset(request)
        qs = qs.filter(Q(is_staff=True) | Q(is_superuser=True))
        return qs

class CustomerAdmin(StaffAdmin):

    def queryset(self, request):
        qs = super(UserAdmin, self).queryset(request)
        qs = qs.exclude(Q(is_staff=True) | Q(is_superuser=True))
        return qs

admin.site.unregister(User)
admin.site.register(User, StaffAdmin)
admin.site.register(User, CustomerAdmin)

The problem with the above code is: it won't work. Django's admin is picky about registering a Model twice, it retuns an AlreadyRegistered exception. But we have a cool way to fool it: Proxy Models. Simply define a Proxy Model which inherits the User model and call it Customer. It's important here to define the model in a models.py, otherwise Django won't add the base permissions for it.

# models.py
from django.contrib.auth.models import User

class Customer(User):
    class Meta:
        proxy = True
        app_label = 'auth'
        verbose_name = 'Customer account'
        verbose_name_plural = 'Customer accounts'

# admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User
from django.db.models import Q
from .models import Customer

class StaffAdmin(UserAdmin):

    def queryset(self, request):
        qs = super(UserAdmin, self).queryset(request)
        qs = qs.filter(Q(is_staff=True) | Q(is_superuser=True))
        return qs

class CustomerAdmin(StaffAdmin):

    def queryset(self, request):
        qs = super(UserAdmin, self).queryset(request)
        qs = qs.exclude(Q(is_staff=True) | Q(is_superuser=True))
        return qs

admin.site.unregister(User)
admin.site.register(User, StaffAdmin)
admin.site.register(Customer, CustomerAdmin)

Now we have two virtual user applications, each with it's set of users:

http://static.mahner.org/assets/weblog/django/proxyadmin2.png

It's still a bit confusing, staff members are simply called "Users". So add an additional Proxy Model and rename the users to "Staff accounts":

# models.py
class Staff(User):
    class Meta:
        proxy = True
        app_label = 'auth'
        verbose_name = 'Staff account'
        verbose_name_plural = 'Staff accounts'

# admin.py
from .models import Customer, Staff

# ...

admin.site.unregister(User)
admin.site.register(Staff, StaffAdmin)
admin.site.register(Customer, CustomerAdmin)

Looks much better now:

http://static.mahner.org/assets/weblog/django/proxyadmin3.png

One additional note: It's probably a rare case but when you edit a customer and give him staff status and select "Save and continue" you get redirected to a 404 page as the record no longer exist in that "virtual table". If you want to tackle this, update the ModelAdmin's response_change method as well.