Django admin panel is one of the biggest strengths on Django. It allows quickly have interface access data stored in DB, have forms to add and edit data and manage users.
But since in its default state Django Admin Site is quite basic, so in this article I want to go over all the ways to make the most out of it by customising it.
In this article:
- Django project setup for customising admin site
- Django Admin Site Tutorial
- Setting up models and basic ModelAdmin classes
- Search objects in Django Admin
- Adding filters to Django Admin Site Change List
- Writing custom filters for Django Admin Site
- CSV and Excel Import and Export in Django Admin Site
- Adding links to related objects and foreign keys in Django Admin
Django project setup for customising admin site¶
In order to make all the code easily accessible I have created a Django project from scratch and created a git repository, so you can see the end project in case you want to see how all pieces work together.
Here is the repository: https://github.com/appliku/admin_customization
To allow you follow along here is the whole process.
Let's start with project creation:
cd ~/src # i keep all my projects in the src directory in my users $HOME
mkdir admin_customization
cd admin_customization
python3 -m venv env/
source env/bin/activate
pip install --upgrade pip
pip install django
django-admin startproject project .
pip install django-environ
pip freeze > requirements.txt
./manage.py startapp myapp
Open project/settings.py
and add 'myapp'
to the INSTALLED_APPS.
Run migrations
python manage.py makemigrations
python manage.py migrate
Now let's create a superuser.
DJANGO_SUPERUSER_PASSWORD=somethingsupersecret123 python manage.py createsuperuser --username=admin --email=admin@example.com --noinput
If you are looking for a deployment tutorial for your Django app check one of these:
- Deploy Django on Hetzner Cloud
- Deploy Django to Digital Ocean Droplet
- Deploy Django to AWS Lightsail
- Deploy Django to AWS EC2
- Deploy Django on Linode
- Deploy Django to Google Cloud Platform
Django Admin Site Tutorial¶
Let's start with the basics first.
The admin site is enabled by default when you create a project with startproject
command.
If you didn't use the default project then make sure that these requirements are met:
- The following lines are added in INSTALLED_APPS
: django.contrib.admin
, django.contrib.auth
, django.contrib.contenttypes
,django.contrib.messages
, django.contrib.sessions
- The TEMPLATES
setting must include django.template.backends.django.DjangoTemplates
backend like this:
# settings.py
from pathlib import Path
BASE_DIR = Path(__file__).resolve(strict=True).parent.parent
# ...
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
- The
MIDDLEWARE
setting must includedjango.contrib.auth.middleware.AuthenticationMiddleware
anddjango.contrib.messages.middleware.MessageMiddleware
. - Admin must be included into the
URLconf
. This one you want to alter for improved security. Either hardcode it to be anything different from the standard/admin/
or better yet – make it use an environment variable so you can set a different secret admin url in production.
How to set secret Django admin URL from environment variable¶
In your settings.py
you should set a variable, let's call it ADMIN_URL
. In this example we'll use the django-environ
package to work with env vars.
If you don't have it installed please do:
python -m pip install django-environ
And include it in your requirements.txt
.
In settings.py
you should import the environ
and create env
object. Then you can set the value for the ADMIN_URL
defaulting to something that is not the standard admin/
.
# settings.py
import environ
env = environ.Env()
ADMIN_URL = env('ADMIN_URL', default='notadmin123/')
Now open the main urls.py
and in the urlpatterns
list add the admin line (or edit the standard admin/
one if you have it already).
# urls.py
from django.conf import settings
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
# ... your other URLs
path(settings.ADMIN_URL, admin.site.urls),
# ... your other URLs
]
Setting up models and basic ModelAdmin classes¶
In order to demonstrate how to overcome some struggles that you might have in real apps we will need several models with various types of relationships. I will also show an example where something should go into models and be editable in the admin and what can be hardcoded because it won't be changed that often.
Our example app will be an oversimplified e-commerce CRM/Order management.
The database will consist of the following models:
– Category – Product – Customer – Order – Order Log
Here is the content of myapp/models.py
:
from django.db import models
from django.utils import timezone
from myapp.tuples import ORDER_STATUSES_CHOICES, ORDER_STATUSES
class Category(models.Model):
name = models.CharField(max_length=200)
slug = models.SlugField(max_length=200, unique=True)
is_active = models.BooleanField(default=True, db_index=True)
class Meta:
ordering = ('name',)
verbose_name = 'category'
verbose_name_plural = 'categories'
def __str__(self):
return self.name
class Product(models.Model):
name = models.CharField(max_length=200)
slug = models.SlugField(max_length=200, unique=True)
category = models.ManyToManyField(Category, related_name='products')
is_active = models.BooleanField(default=True, db_index=True)
class Meta:
ordering = ('name',)
verbose_name = 'product'
verbose_name_plural = 'products'
def __str__(self):
return self.name
class Customer(models.Model):
first_name = models.CharField(max_length=200)
last_name = models.CharField(max_length=200)
phone = models.CharField(max_length=200, unique=True)
class Meta:
ordering = ('first_name',)
verbose_name = 'customer'
verbose_name_plural = 'customers'
def __str__(self):
return ' '.join([self.first_name, self.last_name])
class Order(models.Model):
customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
created_dt = models.DateTimeField(auto_now_add=True)
completed_dt = models.DateTimeField(null=True, blank=True)
status = models.IntegerField(default=ORDER_STATUSES.new, choices=ORDER_STATUSES_CHOICES)
class Meta:
ordering = ('-created_dt',)
verbose_name = 'order'
verbose_name_plural = 'orders'
def __str__(self):
return str(self.id)
def save(self, *args, **kwargs):
if self.completed_dt:
self.status = ORDER_STATUSES.complete
if self.status == ORDER_STATUSES.complete and not self.completed_dt:
self.completed_dt = timezone.now()
super().save(*args, **kwargs)
class OrderItem(models.Model):
order = models.ForeignKey(Order, on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.PositiveIntegerField(default=1)
class Meta:
verbose_name = 'order item'
verbose_name_plural = 'order items'
unique_together = ('order', 'product')
def __str__(self):
return self.product.name
Create a file myapp/tuples.py
and put this in it.
from collections import namedtuple
ORDER_STATUSES = namedtuple('ORDER_STATUSES', 'new processing shipped complete canceled')._make(range(5))
ORDER_STATUSES_CHOICES = (
(ORDER_STATUSES.new, 'New'),
(ORDER_STATUSES.processing, 'Processing'),
(ORDER_STATUSES.shipped, 'Shipped'),
(ORDER_STATUSES.complete, 'Complete'),
(ORDER_STATUSES.canceled, 'Canceled'),
)
Now create new migrations and apply them:
python manage.py makemigrations
python manage.py migrate
Now let's run the development server and open our admin panel:
python manage.py runserver
The output should be like this:
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
July 09, 2023 - 07:18:13
Django version 4.2.3, using settings 'project.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
Open your browser at this URL: (http://127.0.0.1:8000/notadmin123/)[http://127.0.0.1:8000/notadmin123/] because that's where our admin is located.
The credentials are admin
/ somethingsupersecret123
You will see this basic admin panel without our models in there.
Let's fix that.
How to register Django model in the admin site¶
Open the myapp/admin.py
file and let's create very basic admin models.
from django.contrib import admin
from .models import Category, Product, Customer, Order
class CategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'slug', 'is_active', 'id',)
admin.site.register(Category, CategoryAdmin)
class ProductAdmin(admin.ModelAdmin):
list_display = ('name', 'slug', 'is_active', 'id',)
admin.site.register(Product, ProductAdmin)
class CustomerAdmin(admin.ModelAdmin):
list_display = ('first_name', 'last_name', 'phone', 'id',)
admin.site.register(Customer, CustomerAdmin)
class OrderAdmin(admin.ModelAdmin):
list_display = ('customer', 'created_dt', 'completed_dt', 'status', 'id',)
admin.site.register(Order, OrderAdmin)
Refresh the admin page and you should see the "MYAPP" block appear and our 4 models listed.
In this step, we have registered models in the admin panel. Thanks to the list_display
attribute, we tell the Django admin site what fields to display in the table for each model.
I will create a few records in every table so we can see how our admin modifications affect the admin site.
Pre-populate slug fields in the Django admin site¶
If you have started filling the database with data, you might have noticed that you have to manually enter values for the slug field.
Luckily, Django has a feature to automatically populate the content of a SlugField
.
And this will be our first admin site customization.
In the file myapp/admin.py
add the prepopulated_fields
attribute to CategoryAdmin
and ProductAdmin
classes:
class CategoryAdmin(admin.ModelAdmin):
prepopulated_fields = {'slug': ('name',)} # new
list_display = ('name', 'slug', 'is_active', 'id',)
admin.site.register(Category, CategoryAdmin)
class ProductAdmin(admin.ModelAdmin):
prepopulated_fields = {'slug': ('name',)} # new
list_display = ('name', 'slug', 'is_active', 'id',)
admin.site.register(Product, ProductAdmin)
The prepopulated_fields
attribute will make the Django admin site fill the slug field with what you type into the product or category name
field during the creation of the record. It will not be changing the slug's value during the editing process though, because changing slugs is usually undesired behavior because it changes URLs.
Now when adding a category, I only had to type the category's name, and the slug field is automatically filled with the appropriate slug.
I have created four categories, and the change list looks like this:
Now I want to create some products. The slug field is populated from the name, which is cool. But to select multiple categories, I would have to use CTRL/CMD click, which is inconvenient. Plus we are here to show off Django admin site customization kung fu 🤣
Let's edit our myapp/admin.py
to add filter_horizontal
class ProductAdmin(admin.ModelAdmin):
prepopulated_fields = {'slug': ('name',)}
list_display = ('name', 'slug', 'is_active', 'id',)
filter_horizontal = ('category',) # new
admin.site.register(Product, ProductAdmin)
Now we go back to the form and the "category" field looks much better:
Customise Ordering in Django Admin¶
If you need to customise ordering in the change list you can approach it two ways.
The first one is to apply ordering in the model itself
# models.py
class SomeModel(models.Model):
name = models.CharField(...)
class Meta:
ordering = ('name', )
or if you want this customisation only apply to admin then do it in the ModelAdmin
# admin.py
class SomeModelAdmin(admin.ModelAdmin):
ordering = ('name',)
Search objects in Django Admin¶
Built-in search functionality in Django Admin¶
I have added a bit more products and it looks amazing, but something is missing.
Since we have so many products we need a way to search for what we need.
Let's add a search bar.
Add search_fields
to the ProductAdmin
class ProductAdmin(admin.ModelAdmin):
prepopulated_fields = {'slug': ('name',)}
list_display = ('name', 'slug', 'is_active', 'id',)
filter_horizontal = ('category',)
search_fields = ('name',) # new
admin.site.register(Product, ProductAdmin)
And now we have this small search bar on top of the list of products. It will search over the name
field in our model.
But we want some more tools for finding the right item and for that we need filters.
Advanced search and custom search tools in Django Admin¶
For the advanced search features let's install djangoql
package.
Run this command within the virtual env:
pip install djangoql==0.17.1
and add djangoql==0.17.1
to the requirements.txt
Add 'djangoql' to INSTALLED_APPS in your settings.py:
INSTALLED_APPS = [
...
'djangoql',
...
]
Adding DjangoQLSearchMixin
your model admin will replace the standard Django search functionality with DjangoQL search. DjangoQL will recognise if you have defined search_fields in your ModelAdmin class, and doing so will allow you to choose between an advanced search with DjangoQL and a standard Django search (as specified by search fields).
Example for our Product model:
from djangoql.admin import DjangoQLSearchMixin
class ProductAdmin(DjangoQLSearchMixin, admin.ModelAdmin):
prepopulated_fields = {'slug': ('name',)}
list_display = ('name', 'slug', 'is_active', 'id',)
filter_horizontal = ('category',)
search_fields = ('name',)
list_filter = ('category', 'is_active',)
admin.site.register(Product, ProductAdmin)
Adding filters to Django Admin Site Change List¶
In order to add filters you need to add list_filter
attribute to the ModelAdmin
.
So go to myapp/admin.py
and add a list_filter
attribute to our ProductAdmin
class
class ProductAdmin(admin.ModelAdmin):
prepopulated_fields = {'slug': ('name',)}
list_display = ('name', 'slug', 'is_active', 'id',)
filter_horizontal = ('category',)
search_fields = ('name',)
list_filter = ('category', 'is_active',) # new
admin.site.register(Product, ProductAdmin)
Now you can see filters to the right from the change list:
Writing custom filters for Django Admin Site¶
There is a plenty of opportunity how to add custom filters, but there are a few typical cases for that.
Custom Django Admin filter multiple values¶
Look at our Order
model. Imagine you want to see all orders in any active state? This would include "New", "Processing" and "Shipped".
Let's make it.
First, I have created 1 order in each status.
Let's see what happens if I just add a list_filter
attribute with status:
# myapp/admin.py
class OrderAdmin(admin.ModelAdmin):
list_display = ('customer', 'created_dt', 'completed_dt', 'status', 'id',)
list_filter = ('status',)
admin.site.register(Order, OrderAdmin)
Such filter doesn't help a manager or store owner who wants to see all active orders at a glance.
Let's make a custom admin filter for order status and add it to list_filter
class OnlyActiveOrdersFilter(admin.SimpleListFilter):
title = 'Show Only Active Orders'
parameter_name = 'status'
def lookups(self, request, model_admin):
return (
('active', 'Active'),
)
def queryset(self, request, queryset):
if self.value() == 'active':
return queryset.filter(status__in=(ORDER_STATUSES.new, ORDER_STATUSES.processing, ORDER_STATUSES.shipped))
return queryset
class OrderAdmin(admin.ModelAdmin):
list_display = ('customer', 'created_dt', 'completed_dt', 'status', 'id',)
list_filter = ('status', OnlyActiveOrdersFilter,)
admin.site.register(Order, OrderAdmin)
As you can see if you select filter by "Active" then there will only be 3 orders in the list in statuses "Shipped", "Processing" and "New".
CSV and Excel Import and Export in Django Admin Site¶
The whole corporate world is powered by spreadsheets.
Spreadsheets are the most popular way of bulk import and updating data.
Luckily, we have a great package called django-import-export
which makes it super easy to enabled CSV, XLS, etc import export for our models in the admin site.
Install the package
pip install django-import-export
and add it to "requirements.txt":
django-import-export==3.2.0
Add it to your INSTALLED_APPS
:
# settings.py
INSTALLED_APPS = [
...
'import_export',
]
Documentation says you also need to run python manage.py collectstatic
in order for it to work.
In the myapp/admin.py
add two imports and add a resource for our import & export operations.
Since we are using other mixin for our ProductAdmin
let's keep being consistent and add ImportExportMixin
from import_export
package instead of the ImportExportModelAdmin
.
# admin.py
from import_export import resources
from import_export.admin import ImportExportMixin
...
class ProductResource(resources.ModelResource):
class Meta:
model = Product
fields = ('name', 'slug', 'is_active', 'id',)
class ProductAdmin(ImportExportMixin, DjangoQLSearchMixin, admin.ModelAdmin):
prepopulated_fields = {'slug': ('name',)}
list_display = ('name', 'slug', 'is_active', 'id',)
filter_horizontal = ('category',)
search_fields = ('name',)
list_filter = ('category', 'is_active',)
resource_classes = (ProductResource,)
Now in our we see two buttons added in our Django Admin Site change list: Import and Export
If we click on the "Import" button we'll see this form where they hint you which fields are expected, file to import and format.
CSV export for model in Django Admin Site¶
If we click on the "Export" button we see the export form with only format selection
Exported file looks like this:
The list of objects that will get exported depends on filters and search queries in the change list. So which queryset is used to display objects will be also used for the export.
To demonstrate that I have set is_active=False
for a couple of products and searched by is_active=False
And in the export we get only inactive products.
CSV Import and update in Django Admin site¶
Let's now change some product names and import this file back
I have changed names of both products, leaving the rest as is in this XLS file.
Going back to the change list, clicking export and in the import form I am selecting the file to import and XLS format, then submit.
I get a confirmation step where the difference with the existing data is shown.
Click "Confirm Import" and see that our data has indeed changed!
Adding links to related objects and foreign keys in Django Admin¶
Let's look at our order list in Django Admin. I have slightly changed the OrderAdmin because I previously put the ID field as the last column and the customer field as the first and since it was the first field it became a link. This might confuse the user of such interface because link opens the order and not the customer.
And I need this customer field for the demonstration of making a link to a related object.
So this is our OrderAdmin
in the code:
class OrderAdmin(admin.ModelAdmin):
list_display = ('id', 'created_dt', 'completed_dt', 'status', 'customer', )
list_filter = ('status', OnlyActiveOrdersFilter,)
admin.site.register(Order, OrderAdmin)
And here is how it looks like:
Let's make it easier to click first by using list_display_links
attribute where we'll list which fields should become links to the edit form of the model. This is a very convenient feature, because numbers are very hard to click at, especially small numbers.
# admin.py
class OrderAdmin(admin.ModelAdmin):
list_display = ('id', 'created_dt', 'completed_dt', 'status', 'customer', )
list_filter = ('status', OnlyActiveOrdersFilter,)
list_display_links = ('id', 'created_dt',)
admin.site.register(Order, OrderAdmin)
Here we go, both columns ID and created_dt
fields are both clickable.
Now let's make the customer column value clickable, but it will lead to the customer edit page.
If you just include customer
in the list_display_links
it will not make it linked to the customer page, but will be yet another field that has a link to the ORDER model.
In order to link to the customer edit page we need to add a method and include that method in the list_display.
This method will return a piece of HTML and must be marked safe so that Django injects HTML and not an escaped text.
# admin.py
class OrderAdmin(admin.ModelAdmin):
list_display = (
'id',
'created_dt',
'completed_dt',
'status',
'link_to_customer', # new
)
list_filter = ('status', OnlyActiveOrdersFilter,)
list_display_links = ('id', 'created_dt',)
def link_to_customer(self, obj): # new
link = reverse("admin:myapp_customer_change", args=[obj.customer.id])
return format_html(
'<a href="{}">{}</a>',
link,
obj.customer,
)
link_to_customer.short_description = 'Customer' # new
admin.site.register(Order, OrderAdmin)
Now the link to customer looks like this: /notadmin123/myapp/customer/1/change/
and leads to the customer edit form:
By the way, it is important to make a performance optimisation here. Let's add list_select_related
here.
We can either set it to True(the default is False) or be more specific and provide a specific list of foreign keys to select.
If set to True or the list/tuple it would use the select_related
in the queryset
which would load OneToOneField
and ForeignKeyField
values in a single SELECT
statement avoiding the need to send a separate query for every object.
class OrderAdmin(admin.ModelAdmin):
list_display = (
'id',
'created_dt',
'completed_dt',
'status',
'link_to_customer',
)
list_filter = ('status', OnlyActiveOrdersFilter,)
list_display_links = ('id', 'created_dt',)
list_select_related = ('customer',) # new
def link_to_customer(self, obj):
link = reverse("admin:myapp_customer_change", args=[obj.customer.id])
return format_html(
'<a href="{}">{}</a>',
link,
obj.customer,
)
link_to_customer.short_description = 'Customer'
admin.site.register(Order, OrderAdmin)