Django
Django Admin Customization

Django Admin Customization

In Django Admin Customization we will look various ways to customize the admin panel of Django. We will also look how to add custom filters, how to use 3rd party packages to search and style. We will learn how to import and export our data and many more.

Table of Contents

Basic Setup

#terminal
mkdir Django_Admin_Custom
cd Django_Admin_Custom
python3 -m venv venv
source venv/bin/activate
gh repo create
#create repo name Django_Custom_Admin
cd Django_Custom_Admin

Install Django

#terminal
pip install django
django-admin startproject core .
#create application
python manage.py startapp tickets

Open core/settings.py and add the application

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'tickets',
]

Push to git repo

#terminal
git add .
git commit -m "Basic Done"
git push

Models

Add following codes to tickets/models.py

from django.db import models

# Create your models here.
class Venue(models.Model):
    name = models.CharField(max_length=64)
    description = models.TextField(max_length=256,blank=True,null=True)
    addres = models.CharField(max_length=256,unique=True)
    capacity = models.PositiveIntegerField()

    def __str__(self):
        return f"{self.name}"
    
class ConcertCategory(models.Model):
    name = models.CharField(max_length=64)
    description = models.TextField(max_length=256,blank=True,null=True)

    class Meta:
        verbose_name = "concert_category"
        verbose_name_plural = "concert categories"
        ordering = ["name"]

    def __str__(self):
        return f"{self.name}"

class Concert(models.Model):
    name = models.CharField(max_length=64)
    description = models.TextField(max_length=256,blank=True,null=True)
    categories = models.ManyToManyField(ConcertCategory)
    venue = models.ForeignKey(to=Venue,on_delete=models.SET_NULL,null=True)
    starts_at = models.DateTimeField()
    price = models.DecimalField(max_digits=6,decimal_places=0)
    tickets_left = models.IntegerField(default=0)

    class Meta:
        ordering = ['starts_at']

    def save(self,force_insert=False,force_update=False,using=None,update_fields=None):
        if self.id is None:
            self.tickets_left = self.venue.capacity

        super().save(force_insert,force_update,using,update_fields)

    def is_sold_out(self):
        return self.tickets_left == 0

    def __str__(self):
        return f"{self.venue}:{self.name}"

class Ticket(models.Model):
    concert = models.ForeignKey(to=Concert,on_delete=models.CASCADE)
    customer_full_name = models.CharField(max_length=64)
    PAYMENT_METHODS = [
        ("ETH","Ethereum"),
        ("BTC","Bitcoin"),
        ("USDT","Tether"),
        ("SOL","Solana"),
    ] 

    payment_method = models.CharField(max_length=4,default="BTC",choices=PAYMENT_METHODS)
    paid_at = models.DateTimeField(auto_now_add=True)
    is_active = models.BooleanField(default=True)

    def __str__(self):
        return f"{self.customer_full_name}({self.concert})"

Adding models to Admin

Add following codes to tickets/admin.py.

from django.contrib import admin

from tickets.models import Venue,ConcertCategory,Concert,Ticket


# Register your models here.
class VenueAdmin(admin.ModelAdmin):
    pass

class ConcertCategoryAdmin(admin.ModelAdmin):
    pass

class ConcertAdmin(admin.ModelAdmin):
    pass

class TicketAdmin(admin.ModelAdmin):
    pass

admin.site.register(Venue,VenueAdmin)
admin.site.register(ConcertCategory,ConcertCategoryAdmin)
admin.site.register(Concert,ConcertAdmin)
admin.site.register(Ticket,TicketAdmin)

Go to terminal, makemigrations and migrate

#Terminal
python manage.py makemigrations
python manage.py migrate

Create a superuser

#Terminal
python manage.py create superuser

Run the server and visit: http://127.0.0.1:8000/admin/

Populate Database with Data

Create tickets/management/commands/populate_db.py and add following commands to it.

import random
from datetime import datetime, timedelta

import pytz
from django.core.management.base import BaseCommand

from tickets.models import Venue, ConcertCategory, Concert, Ticket


class Command(BaseCommand):
    help = "Populates the database with random generated data."

    def handle(self, *args, **options):
        # populate the database with venues
        venues = [
            Venue.objects.get_or_create(
                name="NachGhar", address="Jamal, Kathmandu", capacity=960,
            ),
            Venue.objects.get_or_create(
                name="Townhall", address="Adarshanagar, Birgunj", capacity=220,
            ),
            Venue.objects.get_or_create(
                name="Jhamel Ground", address="Jhamsikhel Lalitpur", capacity=640,
            ),
            Venue.objects.get_or_create(
                name="Tiananmen square", address="Dongcheng China", capacity=12000,
            ),
            Venue.objects.get_or_create(
                name="Base Rock Cafe", address="Karachi, Pakistan", capacity=62,
            ),
        ]

        # populate the database with categories
        categories = ["Rock", "Pop", "Metal", "Hip Hop", "Jazz","Raag"]
        for category in categories:
            ConcertCategory.objects.get_or_create(name=category)

        # populate the database with concerts
        concert_prefix = ["Underground", "Midnight", "Late Night", "Secret", "Morning" * 10]
        concert_suffix = ["Party", "Rave", "Concert", "Gig", "Revolution", "Jam", "Tour"]
        for i in range(10):
            venue = random.choice(venues)[0]
            category = ConcertCategory.objects.order_by("?").first()
            concert = Concert.objects.create(
                name=f"{random.choice(concert_prefix)} {category.name} {random.choice(concert_suffix)}",
                description="",
                venue=venue,
                starts_at=datetime.now(pytz.utc)
                          + timedelta(days=random.randint(1, 365)),
                price=random.randint(10, 100),
            )
            concert.categories.add(category)
            concert.save()

        # populate the database with ticket purchases
        names = ["Janardhan", "John", "Rashmi", "Preeti", "Yuvi", "David", "Rekha", "Joseph", "Rakesh", "Rajesh"]
        surname = ["Sharma", "Ali", "Maharjan", "Brown", "Poudel", "Kapadia", "Azmat", "Deo", "Kumar", "Wagle"]
        for i in range(500):
            concert = Concert.objects.order_by("?").first()
            Ticket.objects.create(
                concert=concert,
                customer_full_name=f"{random.choice(names)} {random.choice(surname)}",
                payment_method=random.choice(
                    ["ETH", "BTC", "USDT", "SOL", "ETH", "ETH", "ETH", "BTC"]
                ),
                paid_at=datetime.now(pytz.utc) - timedelta(days=random.randint(1, 365)),
                is_active=random.choice([True, False]),
            )
            concert.tickets_left -= 1
            concert.save()

        self.stdout.write(self.style.SUCCESS("Successfully populated the database."))
#terminal
pip install pytz
python manage.py populate_db

Before Populating

After Populating

Basic Admin Customization

Goto core/urls.py and add the following codes.

from django.contrib import admin
from django.urls import path

urlpatterns = [
    path('primeuser/', admin.site.urls),
]

Run the server and visit http://127.0.0.1:8000/admin/ you will get an error.

Visit http://127.0.0.1:8000/primeuser/ to login.

Go back to core/urls.py

............................................
admin.site.site_title = "Concert Admin"
admin.site.site_header = "Our Concert Administration"
admin.site.index_title = "Our concert administration"

Refresh to see the changes.

Customize Admin Site with ModelAdmin Class

If you visit http://127.0.0.1:8000/primeuser/tickets/concert/ you will only find the name of the concert.

We can customize our admin to show other attributes of Concerts.

Edit tickets/admin.py as follow.

..............................................
class ConcertAdmin(admin.ModelAdmin):
    list_display = ["name","venue","starts_at","price","tickets_left"]
    list_select_related = ["venue"] 
    #This can save us a bunch of database queries
    readonly_fields = ["tickets_left"]
    #since it is directly related to venue
................................................

Similarly for venue and tickets.

class VenueAdmin(admin.ModelAdmin):
    list_display = ["name","address","capacity"]

.............................................................

class TicketAdmin(admin.ModelAdmin):
    list_display = ["customer_full_name","concert",
                    "payment-method","paid_at"]
    list_select_related = ["concert","concert__venue"]

..................................................................

Create and list custom fields

class ConcertAdmin(admin.ModelAdmin):
    list_display = ["name","venue","starts_at","price","tickets_left","display_sold_out"]
    list_select_related = ["venue"] #This can save us a bunch of database queries
    readonly_fields = ["tickets_left"]

    def display_sold_out(self,obj):
        return obj.tickets_left == 0
    
    display_sold_out.short_description = "Sold Out"
    display_sold_out.boolean = True

Here, we created a new attribute called “display_sold_out” and added it to list.

display_sold_out.short_description = “Sold Out” This defines column header and

display_sold_out.boolean = True this tells Django that this column has Boolean value.

Link related model objects

We will link venues on the concert page.

....................................................................
class ConcertAdmin(admin.ModelAdmin):
    list_display = ["name","starts_at","price","tickets_left","display_sold_out","display_venue"]
    list_select_related = ["venue"] #This can save us a bunch of database queries
    readonly_fields = ["tickets_left"]

    def display_sold_out(self,obj):
        return obj.tickets_left == 0
    
    display_sold_out.short_description = "Sold Out"
    display_sold_out.boolean = True

    def display_venue(self,obj):
        link = reverse("admin:tickets_venue_change",args=[obj.venue.id])
        return format_html('<a href="{}">{}</a>',link,obj.venue)
    
    display_venue.short_description = "Venue"

..............................................................

We have removed “venue” from the list since “display_venue” also does its work.

Filter

Let us filter our Concerts based on venues.

.......................................................
class ConcertAdmin(admin.ModelAdmin):
    list_display = ["name","starts_at","price","tickets_left","display_sold_out","display_venue"]
    list_select_related = ["venue"] #This can save us a bunch of database queries
    readonly_fields = ["tickets_left"]
    list_filter = ['venue']

    def display_sold_out(self,obj):
        return obj.tickets_left == 0
    
    display_sold_out.short_description = "Sold Out"
    display_sold_out.boolean = True

    def display_venue(self,obj):
        link = reverse("admin:tickets_venue_change",args=[obj.venue.id])
        return format_html('<a href="{}">{}</a>',link,obj.venue)
    
    display_venue.short_description = "Venue"
................................................

Adding more filters.

class ConcertAdmin(admin.ModelAdmin):
    list_display = ["name","starts_at","price","tickets_left","display_sold_out","display_venue"]
    list_select_related = ["venue"] #This can save us a bunch of database queries
    readonly_fields = ["tickets_left"]
    list_filter = ['venue','price']

    def display_sold_out(self,obj):
        return obj.tickets_left == 0
    
    display_sold_out.short_description = "Sold Out"
    display_sold_out.boolean = True

    def display_venue(self,obj):
        link = reverse("admin:tickets_venue_change",args=[obj.venue.id])
        return format_html('<a href="{}">{}</a>',link,obj.venue)
    
    display_venue.short_description = "Venue"

Custom Filters

For custom filter, we must specify the options (lookups) and a queryset for each lookup.

Let’s create a PoshConcert and include it in ConcertAdmin‘s list_filters.

Here PoshConcert has two options : “Yes” and “No”.

When “Yes” is selected the query will return all the concerts with ticket price greater than 60.

.................................................................
from django.contrib.admin import SimpleListFilter


from tickets.models import Venue,ConcertCategory,Concert,Ticket



# Register your models here.

        
class PoshConcert(SimpleListFilter):
    title = "Posh Concert"
    parameter_name = "posh_concert"

    def lookups(self,request,model_admin):
        return [("yes","Yes"),
                ("no","No"),
                ]
    
    def queryset(self,request,queryset):
        if self.value() == "yes":
            return queryset.filter(price__gt=60)
        elif self.value() == "no":
            return queryset.exclude(price__gt=60)
        


class VenueAdmin(admin.ModelAdmin):
    list_display = ["name","address","capacity"]

class ConcertCategoryAdmin(admin.ModelAdmin):
    pass

class ConcertAdmin(admin.ModelAdmin):
    list_display = ["name","starts_at","price","tickets_left","display_sold_out","display_venue"]
    list_select_related = ["venue"] #This can save us a bunch of database queries
    readonly_fields = ["tickets_left"]
    list_filter = [PoshConcert]
   

..........................................................

Edit tickets/admin.py and add search_fields to TicketsAdmin

.....................................................
class TicketAdmin(admin.ModelAdmin):
    list_display = ["customer_full_name","concert",
                    "payment_method","paid_at"]
    list_select_related = ["concert","concert__venue"]
    search_fields = ["customer_full_name"]

......................................................

You will see a searchbar. Try searching

We can add more.

class TicketAdmin(admin.ModelAdmin):
    list_display = ["customer_full_name","concert",
                    "payment_method","paid_at"]
    list_select_related = ["concert","concert__venue"]
    search_fields = ["customer_full_name","payment_method","concert__price"]

DjangoQL for Advance Search

#terminal
pip install djangoql

Open core/settings.py

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'tickets',
    
    #3rd party
    'djangoql',
]

Now edit tickets/admin.py as follow:

........................................................
from djangoql.admin import DjangoQLSearchMixin

......................................
class TicketAdmin(DjangoQLSearchMixin,admin.ModelAdmin):
    list_display = ["customer_full_name","concert",
                    "payment_method","paid_at"]
    list_select_related = ["concert","concert__venue"]
    search_fields = ["customer_full_name","payment_method","concert__price"]

Inlines

In our example, a venue can have many concerts. In Django admin, we can use Inlines to show and edit all the concerts related to a particular venue.

There are two types of inlines: StackedInline and TabularInline.

Add following codes to tickets/admin.py.

.....................................................
class ConcertInline(admin.TabularInline):
    model = Concert
    fields = ['name','starts_at',"price","tickets_left"]


class VenueAdmin(admin.ModelAdmin):
    list_display = ["name","address","capacity"]
    inlines = [ConcertInline]

........................................................

Goto http://127.0.0.1:8000/primeuser/tickets/venue/

Click on any venue and you will find all the concerts at that venue.

Now change TabularInline to StackedInline.

class ConcertInline(admin.StackedInline):
    model = Concert
    fields = ['name','starts_at',"price","tickets_left"]

Custom Actions

The only action available for us is Delete selected

Let’s add two actions, so that admin can manually set “Sold Out” options to True. The “Sold Out” column that appears here in http://127.0.0.1:8000/primeuser/tickets/concert/ is not the attribute of the Model Concert as we defined its function in tickets/admin.py. So we cannot write an action that will set it to True but we can set “tickets_left” to zero which will in turn set “Sold Out” to True.

Edit tickets/admin.py as follows.

....................................................
@admin.action(description="Sold Out True")
def sold_out_true(modeladmin,request,queryset):
    queryset.update(tickets_left=0)
...........................................
class ConcertAdmin(admin.ModelAdmin):
    list_display = ["name","starts_at","price","tickets_left","display_sold_out","display_venue"]
    list_select_related = ["venue"] #This can save us a bunch of database queries
    readonly_fields = ["tickets_left"]
    list_filter = [PoshConcert]
    actions = [sold_out_true]
....................................................
    

Custom Admin Forms

Django admin create all the forms by default.

For example we have form to create a new ticket as follows.

Create tickets/forms.py.

from django import forms
from django.forms import ModelForm, RadioSelect

from tickets.models import Ticket

class TicketAdminForm(ModelForm):
    class Meta:
        model = Ticket
        fields = [
            "concert","customer_full_name",
            "payment_method","is_active"
        ]

        widgets = {
            "payment_method":RadioSelect(),
        }

Now, edit tickets/admin.py as follow.

...................................................
from tickets.forms import TicketAdminForm
.....................................................

class TicketAdmin(DjangoQLSearchMixin,admin.ModelAdmin):
    
    list_display = ["customer_full_name","concert",
                    "payment_method","paid_at"]
    list_select_related = ["concert","concert__venue"]
    search_fields = ["customer_full_name","payment_method","concert__price"]
    form = TicketAdminForm

Import and Export

#terminal
pip install django-import-export

Open settings.py to add this new app.

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'tickets',
    
    #3rd party
    'djangoql',
    'import_export',
]
#Terminal
python manage.py collectstatic

By running python manage.py collectstatic before using the ImportExportMixin, you ensure that any necessary static files associated with the django-import-export library are collected from the library and any other relevant apps and placed in the directory specified by your STATIC_ROOT setting. This directory is then served by your web server or made available through your chosen deployment method.

Now, edit tickets/admin.py as follow

.....................................................................
from  import_export.admin import ImportExportActionModelAdmin


from tickets.models import Venue,ConcertCategory,Concert,Ticket
from tickets.forms import TicketAdminForm
..........................................................
class TicketAdmin(DjangoQLSearchMixin,ImportExportActionModelAdmin):
    
    list_display = ["customer_full_name","concert",
                    "payment_method","paid_at"]
    list_select_related = ["concert","concert__venue"]
    search_fields = ["customer_full_name","payment_method","concert__price"]
    form = TicketAdminForm
................................................................

We had to remove the admin.ModelAdmin base class because ImportExportActionModelAdmin already inherits from it. Including both of the classes would result in a TypeError.

Run the server and visit http://127.0.0.1:8000/primeuser/tickets/ticket/.

The exported file.

Now, Try importing

We have following json file.

Change the Style of Admin Interface

#Terminal
pip install django-admin-interface

Now, edit settings.py

......................................................
# Application definition

INSTALLED_APPS = [
    #3rd party
    "admin_interface",
    "colorfield",

    
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'tickets',
    
    #3rd party
    'djangoql',
    'import_export',
]
........................................................

X_FRAME_OPTIONS = "SAMEORIGIN"            
  # allows you to use modals insated of popups
SILENCED_SYSTEM_CHECKS = ["security.W019"]  
# ignores redundant warning messages

Migrate the database.

#terminal
python manage.py migrate

Collect static files.

python manage.py collectstatic --clear

Run the server and visit admin panel.

Add more themes.

#terminal
python manage.py loaddata admin_interface_theme_bootstrap.json

You can select between themes.

You can customize themes. Click Django and let’s change the color.

More themes.

#terminal
python manage.py loaddata admin_interface_theme_foundation.json
python manage.py loaddata admin_interface_theme_uswds.json