Skip to main content

Forms & User Input

Django forms provide a way to handle HTML forms, validate user input, and convert form data to Python types.

Form Basics

Creating Forms

# forms.py
from django import forms
from .models import Article

class ContactForm(forms.Form):
name = forms.CharField(max_length=100)
email = forms.EmailField()
subject = forms.CharField(max_length=200)
message = forms.CharField(widget=forms.Textarea)

def clean_email(self):
email = self.cleaned_data['email']
if not email.endswith('@company.com'):
raise forms.ValidationError('Must use company email')
return email

Model Forms

class ArticleForm(forms.ModelForm):
class Meta:
model = Article
fields = ['title', 'content', 'tags']
widgets = {
'content': forms.Textarea(attrs={'rows': 10}),
'tags': forms.CheckboxSelectMultiple(),
}

def clean_title(self):
title = self.cleaned_data['title']
if len(title) < 5:
raise forms.ValidationError('Title must be at least 5 characters')
return title

Form Handling in Views

Function-Based Views

from django.shortcuts import render, redirect
from django.contrib import messages
from .forms import ContactForm, ArticleForm

def contact_view(request):
if request.method == 'POST':
form = ContactForm(request.POST)
if form.is_valid():
# Process form data
name = form.cleaned_data['name']
email = form.cleaned_data['email']
# Send email, save to database, etc.
messages.success(request, 'Thank you for your message!')
return redirect('contact_success')
else:
form = ContactForm()

return render(request, 'contact.html', {'form': form})

def create_article(request):
if request.method == 'POST':
form = ArticleForm(request.POST, request.FILES)
if form.is_valid():
article = form.save(commit=False)
article.author = request.user
article.save()
form.save_m2m() # Save many-to-many relationships
return redirect('article_detail', pk=article.pk)
else:
form = ArticleForm()

return render(request, 'articles/create.html', {'form': form})

Class-Based Views

from django.views.generic import FormView, CreateView, UpdateView
from django.urls import reverse_lazy

class ContactFormView(FormView):
template_name = 'contact.html'
form_class = ContactForm
success_url = reverse_lazy('contact_success')

def form_valid(self, form):
# Process form data
messages.success(self.request, 'Thank you for your message!')
return super().form_valid(form)

class ArticleCreateView(CreateView):
model = Article
form_class = ArticleForm
template_name = 'articles/create.html'

def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)

Form Templates

Basic Form Rendering

<!-- Manual form rendering -->
<form method="post">
{% csrf_token %}

<div class="field">
<label for="{{ form.name.id_for_label }}">Name:</label>
{{ form.name }} {% if form.name.errors %}
<ul class="errors">
{% for error in form.name.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>

<div class="field">
<label for="{{ form.email.id_for_label }}">Email:</label>
{{ form.email }} {{ form.email.errors }}
</div>

<button type="submit">Submit</button>
</form>

<!-- Automatic form rendering -->
<form method="post">
{% csrf_token %} {{ form.as_p }}
<button type="submit">Submit</button>
</form>

Form with Bootstrap

<form method="post" class="needs-validation" novalidate>
{% csrf_token %} {% for field in form %}
<div class="mb-3">
<label for="{{ field.id_for_label }}" class="form-label">
{{ field.label }}
</label>
{{ field|add_class:"form-control" }} {% if field.errors %}
<div class="invalid-feedback d-block">
{% for error in field.errors %} {{ error }} {% endfor %}
</div>
{% endif %} {% if field.help_text %}
<div class="form-text">{{ field.help_text }}</div>
{% endif %}
</div>
{% endfor %}

<button type="submit" class="btn btn-primary">Submit</button>
</form>

Advanced Form Features

Formsets

# Inline formsets for related models
from django.forms import inlineformset_factory

AuthorBookFormSet = inlineformset_factory(
Author, Book, fields=('title', 'publication_date'), extra=1
)

def author_books(request, author_id):
author = get_object_or_404(Author, id=author_id)

if request.method == 'POST':
formset = AuthorBookFormSet(request.POST, instance=author)
if formset.is_valid():
formset.save()
return redirect('author_detail', pk=author.pk)
else:
formset = AuthorBookFormSet(instance=author)

return render(request, 'author_books.html', {
'author': author,
'formset': formset
})

File Uploads

class ArticleForm(forms.ModelForm):
image = forms.ImageField(required=False)
attachment = forms.FileField(required=False)

class Meta:
model = Article
fields = ['title', 'content', 'image', 'attachment']

def clean_attachment(self):
attachment = self.cleaned_data.get('attachment')
if attachment:
if attachment.size > 5 * 1024 * 1024: # 5MB limit
raise forms.ValidationError('File too large')
if not attachment.name.endswith('.pdf'):
raise forms.ValidationError('Only PDF files allowed')
return attachment
<!-- File upload form -->
<form method="post" enctype="multipart/form-data">
{% csrf_token %} {{ form.as_p }}
<button type="submit">Upload</button>
</form>

Custom Widgets

class DatePickerWidget(forms.DateInput):
def __init__(self, attrs=None):
default_attrs = {'class': 'datepicker', 'type': 'date'}
if attrs:
default_attrs.update(attrs)
super().__init__(attrs=default_attrs)

class ArticleForm(forms.ModelForm):
publication_date = forms.DateField(widget=DatePickerWidget())

class Meta:
model = Article
fields = ['title', 'content', 'publication_date']
widgets = {
'content': forms.Textarea(attrs={
'rows': 10,
'class': 'form-control',
'placeholder': 'Enter article content...'
}),
}

Form Validation

Field-Level Validation

class ContactForm(forms.Form):
phone = forms.CharField(max_length=20)

def clean_phone(self):
phone = self.cleaned_data['phone']
import re
if not re.match(r'^\+?[1-9]\d{1,14}$', phone):
raise forms.ValidationError('Invalid phone number format')
return phone

Form-Level Validation

class RegistrationForm(forms.Form):
password = forms.CharField(widget=forms.PasswordInput)
password_confirm = forms.CharField(widget=forms.PasswordInput)

def clean(self):
cleaned_data = super().clean()
password = cleaned_data.get('password')
password_confirm = cleaned_data.get('password_confirm')

if password and password_confirm:
if password != password_confirm:
raise forms.ValidationError('Passwords do not match')

return cleaned_data

Custom Validators

from django.core.exceptions import ValidationError

def validate_even(value):
if value % 2 != 0:
raise ValidationError('%(value)s is not even', params={'value': value})

class NumberForm(forms.Form):
even_number = forms.IntegerField(validators=[validate_even])

AJAX Forms

JavaScript Form Submission

// AJAX form submission
document
.getElementById('contact-form')
.addEventListener('submit', function (e) {
e.preventDefault();

const formData = new FormData(this);

fetch(this.action, {
method: 'POST',
body: formData,
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]')
.value,
},
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Handle success
document.getElementById('message').innerHTML = data.message;
} else {
// Handle errors
displayFormErrors(data.errors);
}
});
});

AJAX View

from django.http import JsonResponse

def ajax_contact(request):
if request.method == 'POST':
form = ContactForm(request.POST)
if form.is_valid():
# Process form
return JsonResponse({
'success': True,
'message': 'Thank you for your message!'
})
else:
return JsonResponse({
'success': False,
'errors': form.errors
})

return JsonResponse({'success': False, 'error': 'Invalid request'})

Form Security

CSRF Protection

<!-- Always include CSRF token -->
<form method="post">
{% csrf_token %}
<!-- form fields -->
</form>
# For AJAX requests
from django.views.decorators.csrf import csrf_exempt
from django.middleware.csrf import get_token

# Get CSRF token for JavaScript
def get_csrf_token(request):
return JsonResponse({'csrfToken': get_token(request)})

Input Sanitization

import bleach

class CommentForm(forms.Form):
content = forms.CharField(widget=forms.Textarea)

def clean_content(self):
content = self.cleaned_data['content']
# Allow only safe HTML tags
allowed_tags = ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li']
cleaned_content = bleach.clean(content, tags=allowed_tags)
return cleaned_content