DEV Community

Cover image for Creating a custom page in Django Admin
Yevhen Bondar for Daiquiri Team

Posted on • Updated on

Creating a custom page in Django Admin

Django is a great web framework for fast development. Django Admin allows you to manage your data without creating your own views.

The primary use case of the Admin is CRUD operations. You can populate your local database with test data or change some real data in the production environment.

Sometimes, you need some custom interfaces to perform your routines on the project. Fortunately, Django Admin has a lot of room for customization. In this guide, I'll show how to create a custom detail page in Django Admin.

Basic setup

Let's start from scratch. I'll use python3.9 in this guide. If you are not interested in the basic details, you can jump to the custom Order page part.

First, create a virtual environment and install Django. Also, we need Pillow for Django's ImageField:

python3.9 -m venv venv 
. ./venv/bin/activate
pip install django==4.1.1 Pillow==9.2.0
Enter fullscreen mode Exit fullscreen mode

Then, create a new Django project and product app inside.

python -m django startproject django_admin_example .
python -m django startapp products
Enter fullscreen mode Exit fullscreen mode

It will be a simple shop application with Product, Order, and OrderItem models. Order can have several OrderItem and belongs to some User. Each OrderItem contains the Product and count—quantity of the Product in the Order.

Add these models in products/models.py:

from django.contrib.auth import get_user_model
from django.db import models

User = get_user_model()


class Product(models.Model):
    title = models.CharField(max_length=255)
    price = models.IntegerField()
    image = models.ImageField()

    def __str__(self):
        return self.title


class Order(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    created = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return f"{self.user} {self.created.date()}"


class OrderItem(models.Model):
    order = models.ForeignKey(Order, on_delete=models.CASCADE)
    product = models.ForeignKey(Product, on_delete=models.CASCADE)
    count = models.PositiveIntegerField()

    def __str__(self):
        return f"{self.product} x{self.count}"
Enter fullscreen mode Exit fullscreen mode

Let's include the product app in django_admin_example/settings.py. Also, we need to set MEDIA_URL and MEDIA_ROOT for file uploading:

INSTALLED_APPS = [
    # Other apps
    "products",
]

...

MEDIA_URL = 'media/'
MEDIA_ROOT = BASE_DIR / 'media/'
Enter fullscreen mode Exit fullscreen mode

Now we can create and run migrations:

python manage.py makemigrations
python manage.py migrate
Enter fullscreen mode Exit fullscreen mode

We are done with the models. Moving to the Admin part. Create a products/admin.py with the following content:

from django.contrib import admin
from products.models import Product, OrderItem, Order


@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
    list_display = ['title', 'price']


class OrderItemInline(admin.TabularInline):
    model = OrderItem

@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    list_display = ['user', 'created']
    inlines = [OrderItemInline]


@admin.register(OrderItem)
class OrderItemAdmin(admin.ModelAdmin):
    list_display = ['order', 'product', 'count']
Enter fullscreen mode Exit fullscreen mode

We created OrderItemInline to change order items on the Order form.

Next, we need to add a URL configuration for the admin site in urls.py. Also, we want to add media file URLs to serve images:

from django.contrib import admin
from django.urls import path
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    *static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT),
]
Enter fullscreen mode Exit fullscreen mode

Finally, we need a superuser to login into the admin:

$ python manage.py createsuperuser
Username (leave blank to use 'user'): admin
Email address: admin@test.com
Password: 
Password (again): 
Superuser created successfully.
Enter fullscreen mode Exit fullscreen mode

Run python manage.py runserver, go to the http://127.0.0.1:8000/admin/, and log in with the admin user.

Now, it's time to fill our database with some dummy data!

Let's add a customer user;

Some product to our shop;

And create an order for the customer.

Custom Order Page

Now we are ready to create a page for order summary. We want customer information, a list of ordered products, and a total order sum on this page.

Let's start with an empty template in products/templates/admin/products/order/detail.html. Django Admin enforces this structure for the templates: admin/APP_NAME/MODEL_NAME/some_template.html. So, all custom templates for the Order model will be in this folder.

Then, let's create an OrderDetailView in admin.py:

from django.contrib.auth.mixins import PermissionRequiredMixin
from django.views.generic.detail import DetailView
from products.models import Order

class OrderDetailView(PermissionRequiredMixin, DetailView):
    permission_required = "products.view_order"
    template_name = "admin/products/order/detail.html"
    model = Order
Enter fullscreen mode Exit fullscreen mode

Add this view to the OrderAdmin.get_urls, create the detail column and add it to the list_display:

from django.contrib import admin
from django.urls import path, reverse
from django.utils.html import format_html
from products.models import Order


@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    list_display = ['user', 'created', 'detail']
    inlines = [OrderItemInline]

    def get_urls(self):
        return [
            path(
                "<pk>/detail",
                self.admin_site.admin_view(OrderDetailView.as_view()),
                name=f"products_order_detail",
            ),
            *super().get_urls(),
        ]

    def detail(self, obj: Order) -> str:
        url = reverse("admin:products_order_detail", args=[obj.pk])
        return format_html(f'<a href="{url}">📝</a>')
Enter fullscreen mode Exit fullscreen mode

OrderDetailView will accept pk as an argument. Also, we wrap the view into admin_site.admin_view. This wrapper checks that user is logged in, user.is_staff=True, and enforces CSRF validation.

Let's check how it looks:

Creating a template

Now the detail link leads to an empty page. Let's create a real one.

As docs says:

If you want to use the admin layout, extend from admin/base_site.html:

Add this code to the detail.html and check the page in your browser:

{% extends 'admin/base_site.html' %}

{% block content %}

    <h1>Order by {{ object.user }} {{ object.created.date }}</h1>

    <dl>
        <dt>Name:</dt>
        <dd>{{ object.user.get_full_name }}</dd>
        <dt>Email:</dt>
        <dd>{{ object.user.email }}</dd>
    </dl>

{% endblock %}
Enter fullscreen mode Exit fullscreen mode

The page looks like a Django Page, but we are missing some components on the page: breadcrumbs, sidebar, and change password/logout buttons. You can check admin/base.html and admin/change_form.html to see how they are implemented.

First, we need a context for the template. Let's override a OrderDetailView.get_context method:

class OrderDetailView(PermissionRequiredMixin, DetailView):
    ...

    def get_context_data(self, **kwargs):
        return {
            **super().get_context_data(**kwargs),
            **admin.site.each_context(self.request),
            "opts": self.model._meta,
        }
Enter fullscreen mode Exit fullscreen mode

This will be enough for the sidebar and change password/logout buttons. Now, let's add the breadcrumbs block to the detail.html. We can take the breadcrumbs block from the admin/change_form.html and modify it like this:

{% load i18n admin_urls %}

{% block breadcrumbs %}
    <div class="breadcrumbs">
        <a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
        &rsaquo; <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
        &rsaquo; <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
        &rsaquo; {{ object }}
    </div>
{% endblock %}
Enter fullscreen mode Exit fullscreen mode

Finally, let's add a product table. We need a total amount of Order, so create a total_amount method for OrderItem and Order:

class Order(models.Model):
    ...

    @property
    def total_amount(self):
        return sum([item.total_amount for item in self.orderitem_set.all()])

class OrderItem(models.Model):
    ...

    @property
    def total_amount(self):
        return self.product.price * self.count
Enter fullscreen mode Exit fullscreen mode

Add the table and the total to the detail.html:

{% block content %} 
    ...

    <table style="width: 100%; margin-bottom: 10px;">
        <thead>
        <tr>
            <td></td>
            <td>Product</td>
            <td>Price</td>
            <td>Count</td>
            <td>Total</td>
        </tr>
        </thead>
        <tbody>
        {% for item in object.orderitem_set.all %}
            <tr>
                <td><img src="{{ item.product.image.url }}" height="50"></td>
                <td>{{ item.product.title }}</td>
                <td>${{ item.product.price }}</td>
                <td>x{{ item.count }}</td>
                <td>${{ item.total_amount }}</td>
            </tr>
        {% endfor %}
        </tbody>
    </table>

    <p style="font-size: 18px;text-align: end;padding-right: 60px;">
        Total: ${{ object.total_amount }}
    </p>

{% endblock %}
Enter fullscreen mode Exit fullscreen mode

Check the final result:

The end

Congratulations! We added a custom page to the Django Admin. This is a way you can customize the Admin Django according to your project needs.

You can find the source code here.

If you need to build a custom web application, check out our website or connect with me directly on LinkedIn.

Top comments (2)

Collapse
 
13hakta profile image
Vitaly Chekryzhev

Thanks, rather detailed article about constructing detail page :)

Collapse
 
jacques3d profile image
Jacques

Thank you for this! exactly what i was looking for!