Bilan : niveau avancé¶
Vous venez de construire à la main, palier après palier, un seul et même mini-projet : le Catalogue d'articles.
Cette page récapitule les trois notions acquises, puis montre l'état final complet du contrôleur et des routes.
Les trois notions acquises¶
- Palier 1 : relier deux tables par une clé étrangère et les lire avec un
JOINSQL visible (articlesjoint àcategories). - Palier 2 : grouper des écritures atomiques avec
with transaction() as tx:(créer un article et incrémenter le compteur de sa catégorie, rollback sur erreur). - Palier 3 : exposer le catalogue en JSON (
Response.json) derrière un jetonAuthorization: Bearer …lu avecrequest.header.
État final de mvc/controllers/article_controller.py
# mvc/controllers/article_controller.py
from core.database.db import execute, fetch_all, insert
from core.database.transaction import transaction
from core.http.request import Request
from core.http.response import Response
from core.mvc.controller.base_controller import BaseController
from core.security.cookies import set_session_cookie
from core.security.session import get_session, get_session_id
from core.sessions.manager import get_session_store
SELECT_ARTICLES_WITH_CATEGORY = (
"SELECT a.id, a.title, c.name AS category "
"FROM articles a "
"JOIN categories c ON c.id = a.category_id "
"ORDER BY a.id"
)
SELECT_CATEGORIES = "SELECT id, name FROM categories ORDER BY name"
INSERT_ARTICLE = "INSERT INTO articles (title, category_id) VALUES (?, ?)"
INCREMENT_COUNT = "UPDATE categories SET article_count = article_count + 1 WHERE id = ?"
API_TOKEN = "forge-demo-token"
class ArticleController(BaseController):
@staticmethod
def _start_session(request: Request):
"""Garantit une session active et renvoie (session_id, csrf_token)."""
session_id = get_session_id(request)
if session_id is None or get_session(session_id) is None:
session_id = get_session_store().create()
session = get_session(session_id) or {}
return session_id, session.get("csrf_token", "")
@staticmethod
def index(request: Request) -> Response:
articles = fetch_all(SELECT_ARTICLES_WITH_CATEGORY)
return BaseController.render(
"article/index.html",
request=request,
context={"articles": articles},
)
@staticmethod
def create(request: Request) -> Response:
session_id, csrf_token = ArticleController._start_session(request)
response = BaseController.render(
"article/new.html",
request=request,
context={"categories": fetch_all(SELECT_CATEGORIES), "csrf_token": csrf_token},
)
set_session_cookie(response, session_id)
return response
@staticmethod
def store(request: Request) -> Response:
title = request.form("title", default="").strip()
category_id = request.form("category_id", default="").strip()
try:
with transaction() as tx:
insert(INSERT_ARTICLE, (title, category_id), tx=tx)
if not title:
raise ValueError("Le titre est obligatoire : tout est annulé.")
execute(INCREMENT_COUNT, (category_id,), tx=tx)
except ValueError as exc:
return BaseController.render(
"article/new.html",
status=422,
request=request,
context={"categories": fetch_all(SELECT_CATEGORIES), "error": str(exc)},
)
return BaseController.redirect("/article", request=request, flash="Article créé.")
@staticmethod
def api_index(request: Request) -> Response:
authorization = request.header("Authorization") or ""
if authorization != f"Bearer {API_TOKEN}":
return Response.json({"error": "Jeton manquant ou invalide."}, status=401)
articles = fetch_all(SELECT_ARTICLES_WITH_CATEGORY)
return Response.json({"articles": articles})
État final de mvc/routes.py
# mvc/routes.py
from core.http.router import Router
from mvc.controllers.home_controller import HomeController
from mvc.controllers.article_controller import ArticleController
router = Router()
with router.group("", public=True) as public:
public.add("GET", "/", HomeController.index, name="home-index")
public.add("GET", "/article", ArticleController.index, name="article-index")
public.add("GET", "/article/create", ArticleController.create, name="article-create")
public.add("POST", "/article/store", ArticleController.store, name="article-store")
public.add("GET", "/article/api-index", ArticleController.api_index, name="article-api_index")
Et ensuite¶
Vous savez relier vos données sans ORM, écrire de façon atomique et exposer une API JSON protégée, le SQL restant explicite.
Le récapitulatif rassemble toutes les API de la progression sur une seule page.