Côté serveur



Les données envoyées par le formulaire arrivent au serveur. Bottle fournit l'objet request, qui contient toutes les informations sur la requête, et en particulier les données du formulaire.

Classe Request

Référence de la classe Request : https://bottlepy.org/docs/dev/api.html#bottle.BaseRequest

Il faut importer request depuis bottle :

from bottle import request

Les données de formulaire sont accessible via :

  • request.query ou request.GET pour les données envoyées par la méthode GET
  • request.forms et request.files, ou request.POST qui combine les deux pour les données envoyées par la méthode POST

Ces données sont rendues disponibles sous forme de dictionnaire (plus précisément un MultiDict, qui est un type de données permettant de gérer plusieurs valeurs pour une même clé. Il est utile pour traiter les formulaires où plusieurs valeurs peuvent être envoyées pour un même champ).

Accéder aux données

Pour accéder aux données, on peut utiliser la syntaxe request.POST['nom'] ou request.forms.get('nom'). La méthode get permet de spécifier une valeur par défaut si la clé n'existe pas, par exemple request.forms.get('nom', default=None).

Il est aussi possible d'utiliser directement un accès par attribut, par exemple request.forms.nom.

Si plusieurs valeurs sont envoyées pour un même champ (exemple avec une requète GET /formulaire?choix=1&choix=2&choix=3), on peut les récupérer avec request.GET.getall('choix') ou request.GET.getlist('choix'), qui renvoient une liste de toutes les valeurs associées à la clé choix.

.keys(), .values() et .items() fonctionnent aussi sur les objets request.GET et request.POST, ce qui permet de les parcourir comme un dictionnaire classique.

Lorsqu'on envoie des fichiers via un formulaire (voir ci-dessous), on peut les récupérer avec request.files, qui est un dictionnaire associant le nom du champ à un objet FileUpload. Cet objet contient des méthodes pour accéder au contenu du fichier, à son nom, à son type MIME, etc. Par exemple, pour récupérer le nom du fichier téléchargé, on peut utiliser request.files['fichier'].filename.

La méthode save de l'objet FileUpload permet d'enregistrer le fichier sur le serveur. Elle prend en paramètre le chemin où le fichier doit être enregistré. Par exemple, pour enregistrer le fichier dans le dossier /tmp, on peut utiliser (sauf sous Windows) :

request.files['fichier'].save('/tmp/' + request.files['fichier'].filename)

Routage

Le traitement des données du formulaire se fait dans une route; deux options sont possibles :

  • utiliser la même route que celle qui affiche le formulaire, en spécifiant la méthode POST dans le décorateur de la route. C'est souvent la méthode la plus simple pour pouvoir gérer les erreurs de saisie et afficher le formulaire avec les données déjà saisies pour correction.

    from bottle import route, run, template, request
    
    @route('/formulaire', method=['GET', 'POST'])
    def formulaire():
        if request.method == 'POST':
            # Traitement des données du formulaire
            nom = request.forms.get('nom', None)
            message = request.forms.get('message', None)
            # ... traitement des données
            if erreur_dans_le_formulaire:
                return template("formulaire.html",
                                erreur=erreur,
                                nom=nom,
                                message=message)
            # Vérification des données OK
            # ... traitement des données (envoi de mail, etc..)
            return template("merci_message.html")
        else:
            return template("formulaire.html")
    
  • utiliser une route différente pour traiter les données du formulaire, en spécifiant l'URL de la route dans l'attribut action du formulaire

    from bottle import route, run, template, request
    
    @route('/formulaire', method='GET')
    def formulaire():
        return template("formulaire.html")
    
    @route('/traitement', method='POST')
    def traitement():
        # Traitement des données du formulaire
        nom = request.forms.get('nom')
        message = request.forms.get('message')
        # ...
        return template("merci_message.html")
    

Exemples

Formulaire de contact basique

Télécharger le code

<body>
    <form action="/contact" method="POST">
        <label for="nom">Nom :</label>
        <input type="text" id="nom" name="nom" />
        <label for="message">Message :</label>
        <textarea id="message" name="message"></textarea>
        <button type="submit">Envoyer</button>
    </form>
</body>

Télécharger le code

from bottle import route, request, template, run

def envoyer_email(nom, message):
    # Simuler l'envoi d'un email
    print(f"Envoi d'un email à {nom} avec le message : {message}")

@route('/contact', method=['GET', 'POST'])
def contact():
    if request.method == 'GET':
        return template("contact_formulaire.html")
    # Récupération des données du formulaire
    nom = request.forms.get('nom', default=None)
    message = request.forms.get('message', default=None)

    # Traitement des données (par exemple, envoi d'un email)
    envoyer_email(nom, message)

    return template("merci.html")

Formulaire de contact avec vérification des erreurs

Télécharger le code

% setdefault("erreur", None)
% setdefault("nom", "")
% setdefault("message", "")
<!-- ... -->
<body>
    <form action="/contact" method="POST">
    % if erreur is not None:
        <p class="erreur">{{ erreur }}</p>
    % end
    <label for="nom">Nom :</label>
    <input type="text" id="nom" name="nom" value="{{ nom }}" />
    <label for="message">Message :</label>
    <textarea id="message" name="message">{{ message }}</textarea>
    <button type="submit">Envoyer</button>
    </form>
</body>

Télécharger le code

from bottle import route, request, template, run

def envoyer_email(nom, message):
    # Simuler l'envoi d'un email
    print(f"Envoi d'un email à {nom} avec le message : {message}")

@route('/contact', method=['GET', 'POST'])
def contact():
    if request.method == 'POST':
        # Récupération des données du formulaire
        nom = request.forms.get('nom', default=None)
        message = request.forms.get('message', default=None)

        # Vérification des erreurs
        if not nom or not message:
            erreur = "Tous les champs sont obligatoires."
            return template("contact_formulaire_check.html",
                             erreur=erreur, nom=nom, message=message)
        if len(message) < 10:
            erreur = "Le message doit contenir au moins 10 caractères."
            return template("contact_formulaire_check.html", 
                            erreur=erreur, nom=nom, message=message)
        if "spam" in message:
            erreur = "Le message contient un mot interdit."
            return template("contact_formulaire_check.html",
                             erreur=erreur, nom=nom, message=message)
        # Traitement des données (par exemple, envoi d'un email)
        envoyer_email(nom, message)
        return template("merci.html")
    else:
        return template("contact_formulaire_check.html")

Choix d'options multiples

Télécharger le code : choix_formulaire.html (avec les 2 versions et l'affichage des choix)

% setdefault("choix", list())
<!DOCTYPE html>
<html lang="fr">

Avec un `select[multiple]` :

```html
% setdefault("choix", list())

    <form action="/choix" method="POST">
        <label for="choix">Choisissez une ou plusieurs options :</label>
        <select id="choix" name="choix" multiple>
          <option value="choix1" {{"selected" if "choix1" in choix else ""}}>Option 1</option>
          <option value="choix2" {{"selected" if "choix2" in choix else ""}}>Option 2</option>
          <option value="choix3" {{"selected" if "choix3" in choix else ""}}>Option 3</option>
        </select>
        <button type="submit">Envoyer</button>
    </form>

ou bien avec des input[type=checkbox] :

% setdefault("choix", list())

    <form action="/choix" method="POST">
        <input type="checkbox" id="choix1" name="choix"
         value="choix1" {{"checked" if "choix1" in choix else ""}}>
        <label for="choix1">Option 1</label><br>
        <input type="checkbox" id="choix2" name="choix"
         value="choix2" {{"checked" if "choix2" in choix else ""}}>
        <label for="choix2">Option 2</label><br>
        <input type="checkbox" id="choix3" name="choix" 
          value="choix3" {{"checked" if "choix3" in choix else ""}}>
        <label for="choix3">Option 3</label><br>
        <button type="submit">Envoyer</button>
    </form>

Dans les deux cas, affichage des options choisies :

    % if len(choix) > 0:
        <hr>
        <h2>Vous avez choisi :</h2>
        <ul>
        % for c in choix:
            <li>{{ c }}</li>
        % end
        </ul>
    % end

Télécharger le code du serveur

@route('/choix', method=['GET', 'POST'])
def choix():
    if request.method == 'POST':
        # Récupération des choix
        choix = request.POST.getall('choix')
        print(choix)
        return template("choix_formulaire.html", choix=choix)
    else:
        return template("choix_formulaire.html")

Upload de fichiers

Pour permettre à l'utilisateur d'uploader un fichier, il faut utiliser l'attribut enctype="multipart/form-data" dans la balise <form>. Cela permet d'envoyer des fichiers binaires au serveur. request.files contiendra alors le fichier uploadé.

Télécharger le code : upload.html

    <form action="/upload" method="POST" enctype="multipart/form-data">
        <label for="fichier">Choisissez un fichier :</label>
        <input type="file" id="fichier" name="fichier" />
        <button type="submit">Envoyer</button>
    </form>

Télécharger le code du serveur

from bottle import route, request, run, template
from os.path import join, dirname

@route('/upload', method=['GET', 'POST'])
def upload():
    if request.method == 'GET':
        return template("upload.html")
    
    # Récupération du fichier
    fichier = request.files.get('fichier')
    # Traitement du fichier (par exemple, enregistrement sur le serveur
    # dans un dossier "uploads" situé dans le même répertoire que le script)
    if fichier:
        fichier.save(join(dirname(__file__), "uploads", fichier.filename))
    return template("merci.html")

Référence : Documentation de la balise input type="file" en HTML et Documentation de la méthode save() de bottle

En pratique

Calculatrice

Créer un formulaire de calcul qui permet de saisir deux nombres et de choisir (avec une liste déroulante) une opération (addition, soustraction, multiplication, division), et qui affiche le résultat. La route du formulaire et du résultat est la même (/calculatrice), la méthode est GET, et la page doit afficher

  • un message d'erreur si au moins un des deux nombres n'est pas un nombre ou si l'opération n'est pas reconnue
  • le résultat du calcul juste après le formulaire (forme "[a] [op] [b] = [resultat]")

Calculatrice : erreur

Calculatrice : résultat

Correction

Télécharger le code : calculatrice.html

% setdefault("a", "")
% setdefault("b", "")
% setdefault("op", "")
% setdefault("resultat", None)
% setdefault("erreur", None)

<body>
    <h1>Calculatrice</h1>
    <form action="/calculatrice" method="GET">
        % if erreur is not None:
        <p class="erreur">{{erreur }}</p>
        % end
        <input type="text" id="a" name="a" value="{{ a }}" placeholder="Nombre 1" />
        <select id="op" name="op">
            <option value="" {{ "selected" if op is None else "" }}>??</option>
            <option value="add" {{ "selected" if op == "add" else "" }}>+</option>
            <option value="sub" {{ "selected" if op == "sub" else "" }}>-</option>
            <option value="mul" {{ "selected" if op == "mul" else "" }}>*</option>
            <option value="div" {{ "selected" if op == "div" else "" }}>/</option>
        </select>
        <input type="text" id="b" name="b" value="{{ b }}" placeholder="Nombre 2"/>
        <button type="submit">=</button>
        % if resultat is not None:
            <span class="resultat">= {{ resultat }}</span>
        % end
    </form>
    % end
</body>

Télécharger le code du serveur

from bottle import route, run, template, static_file, get, request

# Calculatrice
@get('/calculatrice')
def calculatrice():
    a = request.query.get('a', default=None)
    b = request.query.get('b', default=None)
    op = request.query.get('op', default=None)
    if a is None and b is None and op is None:
        # Aucune valeur saisie, on affiche le formulaire vide
        return template("calculatrice.html")
    try:
        a = float(a)
        b = float(b)
    except ValueError:
        # Erreur de conversion, on affiche le formulaire avec un message d'erreur
        return template("calculatrice.html", 
                        erreur="Les valeurs saisies ne sont pas des nombres.",
                        a=a, b=b, op=op)
    match op:
        case "add":
            resultat = a + b
        case "sub":
            resultat = a - b
        case "mul":
            resultat = a * b
        case "div":
            if b == 0:
                return template("calculatrice.html", erreur="Division par zéro.",
                                a=a, b=b, op=op)
            resultat = a / b
        case _:
            return template("calculatrice.html", erreur="Opération inconnue.",
                            a=a, b=b, op=op)
    # Affichage du résultat
    return template("calculatrice.html", a=a, b=b, op=op, resultat=resultat)

Options de voiture

Créer un formulaire à l'adresse /mavoiture, permettant de choisir des options pour une voiture, et qui affiche un résumé des choix ainsi que le prix de la voiture (prix de base sans options = 18000 euros) lorsque le formulaire est soumis (méthode POST). Les options sont disponibles dans le dictionnaire suivant :

{
    "pmetal": {
        "titre": "Peinture métallisée",
        "prix": 500
    },
    "gps": {
        "titre": "GPS",
        "prix": 1000
    },
    "sieges": {
        "titre": "Sièges cuir",
        "prix": 1500
    },
    "toit": {
        "titre": "Toit ouvrant panoramique",
        "prix": 2000
    },
    "jantes": {
        "titre": "Jantes alliage",
        "prix": 400
    }
}

Vous pouvez choisir d'afficher les options dans une liste déroulante <select> (avec l'attribut multiple) ou avec des cases à cocher <input type="checkbox">. Dans les deux cas, le prix total doit être affiché sous le formulaire.

Ma voiture : formulaire

Correction

En version select[multiple] (Télécharger le code) :

% setdefault("options", {})
% setdefault("selected", [])
% setdefault("prix", 18000)
<body>
    <form action="/mavoiture" method="POST">
        <label for="options">Choisissez vos options :</label>
        <select id="options" name="options" multiple>
            % for key, value in options.items():
                <option value="{{ key }}" 
                    {{ "selected" if key in selected else "" }}>
                    {{ value['titre'] }} ({{ value['prix'] }} €)
                </option>
            % end
        </select>
        <button type="submit">Calculer le prix</button>
    </form>
    <p>Prix de base : 18000 €</p>
    % if prix > 18000:
        <p>Prix total : {{ prix }} €</p>
    % end
</body>

Ou en version input[type=checkbox] (Télécharger le code) :

% setdefault("options", {})
% setdefault("selected", [])
% setdefault("prix", 18000)
<body>
    <form action="/mavoiture" method="POST">
        <label for="options">Choisissez vos options :</label>
        % for key, value in options.items():
            <div>
                <input type="checkbox" id="{{ key }}" 
                    name="options" value="{{ key }}"
                    {{ "checked" if key in selected else "" }}>
                <label for="{{ key }}">
                    {{ value['titre'] }} ({{ value['prix'] }} €)
                </label>
            </div>
        % end
        <button type="submit">Calculer le prix</button>
    </form>
    <p>Prix de base : 18000 €</p>
    % if prix > 18000:
        <p>Prix total : {{ prix }} €</p>
    % end

Télécharger le code du serveur

from bottle import route, run, template, static_file, get, request

@route('/mavoiture', method=['GET', 'POST'])
def mavoiture():
    if request.method == 'POST':
        # Récupération des options
        options = request.POST.getall('options')
        prix = 18000
        for option in options:
            prix += OPTIONS[option]['prix']
        return template("mavoiture.html", options=OPTIONS, selected=options, prix=prix)
    else:
        return template("mavoiture.html", options=OPTIONS, selected=[], prix=18000)

Programme

  • NSI 1ère : intéraction client/serveur, requêtes HTTP, réponses du serveur
  • NSI 1ère : formulaire d'une page web, interaction avec l'utilisateur dans une page web