UE5 : Fondamentaux du développement web frontal, niveau 2

Séance 2 : Routes et templates

  1. Introduction aux templates
  2. Utilisation des templates
  3. Jinja2
    1. Appels de paramètres et filtres
    2. Structures de contrôle
      1. La boucle if
      2. La boucle for
    3. Dictionnaires
    4. Héritages
      1. Les extends
      2. Les include
  4. Liens internes
  5. Les statiques: CSS et JS

Introduction aux templates

Plutôt que de renvoyer une chaîne de caractères brute aux routes Flask, il est possible de renvoyer du HTML. Prenons l'exemple d'une barre de navigation sur une page Web, le retour de la fonction serait le suivant pour la route /home:

@app.route("/home")
def home():
   return '''
      <html>
         <body>
            <nav>
               <ol>
                  <li>Lien1</li>
                  <li>Lien2</li>
               </ol>
            </nav>
         </body>
      </html>
   '''

Une barre de navigation est normalement partagée entre toutes les pages du site. La route /page suivante renverrait donc cette barre également:

@app.route("/page")
def page():
   return '''
      <html>
         <body>
            <nav>
               <ol>
                  <li>Lien1</li>
                  <li>Lien2</li>
               </ol>
            </nav>
         </body>
      </html>
   '''

Si maintenant on veut ajouter un lien dans cette barre de navigation, il faut l'ajouter dans chacune des pages du site! Pas très pratique et efficient... De plus, le code HTML d'une page peut être incroyablement long, surtout quand on génrérera plus tard le HTML à la volée en fonction des données: pas question donc de le mettre directement dans le return de la fonction.

Utilisation des templates

La plupart des frameworks offrent un système de templates. Un template est dans notre cas un fichier HTML qui sera rempli par les données qu'on lui fournira quand nous l'appelerons en sortie de fonction.

Les templates sont rangés dans le dossier templates/ pour plusieurs raisons:

Note: si le dossier des templates s'appele autrement que templates/, il est possible d'indiquer le nom de notre module lors de l'initialisation de Flask.

#app.py
...
app = Flask(
   __name__,
   template_folder='nom_du_module'
   )
...

Note 2: A partir de maintenant, nous allons développer une petite application autour des données du World Factbook de la CIA. Une base SQLITE comprenant les données est disponible au lien suivant: https://github.com/MaximeChallon/CoursM2TNAH_Flask_code/blob/master/factbook.sqlite. Bootstrap sera utilisé pour mettre en page le site.

Commençons par construire une page d'accueil, c'est à dire une route et un template.

from flask import render_template

...

@app.route("/")
def accueil():
    return render_template("pages/accueil.html")

render_template() est la fonction qui va parser les templates que nous créons, et va y rajouter, au besoin, les données que l'on souhaitera (ce que nous verrons plus loin). Cette fonction accepte en premier argument le chemin relatif vers le template depuis le module templates/, et dans les arguments suivants les variables que l'on souhaite transmettre au template.

<!-- templates/accueil.html -->
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Factbook</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
  </head>
  <body>
    <h1>Bienvenue sur l'application du Factbook!</h1>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-OERcA2EqjJCMA+/3y+gxIOqMEjwtxJY7qPCqsdltbNJuaOe923+mo//f6V8Qbsw3" crossorigin="anonymous"></script>
  </body>
</html>

Note 3: Naturellement, nous venons de mettre la route dans app.py. Maintenant que nous débutons le développement d'une application, il est temps d'utiliser au maximum les modules Python. Dans la Séance1, nous avons abordé la structure de l'application, et notamment le dossier routes/. C'est ce dossier qui va contenir nos routes, et non plus le fichier d'initialisation app.py. Pour cela; il est nécessaire de créer le module routes avec un script Python generales.py qui accueillera nos premières routes:

app
| app.py
| templates/
| routes/
|  |-- __init__.py
|  |-- generales.py

Il va falloir ensuite indiquer dans app.py où se trouvent nos routes. Pour cela, il faut ajouter à la toute fin du fichier (à la fin du fichier est important puisque generales.py contient un appel à app, il faut donc que app soit initialisé pour que generales.py puisse fonctionner sans erreurs) :

#app.py
...
from .routes import generales

De même, dans generales.py, il faut indiquer que l'on importe app depuis app.py (avec un chemin relatif bien entendu!).

# generale.py
from ..app import app
...

Exemple concerné: Seance2/premiers_templates

L'utilisation des templates n'est pas plus compliqué! Seulement, nous avons encore des pages uniquement statiques, nos fonctions Python ne font que renvoyer vers un template HTML qui ne bouge pas pour le moment.

Jinja2

Le système de templates de Flask s'appelle Jinja2: il n'est pas spécifique à Flask et est aussi utilisé dans d'autres frameworks.

Appels de paramètres et filtres

Le World Factbook contient des données sur tous les pays. Imaginons que nous souhaitions récupérer les données d'un pays spécifique, nous pourrions simplement l'indiquer dans les paramètres de l'URL. Développons une route Flask qui affiche dans le titre de la page le pays sélectionné.

# la route
@app.route("/pays/<string:nom>")
def pays(nom):
    return render_template("pages/pays.html", pays=nom)

Comme vu dans la partie sur les routes avec des paramètres, le paramètre nom de la route est appelé dans la fonction pays(). C'est la valeur de ce paramètre qui est transmise à Jinja via render_template():

<!-- dans le body du template-->
<div class="container">
        <h1>Bienvenue sur l'application du Factbook!</h1>
        <p>Voici le lien vers le site officiel : <a
                href="https://www.cia.gov/the-world-factbook/">https://www.cia.gov/the-world-factbook/</a></p>
        <p>Vous avez choisi d'afficher les données du pays suivant: {{pays}}</p>
</div>

Commentaires:

Exemple concerné: Seance2/variables_jinja

Un autre point fort de Jinja est qu'il permet de faire des opérations simples sur les paramètres qu'il reçoit: ces opérations sont nommées 'filtres'. Différents filtres existent, les plus courants et utiles étant les suivants:

Il s'utilisent comme suit: {{paramètre|length}}

<div class="container">
        <h1>Bienvenue sur l'application du Factbook!</h1>
        <p>Voici le lien vers le site officiel : <a
                href="https://www.cia.gov/the-world-factbook/">https://www.cia.gov/the-world-factbook/</a></p>
        <p>Vous avez choisi d'afficher les données du pays suivant: {{pays}}</p>
        <p>Longueur de la chaîne de caractères du pays choisi: {{pays|length}} caractères</p>
</div>

Exemple concerné: Seance2/filtres_jinja

Structures de contrôle

La boucle if

Jinja n'accepte pas seulement des blocs de variables (les {{...}}): les structures de contrôle sont permises avec des blocs {%...%}...{%end...%}. Voyons d'abord un bloc avec une condition if: la syntaxe est identique à celle de Python et n'est pas trop déroutante, il ne faut juste pas oublier de l'entourer par la syntaxe Jinja.

Les blocs if s'écrivent de la manière suivante:

{%if condition %}
   <p>Du HTML correspondant à la condition réussie</p>
{%else%}
   <p>Du HTML correspondant à la condition non réussie</p>
{%endif%}
<div class="container">
        <h1>Bienvenue sur l'application du Factbook!</h1>
        <p>Voici le lien vers le site officiel : <a
                href="https://www.cia.gov/the-world-factbook/">https://www.cia.gov/the-world-factbook/</a></p>
        <p>Vous avez choisi d'afficher les données du pays suivant: {{pays}}</p>
        <p>Longueur de la chaîne de caractères du pays choisi: {{pays|length}} caractères</p>
        {%if pays|length > 10%}
            <p>C'est un long nom de pays</p>
        {%else%}
            <p>C'est un nom de pays plutôt court</p>
        {%endif%}
</div>

Testons les deux URLs suivantes pour s'assurer du bon fonctionnement de notre structure de contrôle:

Exemple concerné: Seance2/if_jinja

La boucle for

Voyons maintenant une autre structure de contrôle: le for. Avant de l'utiliser, il va falloir renvoyer non pas une string à Jinja, mais une liste (le for s'exécutant généralement sur une liste). Renvoyer une liste ne pose pas de problème avec Flask et render_template(). En effet, les paramètres de render_template() acceptent n'importe quel type de donnée (string, integer, list, tuple, etc.).

@app.route("/pays/<string:nom>")
def pays(nom):
   grandes_villes = []
   if nom =='France':
      grandes_villes = ['Paris', 'Lyon', 'Marseille']
   return render_template("pages/pays.html", pays=nom, grandes_villes=grandes_villes)
<div class="container">
        <h2>Grandes villes de {{pays}}</h2>
        {%if grandes_villes %}
        <ul>
            {%for ville in grandes_villes %}
            <li>{{ville}}</li>
            {%endfor%}
        </ul>
        {%else%}
        <p>Il n'y a pas de grande ville connue pour ce pays</p>
        {%endif%}
</div>

Commentaires:

Exemple concerné: Seance2/for_jinja

Dictionnaires

La transmission de données, remontées d'une base de données par exemple, se fait généralement via JSON dans une application Web. Ainsi, un seul paramètre de render_template() prendra l'ensemble du JSON remonté depuis la source de données. Jinja va ensuite pouvoir parser ce paramètre et afficher ce qu'on lui demande.

Tentons de créer une route pays/ (sans paramètre cette fois-ci) qui affichera, pour tous les pays stockés dans notre source de données, une table HTML avec les champs suivants:

Pour cela, réfléchissons aux besoins:

Il faut débuter par la création de la route et des données (nous verrons dans d'autres séances comment récupérer des données dans une base; en attendant, nous créons la liste d'objets donnees à la main):

@app.route("/pays")
def pays():
    donnees = [{
        "nom":"France",
        "capitale":"Paris",
        "continent":"Europe"
    },{
        "nom":"Etats-Unis",
        "capitale":"Washington",
        "continent":"Amérique"
    },{
        "nom":"Egypte",
        "capitale":"Le Caire",
        "continent":"Afrique"
    },{
        "nom":"Chine",
        "capitale":"Pékin",
        "continent":"Asie"
    }]
    return render_template("pages/pays.html", donnees=donnees)
{%if donnees%}
        <table class="table">
            <thead>
                <tr>
                    <th scope="col">#</th>
                    <th scope="col">Nom</th>
                    <th scope="col">Capitale</th>
                    <th scope="col">Continent</th>
                </tr>
            </thead>
            <tbody>
                {%for pays in donnees%}
                <tr>
                    <th scope="row">{{loop.index}}</th>
                    <td>{{pays.nom}}</td>
                    <td>{{pays.capitale}}</td>
                    <td>{{pays.continent}}</td>
                </tr>
                {%endfor%}
            </tbody>
        </table>
{%endif%}

Commentaires:

Exemple concerné: Seance2/dico_jinja

Héritages

Comme vu au début de l'UE dans le cours HTML/CSS/JS, une page Web est composée de plusieurs parties, parmi lesquelles:

Trois de ces parties sont répétées entre toutes les pages du site Web (header, menu et navbar, et footer). Comme il est impensable de devoir changer toutes les pages HTML de notre site dès qu'il y a un changement fait sur l'une de ces trois parties, il convient de conteneuriser chacune des parties, et de les lier ensuite entre elles grâce à Jinja. On se retrouvera alors avec un seul template HTML pour le menu; il sera appelé ensuite par toutes les pages: c'est ce que l'on appele l'héritage de templates.

Les extends

Dans le template qui sert de conteneur, on va déclarer un (ou plusieurs) bloc qui sera complété par le(s) template(s) qui étend(ront) ce conteneur.

<!-- partials/conteneur.html -->
<!doctype html>
<html lang="en">
<head>
    <title>Factbook | {{sous_titre}}</title>
</head>
<body>
    {% block body%}{%endblock%}
</body>

</html>

Dans le conteneur, on déclare ici:

<!-- pages/pays.html -->
{% extends "partials/conteneur.html" %}

{% block body %}

    <div class="container">
        <h1>Bienvenue sur l'application du Factbook!</h1>
        <p>Voici le lien vers le site officiel : <a
                href="https://www.cia.gov/the-world-factbook/">https://www.cia.gov/the-world-factbook/</a></p>
      
      ...

    </div>

{% endblock %}

Dans ce template, on rempli le bloc body avec du html et l'on n'a plus à se soucier des autres parties de la page comme le menu ou le header. La syntaxe est la suivante:

Avec Jinja, on peut emboîter des conteneurs à l'infini grâce à ce jeu de blocs étendus.

Exemple concerné: Seance2/extends_jinja

Les include

En plus d'étendre un template, Flask donne la possibilité d'inclure un template. On peut alors insérer des sous-éléments dans des conteneurs ou des pages.

L'inclusion, et l'extension, sont très pratiques. Prenons l'exemple du <head>...</head> d'un fichier HTML (ici partials/conteneur.html):

<!-- partials/conteneur.html -->
<head>
    <meta charset="UTF-8">
    <title>{%block titre %}{%endblock%} | {{sous_titre}}</title>
    {% include "partials/css.html" %}
    {% include "partials/metadata.html" %}
    {% include "partials/js.html" %}
    {% block js %}{%endblock%}
    {% block css %}{%endblock%}
</head>

On permet ici plusieurs choses:

Exemple concerné: Seance2/include_jinja

Liens internes

La navigation de l'utilisateur au sein d'un site Web est primordiale: cliquer sur des liens est constant afin de se diriger vers les bonnes pages de notre site (nous évoquerons ici seulement les liens internes). Créons/réutilisons la route /pays d'affichage de tous les pays disponibles, et la route /pays/<string:nom> spécifique à un pays.

# generales.py
@app.route("/pays")
def pays():
    donnees = [{
        "nom":"France",
        "capitale":"Paris",
        "continent":"Europe"
    },{
        "nom":"Etats-Unis",
        "capitale":"Washington",
        "continent":"Amérique"
    },{
        "nom":"Egypte",
        "capitale":"Le Caire",
        "continent":"Afrique"
    },{
        "nom":"Chine",
        "capitale":"Pékin",
        "continent":"Asie"
    }]
    return render_template("pages/tous_pays.html", donnees=donnees, sous_titre="Tous les pays")

@app.route("/pays/<string:nom>")
def pays_specifique(nom):
    grandes_villes = []
    if nom =='France':
        grandes_villes = ['Paris', 'Lyon', 'Marseille']
    return render_template("pages/pays.html", pays=nom, grandes_villes=grandes_villes, sous_titre=nom)

Afin de renvoyer vers la page du pays, il faut créer un lien vers /pays/<string:nom> dans le tableau de la page /pays. Ce lien est créé dans le template grâce à la fonction url_for(). Le premier argument de cette fonction est le nom de la fonction Python associée à une route Flask -- et non le nom de la route elle-même --; les autres arguments sont les paramètres nécessaires au fonctionnement de la fonction. Aucune URL ne doit être écrite en dur dans le code, ainsi, pour appeler localhost:5000/pays/France, il faut écrire url_for(pays_specifique, nom="France").

<!-- pages/tous_pays.html -->
{%for pays in donnees%}
   <tr>
      <th scope="row">{{loop.index}}</th>
      <td><a href="{{url_for('pays_specifique', nom=pays.nom)}}">{{pays.nom}}</a></td>
      <td>{{pays.capitale}}</td>
      <td>{{pays.continent}}</td>
   </tr>
{%endfor%}

Note: Il ne devra y avoir aucune URL absolue dans les devoirs, les liens internes doivent s'écrire avec url_for(): si l'on décide de modifier le nom de la route, ou son chemin, seul app.route() sera impacté et l'on n'aura pas à réécrire tous nos liens dans les templates.

Exemple concerné: Seance2/url_for

Les statiques: CSS et JS

Jusqu'à présent, nous avons vu comment importer du JS ou du CSS depuis une URL externe. Mais il est parfois nécessaire d'uiliser du CSS ou du JS maisons: ce sont des assets pour Flask, ou encore des fichiers statiques. Ces fichiers sont rangés dans le dossier app/statics/ comme le montre l'arbre suivant:

| app
|  |-- statics
|  |  |-- css
|  |  |  |-- css.css
|  |  |-- js
|  |  |  |-- js.js
|  |  |-- img

A la différence des templates, le dossier des statiques n'est pas un module, simplement un dossier de rangements de fichiers (CSS, JS, données, images, etc.) nécessaires au bon fonctionnement de l'application. Si l'on nomme le dossier static, Flask comprendra par défaut qu'il doit y chercher les fichiers qu'on appelera par la suite dans l'application. Si le dossier est nommé autrement, comme statics, il va falloir l'indiquer, comme pour les templates, dans l'initialisation de Flask dans app.py.

# app.py
...
app = Flask(
    __name__, 
    template_folder='templates',
    static_folder='statics')
...

Maintenant que nous avons un fichier CSS dans les statics, il faut l'importer dans les templates, ou plutôt dans le HTML. Cet import se fait via une balise <link ...> dans le <head></head>. Le meilleur endroit pour importer un CSS statique est le template partials/css.html:

<link rel="stylesheet" href="{{ url_for('static', filename='css/font-awesome.css')}}">

Remarque: si l'on regarde les logs de Flask, on remarque que l'import d'une ressource statique interne est en réalité un appel GET de cette ressource sur le serveur

127.0.0.1 - - [06/Nov/2022 16:13:31] "GET /pays HTTP/1.1" 200 -
127.0.0.1 - - [06/Nov/2022 16:13:31] "GET /statics/css/font-awesome.css HTTP/1.1" 200 -

Exemple concerné: Seance2/statics