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"
Links in Custom Columns¶
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"
Accessing Related Objects¶
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:
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¶
- Basic Usage - Core configuration options
- Actions Guide - Creating custom actions
- CRUD Operations - Customizing CRUD behavior
- Plugin Development - Creating plugins and extending functionality
- Theming Guide - Styling and theme customization