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).