Conformité RGPD (P3) + transparence éditoriale (P4)
P3 — RGPD :
- Table `consents` + `POST /api/consent` (art. 7.1 — preuve du consentement)
- Dialogue de consentement explicite avant la première contribution (art. 9.2.a)
- Pages `/mentions-legales` et `/politique-confidentialite`
- `docs/RGPD.md` — registre des traitements, bases légales, sous-traitants
- `getVisitorId()` exporté depuis l'API client React
P4 — Transparence éditoriale :
- Page `/contributions-brutes` avec pagination et export JSON/CSV
- `GET /api/contributions`, `GET /api/contributions/export/{json,csv}`
- `GET /api/stats/public` — stats publiques sans données de rejet
- Label de transparence IA sur la colonne de synthèse
- Compteurs (acceptées / soumises) dans le bandeau d'intro
- `docs/PROMPTS_IA.md` — prompts intégraux publiés + analyse des biais
- Pied de page avec liens légaux et transparence
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -62,6 +62,18 @@ def init_db() -> None:
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)
|
||||
""")
|
||||
# Table de traçabilité des consentements RGPD (art. 7.1 — charge de la preuve)
|
||||
cur.execute("""
|
||||
CREATE TABLE IF NOT EXISTS consents (
|
||||
id SERIAL PRIMARY KEY,
|
||||
fingerprint_hash VARCHAR(64) NOT NULL,
|
||||
consent_version VARCHAR(20) NOT NULL,
|
||||
consented_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)
|
||||
""")
|
||||
cur.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_consents_fingerprint ON consents(fingerprint_hash)"
|
||||
)
|
||||
logger.info("Base de données initialisée.")
|
||||
|
||||
|
||||
@@ -85,6 +97,55 @@ def insert_idea(
|
||||
return dict(cur.fetchone())
|
||||
|
||||
|
||||
def create_consent(fingerprint_hash: str, consent_version: str) -> dict:
|
||||
"""Enregistre un consentement explicite (art. 7.1 RGPD — preuve)."""
|
||||
with db_cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO consents (fingerprint_hash, consent_version)
|
||||
VALUES (%s, %s)
|
||||
RETURNING *
|
||||
""",
|
||||
(fingerprint_hash, consent_version),
|
||||
)
|
||||
return dict(cur.fetchone())
|
||||
|
||||
|
||||
def get_public_contributions(page: int = 1, per_page: int = 20) -> tuple[list[dict], int]:
|
||||
"""Retourne les contributions acceptées paginées pour la vue publique."""
|
||||
offset = (page - 1) * per_page
|
||||
with db_cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT COUNT(*) as total FROM ideas WHERE accepted = TRUE AND flagged = FALSE"
|
||||
)
|
||||
total = cur.fetchone()["total"]
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, content, author, created_at
|
||||
FROM ideas
|
||||
WHERE accepted = TRUE AND flagged = FALSE
|
||||
ORDER BY created_at DESC
|
||||
LIMIT %s OFFSET %s
|
||||
""",
|
||||
(per_page, offset),
|
||||
)
|
||||
rows = [dict(row) for row in cur.fetchall()]
|
||||
return rows, total
|
||||
|
||||
|
||||
def get_public_stats() -> dict:
|
||||
"""Statistiques publiques — ne révèle pas les chiffres de rejet."""
|
||||
with db_cursor() as cur:
|
||||
cur.execute("SELECT COUNT(*) as total FROM ideas")
|
||||
total = cur.fetchone()["total"]
|
||||
cur.execute("SELECT COUNT(*) as accepted FROM ideas WHERE accepted = TRUE")
|
||||
accepted = cur.fetchone()["accepted"]
|
||||
cur.execute("SELECT updated_at FROM synthesis LIMIT 1")
|
||||
row = cur.fetchone()
|
||||
last_updated = row["updated_at"].isoformat() if row and row.get("updated_at") else None
|
||||
return {"total": total, "accepted": accepted, "lastUpdated": last_updated}
|
||||
|
||||
|
||||
def get_accepted_ideas() -> list[dict]:
|
||||
with db_cursor() as cur:
|
||||
cur.execute(
|
||||
|
||||
Reference in New Issue
Block a user