Apprenez à créer, valider et gérer les formulaires dans Django. Ce guide complet vous montre comment utiliser Form, ModelForm, la validation personnalisée, les vues et les bonnes pratiques professionnelles pour vos applications web.
Introduction aux Formulaires Django
Les formulaires sont au cœur de toute application web interactive. Django fournit un système de formulaires puissant qui simplifie la collecte, la validation et le traitement des données utilisateur. Dans ce guide, nous allons explorer les formulaires Django de manière approfondie.
Pourquoi les Formulaires Django sont Excellents
- Validation automatique des données
- Protection CSRF intégrée
- Génération HTML automatique
- Nettoyage et normalisation des données
- Messages d'erreur contextuels
Types de Formulaires Django
- Formulaires Standards (forms.Form)
- Formulaires Modèles (ModelForm)
- Formulaires avec Validation Personnalisée
Création d'un Formulaire de Base
Commençons par créer un formulaire de contact simple :
# forms.py
from django import forms
from django.core.validators import validate_email
class ContactForm(forms.Form):
SUJET_CHOICES = [
('', 'Sélectionnez un sujet'),
('tech', 'Support technique'),
('billing', 'Facturation'),
('general', 'Question générale'),
]
nom = forms.CharField(
max_length=100,
label='Votre nom',
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Entrez votre nom complet'
})
)
email = forms.EmailField(
label='Adresse email',
widget=forms.EmailInput(attrs={
'class': 'form-control',
'placeholder': 'votre@email.com'
})
)
sujet = forms.ChoiceField(
choices=SUJET_CHOICES,
widget=forms.Select(attrs={'class': 'form-control'})
)
message = forms.CharField(
widget=forms.Textarea(attrs={
'class': 'form-control',
'rows': 5,
'placeholder': 'Décrivez votre demande...'
})
)
newsletter = forms.BooleanField(
required=False,
label='S'abonner à la newsletter'
)
def clean_nom(self):
nom = self.cleaned_data['nom']
if len(nom) < 2:
raise forms.ValidationError("Le nom doit contenir au moins 2 caractères.")
return nom
def clean_message(self):
message = self.cleaned_data['message']
if len(message) < 10:
raise forms.ValidationError("Le message doit contenir au moins 10 caractères.")
return message
Utilisation des ModelForm
Les ModelForm permettent de créer des formulaires directement liés à vos modèles :
# models.py
from django.db import models
from django.contrib.auth.models import User
class Article(models.Model):
STATUT_CHOICES = [
('brouillon', 'Brouillon'),
('publie', 'Publié'),
('archive', 'Archivé'),
]
titre = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
contenu = models.TextField()
auteur = models.ForeignKey(User, on_delete=models.CASCADE)
date_creation = models.DateTimeField(auto_now_add=True)
date_publication = models.DateTimeField(null=True, blank=True)
statut = models.CharField(max_length=20, choices=STATUT_CHOICES, default='brouillon')
image_couverture = models.ImageField(upload_to='articles/', null=True, blank=True)
def __str__(self):
return self.titre
# forms.py
from django import forms
from .models import Article
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
fields = ['titre', 'slug', 'contenu', 'statut', 'image_couverture']
widgets = {
'titre': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': "Titre de l'article"
}),
'slug': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'slug-de-l-article'
}),
'contenu': forms.Textarea(attrs={
'class': 'form-control',
'rows': 15
}),
'statut': forms.Select(attrs={'class': 'form-control'}),
}
labels = {
'titre': "Titre de l'article",
'slug': 'URL simplifiée',
'contenu': 'Contenu',
}
help_texts = {
'slug': "Utilisez des traits d'union, pas d'espaces ni de caractères spéciaux",
}
def clean_slug(self):
slug = self.cleaned_data['slug']
if not slug.replace('-', '').isalnum():
raise forms.ValidationError("Le slug ne doit contenir que des lettres, chiffres et traits d'union.")
return slug
def clean(self):
cleaned_data = super().clean()
statut = cleaned_data.get('statut')
date_publication = cleaned_data.get('date_publication')
if statut == 'publie' and not date_publication:
cleaned_data['date_publication'] = timezone.now()
return cleaned_data
Vues pour Gérer les Formulaires
Voici comment gérer les formulaires dans les vues :
# views.py
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.views.generic import CreateView, UpdateView, ListView
from .models import Article
from .forms import ContactForm, ArticleForm
# Vue fonction-based pour le formulaire de contact
def contact_view(request):
if request.method == 'POST':
form = ContactForm(request.POST)
if form.is_valid():
# Traitement des données valides
nom = form.cleaned_data['nom']
email = form.cleaned_data['email']
sujet = form.cleaned_data['sujet']
message = form.cleaned_data['message']
newsletter = form.cleaned_data['newsletter']
# Ici vous pourriez envoyer un email ou sauvegarder en base
messages.success(
request,
f"Merci {nom}, votre message a été envoyé avec succès !"
)
return redirect('contact_success')
else:
form = ContactForm()
return render(request, 'contact/contact.html', {'form': form})
# Vue classe-based pour créer un article
class ArticleCreateView(CreateView):
model = Article
form_class = ArticleForm
template_name = 'articles/article_form.html'
success_url = '/articles/'
def form_valid(self, form):
form.instance.auteur = self.request.user
messages.success(self.request, "Article créé avec succès !")
return super().form_valid(form)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = 'Créer un nouvel article'
return context
# Vue pour éditer un article
class ArticleUpdateView(UpdateView):
model = Article
form_class = ArticleForm
template_name = 'articles/article_form.html'
def get_success_url(self):
return f'/articles/{self.object.id}/'
def form_valid(self, form):
messages.success(self.request, "Article mis à jour avec succès !")
return super().form_valid(form)
def get_queryset(self):
# Sécurité : l'utilisateur ne peut éditer que ses propres articles
return Article.objects.filter(auteur=self.request.user)
Templates pour les Formulaires
<!-- contact/contact.html -->
{% extends 'base.html' %}
{% load static %}
{% block content %}
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h2 class="mb-0">Contactez-nous</h2>
</div>
<div class="card-body">
<!-- Affichage des messages -->
{% if messages %}
<div class="messages">
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
</div>
{% endif %}
<!-- Formulaire -->
<form method="post" novalidate>
{% csrf_token %}
<!-- Affichage des erreurs non-field -->
{% if form.non_field_errors %}
<div class="alert alert-danger">
{% for error in form.non_field_errors %}
<p class="mb-0">{{ error }}</p>
{% endfor %}
</div>
{% endif %}
<!-- Champs du formulaire -->
{% for field in form %}
<div class="mb-3">
<label for="{{ field.id_for_label }}" class="form-label">
{{ field.label }}
{% if field.field.required %}
<span class="text-danger">*</span>
{% endif %}
</label>
{{ field }}
<!-- Aide et erreurs -->
{% if field.help_text %}
<div class="form-text">{{ field.help_text }}</div>
{% endif %}
{% if field.errors %}
<div class="invalid-feedback d-block">
{% for error in field.errors %}
<p class="mb-0">{{ error }}</p>
{% endfor %}
</div>
{% endif %}
</div>
{% endfor %}
<div class="d-grid">
<button type="submit" class="btn btn-primary btn-lg">
<i class="fas fa-paper-plane me-2"></i>
Envoyer le message
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_css %}
<style>
.form-control:focus {
border-color: #0d6efd;
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
}
.is-invalid {
border-color: #dc3545;
}
</style>
{% endblock %}
Validation Avancée
# validators.py
from django.core.exceptions import ValidationError
from django.utils import timezone
import re
def validate_password_strength(password):
"""Valide la force du mot de passe"""
if len(password) < 8:
raise ValidationError("Le mot de passe doit contenir au moins 8 caractères.")
if not re.search(r'[A-Z]', password):
raise ValidationError("Le mot de passe doit contenir au moins une majuscule.")
if not re.search(r'[a-z]', password):
raise ValidationError("Le mot de passe doit contenir au moins une minuscule.")
if not re.search(r'[0-9]', password):
raise ValidationError("Le mot de passe doit contenir au moins un chiffre.")
def validate_future_date(value):
"""Valide que la date est dans le futur"""
if value < timezone.now().date():
raise ValidationError("La date doit être dans le futur.")
# forms.py - Formulaire avec validation avancée
class InscriptionForm(forms.Form):
email = forms.EmailField()
mot_de_passe = forms.CharField(
widget=forms.PasswordInput,
validators=[validate_password_strength]
)
confirmation_mot_de_passe = forms.CharField(widget=forms.PasswordInput)
date_naissance = forms.DateField(
validators=[validate_future_date],
widget=forms.DateInput(attrs={'type': 'date'})
)
def clean(self):
cleaned_data = super().clean()
mot_de_passe = cleaned_data.get('mot_de_passe')
confirmation = cleaned_data.get('confirmation_mot_de_passe')
if mot_de_passe and confirmation and mot_de_passe != confirmation:
self.add_error('confirmation_mot_de_passe',
"Les mots de passe ne correspondent pas.")
return cleaned_data
Formulaires avec Fichiers
# forms.py
class ArticleAvecFichierForm(forms.ModelForm):
fichier_joint = forms.FileField(
required=False,
widget=forms.FileInput(attrs={
'class': 'form-control',
'accept': '.pdf,.doc,.docx,.jpg,.png'
})
)
class Meta:
model = Article
fields = ['titre', 'contenu', 'fichier_joint']
def clean_fichier_joint(self):
fichier = self.cleaned_data.get('fichier_joint')
if fichier:
# Vérification de la taille (5MB max)
if fichier.size > 5 * 1024 * 1024:
raise forms.ValidationError("Le fichier ne doit pas dépasser 5MB.")
# Vérification de l'extension
extensions_autorisees = ['.pdf', '.doc', '.docx', '.jpg', '.png']
if not any(fichier.name.lower().endswith(ext) for ext in extensions_autorisees):
raise forms.ValidationError(
"Type de fichier non autorisé. Utilisez PDF, DOC, JPG ou PNG."
)
return fichier
# views.py
def create_article_with_file(request):
if request.method == 'POST':
form = ArticleAvecFichierForm(request.POST, request.FILES)
if form.is_valid():
article = form.save(commit=False)
article.auteur = request.user
article.save()
messages.success(request, "Article créé avec fichier joint !")
return redirect('article_detail', pk=article.pk)
else:
form = ArticleAvecFichierForm()
return render(request, 'articles/create_with_file.html', {'form': form})
FormSets pour Formulaires Multiples
# forms.py
from django.forms import modelformset_factory
ArticleFormSet = modelformset_factory(
Article,
fields=('titre', 'statut'),
extra=3,
can_delete=True,
widgets={
'titre': forms.TextInput(attrs={'class': 'form-control'}),
'statut': forms.Select(attrs={'class': 'form-control'}),
}
)
# views.py
def manage_articles(request):
if request.method == 'POST':
formset = ArticleFormSet(request.POST, queryset=Article.objects.filter(auteur=request.user))
if formset.is_valid():
instances = formset.save(commit=False)
for instance in instances:
if not instance.auteur_id:
instance.auteur = request.user
instance.save()
formset.save_m2m()
# Gestion des suppressions
for obj in formset.deleted_objects:
obj.delete()
messages.success(request, "Articles mis à jour avec succès !")
return redirect('manage_articles')
else:
formset = ArticleFormSet(queryset=Article.objects.filter(auteur=request.user))
return render(request, 'articles/manage_articles.html', {'formset': formset})
Bonnes Pratiques Professionnelles
1. Sécurité
# Sécurisation des formulaires
class SecureForm(forms.Form):
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request', None)
super().__init__(*args, **kwargs)
def clean(self):
# Vérification CSRF supplémentaire
if self.request and not self.request.user.is_authenticated:
raise forms.ValidationError("Authentification requise.")
return super().clean()
2. Performance
# Optimisation des requêtes
class OptimizedArticleForm(forms.ModelForm):
class Meta:
model = Article
fields = '__all__'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Utilisation de select_related pour optimiser les foreign keys
self.fields['auteur'].queryset = User.objects.select_related('profile')
3. Réutilisabilité
# Mixin pour formulaires
class StyleFormMixin:
"""Mixin pour appliquer un style cohérent à tous les formulaires"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field_name, field in self.fields.items():
if hasattr(field, 'widget') and hasattr(field.widget, 'attrs'):
field.widget.attrs['class'] = field.widget.attrs.get('class', '') + ' form-control'
if field.required:
field.widget.attrs['required'] = 'required'
class ContactForm(StyleFormMixin, forms.Form):
# Les champs hériteront automatiquement du style
pass
Tests des Formulaires
# tests.py
from django.test import TestCase
from .forms import ContactForm, ArticleForm
class ContactFormTest(TestCase):
def test_form_valide(self):
form_data = {
'nom': 'Jean Dupont',
'email': 'jean@example.com',
'sujet': 'general',
'message': 'Ceci est un message de test assez long.'
}
form = ContactForm(data=form_data)
self.assertTrue(form.is_valid())
def test_form_message_trop_court(self):
form_data = {
'nom': 'Jean Dupont',
'email': 'jean@example.com',
'sujet': 'general',
'message': 'Court'
}
form = ContactForm(data=form_data)
self.assertFalse(form.is_valid())
self.assertIn('message', form.errors)
Conclusion
Les formulaires Django sont extrêmement puissants et flexibles. En maîtrisant ces concepts, vous serez capable de :
- Créer des formulaires complexes avec validation avancée
- Sécuriser efficacement les données utilisateur
- Optimiser les performances
- Maintenir un code propre et réutilisable
- Tester rigoureusement vos formulaires
Rappel important : Toujours utiliser {% raw %}{% csrf_token %}{% endraw %} dans vos templates et valider les données côté serveur, même si vous avez une validation JavaScript côté client.
Avec cette expertise, vous êtes maintenant prêt à construire des applications Django robustes et professionnelles !
Prochaines Étapes
- Apprendre à gérer les formulaires Django
- Explorer le système d’authentification utilisateur
- Découvrir les class-based views
- Mettre en place des tests automatiques
- Déployer votre application sur un hébergeur
Ressources Utiles