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.
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:
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:
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.
Corey Oordt Oct. 27, 2009
Great post. It is an innovative way to do a commonly wanted task, but all within Django.
I wish I had thought of it! :)
Kevin Oct. 27, 2009
Terrific post. Nice solution. Also, love the blog UI theme and attention to detail. +1 really nice job.
Christian Metts Oct. 30, 2009
Once you've made the proxy model wouldn't it make more sense to override the default manager on your proxy to provide the customized queryset instead of in the admin class?
That way you would have the same access to just those objects in the rest of your code.
Samir Mamude Nov. 16, 2009
Great post, help me a lot!
Martin Geber Dec. 7, 2009
Very, very great post and an even better solution.