Tutos Django

Un Formulaire sexy en Ajax !

Bien que Django ne soit pas un framework web incluant la technologie Ajax en son cœur, vous pouvez parfaitement l'utiliser en complément.
Nous verrons dans ce tutoriel comment réaliser un formulaire sécurisé en utilisant la technologie AJAX. Il vous faut donc en pré-requis avoir des notions de javascript. J'utiliserai également la bibliothèque javascript : JQuery pour plus de commodité.

Ajax

Commençons à présent !

Créez un nouveau projet et une nouvelle application :) (dans la suite du tutoriel mon projet est nommé : form_ajax et mon application : sexy_form).
Et n'oubliez pas de renseigner votre application dans la variable INSTALLED_APPS de votre fichier settings.py.Puis de la même manière dont nous avons procédé pour le tutoriel sur l'inclusion des fichiers statiques, créez dans votre application un répertoire static contenant deux répertoires : js et css.
Ayant l'architecture de notre projet, nous pouvons à présent écrire la logique de code.

Premièrement, renseignez le fichier urls.py comme ceci :

from django.conf.urls.defaults import patterns, include, url

urlpatterns = patterns('sexy_form.views',
    url(r'^contact/$', 'contact', name='contact'),
    url(r'^success/$', 'success', name='success'),
)

Nous définissons deux urls, contact où s'affichera notre formulaire et success la page de succès si le formulaire ne comporte pas d'erreurs.

Nous allons maintenant écrire le code de nos vues. Éditez votre fichier views.py, comme ceci :

from django.http import HttpResponse
from django.shortcuts import render_to_response, redirect
from django import forms
from django.template.context import RequestContext

from django.forms.widgets import Input
class Html5EmailInput(Input):
    input_type = 'email'

class ContactForm(forms.Form):
    name = forms.CharField(max_length=30)
    firstname = forms.CharField(max_length=30)
    email = forms.EmailField(max_length=50, widget=Html5EmailInput())
    message = forms.CharField(max_length=1000)
    password = forms.CharField(max_length=50, widget=forms.PasswordInput())

    def clean_password(self):
        password = self.cleaned_data['password']
        length = len(password)
        if length < 8:
            raise forms.ValidationError("Password has to be at least 8 characters long.")
        return password

def contact(request):
    if request.method == 'POST': # If the form has been submitted...
        form = ContactForm(request.POST) # A form bound to the POST data
        if form.is_valid(): # All validation rules pass
            name = form.cleaned_data['name']
            firstname = form.cleaned_data['firstname']
            email = form.cleaned_data['email']
            message = form.cleaned_data['message']
            password = form.cleaned_data['password']
            # do_something

            # then return
            return redirect('success') # Redirect after POST
    else:
        form = ContactForm() # An unbound form
    return render_to_response('form.html', {'form' : form}, context_instance=RequestContext(request))


def success(request):
    return HttpResponse('success')

Comme vous pouvez le voir, beaucoup de choses sont déclarées  ici.
Dans un premier temps, la déclaration des import est faite.
Ensuite je définis un formulaire avec une particularité : je change le type du tag input du champ email pour qu'il corresponde au format HTML5, c'est à dire email et non text. Django 1.3 ne générant pas directement des champs correspondant aux nouvelles normes HTML5.
Puis je définis une règle pour le champ password, indiquant qu'il ne doit pas être inférieur à 8 caractères.
Enfin de manière classique je définis les fonctions de vues comme nous l'avons étudié dans les tutoriels précédent.

Il nous reste à écrire le template de rendu. Créez si ce n'est déjà fait un répertoire templates dans votre application et vérifiez qu'il est correctement renseigné dans la variable TEMPLATE_DIRS de votre fichier settings.py.
Puis créez un fichier HTML nomme form.html dans ce dossier et éditez-le comme ceci :

<!DOCTYPE html>
<html>
<head>
    <title>Sexy Ajax Form</title>
    <link rel="stylesheet" type="text/css" href="{{ STATIC_URL}}css/sexy_form.css">
</head>
<body>
    <form id="contact" action="{% url contact %}" method="post" class="niceform" novalidate>{% csrf_token %}
        <dl>
            <dt>{{ form.name.label_tag }} :</dt>
            <dd>{{ form.name }}</dd>
            <dd>{{ form.name.errors }}</dd>
        </dl>
        <dl>
            <dt>{{ form.firstname.label_tag }} :</dt>
            <dd>{{ form.firstname }}</dd>
            <dd>{{ form.firstname.errors }}</dd>
        </dl>
        <dl>
            <dt>{{ form.email.label_tag }} :</dt>
            <dd>{{ form.email }}</dd>
            <dd>{{ form.email.errors }}</dd>
        </dl>
        <dl>
            <dt>{{ form.password.label_tag }} :</dt>
            <dd>{{ form.password }}</dd>
            <dd>{{ form.password.errors }}</dd>
        </dl>
        <dl>
            <dt>{{ form.message.label_tag }} :</dt>
            <dd>{{ form.message }}</dd>
            <dd>{{ form.message.errors }}</dd>
        </dl>
        <div class="break">
            <input type="submit" value="Submit" />
        </div>
    </form>
</body>
</html>

Je déclare donc mon formulaire de façon personnalisée, en incluant la protection {% csrf_token %} et comme vous avez pu le remarquer, je passe l'attribut novalidate à mon formulaire pour que les navigateurs supportant la norme HTML5 n'exécutent pas de règles de validations automatiques sur les champs. Ceci afin de vous montrer comment nous allons nous même créer ces règles et ces effets en utilisant la technologie javascript et AJAX.
A cela nous pouvons rajouter une feuille de style css comme je l'ai déclaré dans le head de mon document HTML. Placez-vous alors dans le dossier css contenu dans le dossier static de votre application et créez un nouveau fichier css nommé sexy_form.css que vous remplirez comme ceci :

.niceform dl { clear: left;margin: 0; }

.niceform dt { float: left;text-align: right;width: 120px;line-height: 25px;margin: 0 10px 10px 0; }

.niceform dd { float: left;margin-left: 10px; }

.niceform ul.errorlist { float: left;color: white; margin: -3px 0 3px 0;list-style: none; }

.niceform .break { clear: left;margin-left: 200px; }

.niceform ul.errorlist li {
    padding: 5px 3px 4px 10px;
    background: rgb(255, 0, 0); /* ie 6,7,8 */
    background: rgba(255, 0, 0, 0.4);
    position: relative;
    -moz-border-radius:    10px;
    -webkit-border-radius: 10px;
    border-radius:         10px;
}
.niceform ul.errorlist li:before {
    content:"";
    position: absolute;
    right: 100%;
    top: 8px;
    width: 0;
    height: 0;
    border-top: 6px solid transparent;
    border-right: 12px solid rgba(255, 0, 0, 0.4);
    border-bottom: 6px solid transparent;
}

Nous pouvons à présent lancer notre serveur et observer le rendu en nous dirigeant sur l'url : http://127.0.0.1:8000/contact/

Raw Form

Notre formulaire s'affiche correctement en utilisant la CSS que nous avons défini !
Si nous cliquons sur le bouton Submit, nous obtenons un affichage d'erreur comme ceci :

Raw Form Fail

Les erreurs s'affichent correctement elles aussi. Nous pouvons également remplir le formulaire d'une façon différente afin d'obtenir d'autres erreurs :

Raw From Fail email password

Cela fonctionne ! Mais toute la page se recharge à chaque fois.
C'est pourquoi nous allons utiliser la technologie AJAX et javascript afin de rendre l'expérience utilisateur meilleure.

Pour cela nous allons charger la bibliothèque javascript JQuery dans notre template form.html.
Rajoutez à la partie head ceci :

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" type="text/javascript"></script>
<script src="{{ STATIC_URL }}js/sexy_form.js" type="text/javascript"></script>
<script type="text/javascript">
    // Definition of global variables for javascript context
    var contact_url = '{% url contact %}';
</script>

Je déclare également un fichier javascript sexy_form.js qui se trouvera dans le dossier js contenu dans le dossier static de notre application qui contiendra la logique javascript de validation du formulaire.
Enfin, je déclare un script dit "inline" où je définis les variables globales javascript se relatant aux variables que Django fournit afin de pouvoir les utiliser dans des scripts extérieurs comme c'est le cas ici avec sexy_form.js.

Il nous reste maintenant à écrire la logique du code javascript : créez si ce n'est déjà fait le fichier sexy_form.js et éditez-le comme ceci :

var form = {
    fields : {},
    init : function(obj){
        form.that = obj; // storing form object
        // init fields with value and validation function
        $.each(form.that.serializeArray(), function(i, field) {
            form.fields[field.name] = { value : field.value};
        });
        form.that.submit(form.submit);  // submit form handler
    },
    submit : function(e){
        e.preventDefault();

        var values = {};
        for (var v in form.fields) {
            values[v] = form.fields[v].value;
        }
        $.post(contact_url, values, form.success, 'json').error(form.error); // making ajax post
    },
    display_error : function(e, error){
        var $dd = $('#id_' + e).parent().next('dd'); // get dd error field
        var $error_list = $('<ul/>', {'class' : 'errorlist'}); // create error list
        var $error_em = $('<li/>', {html : error}); // create error element
        $error_em.appendTo($error_list); // append error element to error list
        $dd.append($error_list); // append error list to dd error field
    },
    success : function(data, textStatus, jqXHR){
        if (!data['success']){
            form.that.find('dl dd:last-child').empty();// empty old error messages
            var errors = data;
            for (var e in errors){ // iterating over errors
                var error = errors[e][0];
                form.display_error(e, error);
            }
        }
        else {
            window.location.href = data['success']; // redirection to success page
        }
    },
    error : function(jqXHR, textStatus, errorThrown){
        alert('error: ' + textStatus + errorThrown);
    }
};

$(function(){
    form.init($('#contact')); // initialize form
});

Beaucoup de choses sont déclarées ici mais le code n'est au final pas si compliqué.
Partons de la fin :
Tout d'abord, on initialise notre formulaire dont l'id est contact à l'aide de la méthode de départ "onReady" de JQuery (ici écrite de manière concise).
La fonction init va récupérer les données du formulaire et les réorganiser, puis elle surcharge la fonction de soumission du formulaire à l'aide de la méthode submit. Cette dernière réorganise les valeurs afin de les passer à la fonction ajax "post" de JQuery en utilisant le type de communication "json" et associe deux méthodes en cas de succès success ou d'erreur error. L'utilisation de json s'avère très pratique étant donné qu'il est très proche de l'objet dict du langage python. De même vous remarquerez que nous utilisons la variable contact_url que nous avons défini précédemment dans notre template form.html.
success renvoie l'utilisateur vers la page de succès si le formulaire est valide, dans le cas contraire elle affiche chaque erreur grâce à la méthode display_error qui mime Django lors de sa génération html d'erreurs contenues dans les formulaire.
error affiche la nature de l'erreur si une erreur a été levée du côté serveur. Il faut savoir qu'en utilisant la technologie AJAX, si une erreur est levée Django ne vous l'affichera pas en rechargeant la page, c'est pourquoi vous devez-vous même choisir le mode d'affichage de votre page d'erreur.

Mais pour que ce code javascript fonctionne correctement, il va nous falloir modifier un peu la fonction contact dans le code du fichier views.py :

import django.utils.simplejson as json

def contact(request):
    if request.method == 'POST': # If the form has been submitted...
        form = ContactForm(request.POST) # A form bound to the POST data
        if form.is_valid(): # All validation rules pass
            name = form.cleaned_data['name']
            firstname = form.cleaned_data['firstname']
            email = form.cleaned_data['email']
            message = form.cleaned_data['message']
            password = form.cleaned_data['password']
            # do_something

            # then return
            if request.is_ajax():
                return HttpResponse(content=json.dumps({'success' : '/success'}), mimetype='application/json')

            return redirect('success') # Redirect after POST
        elif request.is_ajax():
                errors = json.dumps(form.errors)
                return HttpResponse(errors, mimetype='application/json')
    else:
        form = ContactForm() # An unbound form
    return render_to_response('form.html', {'form' : form}, context_instance=RequestContext(request))

La nouveauté ici est : is_ajax() qui va nous permettre de faire évoluer ce que nous voulons retourner comme données dans le cas d'une requête AJAX ou non.
Ici, si le formulaire est valide, un objet dict transformé en json est retourné contenant la clef 'success' alliée à la valeur '/success'. Dans le cas contraire un objet dict contenant les erreurs du formulaire est renvoyé lui aussi transformé en json.

Dans votre navigateur, vérifiez que le javascript est bien activé, et testez la soumission de votre formulaire (n'oubliez pas de redémarrer votre serveur).
Vous devriez observer l'affichage des erreurs du formulaire sans rechargement de page !

Mission accomplie ! Vous venez d'obtenir un formulaire "ajaxisé" et de comprendre des notions de javascript et d'ajax associées aux formulaires.
La technologie AJAX peut s'appliquer à bien d'autres choses que la soumission de formulaire, vous pouvez bien évidemment charger des contenus depuis n'importe quelle url en accord avec la politique csrf.

Bien que nous maitrisions la partie AJAX pour notre formulaire, à chaque fois que l'utilisateur clique sur le bouton de soumission, une requête est envoyée au serveur, et cela coute de la ressource. Nous pourrions directement faire ces validations en javascript du côté client avant de décider d'exécuter la requête AJAX.
Mais rappelez-vous ! La validation doit TOUJOURS au moins être faite du côté serveur. La validation javascript n'est la que pour apporter de la commodité à l'utilisateur.

Voici comment réaliser une validation encore plus commode pour l'utilisateur. Afficher une erreur à côté du champ si celui est mal rempli à chaque fois qu'il passe au champ suivant : utiliser l'événement de perte de focus nomme "blur" en javascript.
Voici le nouveau code javascript pour sexy_form.js :

var form = {
    validated : false,
    fields : {},
    init : function(obj){
        form.that = obj; // storing form object
        // init fields with value and validation function
        $.each(form.that.serializeArray(), function(i, field) {
            form.fields[field.name] = { value : field.value, 'function' : 'valid_' + $('#id_' + field.name).attr('type')  + '_field'};
        });
        form.that.submit(form.submit);  // submit form handler
        form.blur(); // validation on blur handlers
    },
    blur: function() {
        form.that.find('dl :input').blur(form.valid_field); // checking validation
    },
    submit : function(e){
        e.preventDefault();
        form.valid(); // checking form

        if (form.validated){
            var values = {};
            for (var v in form.fields) {
                values[v] = form.fields[v].value;
            }
            $.post(contact_url, values, form.success, 'json').error(form.error); // making ajax post
        }
    },
    valid : function (){ // checking values from client side
        form.validated = true; // assuming form is correct
        form.that.find('dl :input').each(form.valid_field);
    },
    valid_field : function() {
        $(this).parent().next('dd').empty(); // empty error field
        var to_valid = $(this).attr('name'); // get field name
        form.fields[to_valid].value = $(this).val(); // updating values
        if (typeof(window['form'][form.fields[to_valid]['function']]) === 'function') {
            var validated = window['form'][form.fields[to_valid]['function']](to_valid);
            if (validated['error_message']) {
                form.validated = false;
                form.display_error(validated['field'], validated['error_message']);
            }
        }
    },
    valid_text_field : function(field) {
        var error_message = '';
        if (form.fields[field].value === '') {
            error_message = 'This field is required.';
        }

        return {field: field, error_message : error_message};
    },
    valid_password_field : function(field) {
        var error_message = '';
        var value = form.fields[field].value;
        if (value === '') {
            error_message = 'This field is required.';
        }
        else if (value.length < 8) {
            error_message = "Password has to be at least 8 characters long.";
        }

        return {field: field, error_message : error_message};
    },
    valid_email_field : function (field){
        var value = form.fields[field].value;
        var filter = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/;
        var error_message = '';
        if (value === '') {
            error_message = 'This field is required.';
        }
        else if (!filter.test(value)) {
            error_message = 'Enter a valid e-mail address.';
        }

        return {field: field, error_message : error_message};
    },
    display_error : function(e, error){
        var $dd = $('#id_' + e).parent().next('dd'); // get dd error field
        var $error_list = $('<ul/>', {'class' : 'errorlist'}); // create error list
        var $error_em = $('<li/>', {html : error}); // create error element
        $error_em.appendTo($error_list); // append error element to error list
        $dd.append($error_list); // append error list to dd error field
    },
    success : function(data, textStatus, jqXHR){
        if (!data['success']){
            form.that.find('dl dd:last-child').empty();// empty old error messages
            var errors = data;
            for (var e in errors){ // iterating over errors
                var error = errors[e][0];
                form.display_error(e, error);
            }
        }
        else {
            window.location.href = data['success']; // redirection to success page
        }
    },
    error : function(jqXHR, textStatus, errorThrown){
        alert('error: ' + textStatus + errorThrown);
    }
};

$(function(){
    form.init($('#contact')); // initialize form
});

Première chose, un booléen validated est créé permettant de savoir si le formulaire est considéré validé ou non.
Ensuite la fonction valid est exécuté avant toute soumission, c'est elle qui affiche les erreurs s'il y en a et décide de modifier la valeur du booléen validated ou non.
Cette dernière fait appel à la méthode valid_field sur chaque champ de formulaire afin de savoir s'ils sont valides. Elle-même exécute une fonction différente valid_text_field, valid_email_field, valid_password_field associé au type du tag input de l'élément en cours de validation. Vous remarquerez alors que le bout de code permettant de récupérer et d'organiser les valeurs des champs dans la méthode init a changé lui aussi et que l'événement de perte de focus blur a été ajouté .

J'ai délibérément associé une validation à chaque type de champ : text, email ou password. Mais vous pouvez très bien définir des validations comme il vous plait. Ceci n'est qu'un exemple.

Enfin si vous testez à nouveau votre formulaire, vous devriez obtenir des messages d'erreurs à chaque fois que vous quittez un champ, rendant l'expérience utilisateur encore meilleure !

Article suivant

Article précédent

Articles similaires

Commentaires

Les commentaires sont fermés.

Pingbacks

Les pingbacks sont fermés.