Introduction
Django admin supports custom views for reports, dashboards, and specialized workflows. By overriding get_urls() on a ModelAdmin or AdminSite, you can add any view to the admin while inheriting its authentication and permission system.
Key Concepts
get_urls(): Method that returns URL patterns for the admin. Override it to add custom routes.
admin_view(): Wrapper that enforces admin authentication and CSRF protection on custom views.
TemplateResponse: Response class that uses Django's template system for rendering.
each_context(): Provides standard admin context variables (site_header, etc.) to custom templates.
Real World Context
A marketing team wants a dashboard inside the admin showing article publication statistics, top authors, and content gaps. Instead of building a separate analytics application, a custom statistics_view on ArticleAdmin renders a TemplateResponse with aggregated data, giving the team real-time insights without leaving the admin.
Deep Dive
Django admin allows you to add custom views for reports, dashboards, and specialized functionality.
Custom View on ModelAdmin
pythonfrom django.contrib import admin from django.urls import path from django.template.response import TemplateResponse from django.shortcuts import get_object_or_404 from .models import Article @admin.register(Article) class ArticleAdmin(admin.ModelAdmin): list_display = ['title', 'status', 'pub_date'] def get_urls(self): urls = super().get_urls() custom_urls = [ path( 'statistics/', self.admin_site.admin_view(self.statistics_view), name='article_statistics', ), path( '<int:pk>/preview/', self.admin_site.admin_view(self.preview_view), name='article_preview', ), ] return custom_urls + urls def statistics_view(self, request): """Custom statistics page.""" context = { **self.admin_site.each_context(request), 'title': 'Article Statistics', 'total_articles': Article.objects.count(), 'published': Article.objects.filter(status='published').count(), 'drafts': Article.objects.filter(status='draft').count(), 'by_author': Article.objects.values('author__username').annotate( count=Count('id') ).order_by('-count')[:10], } return TemplateResponse( request, 'admin/blog/article/statistics.html', context ) def preview_view(self, request, pk): """Preview an article.""" article = get_object_or_404(Article, pk=pk) context = { **self.admin_site.each_context(request), 'title': f'Preview: {article.title}', 'article': article, 'opts': self.model._meta, } return TemplateResponse( request, 'admin/blog/article/preview.html', context )
Template for Custom View
html<!-- templates/admin/blog/article/statistics.html --> {% extends "admin/base_site.html" %} {% load static %} {% block content %} <h1>{{ title }}</h1> <div class="dashboard-stats"> <div class="stat-card"> <h3>Total Articles</h3> <span class="stat-number">{{ total_articles }}</span> </div> <div class="stat-card"> <h3>Published</h3> <span class="stat-number">{{ published }}</span> </div> <div class="stat-card"> <h3>Drafts</h3> <span class="stat-number">{{ drafts }}</span> </div> </div> <h2>Top Authors</h2> <table> <thead> <tr> <th>Author</th> <th>Articles</th> </tr> </thead> <tbody> {% for author in by_author %} <tr> <td>{{ author.author__username }}</td> <td>{{ author.count }}</td> </tr> {% endfor %} </tbody> </table> {% endblock %}
Adding Links to Custom Views
pythonfrom django.utils.html import format_html from django.urls import reverse @admin.register(Article) class ArticleAdmin(admin.ModelAdmin): list_display = ['title', 'status', 'view_actions'] change_list_template = 'admin/blog/article/change_list.html' @admin.display(description='Actions') def view_actions(self, obj): preview_url = reverse('admin:article_preview', args=[obj.pk]) return format_html( '<a href="{}" class="button">Preview</a>', preview_url )
html<!-- templates/admin/blog/article/change_list.html --> {% extends "admin/change_list.html" %} {% block object-tools-items %} <li> <a href="{% url 'admin:article_statistics' %}" class="viewlink"> View Statistics </a> </li> {{ block.super }} {% endblock %}
Custom Admin Site Views
pythonfrom django.contrib.admin import AdminSite class MyAdminSite(AdminSite): def get_urls(self): urls = super().get_urls() custom_urls = [ path( 'dashboard/', self.admin_view(self.dashboard_view), name='dashboard', ), path( 'reports/', self.admin_view(self.reports_view), name='reports', ), ] return custom_urls + urls def dashboard_view(self, request): from .models import Article, User, Order context = { **self.each_context(request), 'title': 'Dashboard', 'recent_articles': Article.objects.order_by('-created_at')[:5], 'new_users': User.objects.filter( date_joined__gte=timezone.now() - timedelta(days=7) ).count(), 'pending_orders': Order.objects.filter(status='pending').count(), } return TemplateResponse(request, 'admin/dashboard.html', context)
Common Pitfalls
-
Placing custom URLs after super().get_urls(): Django's default URL patterns include catch-all patterns. If your custom URLs come after them, they will never match. Always prepend custom URLs.
-
Forgetting to wrap views with admin_view(): Without self.admin_site.admin_view(), your custom view has no authentication check, making it publicly accessible to anyone who knows the URL.
-
Not including each_context(): Custom views that skip
self.admin_site.each_context(request)in the template context will render without the standard admin header, sidebar, and navigation.
Best Practices
-
Use TemplateResponse instead of render(): TemplateResponse allows middleware to modify the response before rendering, which is the pattern Django admin uses internally.
-
Link custom views from the change list: Add buttons in the object-tools block or use format_html in list_display to make custom views discoverable.
-
Reuse admin base templates: Extend admin/base_site.html so your custom pages get the admin look and feel, including breadcrumbs and navigation.
Summary
- Override get_urls() to add custom URL patterns to any ModelAdmin or AdminSite.
- Wrap custom views with admin_site.admin_view() for authentication.
- Include each_context() in template context for the standard admin chrome.
- Place custom URLs before super().get_urls() to avoid catch-all conflicts.
- Use TemplateResponse and extend admin templates for consistent styling.
Code Examples
class MyModelAdmin(admin.ModelAdmin):
def get_urls(self):
urls = super().get_urls()
custom_urls = [
path('statistics/',
self.admin_site.admin_view(self.statistics_view),
name='model_statistics'),
]
return custom_urls + urls
def statistics_view(self, request):
context = {
**self.admin_site.each_context(request),
'title': 'Statistics',
}
return TemplateResponse(request, 'admin/statistics.html', context)