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