Les formulaires Django

Le formulaire (form en anglais) est un élément incontournable du web. Il permet d'envoyer au serveur des données qu'indique l'utilisateur dans des champs dynamiques. Un formulaire complet et intelligent assiste l'utilisateur sur la manière de remplir les champs mais vérifie surtout l'intégrité des informations, hors de question d'envoyer un integer si une adresse mail est attendue.

L'objet Form

Chaque formulaire dans Django est composé de l'objet Form. Celui possède deux concepts que vous devez maitriser: le field (champ) et le widget. Le field est déterminé en fonction de type de données et le widget est l'outil utilisé pour entrer une donnée.

Créer un formulaire de base

Nous allons créer un simple formulaire:

#!/usr/bin/env python
#-*- coding: utf-8 -*-

from django.conf.urls import patterns, include, url
from django.views.generic import *
from django import forms

class ContactForm(forms.Form):
    subject = forms.CharField()
    message = forms.CharField(widget=forms.Textarea)

    def send_email(self):
        print self.cleaned_data
        
class ContactView(FormView):
    template_name = 'backoffice/templates/contact.html'
    form_class = ContactForm
    success_url = '/thanks/'

    def form_valid(self, form):
        form.send_email()
        return super(ContactView, self).form_valid(form)
    
urlpatterns = patterns('',
    url(r'^contact/$', ContactView.as_view()),
)

Créons notre template:

backoffice/templates/contact.html

<form action="" method="POST">
{% csrf_token %}
{{form.as_ul}}
<input type="submit" value="Envoyer"/>
</form>

Regardons le résultat:

Envoyons le formulaire et regardons dans notre terminal la valeur retournée par print:

{'message': u'Connaissez-vous blablabla', 'subject': u"demande d'information"}

Tadaa! En quelques lignes de code, nous avons crée un formulaire capable de nous retourner des informations.

Les méthodes de la classe Form

add_initial_prefix(self, field_name) ajoute un prefix initial pour vérifier les données dynamiques initiales
add_prefix(self, field_name) retourne le nom du champ avec un préfix
as_p(self) retourne le rendu HTML du formulaire avec des balises p
as_table(self) retourne le rendu HTML du formulaire avec les balises tr (sans la balise table)
as_ul(self) retourne le rendu HTML du formulaire avec les balises li (sans la balise ul)
clean(self) hook pour faire un clean après Field.clean() Utilisé pour relever des erreurs non spécifique à un champ
full_clean(self) clean toutes les self.data et remplit self._errors et self.cleaned_data
has_changed(self) retourne True si les données ont changé du formulaire initial
is_multipart(self) retourne True si le formulaire est multipart-encoded c'est à dire qu'un FileInput est utilisé
is_valid(self) retourne True si le formulaire n'a aucune erreur.
errors(self) retourne les erreurs
changed_data(self) retourne les données modifiées
non_field_errors(self) retourne une liste d'erreurs qui ne sont pas associées à un champ particulier c'est à dire de Form.clean()
visible_fields(self) retourne les champs visibles
hidden_fields(self) retourne les champs invisibles

Créer un formulaire de modèle

#!/usr/bin/env python
#-*- coding: utf-8 -*-

from django.conf.urls import patterns, include, url
from django.views.generic import *
from backoffice.models import *
from django import forms

class ProductForm(forms.ModelForm):
    class Meta:
        model = Product

class ProductCreateView(CreateView):
    form_class = ProductForm
    model = Product
    success_url = "/product/new/"

urlpatterns = patterns('',
    url(r'^product/new/$', ProductCreateView.as_view()),
)

Nous travaillerons avec des vues-classes pour les exemples, je trouve ce type de vue plus lisible mais vous pouvez faire vos tests avec des vues-fonctions comme nous l'avons vu précédement.

Si vous exécutez ce code vous remarquerez que de base le paramètre form n'est pas indispensable. Nous avons d'ailleurs vu dans le chapitre sur les vues qu'il était possible de créer un formulaire complet sans que nous ayons intervenu dans quoi que ce soit. Associer une classe forms.ModelForm permet de personnaliser son formulaire.

Créer ses propres validations de formulaire

Vous pouvez relever une erreur générale comme ceci:

class ProductForm(forms.ModelForm):
    class Meta:
        model = Product

    def clean(self):
        
        cleaned_data = super(ProductForm, self).clean()
        price_ht = cleaned_data.get("price_ht")
        price_ttc = cleaned_data.get("price_ttc")

        if price_ht > price_ttc:
            raise forms.ValidationError(u"Erreur le prix HT ne peut être supérieur à TTC")
        
        return cleaned_data

Ou cibler l'erreur au niveau du champ:

class ProductForm(forms.ModelForm):
    class Meta:
        model = Product

    def clean(self):
        
        cleaned_data = super(ProductForm, self).clean()
        price_ht = cleaned_data.get("price_ht")
        price_ttc = cleaned_data.get("price_ttc")

        if price_ht > price_ttc:
            msg = u"Le prix HT doit être plus élevé que le prix TT"
            self._errors['price_ht'] = self.error_class([msg])

        return cleaned_data

Formset

Vous pouvez associer des formulaires de clés étrangères (comme nous l'avons vu dans le chapitre sur la contrib admin) comme ceci:

#!/usr/bin/env python
#-*- coding: utf-8 -*-

from django.conf.urls import patterns, include, url
from django.views.generic import *
from backoffice.models import *
from django import forms
from django.forms.models import inlineformset_factory
from django.http import HttpResponseRedirect

class ProductForm(forms.ModelForm):

    class Meta:
        model = Product

ProductItemFormSet = inlineformset_factory(Product, ProductItem)

class ProductCreateView(CreateView):

    form_class = ProductForm
    model = Product
    success_url = "/product/new/"
    
    def get(self, request, *args, **kwargs):
        
        self.object = None
        form_class = self.get_form_class()
        form = self.get_form(form_class)
        product_item_form = ProductItemFormSet()
        return self.render_to_response(self.get_context_data(form=form, product_item_form=product_item_form))
    
    def post(self, request, *args, **kwargs):
      
        self.object = None
        form_class = self.get_form_class()
        form = self.get_form(form_class)
        product_item_form = ProductItemFormSet(self.request.POST)
         
        if form.is_valid() and product_item_form.is_valid():
            return self.form_valid(form, product_item_form)
        else:
            return self.form_invalid(form, product_item_form)
        
    def form_valid(self, form, product_item_form):
        
        self.object = form.save()
        product_item_form.instance = self.object
        product_item_form.save()
        return HttpResponseRedirect(self.get_success_url())

    def form_invalid(self, form, product_item_form):
        
        return self.render_to_response(self.get_context_data(form=form, product_item_form=product_item_form))

urlpatterns = patterns('',
    url(r'^product/new/$', ProductCreateView.as_view()),
)

Notre template:

<form action="" method="POST">
{% csrf_token %}
{{form.as_ul}}
{{ product_item_form.management_form }}
{{ product_item_form.non_form_errors }}
{% for form in product_item_form %}  
    <br />              
    {{form.as_table}}
{% endfor %}
<input type="submit" value="Envoyer"/>
</form>

Résultat: