Files
cesizen-individuel/generate_pdf.py
T
Azmog 0e07f5a57e test: ajoute des tests automatisés frontend (Vitest) et mobile (Jest)
- Frontend Next.js : Vitest + Testing Library
  - Button (rendu, clic, état loading)
  - auth-store (setAuth, déconnexion, persistance token)
  - script `npm test`, config jsdom + alias @/
- Mobile Expo : Jest (preset jest-expo)
  - getApiError (erreur Axios, validation, erreur générique, sans réponse)
  - script `npm test`, transformIgnorePatterns adapté
- Cahier de tests : section « Outils de test automatisés » (PHPUnit + Vitest + Jest)
  et entrée 6.4 (PV de recette) au sommaire

Suites vertes : backend 15, frontend 6, mobile 4. tsc/lint/build OK, expo-doctor 18/18.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 22:17:20 +02:00

967 lines
46 KiB
Python

#!/usr/bin/env python3
"""
Génération du document PDF : Cahier de Tests & Étude Comparative - CESIZen
"""
from fpdf import FPDF
import os
class CESIZenPDF(FPDF):
def __init__(self):
super().__init__()
self.set_auto_page_break(auto=True, margin=25)
def header(self):
if self.page_no() > 1:
self.set_font('Helvetica', 'I', 8)
self.set_text_color(100, 100, 100)
self.cell(0, 8, 'CESIZen - Cahier de Tests & Étude Comparative', align='L')
self.cell(0, 8, f'Page {self.page_no()}/{{nb}}', align='R', new_x="LMARGIN", new_y="NEXT")
self.set_draw_color(0, 102, 204)
self.set_line_width(0.5)
self.line(10, 18, 200, 18)
self.ln(5)
def footer(self):
self.set_y(-15)
self.set_font('Helvetica', 'I', 7)
self.set_text_color(150, 150, 150)
self.cell(0, 10, 'CESI École d\'Ingénieurs - Bloc INFCDAAL1 - Ministère des Solidarités et de la Santé', align='C')
def chapter_title(self, num, title):
self.set_font('Helvetica', 'B', 16)
self.set_text_color(0, 51, 102)
self.set_fill_color(230, 240, 250)
self.cell(0, 12, f' {num}. {title}', fill=True, new_x="LMARGIN", new_y="NEXT")
self.ln(4)
def section_title(self, title):
self.set_font('Helvetica', 'B', 12)
self.set_text_color(0, 80, 140)
self.cell(0, 9, f' {title}', new_x="LMARGIN", new_y="NEXT")
self.ln(2)
def subsection_title(self, title):
self.set_font('Helvetica', 'B', 10)
self.set_text_color(50, 50, 50)
self.cell(0, 8, f' {title}', new_x="LMARGIN", new_y="NEXT")
self.ln(1)
def body_text(self, text):
self.set_font('Helvetica', '', 9)
self.set_text_color(30, 30, 30)
self.multi_cell(0, 5, text)
self.ln(2)
def bullet(self, text, indent=10):
self.set_font('Helvetica', '', 9)
self.set_text_color(30, 30, 30)
self.set_x(self.l_margin + indent)
self.multi_cell(self.w - self.l_margin - self.r_margin - indent, 5, '- ' + text)
def colored_table(self, headers, data, col_widths=None):
if col_widths is None:
col_widths = [190 / len(headers)] * len(headers)
# Header
self.set_font('Helvetica', 'B', 8)
self.set_fill_color(0, 70, 130)
self.set_text_color(255, 255, 255)
for i, h in enumerate(headers):
self.cell(col_widths[i], 7, h, border=1, fill=True, align='C')
self.ln()
# Data rows
self.set_font('Helvetica', '', 8)
self.set_text_color(30, 30, 30)
fill = False
for row in data:
if self.get_y() > 260:
self.add_page()
self.set_font('Helvetica', 'B', 8)
self.set_fill_color(0, 70, 130)
self.set_text_color(255, 255, 255)
for i, h in enumerate(headers):
self.cell(col_widths[i], 7, h, border=1, fill=True, align='C')
self.ln()
self.set_font('Helvetica', '', 8)
self.set_text_color(30, 30, 30)
fill = False
if fill:
self.set_fill_color(240, 245, 250)
else:
self.set_fill_color(255, 255, 255)
max_h = 7
for i, val in enumerate(row):
self.cell(col_widths[i], max_h, str(val), border=1, fill=True, align='C')
self.ln()
fill = not fill
def score_table(self, headers, data, col_widths=None):
"""Table with colored last column based on score"""
if col_widths is None:
col_widths = [190 / len(headers)] * len(headers)
self.set_font('Helvetica', 'B', 8)
self.set_fill_color(0, 70, 130)
self.set_text_color(255, 255, 255)
for i, h in enumerate(headers):
self.cell(col_widths[i], 7, h, border=1, fill=True, align='C')
self.ln()
self.set_font('Helvetica', '', 8)
for row in data:
if self.get_y() > 260:
self.add_page()
self.set_font('Helvetica', 'B', 8)
self.set_fill_color(0, 70, 130)
self.set_text_color(255, 255, 255)
for i, h in enumerate(headers):
self.cell(col_widths[i], 7, h, border=1, fill=True, align='C')
self.ln()
self.set_font('Helvetica', '', 8)
for i, val in enumerate(row):
if i == len(row) - 1:
try:
score = float(str(val).replace(',', '.'))
if score >= 4.0:
self.set_fill_color(200, 240, 200)
elif score >= 3.0:
self.set_fill_color(255, 255, 200)
else:
self.set_fill_color(255, 210, 210)
except ValueError:
self.set_fill_color(255, 255, 255)
self.set_text_color(30, 30, 30)
self.cell(col_widths[i], 7, str(val), border=1, fill=True, align='C')
else:
self.set_fill_color(255, 255, 255)
self.set_text_color(30, 30, 30)
self.cell(col_widths[i], 7, str(val), border=1, fill=True, align='C' if i > 0 else 'L')
self.ln()
def generate_pdf():
pdf = CESIZenPDF()
pdf.alias_nb_pages()
# ============================
# PAGE DE GARDE
# ============================
pdf.add_page()
pdf.ln(30)
pdf.set_font('Helvetica', 'B', 28)
pdf.set_text_color(0, 51, 102)
pdf.cell(0, 15, 'CESIZen', align='C', new_x="LMARGIN", new_y="NEXT")
pdf.ln(5)
pdf.set_font('Helvetica', '', 14)
pdf.set_text_color(80, 80, 80)
pdf.cell(0, 10, 'Plateforme de Gestion du Stress & Suivi Émotionnel', align='C', new_x="LMARGIN", new_y="NEXT")
pdf.ln(10)
pdf.set_draw_color(0, 102, 204)
pdf.set_line_width(1)
pdf.line(60, pdf.get_y(), 150, pdf.get_y())
pdf.ln(15)
pdf.set_font('Helvetica', 'B', 16)
pdf.set_text_color(0, 70, 130)
pdf.cell(0, 10, 'Cahier de Tests, Étude Comparative', align='C', new_x="LMARGIN", new_y="NEXT")
pdf.cell(0, 10, '& Guide d\'Installation', align='C', new_x="LMARGIN", new_y="NEXT")
pdf.ln(20)
pdf.set_font('Helvetica', '', 11)
pdf.set_text_color(60, 60, 60)
pdf.cell(0, 8, 'Commanditaire : Ministère des Solidarités et de la Santé', align='C', new_x="LMARGIN", new_y="NEXT")
pdf.cell(0, 8, 'Bloc : Concevoir les solutions logicielles (INFCDAAL1)', align='C', new_x="LMARGIN", new_y="NEXT")
pdf.cell(0, 8, 'École : CESI École d\'Ingénieurs', align='C', new_x="LMARGIN", new_y="NEXT")
pdf.ln(15)
pdf.set_font('Helvetica', 'I', 10)
pdf.set_text_color(120, 120, 120)
pdf.cell(0, 8, 'Date : Mars 2026', align='C', new_x="LMARGIN", new_y="NEXT")
pdf.cell(0, 8, 'Version : 1.0', align='C', new_x="LMARGIN", new_y="NEXT")
# ============================
# TABLE DES MATIÈRES
# ============================
pdf.add_page()
pdf.set_font('Helvetica', 'B', 18)
pdf.set_text_color(0, 51, 102)
pdf.cell(0, 12, 'Table des matières', new_x="LMARGIN", new_y="NEXT")
pdf.ln(5)
toc = [
('1', 'Cahier de Tests & Critères d\'Évaluation', 3),
(' 1.1', 'Critères d\'évaluation techniques', 3),
(' 1.2', 'Tests fonctionnels - Authentification', 3),
(' 1.3', 'Tests fonctionnels - Tracker émotionnel', 4),
(' 1.4', 'Tests fonctionnels - Feeds / Ressources', 4),
(' 1.5', 'Tests fonctionnels - Administration', 5),
(' 1.6', 'Tests fonctionnels - Profil utilisateur', 5),
(' 1.7', 'Tests non fonctionnels', 5),
('2', 'Tableau Comparatif Détaillé', 6),
(' 2.1', 'Solutions comparées', 6),
(' 2.2', 'Comparaison par catégorie', 7),
('3', 'Synthèse des Scores Pondérés', 8),
(' 3.1', 'Matrice de pondération', 8),
(' 3.2', 'Résultats finaux', 8),
('4', 'Pertinence de la Solution Retenue', 9),
(' 4.1', 'Solution idéale identifiée', 9),
(' 4.2', 'Justification technique', 9),
(' 4.3', 'Adéquation aux besoins', 10),
('5', 'Guide d\'Installation', 10),
(' 5.1', 'Prérequis', 10),
(' 5.2', 'Installation avec Docker', 11),
(' 5.3', 'Installation manuelle', 11),
(' 5.4', 'Configuration', 12),
('6', 'Procédures de Validation', 12),
(' 6.1', 'Plan de validation', 12),
(' 6.2', 'Checklist de validation', 13),
(' 6.3', 'Critères d\'acceptation', 13),
(' 6.4', 'Modèle de procès-verbal de recette', 14),
]
pdf.set_font('Helvetica', '', 10)
for num, title, page in toc:
if num.strip().isdigit():
pdf.set_font('Helvetica', 'B', 10)
pdf.set_text_color(0, 51, 102)
else:
pdf.set_font('Helvetica', '', 9)
pdf.set_text_color(60, 60, 60)
pdf.cell(15, 7, num)
pdf.cell(155, 7, title)
pdf.cell(20, 7, str(page), align='R', new_x="LMARGIN", new_y="NEXT")
# ============================
# 1. CAHIER DE TESTS & CRITÈRES D'ÉVALUATION
# ============================
pdf.add_page()
pdf.chapter_title('1', 'Cahier de Tests & Critères d\'Évaluation')
pdf.section_title('1.1 Critères d\'évaluation techniques')
pdf.body_text(
'Les critères ci-dessous définissent les exigences qualité du projet CESIZen. '
'Chaque critère est noté de 1 (insuffisant) à 5 (excellent) et pondéré selon son importance.'
)
pdf.colored_table(
['Critère', 'Pondération', 'Description', 'Seuil min.'],
[
['Performance', '15%', 'Temps de réponse API < 200ms', '3/5'],
['Sécurité', '20%', 'Auth JWT, CORS, validation, RGPD', '4/5'],
['Maintenabilité', '15%', 'Architecture MVC, code documenté', '3/5'],
['Scalabilité', '10%', 'Docker, services découplés', '3/5'],
['Ergonomie (UX)', '15%', 'Interface intuitive, responsive', '3/5'],
['Conformité fonctionnelle', '15%', 'Couverture des user stories', '4/5'],
['Fiabilité', '10%', 'Gestion d\'erreurs, logs, audit', '3/5'],
],
[45, 25, 85, 35]
)
pdf.ln(2)
pdf.subsection_title('Outils de test automatisés')
pdf.body_text(
'Les tests sont automatisés sur les trois briques du projet et exécutables en une commande :'
)
pdf.bullet('Backend (API Laravel) : PHPUnit - 15 tests (authentification, tracker, admin, RGPD). Commande : php artisan test.')
pdf.bullet('Frontend Web (Next.js) : Vitest + Testing Library - composants UI et store d\'authentification. Commande : npm test.')
pdf.bullet('Mobile (React Native / Expo) : Jest (preset jest-expo) - utilitaires d\'appel API. Commande : npm test.')
pdf.ln(4)
pdf.section_title('1.2 Tests fonctionnels - Authentification')
pdf.colored_table(
['ID', 'Cas de test', 'Entrée', 'Résultat attendu', 'Statut', 'Responsable'],
[
['T-AUTH-01', 'Inscription valide', 'Nom, prénom, email, mdp', 'Compte créé, token JWT', 'OK', 'Dév. backend'],
['T-AUTH-02', 'Inscription email existant', 'Email déjà utilisé', 'Erreur 422, msg clair', 'OK', 'Dév. backend'],
['T-AUTH-03', 'Connexion valide', 'Email + mdp corrects', 'Token JWT retourné', 'OK', 'Dév. backend'],
['T-AUTH-04', 'Connexion mdp erroné', 'Email + mdp incorrect', 'Erreur 401', 'OK', 'Dév. backend'],
['T-AUTH-05', 'Connexion compte inactif', 'Compte désactivé', 'Erreur 403', 'OK', 'Dév. backend'],
['T-AUTH-06', 'Rate limiting login', '6 tentatives en 1 min', 'Erreur 429 throttle', 'OK', 'Dév. backend'],
['T-AUTH-07', 'Déconnexion', 'Token valide', 'Token invalidé', 'OK', 'Dév. backend'],
['T-AUTH-08', 'Refresh token', 'Token expirant', 'Nouveau token retourné', 'OK', 'Dév. backend'],
['T-AUTH-09', 'Accès route protégée sans token', 'Aucun token', 'Erreur 401', 'OK', 'Dév. backend'],
['T-AUTH-10', 'Accès route admin sans rôle', 'Token membre', 'Erreur 403', 'OK', 'Dév. backend'],
],
[20, 42, 40, 49, 15, 24]
)
pdf.ln(3)
pdf.section_title('1.3 Tests fonctionnels - Tracker émotionnel')
pdf.colored_table(
['ID', 'Cas de test', 'Entrée', 'Résultat attendu', 'Statut', 'Responsable'],
[
['T-TRK-01', 'Créer saisie', 'Émotion, intensité 1-10, note', 'Saisie enregistrée', 'OK', 'Dév. backend'],
['T-TRK-02', 'Créer saisie sans tracker', 'Premier usage', 'Tracker auto-créé', 'OK', 'Dév. backend'],
['T-TRK-03', 'Lister saisies', 'Token valide', 'Liste paginée (20/page)', 'OK', 'Dév. backend'],
['T-TRK-04', 'Filtrer par date', 'date_debut, date_fin', 'Saisies dans la période', 'OK', 'Dév. backend'],
['T-TRK-05', 'Filtrer par émotion', 'emotion_id', 'Saisies correspondantes', 'OK', 'Dév. backend'],
['T-TRK-06', 'Modifier saisie', 'Nouvelle intensité/note', 'Saisie mise à jour', 'OK', 'Dév. backend'],
['T-TRK-07', 'Supprimer saisie', 'ID saisie existante', 'Saisie supprimée', 'OK', 'Dév. backend'],
['T-TRK-08', 'Rapport semaine', 'period=week', 'Stats hebdomadaires', 'OK', 'Dév. backend'],
['T-TRK-09', 'Rapport mois', 'period=month', 'Stats mensuelles', 'OK', 'Dév. backend'],
['T-TRK-10', 'Rapport trimestre', 'period=quarter', 'Stats trimestrielles', 'OK', 'Dév. backend'],
['T-TRK-11', 'Rapport année', 'period=year', 'Stats annuelles', 'OK', 'Dév. backend'],
['T-TRK-12', 'Saisie intensité hors bornes', 'intensité = 15', 'Erreur validation 422', 'OK', 'Dév. backend'],
],
[20, 42, 42, 47, 15, 24]
)
pdf.add_page()
pdf.section_title('1.4 Tests fonctionnels - Feeds / Ressources')
pdf.colored_table(
['ID', 'Cas de test', 'Entrée', 'Résultat attendu', 'Statut', 'Responsable'],
[
['T-FEED-01', 'Lister feeds publiés', 'Aucun (public)', 'Liste ordonnée des articles', 'OK', 'Dév. backend'],
['T-FEED-02', 'Détail feed par slug', 'Slug existant', 'Contenu complet + auteur', 'OK', 'Dév. backend'],
['T-FEED-03', 'Feed slug inexistant', 'Slug invalide', 'Erreur 404', 'OK', 'Dév. backend'],
['T-FEED-04', 'Feed non publié inaccessible', 'Feed draft', 'Non listé / 404', 'OK', 'Dév. backend'],
],
[22, 45, 38, 46, 15, 24]
)
pdf.ln(5)
pdf.section_title('1.5 Tests fonctionnels - Administration')
pdf.colored_table(
['ID', 'Cas de test', 'Entrée', 'Résultat attendu', 'Statut', 'Responsable'],
[
['T-ADM-01', 'Lister utilisateurs', 'Token admin', 'Liste paginée (15/p)', 'OK', 'Dév. backend'],
['T-ADM-02', 'Rechercher utilisateur', 'search=nom', 'Résultats ilike', 'OK', 'Dév. backend'],
['T-ADM-03', 'Créer utilisateur', 'Données complètes', 'Utilisateur + Tracker', 'OK', 'Dév. backend'],
['T-ADM-04', 'Modifier utilisateur', 'Nouvelles données', 'Utilisateur mis à jour', 'OK', 'Dév. backend'],
['T-ADM-05', 'Activer/Désactiver', 'ID user', 'Toggle est_actif', 'OK', 'Dév. backend'],
['T-ADM-06', 'Supprimer utilisateur', 'ID user', 'Soft delete', 'OK', 'Dév. backend'],
['T-ADM-07', 'CRUD feeds (admin)', 'Données feed', 'Création/modif/suppr.', 'OK', 'Dév. backend'],
['T-ADM-08', 'CRUD émotions', 'Données émotion', 'Création/modif/désact.', 'OK', 'Dév. backend'],
['T-ADM-09', 'Désactiver émotion parent', 'ID parent', 'Parent + enfants désact.', 'OK', 'Dév. backend'],
['T-ADM-10', 'Audit trail', 'Actions admin', 'Logs en base (audits)', 'OK', 'Dév. backend'],
],
[20, 42, 38, 46, 15, 29]
)
pdf.ln(5)
pdf.section_title('1.6 Tests fonctionnels - Profil utilisateur')
pdf.colored_table(
['ID', 'Cas de test', 'Entrée', 'Résultat attendu', 'Statut', 'Responsable'],
[
['T-PRO-01', 'Voir profil', 'Token valide', 'Infos profil complètes', 'OK', 'Dév. backend'],
['T-PRO-02', 'Modifier nom/prénom', 'Nouvelles valeurs', 'Profil mis à jour', 'OK', 'Dév. backend'],
['T-PRO-03', 'Changer mot de passe', 'Ancien + nouveau mdp', 'Mot de passe changé', 'OK', 'Dév. backend'],
['T-PRO-04', 'Ancien mdp incorrect', 'Mauvais ancien mdp', 'Erreur 422', 'OK', 'Dév. backend'],
['T-PRO-05', 'Supprimer compte (RGPD)', 'Confirmation', 'Soft delete du compte', 'OK', 'DPO / Dév.'],
],
[20, 42, 42, 47, 15, 24]
)
pdf.ln(5)
pdf.section_title('1.7 Tests non fonctionnels')
pdf.colored_table(
['ID', 'Catégorie', 'Test', 'Critère', 'Statut', 'Responsable'],
[
['T-NF-01', 'Performance', 'Temps de réponse API', '< 200ms (95e centile)', 'OK', 'DevOps'],
['T-NF-02', 'Performance', 'Chargement page front', '< 2s (First Contentful Paint)', 'OK', 'DevOps'],
['T-NF-03', 'Sécurité', 'Injection SQL', 'Eloquent ORM paramétré', 'OK', 'Dév. backend'],
['T-NF-04', 'Sécurité', 'XSS', 'Échappement React natif', 'OK', 'Dév. front'],
['T-NF-05', 'Sécurité', 'CORS', 'Origines restreintes', 'OK', 'Dév. backend'],
['T-NF-06', 'Sécurité', 'JWT expiration', 'Tokens à durée limitée', 'OK', 'Dév. backend'],
['T-NF-07', 'Sécurité', 'Rate limiting', 'Throttle 5 tentatives/min', 'OK', 'Dév. backend'],
['T-NF-08', 'Accessibilité', 'Responsive design', 'Mobile, tablette, desktop', 'OK', 'Dév. front'],
['T-NF-09', 'Accessibilité', 'Mode sombre', 'Toggle dark/light mode', 'OK', 'Dév. front'],
['T-NF-10', 'RGPD', 'Droit à l\'effacement', 'Soft delete + anonymisation', 'OK', 'DPO'],
['T-NF-11', 'RGPD', 'Consentement', 'Champ consentement_rgpd', 'OK', 'DPO'],
['T-NF-12', 'Fiabilité', 'Gestion erreurs API', 'Réponses JSON structurées', 'OK', 'DevOps'],
['T-NF-13', 'Fiabilité', 'Audit trail admin', 'Table audits traçabilité', 'OK', 'DevOps'],
],
[16, 26, 38, 52, 14, 34]
)
# ============================
# 2. TABLEAU COMPARATIF DÉTAILLÉ
# ============================
pdf.add_page()
pdf.chapter_title('2', 'Tableau Comparatif Détaillé')
pdf.section_title('2.1 Solutions comparées')
pdf.body_text(
'Quatre architectures techniques ont été évaluées pour répondre aux besoins de la plateforme CESIZen. '
'La comparaison porte sur des critères techniques, organisationnels et économiques.'
)
pdf.ln(2)
pdf.subsection_title('Solution A : Laravel 12 + Next.js 16 (Solution retenue)')
pdf.body_text(
'Backend API REST Laravel 12 (PHP 8.4) avec authentification JWT, '
'frontend Next.js 16 (React 19, TypeScript), PostgreSQL 16, Docker. '
'Architecture découplée, API-first.'
)
pdf.subsection_title('Solution B : Symfony 7 + Vue.js 3')
pdf.body_text(
'Backend Symfony 7 (PHP 8.3) avec API Platform, frontend Vue.js 3 (Composition API), '
'MySQL 8, Docker. Architecture similaire mais écosystème Symfony.'
)
pdf.subsection_title('Solution C : Django 5 + React 19')
pdf.body_text(
'Backend Django 5 (Python 3.12) avec Django REST Framework, frontend React 19 (Vite), '
'PostgreSQL 16, Docker. Écosystème Python.'
)
pdf.subsection_title('Solution D : Express.js + Angular 18')
pdf.body_text(
'Backend Express.js (Node.js 20) avec TypeORM, frontend Angular 18 (TypeScript), '
'MongoDB 7, Docker. Stack full JavaScript/TypeScript.'
)
pdf.ln(3)
pdf.section_title('2.2 Comparaison par catégorie')
pdf.subsection_title('Architecture & Performance')
pdf.colored_table(
['Critère', 'Sol. A (Laravel+Next)', 'Sol. B (Symfony+Vue)', 'Sol. C (Django+React)', 'Sol. D (Express+Angular)'],
[
['Architecture API', 'REST JSON, MVC', 'REST API Platform', 'DRF ViewSets', 'REST Express'],
['Pattern', 'MVC + App Router', 'MVC + Composition', 'MVT + SPA', 'Middleware + MVC'],
['SSR/SSG', 'Oui (Next.js natif)', 'Non (SPA)', 'Non (SPA)', 'Oui (Angular Universal)'],
['Temps rép. API', '< 150ms', '< 180ms', '< 160ms', '< 120ms'],
['ORM', 'Eloquent', 'Doctrine', 'Django ORM', 'TypeORM'],
['Cache', 'Redis/File', 'Redis/Doctrine', 'Redis/Memcached', 'Redis/in-memory'],
],
[30, 40, 40, 40, 40]
)
pdf.ln(3)
pdf.subsection_title('Sécurité & Authentification')
pdf.colored_table(
['Critère', 'Sol. A', 'Sol. B', 'Sol. C', 'Sol. D'],
[
['Auth', 'JWT (tymon/jwt)', 'JWT (lexik/jwt)', 'JWT (simplejwt)', 'JWT (jsonwebtoken)'],
['CORS', 'fruitcake/cors', 'NelmioCors', 'django-cors', 'cors middleware'],
['CSRF', 'N/A (API stateless)', 'N/A (API)', 'Built-in', 'N/A (API)'],
['Validation input', 'Form Requests', 'Validators', 'Serializers', 'Joi/Zod'],
['Rate limiting', 'Natif Laravel', 'Symfony Rate', 'DRF Throttle', 'express-rate-limit'],
['SQL Injection', 'Eloquent param.', 'Doctrine param.', 'Django ORM', 'TypeORM param.'],
['RGPD', 'Soft delete', 'Soft delete', 'GDPR lib', 'Custom'],
],
[30, 40, 40, 40, 40]
)
pdf.add_page()
pdf.subsection_title('Écosystème & Productivité')
pdf.colored_table(
['Critère', 'Sol. A', 'Sol. B', 'Sol. C', 'Sol. D'],
[
['Courbe d\'apprentissage', 'Faible', 'Moyenne', 'Faible', 'Élevée'],
['Documentation', 'Excellente', 'Très bonne', 'Excellente', 'Bonne'],
['Communauté', 'Très large', 'Large', 'Très large', 'Large'],
['Tooling CLI', 'Artisan', 'Console', 'manage.py', 'Custom CLI'],
['Migrations', 'Artisan migrate', 'Doctrine migr.', 'Django migrate', 'TypeORM migr.'],
['Seeders natifs', 'Oui', 'Fixtures', 'Fixtures', 'Custom seeders'],
['Tests intégrés', 'PHPUnit natif', 'PHPUnit natif', 'pytest natif', 'Jest/Mocha'],
['State management', 'Zustand', 'Pinia', 'Redux/Zustand', 'NgRx/Signals'],
['Styling', 'Tailwind CSS 4', 'Tailwind/Vuetify', 'Tailwind/MUI', 'Angular Material'],
],
[35, 38, 38, 38, 41]
)
pdf.ln(3)
pdf.subsection_title('Infrastructure & Déploiement')
pdf.colored_table(
['Critère', 'Sol. A', 'Sol. B', 'Sol. C', 'Sol. D'],
[
['Conteneurisation', 'Docker Compose', 'Docker Compose', 'Docker Compose', 'Docker Compose'],
['Base de données', 'PostgreSQL 16', 'MySQL 8', 'PostgreSQL 16', 'MongoDB 7'],
['Services Docker', '3 (PG+API+Front)', '3 (MySQL+API+Front)', '3 (PG+API+Front)', '3 (Mongo+API+Front)'],
['Hot reload dev', 'Oui (volumes)', 'Oui (volumes)', 'Oui (volumes)', 'Oui (volumes)'],
['Build prod', 'Optimisé', 'Optimisé', 'Multi-stage', 'Multi-stage'],
['Hébergement coût', 'Faible', 'Faible', 'Moyen', 'Faible'],
],
[30, 40, 40, 40, 40]
)
# ============================
# 3. SYNTHÈSE DES SCORES PONDÉRÉS
# ============================
pdf.add_page()
pdf.chapter_title('3', 'Synthèse des Scores Pondérés')
pdf.section_title('3.1 Matrice de pondération')
pdf.body_text(
'Chaque critère est noté de 1 à 5 et multiplié par son coefficient de pondération. '
'Le score total maximum est de 5,00. Les pondérations reflètent les priorités du Ministère '
'des Solidarités et de la Santé : sécurité des données, conformité RGPD et fiabilité.'
)
pdf.score_table(
['Critère', 'Poids', 'Sol. A', 'Sol. B', 'Sol. C', 'Sol. D'],
[
['Sécurité & RGPD', '20%', '5', '4', '4', '3'],
['Conformité fonctionnelle', '15%', '5', '4', '4', '4'],
['Performance', '15%', '4', '4', '4', '5'],
['Ergonomie (UX)', '15%', '5', '4', '4', '3'],
['Maintenabilité', '15%', '5', '4', '4', '3'],
['Scalabilité', '10%', '4', '4', '5', '4'],
['Fiabilité', '10%', '5', '4', '4', '3'],
],
[40, 20, 25, 25, 25, 25]
)
pdf.ln(5)
pdf.section_title('3.2 Résultats finaux')
pdf.body_text('Calcul : Score total = Somme(Note x Poids) pour chaque solution.')
# Calculate weighted scores
weights = [0.20, 0.15, 0.15, 0.15, 0.15, 0.10, 0.10]
scores_a = [5, 5, 4, 5, 5, 4, 5]
scores_b = [4, 4, 4, 4, 4, 4, 4]
scores_c = [4, 4, 4, 4, 4, 5, 4]
scores_d = [3, 4, 5, 3, 3, 4, 3]
total_a = sum(w * s for w, s in zip(weights, scores_a))
total_b = sum(w * s for w, s in zip(weights, scores_b))
total_c = sum(w * s for w, s in zip(weights, scores_c))
total_d = sum(w * s for w, s in zip(weights, scores_d))
pdf.score_table(
['Solution', 'Score pondéré /5', 'Classement', 'Décision'],
[
[f'A - Laravel 12 + Next.js 16', f'{total_a:.2f}', '1er', 'RETENUE'],
[f'B - Symfony 7 + Vue.js 3', f'{total_b:.2f}', '2ème', 'Alternative'],
[f'C - Django 5 + React 19', f'{total_c:.2f}', '3ème', 'Écartée'],
[f'D - Express.js + Angular 18', f'{total_d:.2f}', '4ème', 'Écartée'],
],
[60, 40, 40, 50]
)
pdf.ln(5)
pdf.set_font('Helvetica', 'B', 10)
pdf.set_text_color(0, 100, 0)
pdf.cell(0, 8, f' Solution A (Laravel 12 + Next.js 16) obtient le meilleur score : {total_a:.2f}/5.00', new_x="LMARGIN", new_y="NEXT")
pdf.set_text_color(30, 30, 30)
pdf.ln(3)
pdf.subsection_title('Détail des scores pondérés - Solution A')
detail_data = []
criteria = ['Sécurité & RGPD', 'Conformité fonctionnelle', 'Performance', 'Ergonomie (UX)', 'Maintenabilité', 'Scalabilité', 'Fiabilité']
for i, c in enumerate(criteria):
weighted = weights[i] * scores_a[i]
detail_data.append([c, f'{weights[i]*100:.0f}%', f'{scores_a[i]}/5', f'{weighted:.2f}'])
detail_data.append(['TOTAL', '100%', '-', f'{total_a:.2f}/5.00'])
pdf.colored_table(
['Critère', 'Poids', 'Note', 'Score pondéré'],
detail_data,
[55, 35, 40, 60]
)
# ============================
# 4. PERTINENCE DE LA SOLUTION RETENUE
# ============================
pdf.add_page()
pdf.chapter_title('4', 'Pertinence de la Solution Retenue')
pdf.section_title('4.1 Solution idéale identifiée')
pdf.body_text(
'La solution idéale pour le projet CESIZen est un stack composé de Laravel 12 (PHP 8.4) '
'en backend API REST et Next.js 16 (React 19, TypeScript) en frontend, avec PostgreSQL 16 '
'comme base de données et une orchestration Docker Compose.'
)
pdf.body_text(
'Cette architecture répond à l\'ensemble des exigences fonctionnelles et non fonctionnelles '
'identifiées lors de la phase d\'analyse, tout en offrant le meilleur compromis entre '
'productivité de développement, sécurité, performance et maintenabilité.'
)
pdf.section_title('4.2 Justification technique')
pdf.subsection_title('Backend : Laravel 12 (PHP 8.4)')
pdf.bullet('Architecture MVC éprouvée avec séparation claire des responsabilités')
pdf.bullet('Eloquent ORM : protection native contre les injections SQL')
pdf.bullet('Form Requests : validation centralisée et réutilisable des entrées')
pdf.bullet('Middleware JWT + RBAC : authentification et autorisation robustes')
pdf.bullet('Artisan CLI : migrations, seeders, génération de code automatisée')
pdf.bullet('Soft Deletes natifs : conformité RGPD (droit à l\'effacement)')
pdf.bullet('Audit trail intégré : traçabilité complète des actions admin')
pdf.bullet('Rate limiting natif : protection contre les attaques par force brute')
pdf.ln(2)
pdf.subsection_title('Frontend : Next.js 16 (React 19, TypeScript)')
pdf.bullet('App Router : routage déclaratif avec layouts imbriqués')
pdf.bullet('SSR natif : performances de rendu optimales & SEO')
pdf.bullet('TypeScript strict : détection d\'erreurs à la compilation')
pdf.bullet('Zustand : gestion d\'état légère avec persistance localStorage')
pdf.bullet('TanStack Query : cache intelligent, invalidation automatique')
pdf.bullet('Tailwind CSS 4 : styling utilitaire, dark mode, responsive natif')
pdf.bullet('Middleware Next.js : protection des routes côté serveur')
pdf.bullet('Recharts : visualisation de données émotionnelles interactive')
pdf.ln(2)
pdf.subsection_title('Base de données : PostgreSQL 16')
pdf.bullet('Performances supérieures pour les requêtes complexes (rapports)')
pdf.bullet('Support JSON natif pour les champs d\'audit (anciennes/nouvelles valeurs)')
pdf.bullet('ILIKE natif pour la recherche utilisateur (sensibilité casse)')
pdf.bullet('Intégrité référentielle stricte avec contraintes FK et cascades')
pdf.ln(2)
pdf.subsection_title('Infrastructure : Docker Compose')
pdf.bullet('3 services isolés : PostgreSQL, Backend Laravel, Frontend Next.js')
pdf.bullet('Environnement reproductible sur tout poste de développement')
pdf.bullet('Health checks PostgreSQL pour l\'ordre de démarrage')
pdf.bullet('Volumes montés pour le hot-reload en développement')
pdf.add_page()
pdf.section_title('4.3 Adéquation aux besoins du Ministère')
pdf.body_text(
'Le Ministère des Solidarités et de la Santé a des exigences spécifiques en matière '
'de sécurité, conformité RGPD et accessibilité. La solution retenue y répond de manière exhaustive :'
)
pdf.colored_table(
['Besoin du Ministère', 'Réponse technique', 'Couverture'],
[
['Sécurité des données de santé', 'JWT + HTTPS + Validation strict', '100%'],
['Conformité RGPD', 'Soft delete, consentement, droit effacem.', '100%'],
['Traçabilité des actions', 'Table audits (action, IP, valeurs)', '100%'],
['Suivi émotionnel', 'Tracker + 8 émotions + 27 sous-émotions', '100%'],
['Rapports statistiques', 'RapportService (sem/mois/trim/année)', '100%'],
['Ressources informatives', 'Feeds CMS avec publication ordonnée', '100%'],
['Gestion des rôles', '3 rôles (visiteur, membre, admin)', '100%'],
['Responsive & accessible', 'Tailwind responsive + dark mode', '100%'],
['Contacts d\'urgence', 'CRUD contacts urgence par utilisateur', '100%'],
['Déploiement simple', 'Docker Compose (1 commande)', '100%'],
],
[50, 85, 55]
)
pdf.ln(5)
pdf.set_font('Helvetica', 'B', 10)
pdf.set_text_color(0, 100, 0)
pdf.set_fill_color(230, 250, 230)
pdf.cell(0, 10, ' CONCLUSION : La solution Laravel 12 + Next.js 16 couvre 100% des besoins identifiés.', fill=True, new_x="LMARGIN", new_y="NEXT")
pdf.set_text_color(30, 30, 30)
# ============================
# 5. GUIDE D'INSTALLATION
# ============================
pdf.add_page()
pdf.chapter_title('5', 'Guide d\'Installation')
pdf.section_title('5.1 Prérequis')
pdf.body_text('Logiciels requis pour installer et exécuter CESIZen :')
pdf.colored_table(
['Logiciel', 'Version min.', 'Rôle', 'Vérification'],
[
['Docker Desktop', '24.x', 'Conteneurisation', 'docker --version'],
['Docker Compose', '2.x', 'Orchestration', 'docker compose version'],
['Git', '2.x', 'Gestion de version', 'git --version'],
['Node.js (optionnel)', '20.x', 'Dev frontend local', 'node --version'],
['PHP (optionnel)', '8.4', 'Dev backend local', 'php --version'],
['Composer (optionnel)', '2.x', 'Dépendances PHP', 'composer --version'],
],
[40, 30, 55, 65]
)
pdf.ln(5)
pdf.section_title('5.2 Installation avec Docker (recommandée)')
pdf.subsection_title('Étape 1 : Cloner le dépôt')
pdf.set_font('Courier', '', 9)
pdf.set_fill_color(245, 245, 245)
pdf.cell(0, 6, ' git clone <url-du-depot> cesizen', fill=True, new_x="LMARGIN", new_y="NEXT")
pdf.cell(0, 6, ' cd cesizen', fill=True, new_x="LMARGIN", new_y="NEXT")
pdf.ln(3)
pdf.set_font('Helvetica', '', 9)
pdf.subsection_title('Étape 2 : Lancer les conteneurs')
pdf.set_font('Courier', '', 9)
pdf.cell(0, 6, ' docker compose up --build', fill=True, new_x="LMARGIN", new_y="NEXT")
pdf.ln(3)
pdf.set_font('Helvetica', '', 9)
pdf.body_text(
'Cette commande unique démarre 3 services :\n'
' - PostgreSQL 16 (port 5432) avec health check\n'
' - Backend Laravel (port 8000) avec migration + seed automatiques\n'
' - Frontend Next.js (port 3000) en mode développement'
)
pdf.subsection_title('Étape 3 : Vérifier le déploiement')
pdf.colored_table(
['Service', 'URL', 'Test'],
[
['Frontend', 'http://localhost:3000', 'Page d\'accueil affichée'],
['API Backend', 'http://localhost:8000/api/v1/feeds', 'JSON feeds retourné'],
['PostgreSQL', 'localhost:5432', 'Connexion psql réussie'],
],
[40, 70, 80]
)
pdf.ln(3)
pdf.subsection_title('Étape 4 : Compte administrateur par défaut')
pdf.colored_table(
['Champ', 'Valeur'],
[
['Email', 'admin@cesizen.fr'],
['Mot de passe', 'Admin123!'],
['Rôle', 'Administrateur'],
],
[50, 140]
)
pdf.add_page()
pdf.section_title('5.3 Installation manuelle (développement)')
pdf.subsection_title('Backend (Laravel)')
pdf.set_font('Courier', '', 8)
pdf.set_fill_color(245, 245, 245)
cmds_backend = [
' cd backend',
' cp .env.example .env # Configurer les variables',
' composer install',
' php artisan key:generate',
' php artisan jwt:secret',
' php artisan migrate --seed',
' php artisan serve # http://localhost:8000',
]
for c in cmds_backend:
pdf.cell(0, 5.5, c, fill=True, new_x="LMARGIN", new_y="NEXT")
pdf.ln(3)
pdf.set_font('Helvetica', '', 9)
pdf.subsection_title('Frontend (Next.js)')
pdf.set_font('Courier', '', 8)
cmds_frontend = [
' cd frontend',
' cp .env.example .env.local # NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1',
' npm install',
' npm run dev # http://localhost:3000',
]
for c in cmds_frontend:
pdf.cell(0, 5.5, c, fill=True, new_x="LMARGIN", new_y="NEXT")
pdf.ln(3)
pdf.set_font('Helvetica', '', 9)
pdf.subsection_title('Base de données PostgreSQL')
pdf.set_font('Courier', '', 8)
cmds_db = [
' # Installer PostgreSQL 16 localement ou via Docker :',
' docker run -d --name cesizen-db \\',
' -e POSTGRES_DB=cesizen \\',
' -e POSTGRES_USER=cesizen \\',
' -e POSTGRES_PASSWORD=cesizen_secret \\',
' -p 5432:5432 postgres:16-alpine',
]
for c in cmds_db:
pdf.cell(0, 5.5, c, fill=True, new_x="LMARGIN", new_y="NEXT")
pdf.ln(5)
pdf.set_font('Helvetica', '', 9)
pdf.section_title('5.4 Configuration des variables d\'environnement')
pdf.colored_table(
['Variable', 'Service', 'Valeur par défaut', 'Description'],
[
['APP_ENV', 'Backend', 'local', 'Environnement applicatif'],
['APP_DEBUG', 'Backend', 'true', 'Mode debug Laravel'],
['APP_KEY', 'Backend', 'base64:...', 'Clé de chiffrement'],
['DB_CONNECTION', 'Backend', 'pgsql', 'Driver BDD'],
['DB_HOST', 'Backend', 'postgres', 'Hôte BDD (conteneur)'],
['DB_PORT', 'Backend', '5432', 'Port PostgreSQL'],
['DB_DATABASE', 'Backend', 'cesizen', 'Nom de la BDD'],
['DB_USERNAME', 'Backend', 'cesizen', 'Utilisateur BDD'],
['DB_PASSWORD', 'Backend', 'cesizen_secret', 'Mot de passe BDD'],
['JWT_SECRET', 'Backend', '(généré)', 'Secret JWT tokens'],
['FRONTEND_URL', 'Backend', 'http://localhost:3000', 'URL CORS frontend'],
['NEXT_PUBLIC_API_URL', 'Frontend', 'http://localhost:8000/api/v1', 'URL de l\'API'],
],
[45, 25, 55, 65]
)
# ============================
# 6. PROCÉDURES DE VALIDATION
# ============================
pdf.add_page()
pdf.chapter_title('6', 'Procédures de Validation')
pdf.section_title('6.1 Plan de validation')
pdf.body_text(
'Le plan de validation de CESIZen s\'articule en 4 phases progressives, '
'chacune avec des objectifs et critères de passage spécifiques.'
)
pdf.colored_table(
['Phase', 'Nom', 'Objectif', 'Responsable', 'Critère de passage'],
[
['1', 'Validation unitaire', 'Vérifier chaque endpoint API', 'Développeur', '100% endpoints OK'],
['2', 'Validation intégration', 'Flux complets front-to-back', 'Développeur', 'Scénarios complets'],
['3', 'Validation système', 'Docker, performances, sécurité', 'Tech Lead', 'Tests NF passés'],
['4', 'Recette utilisateur', 'Validation métier (UAT)', 'Product Owner', 'Critères acceptés'],
],
[15, 35, 50, 35, 55]
)
pdf.ln(5)
pdf.section_title('6.2 Procédure de validation détaillée')
pdf.subsection_title('Phase 1 : Validation unitaire des endpoints API')
pdf.body_text('Exécuter les requêtes suivantes et vérifier les codes de retour :')
pdf.colored_table(
['Méthode', 'Route', 'Corps', 'Code attendu'],
[
['POST', '/api/v1/auth/register', '{"nom","prenom","email","password"}', '201'],
['POST', '/api/v1/auth/login', '{"email","password"}', '200 + token'],
['GET', '/api/v1/auth/me', 'Header: Bearer token', '200 + user'],
['GET', '/api/v1/emotions', 'Header: Bearer token', '200 + array'],
['POST', '/api/v1/tracker/saisies', '{"emotion_id","intensite","note"}', '201'],
['GET', '/api/v1/tracker/saisies', 'Header: Bearer token', '200 paginé'],
['GET', '/api/v1/tracker/rapports?period=week', 'Header: Bearer token', '200 + stats'],
['GET', '/api/v1/feeds', '(aucun - public)', '200 + array'],
['GET', '/api/v1/feeds/{slug}', '(aucun - public)', '200 + feed'],
['GET', '/api/v1/profil', 'Header: Bearer token', '200 + profil'],
['PUT', '/api/v1/profil', '{"nom","prenom"}', '200'],
['PUT', '/api/v1/profil/password', '{"current_password","password"}', '200'],
],
[20, 65, 60, 45]
)
pdf.ln(3)
pdf.subsection_title('Phase 2 : Validation intégration (scénarios end-to-end)')
pdf.body_text('Scénarios fonctionnels complets à exécuter via l\'interface web :')
pdf.colored_table(
['N°', 'Scénario', 'Étapes', 'Validation'],
[
['S1', 'Parcours inscription', '1.Accueil > 2.Inscription > 3.Dashboard', 'Dashboard affiché'],
['S2', 'Saisie émotionnelle', '1.Login > 2.Journal > 3.Nouvelle saisie', 'Saisie dans la liste'],
['S3', 'Consulter rapports', '1.Login > 2.Rapports > 3.Changer période', 'Graphiques affichés'],
['S4', 'Lire ressources', '1.Accueil > 2.Informations > 3.Article', 'Contenu affiché'],
['S5', 'Modifier profil', '1.Login > 2.Profil > 3.Modifier > 4.Sauver', 'Nom mis à jour'],
['S6', 'Changer mot de passe', '1.Profil > 2.Ancien mdp > 3.Nouveau', 'Re-login OK'],
['S7', 'Admin: gérer users', '1.Admin > 2.Utilisateurs > 3.Modifier', 'User modifié'],
['S8', 'Admin: gérer feeds', '1.Admin > 2.Contenus > 3.Créer article', 'Feed visible public'],
['S9', 'Admin: gérer émotions', '1.Admin > 2.Émotions > 3.Ajouter', 'Émotion dispo tracker'],
['S10', 'Suppression RGPD', '1.Profil > 2.Supprimer compte', 'Compte soft-deleted'],
],
[12, 40, 75, 63]
)
pdf.add_page()
pdf.subsection_title('Phase 3 : Validation système')
pdf.colored_table(
['N°', 'Test système', 'Commande / Action', 'Critère de réussite'],
[
['SYS-1', 'Docker build', 'docker compose up --build', 'Build sans erreur'],
['SYS-2', 'Migrations auto', 'Vérifier logs backend', 'Tables créées'],
['SYS-3', 'Seed auto', 'Vérifier logs backend', 'Données initiales OK'],
['SYS-4', 'Health check DB', 'docker inspect cesizen-db', 'Status: healthy'],
['SYS-5', 'Perf. API', 'Test temps de réponse', '< 200ms (95e centile)'],
['SYS-6', 'Perf. Frontend', 'Lighthouse audit', 'Score > 80'],
['SYS-7', 'Sécurité CORS', 'Requête origin non autorisé', 'Bloqué par CORS'],
['SYS-8', 'Rate limiting', '6+ tentatives login rapides', 'HTTP 429'],
['SYS-9', 'JWT expiration', 'Token expiré', 'HTTP 401 + refresh'],
['SYS-10', 'Responsive', 'Test mobile viewport', 'Layout adapté'],
],
[14, 35, 75, 66]
)
pdf.ln(5)
pdf.subsection_title('Phase 4 : Recette utilisateur (UAT)')
pdf.body_text(
'La recette utilisateur est effectuée par le Product Owner et les utilisateurs finaux '
'selon les critères d\'acceptation suivants :'
)
pdf.ln(3)
pdf.section_title('6.3 Critères d\'acceptation finaux')
pdf.colored_table(
['N°', 'Critère d\'acceptation', 'Validation', 'Statut'],
[
['CA-01', 'Un visiteur peut consulter les articles sans compte', 'GET /feeds public', 'VALIDÉ'],
['CA-02', 'Un utilisateur peut s\'inscrire et se connecter', 'Flow auth complet', 'VALIDÉ'],
['CA-03', 'Un membre peut enregistrer une émotion avec intensité', 'POST saisie tracker', 'VALIDÉ'],
['CA-04', 'Un membre peut consulter son historique émotionnel', 'GET saisies + filtres', 'VALIDÉ'],
['CA-05', 'Un membre peut visualiser des rapports statistiques', 'GET rapports par période', 'VALIDÉ'],
['CA-06', 'Un membre peut modifier son profil et mot de passe', 'PUT profil + password', 'VALIDÉ'],
['CA-07', 'Un membre peut supprimer son compte (RGPD)', 'DELETE profil (soft)', 'VALIDÉ'],
['CA-08', 'Un admin peut gérer les utilisateurs (CRUD)', 'Panel admin users', 'VALIDÉ'],
['CA-09', 'Un admin peut gérer les contenus du feed', 'Panel admin feeds', 'VALIDÉ'],
['CA-10', 'Un admin peut gérer les émotions (hiérarchie)', 'Panel admin émotions', 'VALIDÉ'],
['CA-11', 'Les actions admin sont tracées (audit trail)', 'Table audits remplie', 'VALIDÉ'],
['CA-12', 'L\'application se déploie en une commande', 'docker compose up', 'VALIDÉ'],
['CA-13', 'L\'interface est responsive et accessible', 'Mobile + dark mode', 'VALIDÉ'],
['CA-14', 'Les données sensibles sont protégées', 'JWT + hash + CORS', 'VALIDÉ'],
],
[14, 70, 56, 50]
)
pdf.ln(5)
pdf.set_font('Helvetica', 'B', 10)
pdf.set_text_color(0, 100, 0)
pdf.set_fill_color(230, 250, 230)
pdf.cell(0, 10, ' RÉSULTAT : 14/14 critères d\'acceptation validés - Projet conforme aux exigences.', fill=True, new_x="LMARGIN", new_y="NEXT")
# ============================
# 6.4 Modèle de PV de recette
# ============================
pdf.add_page()
pdf.section_title('6.4 Modèle de procès-verbal de recette')
pdf.body_text(
'Le procès-verbal (PV) de recette formalise l\'acceptation de la solution par le commanditaire '
'à l\'issue de la recette utilisateur (UAT). Modèle type à compléter et signer par les deux parties :'
)
pdf.subsection_title('Identification')
pdf.colored_table(
['Rubrique', 'Renseignement'],
[
['Projet', 'CESIZen - Plateforme de santé mentale'],
['Version recettée', 'v1.0'],
['Date de recette', '...... / ...... / ............'],
['Maîtrise d\'ouvrage (commanditaire)', 'Ministère des Solidarités et de la Santé'],
['Maîtrise d\'oeuvre (réalisation)', 'Adam Marzuk - CESI École d\'Ingénieurs'],
['Périmètre recetté', 'Comptes utilisateurs, Informations, Tracker d\'émotions'],
],
[70, 120]
)
pdf.ln(2)
pdf.subsection_title('Synthèse des résultats')
pdf.colored_table(
['Indicateur', 'Valeur'],
[
['Cas de test exécutés', '............ / 54'],
['Cas conformes (OK)', '............'],
['Anomalies bloquantes', '............'],
['Anomalies mineures', '............'],
['Taux de conformité', '............ %'],
],
[95, 95]
)
pdf.ln(2)
pdf.subsection_title('Réserves éventuelles')
pdf.body_text('1. ................................................................................................................')
pdf.body_text('2. ................................................................................................................')
pdf.ln(1)
pdf.subsection_title('Décision de recette')
pdf.body_text(
'[ ] Recette prononcée sans réserve '
'[ ] Recette prononcée avec réserves '
'[ ] Recette refusée'
)
pdf.ln(4)
pdf.set_font('Helvetica', 'B', 9)
pdf.set_text_color(30, 30, 30)
pdf.cell(95, 7, 'Pour la maîtrise d\'ouvrage', border=0)
pdf.cell(95, 7, 'Pour la maîtrise d\'oeuvre', border=0, new_x="LMARGIN", new_y="NEXT")
pdf.set_font('Helvetica', '', 8)
pdf.cell(95, 6, 'Nom : .....................................', border=0)
pdf.cell(95, 6, 'Nom : .....................................', border=0, new_x="LMARGIN", new_y="NEXT")
pdf.cell(95, 6, 'Fonction : Product Owner', border=0)
pdf.cell(95, 6, 'Fonction : Développeur / Apprenant', border=0, new_x="LMARGIN", new_y="NEXT")
pdf.cell(95, 6, 'Date et signature :', border=0)
pdf.cell(95, 6, 'Date et signature :', border=0, new_x="LMARGIN", new_y="NEXT")
pdf.ln(2)
sig_y = pdf.get_y()
pdf.set_draw_color(120, 120, 120)
pdf.rect(pdf.l_margin, sig_y, 90, 26)
pdf.rect(pdf.l_margin + 100, sig_y, 90, 26)
pdf.ln(30)
# Sauvegarde
output_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'CESIZen_Cahier_Tests_Etude_Comparative.pdf')
pdf.output(output_path)
print(f"PDF généré avec succès : {output_path}")
return output_path
if __name__ == '__main__':
generate_pdf()