Creating Different Admin Pages for Different Trusted Users in Django

Abenezer Belachew

Abenezer Belachew · October 10, 2021

13 min read

Headmaster Admin

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

  • By the end of this article, you should be able to:
    1. Set different permissions for different users
    2. Display different admin pages based on the group users are in
    3. Display different headers depending on whether you're in production or development
    4. 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 to True so Django knows that they are staff members and need to be trusted.

Starter Code

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

school/models.py

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}"
accounts/models.py
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 and UserChangeForm so the forms use the CustomUser 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.

Creating User Groups using the Django Admin Interface
  • 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.

Counselor's Admin Interface Teacher's Admin Interface Headmaster's Admin Interface

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 the self.is_superuser because we want the superuser to have access to all the models.
accounts/models.py
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

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 subclasses admin.sites.AdminSite. Add permissions to it using the has_permissionfunction 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.
school/admin.py
...
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
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.
school/admin.py

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)
config/urls.py

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 a base_site.html file so that django uses our template and not the default one.
templates/admin/base_site.html
{% 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 with header ids and assigning their background to a context we will pass as site_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 the default template filter here

How to pass the context

  • Add this ColoredAdminSite that subclasses admin.sites.AdminSite before the admin sites for the different user groups.
school/admin.py
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.

school/admin.py

...
    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 and False 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.
school/admin.py
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]"
		...
Headmaster Dev't Admin

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.