Skip to content

Advanced Customization

This guide covers advanced customization techniques for django-admin-deux.

Overview

django-admin-deux is designed for extensibility. This guide shows you how to: - Override templates for custom layouts - Create custom columns with callable methods - Customize pagination behavior - Override view classes - Add custom methods to ModelAdmin - Use feature indicators for plugin integration

Template Customization

Template Resolution Order

django-admin-deux follows Django's template resolution, checking model-specific templates before falling back to generic ones:

For ListView: 1. djadmin/<app>/<model>_list.html 2. djadmin/{app}/{model}_list.html or actions/list.html

For Create: 1. djadmin/<app>/<model>_add.html 2. djadmin/actions/add.html

For Update: 1. djadmin/<app>/<model>_edit.html 2. djadmin/actions/edit.html

For Delete: 1. djadmin/<app>/<model>_delete.html 2. djadmin/actions/delete.html

Overriding List View Template

Create a model-specific ListView template:

{# templates/djadmin/webshop/product_list.html #}
{% extends "djadmin/admin_base.html" %}

{% block title %}Products - {{ site_title }}{% endblock %}

{% block content %}
    <header>
        <h1>Product Management</h1>

        {# Custom toolbar #}
        <div class="toolbar">
            {% for action in list_actions %}
                <a href="{{ action.url }}" class="btn {{ action.css_class }}">
                    {% if action.icon %}
                        <i class="icon-{{ action.icon }}"></i>
                    {% endif %}
                    {{ action.label }}
                </a>
            {% endfor %}
        </div>
    </header>

    {# Custom table layout #}
    <div class="product-grid">
        {% for obj in object_list %}
            <div class="product-card">
                <h3>{{ obj.name }}</h3>
                <p class="sku">SKU: {{ obj.sku }}</p>
                <p class="price">${{ obj.price }}</p>

                <div class="actions">
                    {% for action in record_actions %}
                        <a href="{{ action.url }}?pk={{ obj.pk }}">
                            {{ action.label }}
                        </a>
                    {% endfor %}
                </div>
            </div>
        {% endfor %}
    </div>

    {# Pagination #}
    {% include "djadmin/includes/pagination.html" %}
{% endblock %}

Overriding Form Templates

Customize create/update forms:

{# templates/djadmin/webshop/product_add.html #}
{% extends "djadmin/admin_base.html" %}

{% block title %}Add Product{% endblock %}

{% block content %}
    <h1>Create New Product</h1>

    <form method="post" enctype="multipart/form-data">
        {% csrf_token %}

        {# Custom form layout #}
        <div class="form-grid">
            <div class="form-section">
                <h2>Basic Information</h2>
                {{ form.name }}
                {{ form.sku }}
                {{ form.category }}
            </div>

            <div class="form-section">
                <h2>Pricing</h2>
                {{ form.price }}
                {{ form.cost }}
            </div>

            <div class="form-section">
                <h2>Inventory</h2>
                {{ form.stock_quantity }}
                {{ form.status }}
            </div>
        </div>

        <div class="form-actions">
            <button type="submit" class="btn primary">Save</button>
            <a href="{{ list_url }}" class="btn secondary">Cancel</a>
        </div>
    </form>
{% endblock %}

Shared Partials

Create reusable template fragments:

{# templates/djadmin/includes/product_card.html #}
<div class="product-card">
    <h3>{{ product.name }}</h3>
    <p>{{ product.sku }}</p>
    <p class="price">${{ product.price }}</p>
    {% if product.stock_quantity == 0 %}
        <span class="badge danger">Out of Stock</span>
    {% else %}
        <span class="badge success">In Stock</span>
    {% endif %}
</div>

Use it in templates:

{% for product in object_list %}
    {% include "djadmin/includes/product_card.html" with product=product %}
{% endfor %}

Template Context

All templates receive:

context = {
    'object_list': queryset,     # ListView: list of objects
    'object': obj,               # Detail/Update/Delete: single object
    'form': form,                # Create/Update: form instance
    'opts': model._meta,         # Model metadata
    'model_admin': model_admin,  # ModelAdmin instance
    'site_title': site_title,    # Site title
    'list_actions': [...],       # List actions
    'record_actions': [...],     # Record actions
    'bulk_actions': [...],       # Bulk actions
}

Custom Columns in list_display

Basic Callable Method

Add computed columns using methods:

from djadmin import ModelAdmin, register

@register(Product)
class ProductAdmin(ModelAdmin):
    list_display = ['name', 'price', 'stock_status', 'profit_margin']

    def stock_status(self, obj):
        """Display stock status with icon"""
        if obj.stock_quantity == 0:
            return "❌ Out of Stock"
        elif obj.stock_quantity < 10:
            return f"⚠️ Low Stock ({obj.stock_quantity})"
        return f"✅ In Stock ({obj.stock_quantity})"

    stock_status.short_description = "Stock"

    def profit_margin(self, obj):
        """Calculate and display profit margin"""
        if obj.cost == 0:
            return "N/A"
        margin = ((obj.price - obj.cost) / obj.cost) * 100
        return f"{margin:.1f}%"

    profit_margin.short_description = "Margin"

HTML in Custom Columns

Return HTML from custom methods:

from django.utils.html import format_html

@register(Product)
class ProductAdmin(ModelAdmin):
    list_display = ['name', 'price', 'colored_status']

    def colored_status(self, obj):
        """Display status with color coding"""
        colors = {
            'active': 'success',
            'inactive': 'muted',
            'discontinued': 'danger',
        }
        color = colors.get(obj.status, 'secondary')
        return format_html(
            '<span class="badge {}">{}</span>',
            color,
            obj.get_status_display()
        )

    colored_status.short_description = "Status"

Boolean Icons

Display boolean fields as icons:

from django.utils.html import format_html

@register(Product)
class ProductAdmin(ModelAdmin):
    list_display = ['name', 'is_featured_icon', 'in_stock_icon']

    def is_featured_icon(self, obj):
        if obj.is_featured:
            return format_html('<span class="icon-star text-warning"></span>')
        return format_html('<span class="icon-star-outline text-muted"></span>')

    is_featured_icon.short_description = "Featured"

    def in_stock_icon(self, obj):
        if obj.stock_quantity > 0:
            return format_html('<span class="icon-check text-success"></span>')
        return format_html('<span class="icon-x text-danger"></span>')

    in_stock_icon.short_description = "In Stock"

Create clickable links:

from django.urls import reverse
from django.utils.html import format_html

@register(Order)
class OrderAdmin(ModelAdmin):
    list_display = ['order_number', 'customer_link', 'total', 'status']

    def customer_link(self, obj):
        """Link to customer detail page"""
        url = reverse(
            'djadmin:webshop_customer_detail',
            args=[obj.customer.pk],
            current_app=self.admin_site.name,
        )
        return format_html(
            '<a href="{}">{}</a>',
            url,
            obj.customer.full_name
        )

    customer_link.short_description = "Customer"

Use related objects in custom columns:

@register(OrderItem)
class OrderItemAdmin(ModelAdmin):
    list_display = ['order_number', 'product_name', 'quantity', 'subtotal']

    def order_number(self, obj):
        return obj.order.order_number

    order_number.short_description = "Order"

    def product_name(self, obj):
        return obj.product.name

    product_name.short_description = "Product"

    def subtotal(self, obj):
        return obj.quantity * obj.unit_price

    subtotal.short_description = "Subtotal"

Sorting Custom Columns

Enable sorting on custom columns:

@register(Product)
class ProductAdmin(ModelAdmin):
    list_display = ['name', 'calculated_value']

    def calculated_value(self, obj):
        return obj.price * obj.stock_quantity

    calculated_value.short_description = "Total Value"
    calculated_value.admin_order_field = 'price'  # Sort by price field

Custom Pagination

Custom Page Size

Change default pagination:

@register(Product)
class ProductAdmin(ModelAdmin):
    paginate_by = 25  # Default is 100

Custom Paginator Class

Implement custom pagination logic:

from django.core.paginator import Paginator

class CustomPaginator(Paginator):
    """Custom paginator with additional features"""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def get_page_range(self, number):
        """Get smart page range around current page"""
        # Show 5 pages around current page
        start = max(1, number - 2)
        end = min(self.num_pages, number + 2)
        return range(start, end + 1)

@register(Product)
class ProductAdmin(ModelAdmin):
    paginate_by = 50
    pagination_class = CustomPaginator

Dynamic Page Size

Allow users to choose page size:

@register(Product)
class ProductAdmin(ModelAdmin):
    paginate_by = 50

    def get_paginate_by(self, request):
        """Allow per-page parameter from request"""
        try:
            return int(request.GET.get('per_page', self.paginate_by))
        except ValueError:
            return self.paginate_by

Custom View Classes

Override ListView

Provide a custom ListView class:

from django.views.generic import ListView

class ProductListView(ListView):
    """Custom ListView with additional context"""

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        # Add custom context
        context['total_value'] = sum(
            p.price * p.stock_quantity
            for p in context['object_list']
        )
        context['low_stock_count'] = context['object_list'].filter(
            stock_quantity__lt=10
        ).count()

        return context

@register(Product)
class ProductAdmin(ModelAdmin):
    list_view_class = ProductListView

Override Create/Update Views

from django.views.generic import CreateView, UpdateView

class ProductCreateView(CreateView):
    """Custom CreateView with pre-processing"""

    def form_valid(self, form):
        # Set default values
        form.instance.created_by = self.request.user

        # Send notification
        send_notification(f"New product: {form.instance.name}")

        return super().form_valid(form)

class ProductUpdateView(UpdateView):
    """Custom UpdateView with change tracking"""

    def form_valid(self, form):
        # Track changes
        if 'price' in form.changed_data:
            log_price_change(
                form.instance,
                form.initial['price'],
                form.cleaned_data['price']
            )

        return super().form_valid(form)

@register(Product)
class ProductAdmin(ModelAdmin):
    create_view_class = ProductCreateView
    detail_view_class = ProductUpdateView  # Used for updates

Custom Methods in ModelAdmin

Helper Methods

Add utility methods to your ModelAdmin:

@register(Product)
class ProductAdmin(ModelAdmin):
    list_display = ['name', 'price', 'stock_status']

    def get_low_stock_products(self):
        """Helper method to get low stock products"""
        return self.model.objects.filter(stock_quantity__lt=10)

    def get_total_inventory_value(self):
        """Calculate total inventory value"""
        products = self.model.objects.all()
        return sum(p.price * p.stock_quantity for p in products)

    def stock_status(self, obj):
        """Use helper logic"""
        if obj.stock_quantity < 10:
            return "⚠️ Low Stock"
        return "✅ In Stock"

    stock_status.short_description = "Stock"

Queryset Customization

Override queryset methods:

@register(Product)
class ProductAdmin(ModelAdmin):

    def get_queryset(self, request):
        """Customize default queryset"""
        qs = super().get_queryset(request)

        # Show only active products to non-staff
        if not request.user.is_staff:
            qs = qs.filter(status='active')

        # Optimize queries with select_related
        qs = qs.select_related('category')

        return qs

Form Initialization

Customize form initialization:

@register(Product)
class ProductAdmin(ModelAdmin):

    def get_form_kwargs(self, request, obj=None):
        """Add custom form kwargs"""
        kwargs = super().get_form_kwargs(request, obj)
        kwargs['request'] = request  # Pass request to form
        return kwargs

    def get_initial(self, request):
        """Provide initial form data"""
        return {
            'status': 'draft',
            'created_by': request.user,
        }

Feature Indicators

Feature indicators trigger plugin functionality by declaring which features your ModelAdmin needs.

Built-in Indicators

@register(Product)
class ProductAdmin(ModelAdmin):
    # Triggers 'search' feature (requires search plugin)
    search_fields = ['name', 'sku', 'description']

    # Triggers 'filter' feature (requires filter plugin)
    list_filter = ['category', 'status', 'is_featured']

    # Triggers 'ordering' feature (requires ordering plugin)
    ordering = ['-created_at', 'name']

    # Triggers 'inlines' feature (requires inlines plugin)
    inlines = [ReviewInline, ImageInline]

    # Triggers 'sortable' feature (requires sortable plugin)
    inline_sortable = True

How Feature Validation Works

At Django startup: 1. ModelAdmin declares features via indicators 2. System checks which features are requested 3. Validates that plugins provide those features 4. Raises ImproperlyConfigured if features are missing

Example: Search Feature

# This triggers feature validation
@register(Product)
class ProductAdmin(ModelAdmin):
    search_fields = ['name', 'sku']  # Requests 'search' feature

# At startup, if no plugin provides 'search':
# ImproperlyConfigured: Feature 'search' requested by ProductAdmin
# but no plugin provides it. Install a search plugin.

Custom Feature Indicators

Extend the feature system for your plugins:

from djadmin import ModelAdmin

class MyModelAdmin(ModelAdmin):
    # Add custom feature indicator
    FEATURE_INDICATORS = {
        **ModelAdmin.FEATURE_INDICATORS,
        'export': ['export_formats'],
        'audit': ['enable_audit_log'],
    }

@register(Product)
class ProductAdmin(MyModelAdmin):
    export_formats = ['csv', 'xlsx']  # Triggers 'export' feature
    enable_audit_log = True           # Triggers 'audit' feature

Advanced Patterns

Conditional Fields

Show different fields based on conditions:

@register(Product)
class ProductAdmin(ModelAdmin):

    def get_fields(self, request, obj=None):
        """Conditional field display"""
        fields = ['name', 'sku', 'price']

        # Show cost only to staff
        if request.user.is_staff:
            fields.append('cost')

        # Show internal notes only on existing objects
        if obj:
            fields.append('internal_notes')

        return fields

Dynamic Actions

Add actions conditionally:

@register(Product)
class ProductAdmin(ModelAdmin):

    def get_record_actions(self, request, obj=None):
        """Dynamic action list"""
        actions = [EditAction]

        # Show publish only for draft products
        if obj and obj.status == 'draft':
            actions.append(PublishAction)

        # Show delete only for staff
        if request.user.is_staff:
            actions.append(DeleteAction)

        return [action(self.model, self, self.admin_site) for action in actions]

Per-User Customization

Customize based on user:

@register(Product)
class ProductAdmin(ModelAdmin):

    def get_list_display(self, request):
        """Different columns for different users"""
        if request.user.is_superuser:
            return ['name', 'sku', 'price', 'cost', 'profit_margin']
        elif request.user.is_staff:
            return ['name', 'sku', 'price', 'stock_quantity']
        return ['name', 'price']

    def get_paginate_by(self, request):
        """Different page sizes for different users"""
        if request.user.is_superuser:
            return 100
        return 25

Multi-Tenant Filtering

Filter data by tenant:

@register(Product)
class ProductAdmin(ModelAdmin):

    def get_queryset(self, request):
        """Filter by user's tenant"""
        qs = super().get_queryset(request)

        if hasattr(request.user, 'tenant'):
            qs = qs.filter(tenant=request.user.tenant)

        return qs

    def save_model(self, request, obj, form, change):
        """Automatically set tenant"""
        if not change and hasattr(request.user, 'tenant'):
            obj.tenant = request.user.tenant
        obj.save()

Inline Editing Preparation

Prepare for inline editing (future feature):

@register(Order)
class OrderAdmin(ModelAdmin):
    # These will be used when inline plugin is available
    inlines = [OrderItemInline]

    def get_inlines(self, request, obj=None):
        """Conditional inlines"""
        if obj and obj.status != 'completed':
            return [OrderItemInline]
        return []

Performance Optimization

Query Optimization

@register(Product)
class ProductAdmin(ModelAdmin):

    def get_queryset(self, request):
        """Optimize queries"""
        qs = super().get_queryset(request)

        # Use select_related for foreign keys
        qs = qs.select_related('category', 'supplier')

        # Use prefetch_related for reverse relations
        qs = qs.prefetch_related('reviews', 'tags')

        return qs

Caching

from django.core.cache import cache

@register(Product)
class ProductAdmin(ModelAdmin):
    list_display = ['name', 'price', 'cached_review_count']

    def cached_review_count(self, obj):
        """Cache expensive calculation"""
        cache_key = f'product_{obj.pk}_review_count'
        count = cache.get(cache_key)

        if count is None:
            count = obj.reviews.count()
            cache.set(cache_key, count, 300)  # Cache for 5 minutes

        return count

    cached_review_count.short_description = "Reviews"

Lazy Loading

@register(Product)
class ProductAdmin(ModelAdmin):

    def get_list_display(self, request):
        """Load expensive columns only for staff"""
        base = ['name', 'sku', 'price']

        if request.user.is_staff:
            base.extend(['total_orders', 'total_revenue'])

        return base

    def total_orders(self, obj):
        """Expensive calculation"""
        return obj.order_items.count()

    total_orders.short_description = "Orders"

See Also