Skip to content

Chapter 10: Django Admin

10.2 Advanced Admin Configuration

Custom List Display

Advanced list display configuration can significantly improve the usability of the admin backend:

python
# admin.py
from django.contrib import admin
from django.utils.html import format_html
from django.urls import reverse
from django.utils.safestring import mark_safe
from .models import Article, Category, Tag

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    # Basic list configuration
    list_display = [
        'title', 'author', 'category', 'is_published', 
        'created_at', 'view_count', 'status_indicator'
    ]
    
    # Number of items per page in list
    list_per_page = 25
    
    # Editable fields in list
    list_editable = ['is_published', 'category']
    
    # Sortable fields
    sortable_by = ['title', 'created_at', 'author']
    
    # Custom method fields
    def view_count(self, obj):
        """Show article view count"""
        return obj.view_count or 0
    view_count.short_description = 'Views'
    view_count.admin_order_field = 'view_count'
    
    def status_indicator(self, obj):
        """Show status indicator"""
        if obj.is_published:
            color = '#28a745'  # Green
            text = 'Published'
        else:
            color = '#ffc107'  # Yellow
            text = 'Draft'
        
        return format_html(
            '<span style="background-color: {}; color: white; '
            'padding: 4px 8px; border-radius: 4px; font-size: 12px;">{}</span>',
            color, text
        )
    status_indicator.short_description = 'Status'
    
    # Fields with links
    def title_link(self, obj):
        """Title with edit link"""
        url = reverse('admin:myapp_article_change', args=[obj.pk])
        return format_html('<a href="{}">{}</a>', url, obj.title)
    title_link.short_description = 'Title'
    title_link.admin_order_field = 'title'
    
    # Conditional field display
    def get_list_display(self, request):
        """Dynamically adjust displayed fields based on user permissions"""
        if request.user.is_superuser:
            return self.list_display + ['edit_link']
        return self.list_display
    
    def edit_link(self, obj):
        """Edit link"""
        url = reverse('admin:myapp_article_change', args=[obj.pk])
        return format_html('<a href="{}">Edit</a>', url)
    edit_link.short_description = 'Action'

# Custom list filters
from django.contrib import admin
from django.utils.translation import gettext_lazy as _

class PublishedFilter(admin.SimpleListFilter):
    title = _('Publish Status')
    parameter_name = 'published'
    
    def lookups(self, request, model_admin):
        return (
            ('published', _('Published')),
            ('draft', _('Draft')),
            ('recent', _('Recently Published')),
        )
    
    def queryset(self, request, queryset):
        if self.value() == 'published':
            return queryset.filter(is_published=True)
        if self.value() == 'draft':
            return queryset.filter(is_published=False)
        if self.value() == 'recent':
            from django.utils import timezone
            from datetime import timedelta
            recent_date = timezone.now() - timedelta(days=7)
            return queryset.filter(
                is_published=True,
                created_at__gte=recent_date
            )
        return queryset

# Use custom filter in ArticleAdmin
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_filter = [
        'category', 'tags', 'is_published', 'created_at', 
        PublishedFilter, 'author'
    ]

Configure advanced filters and search functions:

python
# admin.py
from django.contrib import admin
from django.db import models
from django.forms import TextInput, Textarea

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    # Search fields
    search_fields = [
        'title', 'content', 'excerpt', 
        'author__username', 'author__first_name', 'author__last_name',
        'category__name', 'tags__name'
    ]
    
    # Search field configuration
    search_help_text = 'Search title, content, author, category or tags'
    
    # List filters
    list_filter = [
        'is_published',
        'category',
        'tags',
        'created_at',
        'author',
        'updated_at'
    ]
    
    # Date hierarchy filter
    date_hierarchy = 'created_at'
    
    # Custom search function
    def get_search_results(self, request, queryset, search_term):
        """Custom search logic"""
        queryset, use_distinct = super().get_search_results(
            request, queryset, search_term
        )
        
        # Add custom search logic
        if search_term:
            # Search articles with related tags
            queryset |= self.model.objects.filter(
                tags__name__icontains=search_term
            )
            
            # Search author names
            queryset |= self.model.objects.filter(
                author__first_name__icontains=search_term
            ) | self.model.objects.filter(
                author__last_name__icontains=search_term
            )
        
        return queryset, True
    
    # Custom filter
    class WordCountFilter(admin.SimpleListFilter):
        title = 'Word Count Range'
        parameter_name = 'word_count'
        
        def lookups(self, request, model_admin):
            return (
                ('short', 'Short articles (< 500 words)'),
                ('medium', 'Medium articles (500-2000 words)'),
                ('long', 'Long articles (> 2000 words)'),
            )
        
        def queryset(self, request, queryset):
            if self.value() == 'short':
                return queryset.filter(content__length__lt=500)
            if self.value() == 'medium':
                return queryset.filter(
                    content__length__gte=500,
                    content__length__lt=2000
                )
            if self.value() == 'long':
                return queryset.filter(content__length__gte=2000)
            return queryset
    
    # Use in filters
    list_filter = [
        'is_published',
        'category',
        WordCountFilter,  # Custom filter
        'created_at'
    ]

Inline Editing

Inline editing allows editing related models on the same page:

python
# models.py
from django.db import models

class Article(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    author = models.ForeignKey(User, on_delete=models.CASCADE)

class Comment(models.Model):
    article = models.ForeignKey(Article, on_delete=models.CASCADE)
    author_name = models.CharField(max_length=100)
    email = models.EmailField()
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    is_approved = models.BooleanField(default=False)

# admin.py
from django.contrib import admin
from .models import Article, Comment

# Basic inline editing
class CommentInline(admin.TabularInline):
    model = Comment
    extra = 1  # Extra empty rows to display
    fields = ['author_name', 'email', 'content', 'is_approved', 'created_at']
    readonly_fields = ['created_at']

# Stacked inline editing
class CommentStackedInline(admin.StackedInline):
    model = Comment
    extra = 0
    fields = ['author_name', 'email', 'content', 'is_approved']
    readonly_fields = ['created_at']

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    inlines = [CommentInline]  # Use inline editing
    list_display = ['title', 'author', 'comment_count']
    
    def comment_count(self, obj):
        return obj.comment_set.count()
    comment_count.short_description = 'Comment Count'

# More complex inline editing
class CommentInline(admin.TabularInline):
    model = Comment
    extra = 0
    fields = [
        'author_name', 'email', 'content', 'is_approved', 
        'created_at', 'approve_button'
    ]
    readonly_fields = ['created_at', 'approve_button']
    
    def approve_button(self, obj):
        """Approve button"""
        if obj.pk and not obj.is_approved:
            url = reverse('admin:approve_comment', args=[obj.pk])
            return format_html(
                '<a class="button" href="{}">Approve</a>', url
            )
        elif obj.is_approved:
            return 'Approved'
        return ''
    approve_button.short_description = 'Action'
    
    # Limit number of inline objects displayed
    def get_queryset(self, request):
        qs = super().get_queryset(request)
        return qs.select_related('article')

# Custom inline editing behavior
class CommentInline(admin.TabularInline):
    model = Comment
    extra = 1
    
    # Custom form
    def get_formset(self, request, obj=None, **kwargs):
        formset = super().get_formset(request, obj, **kwargs)
        # Modify form based on user permissions
        if not request.user.is_superuser:
            # Non-superusers cannot modify approved comments
            if 'is_approved' in formset.form.base_fields:
                formset.form.base_fields['is_approved'].widget.attrs['disabled'] = True
        return formset
    
    # Custom save behavior
    def save_formset(self, request, form, formset, change):
        instances = formset.save(commit=False)
        for instance in instances:
            # Automatically set article author to current user (if not set)
            if not instance.article.author_id:
                instance.article.author = request.user
                instance.article.save()
            instance.save()
        formset.save_m2m()

Custom Admin Actions

Create custom admin actions to batch process objects:

python
# admin.py
from django.contrib import admin
from django.contrib import messages
from django.utils.translation import gettext_lazy as _
from .models import Article

def make_published(modeladmin, request, queryset):
    """Publish selected articles"""
    updated = queryset.update(is_published=True)
    modeladmin.message_user(
        request,
        f'Successfully published {updated} articles.',
        messages.SUCCESS
    )
make_published.short_description = "Publish selected articles"

def make_draft(modeladmin, request, queryset):
    """Set selected articles as draft"""
    updated = queryset.update(is_published=False)
    modeladmin.message_user(
        request,
        f'Successfully set {updated} articles as draft.',
        messages.INFO
    )
make_draft.short_description = "Set selected articles as draft"

def duplicate_articles(modeladmin, request, queryset):
    """Duplicate selected articles"""
    for article in queryset:
        # Create article copy
        article.pk = None
        article.title = f"{article.title} (Copy)"
        article.slug = f"{article.slug}-copy"
        article.is_published = False
        article.save()
    
    modeladmin.message_user(
        request,
        f'Successfully duplicated {queryset.count()} articles.',
        messages.SUCCESS
    )
duplicate_articles.short_description = "Duplicate selected articles"

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = ['title', 'author', 'is_published', 'created_at']
    actions = [make_published, make_draft, duplicate_articles]
    
    # Custom action permissions
    def get_actions(self, request):
        actions = super().get_actions(request)
        # Non-superusers cannot use duplicate function
        if not request.user.is_superuser:
            if 'duplicate_articles' in actions:
                del actions['duplicate_articles']
        return actions
    
    # Conditional actions
    def get_action_choices(self, request, default_choices=[]):
        """Dynamically adjust action options based on conditions"""
        choices = super().get_action_choices(request, default_choices)
        if not request.user.is_superuser:
            # Remove certain actions
            choices = [choice for choice in choices if choice[0] != 'duplicate_articles']
        return choices

# More complex custom actions
def export_selected_articles(modeladmin, request, queryset):
    """Export selected articles"""
    import csv
    from django.http import HttpResponse
    
    response = HttpResponse(content_type='text/csv')
    response['Content-Disposition'] = 'attachment; filename="articles.csv"'
    
    writer = csv.writer(response)
    writer.writerow(['Title', 'Author', 'Category', 'Publish Time', 'Content'])
    
    for article in queryset:
        writer.writerow([
            article.title,
            article.author.username,
            article.category.name if article.category else '',
            article.created_at.strftime('%Y-%m-%d %H:%M:%S'),
            article.content[:100] + '...' if len(article.content) > 100 else article.content
        ])
    
    return response

export_selected_articles.short_description = "Export selected articles as CSV"

# Actions with confirmation step
def delete_selected_articles(modeladmin, request, queryset):
    """Delete selected articles (with confirmation)"""
    if 'apply' in request.POST:
        # Execute deletion
        count = queryset.count()
        queryset.delete()
        modeladmin.message_user(
            request,
            f'Successfully deleted {count} articles.',
            messages.WARNING
        )
        return HttpResponseRedirect(request.get_full_path())
    
    # Show confirmation page
    context = {
        'articles': queryset,
        'action_checkbox_name': admin.ACTION_CHECKBOX_NAME,
    }
    return render(request, 'admin/confirm_delete_articles.html', context)

delete_selected_articles.short_description = "Delete selected articles"

# Use in ArticleAdmin
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = ['title', 'author', 'is_published', 'created_at']
    actions = [
        make_published, 
        make_draft, 
        duplicate_articles,
        export_selected_articles,
        delete_selected_articles
    ]

With these advanced configurations, you can create a powerful Django admin backend with good user experience.

Summary

Django advanced admin configuration provides rich customization options:

  1. ✅ Custom list display and filters improve management efficiency
  2. ✅ Inline editing simplifies related model operations
  3. ✅ Custom admin actions support batch processing
  4. ✅ Permission control ensures operation security
  5. ✅ Search and filtering enhance data lookup

Mastering advanced configuration skills can build professional admin backends.

Next Article

We will learn about static file and media file handling.

11.1 Static File Management →

Directory

Return to Course Directory

Released under the [BY-NC-ND License](https://creativecommons.org/licenses/by-nc-nd/4.0/deed.en).