Hey salut, bienvenue dans cette seconde partie sur comment développer une application web avec Django. Dans la première partie, nous avons défini nos routes, créer nos vues et nous avons aussi parler de comment ajouter des fichiers statiques (CSS, JS, images, ...) à nos vues. Aujourd'hui, nous allons plus parler de ce qui se passe derrière, nous allons commencer par connecter notre application à une base de données MySQL, nous allons ensuite créer nos entités et voir comment traiter un formulaire. Aller c'est parti.

Pour commencer, si tu n'as pas suivi la première partie de ce tutoriel, je te conseille d'aller le faire avant.

Nous allons commencer par mettre en place une base de données MySQL.

Connecter une BDD MySQL

Par défaut, Django vient avec une BDD SQLite, mais tu le trouveras très rarement en production sur une grande application. Je vais donc dans ce tutoriel te présenter MySQL, tu en as sûrement entendu parler, c'est une base de données relationnel gratuite et très utilisé.

Pour utiliser MySQL avec Django, nous allons installer un package qui s'appelle mysqlclient:

$ pip install mysqlclient

Il faut ensuite configurer notre application pour utiliser MySQL. Dans le fichier community_app/settings.py, il faut modifier le dictionnaire DATABASES comme ceci:

# community_app/settings.py

# Database
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'OPTIONS': {
            'read_default_file': os.path.join(BASE_DIR, 'mysql.cnf'),
        },
    }
}

Nous définissons le moteur de base de données comme étant MySQL, ensuite dans options, on demande à Django de lire un fichier mysql.cnf que nous allons mettre à la racine du projet. Nous aurions pu directement définir dans le fichier settings.py les accès à notre base de données, mais ce fichier est pris en compte par git, nous risquons ainsi de divulguer des informations confidentielles. Nous allons donc créer un fichier de configuration mysql.cnf y renseigner les informations de connexion à notre base de données, ce fichier doit être à la racine du projet, au même niveau que le fichier manage.py:

[client]
database = "django_community_app"
user = "orion"
password = "password"
default-character-set = utf8

Il faut ensuite renseigner le nom de la base de données (database), le nom d'utilisateur (user), le mot de passe de l'utilisateur (password) et nous utilisons utf8 comme encodage. Il faudra ensuite rajouter ce fichier au fichier .gitignore.

Maintenant, nous allons créer la base de données dans MySQL, connecte toi à MySQL avec PHPMyadmin ou en ligne de commande et créer la base de données de ton application, le nom de la base données doit correspondre à la valeur de database dans le fichier mysql.cnf. Dans  mon cas ça va être django_community_app:

create database django_community_app;

Nous pouvons maintenant créer les migrations:

$ python manage.py makemigrations
$ python manage.py migrate

Tu dois ensuite avoir un retour comme sur l'image ci-dessous, qui dit que les migrations ont bien été effectuées:

Si tu regardes dans ta BDD, tu peux voir les tables qui ont été créées.

Créer des Modèles

Un Modèle est la représentation des données que tu veux enregistrer en base de données. Un modèle est une classe qui hérite de la classe django.db.models.Model, généralement un modèle est une table dans la base de données et les attributs de la classe représente les champs de cette table.

On définit les modèles d'une application dans le fichier models.py du dit application. Dans notre cas, si nous voulons créer les modèles de notre application members, nous allons les définir dans le fichier members/models.py.

# members/models.py

from django.db import models


class Member(models.Model):
    full_name = models.CharField(max_length=255)
    email = models.EmailField(unique=True)
    title = models.CharField(max_length=255)
    bio = models.TextField(null=True, blank=True)
    city = models.CharField(max_length=255)
    github = models.CharField(max_length=255, null=True, blank=True)
    twitter = models.CharField(max_length=255, null=True, blank=True)
    linkedin = models.CharField(max_length=255, null=True, blank=True)
    facebook = models.CharField(max_length=255, null=True, blank=True)
    instagram = models.CharField(max_length=255, null=True, blank=True)
    website = models.CharField(max_length=255, null=True, blank=True)
    picture = models.FileField(null=True)
    is_active = models.BooleanField(default=True)
    joined_at = models.DateField(auto_now_add=True)

    def __str__(self):
        return self.full_name

Nous définissons ici une classe Member avec ses attributs qui sont les champs de notre table en base de données. Pour chaque champ, nous faisons models.TypeChamp(options), le TypeChamp change en fonction du type de l'attribut, nous avons des booléens (BooleanField), fichiers (FileField), ... et les options aussi. Tu peux trouver la liste complètes des types de champs et leurs options sur la documentation.

Si tu remarques, nous ne définissons pas d'attribut id, django s'occupe de cela pour nous.

Maintenant que nous avons notre modèle, on peut effectuer les migrations en base de données:

$ python manage.py makemigrations
$ python manage.py migrate

Une table members_member va être créé en base de données. Le nom de la table vient du nom de l'application et de celui de la classe du modèle.

Traiter un formulaire

Dans une application, quand on a une base de données, on aura généralement besoin de formulaires pour permettre à nos utilisateurs de fournir des données à cette base de données.

En Django, nous allons définir les formulaires d'une application dans le fichier forms.py de l'application, c'est comme le fichier models.py pour les modèles au fait. Un formulaire est une classe qui hérite de la classe django.forms.Form.

Nous avons deux manières de définir un formulaire, soit en rattachant le formulaire à un modèle qui existe déjà, ça va être le cas du formulaire pour ajouter un membre, ou sans le rattacher à un modèle, c'est le cas d'un formulaire de connexion par exemple. Tu l'as donc compris, nous allons créer un formulaire qui va être rattacher à notre modèle Member, notre formulaire va donc hériter de la classe django.forms.ModelForm, il faut donc ajouter ce code dans le fichier members/forms.py (il faudra créer le fichier):

# members/forms.py

from django import forms

from .models import Member


class MemberForm(forms.ModelForm):

    class Meta:
        model = Member
        fields = ("full_name", "email", "title", "bio", "city", "github",
                  "twitter", "linkedin", "facebook", "instagram", "website", "picture",)

Nous définissons une classe MemberForm qui hérite de django.forms.ModelForm et dans la classe Meta, nous spécifions que le modèle rattacher à ce formulaire est la classe Member que nous avons précédemment ajouter puis nous définissons les champs dont nous avons besoin dans la liste fields.

On peut maintenant définir le formulaire dans la vue et l'envoyer au template pour l'afficher:

# members/views.py

from django.shortcuts import render

from .forms import MemberForm


def homepage(request):
    form = MemberForm() # le formulaire à afficher sur la page
    return render(request, 'members/index.html', locals())

# ...

Et modifier le template:

{# members/templates/members/index.html #}

{# ... #}
    <form action="/" method="post" class="join-form mt-8 m-auto">
      {% csrf_token %}
      <div class="">
        <label for="" class="block mb-2 text-lg">Une image de toi</label>
        {{ form.picture }}
      </div>
      <div class="mt-4">
        {{ form.full_name }}
      </div>
      <div class="mt-4">
        {{ form.email }}
      </div>
      <div class="mt-4">
        {{ form.title }}
      </div>
      <div class="mt-4">
        {{ form.city }}
      </div>
      <div class="flex justify-between -mx-1">
        <div class="mt-4 w-1/2 mx-1">
          {{ form.github }}
        </div>
        <div class="mt-4 w-1/2 mx-1">
          {{ form.facebook }}
        </div>
      </div>
      <div class="flex justify-between -mx-1">
        <div class="mt-4 w-1/2 mx-1">
          {{ form.twitter }}
        </div>
        <div class="mt-4 w-1/2 mx-1">
          {{ form.linkedin }}
        </div>
      </div>
      <div class="flex justify-between -mx-1">
        <div class="mt-4 w-1/2 mx-1">
          {{ form.instagram }}
        </div>
        <div class="mt-4 w-1/2 mx-1">
          {{ form.website }}
        </div>
      </div>
      <div class="mt-4">
        {{ form.bio }}
      </div>
      <div class="mt-4">
        <input type="submit" value="Valider" class="bg-gray-900 p-3 cursor-pointer w-full">
      </div>
    </form>
{# ... #}

Il ne faut pas oublier le {% csrf_token %} qui va permettre de sécuriser vos requêtes et sans quoi le formulaire ne marchera pas.

On affiche les champs du formulaire avec {{ form.champ }} et Django s'occupe de rendre l'élément HTML adéquat. Pour {{ form.picture }} nous aurons un input de type file et pour {{ form.email }} un input de type email. Tout cela vient des types de champs que nous avons défini dans le modèle Member.

Mais nous avons un problème, nous avons perdu le style de notre formulaire:

Django nous génère les éléments HTML, mais pas l'attribut class qui permet d'apporter le style à nos champs, nous allons donc devoir modifier le formulaire pour ajouter les classes de chaque champ et aussi l'attribut placeholder:

# members/forms.py

from django import forms

from .models import Member


class MemberForm(forms.ModelForm):

    class Meta:
        model = Member
        fields = ("full_name", "email", "title", "bio", "city", "github",
                  "twitter", "linkedin", "facebook", "instagram", "website", "picture",)
        widgets = {
            'full_name': forms.TextInput(
                attrs={
                    'placeholder': 'Prénom et Nom',
                    'class': 'w-full px-3 py-2 outline-none text-black',
                }
            ),
            'email': forms.EmailInput(
                attrs={
                    'placeholder': 'Adresse Email',
                    'class': 'w-full px-3 py-2 outline-none text-black',
                }
            ),
            'title': forms.TextInput(
                attrs={
                    'placeholder': 'Titre',
                    'class': 'w-full px-3 py-2 outline-none text-black',
                }
            ),
            'bio': forms.Textarea(
                attrs={
                    'placeholder': 'Bio',
                    'class': 'w-full px-3 py-2 outline-none text-black',
                    'rows': 4,
                }
            ),
            'city': forms.TextInput(
                attrs={
                    'placeholder': 'Ville',
                    'class': 'w-full px-3 py-2 outline-none text-black',
                }
            ),
            'github': forms.TextInput(
                attrs={
                    'placeholder': 'Compte Github',
                    'class': 'w-full px-3 py-2 outline-none text-black',
                }
            ),
            'linkedin': forms.TextInput(
                attrs={
                    'placeholder': 'Compte Linkedin',
                    'class': 'w-full px-3 py-2 outline-none text-black',
                }
            ),
            'facebook': forms.TextInput(
                attrs={
                    'placeholder': 'Compte Facebook',
                    'class': 'w-full px-3 py-2 outline-none text-black',
                }
            ),
            'instagram': forms.TextInput(
                attrs={
                    'placeholder': 'Compte Instagram',
                    'class': 'w-full px-3 py-2 outline-none text-black',
                }
            ),
            'website': forms.TextInput(
                attrs={
                    'placeholder': 'Site Web',
                    'class': 'w-full px-3 py-2 outline-none text-black',
                }
            ),
            'twitter': forms.TextInput(
                attrs={
                    'placeholder': 'Compte Twitter',
                    'class': 'w-full px-3 py-2 outline-none text-black',
                }
            ),
        }

Et maintenant le formulaire s'affiche bien:

Maintenant que nous avons notre formulaire, nous allons le traiter, c'est à dire que quand un visiteur va remplir le formulaire et le soumettre, nous allons vérifier que les informations sont valides et les enregistrer en base de données.

Dans l'attribut action de la balise form, j'ai fais de sorte que le formulaire soit traiter sur la même page, cela va m'éviter de créer une autre vue pour traiter le formulaire et faire une redirection sur la page d'accueil. Vu que le formulaire va être sur la vue homepage, je dois donc vérifier le type de la requête qui sera envoyer à la vue, si c'est une requête de type GET, j'initialise juste le formulaire, si par contre c'est une méthode de type POST, je dois donc traiter le formulaire. Notre vue prend déjà la requête en paramètre, je peux donc avoir le type de la requête avec request.method:

# members/views.py

from django.shortcuts import render

from .forms import MemberForm


def homepage(request):
    if request.method == 'POST':
        # initialise le formulaire avec les données envoyés
        form = MemberForm(request.POST)

        # si les données sont valides
        if form.is_valid():
            form.save()  # enregistrer le membre en base de données
    else:
        form = MemberForm()  # le formulaire à afficher sur la page

    return render(request, 'members/index.html', locals())

# ...

On vérifie que la requête est bien de type POST, puis on initialise le formulaire avec les données que l'utilisateur a saisi, enfin on vérifie si le formulaire est valide et ne contient pas d'erreurs, si c'est le cas, on enregistre les données.

On peut maintenant essayer de remplir le formulaire et le soumettre.

Bon tu as l'impression que rien ne se passe et c'est logique, pour l'instant nous n'affichons aucun message. Nous allons afficher un message d'erreur pour les champs du formulaire. Pour afficher un message d'erreur pour un champ, il faut faire {{ form.champ.errors }}, pour le champ picture par exemple, ce sera {{ form.picture.errors }}, tu as compris, je vais faire cela pour tous les champs du formulaire et il faut le faire aussi de ton côté.

Re-essaye encore avec le formulaire, tu peux maintenant voir les messages d'erreur, pour mon cas c'est le champ picture, nous ne gérons pas l'upload du fichier pour l'instant, je vais donc juste modifier le modèle Member pour dire que le champ picture peut être vide dans le fichier members/models.py:

# members/models.py

from django.db import models


class Member(models.Model):
    picture = models.FileField(null=True, blank=True)
    # ...

Puis faire la migration:

$ python manage.py makemigrations
$ python manage.py migrate

On essaie encore d'ajouter un membre avec le formulaire, et maintenant plus aucun message d'erreur ne s'affiche (chez moi en tout cas, n'hésite pas à me le faire savoir en commentaire ou sur discord si tu as des erreurs). Mais encore une fois, nous ne savons pas si l'ajout a été effectif, vérifions dans la base de données pour le savoir. Connecte toi à ta base de données et affiche le contenu de la table members_member, tu dois avoir un enregistrement, celui que tu viens de saisir depuis le formulaire.

Nous avons à ce niveau 2 choses à faire pour avoir un formulaire vraiment effectif, afficher un message à l'utilisateur pour lui dire que l'enregistrement a été effectif et vider les champs de la base de données.

Pour afficher le message de succès, nous allons utiliser ce qu'on appelle les flash message, ces messages sont comme des notifications et s'affichent une seule fois sur la page:

# members/views.py

from django.shortcuts import render
from django.contrib import messages

from .forms import MemberForm


def homepage(request):
    if request.method == 'POST':
        # initialise le formulaire avec les données envoyés
        form = MemberForm(request.POST)

        # si les données sont valides
        if form.is_valid():
            form.save()  # enregistrer le membre en base de données

            # enregistrer un message de succès
            messages.success(request, 'Merci pour ton inscription!')
    else:
        form = MemberForm()  # le formulaire à afficher sur la page

    return render(request, 'members/index.html', locals())

# ...

Nous ajoutons le message avec message.success(). Nous pouvons maintenant l'afficher sur le template:

{# members/templates/members/index.html #}

{% extends 'layout.html' %}

{% block title %}Accueil{% endblock title %}

{% block content %}
  {# ... #}

  <div class="py-12 px-4 text-white bg-green-900">
    <h2 class="text-center font-semibold text-2xl">Rejoins-nous maintenant</h2>

    {% if messages %}
      {% for message in messages %}
        <span class="block text-center text-xl">{{ message }}</span>
      {% endfor %}
    {% endif %}

    <form action="/" method="post" class="join-form mt-8 m-auto">
      {# ... #}
    </form>
  </div>
{% endblock content %}

Et si tu enregistres un nouveau membre, tu as bien le message qui s'affiche:

Le message Merci pour ton inscription!

 

J'avoue que c'est pas le meilleur design que j'ai eu à faire.

Prochaine étape, vider le formulaire. Rien de plus simple, il suffit de re-créer un formulaire vide après l'enregistrement:

# members/views.py

# ...

def homepage(request):
    if request.method == 'POST':
        # initialise le formulaire avec les données envoyés
        form = MemberForm(request.POST)

        # si les données sont valides
        if form.is_valid():
            form.save()  # enregistrer le membre en base de données
            form = MemberForm()  # créer un nouveau formulaire vide

            # enregistrer un message de succès
            messages.success(request, 'Merci pour ton inscription!')
    else:
        form = MemberForm()  # le formulaire à afficher sur la page

    return render(request, 'members/index.html', locals())

# ...

Et voilà.

Pour gérer l'upload de fichier, il suffit juste de dire à django où enregistrer le fichier et mettre l'attribut enctype de la balise form.

Dans le fichier members/models.py, nous allons specifier à django où uploader le fichier:

from django.db import models
from uuid import uuid4


def upload_picture(instance, filename):
    extension = filename.split('.')[-1]
    return 'static/uploads/members/{}.{}'.format(uuid4().hex, extension)


class Member(models.Model):
    picture = models.FileField(upload_to=upload_picture, null=True)
    # ...

Je définis d'abord une fonction upload_picture qui prend l'instance de l'objet que nous traitons et le nom du fichier charger par le client, ce que je fais c'est que je renomme le fichier et je l'enregistre dans le dossier static/uploads/members/, il faut donc créer cet arborescence dans le dossier static à la racine de ton projet. Et quand je définis l'attribut picture, j'affecte à la clé upload_to la fonction upload_picture que je viens de créer.

Dans le template, je rajoute l'attribut enctype à la balise form:

{# members/templates/members/index.html #}

<form action="/" method="post" class="join-form mt-8 m-auto" enctype="multipart/form-data">

</form>

Il ne faut pas l'oublier, sinon tu risques de galérer pendant 30mn à débugger comme moi.

Et dans la vue, remplir le formulaire avec les données de request.FILES aussi:

from django.shortcuts import render
from django.contrib import messages

from .forms import MemberForm


def homepage(request):
    if request.method == 'POST':
        # initialise le formulaire avec les données envoyés
        form = MemberForm(request.POST, request.FILES)

        #...

    return render(request, 'members/index.html', locals())

# ...

Et voilà, tu peux maintenant uploader tes images sans problèmes, et elles iront toutes dans le dossier que nous avons mentionné dans la méthode upload_picture()

Ça y est, nos visiteurs peuvent maintenant s'enregistrer en tant que membre sur notre communauté, la prochaine partie, nous allons voir comment lire les informations de la base de données et les afficher sur nos templates, puis nous terminerons par le puissant espace d'administration de Django. D'ici là porte toi bien et n'hésite pas à continuer de pratiquer ce que nous avons vu jusque là. Si tu as des questions n'hésite pas à me les poser dans les commentaires ci-dessous ou à m'écrire sur le chat discord de Kaherecode ou je suis plus actif. Merci à bientôt.


Partager cet article

alioukahere

Mamadou Aliou Diallo

@alioukahere

Développeur web fullstack avec une passion pour l’entrepreneuriat et les nouvelles technologies. Fondateur de Kaherecode.