Django Admin and Forms

Registering models with the admin site and building forms with Form and ModelForm.

Registering a model with the admin site

Django's admin site — a full CRUD interface for your models, generated automatically — is one of its most distinctive built-in features. Registering a model takes one line:

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

admin.site.register(Author)
admin.site.register(Post)

Before it's usable, create an admin (superuser) account:

Bash
python3 manage.py createsuperuser

Then visit /admin/ and log in — every registered model now has a working list view, a detail/edit form, search, and delete actions, none of which you wrote by hand.

Customizing the admin

The default registration works, but a ModelAdmin subclass lets you shape exactly what the admin shows:

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

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ("title", "author", "published", "created_at")
    list_filter = ("published", "author")
    search_fields = ("title", "body")
    prepopulated_fields = {"slug": ("title",)}

admin.site.register(Author)
  • list_display — which columns show in the list view (by default, it's just each object's __str__).
  • list_filter — adds a sidebar of filters, generated from the given fields.
  • search_fields — enables a search box across the given fields.

For a real internal tool, the admin site alone is often enough to let non-engineers manage content — no separate CRUD UI needs to be hand-built at all.

Django forms: forms.Form

A forms.Form describes a set of fields, their types, and their validation rules, independent of any specific model:

Python
# blog/forms.py
from django import forms

class ContactForm(forms.Form):
    name = forms.CharField(max_length=100)
    email = forms.EmailField()
    message = forms.CharField(widget=forms.Textarea)
Python
# blog/views.py
from django.shortcuts import render
from .forms import ContactForm

def contact(request):
    if request.method == "POST":
        form = ContactForm(request.POST)
        if form.is_valid():
            name = form.cleaned_data["name"]
            email = form.cleaned_data["email"]
            message = form.cleaned_data["message"]
            # ... send the email, save it, whatever the app needs
            return render(request, "blog/thank_you.html")
    else:
        form = ContactForm()

    return render(request, "blog/contact.html", {"form": form})
HTML
<!-- blog/templates/blog/contact.html -->
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Send</button>
</form>

form.is_valid() runs every field's validation (required-ness, type, EmailField's format check, and any custom validators) and populates form.cleaned_data only if everything passes. {% csrf_token %} is required on every POST form — Django's CSRF protection is on by default and rejects an unprotected POST request outright.

Django forms: forms.ModelForm

When a form maps directly to a model — which is extremely common — ModelForm generates the fields for you straight from the model definition:

Python
# blog/forms.py
from django import forms
from .models import Post

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ["title", "body", "published"]
Python
# blog/views.py
from django.shortcuts import render, redirect
from .forms import PostForm

def create_post(request):
    if request.method == "POST":
        form = PostForm(request.POST)
        if form.is_valid():
            form.save()   # creates and saves a new Post directly from the validated data
            return redirect("post_list")
    else:
        form = PostForm()

    return render(request, "blog/post_form.html", {"form": form})

ModelForm derives its fields, widgets, and basic validation (max lengths, required-ness) straight from the model's own field definitions — the fields list on Meta controls which of the model's fields the form exposes at all. form.save() both validates-adjacent (it assumes is_valid() already passed) and persists the result in one call, which is why editing an existing Post looks almost identical: PostForm(request.POST, instance=existing_post).

Common mistakes

  • Forgetting {% csrf_token %} in a template form — Django's CSRF middleware rejects the POST outright with a 403 Forbidden.
  • Accessing form.cleaned_data before checking form.is_valid()cleaned_data is only populated (and only trustworthy) after validation has actually run and passed.
  • Manually rebuilding a model's fields in a plain forms.Form when a ModelForm would derive them automatically from the model — extra code to keep in sync every time the model changes.

Interview questions

Q: What real, practical value does Django's built-in admin site provide? It gives every registered model a complete, working CRUD interface — list views with search and filtering, detail/edit forms with validation, and delete actions — generated automatically from the model definition, with zero custom UI code. For internal tools and content management, this alone can remove the need to build a separate admin dashboard, and it stays in sync automatically as models change.

Q: What's the difference between forms.Form and forms.ModelForm? forms.Form is a plain, model-independent set of fields and validation rules you define entirely by hand — appropriate for something that isn't a direct one-to-one match with a database model, like a contact form. forms.ModelForm generates its fields, widgets, and basic validation directly from an existing model (via its Meta.model and Meta.fields), and adds a save() method that persists a valid submission straight to the database — the standard choice whenever a form exists specifically to create or edit a model instance.