How to Build a Django-Unfold Admin Dashboard with Custom Models, Filters, Actions, and KPIs

Editor
5 Min Read


(ROOT / "shop" / "admin.py").write_text('''
from django.contrib import admin, messages
from django.contrib.auth.admin import (UserAdmin as DjangoUserAdmin,
                                      GroupAdmin as DjangoGroupAdmin)
from django.contrib.auth.models import User, Group
from django.shortcuts import redirect
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from unfold.admin import ModelAdmin, TabularInline
from unfold.contrib.filters.admin import (
   ChoicesDropdownFilter, RangeNumericFilter, RangeDateFilter,
   MultipleChoicesDropdownFilter,
)
from unfold.decorators import display, action
from .models import Category, Customer, Product, Order, OrderItem
admin.site.unregister(User); admin.site.unregister(Group)
@admin.register(User)
class UserAdmin(DjangoUserAdmin, ModelAdmin):
   pass
@admin.register(Group)
class GroupAdmin(DjangoGroupAdmin, ModelAdmin):
   pass
@admin.register(Category)
class CategoryAdmin(ModelAdmin):
   list_display = ("name", "parent", "show_active", "created_at")
   list_filter  = (("is_active", ChoicesDropdownFilter),)
   search_fields = ("name", "slug")
   prepopulated_fields = {"slug": ("name",)}
   list_filter_submit = True
   compressed_fields = True
   @display(description=_("Active"), boolean=True)
   def show_active(self, obj): return obj.is_active
@admin.register(Customer)
class CustomerAdmin(ModelAdmin):
   list_display = ("name","email","show_tier","lifetime_value","joined")
   list_filter  = (
       ("tier",            MultipleChoicesDropdownFilter),
       ("lifetime_value",  RangeNumericFilter),
       ("joined",          RangeDateFilter),
   )
   search_fields = ("name","email")
   list_filter_submit = True
   warn_unsaved_form  = True
   list_per_page = 25
   @display(description=_("Tier"), label={
       "bronze":"warning","silver":"info","gold":"success","platinum":"primary"})
   def show_tier(self, obj):
       return obj.get_tier_display(), obj.tier
class OrderItemInline(TabularInline):
   model = OrderItem
   extra = 0
   fields = ("product", "quantity", "unit_price", "position")
   ordering_field = "position"
   tab = True
@admin.register(Order)
class OrderAdmin(ModelAdmin):
   list_display = ("number","customer_link","show_status","total","created_at")
   list_filter  = (
       ("status",     ChoicesDropdownFilter),
       ("total",      RangeNumericFilter),
       ("created_at", RangeDateFilter),
   )
   search_fields = ("number","customer__name","customer__email")
   readonly_fields = ("created_at",)
   autocomplete_fields = ("customer",)
   inlines = [OrderItemInline]
   list_filter_submit = True
   fieldsets = (
       (_("Order"), {"classes":["tab"], "fields":("number","customer","status","total")}),
       (_("Notes"), {"classes":["tab"], "fields":("notes","created_at")}),
   )
   actions_list        = ["mark_paid_bulk"]
   actions_row         = ["mark_paid_row"]
   actions_detail      = ["duplicate_order"]
   actions_submit_line = ["save_and_ship"]
   @display(description=_("Status"), label={
       "pending":"warning","paid":"info","shipped":"primary",
       "delivered":"success","cancelled":"danger"})
   def show_status(self, obj):
       return obj.get_status_display(), obj.status
   @display(description=_("Customer"))
   def customer_link(self, obj):
       return format_html(\'<a href="https://www.marktechpost.com/admin/shop/customer/{}/change/">{}</a>\',
                          obj.customer_id, obj.customer.name)
   @action(description=_("Mark pending → PAID (all)"), icon="payments")
   def mark_paid_bulk(self, request, queryset=None):
       n = Order.objects.filter(status="pending").update(status="paid")
       self.message_user(request, f"Marked {n} orders as paid.", level=messages.SUCCESS)
   @action(description=_("Mark paid"), icon="payments", url_path="mark-paid-row")
   def mark_paid_row(self, request, object_id):
       Order.objects.filter(pk=object_id).update(status="paid")
       self.message_user(request, "Order marked as paid.", level=messages.SUCCESS)
       return redirect(request.META.get("HTTP_REFERER","/admin/"))
   @action(description=_("Duplicate"), icon="content_copy", url_path="duplicate")
   def duplicate_order(self, request, object_id):
       o = Order.objects.get(pk=object_id)
       o.pk = None; o.number = o.number + "-COPY"; o.status = "pending"; o.save()
       self.message_user(request, "Order duplicated.", level=messages.SUCCESS)
       return redirect(f"/admin/shop/order/{o.pk}/change/")
   @action(description=_("Save & ship"))
   def save_and_ship(self, request, obj):
       obj.status = "shipped"; obj.save()
       self.message_user(request, f"Order {obj.number} shipped.", level=messages.SUCCESS)
@admin.register(Product)
class ProductAdmin(ModelAdmin):
   list_display = ("name","sku","category","show_status",
                   "price_display","stock_badge","featured")
   list_editable = ("featured",)
   list_filter   = (
       ("status",   ChoicesDropdownFilter),
       ("category", admin.RelatedFieldListFilter),
       ("price",    RangeNumericFilter),
       ("featured", ChoicesDropdownFilter),
   )
   search_fields = ("name","sku")
   autocomplete_fields = ("category",)
   list_filter_submit = True
   list_per_page = 20
   save_on_top = True
   fieldsets = (
       (_("Basics"),  {"classes":["tab"],
                       "fields":("name","sku","category","status","featured")}),
       (_("Pricing"), {"classes":["tab"],
                       "fields":("price","has_discount","discount_percent","stock")}),
       (_("Content"), {"classes":["tab"], "fields":("description",)}),
   )
   conditional_fields = {"discount_percent": "has_discount == true"}
   @display(description=_("Status"), label={
       "draft":"info","active":"success","archived":"warning"})
   def show_status(self, obj):
       return obj.get_status_display(), obj.status
   @display(description=_("Price"))
   def price_display(self, obj):
       if obj.has_discount and obj.discount_percent:
           return format_html(
               \'<span style="text-decoration:line-through;opacity:.6">${}</span> \'
               \'<strong>${}</strong>\', obj.price, obj.final_price)
       return f"${obj.price}"
   @display(description=_("Stock"), ordering="stock",
            label={"out":"danger","low":"warning","ok":"success"})
   def stock_badge(self, obj):
       if obj.stock == 0:                  return "Out of stock", "out"
       if obj.stock < 10:                  return f"Low ({obj.stock})", "low"
       return f"{obj.stock} in stock", "ok"
''')
(ROOT / "templates" / "admin" / "index.html").write_text('''{% extends "admin/index.html" %}
{% load i18n %}
{% block content %}
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
{% for k in kpis %}
 <div class="border border-base-200 dark:border-base-800 bg-white dark:bg-base-900 rounded-default p-5 shadow-xs">
   <div class="font-medium text-font-subtle-light dark:text-font-subtle-dark text-sm">{{ k.title }}</div>
   <div class="font-bold text-2xl mt-2 text-font-important-light dark:text-font-important-dark">{{ k.value }}</div>
   <div class="text-xs mt-1 text-font-default-light dark:text-font-default-dark">{{ k.footer }}</div>
 </div>
{% endfor %}
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
 <div class="border border-base-200 dark:border-base-800 bg-white dark:bg-base-900 rounded-default p-5">
   <h3 class="font-semibold mb-3 text-font-important-light dark:text-font-important-dark">{% trans "Top categories" %}</h3>
   <ul class="space-y-2">
     {% for c in top_cats %}<li class="flex justify-between"><span>{{ c.name }}</span><span class="font-semibold">{{ c.n }}</span></li>{% endfor %}
   </ul>
 </div>
 <div class="border border-base-200 dark:border-base-800 bg-white dark:bg-base-900 rounded-default p-5">
   <h3 class="font-semibold mb-3 text-font-important-light dark:text-font-important-dark">{% trans "Orders by status" %}</h3>
   <ul class="space-y-2">
     {% for s in by_status %}<li class="flex justify-between"><span class="capitalize">{{ s.status }}</span><span class="font-semibold">{{ s.c }}</span></li>{% endfor %}
   </ul>
 </div>
</div>
{{ block.super }}
{% endblock %}
''')
Share this Article
Please enter CoinGecko Free Api Key to get this plugin works.