Forms and CSRF

Introduction

Django forms validate and render HTML input, bridging browser POST data to models and views. ModelForm generates fields from your Post model; Form handles arbitrary input. Every POST form must include CSRF protection—Django’s built-in defense against cross-site request forgery. This chapter builds create/edit post flows with FBV and templates, explains validation, and notes safe CSRF patterns for AJAX.

Prerequisites

forms.Form vs forms.ModelForm

TypeUse
forms.FormSearch boxes, contact forms, non-model data
forms.ModelFormCreate/update model instances

Manual Form example

blog/forms.py:

python
from django import forms
 
 
class SearchForm(forms.Form):
    q = forms.CharField(label="Search", max_length=100, required=False)
    sort = forms.ChoiceField(
        choices=[("newest", "Newest"), ("oldest", "Oldest")],
        required=False,
    )

View:

python
def search(request):
    form = SearchForm(request.GET or None)
    results = Post.objects.all()
    if form.is_valid():
        q = form.cleaned_data.get("q")
        if q:
            results = results.filter(title__icontains=q)
    return render(request, "blog/search.html", {"form": form, "results": results})

Use request.GET for search (idempotent); request.POST for mutations.

ModelForm for Post

blog/forms.py:

python
from django import forms
 
from .models import Post
 
 
class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ["title", "slug", "body", "published", "tags"]
        widgets = {
            "body": forms.Textarea(attrs={"rows": 12}),
        }

Exclude sensitive fields:

python
fields = ["title", "body", "published"]  # slug auto-generated in view

Or:

python
exclude = ["author", "created_at", "updated_at"]

Create View with FBV

blog/views.py:

Code explanation:

  • commit=False — set author before first save
  • save_m2m() — persists tags after instance has PK
  • @login_required — redirect anonymous users to login (chapter 14)

Update View

Object-level checks prevent editing others’ posts.

Template with CSRF

blog/templates/blog/post_form.html:

{% csrf_token %} renders a hidden input Django validates on POST.

Manual field rendering

django
<p>
  <label for="{{ form.title.id_for_label }}">Title</label>
  {{ form.title }}
  {{ form.title.errors }}
</p>

Better control over HTML/CSS than {{ form.as_p }}.

CSRF How It Works

text
GET form page  → Django sets csrftoken cookie + hidden field in form
POST submit    → Middleware compares cookie token vs POST csrfmiddlewaretoken
Mismatch/missing → 403 Forbidden

Settings (defaults usually fine):

python
CSRF_COOKIE_SECURE = True      # production HTTPS
CSRF_TRUSTED_ORIGINS = ["https://example.com"]

AJAX POST (Concept)

Read token from cookie and send header:

DRF and SPA patterns — chapter 20.

@csrf_exempt

python
from django.views.decorators.csrf import csrf_exempt
 
@csrf_exempt
def webhook(request):
    ...

Use only for trusted webhooks with alternate auth (signature verification)—never on user-facing forms.

Validation Layers

LayerWhere
HTML5required, maxlength on widgets
Field validatorsforms.CharField(max_length=200)
Form clean_*Cross-field rules
Modelvalidators=, clean() on model
DBunique=True, constraints

clean_* methods

Errors appear in form.errors and {{ field.errors }}.

ModelForm with CreateView (CBV)

Template still needs {% csrf_token %}—CBV includes it via FormView.

URL Wiring

blog/urls.py:

python
path("posts/new/", views.post_create, name="post-create"),
path("posts/<int:pk>/edit/", views.post_edit, name="post-edit"),

Or CBV:

python
path("posts/new/", views.PostCreateView.as_view(), name="post-create"),

Common Mistakes

MistakeSymptomFix
Missing {% csrf_token %}403 CSRF verification failedAdd token to POST forms
Forgot save_m2m()Tags not savedAfter commit=False
POST without form.is_valid()Invalid data savedAlways validate before save
@csrf_exempt on user formsCSRF vulnerabilityRemove exempt
Editing request.POST immutableCannot modifyCopy to mutable QueryDict if needed

Post-Chapter Checklist

  • PostForm ModelForm with validation
  • Create/edit views handle GET and POST
  • Templates include {% csrf_token %}
  • Field and form errors display to users
  • You know when csrf_exempt is acceptable

FAQ## FAQ

File upload forms?

Set enctype="multipart/form-data" and request.FILES in view—media in chapter 16.

Disable CSRF for API?

JSON APIs often use token/JWT auth instead—DRF handles this; do not mix exempt FBV with session auth carelessly.

form.as_table vs as_p?

as_p wraps fields in <p>; as_table for tables—manual rendering preferred for production CSS.

Repeat POST on refresh?

Redirect after successful POST (PRG pattern)—redirect() after save.

Custom error CSS?

Add error_css_class = "is-invalid" in Meta or widget attrs.