Membangun input data publik dengan Form dan ModelForm, merancang validasi bersih dan validasi per-field, menyesuaikan widgets dan error rendering, serta menyelesaikan form komentar dengan proteksi CSRF yang benar.

Admin di episode 8 memberi panel CRUD untuk staf, tetapi pengunjung blog juga perlu berinteraksi — mengirim komentar, mengisi form kontak. Di episode ini kita membangun Forms & Validation: sistem form Django yang menangani rendering HTML, validasi input, dan penyimpanan data secara konsisten.
Mengapa topik ini penting? Form adalah pintu masuk data dari luar sistem, dan pintu yang salah dibuka akan membocorkan data kotor, XSS, atau injeksi. Django menyediakan lapisan validasi berlapis (browser, form, dan model) plus proteksi CSRF bawaan — tetapi semuanya hanya berguna jika kalian memakainya dengan benar.
Ada dua jenis form di Django. Form untuk input yang tidak selalu terikat model (form kontak), dan ModelForm untuk form yang langsung memetakan field model (form komentar).
from django import forms
from .models import Comment
class ContactForm(forms.Form):
name = forms.CharField(max_length=100)
email = forms.EmailField()
subject = forms.CharField(max_length=150)
message = forms.CharField(widget=forms.Textarea)
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
fields = ["body"]
widgets = {
"body": forms.Textarea(attrs={"rows": 4, "placeholder": "Tulis komentar..."}),
}CommentForm mewarisi validasi dan konversi dari model Comment: field body yang TextField otomatis jadi textarea (lewat widgets), dan aturan model (misal blank) ikut berlaku. Form kontak memakai Form karena tidak ada model terkait — EmailField memastikan format email valid sejak di form.
Siklus form di view punya pola baku: POST + valid → simpan; selain itu → render ulang dengan error.
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404, redirect, render
from .forms import CommentForm
from .models import Post
@login_required
def add_comment(request, pk):
post = get_object_or_404(Post, pk=pk)
if request.method == "POST":
form = CommentForm(request.POST)
if form.is_valid():
comment = form.save(commit=False)
comment.post = post
comment.author = request.user
comment.save()
return redirect("blog:post_detail", pk=post.pk)
else:
form = CommentForm()
return render(request, "blog/add_comment.html", {"form": form, "post": post})form.is_valid() menjalankan seluruh pipeline validasi. form.save(commit=False) membuat objek model tanpa menyimpan, sehingga kita bisa mengisi post dan author yang berasal dari request, bukan dari input user — pola POST-Redirect-GET yang mencegah re-submit form saat refresh.
Django memvalidasi dalam urutan tetap — memahami ini memudahkan debugging:
to_python() — konversi nilai mentah ke tipe Python.validate() — validasi khusus field.run_validators() — validator umum.clean_<fieldname>() — validasi per-field kustom.clean() — validasi lintas field.Jika langkah 1-3 gagal, langkah 4-5 untuk field itu dilewati. Error terakumulasi di form.errors.
clean_<fieldname>()from django import forms
class ContactForm(forms.Form):
name = forms.CharField(max_length=100)
email = forms.EmailField()
message = forms.CharField(widget=forms.Textarea, min_length=10)
def clean_message(self):
message = self.cleaned_data["message"]
if "promo" in message.lower():
raise forms.ValidationError("Pesan tidak boleh mengandung kata 'promo'.")
return messageclean_message() dijalankan setelah validasi dasar lolos. Nilai yang sudah bersih ada di cleaned_data; jika melempar ValidationError, field ini dianggap gagal dan error tampil di bawah input.
clean()class ContactForm(forms.Form):
email = forms.EmailField()
email_confirm = forms.EmailField()
def clean(self):
cleaned = super().clean()
email = cleaned.get("email")
email_confirm = cleaned.get("email_confirm")
if email and email_confirm and email != email_confirm:
self.add_error("email_confirm", "Email dan konfirmasi harus sama.")
return cleanedclean() melihat seluruh data sekaligus. add_error(field, message) menempelkan error ke field spesifik; error tanpa field tampil di bagian atas form (__all__).
Tip
Prinsip "validasi di layer paling dekat dengan sumber data": periksa format di form (cepat, hemat query), lalu biarkan constraints database (unique, CheckConstraint) menjadi jaring pengaman terakhir di model. Jangan duplikasi aturan yang sama di dua tempat tanpa alasan — perbedaannya akan cepat melenceng.
Widget mengontrol representasi HTML sebuah field. Django punya TextInput, Textarea, Select, CheckboxInput, DateInput, dan banyak lagi:
class ContactForm(forms.Form):
name = forms.CharField(
max_length=100,
widget=forms.TextInput(attrs={"class": "form-control", "placeholder": "Nama"}),
)
email = forms.EmailField(
widget=forms.EmailInput(attrs={"class": "form-control"}),
)
message = forms.CharField(
widget=forms.Textarea(attrs={"rows": 5, "class": "form-control"}),
)attrs disisipkan ke tag HTML — cara umum memasang class Bootstrap/Tailwind. Untuk error rendering yang konsisten, template form memakai helper loop:
{% extends "base.html" %}
{% block content %}
<h2>Tambahkan Komentar</h2>
<form method="post">
{% csrf_token %}
{% for field in form %}
<div class="field{% if field.errors %} has-error{% endif %}">
{{ field.label_tag }}
{{ field }}
{% if field.help_text %}<small>{{ field.help_text }}</small>{% endif %}
{% for error in field.errors %}
<div class="error">{{ error }}</div>
{% endfor %}
</div>
{% endfor %}
<button type="submit">Kirim</button>
</form>
{% endblock %}{{ field.errors }} berisi daftar error; {{ field.label_tag }} menampilkan label. Alternatif cepat: {{ form.as_p }} merender semua field sekaligus, tetapi kurang fleksibel untuk layout khusus.
Setiap form POST di Django wajib membawa CSRF token. Tanpa {% csrf_token %}, view akan melempar 403 Forbidden. Mekanismenya:
csrftoken dan menyuntikkan token ke form.POST, middleware CsrfViewMiddleware membandingkan token dari form dengan cookie.Karena itu jangan pernah menonaktifkan CSRF (@csrf_exempt) kecuali untuk API tanpa session yang memakai mekanisme auth lain — ini kita bedah saat DRF di episode 11 dan keamanan CSRF lebih dalam di episode 18. Untuk semua form publik berbasis session, biarkan default.
Warning
@csrf_exempt pada form biasa adalah salah satu lubang keamanan yang paling sering dibuat pemula. CSRF attack memanfaatkan browser korban yang masih punya session valid — form tanpa token artinya form itu bisa dipicu oleh halaman jahat mana pun. Gunakan @csrf_exempt hanya untuk API endpoint yang ber-authentikasi lewat header, bukan cookie.
Inti yang harus dibawa pulang:
Form untuk input tanpa model; ModelForm untuk form yang terikat model.POST → is_valid() → simpan → redirect (PRG).to_python → validators → clean_<field> → clean.field.errors memberi error rendering yang konsisten.{% csrf_token %} wajib di semua form POST — jangan pernah @csrf_exempt tanpa alasan API.Di episode 10 selanjutnya kita masuk ke sistem yang menggerakkan semua form di atas: Authentication & User Management — model User bawaan, login/logout, permissions & groups, RBAC, dan custom user model sebagai best practice sejak awal. Sampai jumpa di episode 10!