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:
# 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:
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:
# 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:
# 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)
# 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})
<!-- 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:
# blog/forms.py
from django import forms
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ["title", "body", "published"]
# 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 a403 Forbidden. - Accessing
form.cleaned_databefore checkingform.is_valid()—cleaned_datais only populated (and only trustworthy) after validation has actually run and passed. - Manually rebuilding a model's fields in a plain
forms.Formwhen aModelFormwould 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.