Creating Different Admin Pages for Different Trusted Users in Django
Abenezer Belachew · October 10, 2021
13 min read
Intro
- One of the cool features of Django is that it generates an admin interface automatically after reading the metadata of the models you provide. This admin interface can be used to easily allow trusted users to manage the contents from different models.
- By default, it generates the same interface for all trusted users. This may not be what we always need. There may be certain models and permissions that should only be modified by only certain user groups (For example, it wouldn’t be ideal to let, say, a new intern, have access to edit/delete features for registered accounts).
- Even if we leave trustworthiness aside, there are certain models a staff member doesn’t really need to know about to do his/her job. In this article, I will be showing you how to generate different admin views for different user groups with different permissions. This admin’s recommended use is limited to an organization’s internal management tool. It’s not intended for building your entire front end around.
- Django admin can also be a great tool to use if we need to produce something quickly and don't have time for all the views and templates.
Table of Contents
- Objective
- What we are building
- Starter Code
- More Customization
- Customizing the look of the pages
- Differentiating Development and Production Admin
- Conclusion
Objective
- By the end of this article, you should be able to:
- Set different permissions for different users
- Display different admin pages based on the group users are in
- Display different headers depending on whether you're in production or development
- Modify the layout of the admin page to your liking
What we are building
- We will be making a very simple school system that consists of four types of user groups: headmaster, guidance counselor, teacher and student.
- There will be three models in the school app: Subject, Grade and Advice.
- Teachers will be able to add, modify, or delete student grades and only view advices made by the guidance counselor on their students.
- Guidance counselors will be able to add, modify, or delete advices for students based on their grades. They will be allowed to only view grades (they don't have permission to modify them).
- The Headmaster will only be able to view grades and advices. In addition to that, the headmaster will be able to see the logs entries for the different models and add new Subjects (Math, Physics, etc...).
- All users in teachers, guidance counselors and headmasters groups will have
is_staff
set toTrue
so Django knows that they are staff members and need to be trusted.
Starter Code
- Since the main focus of the article is concerned with admin views, I will link a GitHub page similar to the end product of the custom user model article (https://testdriven.io/blog/django-custom-user-model/) to get started with.
Requirements:
- You only need Django 3.x for this article
Set Up
git clone https://github.com/abenezerBelachew/differentadmins
cd differentadmins
This setup includes
Models
from django.conf import settings
from django.db import models
User = settings.AUTH_USER_MODEL
class Subject(models.Model):
name = models.CharField(max_length=50)
description = models.TextField(max_length=120, blank=True)
def __str__(self):
return self.name
class Grade(models.Model):
GRADE_CHOICES = (
('A', 'A'),
('B', 'B'),
('C', 'C'),
('D', 'D'),
('F', 'F'),
('U', 'Unassigned')
)
student = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name="grades")
subject = models.ForeignKey(to=Subject, on_delete=models.CASCADE, related_name="grades")
grade = models.CharField(max_length=4, choices=GRADE_CHOICES, default='U')
def __str__(self):
return self.student.name
class Advice(models.Model):
student = models.OneToOneField(to=User, on_delete=models.CASCADE, related_name="advices")
advice = models.TextField(max_length=240)
def __str__(self):
return f"Advice for {self.student.name}"
from django.db import models
from django.contrib.auth.models import (
AbstractUser
)
from .managers import CustomUserManager
class CustomUser(AbstractUser):
username = None
email = models.EmailField('email address', unique=True)
name = models.CharField(max_length=100)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
objects = CustomUserManager()
def __str__(self):
return f'{self.name}'
- managers.py and forms.py in the accounts app include code to create a custom user model that uses an email as the unique identifier instead of a username and forms that subclass
UserCreationForm
andUserChangeForm
so the forms use theCustomUser
model. Read more about it here: https://testdriven.io/blog/django-custom-user-model/. This model is not that relevant to this article.
Migrations
- Make migrations
python manage.py makemigrations accounts
python manage.py makemigrations school
python manage.py migrate
Load the Fixtures
-
Go to the fixtures folder and take a look at the
.json
files in them. They contain data to fill up the different models we have migrated. -
Let's take a look at
fixtures/user_groups.json
, for example. It contains the user groups we discussed earlier and the permissions to go along with them. -
In the code below, you can see that counselors will be able to view, add, change, delete advices, and only view grade and subjects. This is specified under the "permissions" field.
[ { "model": "auth.group", "pk": 5, "fields": { "name": "Counselors", "permissions": [ ["add_advice", "school", "advice"], ["change_advice", "school", "advice"], ["delete_advice", "school", "advice"], ["view_advice", "school", "advice"], ["view_grade", "school", "grade"], ["view_subject", "school", "subject"] ] } }, { "model": "auth.group", "pk": 6, "fields": { "name": "Headmaster", "permissions": [ ["view_logentry", "admin", "logentry"], ["view_group", "auth", "group"], ["view_permission", "auth", "permission"], ["view_advice", "school", "advice"], ["view_grade", "school", "grade"], ["add_subject", "school", "subject"], ["change_subject", "school", "subject"], ["delete_subject", "school", "subject"], ["view_subject", "school", "subject"] ] } }, { "model": "auth.group", "pk": 7, "fields": { "name": "Teachers", "permissions": [ ["view_advice", "school", "advice"], ["add_grade", "school", "grade"], ["change_grade", "school", "grade"], ["delete_grade", "school", "grade"], ["view_grade", "school", "grade"], ["view_subject", "school", "subject"] ] } }, { "model": "auth.group", "pk": 8, "fields": { "name": "Students", "permissions": [] } } ]
-
This can also be done using the Django Admin interface by going under Groups (http://localhost:8000/admin/auth/group/) → Authentication and Authorization and adding a group with the permissions specified.
- Let's load all the fixtures found in the fixtures folder.
python manage.py loaddata fixtures/user_groups.json
python manage.py loaddata fixtures/users.json
python manage.py loaddata fixtures/subjects.json
python manage.py loaddata fixtures/grades.json
python manage.py loaddata fixtures/advices.json
-
users.json
includes the following 8 users:- Admin (admin@admin.com), a superuser that has access to all models
- Counselor (counselor@school.com), a staff member in the counselors user group
- Headmaster (headmaster@school.com), a staff member in the headmaster user group
- Teacher (teacher@school.com), a staff member in the teachers user group
- And 4 students (Student 1, Student 2, Student 3 and Student 4), users in the students user group
- The Password for all users is set to
password321
-
Just by specifying permissions for the different user groups, the Django Admin interface is smart enough to only show the appropriate models to the appropriate staff users. For example, if you log in to the admin dashboard as a counselor, you will only be able to access the permissions that are allowed for Counselors.
-
Try logging in using the different staff user accounts and see for yourself.
More Customization
- To add more customization to our admin pages, it would help to add helper functions to the
CustomUser
model to easily identify if a specific user belongs to a particular group or not.
Helper Functions
- Let's now add helper functions to the
CustomUser
model so we can identify which groups the active users belong to. We include theself.is_superuser
because we want the superuser to have access to all the models.
class CustomUser(AbstractUser):
...
...
@property
def is_teacher(self):
"""
Checks if the user has superuser or staff status and
exists in the Teachers group.
"""
return self.is_active and (
self.is_superuser
or self.is_staff
and self.groups.filter(name="Teachers").exists()
)
@property
def is_counselor(self):
"""
Checks if the user has superuser or staff status and
exists in the Counselors group.
"""
return self.is_active and (
self.is_superuser
or self.is_staff
and self.groups.filter(name="Counselors").exists()
)
@property
def is_headmaster(self):
"""
Checks if the user has superuser or staff status and
exists in the Headmasters group.
"""
return self.is_active and (
self.is_superuser
or self.is_staff
and self.groups.filter(name="Headmaster").exists()
)
- In
school/admin.py
, we currently have a simple admin site that shows the different models found in the school app.
from django.contrib import admin
from .models import Advice, Grade, Subject
class GradeAdmin(admin.ModelAdmin):
list_display = ["student", "subject", "grade"]
list_filter = ["subject", "grade", "student"]
list_editable = ["grade"]
class AdviceAdmin(admin.ModelAdmin):
list_display = ["student", "advice"]
list_filter = ["student",]
list_editable = ["advice",]
class SubjectAdmin(admin.ModelAdmin):
list_display = ["name", "description"]
admin.site.register(Grade, GradeAdmin)
admin.site.register(Advice, AdviceAdmin)
admin.site.register(Subject, SubjectAdmin)
And the path to the admin site in config/urls.py
urlpatterns = [
path('admin/', admin.site.urls),
...
]
- But let's say we want to have different URLs for the different staff users based on their group. For example, we want only teachers to log in through the
/teachers-admin
URL. - To do this, we'll have to extend the
AdminSite
. - Let's start with the teacher's admin. In admin.py of the school app, add a
TeachersAdminSite
class that subclassesadmin.sites.AdminSite
. Add permissions to it using thehas_permission
function so only staff members who are active and are in the teachers group have access to it. - We then register the Grade and Advice models with their respective admin views.
...
class TeachersAdminSite(admin.sites.AdminSite):
def has_permission(self, request):
return (request.user.is_active and request.user.is_teacher)
teacher_admin_site = TeachersAdminSite(name="teachers_admin")
teacher_admin_site.register(Grade, GradeAdmin)
teacher_admin_site.register(Advice, AdviceAdmin)
- Let's now register this in
config/urls.py
from django.contrib import admin
from django.urls import path
from school.admin import teacher_admin_site # New
urlpatterns = [
path('admin/', admin.site.urls),
path("teachers-admin/", teacher_admin_site.urls), # New
]
- We do the same for the other staff groups.
from django.contrib.admin.models import LogEntry
from accounts.admin import LogEntryAdmin
...
teacher_admin_site.register(Grade, GradeAdmin)
teacher_admin_site.register(Advice, AdviceAdmin)
# New
class CounselorsAdminSite(admin.sites.AdminSite):
def has_permission(self, request):
return (
request.user.is_active and request.user.is_counselor
)
counselor_admin_site = CounselorsAdminSite(name="counselors_admin")
counselor_admin_site.register(Advice, AdviceAdmin)
counselor_admin_site.register(Grade, GradeAdmin)
class HeadmasterAdminSite(admin.sites.AdminSite):
def has_permission(self, request):
return (
request.user.is_active and request.user.is_headmaster
)
headmaster_admin_site = HeadmasterAdminSite(name="headmasters_admin")
headmaster_admin_site.register(Grade, GradeAdmin)
headmaster_admin_site.register(Subject, SubjectAdmin)
headmaster_admin_site.register(Advice, AdviceAdmin)
headmaster_admin_site.register(LogEntry, LogEntryAdmin)
from school.admin import (counselor_admin_site, headmaster_admin_site,
teacher_admin_site) # New
urlpatterns = [
...
path("teachers-admin/", teacher_admin_site.urls),
path("counselors-admin/", counselor_admin_site.urls), # New
path("headmaster-admin/", headmaster_admin_site.urls), # New
]
- Counselor's will now be able to log in using their own URL, just like teachers and headmasters
Customizing the look of the pages
- We can also go further and differentiate the look of the different admin pages. We can add colors, add different type of icons for the different admin pages. For example, we may want the teacher's admin page header to be green, the counselor's yellow and the headmasters red.
- This HTML page is what is used by default to generate the base html of the different admin pages: You can find the source code here
<!-- Django's admin/base.html -->
{% extends "admin/base.html" %}
{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
{% block branding %}
<h1 id="site-name"><a href="{% url 'admin:index' %}">{{ site_header|default:_('Django administration') }}</a></h1>
{% endblock %}
{% block nav-global %}{% endblock %}
- In order to customize it, we will need to overwrite it. To do that, create a
templates/admin
folder and inside it create abase_site.html
file so that django uses our template and not the default one.
{% extends "admin/base.html" %}
{% block title %}
{{ title }} | {{ site_title|default:_('Django site admin') }}
{% endblock %}
{% block extrastyle %}
<style type="text/css" media="screen">
#header {
background: {{ site_header_color|default:"#417690"}};
}
.module caption {
background: {{module_caption_color|default:"#79aec8"}};
}
div.breadcrumbs {
background: {{ breadcrumb_background_color|default:"#79aec8"}};
}
</style>
{% endblock extrastyle %}
{% block branding %}
<h1 id="site-name">
<a href="{% url 'admin:index' %}">
{{ site_header|default:"Django Administration" }}
</a>
</h1>
{% endblock %}
{% block nav-global %}{% endblock %}
- We create a new block called
extrastyle
where we can add different styles to our page. In the above code, we are getting tags withheader
ids and assigning their background to a context we will pass assite_header_color
shortly. If we don't provide any context by that name, it will default to the normal admin color, #417690. (Read more about thedefault
template filter here
How to pass the context
- Add this
ColoredAdminSite
that subclassesadmin.sites.AdminSite
before the admin sites for the different user groups.
class ColoredAdminSite(admin.sites.AdminSite):
def each_context(self, request):
context = super().each_context(request)
context["site_header_color"] = getattr(
self, "site_header_color", None
)
context["module_caption_color"] = getattr(
self, "module_caption_color", None
)
context["breadcrumb_background_color"] = getattr(
self, "breadcrumb_background_color", None
)
return context
class TeacherAdminSite(admin.sites.AdminSite):
...
-
This
ColoredAdminSite
returns a dictionary of variables to put in the template context for every page in the admin site (From Django's Docs). Read more here. -
So now we know what it does, we can subclass for the different admin sites and add the different attributes we want to pass to the django admin.
...
return context
class TeacherAdminSite(ColoredAdminSite): # New
site_title = "Teacher's Admin Portal"
index_title = "Welcome to the Teacher's Admin"
site_header_color = "gray"
module_caption_color = "black"
breadcrumb_background_color = "#a06767"
def has_permission(self, request):
...
...
class CounselorsAdminSite(ColoredAdminSite): # New
site_title = "Counselor's Admin Portal"
index_title = "Welcome to the Counselor's Admin"
site_header_color = "green"
module_caption_color = "blue"
def has_permission(self, request):
...
...
class HeadmasterAdminSite(ColoredAdminSite): # New
site_title = "Headmaster's Admin Portal"
index_title = "Welcome to the Headmaster's Admin"
site_header_color = "purple"
module_caption_color = "blue"
def has_permission(self, request):
...
...
- Now check the different pages and how they have changed.
Differentiating Development and Production Admin
- More often than not,
DEBUG=True
in development environments andFalse
in production. Using this knowledge, we'll add a[Development]
text in the header when the user has DEBUG set to True and[Prod]
when it is False.
from django.conf import settings
...
class TeacherAdminSite(ColoredAdminSite):
if settings.DEBUG == True:
site_header = "Teacher's Admin [Development]"
else:
site_header = "Teacher's Admin [Prod]"
...
class CounselorsAdminSite(ColoredAdminSite):
if settings.DEBUG == True:
site_header = "Counselor's Admin [Development]"
else:
site_header = "Counselor's Admin [Prod]"
...
class HeadmasterAdminSite(ColoredAdminSite):
if settings.DEBUG == True:
site_header = "Headmaster's Admin [Development]"
else:
site_header = "Headmaster's Admin [Prod]"
...
Conclusion
- In this article, you've seen how to customize admin pages based on the user group a user belongs to. You should now have an idea of how to create different URLs and limit permissions to admin page and give access to only users in a specific user groups.