CodeOath
← All posts
Python65 min total · 18 parts

Django Fundamentals: The ORM, Migrations, and Shipping a Real App

Part 12 of 18 · ~2 min

Forms: Requesting a Loan Without Trusting the Request Either

class LoanRequestForm(forms.ModelForm):
    class Meta:
        model = Loan
        fields = ["due_date"]

    def clean_due_date(self):
        due_date = self.cleaned_data["due_date"]
        today = timezone.now().date()
        if due_date <= today:
            raise forms.ValidationError("The return date has to be in the future.")
        if (due_date - today).days > 21:
            raise forms.ValidationError("ToolShed loans can't run longer than three weeks.")
        return due_date
<form method="post">
  {% csrf_token %}
  {{ form.as_p }}
  <button type="submit">Request this tool</button>
</form>

Notice LoanRequestForm never redeclares what kind of field due_date is — Meta.model and Meta.fields are enough for it to borrow that straight from Loan itself, so a rule about the field only has to be stated in one place rather than kept in sync between two. Calling form.is_valid() is what actually runs clean_due_date and every other field's own validation, and the payoff for that is form.cleaned_data, a dictionary of values that have genuinely been converted to real Python types and checked — a real datetime.date for due_date, not the raw string sitting in request.POST["due_date"] before any of that happened. clean_due_date is the right home for a rule about that one field specifically; a bare clean(), with no field name in its own name, is reserved for a rule that needs to look at two or more fields together to make sense.

CSRF protection, and why the tag can't be skipped

Every one of these forms also needs {% csrf_token %} in the template. CsrfViewMiddleware rejects a POST that's missing a valid token with a 403, specifically to block a malicious page elsewhere on the internet from silently submitting a request to ToolShed on a logged-in visitor's behalf — the attack this defends against works because a browser attaches a logged-in visitor's session cookie to any request that lands on ToolShed's domain, regardless of which page actually triggered it. The token is what proves the request genuinely came from a page ToolShed itself rendered, rather than from a form sitting on some unrelated site. Leave the tag out and the form will look completely fine until the exact moment someone actually submits it — a 403 is one of the first genuinely confusing errors most people building their first Django form run straight into. A request sent by JavaScript instead of an HTML form needs the same protection through a different door — the token has to be read out of the cookie Django sets and attached as an X-CSRFToken header by hand, since there's no {% csrf_token %} tag available inside a fetch() call.