PROJET AUTOBLOG


Planet-Libre

source: Planet-Libre

⇐ retour index

Marco : Django: enregistrer une entité en utilisant plusieurs pages

mardi 5 avril 2016 à 16:43

Le but de cet article est de vous présenter la méthode que j’ai utilisée pour enregistrer une entité en plusieurs étapes, donc en utilisant plusieurs pages. La sauvegarde en base de données s’effectue seulement lors de la dernière étape.

Contexte technique: Django 1.9.5, Python 3.5.1

Cas concret: enregistrement d’un profil utilisateur sur plusieurs pages. Ici nous nous limiterons à deux pages mais il est très facile de l’étendre à n pages. Donc sur la page 1 nous permettrons à l’utilisateur de saisir certains champs. Ensuite sur la page 2 il renseignera les champs restants et l’enregistrement en base de la totalité des éléments (page 1 + page 2) s’effectuera.

Installation de l’application django

pip install django django-countries
django-admin start project user_profile_several_pages
cd user_profile_several_pages
# Notre application de création de profil utilisateur s'appelle ici website
./manage.py startapp website
# Création du super utilisateur pour la partie admin
./manage.py createsuperuser
INSTALLED_APPS = [
    ...
    'website',
]
from django.conf.urls import include, url
from django.contrib import admin

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^', include('website.urls', namespace='website')),
]

Le modèle (fichier website/models.py)

from django.db import models
from django.contrib.auth.models import User as DjangoUser
from django_countries.fields import CountryField

class UserProfile(models.Model):
    user = models.OneToOneField(DjangoUser, on_delete=models.CASCADE)
    sex = models.CharField(
        u"Sexe", max_length=1, choices=(('m', u'Masculin'), ('f', u'Féminin'))
    )
    city = models.CharField(u"Ville", max_length=20)
    country = CountryField(verbose_name=u"Pays", default="FR")
    web_site = models.URLField(u"Site web", blank=True, null=True)
    age = models.PositiveSmallIntegerField(u"Age", null=True, blank=True)
    information = models.TextField(u"Autres informations", null=True, blank=True)

    class Meta:
        verbose_name = u"Profil utilisateur"
        verbose_name_plural = u"Profils utilisateur"

    def __str__(self):
        return "%s %s" % (self.user.first_name, self.user.last_name)

Rien de bien compliqué dans ce modèle. On notera simplement la référence vers django.contrib.auth.models.User pour les éléments spécifiques au compte utilisateur « système » (first_name, last_name, email, password).

Les forms (fichier website/forms.py)

from django import forms
from django.utils.translation import ugettext, ugettext_lazy as _
from website.models import DjangoUser, UserProfile


class DjangoUserForm(forms.ModelForm):
    error_messages = {
        'password_mismatch': _("The two password fields didn't match."),
    }
    first_name = forms.CharField(required=True, label=u'Nom')
    last_name = forms.CharField(required=True, label=u'Prénom')
    email = forms.EmailField(required=True, label=u'Mail')
    password = forms.CharField(required=True, label=u'Password', widget=forms.PasswordInput)
    password_conf = forms.CharField(required=True, label=u'Password (confirmation)', widget=forms.PasswordInput)

    def clean_password_conf(self):
        """
        Cette méthode est exécutée automatiquement
        lorsque form.is_valid() est invoquée dans la vue
        """
        password = self.cleaned_data.get('password')
        password_conf = self.cleaned_data.get('password_conf')
        if password and password_conf:
            if password != password_conf:
                raise forms.ValidationError(
                    self.error_messages['password_mismatch'],
                    code='password_mismatch',
                )
        return password_conf

    class Meta:
        model = DjangoUser
        fields = ['first_name', 'last_name', 'email',]

class UserProfileFormStep1(forms.ModelForm):
    class Meta:
        model = UserProfile
        exclude = ['user', 'web_site', 'age', 'information']

class UserProfileFormStep2(forms.ModelForm):
    class Meta:
        model = UserProfile
        fields = ['web_site', 'age', 'information']

On peut noter ici la présence de trois formulaires:

La vue (fichier website/views.py)

On utilise ici une vue générique basique pour bénéficier d’un minimum de traitements automatiques du framework Django. J’hérite ici de la TemplateView qui est assez rudimentaire. Je n’ai pas utilisé de vue de plus « haut niveau » (type FormView) car j’avais besoin d’effectuer des traitements manuels pour coller à mes besoins.

from django.core.urlresolvers import reverse
from django.db import transaction
from django.views.generic.base import TemplateView
from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.contrib import messages
from django.contrib.auth.models import User

from website.forms import *


class ProfileAdd(TemplateView):
    template_name = 'website/profile_edit.html'

    def get(self, request, step, **kwargs):
        context = super(ProfileAdd, self).get_context_data(**kwargs)
        if step == '1':
            context['up_form'] = UserProfileFormStep1(prefix='user_profile')
            context['user_form'] = DjangoUserForm(prefix='user')
        elif step == '2':
            context['up_form'] = UserProfileFormStep2()
        else:
            logger.error("Step number invalid: '%s'" % step)

        context['step'] = step
        return render(
            request,
            self.template_name,
            context,
        )

    def post(self, request, step, **kwargs):
        context = super(ProfileAdd, self).get_context_data(**kwargs)
        context['step'] = step
        if step == '1':
            user_form = DjangoUserForm(request.POST, prefix='user')
            up_form = UserProfileFormStep1(
                request.POST, prefix='user_profile'
            )
            context['user_form'] = user_form
            context['up_form'] = up_form
            if up_form.is_valid() and user_form.is_valid():
                user = User.objects.filter(email=user_form.cleaned_data['email'])
                if user.exists():
                    user_form.add_error('email', u'Ce mail existe déjà.')
                    return render(
                        request,
                        self.template_name,
                        context,
                    )
                request.session['user_form'] = user_form.cleaned_data
                request.session['up_form'] = up_form.cleaned_data
                return HttpResponseRedirect(
                    reverse('website:profile_add', args=[2])  # step=2
                )
            else:
                return render(
                    request,
                    self.template_name,
                    context,
                )
                
        elif step == '2':
            up_form2 = UserProfileFormStep2(
                request.POST
            )
            context['up_form'] = up_form2
            if up_form2.is_valid():
                user_form = DjangoUserForm(
                    request.session['user_form']
                )
                up_form1 = UserProfileFormStep1(request.session['up_form'])
                with transaction.atomic():
                    user = user_form.save(commit=False)
                    # Dans notre système le mail est aussi le nom d'utilisateur
                    user.username = user_form.cleaned_data['email']
                    # Le formulaire contient un mot de passe en clair
                    unencrypter_pass = user.password
                    # Donc on utilise set_password pour le chiffrer
                    # Afin qu'il ne soit pas en clair dans la base de données
                    user.set_password(unencrypter_pass)
                    user.save()
                    user_profile = up_form1.save(commit=False)
                    user_profile.user = user
                    user_profile.web_site = up_form2.cleaned_data['web_site']
                    user_profile.age = up_form2.cleaned_data['age']
                    user_profile.information = up_form2.cleaned_data['information']
                    user_profile.save()

                return HttpResponseRedirect(
                    reverse('website:user_profile_created')
                )

        return render(
            request,
            self.template_name,
            context,
        )

La méthode get

Elle est appelée lorsqu’on accède à la première ou seconde page. Le paramètre step est passé en paramètre lorsqu’on appelle cette vue (voir plus loin la configuration des urls). Cette méthode effectue essentiellement deux choses:

La méthode post

Elle est évidemment un peu plus compliquée et comme son nom l’indique récupère les données saisies dans la première page et dans la seconde page suite à l’envoi du formulaire.

Les urls (fichier website/urls.py)

from django.conf.urls import url
from django.views.generic import TemplateView

from website import views

urlpatterns = [
    url(
        r'^$', TemplateView.as_view(
            template_name='website/index.html'
        ),
        name="home"
    ),
    url(
        r'^profile_add/(?P\\d+)/$',
        views.ProfileAdd.as_view(), name="profile_add"
    ),
    url(
        r'^user_profile_created/$', TemplateView.as_view(
            template_name='website/user_profile_created.html'
        ),
        name="user_profile_created"
    ),
]

On a trois urls qui correspondent aux trois vues qui nous redirigent vers trois templates:

Les templates

website/templates/index.html


    
        Page d'accueil
    
    
        

Créer un nouveau profil utilisateur

website/templates/profile_edit.html

Il est commun pour la saisie des données en page 1 et en page 2.

{% csrf_token %}   {% if up_form.errors or user_form.errors %}     

Le formulaire contient des erreurs :

  {% endif %} {% if step == "1" %} {{ user_form.as_table }} {{ up_form.as_table }} {% endif %} {% if step == "2" %} {{ up_form.as_table }} {% endif %}
{% if step == "1" %}Suivant{% elif step == "2" %}Enregistrer{% endif %}>

A noter qu’à la limite on aurait pu se passer du test sur la variable step pour l’affichage des formulaires. A l’étape 2 user_form n’est pas instancié donc il n’aurait pas été affiché. Mais une des règles de Python est bien « explicite est mieux qu’implicite » non ?

website/templates/user_profile_created.html


    
        Création utilisateur
    
    
        

L'utilisateur a bien été créé

Création du module d’administration (fichier website/admin.py)

Il est simplement utilisé ici pour vérifier que la création du profil utilisateur s’est effectuée correctement.

from django.contrib import admin

from website.models import UserProfile


admin.site.register(UserProfile)

Lancement de l’application

./manage.py makemigrations
./manage.py migrate
./manage.py runserver

Remarques sur cet article

Voilà j’espère que cet article a pu vous être utile. Ceci dit en aucun cas je ne prétends que c’est la meilleur façon de faire, que c’est la solution la plus rapide. D’ailleurs si vous avez des remarques je suis preneur, car parfois quand on code et qu’on souhaite à tout prix résoudre un problème, on a du mal à prendre du recul.

Gravatar de Marco
Original post of Marco.Votez pour ce billet sur Planet Libre.