Admin Site

Admin Actions

Admin actions provide a powerful way to perform bulk operations on selected objects in the Django admin interface. This chapter covers creating custom actions, handling complex operations, and building efficient bulk processing tools.

Admin Actions

Admin actions provide a powerful way to perform bulk operations on selected objects in the Django admin interface. This chapter covers creating custom actions, handling complex operations, and building efficient bulk processing tools.

Understanding Admin Actions

Built-in Actions

Django provides a default "Delete selected" action:

from django.contrib import admin
from .models import Article

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = ['title', 'status', 'author', 'created_at']
    
    # The delete action is enabled by default
    # To disable it:
    # actions = None

Action Workflow

  1. User selects objects in the admin list view
  2. User chooses an action from the dropdown
  3. Action function is executed with selected objects
  4. Results are displayed to the user

Creating Custom Actions

Basic Action Function

from django.contrib import admin
from django.contrib import messages
from .models import Article

def make_published(modeladmin, request, queryset):
    """Publish selected articles"""
    updated = queryset.update(status='published')
    messages.success(
        request,
        f'{updated} articles were successfully published.'
    )

make_published.short_description = "Mark selected articles as published"

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = ['title', 'status', 'author', 'created_at']
    actions = [make_published]

Action as Method

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = ['title', 'status', 'author', 'created_at']
    
    def make_published(self, request, queryset):
        """Publish selected articles"""
        updated = queryset.update(status='published')
        self.message_user(
            request,
            f'{updated} articles were successfully published.',
            messages.SUCCESS
        )
    
    make_published.short_description = "Mark selected articles as published"
    
    actions = ['make_published']

Advanced Action Examples

Bulk Status Updates

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    def make_published(self, request, queryset):
        """Publish selected articles"""
        from datetime import datetime
        
        # Update status and set published date
        updated = 0
        for article in queryset:
            if article.status != 'published':
                article.status = 'published'
                article.published_at = datetime.now()
                article.save()
                updated += 1
        
        self.message_user(
            request,
            f'{updated} articles were published.',
            messages.SUCCESS
        )
    
    def make_draft(self, request, queryset):
        """Convert selected articles to draft"""
        updated = queryset.exclude(status='draft').update(
            status='draft',
            published_at=None
        )
        
        self.message_user(
            request,
            f'{updated} articles were converted to draft.',
            messages.SUCCESS
        )
    
    def archive_articles(self, request, queryset):
        """Archive selected articles"""
        updated = queryset.update(status='archived')
        
        self.message_user(
            request,
            f'{updated} articles were archived.',
            messages.SUCCESS
        )
    
    make_published.short_description = "Publish selected articles"
    make_draft.short_description = "Convert to draft"
    archive_articles.short_description = "Archive selected articles"
    
    actions = ['make_published', 'make_draft', 'archive_articles']

Data Export Actions

import csv
from django.http import HttpResponse
from datetime import datetime

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    def export_as_csv(self, request, queryset):
        """Export selected articles as CSV"""
        
        response = HttpResponse(content_type='text/csv')
        response['Content-Disposition'] = f'attachment; filename="articles_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv"'
        
        writer = csv.writer(response)
        
        # Write header
        writer.writerow([
            'ID', 'Title', 'Author', 'Category', 'Status', 
            'Created', 'Published', 'Word Count'
        ])
        
        # Write data
        for article in queryset:
            writer.writerow([
                article.id,
                article.title,
                article.author.username if article.author else '',
                article.category.name if article.category else '',
                article.get_status_display(),
                article.created_at.strftime('%Y-%m-%d %H:%M:%S'),
                article.published_at.strftime('%Y-%m-%d %H:%M:%S') if article.published_at else '',
                len(article.content.split()) if article.content else 0
            ])
        
        return response
    
    def export_as_json(self, request, queryset):
        """Export selected articles as JSON"""
        import json
        
        data = []
        for article in queryset:
            data.append({
                'id': article.id,
                'title': article.title,
                'author': article.author.username if article.author else None,
                'category': article.category.name if article.category else None,
                'status': article.status,
                'content': article.content,
                'created_at': article.created_at.isoformat(),
                'published_at': article.published_at.isoformat() if article.published_at else None,
            })
        
        response = HttpResponse(
            json.dumps(data, indent=2),
            content_type='application/json'
        )
        response['Content-Disposition'] = f'attachment; filename="articles_{datetime.now().strftime("%Y%m%d_%H%M%S")}.json"'
        
        return response
    
    export_as_csv.short_description = "Export selected articles as CSV"
    export_as_json.short_description = "Export selected articles as JSON"
    
    actions = ['export_as_csv', 'export_as_json']

Bulk Email Actions

from django.core.mail import send_mass_mail
from django.template.loader import render_to_string

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    def notify_authors(self, request, queryset):
        """Send notification emails to article authors"""
        
        emails = []
        authors_notified = set()
        
        for article in queryset:
            if article.author and article.author.email:
                if article.author.id not in authors_notified:
                    # Prepare email content
                    subject = 'Article Update Notification'
                    message = render_to_string('admin/email/author_notification.txt', {
                        'author': article.author,
                        'article': article,
                        'admin_user': request.user
                    })
                    
                    emails.append((
                        subject,
                        message,
                        'noreply@example.com',
                        [article.author.email]
                    ))
                    
                    authors_notified.add(article.author.id)
        
        if emails:
            send_mass_mail(emails, fail_silently=False)
            self.message_user(
                request,
                f'Notifications sent to {len(emails)} authors.',
                messages.SUCCESS
            )
        else:
            self.message_user(
                request,
                'No authors with email addresses found.',
                messages.WARNING
            )
    
    notify_authors.short_description = "Notify authors via email"
    
    actions = ['notify_authors']

Actions with Intermediate Pages

Confirmation Actions

from django.shortcuts import render
from django.contrib.admin import helpers

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    def delete_with_confirmation(self, request, queryset):
        """Delete articles with custom confirmation page"""
        
        if 'apply' in request.POST:
            # User confirmed deletion
            count = queryset.count()
            queryset.delete()
            
            self.message_user(
                request,
                f'{count} articles were successfully deleted.',
                messages.SUCCESS
            )
            return None
        
        # Show confirmation page
        context = {
            'title': 'Delete Articles',
            'queryset': queryset,
            'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
            'opts': self.model._meta,
        }
        
        return render(
            request,
            'admin/delete_confirmation.html',
            context
        )
    
    delete_with_confirmation.short_description = "Delete selected articles (with confirmation)"
    
    actions = ['delete_with_confirmation']

Form-Based Actions

from django import forms
from django.shortcuts import render, redirect

class BulkCategoryForm(forms.Form):
    """Form for bulk category assignment"""
    category = forms.ModelChoiceField(
        queryset=Category.objects.all(),
        empty_label="Select a category",
        required=True
    )
    overwrite_existing = forms.BooleanField(
        required=False,
        initial=False,
        help_text="Check to overwrite existing categories"
    )

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    def assign_category(self, request, queryset):
        """Assign category to selected articles"""
        
        if 'apply' in request.POST:
            form = BulkCategoryForm(request.POST)
            
            if form.is_valid():
                category = form.cleaned_data['category']
                overwrite = form.cleaned_data['overwrite_existing']
                
                if overwrite:
                    updated = queryset.update(category=category)
                else:
                    updated = queryset.filter(category__isnull=True).update(category=category)
                
                self.message_user(
                    request,
                    f'{updated} articles were assigned to category "{category}".',
                    messages.SUCCESS
                )
                return redirect(request.get_full_path())
        else:
            form = BulkCategoryForm()
        
        context = {
            'title': 'Assign Category',
            'form': form,
            'queryset': queryset,
            'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
            'opts': self.model._meta,
        }
        
        return render(request, 'admin/assign_category.html', context)
    
    assign_category.short_description = "Assign category to selected articles"
    
    actions = ['assign_category']

Asynchronous Actions

Background Task Actions

from celery import shared_task
from django.contrib import messages

@shared_task
def process_articles_async(article_ids, action_type, user_id):
    """Process articles asynchronously"""
    from django.contrib.auth.models import User
    
    articles = Article.objects.filter(id__in=article_ids)
    user = User.objects.get(id=user_id)
    
    if action_type == 'generate_thumbnails':
        for article in articles:
            if article.featured_image:
                # Generate thumbnails
                article.generate_thumbnails()
    
    elif action_type == 'update_seo':
        for article in articles:
            # Update SEO metadata
            article.update_seo_metadata()
    
    # Send notification email when complete
    send_mail(
        'Bulk Action Complete',
        f'Your bulk action "{action_type}" has been completed for {articles.count()} articles.',
        'noreply@example.com',
        [user.email]
    )

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    def generate_thumbnails(self, request, queryset):
        """Generate thumbnails for selected articles (async)"""
        
        article_ids = list(queryset.values_list('id', flat=True))
        
        # Start background task
        process_articles_async.delay(
            article_ids,
            'generate_thumbnails',
            request.user.id
        )
        
        self.message_user(
            request,
            f'Thumbnail generation started for {len(article_ids)} articles. You will receive an email when complete.',
            messages.INFO
        )
    
    generate_thumbnails.short_description = "Generate thumbnails (background task)"
    
    actions = ['generate_thumbnails']

Action Permissions and Security

Permission-Based Actions

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    def get_actions(self, request):
        """Return actions based on user permissions"""
        actions = super().get_actions(request)
        
        # Remove delete action for non-superusers
        if not request.user.is_superuser:
            if 'delete_selected' in actions:
                del actions['delete_selected']
        
        # Only show publish action to users with publish permission
        if not request.user.has_perm('myapp.can_publish_article'):
            if 'make_published' in actions:
                del actions['make_published']
        
        return actions
    
    def make_published(self, request, queryset):
        """Publish articles (requires permission)"""
        if not request.user.has_perm('myapp.can_publish_article'):
            self.message_user(
                request,
                'You do not have permission to publish articles.',
                messages.ERROR
            )
            return
        
        updated = queryset.update(status='published')
        self.message_user(
            request,
            f'{updated} articles were published.',
            messages.SUCCESS
        )
    
    make_published.short_description = "Publish selected articles"
    
    actions = ['make_published']

Audit Trail for Actions

from django.contrib.admin.models import LogEntry, CHANGE
from django.contrib.contenttypes.models import ContentType

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    def make_published(self, request, queryset):
        """Publish articles with audit trail"""
        
        updated_articles = []
        
        for article in queryset:
            if article.status != 'published':
                old_status = article.status
                article.status = 'published'
                article.published_at = timezone.now()
                article.save()
                
                updated_articles.append(article)
                
                # Log the action
                LogEntry.objects.log_action(
                    user_id=request.user.id,
                    content_type_id=ContentType.objects.get_for_model(article).pk,
                    object_id=article.pk,
                    object_repr=str(article),
                    action_flag=CHANGE,
                    change_message=f'Status changed from "{old_status}" to "published" via bulk action'
                )
        
        self.message_user(
            request,
            f'{len(updated_articles)} articles were published.',
            messages.SUCCESS
        )
    
    make_published.short_description = "Publish selected articles"
    
    actions = ['make_published']

Custom Action Templates

Action Confirmation Template

<!-- templates/admin/assign_category.html -->
{% extends "admin/base_site.html" %}
{% load i18n admin_urls static admin_list %}

{% block title %}Assign Category{% endblock %}

{% block breadcrumbs %}
<div class="breadcrumbs">
    <a href="{% url 'admin:index' %}">Home</a>
    &rsaquo; <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
    &rsaquo; <a href="{% url 'admin:myapp_article_changelist' %}">Articles</a>
    &rsaquo; Assign Category
</div>
{% endblock %}

{% block content %}
<form method="post">
    {% csrf_token %}
    
    <div class="module">
        <h2>Assign Category to Selected Articles</h2>
        
        <p>You have selected {{ queryset.count }} article{{ queryset.count|pluralize }} for category assignment:</p>
        
        <ul>
            {% for obj in queryset %}
            <li>{{ obj }}</li>
            {% endfor %}
        </ul>
        
        <fieldset class="module aligned">
            <div class="form-row">
                {{ form.category.label_tag }}
                {{ form.category }}
                {% if form.category.help_text %}
                <p class="help">{{ form.category.help_text }}</p>
                {% endif %}
            </div>
            
            <div class="form-row">
                {{ form.overwrite_existing }}
                {{ form.overwrite_existing.label_tag }}
                {% if form.overwrite_existing.help_text %}
                <p class="help">{{ form.overwrite_existing.help_text }}</p>
                {% endif %}
            </div>
        </fieldset>
        
        {% for obj in queryset %}
        <input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk }}" />
        {% endfor %}
        
        <input type="hidden" name="action" value="assign_category" />
        
        <div class="submit-row">
            <input type="submit" name="apply" value="Assign Category" class="default" />
            <a href="{% url 'admin:myapp_article_changelist' %}" class="button cancel-link">Cancel</a>
        </div>
    </div>
</form>
{% endblock %}

Testing Admin Actions

Action Tests

from django.test import TestCase, RequestFactory
from django.contrib.auth.models import User
from django.contrib.admin.sites import AdminSite
from .admin import ArticleAdmin
from .models import Article

class AdminActionTests(TestCase):
    def setUp(self):
        self.factory = RequestFactory()
        self.user = User.objects.create_superuser(
            username='admin',
            email='admin@example.com',
            password='testpass123'
        )
        self.site = AdminSite()
        self.admin = ArticleAdmin(Article, self.site)
        
        # Create test articles
        self.articles = [
            Article.objects.create(
                title=f'Article {i}',
                content='Test content',
                status='draft',
                author=self.user
            )
            for i in range(3)
        ]
    
    def test_make_published_action(self):
        """Test the make_published action"""
        request = self.factory.post('/admin/')
        request.user = self.user
        
        queryset = Article.objects.filter(status='draft')
        
        # Execute action
        self.admin.make_published(request, queryset)
        
        # Check results
        published_count = Article.objects.filter(status='published').count()
        self.assertEqual(published_count, 3)
    
    def test_export_csv_action(self):
        """Test CSV export action"""
        request = self.factory.post('/admin/')
        request.user = self.user
        
        queryset = Article.objects.all()
        
        # Execute action
        response = self.admin.export_as_csv(request, queryset)
        
        # Check response
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response['Content-Type'], 'text/csv')
        self.assertIn('attachment', response['Content-Disposition'])

Best Practices

Performance

  • Use queryset.update() for simple bulk updates
  • Implement pagination for large datasets
  • Use background tasks for time-consuming operations
  • Optimize database queries in action functions

User Experience

  • Provide clear action descriptions
  • Show progress feedback for long operations
  • Use confirmation pages for destructive actions
  • Display meaningful success/error messages

Security

  • Validate user permissions before executing actions
  • Sanitize user input in action forms
  • Log important actions for audit trails
  • Use CSRF protection for action forms

Maintainability

  • Keep action functions focused and simple
  • Use descriptive names for actions
  • Document complex action logic
  • Test actions thoroughly

Next Steps

Now that you've mastered admin actions, let's explore security best practices to ensure your Django admin interface is properly protected in production environments.