Free Ebook cover Django Fundamentals: From First App to a Complete Backend

Django Fundamentals: From First App to a Complete Backend

New course

12 pages

Django Admin: Fast Back-Office Interfaces with Proper Configuration

Capítulo 5

Estimated reading time: 11 minutes

+ Exercise

Why Django Admin Matters for Back-Office Work

Django admin is a built-in back-office interface for managing your application data. It can be production-grade when you configure it for real workflows: fast navigation, safe edits, strong validation, and permissions that match who should be able to do what. The goal is not “expose every field,” but to create an interface that helps staff do routine tasks quickly without breaking data integrity.

Prerequisites and Setup

To use admin, ensure the admin app is enabled and you have a staff user. In most projects, these are already present, but verify:

  • django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles are in INSTALLED_APPS.
  • The admin URLs are included in your root URL configuration.
  • You have a user with is_staff=True (and typically is_superuser=True for initial setup).
# project/urls.py (example snippet)
from django.contrib import admin
from django.urls import path

urlpatterns = [
    path('admin/', admin.site.urls),
]

Once you can log in to /admin/, the next step is making it usable.

Registering Models: The Smallest Useful Admin

Admin only shows models you register. Start with a minimal registration, then iterate.

# app/admin.py
from django.contrib import admin
from .models import Product

admin.site.register(Product)

This gives you CRUD screens, but the list view will be slow to scan and hard to search. That’s where ModelAdmin configuration comes in.

Continue in our app.

You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.

Or continue reading below...
Download App

Download the app

Configuring the List View for Speed and Clarity

list_display: Show the Columns Staff Actually Needs

list_display controls which columns appear in the changelist (the table view). Pick fields that help staff identify records quickly and avoid overly wide tables.

from django.contrib import admin
from .models import Product

@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
    list_display = ('sku', 'name', 'status', 'price', 'updated_at')

You can also display computed values by adding a method on the admin class.

@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
    list_display = ('sku', 'name', 'status', 'price', 'inventory_badge')

    @admin.display(description='Inventory', ordering='stock')
    def inventory_badge(self, obj):
        if obj.stock <= 0:
            return 'Out of stock'
        if obj.stock < 5:
            return 'Low'
        return 'OK'

@admin.display lets you set a human label and enable sorting by a real field via ordering=.

ordering: Default Sort That Matches Workflow

Staff often expects newest-first or “needs attention first.” Set a sensible default ordering.

class ProductAdmin(admin.ModelAdmin):
    ordering = ('-updated_at',)

search_fields: Fast Lookups Without Scrolling

search_fields adds a search box. Use identifiers and human-facing fields. For related fields, use Django’s double-underscore traversal.

class ProductAdmin(admin.ModelAdmin):
    search_fields = ('sku', 'name', 'category__name')

For prefix searches (useful for codes), you can use ^ to anchor at the start of the field.

class ProductAdmin(admin.ModelAdmin):
    search_fields = ('^sku', 'name')

list_filter: One-Click Narrowing

list_filter adds sidebar filters. Use it for status fields, booleans, and dates. It reduces mistakes by making it easy to focus on a subset (for example, “Draft” items only).

class ProductAdmin(admin.ModelAdmin):
    list_filter = ('status', 'created_at')

For related models, you can filter by foreign key fields too:

class ProductAdmin(admin.ModelAdmin):
    list_filter = ('status', 'category')

list_select_related: Prevent N+1 Queries in the Changelist

If your list_display includes foreign keys, admin may trigger extra queries. Use list_select_related to prefetch related objects and keep the list view fast.

class ProductAdmin(admin.ModelAdmin):
    list_display = ('sku', 'name', 'category', 'status')
    list_select_related = ('category',)

Configuring the Edit Form for Safe, Efficient Data Entry

Meaningful Labels and Help Text

Admin uses your model field metadata. Good verbose_name and help_text reduce training time and prevent incorrect data entry.

# models.py (example snippet)
class Product(models.Model):
    sku = models.CharField(max_length=32, unique=True, verbose_name='SKU', help_text='Unique stock keeping unit, e.g. ABC-123')
    status = models.CharField(max_length=16, choices=Status.choices, default=Status.DRAFT, help_text='Draft items are not visible to customers')

readonly_fields: Protect System-Managed Values

Fields like timestamps, audit info, or computed values should often be read-only in admin to preserve integrity.

class ProductAdmin(admin.ModelAdmin):
    readonly_fields = ('created_at', 'updated_at')

If you want to display a computed field as read-only, add a method name to readonly_fields.

class ProductAdmin(admin.ModelAdmin):
    readonly_fields = ('created_at', 'updated_at', 'public_url')

    @admin.display(description='Public URL')
    def public_url(self, obj):
        return obj.get_absolute_url()

fields and fieldsets: Make Forms Match Real Tasks

Default admin forms can be long and confusing. Use fields for a simple ordering, or fieldsets to group related inputs.

class ProductAdmin(admin.ModelAdmin):
    fieldsets = (
        ('Core info', {'fields': ('sku', 'name', 'status', 'category')}),
        ('Pricing', {'fields': ('price', 'tax_rate')}),
        ('Inventory', {'fields': ('stock',)}),
        ('System', {'fields': ('created_at', 'updated_at'), 'classes': ('collapse',)}),
    )
    readonly_fields = ('created_at', 'updated_at')

Collapsing “System” fields keeps the form focused.

Validation: Enforce Integrity at the Model Layer

Admin is just one entry point to your data. Validation should live in models so it applies everywhere (admin, scripts, APIs). Use model clean() for cross-field rules and field validators for single-field rules.

from django.core.exceptions import ValidationError

class Product(models.Model):
    price = models.DecimalField(max_digits=10, decimal_places=2)
    tax_rate = models.DecimalField(max_digits=4, decimal_places=2)

    def clean(self):
        if self.price is not None and self.price < 0:
            raise ValidationError({'price': 'Price cannot be negative.'})
        if self.tax_rate is not None and not (0 <= self.tax_rate <= 1):
            raise ValidationError({'tax_rate': 'Tax rate must be between 0 and 1 (e.g. 0.20).'} )

To ensure admin triggers model validation, keep using standard admin save flow. If you override admin save methods, be careful not to bypass validation.

Inline Related Models: Edit Parent and Children Together

Inlines let staff manage related rows on the same page, which is ideal for “order with line items,” “product with images,” or “article with attachments.”

Step-by-step: Add an Inline

  • Create an InlineModelAdmin (tabular or stacked).
  • Attach it to the parent ModelAdmin via inlines.
  • Consider read-only fields and extra empty forms.
# admin.py
from django.contrib import admin
from .models import Order, OrderItem

class OrderItemInline(admin.TabularInline):
    model = OrderItem
    extra = 0
    autocomplete_fields = ('product',)
    readonly_fields = ('line_total',)

    @admin.display(description='Line total')
    def line_total(self, obj):
        return obj.quantity * obj.unit_price

@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    list_display = ('id', 'customer', 'status', 'created_at')
    list_filter = ('status', 'created_at')
    search_fields = ('id', 'customer__email')
    inlines = (OrderItemInline,)

autocomplete_fields is especially useful when the related table is large, preventing huge dropdowns and speeding up the form.

Admin Actions: Safe Bulk Operations

Actions allow staff to apply a change to multiple selected rows. They are powerful and risky, so design them with guardrails: permissions, clear names, and minimal side effects.

Example: Mark Products as Archived

# admin.py
from django.contrib import admin, messages
from django.utils import timezone
from .models import Product

@admin.action(description='Archive selected products')
def archive_products(modeladmin, request, queryset):
    updated = queryset.update(status='archived', updated_at=timezone.now())
    modeladmin.message_user(request, f'Archived {updated} products.', level=messages.SUCCESS)

@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
    actions = (archive_products,)

Prefer queryset.update() for simple field updates (fast), but if you need per-object logic (signals, validations, audit logs), iterate and call save() carefully.

Action Permissions and Safety Checks

You can restrict actions based on permissions or business rules.

@admin.action(description='Archive selected products')
def archive_products(modeladmin, request, queryset):
    if not request.user.has_perm('app.change_product'):
        modeladmin.message_user(request, 'You do not have permission to archive products.', level=messages.ERROR)
        return
    # Example rule: do not archive featured products
    blocked = queryset.filter(is_featured=True).count()
    if blocked:
        modeladmin.message_user(request, f'{blocked} featured products were not archived.', level=messages.WARNING)
    qs = queryset.filter(is_featured=False)
    updated = qs.update(status='archived')
    modeladmin.message_user(request, f'Archived {updated} products.', level=messages.SUCCESS)

Permissions Inside Admin: Who Can See and Do What

Django admin uses Django’s permission system: add, change, delete, and view per model, plus group-based assignment. For many teams, the best practice is: create groups (e.g., “Support,” “Catalog Managers,” “Finance”), assign model permissions to groups, then add users to groups.

ModelAdmin Permission Hooks

For finer control, override permission methods on ModelAdmin. This is useful when a model is editable only in certain states.

from django.contrib import admin
from .models import Order

@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    def has_delete_permission(self, request, obj=None):
        # Example: prevent deleting orders entirely
        return False

    def has_change_permission(self, request, obj=None):
        # Example: allow changing only if not shipped, unless superuser
        if request.user.is_superuser:
            return True
        if obj is None:
            return request.user.has_perm('app.change_order')
        if obj.status == 'shipped':
            return False
        return request.user.has_perm('app.change_order')

Use these hooks to reflect real operational rules, not as a replacement for proper business logic validation.

Field-Level Restrictions (Usability + Integrity)

Sometimes users can edit an object but not certain fields (for example, “Support can update status but not pricing”). You can implement this by dynamically setting read-only fields.

@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
    readonly_fields = ('created_at', 'updated_at')

    def get_readonly_fields(self, request, obj=None):
        ro = list(super().get_readonly_fields(request, obj))
        if request.user.groups.filter(name='Support').exists():
            ro += ['price', 'tax_rate']
        return ro

Usability Details That Make Admin Feel “Professional”

Use autocomplete_fields for Large Relations

Dropdowns for foreign keys don’t scale. Autocomplete improves speed and reduces mistakes.

class OrderItemInline(admin.TabularInline):
    model = OrderItem
    autocomplete_fields = ('product',)

Ensure the related model admin defines search_fields, otherwise autocomplete cannot search meaningfully.

@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
    search_fields = ('sku', 'name')

Optimize Navigation with Date Hierarchy

For time-based models (orders, events, logs), date_hierarchy adds a drill-down navigation by date.

@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    date_hierarchy = 'created_at'

Prevent Accidental Mass Deletes

Admin includes a default “delete selected” action. If that’s too dangerous for a model, remove it.

from django.contrib.admin.actions import delete_selected

@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    actions = None  # removes all actions, including delete_selected

Or remove only delete while keeping your safe actions:

@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
    actions = (archive_products,)

    def get_actions(self, request):
        actions = super().get_actions(request)
        actions.pop('delete_selected', None)
        return actions

Putting It Together: A Practical Admin Configuration Checklist

GoalAdmin featureWhat to configure
Scan lists quicklylist_displayIdentifiers, status, key dates, computed badges
Find records fastsearch_fieldsSKU, name, email, IDs; include related lookups
Filter by workflowlist_filterStatus, category, date fields, booleans
Default “right” orderingorderingNewest-first or “needs attention” first
Protect system fieldsreadonly_fieldsTimestamps, audit fields, computed values
Edit related data togetherInlinesTabularInline/StackedInline, extra, read-only totals
Bulk operations safelyActionsClear names, permission checks, warnings, minimal side effects
Match staff rolesPermissionsGroups + model perms; override has_* when needed
Keep performance acceptableQuery optimizationlist_select_related, autocomplete instead of huge dropdowns

Now answer the exercise about the content:

In Django admin, what is the best way to keep the changelist fast when list_display includes foreign key fields that would otherwise trigger many extra queries?

You are right! Congratulations, now go to the next page

You missed! Try again.

When the changelist displays foreign keys, it can cause extra per-row queries. Setting list_select_related preloads the related objects, improving performance and avoiding N+1 queries.

Next chapter

Views, Templates (Briefly), and the Request Lifecycle for Backend Pages

Arrow Right Icon
Download the app to earn free Certification and listen to the courses in the background, even with the screen off.