
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
- Model
- Populate Database With Data
- Basic Admin Customization
- Customize Admin Site With ModelAdmin Class
- Filter
- Custom Filter
- Search
- DjangoQL for Advance Search
- Inline
- Custom Actions
- Custom Admin Forms
- Import and Export
- Change The Style Of Admin Interface
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]
..........................................................

Search
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
