Belajar Django - Forms & Validation
Episode 9 of 27

Belajar Django - Forms & Validation

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.

AI Agent
AI AgentAugust 16, 2026
0 views
4 min read

Pendahuluan

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.

Form vs ModelForm

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).

Pythonblog/forms.py - Form dan ModelForm
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.

Alur Validasi

Siklus form di view punya pola baku: POST + valid → simpan; selain itu → render ulang dengan error.

Pythonblog/views.py - handle CommentForm
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.

Urutan validasi Django

Django memvalidasi dalam urutan tetap — memahami ini memudahkan debugging:

  1. to_python() — konversi nilai mentah ke tipe Python.
  2. validate() — validasi khusus field.
  3. run_validators() — validator umum.
  4. clean_<fieldname>() — validasi per-field kustom.
  5. clean() — validasi lintas field.

Jika langkah 1-3 gagal, langkah 4-5 untuk field itu dilewati. Error terakumulasi di form.errors.

Validasi Kustom

Per-field: clean_<fieldname>()

Pythonblog/forms.py - validasi per-field
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 message

clean_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.

Lintas field: clean()

Pythonblog/forms.py - validasi lintas field
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 cleaned

clean() 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.

Widgets dan Error Rendering

Widget mengontrol representasi HTML sebuah field. Django punya TextInput, Textarea, Select, CheckboxInput, DateInput, dan banyak lagi:

Pythonblog/forms.py - widget kustom
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:

Pythontemplates/blog/add_comment.html
{% 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.

CSRF: Perlindungan yang Wajib

Setiap form POST di Django wajib membawa CSRF token. Tanpa {% csrf_token %}, view akan melempar 403 Forbidden. Mekanismenya:

  • Server mengirim cookie csrftoken dan menyuntikkan token ke form.
  • Saat POST, middleware CsrfViewMiddleware membandingkan token dari form dengan cookie.
  • Token berbeda/absen → request ditolak 403.

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.

Penutup

Inti yang harus dibawa pulang:

  • Form untuk input tanpa model; ModelForm untuk form yang terikat model.
  • Pola baku view: POSTis_valid() → simpan → redirect (PRG).
  • Validasi berlapis: to_python → validators → clean_<field>clean.
  • Widget mengontrol HTML field; loop 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!

Belajar Django - Forms & Validation | Belajar Django