Pour finalisre notre application de gestion de tâches, nous devons implémenter les opérations de création, de modification et de suppression. Chaque fonctionnalité nécessitera des ajustements au niveau des modèles, des contrôleurs et des vues.
Implémentasion de la création
Le processus d'ajout d'une tâche suit un flux spécifique : saisie de l'utilisateur, envoi via une requête POST, traitement côté serveur, persistance des données et redirection vers la page principale.
Commençons par le gabarit HTML. Un formulaire est nécessaire pour capturer l'entrée :
<!-- templates/accueil.html -->
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Gestionnaire de Tâches</title>
<link rel="stylesheet" href="/static/styles.css">
</head>
<body>
<div class="wrapper">
<h1>Mes Tâches</h1>
<form method="post" action="/ajouter" class="input-group">
<input type="text" name="texte" placeholder="Nouvelle tâche..." required>
<button type="submit" class="btn-add">Créer</button>
</form>
<ul class="task-list">
{% for tache in liste_taches %}
<li><span>{{ tache.texte }}</span></li>
{% endfor %}
</ul>
</div>
</body>
</html>
Le style CSS associé :
/* static/styles.css */
.input-group { display: flex; gap: 10px; max-width: 500px; margin-bottom: 20px; }
.input-group input { flex-grow: 1; padding: 8px; }
.btn-add { padding: 8px 15px; cursor: pointer; }
Le serveur doit désormais interpréter les données envoyées dans le corps des requêtes POST. Nous étendons le parseur de requête :
# utils/http_parser.py
from urllib.parse import unquote_plus
class RequeteHTTP:
def __init__(self, flux_brut):
self.methode, self.chemin, self.en_tetes, self.params_url, self.donnees = self._analyser(flux_brut)
def _analyser(self, flux):
bloc_en_tete, bloc_corps = flux.split('\r\n\r\n', 1)
ligne_req, *rest = bloc_en_tete.split('\r\n')
methode, chemin_param, _ = ligne_req.split()
chemin, params = self._extraire_params(chemin_param)
dico_headers = {}
for ligne in rest:
cle, val = ligne.split(': ', 1)
dico_headers[cle] = val
return methode, chemin, dico_headers, params, self._decoder_corps(bloc_corps)
@staticmethod
def _extraire_params(data):
if '?' not in data:
return data, {}
url, query = data.split('?', 1)
dico = {p.split('=')[0]: p.split('=', 1)[1] for p in query.split('&') if '=' in p}
return url, dico
@staticmethod
def _decoder_corps(data):
if not data.strip():
return {}
return {p.split('=')[0]: unquote_plus(p.split('=', 1)[1]) for p in data.split('&') if '=' in p}
Une fois les données enregistrées, le navigateur doit être redirigé. Nous ajoutons une fonction utilitaire pour générer une réponse HTTP de redirection :
# utils/reponse.py
def redirection(vers_url, code_statut=302):
phrases = {200: 'OK', 302: 'FOUND', 405: 'METHOD NOT ALLOWED'}
entetes = {'Location': vers_url, 'Content-Length': '0'}
ligne_statut = f'HTTP/1.1 {code_statut} {phrases.get(code_statut, "UNKNOWN")}'
entetes_str = '\r\n'.join(f'{k}: {v}' for k, v in entetes.items())
return f'{ligne_statut}\r\n{entetes_str}\r\n\r\n'
Dans la couche modèle, la classe Tache nécessite une méthode pour persister l'objet dans le fichier JSON :
# models/base.py
import json, os
class Tache:
def __init__(self, identifiant=None, texte=""):
self.identifiant = identifiant
self.texte = texte
@classmethod
def _chemin_bd(cls):
return os.path.join(os.path.dirname(__file__), 'db', 'donnees.json')
@classmethod
def charger(cls):
with open(cls._chemin_bd(), 'r', encoding='utf-8') as f:
return json.load(f)
@classmethod
def persister(cls, liste):
with open(cls._chemin_bd(), 'w', encoding='utf-8') as f:
json.dump(liste, f, indent=4, ensure_ascii=False)
def enregistrer(self):
liste_actuelle = [t.__dict__ for t in self._toutes() if t.identifiant != self.identifiant]
if self.identifiant is None:
dernier = liste_actuelle[-1]['identifiant'] if liste_actuelle else 0
self.identifiant = dernier + 1
liste_actuelle.append(self.__dict__)
self.persister(liste_actuelle)
@classmethod
def _toutes(cls):
return [cls(**d) for d in cls.charger()]
Le contrôleur gère désormais la route /ajouter :
# controllers/taches.py
from models.base import Tache
from utils.reponse import redirection
def creer_tache(requete):
contenu = requete.donnees.get("texte", "").strip()
if contenu:
nouvelle = Tache(texte=contenu)
nouvelle.enregistrer()
return redirection("/")
chemins = {
'/ajouter': (creer_tache, ['POST'])
}
Modification d'une entrée
L'édition nécessite deux étapes : afficher le formulaire pré-rempli (via GET) et traiter la soumission (POST). Bien que PUT soit sémantiquement plus précis pour le REST, nous conservons POST pour uniformiser le traitement dans ce framework minimaliste.
Gabararit d'édition :
<!-- templates/modifier.html -->
<html lang="fr">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="/static/styles.css">
</head>
<body>
<div class="wrapper">
<h2>Modifier la tâche</h2>
<form method="post" action="/modifier" class="input-group">
<input type="hidden" name="id" value="{{ tache.identifiant }}">
<input type="text" name="texte" value="{{ tache.texte }}">
<button type="submit">Valider</button>
</form>
</div>
</body>
</html>
Nous devons également pouvoir interroger le modèle par critère. Ajoutons des méthodes de recherche :
# models/base.py (extension)
@classmethod
def filtrer(cls, **criteres):
resultats = []
for element in cls.charger():
match = True
for cle, val in criteres.items():
if str(element.get(cle)) != str(val):
match = False
break
if match:
resultats.append(cls(**element))
return resultats
@classmethod
def obtenir_par_id(cls, identifiant):
trouve = cls.filtrer(identifiant=identifiant)
return trouve[0] if trouve else None
Le contrôleur doit désormais gérer les deux verbes HTTP :
# controllers/taches.py (extension)
from utils.template import render_template # Fonction fictive de rendu
def gerer_modification(requete):
if requete.methode == 'POST':
id_cible = int(requete.donnees.get('id', -1))
nouveau_texte = requete.donnees.get('texte', '').strip()
if id_cible != -1 and nouveau_texte:
cible = Tache.obtenir_par_id(id_cible)
if cible:
cible.texte = nouveau_texte
cible.enregistrer()
return redirection("/")
else:
id_cible = int(requete.params_url.get('id', -1))
cible = Tache.obtenir_par_id(id_cible)
if not cible:
return redirection("/")
return render_template("modifier.html", tache=cible)
chemins.update({
'/modifier': (gerer_modification, ['GET', 'POST'])
})
Suppression d'une entrée
La logique de suppression suit le même schéma : un formulaire caché envoie l'identifiant, le contrôleur valide, le modèle retire l'entrée du fichier et le serveur redirige.
Ajout du bouton dans la liste :
{% for tache in liste_taches %}
<li>
<span>{{ tache.texte }}</span>
<div class="actions">
<a href="/modifier?id={{ tache.identifiant }}"><button>Éditer</button></a>
<form method="post" action="/supprimer" style="display:inline">
<input type="hidden" name="id" value="{{ tache.identifiant }}">
<button type="submit" class="btn-del">Supprimer</button>
</form>
</div>
</li>
{% endfor %}
Méthode de suppression dans le modèle :
# models/base.py (extension)
def supprimer(self):
reste = [t.__dict__ for t in self._toutes() if t.identifiant != self.identifiant]
self.persister(reste)
Et le contrôleur final :
# controllers/taches.py (extension)
def traiter_suppression(requete):
id_cible = int(requete.donnees.get('id', -1))
cible = Tache.obtenir_par_id(id_cible)
if cible:
cible.supprimer()
return redirection("/")
chemins.update({
'/supprimer': (traiter_suppression, ['POST'])
})