Modèles plans et vues pour les pays
Nous allons maintenant créer une page qui affiche la liste de tous les pays présents dans la base de données, ainsi que des formulaires pour ajouter ou supprimer un pays.
Premièrement, il va falloir créer un modèle qui accède à la base de donnée et la modifie. Ensuite, nous allons créer la vue (le template) et le contrôleur qui va appeler le modèle et la vue.
Modèles pour l'accès à la base de donnée
Modèles, plans et vues
Créez le package nommé models (un dossier) dans le dossier mobility si ce n'est pas encore fait.
Ajoutez dans ce nouveau package un fichier nommé country.py et ajoutez-y le code suivant :
mobility/models/country.py
from mobility.db import get_db
def get_country_list():
db = get_db()
return db.execute('SELECT * FROM country ORDER BY iso_country').fetchall()
def search_by_iso(iso_country: str):
db = get_db()
return db.execute('SELECT * FROM country WHERE iso_country=?', (iso_country,)).fetchall()
Avertissement
Il est TRÈS important d'utiliser le mécanisme de remplacement avec des ?.
En effet, si vous faites
db.execute("SELECT * FROM country WHERE iso_country=" + iso_country)
au lieu de
db.execute("SELECT * FROM country WHERE iso_country=?", (iso_country,) )
ce qui est syntaxiquement correct, l'utilisateur peut envoyer un iso_country tel que iso_country OR 1=1, ce qui
introduit une tautologie et permet d'extraire... l'entièreté de la base de données !
En fonction de l'utilisation du résultat, cela peut être extrêmement grave. La RGPD prévoit des amendes jusqu'à 20 millions d'euros pour ce genre de manque de prévoyance élémentaire.
La fonction get_country_list retourne une liste de tous les pays dans la base de données, ordonnée par code ISO.
La fonction search_by_iso retourne le code iso, ainsi que le nom du pays dont le code iso est passé en paramètre.
Ajoutez cette classe en dessous des fonctions créées précédemment.
mobility/models/country.py
class Country:
def __init__(self, name, iso_country):
self.name = name
self.iso_country = iso_country
def delete(self):
db = get_db()
db.execute("DELETE FROM country WHERE iso_country=?", (self.iso_country,))
db.commit()
def save(self):
db = get_db()
db.execute("INSERT INTO country(iso_country,name) VALUES(?, ?)",
(self.iso_country, self.name))
db.commit()
@staticmethod
def get(iso_country: int):
db = get_db()
data = db.execute(
'SELECT * FROM country WHERE iso_country=?', (iso_country,)).fetchone()
if data is None:
return None
else:
return Country(data["name"], data["iso_country"])
La classe Country est une classe qui représente un pays. Elle a deux attributs : name et iso_country.
Elle a aussi trois méthodes : delete, save et get. La méthode delete supprime le pays de la base de données, la méthode save ajoute le pays à la base de données et la méthode get retourne une instance de Country à partir d'un code iso.
Nous allons créer une nouvelle page mobility/templates/country.html qui va afficher la liste de tous les pays, en appelant la fonction get_country_list. N'oubliez pas d'utiliser le principe d'héritage et d'étendre base.html. Le fichier country.html vous est déjà donné ci-dessous.
mobility/country.py
from flask import (
Blueprint, render_template
)
from mobility.models.country import get_country_list
bp = Blueprint('country', __name__)
# route code
@bp.route('/country')
def country_list():
countries = get_country_list()
return render_template("country.html", countries=countries)
mobility/templates/country.html
Solution (Essayez d'abord par vous-même)
{% extends "base.html" %}
{% block content %}
<h2>Country list</h2>
<table>
<tr>
<th>Name</th>
<th>ISO Code</th>
</tr>
{% for country in countries %}
<tr>
<td>{{ country['name'] }}</td>
<td>{{ country['iso_country'] }}</td>
</tr>
{% endfor %}
</table>
{% endblock %}
Interagir avec la base de données depuis le site web
Pour le moment, notre base de données est presque vide, les fonctions que nous avons écrites ne sont donc pas encore très utiles. Nous aimerions avoir la possibilité d'ajouter ou supprimer un pays dans la base de données depuis notre site web. Pour cela, nous allons ajouter deux nouvelles routes à notre plan.
Rappel : les formulaires HTML
Un formulaire HTML permet à l'utilisateur de saisir des données et de les envoyer au serveur. Il est délimité par la balise <form> et ses deux attributs essentiels sont :
method: la méthode HTTP utilisée pour envoyer les données. On utilise"post"pour envoyer des données (création, modification, suppression) et"get"pour des requêtes sans effet de bord (recherche, filtrage).action: l'URL cible qui traitera les données. Avec Jinja, on utiliseurl_for(...)pour générer cette URL dynamiquement.
<form method="post" action="/ma-route">
...
</form>
Champs de saisie : <input>
À l'intérieur du formulaire, chaque champ est représenté par une balise <input>. Ses attributs importants sont :
type: nature du champ ("text","number","email","password", …).name: clé sous laquelle la valeur sera accessible côté serveur viarequest.form["name"]. C'est l'attribut le plus important.id: identifiant unique dans la page HTML, utilisé pour relier le champ à son<label>.
<input type="text" name="iso_country" id="iso_country" />
Côté Flask, la valeur saisie est récupérée ainsi :
iso_country = request.form["iso_country"]
Étiquettes : <label>
La balise <label> associe un texte descriptif à un champ. L'attribut for doit correspondre à l'id du champ concerné. Cela améliore l'accessibilité et permet de cliquer sur le label pour activer le champ.
<label for="iso_country">Code ISO :</label>
<input type="text" name="iso_country" id="iso_country" />
Bouton d'envoi : <button>
Pour soumettre le formulaire, on ajoute un bouton avec type="submit" :
<button type="submit">Ajouter</button>
Lorsque l'utilisateur clique dessus, le navigateur envoie une requête HTTP POST vers l'URL définie dans action, avec toutes les valeurs des champs packagées dans le corps de la requête.
Récapitulatif : anatomie d'un formulaire complet
<form method="post" action="/create">
<div>
<label for="champ1">Valeur 1 :</label>
<input type="text" name="champ1" id="champ1" />
</div>
<div>
<label for="champ2">Valeur 2 :</label>
<input type="text" name="champ2" id="champ2" />
</div>
<div>
<button type="submit">Envoyer</button>
</div>
</form>
Côté Flask, les valeurs sont accessibles via :
valeur1 = request.form["champ1"]
valeur2 = request.form["champ2"]
Note
Dans un template Jinja, l'attribut action est généralement généré dynamiquement avec url_for(), par exemple :
<form method="post" action="{{ url_for('country.country_create') }}">
Cela garantit que l'URL reste correcte même si la route change.
À vous !
Template
Premièrement, il faut mettre à jour le template pour ajouter un formulaire pour ajouter une ville et un bouton pour supprimer une ville. Retournez dans votre fichier country.html.
Ajouter une colonne « Delete » dans le tableau, avec un lien
<a href="...">verscountry.country_deletepour chaque ligne.Ajouter un formulaire en dessous du tableau permettant de saisir un code ISO et un nom, puis de les envoyer à la route
country.country_create.
Solution (Essayez d'abord par vous-même)
mobility/template/country.html
{% extends "base.html" %}
{% block content %}
<h2>Country list</h2>
<table>
<tr>
<th>Name</th>
<th>ISO Code</th>
</tr>
{% for country in countries %}
<tr>
<td>{{ country['name'] }}</td>
<td>{{ country['iso_country'] }}</td>
<td><a href="{{ url_for("country.country_delete", iso_country=country['iso_country']) }}">Delete</a></td>
</tr>
{% endfor %}
<h2>New country</h2>
<form method="post" action="{{ url_for("country.country_create") }}">
<div>
<label for="iso_country"> ISO Code: </label>
<input name="iso_country" id="iso_country" type="text" />
</div>
<div>
<label for="name">Name: </label>
<input name="name" id="name" type="text" />
</div>
<div>
<button type="submit">Add a country </button>
</div>
</table>
{% endblock %}
Nous avons créé un formulaire pour ajouter un pays ainsi qu'un bouton pour en supprimer un. Le formulaire envoie une requête POST à la route country_create et le bouton envoie une requête GET à la route country_delete.
Contrôleur
Maintenant que le formulaire est fait, vous pouvez créer le code python.
Nous devons créer deux routes, une route create_country qui va recevoir le contenu du formulaire, et et une route delete_country qui va supprimer un pays de la base de données.
Voici le squelette du code python. Essayez de le compléter par vous-même.
mobility/country.py
@bp.route("/create_country", methods=["POST"])
def country_create():
# Ajouter le code ici
return redirect(url_for("country.country_list"))
@bp.route("/delete_country/<iso_country>")
def country_delete(iso_country):
# Ajouter le code ici
return redirect(url_for("country.country_list"))
Avertissement
Essayez par vous-même ! A chaque fois que vous copiez-coller, votre cerveau ne s'exerce pas, et vous n'apprenez pas.
Solution (Essayez d'abord par vous-même)
mobility/country.py
@bp.route("/create_country", methods=["POST"])
def country_create():
iso_country = request.form["iso_country"]
if not search_by_iso(str(iso_country)):
print("Creating country")
name = request.form["name"]
country = Country(name, iso_country)
country.save()
print("Country already exists")
return redirect(url_for("country.country_list"))
@bp.route("/delete_country/<iso_country>")
def country_delete(iso_country):
country = Country.get(iso_country)
if country:
country.delete()
return redirect(url_for("country.country_list"))
La vue country_create est liée à la route /create_country et n'accepte que les requêtes de type POST (envoyées par un formulaire HTML, nous le créerons plus tard). Elle récupère le code ISO saisi par l'utilisateur via request.form, puis vérifie avec search_by_iso si un pays avec ce code existe déjà. Si ce n'est pas le cas, elle crée une nouvelle instance de Country avec le nom et le code ISO fournis, puis appelle country.save() pour l'insérer dans la base de données. Dans tous les cas, l'utilisateur est redirigé vers la liste des pays grâce à redirect(url_for("country.country_list")).
La vue country_delete est liée à la route /delete_country/<iso_country>. La partie <iso_country> est un paramètre de route : sa valeur est extraite directement de l'URL et passée en argument à la fonction. Elle récupère l'objet Country correspondant via Country.get(), et si ce pays existe bien en base, elle appelle country.delete() pour le supprimer. L'utilisateur est ensuite redirigé vers la liste des pays.
Vous constatez dans votre IDE que les imports request , redirect, url_for sont manquants. Ajoutez-les en haut de votre fichier.
Vous devez également importer la classe Country et les fonctions search_by_iso et get_country_list depuis mobility/models/country.py. Votre début de fichier devrait ressembler à ceci :
mobility/country.py
from flask import (
Blueprint, render_template, request, redirect, url_for
)
from mobility.models.country import get_country_list, search_by_iso, Country
bp = Blueprint('country', __name__)
N'oubliez pas d'ajouter la nouvelle route :
mobility/__init__.py
...
from . import country
app.register_blueprint(country.bp)
...
Lancement
Testez votre application en lançant le serveur avec la commande flask --app=mobility --debug run
Dans votre navigateur, allez sur la page http://127.0.0.1:5000/country.
Ensuite, ajoutez des pays à votre base de données en utilisant le formulaire que vous venez de créer. Vous pouvez aussi supprimer des pays en cliquant sur le bouton "Delete" à côté de chaque pays.
Vous devriez avoir une page qui ressemble à ceci :
Continuez en lisant le document pour ajouter les aéroports Modèles plans,et vues pour les aéroports.