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