The admin and forms

Turn the Django admin into a usable back office, and build server-validated forms with ModelForm — including the CSRF token people always forget.

Configuring the admin

# blog/admin.py
from django.contrib import admin
from .models import Category, Post

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ("title", "status", "category", "author", "created_at")
    list_filter = ("status", "category", "created_at")
    search_fields = ("title", "body", "author__username")
    prepopulated_fields = {"slug": ("title",)}
    raw_id_fields = ("author", "category")
    readonly_fields = ("created_at",)
    date_hierarchy = "created_at"
    list_select_related = ("author", "category")   # avoids N+1 in the list
    actions = ["mark_published"]

    @admin.action(description="Publish selected posts")
    def mark_published(self, request, queryset):
        for post in queryset:
            post.publish()
        self.message_user(request, f"{queryset.count()} posts published")

admin.site.register(Category)
admin.site.site_header = "Blog back office"
python manage.py createsuperuser
# then sign in at /admin/ — only users with is_staff=True may enter
OptionEffect
list_displayColumns in the change list
list_filterSidebar filters
search_fieldsSearch box; __ traverses relations
list_select_relatedJoin related rows for the list view
readonly_fieldsShow but never write
actionsBulk operations on selected rows

Forms and validation

# blog/forms.py
from django import forms
from django.core.exceptions import ValidationError
from .models import Comment, Post

class CommentForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = ["text"]
        widgets = {"text": forms.Textarea(attrs={"rows": 3, "maxlength": 1000})}

    def clean_text(self):
        text = self.cleaned_data["text"].strip()
        if len(text) < 3:
            raise ValidationError("Say a little more than that.")
        if "http://" in text or "https://" in text:
            raise ValidationError("Links are not allowed in comments.")
        return text

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ["title", "slug", "body", "category"]

    def clean(self):
        data = super().clean()
        if data.get("body") and len(data["body"]) < 50:
            self.add_error("body", "A published post should be at least 50 characters.")
        return data

    def save(self, commit=True):
        post = super().save(commit=False)
        post.author = self.author
        if commit:
            post.save()
        return post
# blog/views.py
from django.shortcuts import get_object_or_404, redirect, render
from .forms import CommentForm
from .models import Comment, Post

def add_comment(request, pk):
    post = get_object_or_404(Post, pk=pk)
    form = CommentForm(request.POST or None)
    if request.method == "POST" and form.is_valid():
        comment = form.save(commit=False)
        comment.post = post
        comment.author = request.user
        comment.save()
        return redirect(post.get_absolute_url())   # redirect after POST
    return render(request, "blog/comment_form.html", {"form": form})
⚠️
Every POST form needs {% csrf_token %} inside the form tag, otherwise Django rejects the request with 403 Forbidden and a CSRF verification message. The same applies to AJAX sends — include the token from the cookie in the X-CSRFToken header.

Rendering the form

<form method="post" action="{% url 'blog:comment' post.pk %}">
  {% csrf_token %}
  {{ form.non_field_errors }}
  <div class="field">
    {{ form.text.label_tag }}
    {{ form.text }}
    {{ form.text.errors }}
  </div>
  <button type="submit">Post comment</button>
</form>
  • Render fields individually to control layout; {{ form.as_p }} is fine only for internal tools.
  • form.errors and form.non_field_errors must be rendered or the user sees a silent failure to submit.
  • Redirect after a successful POST so a refresh does not resubmit the form.

FAQ

Is the admin safe to expose?
It is a staff-only tool protected by authentication and permissions, but do not treat it as a public front end. Keep it on a private path or behind an IP allowlist, and give each operator the least permission they need.
ModelForm or plain Form?
Prefer ModelForm when the input maps to a model: it generates fields from the model, reuses validators and saves with one call. Use a plain Form for inputs that do not correspond to stored rows, such as a search or login form.

Views and URLs Forms, sessions and errors

Last refreshed 2026-09-18.