transfer from private server/git server
This commit is contained in:
13
web/models/__init__.py
Normal file
13
web/models/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""
|
||||
Couche models/ — Repository Pattern (Bloc 1 - C4, Bloc 2 - C4).
|
||||
|
||||
Chaque module de ce package encapsule TOUT l'accès SQL pour une entité :
|
||||
les couches routes/ et services/ n'écrivent jamais de SQL elles-mêmes et
|
||||
ne connaissent pas le schéma de la base. Cela garantit que le changement
|
||||
de SGBDR (ex. migration vers PostgreSQL) ne nécessite que la réécriture de
|
||||
ce package — le reste de l'application est inchangé (dépendance vers
|
||||
l'abstraction, cf. dossier technique section C3/C4).
|
||||
|
||||
Chaque module expose une interface CRUD standardisée :
|
||||
insert(db, ...) / fetch_all(db, ...) / get(db, id) / update(db, ...) / delete(db, id)
|
||||
"""
|
||||
57
web/models/annonce_model.py
Normal file
57
web/models/annonce_model.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
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
|
||||
17
web/models/audit_model.py
Normal file
17
web/models/audit_model.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""audit_model.py — Écriture dans la table de traçabilité audit_log (Bloc 1 - C10)."""
|
||||
|
||||
|
||||
def insert(db, action, statut, ressource=None, utilisateur_sub=None, ip_address=None):
|
||||
query = (
|
||||
"INSERT INTO audit_log (action, ressource, statut, utilisateur_sub, ip_address) "
|
||||
"VALUES (%s, %s, %s, %s, %s)"
|
||||
)
|
||||
return db.execute(query, (action, ressource, statut, utilisateur_sub, ip_address))
|
||||
|
||||
|
||||
def fetch_recent(db, limit=100):
|
||||
query = (
|
||||
"SELECT id, action, ressource, statut, utilisateur_sub, ip_address, created_at "
|
||||
"FROM audit_log ORDER BY created_at DESC LIMIT %s"
|
||||
)
|
||||
return db.fetch_all(query, (limit,))
|
||||
59
web/models/block_model.py
Normal file
59
web/models/block_model.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
block_model.py — Accès aux données pour l'entité "block" (widgets du dashboard).
|
||||
|
||||
Module historique de GestHub, migré de Flask-SQLAlchemy vers PyMySQL pour
|
||||
rester cohérent avec le reste de la couche models/ (accès direct, sans ORM,
|
||||
requêtes paramétrées).
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
|
||||
def _row_to_dict(row):
|
||||
if row is None:
|
||||
return None
|
||||
return {
|
||||
"id": row["id"],
|
||||
"type": row["block_type"],
|
||||
"column": row["column_name"],
|
||||
"position": row["position"],
|
||||
"data": json.loads(row["data"]) if row["data"] else {},
|
||||
}
|
||||
|
||||
|
||||
def fetch_all(db):
|
||||
query = (
|
||||
"SELECT id, block_type, column_name, position, data FROM blocks "
|
||||
"ORDER BY column_name, position"
|
||||
)
|
||||
rows = db.fetch_all(query)
|
||||
return [_row_to_dict(r) for r in rows]
|
||||
|
||||
|
||||
def insert(db, block_type, column_name, data, position=99):
|
||||
query = (
|
||||
"INSERT INTO blocks (block_type, column_name, position, data) "
|
||||
"VALUES (%s, %s, %s, %s)"
|
||||
)
|
||||
new_id = db.execute(query, (block_type, column_name, position, json.dumps(data)))
|
||||
return _row_to_dict({
|
||||
"id": new_id,
|
||||
"block_type": block_type,
|
||||
"column_name": column_name,
|
||||
"position": position,
|
||||
"data": json.dumps(data),
|
||||
})
|
||||
|
||||
|
||||
def update_position(db, block_id, column_name, position):
|
||||
query = "UPDATE blocks SET column_name = %s, position = %s WHERE id = %s"
|
||||
with db.cursor(commit=True) as cur:
|
||||
cur.execute(query, (column_name, position, block_id))
|
||||
return cur.rowcount
|
||||
|
||||
|
||||
def delete(db, block_id):
|
||||
query = "DELETE FROM blocks WHERE id = %s"
|
||||
with db.cursor(commit=True) as cur:
|
||||
cur.execute(query, (block_id,))
|
||||
return cur.rowcount
|
||||
32
web/models/evenement_model.py
Normal file
32
web/models/evenement_model.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""evenement_model.py — Accès aux données pour l'entité "evenement" (planning)."""
|
||||
|
||||
|
||||
def insert(db, titre, description, date_debut, date_fin, createur_sub):
|
||||
query = (
|
||||
"INSERT INTO evenements (titre, description, date_debut, date_fin, createur_sub) "
|
||||
"VALUES (%s, %s, %s, %s, %s)"
|
||||
)
|
||||
return db.execute(query, (titre, description, date_debut, date_fin, createur_sub))
|
||||
|
||||
|
||||
def fetch_all(db):
|
||||
query = (
|
||||
"SELECT id, titre, description, date_debut, date_fin, createur_sub, created_at "
|
||||
"FROM evenements ORDER BY date_debut ASC"
|
||||
)
|
||||
return db.fetch_all(query)
|
||||
|
||||
|
||||
def get(db, evenement_id):
|
||||
query = (
|
||||
"SELECT id, titre, description, date_debut, date_fin, createur_sub, created_at "
|
||||
"FROM evenements WHERE id = %s"
|
||||
)
|
||||
return db.fetch_one(query, (evenement_id,))
|
||||
|
||||
|
||||
def delete(db, evenement_id):
|
||||
query = "DELETE FROM evenements WHERE id = %s"
|
||||
with db.cursor(commit=True) as cur:
|
||||
cur.execute(query, (evenement_id,))
|
||||
return cur.rowcount
|
||||
32
web/models/fichier_model.py
Normal file
32
web/models/fichier_model.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""fichier_model.py — Accès aux données pour l'entité "fichier"."""
|
||||
|
||||
|
||||
def insert(db, nom_original, nom_stocke, mime_type, taille_octets, uploader_sub):
|
||||
query = (
|
||||
"INSERT INTO fichiers (nom_original, nom_stocke, mime_type, taille_octets, uploader_sub) "
|
||||
"VALUES (%s, %s, %s, %s, %s)"
|
||||
)
|
||||
return db.execute(query, (nom_original, nom_stocke, mime_type, taille_octets, uploader_sub))
|
||||
|
||||
|
||||
def fetch_all(db):
|
||||
query = (
|
||||
"SELECT id, nom_original, nom_stocke, mime_type, taille_octets, uploader_sub, created_at "
|
||||
"FROM fichiers ORDER BY created_at DESC"
|
||||
)
|
||||
return db.fetch_all(query)
|
||||
|
||||
|
||||
def get(db, fichier_id):
|
||||
query = (
|
||||
"SELECT id, nom_original, nom_stocke, mime_type, taille_octets, uploader_sub, created_at "
|
||||
"FROM fichiers WHERE id = %s"
|
||||
)
|
||||
return db.fetch_one(query, (fichier_id,))
|
||||
|
||||
|
||||
def delete(db, fichier_id):
|
||||
query = "DELETE FROM fichiers WHERE id = %s"
|
||||
with db.cursor(commit=True) as cur:
|
||||
cur.execute(query, (fichier_id,))
|
||||
return cur.rowcount
|
||||
54
web/models/rgpd_model.py
Normal file
54
web/models/rgpd_model.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
rgpd_model.py — Anonymisation des données personnelles (droit à l'effacement,
|
||||
RGPD art. 17). Bloc 1 - C10 : traçabilité, et Bloc 2 - C4 : la seule donnée
|
||||
personnelle stockée dans l'application est le `sub` OIDC (Keycloak) — aucune
|
||||
autre table ne duplique d'attribut d'identité.
|
||||
|
||||
`sub` est référencé dans quatre tables : annonces.auteur_sub,
|
||||
fichiers.uploader_sub, evenements.createur_sub (toutes NOT NULL — la donnée
|
||||
ne peut donc pas être mise à NULL sans casser la contrainte, d'où le choix
|
||||
d'un pseudonyme irréversible) et audit_log.utilisateur_sub (NULLable, mais
|
||||
pseudonymisée elle aussi par cohérence, pour garder la traçabilité du volume
|
||||
d'actions sans conserver l'identité).
|
||||
|
||||
Toutes les mises à jour sont exécutées dans UNE seule transaction InnoDB
|
||||
(un seul curseur `commit=True`) : soit les quatre tables sont mises à jour,
|
||||
soit aucune ne l'est en cas d'erreur (cohérence, pas d'anonymisation partielle).
|
||||
"""
|
||||
|
||||
|
||||
def anonymize(db, target_sub, anon_value):
|
||||
"""Remplace `target_sub` par `anon_value` dans toutes les tables qui le
|
||||
référencent. Retourne le nombre de lignes modifiées par table.
|
||||
"""
|
||||
with db.cursor(commit=True) as cur:
|
||||
cur.execute(
|
||||
"UPDATE annonces SET auteur_sub = %s WHERE auteur_sub = %s",
|
||||
(anon_value, target_sub),
|
||||
)
|
||||
annonces_rows = cur.rowcount
|
||||
|
||||
cur.execute(
|
||||
"UPDATE fichiers SET uploader_sub = %s WHERE uploader_sub = %s",
|
||||
(anon_value, target_sub),
|
||||
)
|
||||
fichiers_rows = cur.rowcount
|
||||
|
||||
cur.execute(
|
||||
"UPDATE evenements SET createur_sub = %s WHERE createur_sub = %s",
|
||||
(anon_value, target_sub),
|
||||
)
|
||||
evenements_rows = cur.rowcount
|
||||
|
||||
cur.execute(
|
||||
"UPDATE audit_log SET utilisateur_sub = %s WHERE utilisateur_sub = %s",
|
||||
(anon_value, target_sub),
|
||||
)
|
||||
audit_rows = cur.rowcount
|
||||
|
||||
return {
|
||||
"annonces": annonces_rows,
|
||||
"fichiers": fichiers_rows,
|
||||
"evenements": evenements_rows,
|
||||
"audit_log": audit_rows,
|
||||
}
|
||||
Reference in New Issue
Block a user