{"id":909,"date":"2026-05-15T13:54:47","date_gmt":"2026-05-15T05:54:47","guid":{"rendered":"https:\/\/connectword.dpdns.org\/?p=909"},"modified":"2026-05-15T13:54:47","modified_gmt":"2026-05-15T05:54:47","slug":"how-to-build-a-django-unfold-admin-dashboard-with-custom-models-filters-actions-and-kpis","status":"publish","type":"post","link":"https:\/\/connectword.dpdns.org\/?p=909","title":{"rendered":"How to Build a Django-Unfold Admin Dashboard with Custom Models, Filters, Actions, and KPIs"},"content":{"rendered":"<p>In this tutorial, we build an advanced<a href=\"https:\/\/github.com\/unfoldadmin\/django-unfold\"> <strong>Django-Unfold<\/strong><\/a><strong> <\/strong>admin dashboard. We start by installing Django, Django-Unfold, and the required dependencies, then we create a fresh Django project with a shop application. We configure Unfold with a modern admin theme, custom sidebar navigation, dashboard callbacks, product badges, tabs, filters, actions, and a custom admin homepage. We also define realistic e-commerce models such as categories, products, customers, orders, and order items, seed the database with sample data, and launch the Django server through Colab\u2019s proxy so we can access the admin panel from the browser.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\" no-line-numbers\"><code class=\" no-wrap language-php\">import os, sys, shutil, subprocess, time, signal, urllib.request, urllib.error\nfrom pathlib import Path\nprint(\"<img decoding=\"async\" src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/1f4e6.png\" alt=\"\ud83d\udce6\" class=\"wp-smiley\" \/>  Installing django + django-unfold ...\")\nsubprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\",\n               \"django&gt;=5.0,&lt;5.2\", \"django-unfold\", \"Pillow\"], check=True)\nsubprocess.run([\"bash\",\"-c\",\"pkill -9 -f 'manage.py runserver' || true\"])\ntime.sleep(2)\nROOT = Path(\"\/content\/unfold_demo\")\nif ROOT.exists():\n   shutil.rmtree(ROOT)\nROOT.mkdir(parents=True)\nos.chdir(ROOT)\nsubprocess.run([\"django-admin\", \"startproject\", \"config\", \".\"], check=True)\nsubprocess.run([sys.executable, \"manage.py\", \"startapp\", \"shop\"], check=True)\n(ROOT \/ \"templates\" \/ \"admin\").mkdir(parents=True, exist_ok=True)<\/code><\/pre>\n<\/div>\n<\/div>\n<p>We install Django, Django-Unfold, and Pillow so the Colab environment has all the required dependencies for the admin demo. We then stop any previously running Django server to avoid port conflicts. We create a fresh Django project, start the shop app, and prepare the custom admin template directory.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\" no-line-numbers\"><code class=\" no-wrap language-php\">(ROOT \/ \"config\" \/ \"settings.py\").write_text(r'''\nfrom pathlib import Path\nfrom django.urls import reverse_lazy\nfrom django.utils.translation import gettext_lazy as _\nBASE_DIR = Path(__file__).resolve().parent.parent\nSECRET_KEY = \"colab-demo-key-not-for-production\"\nDEBUG = True\nALLOWED_HOSTS = [\"*\"]\nCSRF_TRUSTED_ORIGINS = [\n   \"https:\/\/*.googleusercontent.com\",\n   \"https:\/\/*.colab.research.google.com\",\n   \"https:\/\/*.googleapis.com\",\n   \"https:\/\/*.colab.dev\",\n   \"https:\/\/*.prod.colab.dev\",\n]\nSECURE_PROXY_SSL_HEADER = (\"HTTP_X_FORWARDED_PROTO\", \"https\")\nX_FRAME_OPTIONS = \"ALLOWALL\"\nINSTALLED_APPS = [\n   \"unfold\",\n   \"unfold.contrib.filters\",\n   \"unfold.contrib.forms\",\n   \"unfold.contrib.inlines\",\n   \"django.contrib.admin\",\n   \"django.contrib.auth\",\n   \"django.contrib.contenttypes\",\n   \"django.contrib.sessions\",\n   \"django.contrib.messages\",\n   \"django.contrib.staticfiles\",\n   \"shop\",\n]\nMIDDLEWARE = [\n   \"django.middleware.security.SecurityMiddleware\",\n   \"django.contrib.sessions.middleware.SessionMiddleware\",\n   \"django.middleware.common.CommonMiddleware\",\n   \"django.middleware.csrf.CsrfViewMiddleware\",\n   \"django.contrib.auth.middleware.AuthenticationMiddleware\",\n   \"django.contrib.messages.middleware.MessageMiddleware\",\n   \"django.middleware.clickjacking.XFrameOptionsMiddleware\",\n]\nROOT_URLCONF = \"config.urls\"\nWSGI_APPLICATION = \"config.wsgi.application\"\nTEMPLATES = [{\n   \"BACKEND\": \"django.template.backends.django.DjangoTemplates\",\n   \"DIRS\": [BASE_DIR \/ \"templates\"],\n   \"APP_DIRS\": True,\n   \"OPTIONS\": {\"context_processors\": [\n       \"django.template.context_processors.debug\",\n       \"django.template.context_processors.request\",\n       \"django.contrib.auth.context_processors.auth\",\n       \"django.contrib.messages.context_processors.messages\",\n   ]},\n}]\nDATABASES = {\"default\": {\"ENGINE\": \"django.db.backends.sqlite3\", \"NAME\": BASE_DIR \/ \"db.sqlite3\"}}\nLANGUAGE_CODE, TIME_ZONE, USE_I18N, USE_TZ = \"en-us\", \"UTC\", True, True\nSTATIC_URL, STATIC_ROOT = \"static\/\", BASE_DIR \/ \"staticfiles\"\nMEDIA_URL,  MEDIA_ROOT  = \"media\/\",  BASE_DIR \/ \"media\"\nDEFAULT_AUTO_FIELD = \"django.db.models.BigAutoField\"\nUNFOLD = {\n   \"SITE_TITLE\":   \"Acme Shop Admin\",\n   \"SITE_HEADER\":  \"Acme Shop\",\n   \"SITE_SUBHEADER\": \"Internal back-office\",\n   \"SITE_SYMBOL\":  \"shopping_bag\",\n   \"SHOW_HISTORY\": True,\n   \"SHOW_VIEW_ON_SITE\": True,\n   \"ENVIRONMENT\":        \"shop.utils.environment_callback\",\n   \"DASHBOARD_CALLBACK\": \"shop.utils.dashboard_callback\",\n   \"BORDER_RADIUS\": \"8px\",\n   \"COLORS\": {\n       \"primary\": {\n           \"50\":\"250 245 255\",\"100\":\"243 232 255\",\"200\":\"233 213 255\",\n           \"300\":\"216 180 254\",\"400\":\"192 132 252\",\"500\":\"168 85 247\",\n           \"600\":\"147 51 234\",\"700\":\"126 34 206\",\"800\":\"107 33 168\",\n           \"900\":\"88 28 135\",\"950\":\"59 7 100\",\n       },\n   },\n   \"SIDEBAR\": {\n       \"show_search\": True,\n       \"show_all_applications\": False,\n       \"navigation\": [\n           {\"title\": _(\"Overview\"), \"separator\": True, \"items\": [\n               {\"title\": _(\"Dashboard\"), \"icon\": \"dashboard\",\n                \"link\": reverse_lazy(\"admin:index\")},\n               {\"title\": _(\"Users\"), \"icon\": \"people\",\n                \"link\": reverse_lazy(\"admin:auth_user_changelist\")},\n           ]},\n           {\"title\": _(\"Catalog\"), \"separator\": True, \"collapsible\": True, \"items\": [\n               {\"title\": _(\"Categories\"), \"icon\": \"category\",\n                \"link\": reverse_lazy(\"admin:shop_category_changelist\")},\n               {\"title\": _(\"Products\"), \"icon\": \"inventory_2\",\n                \"link\": reverse_lazy(\"admin:shop_product_changelist\"),\n                \"badge\": \"shop.utils.products_badge\"},\n           ]},\n           {\"title\": _(\"Sales\"), \"separator\": True, \"collapsible\": True, \"items\": [\n               {\"title\": _(\"Orders\"), \"icon\": \"receipt_long\",\n                \"link\": reverse_lazy(\"admin:shop_order_changelist\")},\n               {\"title\": _(\"Customers\"), \"icon\": \"person\",\n                \"link\": reverse_lazy(\"admin:shop_customer_changelist\")},\n           ]},\n       ],\n   },\n   \"TABS\": [{\n       \"models\": [\"shop.product\"],\n       \"items\": [\n           {\"title\": _(\"All products\"),\n            \"link\": reverse_lazy(\"admin:shop_product_changelist\")},\n           {\"title\": _(\"Categories\"),\n            \"link\": reverse_lazy(\"admin:shop_category_changelist\")},\n       ],\n   }],\n}\n''')\n(ROOT \/ \"config\" \/ \"urls.py\").write_text('''\nfrom django.contrib import admin\nfrom django.http import HttpResponseRedirect\nfrom django.urls import path\nfrom django.conf import settings\nfrom django.conf.urls.static import static\nurlpatterns = [\n   path(\"\", lambda r: HttpResponseRedirect(\"\/admin\/\")),\n   path(\"admin\/\", admin.site.urls),\n]\nurlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)\n''')<\/code><\/pre>\n<\/div>\n<\/div>\n<p>We configure the Django settings file with installed apps, middleware, database settings, static\/media paths, and Colab-friendly host and CSRF options. We add Django-Unfold settings to customize the admin title, theme color, sidebar navigation, tabs, dashboard callback, and environment badge. We also define the URL configuration so the root path redirects directly to the admin panel.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\" no-line-numbers\"><code class=\" no-wrap language-php\">(ROOT \/ \"shop\" \/ \"models.py\").write_text('''\nfrom django.db import models\nfrom django.utils.translation import gettext_lazy as _\nclass Category(models.Model):\n   name       = models.CharField(_(\"Name\"), max_length=120)\n   slug       = models.SlugField(unique=True)\n   parent     = models.ForeignKey(\"self\", null=True, blank=True,\n                   on_delete=models.SET_NULL, related_name=\"children\")\n   is_active  = models.BooleanField(default=True)\n   created_at = models.DateTimeField(auto_now_add=True)\n   class Meta: verbose_name_plural = \"Categories\"\n   def __str__(self): return self.name\nclass Customer(models.Model):\n   TIER = [(\"bronze\",\"Bronze\"),(\"silver\",\"Silver\"),\n           (\"gold\",\"Gold\"),(\"platinum\",\"Platinum\")]\n   name           = models.CharField(max_length=120)\n   email          = models.EmailField(unique=True)\n   tier           = models.CharField(max_length=10, choices=TIER, default=\"bronze\")\n   lifetime_value = models.DecimalField(max_digits=10, decimal_places=2, default=0)\n   joined         = models.DateTimeField(auto_now_add=True)\n   def __str__(self): return self.name\nclass Product(models.Model):\n   STATUS = [(\"draft\",\"Draft\"),(\"active\",\"Active\"),(\"archived\",\"Archived\")]\n   category         = models.ForeignKey(Category, on_delete=models.CASCADE,\n                                         related_name=\"products\")\n   name             = models.CharField(max_length=200)\n   sku              = models.CharField(max_length=64, unique=True)\n   description      = models.TextField(blank=True)\n   price            = models.DecimalField(max_digits=10, decimal_places=2)\n   stock            = models.PositiveIntegerField(default=0)\n   status           = models.CharField(max_length=10, choices=STATUS, default=\"draft\")\n   featured         = models.BooleanField(default=False)\n   has_discount     = models.BooleanField(default=False,\n                         help_text=\"Toggle to enable discount field\")\n   discount_percent = models.PositiveIntegerField(default=0)\n   created_at       = models.DateTimeField(auto_now_add=True)\n   updated_at       = models.DateTimeField(auto_now=True)\n   def __str__(self): return self.name\n   @property\n   def final_price(self):\n       if self.has_discount and self.discount_percent:\n           return round(float(self.price)*(1-self.discount_percent\/100), 2)\n       return float(self.price)\nclass Order(models.Model):\n   STATUS = [(\"pending\",\"Pending\"),(\"paid\",\"Paid\"),(\"shipped\",\"Shipped\"),\n             (\"delivered\",\"Delivered\"),(\"cancelled\",\"Cancelled\")]\n   number     = models.CharField(max_length=20, unique=True)\n   customer   = models.ForeignKey(Customer, on_delete=models.PROTECT,\n                                  related_name=\"orders\")\n   status     = models.CharField(max_length=10, choices=STATUS, default=\"pending\")\n   total      = models.DecimalField(max_digits=10, decimal_places=2, default=0)\n   notes      = models.TextField(blank=True)\n   created_at = models.DateTimeField(auto_now_add=True)\n   def __str__(self): return self.number\nclass OrderItem(models.Model):\n   order      = models.ForeignKey(Order, on_delete=models.CASCADE,\n                                  related_name=\"items\")\n   product    = models.ForeignKey(Product, on_delete=models.PROTECT)\n   quantity   = models.PositiveIntegerField(default=1)\n   unit_price = models.DecimalField(max_digits=10, decimal_places=2)\n   position   = models.PositiveIntegerField(default=0)\n   class Meta: ordering = [\"position\"]\n''')\n(ROOT \/ \"shop\" \/ \"utils.py\").write_text('''\nfrom django.db.models import Count, Sum\nfrom django.utils import timezone\nfrom datetime import timedelta\ndef environment_callback(request):\n   return [\"Development\", \"warning\"]\ndef products_badge(request):\n   from .models import Product\n   n = Product.objects.filter(status=\"active\").count()\n   return n if n else None\ndef dashboard_callback(request, context):\n   from .models import Product, Order, Customer, Category\n   last30 = timezone.now() - timedelta(days=30)\n   revenue = Order.objects.filter(\n       created_at__gte=last30,\n       status__in=[\"paid\",\"shipped\",\"delivered\"],\n   ).aggregate(s=Sum(\"total\"))[\"s\"] or 0\n   context.update({\n       \"kpis\": [\n           {\"title\":\"Active products\",\"value\":Product.objects.filter(status=\"active\").count(),\"footer\":\"in catalog\"},\n           {\"title\":\"Pending orders\",\"value\":Order.objects.filter(status=\"pending\").count(),\"footer\":\"awaiting payment\"},\n           {\"title\":\"Customers\",\"value\":Customer.objects.count(),\"footer\":\"registered\"},\n           {\"title\":\"Revenue (30d)\",\"value\":f\"${revenue}\",\"footer\":\"last 30 days\"},\n       ],\n       \"top_cats\": list(Category.objects.annotate(n=Count(\"products\"))\n                       .order_by(\"-n\")[:5].values(\"name\",\"n\")),\n       \"by_status\": list(Order.objects.values(\"status\").annotate(c=Count(\"id\"))),\n   })\n   return context\n''')<\/code><\/pre>\n<\/div>\n<\/div>\n<p>We define the core e-commerce models for categories, customers, products, orders, and order items. We add useful fields such as product status, stock, discounts, customer tiers, order totals, and order statuses. We also create utility callbacks for the Unfold dashboard, product badge, environment label, KPI cards, top categories, and order status summaries.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\" no-line-numbers\"><code class=\" no-wrap language-php\">(ROOT \/ \"shop\" \/ \"admin.py\").write_text('''\nfrom django.contrib import admin, messages\nfrom django.contrib.auth.admin import (UserAdmin as DjangoUserAdmin,\n                                      GroupAdmin as DjangoGroupAdmin)\nfrom django.contrib.auth.models import User, Group\nfrom django.shortcuts import redirect\nfrom django.utils.html import format_html\nfrom django.utils.translation import gettext_lazy as _\nfrom unfold.admin import ModelAdmin, TabularInline\nfrom unfold.contrib.filters.admin import (\n   ChoicesDropdownFilter, RangeNumericFilter, RangeDateFilter,\n   MultipleChoicesDropdownFilter,\n)\nfrom unfold.decorators import display, action\nfrom .models import Category, Customer, Product, Order, OrderItem\nadmin.site.unregister(User); admin.site.unregister(Group)\n@admin.register(User)\nclass UserAdmin(DjangoUserAdmin, ModelAdmin):\n   pass\n@admin.register(Group)\nclass GroupAdmin(DjangoGroupAdmin, ModelAdmin):\n   pass\n@admin.register(Category)\nclass CategoryAdmin(ModelAdmin):\n   list_display = (\"name\", \"parent\", \"show_active\", \"created_at\")\n   list_filter  = ((\"is_active\", ChoicesDropdownFilter),)\n   search_fields = (\"name\", \"slug\")\n   prepopulated_fields = {\"slug\": (\"name\",)}\n   list_filter_submit = True\n   compressed_fields = True\n   @display(description=_(\"Active\"), boolean=True)\n   def show_active(self, obj): return obj.is_active\n@admin.register(Customer)\nclass CustomerAdmin(ModelAdmin):\n   list_display = (\"name\",\"email\",\"show_tier\",\"lifetime_value\",\"joined\")\n   list_filter  = (\n       (\"tier\",            MultipleChoicesDropdownFilter),\n       (\"lifetime_value\",  RangeNumericFilter),\n       (\"joined\",          RangeDateFilter),\n   )\n   search_fields = (\"name\",\"email\")\n   list_filter_submit = True\n   warn_unsaved_form  = True\n   list_per_page = 25\n   @display(description=_(\"Tier\"), label={\n       \"bronze\":\"warning\",\"silver\":\"info\",\"gold\":\"success\",\"platinum\":\"primary\"})\n   def show_tier(self, obj):\n       return obj.get_tier_display(), obj.tier\nclass OrderItemInline(TabularInline):\n   model = OrderItem\n   extra = 0\n   fields = (\"product\", \"quantity\", \"unit_price\", \"position\")\n   ordering_field = \"position\"\n   tab = True\n@admin.register(Order)\nclass OrderAdmin(ModelAdmin):\n   list_display = (\"number\",\"customer_link\",\"show_status\",\"total\",\"created_at\")\n   list_filter  = (\n       (\"status\",     ChoicesDropdownFilter),\n       (\"total\",      RangeNumericFilter),\n       (\"created_at\", RangeDateFilter),\n   )\n   search_fields = (\"number\",\"customer__name\",\"customer__email\")\n   readonly_fields = (\"created_at\",)\n   autocomplete_fields = (\"customer\",)\n   inlines = [OrderItemInline]\n   list_filter_submit = True\n   fieldsets = (\n       (_(\"Order\"), {\"classes\":[\"tab\"], \"fields\":(\"number\",\"customer\",\"status\",\"total\")}),\n       (_(\"Notes\"), {\"classes\":[\"tab\"], \"fields\":(\"notes\",\"created_at\")}),\n   )\n   actions_list        = [\"mark_paid_bulk\"]\n   actions_row         = [\"mark_paid_row\"]\n   actions_detail      = [\"duplicate_order\"]\n   actions_submit_line = [\"save_and_ship\"]\n   @display(description=_(\"Status\"), label={\n       \"pending\":\"warning\",\"paid\":\"info\",\"shipped\":\"primary\",\n       \"delivered\":\"success\",\"cancelled\":\"danger\"})\n   def show_status(self, obj):\n       return obj.get_status_display(), obj.status\n   @display(description=_(\"Customer\"))\n   def customer_link(self, obj):\n       return format_html('&lt;a href=\"\/admin\/shop\/customer\/{}\/change\/\"&gt;{}&lt;\/a&gt;',\n                          obj.customer_id, obj.customer.name)\n   @action(description=_(\"Mark pending \u2192 PAID (all)\"), icon=\"payments\")\n   def mark_paid_bulk(self, request, queryset=None):\n       n = Order.objects.filter(status=\"pending\").update(status=\"paid\")\n       self.message_user(request, f\"Marked {n} orders as paid.\", level=messages.SUCCESS)\n   @action(description=_(\"Mark paid\"), icon=\"payments\", url_path=\"mark-paid-row\")\n   def mark_paid_row(self, request, object_id):\n       Order.objects.filter(pk=object_id).update(status=\"paid\")\n       self.message_user(request, \"Order marked as paid.\", level=messages.SUCCESS)\n       return redirect(request.META.get(\"HTTP_REFERER\",\"\/admin\/\"))\n   @action(description=_(\"Duplicate\"), icon=\"content_copy\", url_path=\"duplicate\")\n   def duplicate_order(self, request, object_id):\n       o = Order.objects.get(pk=object_id)\n       o.pk = None; o.number = o.number + \"-COPY\"; o.status = \"pending\"; o.save()\n       self.message_user(request, \"Order duplicated.\", level=messages.SUCCESS)\n       return redirect(f\"\/admin\/shop\/order\/{o.pk}\/change\/\")\n   @action(description=_(\"Save &amp; ship\"))\n   def save_and_ship(self, request, obj):\n       obj.status = \"shipped\"; obj.save()\n       self.message_user(request, f\"Order {obj.number} shipped.\", level=messages.SUCCESS)\n@admin.register(Product)\nclass ProductAdmin(ModelAdmin):\n   list_display = (\"name\",\"sku\",\"category\",\"show_status\",\n                   \"price_display\",\"stock_badge\",\"featured\")\n   list_editable = (\"featured\",)\n   list_filter   = (\n       (\"status\",   ChoicesDropdownFilter),\n       (\"category\", admin.RelatedFieldListFilter),\n       (\"price\",    RangeNumericFilter),\n       (\"featured\", ChoicesDropdownFilter),\n   )\n   search_fields = (\"name\",\"sku\")\n   autocomplete_fields = (\"category\",)\n   list_filter_submit = True\n   list_per_page = 20\n   save_on_top = True\n   fieldsets = (\n       (_(\"Basics\"),  {\"classes\":[\"tab\"],\n                       \"fields\":(\"name\",\"sku\",\"category\",\"status\",\"featured\")}),\n       (_(\"Pricing\"), {\"classes\":[\"tab\"],\n                       \"fields\":(\"price\",\"has_discount\",\"discount_percent\",\"stock\")}),\n       (_(\"Content\"), {\"classes\":[\"tab\"], \"fields\":(\"description\",)}),\n   )\n   conditional_fields = {\"discount_percent\": \"has_discount == true\"}\n   @display(description=_(\"Status\"), label={\n       \"draft\":\"info\",\"active\":\"success\",\"archived\":\"warning\"})\n   def show_status(self, obj):\n       return obj.get_status_display(), obj.status\n   @display(description=_(\"Price\"))\n   def price_display(self, obj):\n       if obj.has_discount and obj.discount_percent:\n           return format_html(\n               '&lt;span style=\"text-decoration:line-through;opacity:.6\"&gt;${}&lt;\/span&gt; '\n               '&lt;strong&gt;${}&lt;\/strong&gt;', obj.price, obj.final_price)\n       return f\"${obj.price}\"\n   @display(description=_(\"Stock\"), ordering=\"stock\",\n            label={\"out\":\"danger\",\"low\":\"warning\",\"ok\":\"success\"})\n   def stock_badge(self, obj):\n       if obj.stock == 0:                  return \"Out of stock\", \"out\"\n       if obj.stock &lt; 10:                  return f\"Low ({obj.stock})\", \"low\"\n       return f\"{obj.stock} in stock\", \"ok\"\n''')\n(ROOT \/ \"templates\" \/ \"admin\" \/ \"index.html\").write_text('''{% extends \"admin\/index.html\" %}\n{% load i18n %}\n{% block content %}\n&lt;div class=\"grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6\"&gt;\n{% for k in kpis %}\n &lt;div class=\"border border-base-200 dark:border-base-800 bg-white dark:bg-base-900 rounded-default p-5 shadow-xs\"&gt;\n   &lt;div class=\"font-medium text-font-subtle-light dark:text-font-subtle-dark text-sm\"&gt;{{ k.title }}&lt;\/div&gt;\n   &lt;div class=\"font-bold text-2xl mt-2 text-font-important-light dark:text-font-important-dark\"&gt;{{ k.value }}&lt;\/div&gt;\n   &lt;div class=\"text-xs mt-1 text-font-default-light dark:text-font-default-dark\"&gt;{{ k.footer }}&lt;\/div&gt;\n &lt;\/div&gt;\n{% endfor %}\n&lt;\/div&gt;\n&lt;div class=\"grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6\"&gt;\n &lt;div class=\"border border-base-200 dark:border-base-800 bg-white dark:bg-base-900 rounded-default p-5\"&gt;\n   &lt;h3 class=\"font-semibold mb-3 text-font-important-light dark:text-font-important-dark\"&gt;{% trans \"Top categories\" %}&lt;\/h3&gt;\n   &lt;ul class=\"space-y-2\"&gt;\n     {% for c in top_cats %}&lt;li class=\"flex justify-between\"&gt;&lt;span&gt;{{ c.name }}&lt;\/span&gt;&lt;span class=\"font-semibold\"&gt;{{ c.n }}&lt;\/span&gt;&lt;\/li&gt;{% endfor %}\n   &lt;\/ul&gt;\n &lt;\/div&gt;\n &lt;div class=\"border border-base-200 dark:border-base-800 bg-white dark:bg-base-900 rounded-default p-5\"&gt;\n   &lt;h3 class=\"font-semibold mb-3 text-font-important-light dark:text-font-important-dark\"&gt;{% trans \"Orders by status\" %}&lt;\/h3&gt;\n   &lt;ul class=\"space-y-2\"&gt;\n     {% for s in by_status %}&lt;li class=\"flex justify-between\"&gt;&lt;span class=\"capitalize\"&gt;{{ s.status }}&lt;\/span&gt;&lt;span class=\"font-semibold\"&gt;{{ s.c }}&lt;\/span&gt;&lt;\/li&gt;{% endfor %}\n   &lt;\/ul&gt;\n &lt;\/div&gt;\n&lt;\/div&gt;\n{{ block.super }}\n{% endblock %}\n''')<\/code><\/pre>\n<\/div>\n<\/div>\n<p>We customize the Django admin using Django-Unfold\u2019s ModelAdmin, filters, labels, tabs, inline order items, and admin actions. We register custom admin views for users, groups, categories, customers, products, and orders with search, filters, badges, and formatted displays. We also create a custom dashboard template that shows KPI cards, top categories, and order status summaries on the admin homepage.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\" no-line-numbers\"><code class=\" no-wrap language-php\">print(\"<img decoding=\"async\" src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/1f6e0.png\" alt=\"\ud83d\udee0\" class=\"wp-smiley\" \/>   Running migrations ...\")\nsubprocess.run([sys.executable, \"manage.py\", \"makemigrations\", \"shop\"], check=True)\nsubprocess.run([sys.executable, \"manage.py\", \"migrate\"], check=True)\n(ROOT \/ \"seed.py\").write_text(r'''\nimport os, django, random, string\nos.environ.setdefault(\"DJANGO_SETTINGS_MODULE\",\"config.settings\"); django.setup()\nfrom django.contrib.auth.models import User\nfrom shop.models import Category, Customer, Product, Order, OrderItem\nfrom decimal import Decimal\nif not User.objects.filter(username=\"admin\").exists():\n   User.objects.create_superuser(\"admin\",\"admin@example.com\",\"admin\")\nif Category.objects.count() == 0:\n   cats=[Category.objects.create(name=n,slug=n.lower())\n         for n in [\"Electronics\",\"Apparel\",\"Home\",\"Books\",\"Toys\"]]\n   Category.objects.create(name=\"Phones\",slug=\"phones\",parent=cats[0])\n   for i in range(30):\n       c=random.choice(list(Category.objects.all()))\n       Product.objects.create(\n           category=c, name=f\"{c.name} item {i+1}\",\n           sku=\"SKU-\"+\"\".join(random.choices(string.ascii_uppercase+string.digits,k=8)),\n           price=Decimal(random.randint(5,500)),\n           stock=random.choice([0,3,12,25,100]),\n           status=random.choice([\"draft\",\"active\",\"active\",\"active\",\"archived\"]),\n           featured=random.random()&lt;0.2, description=\"Sample product description.\")\n   for i in range(15):\n       Customer.objects.create(\n           name=f\"Customer {i+1}\", email=f\"customer{i+1}@example.com\",\n           tier=random.choice([\"bronze\",\"silver\",\"gold\",\"platinum\"]),\n           lifetime_value=Decimal(random.randint(0,5000)))\n   customers=list(Customer.objects.all()); products=list(Product.objects.all())\n   for i in range(40):\n       o=Order.objects.create(number=f\"ORD-{1000+i}\",customer=random.choice(customers),\n           status=random.choice([\"pending\",\"paid\",\"shipped\",\"delivered\",\"cancelled\"]))\n       total=Decimal(0)\n       for j in range(random.randint(1,4)):\n           p=random.choice(products); qty=random.randint(1,3)\n           OrderItem.objects.create(order=o,product=p,quantity=qty,\n                                    unit_price=p.price,position=j)\n           total += p.price*qty\n       o.total=total; o.save()\nprint(\"<img decoding=\"async\" src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/2705.png\" alt=\"\u2705\" class=\"wp-smiley\" \/>  Seed complete.\")\n''')\nsubprocess.run([sys.executable, \"seed.py\"], check=True)\nprint(\"<img decoding=\"async\" src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/1f680.png\" alt=\"\ud83d\ude80\" class=\"wp-smiley\" \/>  Starting dev server on :8000 ...\")\nLOG = \"\/content\/server.log\"\nlog_fh = open(LOG, \"wb\")\nproc = subprocess.Popen(\n   [sys.executable, \"manage.py\", \"runserver\", \"0.0.0.0:8000\", \"--noreload\"],\n   stdout=log_fh, stderr=log_fh, preexec_fn=os.setsid)\nprint(\"<img decoding=\"async\" src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/1f50e.png\" alt=\"\ud83d\udd0e\" class=\"wp-smiley\" \/>  Verifying Django is responding ...\")\nok = False\nfor attempt in range(15):\n   try:\n       r = urllib.request.urlopen(\"http:\/\/127.0.0.1:8000\/admin\/login\/\", timeout=3)\n       print(f\"<img decoding=\"async\" src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/2705.png\" alt=\"\u2705\" class=\"wp-smiley\" \/>  HTTP {r.status} from \/admin\/login\/\")\n       ok = True; break\n   except urllib.error.HTTPError as e:\n       print(f\"<img decoding=\"async\" src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/2705.png\" alt=\"\u2705\" class=\"wp-smiley\" \/>  HTTP {e.code} from \/admin\/login\/\")\n       ok = True; break\n   except Exception:\n       time.sleep(1)\nif not ok:\n   print(\"n<img decoding=\"async\" src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/274c.png\" alt=\"\u274c\" class=\"wp-smiley\" \/>  Django did NOT respond. Server log:n\" + \"-\"*50)\n   print(open(LOG).read())\n   print(\"-\"*50)\n   raise SystemExit(1)\nfrom google.colab.output import eval_js\nfrom IPython.display import display, HTML\nproxy_root = eval_js(\"google.colab.kernel.proxyPort(8000)\")\nadmin_url  = proxy_root.rstrip(\"\/\") + \"\/admin\/login\/\"\ndisplay(HTML(f'''\n&lt;div style=\"padding:16px;border:2px solid #9333ea;border-radius:8px;background:#faf5ff;font-family:system-ui\"&gt;\n &lt;h2 style=\"margin:0 0 8px 0;color:#6b21a8\"&gt;<img decoding=\"async\" src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/2705.png\" alt=\"\u2705\" class=\"wp-smiley\" \/> Django-Unfold demo is ready&lt;\/h2&gt;\n &lt;p style=\"margin:4px 0\"&gt;Login: &lt;code style=\"background:#fff;padding:2px 6px;border-radius:4px\"&gt;admin&lt;\/code&gt; \/ &lt;code style=\"background:#fff;padding:2px 6px;border-radius:4px\"&gt;admin&lt;\/code&gt;&lt;\/p&gt;\n &lt;p style=\"margin:12px 0\"&gt;\n   <img decoding=\"async\" src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/1f449.png\" alt=\"\ud83d\udc49\" class=\"wp-smiley\" \/> &lt;a href=\"{admin_url}\" target=\"_blank\" style=\"font-size:18px;font-weight:bold;color:#7e22ce\"&gt;\n     Open the admin\n   &lt;\/a&gt;\n &lt;\/p&gt;\n &lt;p style=\"margin:4px 0;font-size:12px;color:#6b7280\"&gt;If the link 404s, try copy-pasting it manually:&lt;br&gt;&lt;code&gt;{admin_url}&lt;\/code&gt;&lt;\/p&gt;\n&lt;\/div&gt;\n'''))\nprint(f\"nProxy root: {proxy_root}\")\nprint(f\"Admin URL:  {admin_url}\")\nprint(f\"nTo stop later:  import os, signal; os.killpg({proc.pid}, signal.SIGTERM)\")<\/code><\/pre>\n<\/div>\n<\/div>\n<p>We run migrations to create the database tables for the Django project and the shop app. We seed the database with an admin user, sample categories, products, customers, orders, and order items. We then start the Django development server, verify that the admin login page responds, and generate a Colab proxy link to open the Unfold admin dashboard.<\/p>\n<p>In conclusion, we had a fully working Django-Unfold admin interface running with seeded e-commerce data and a polished dashboard experience. We used Unfold to transform the default Django admin into a more professional back-office system with custom navigation, visual labels, filters, inline order items, admin actions, conditional fields, and KPI cards. It provides a practical foundation for building modern internal tools, admin panels, and business dashboards with Django, while keeping the setup simple, reproducible, and Colab-friendly.<\/p>\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n<p>Check out\u00a0the\u00a0<strong><a href=\"https:\/\/github.com\/Marktechpost\/AI-Agents-Projects-Tutorials\/blob\/main\/AI%20Agents%20Codes\/django_unfold_admin_dashboard_Marktechpost.ipynb\" target=\"_blank\" rel=\"noreferrer noopener\">Full Codes with Notebook here<\/a>.\u00a0<\/strong>Also,\u00a0feel free to follow us on\u00a0<strong><a href=\"https:\/\/x.com\/intent\/follow?screen_name=marktechpost\" target=\"_blank\" rel=\"noreferrer noopener\"><mark>Twitter<\/mark><\/a><\/strong>\u00a0and don\u2019t forget to join our\u00a0<strong><a href=\"https:\/\/www.reddit.com\/r\/machinelearningnews\/\" target=\"_blank\" rel=\"noreferrer noopener\">150k+ ML SubReddit<\/a><\/strong>\u00a0and Subscribe to\u00a0<strong><a href=\"https:\/\/www.aidevsignals.com\/\" target=\"_blank\" rel=\"noreferrer noopener\">our Newsletter<\/a><\/strong>. Wait! are you on telegram?\u00a0<strong><a href=\"https:\/\/t.me\/machinelearningresearchnews\" target=\"_blank\" rel=\"noreferrer noopener\">now you can join us on telegram as well.<\/a><\/strong><\/p>\n<p>Need to partner with us for promoting your GitHub Repo OR Hugging Face Page OR Product Release OR Webinar etc.?\u00a0<strong><a href=\"https:\/\/forms.gle\/MTNLpmJtsFA3VRVd9\" target=\"_blank\" rel=\"noreferrer noopener\"><mark>Connect with us<\/mark><\/a><\/strong><\/p>\n<p>The post <a href=\"https:\/\/www.marktechpost.com\/2026\/05\/14\/how-to-build-a-django-unfold-admin-dashboard-with-custom-models-filters-actions-and-kpis\/\">How to Build a Django-Unfold Admin Dashboard with Custom Models, Filters, Actions, and KPIs<\/a> appeared first on <a href=\"https:\/\/www.marktechpost.com\/\">MarkTechPost<\/a>.<\/p>","protected":false},"excerpt":{"rendered":"<p>In this tutorial, we build an &hellip;<\/p>\n","protected":false},"author":1,"featured_media":29,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-909","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=\/wp\/v2\/posts\/909","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=909"}],"version-history":[{"count":0,"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=\/wp\/v2\/posts\/909\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=\/wp\/v2\/media\/29"}],"wp:attachment":[{"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=909"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=909"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=909"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}