""" annonce_model.py — Accès aux données pour l'entité "annonce". Toutes les requêtes utilisent des paramètres préparés (%s) : aucune concaténation de chaîne SQL n'est utilisée dans ce module (OWASP A03:2021 — Injection). Voir tests de sécurité TS-01 (tests/test_security.py). """ class AnnonceNotFoundError(Exception): """Exception métier levée quand une annonce n'existe pas. La couche services/ traduit les erreurs SQL bas niveau en exceptions métier compréhensibles, afin que les routes n'aient jamais à connaître les détails de pymysql (cf. C4 — convention de gestion des erreurs). """ def insert(db, titre, contenu, auteur_sub, epinglee=False): query = ( "INSERT INTO annonces (titre, contenu, epinglee, auteur_sub) " "VALUES (%s, %s, %s, %s)" ) return db.execute(query, (titre, contenu, int(epinglee), auteur_sub)) def fetch_all(db): query = ( "SELECT id, titre, contenu, epinglee, auteur_sub, created_at, updated_at " "FROM annonces ORDER BY epinglee DESC, created_at DESC" ) return db.fetch_all(query) def get(db, annonce_id): query = ( "SELECT id, titre, contenu, epinglee, auteur_sub, created_at, updated_at " "FROM annonces WHERE id = %s" ) return db.fetch_one(query, (annonce_id,)) def update(db, annonce_id, titre, contenu, epinglee=False): query = ( "UPDATE annonces SET titre = %s, contenu = %s, epinglee = %s " "WHERE id = %s" ) with db.cursor(commit=True) as cur: cur.execute(query, (titre, contenu, int(epinglee), annonce_id)) return cur.rowcount def delete(db, annonce_id): query = "DELETE FROM annonces WHERE id = %s" with db.cursor(commit=True) as cur: cur.execute(query, (annonce_id,)) return cur.rowcount