Forms
Django forms do three jobs: they render HTML inputs, check what the user sent, and turn the text into proper Python values, with error messages for anything wrong.
Defining and validating a form
import django
from django.conf import settings
settings.configure()
django.setup()
from django import forms
class SignupForm(forms.Form):
name = forms.CharField(max_length=50)
email = forms.EmailField()
age = forms.IntegerField(min_value=13)
newsletter = forms.BooleanField(required=False)
good = SignupForm({"name": "Asha", "email": "asha@example.com", "age": "21"})
print(good.is_valid())
print(good.cleaned_data)
bad = SignupForm({"name": "", "email": "not-an-email", "age": "9"})
print(bad.is_valid())
for field, errors in bad.errors.items():
print(f"{field}: {errors[0]}")- Data arrives as text, like
"21". Afteris_valid(),cleaned_dataholds proper Python values:ageis the number 21, and the untickednewsletterisFalse. form.errorsmaps each field to its error messages.- Every field is required unless you say
required=False.
Custom validation
import django
from django.conf import settings
settings.configure()
django.setup()
from django import forms
class RegisterForm(forms.Form):
username = forms.CharField(min_length=3)
password = forms.CharField(min_length=8)
confirm = forms.CharField()
def clean_username(self):
username = self.cleaned_data["username"].lower()
if username in {"admin", "root"}:
raise forms.ValidationError("That username is reserved.")
return username # what you return becomes the cleaned value
def clean(self):
cleaned = super().clean()
if cleaned.get("password") != cleaned.get("confirm"):
self.add_error("confirm", "The passwords don't match.")
return cleaned
form = RegisterForm({"username": "Admin", "password": "secret-pass", "confirm": "secret-pas"})
print(form.is_valid())
print(dict(form.errors))
form = RegisterForm({"username": "Asha", "password": "secret-pass", "confirm": "secret-pass"})
print(form.is_valid(), form.cleaned_data["username"])- A
clean_<field>()method checks one field. It can also change the value, as lower-casing the username does. clean()checks fields together, such as two passwords matching, and attaches errors withadd_error.
Rendering a form
import django
from django.conf import settings
settings.configure()
django.setup()
from django import forms
class ContactForm(forms.Form):
email = forms.EmailField(label="Your email")
message = forms.CharField(widget=forms.Textarea(attrs={"rows": 3}))
print(ContactForm().as_p())In a template, {{ form.as_p }} renders every field with its label, and Django adds the HTML attributes (required, type="email", maxlength) that match your validation. For full control of the layout, render fields one by one:
<form method="post">
{% csrf_token %}
{{ form.email.label_tag }} {{ form.email }}
{% for error in form.email.errors %}<p class="error">{{ error }}</p>{% endfor %}
<button>Send</button>
</form>{% csrf_token %} is required in every POST form. It protects against cross-site request forgery, where another site tricks a logged-in user's browser into submitting your form.
ModelForm: a form built from a model
import django
from django.conf import settings
settings.configure(
INSTALLED_APPS=["django.contrib.contenttypes"],
DATABASES={"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}},
)
django.setup()
from django import forms
from django.db import connection, models
class Course(models.Model):
title = models.CharField(max_length=100)
price = models.PositiveIntegerField()
class Meta:
app_label = "courses"
with connection.schema_editor() as editor:
editor.create_model(Course)
class CourseForm(forms.ModelForm):
class Meta:
model = Course
fields = ["title", "price"]
form = CourseForm({"title": "Django", "price": "-5"})
print(form.is_valid(), dict(form.errors))
form = CourseForm({"title": "Django", "price": "399"})
if form.is_valid():
course = form.save() # creates the row
print("Saved course", course.id, course.title, course.price)
print(Course.objects.count(), "course in the database")A ModelForm takes its fields and validation from the model, here rejecting a negative price because the field is a PositiveIntegerField, and save() creates or updates the object.
Handling a form in a view
from django.shortcuts import redirect, render
from .forms import CourseForm
def new_course(request):
if request.method == "POST":
form = CourseForm(request.POST) # bind the submitted data
if form.is_valid():
form.save()
return redirect("course_list") # Post/Redirect/Get
else:
form = CourseForm() # an empty form for the first visit
return render(request, "courses/new_course.html", {"form": form})This shape appears in almost every Django project. On the first visit, show an empty form. When it's submitted, validate it: if it's valid, save and redirect, so refreshing can't submit twice; if not, render the same form again, which now shows the user's input with the error messages.