e8b49c6b94
- generate_diagrams.py : schémas PNG (MCD Merise, cas d'usage UML, séquences login/saisie, architecture MVC, 3 maquettes) générés avec Pillow - generate_cahier_charges.py : intègre les schémas et corrige les écarts code/dossier (28 sous-émotions, table audits, est_publie) - generate_slides.py : support de soutenance .pptx (16 slides) via python-pptx - Plan_Oral_CESIZen.md : déroulé minuté 20 min + préparation Q/R - assets_dossier/ : schémas sources - CLAUDE.md : 31 -> 28 sous-émotions (aligné sur le seed réel) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
435 lines
24 KiB
Python
435 lines
24 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Génère le support de soutenance CESIZen (PowerPoint 16:9 éditable).
|
||
|
||
Réutilise les schémas PNG produits par generate_diagrams.py.
|
||
Sortie : ./CESIZen_Soutenance.pptx
|
||
"""
|
||
import os
|
||
from PIL import Image
|
||
from pptx import Presentation
|
||
from pptx.util import Inches, Pt, Emu
|
||
from pptx.dml.color import RGBColor
|
||
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
|
||
from pptx.enum.shapes import MSO_SHAPE
|
||
|
||
# ─── Charte CESIZen ────────────────────────────────────────────────
|
||
GREEN = RGBColor(6, 198, 86)
|
||
GREEN_D = RGBColor(4, 150, 65)
|
||
BLUE = RGBColor(0, 70, 130)
|
||
BLUE_L = RGBColor(230, 240, 250)
|
||
YELLOW = RGBColor(252, 225, 23)
|
||
GREY_D = RGBColor(50, 50, 50)
|
||
GREY = RGBColor(110, 110, 110)
|
||
WHITE = RGBColor(255, 255, 255)
|
||
INK = RGBColor(35, 35, 45)
|
||
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
ASSETS = os.path.join(HERE, 'assets_dossier')
|
||
OUT = os.path.join(HERE, 'CESIZen_Soutenance.pptx')
|
||
|
||
prs = Presentation()
|
||
prs.slide_width = Inches(13.333)
|
||
prs.slide_height = Inches(7.5)
|
||
SW, SH = prs.slide_width, prs.slide_height
|
||
BLANK = prs.slide_layouts[6]
|
||
|
||
|
||
def _no_line(shape):
|
||
shape.line.fill.background()
|
||
|
||
|
||
def rect(slide, x, y, w, h, color, line=None):
|
||
s = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, x, y, w, h)
|
||
s.fill.solid()
|
||
s.fill.fore_color.rgb = color
|
||
if line is None:
|
||
_no_line(s)
|
||
else:
|
||
s.line.color.rgb = line
|
||
s.line.width = Pt(1)
|
||
s.shadow.inherit = False
|
||
return s
|
||
|
||
|
||
def textbox(slide, x, y, w, h, anchor=MSO_ANCHOR.TOP):
|
||
tb = slide.shapes.add_textbox(x, y, w, h)
|
||
tf = tb.text_frame
|
||
tf.word_wrap = True
|
||
tf.vertical_anchor = anchor
|
||
tf.margin_left = Inches(0.05)
|
||
tf.margin_right = Inches(0.05)
|
||
tf.margin_top = Inches(0.02)
|
||
tf.margin_bottom = Inches(0.02)
|
||
return tb, tf
|
||
|
||
|
||
def set_par(p, text, size, color, bold=False, align=PP_ALIGN.LEFT,
|
||
font='Arial', space_after=8):
|
||
p.text = text
|
||
p.alignment = align
|
||
p.space_after = Pt(space_after)
|
||
for r in p.runs:
|
||
r.font.size = Pt(size)
|
||
r.font.color.rgb = color
|
||
r.font.bold = bold
|
||
r.font.name = font
|
||
|
||
|
||
def footer(slide):
|
||
tb, tf = textbox(slide, Inches(0.4), Inches(7.05), Inches(12.5), Inches(0.35))
|
||
set_par(tf.paragraphs[0],
|
||
"CESIZen · Bloc INFCDAAL1 — Concevoir les solutions logicielles · Adam Marzuk",
|
||
9, GREY, align=PP_ALIGN.CENTER, space_after=0)
|
||
|
||
|
||
def band_title(slide, text, num=None):
|
||
rect(slide, 0, 0, SW, Inches(1.05), BLUE)
|
||
rect(slide, 0, Inches(1.05), SW, Inches(0.06), GREEN)
|
||
label = f"{num}. {text}" if num else text
|
||
tb, tf = textbox(slide, Inches(0.5), Inches(0.12), Inches(12.3), Inches(0.85),
|
||
anchor=MSO_ANCHOR.MIDDLE)
|
||
set_par(tf.paragraphs[0], label, 30, WHITE, bold=True, space_after=0)
|
||
|
||
|
||
def bullets(slide, items, x, y, w, h, size=18, gap=10):
|
||
tb, tf = textbox(slide, x, y, w, h)
|
||
for i, it in enumerate(items):
|
||
lvl = 0
|
||
txt = it
|
||
if isinstance(it, tuple):
|
||
txt, lvl = it
|
||
p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
|
||
prefix = "• " if lvl == 0 else "– "
|
||
set_par(p, prefix + txt, size if lvl == 0 else size - 2,
|
||
INK if lvl == 0 else GREY_D, bold=False, space_after=gap)
|
||
p.level = lvl
|
||
if lvl == 1:
|
||
p.runs[0].font.color.rgb = GREY
|
||
return tb
|
||
|
||
|
||
def image_fit(slide, name, x, y, max_w, max_h, caption=None):
|
||
path = os.path.join(ASSETS, name)
|
||
iw, ih = Image.open(path).size
|
||
ar = iw / ih
|
||
box_ar = max_w / max_h
|
||
if box_ar > ar:
|
||
h = max_h
|
||
w = int(h * ar)
|
||
else:
|
||
w = max_w
|
||
h = int(w / ar)
|
||
left = x + (max_w - w) // 2
|
||
top = y + (max_h - h) // 2
|
||
slide.shapes.add_picture(path, left, top, width=w, height=h)
|
||
if caption:
|
||
tb, tf = textbox(slide, x, y + max_h + Inches(0.02), max_w, Inches(0.35))
|
||
set_par(tf.paragraphs[0], caption, 11, GREY, align=PP_ALIGN.CENTER, space_after=0)
|
||
|
||
|
||
# ════════════════════════════════════════════════════════════════════
|
||
# Slide 1 — Titre
|
||
# ════════════════════════════════════════════════════════════════════
|
||
s = prs.slides.add_slide(BLANK)
|
||
rect(s, 0, 0, SW, SH, WHITE)
|
||
rect(s, 0, Inches(2.5), SW, Inches(0.08), GREEN)
|
||
tb, tf = textbox(s, Inches(1), Inches(0.9), Inches(11.3), Inches(1.5))
|
||
set_par(tf.paragraphs[0], "CESIZen", 60, GREEN, bold=True, align=PP_ALIGN.CENTER, space_after=0)
|
||
tb, tf = textbox(s, Inches(1), Inches(2.65), Inches(11.3), Inches(0.9))
|
||
set_par(tf.paragraphs[0], "L'application de votre santé mentale", 26, GREY_D,
|
||
align=PP_ALIGN.CENTER, space_after=0)
|
||
tb, tf = textbox(s, Inches(1), Inches(3.7), Inches(11.3), Inches(0.7))
|
||
set_par(tf.paragraphs[0], "Cahier des charges & conception logicielle", 18, BLUE,
|
||
align=PP_ALIGN.CENTER, space_after=0)
|
||
tb, tf = textbox(s, Inches(1), Inches(5.4), Inches(11.3), Inches(1.4))
|
||
for i, line in enumerate([
|
||
"Adam Marzuk — CESI École d'Ingénieurs",
|
||
"Bloc INFCDAAL1 — Concevoir les solutions logicielles",
|
||
"Commanditaire : Ministère des Solidarités et de la Santé"]):
|
||
p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
|
||
set_par(p, line, 15, GREY, align=PP_ALIGN.CENTER, space_after=4)
|
||
|
||
# ════════════════════════════════════════════════════════════════════
|
||
# Slide 2 — Sommaire
|
||
# ════════════════════════════════════════════════════════════════════
|
||
s = prs.slides.add_slide(BLANK)
|
||
band_title(s, "Sommaire")
|
||
bullets(s, [
|
||
"Contexte, objectifs et enjeux",
|
||
"Parties prenantes et acteurs",
|
||
"Besoins reformulés et priorisation",
|
||
"Réponse fonctionnelle (Comptes · Informations · Tracker)",
|
||
"Modélisation : cas d'usage UML, MCD, architecture MVC",
|
||
"Données personnelles et sensibles (RGPD)",
|
||
"Prototype et perspectives",
|
||
], Inches(1.2), Inches(1.5), Inches(11), Inches(5.2), size=22, gap=14)
|
||
footer(s)
|
||
|
||
# ════════════════════════════════════════════════════════════════════
|
||
# Slide 3 — Contexte & enjeux
|
||
# ════════════════════════════════════════════════════════════════════
|
||
s = prs.slides.add_slide(BLANK)
|
||
band_title(s, "Contexte, objectifs et enjeux", 1)
|
||
bullets(s, [
|
||
"Commanditaire : Ministère des Solidarités et de la Santé.",
|
||
"Enjeu : une plateforme grand public pour prévenir le stress et accompagner la santé mentale.",
|
||
"Constat : 22 % des Français touchés par un trouble psychique au cours de leur vie (Santé Publique France, 2023).",
|
||
"Frein : stigmatisation et méconnaissance limitent l'accès au soin.",
|
||
], Inches(0.8), Inches(1.4), Inches(11.7), Inches(2.6), size=18)
|
||
# encadré objectifs
|
||
rect(s, Inches(0.8), Inches(4.2), Inches(11.7), Inches(2.4), BLUE_L)
|
||
rect(s, Inches(0.8), Inches(4.2), Inches(3.4), Inches(0.55), BLUE)
|
||
tb, tf = textbox(s, Inches(0.95), Inches(4.25), Inches(3.2), Inches(0.5), MSO_ANCHOR.MIDDLE)
|
||
set_par(tf.paragraphs[0], "4 objectifs stratégiques", 15, WHITE, bold=True, space_after=0)
|
||
bullets(s, [
|
||
"Informer : contenu éducatif fiable sur la santé mentale.",
|
||
"Suivre : permettre à chacun de tracer son humeur dans le temps.",
|
||
"Orienter : diriger les personnes en détresse (numéros d'urgence).",
|
||
"Protéger : confidentialité absolue des données émotionnelles (RGPD + HDS).",
|
||
], Inches(1.0), Inches(4.9), Inches(11.3), Inches(1.6), size=15, gap=4)
|
||
footer(s)
|
||
|
||
# ════════════════════════════════════════════════════════════════════
|
||
# Slide 4 — Parties prenantes & acteurs
|
||
# ════════════════════════════════════════════════════════════════════
|
||
s = prs.slides.add_slide(BLANK)
|
||
band_title(s, "Parties prenantes et acteurs", 2)
|
||
tb, tf = textbox(s, Inches(0.8), Inches(1.35), Inches(11.7), Inches(0.5))
|
||
set_par(tf.paragraphs[0], "Parties prenantes du projet", 16, GREEN_D, bold=True, space_after=0)
|
||
bullets(s, [
|
||
"Commanditaire : Ministère (pilotage, exigences santé publique et RGPD).",
|
||
"Bénéficiaires : grand public, personnes en quête de mieux-être psychique.",
|
||
"Exploitants : administrateurs et professionnels de santé (modération, contenus).",
|
||
], Inches(0.8), Inches(1.85), Inches(11.7), Inches(2.0), size=17)
|
||
tb, tf = textbox(s, Inches(0.8), Inches(4.0), Inches(11.7), Inches(0.5))
|
||
set_par(tf.paragraphs[0], "3 acteurs du système", 16, GREEN_D, bold=True, space_after=0)
|
||
cards = [("Visiteur anonyme", "Consulte les informations, crée un compte.", GREEN),
|
||
("Utilisateur connecté", "Journal d'émotions, rapports, profil, RGPD.", BLUE),
|
||
("Administrateur", "Gère utilisateurs, contenus et émotions.", GREEN_D)]
|
||
for i, (t, d, col) in enumerate(cards):
|
||
x = Inches(0.8 + i * 4.0)
|
||
rect(s, x, Inches(4.55), Inches(3.7), Inches(1.8), BLUE_L)
|
||
rect(s, x, Inches(4.55), Inches(3.7), Inches(0.55), col)
|
||
tb, tf = textbox(s, x + Inches(0.1), Inches(4.58), Inches(3.5), Inches(0.5), MSO_ANCHOR.MIDDLE)
|
||
set_par(tf.paragraphs[0], t, 14, WHITE, bold=True, space_after=0)
|
||
tb, tf = textbox(s, x + Inches(0.15), Inches(5.2), Inches(3.4), Inches(1.0))
|
||
set_par(tf.paragraphs[0], d, 13, INK, space_after=0)
|
||
footer(s)
|
||
|
||
# ════════════════════════════════════════════════════════════════════
|
||
# Slide 5 — Besoins & périmètre
|
||
# ════════════════════════════════════════════════════════════════════
|
||
s = prs.slides.add_slide(BLANK)
|
||
band_title(s, "Besoins reformulés et périmètre", 3)
|
||
cols = [
|
||
("2 modules OBLIGATOIRES", GREEN, [
|
||
"Comptes utilisateurs : inscription JWT, profil, reset, admin, RGPD.",
|
||
"Informations : pages de contenu santé mentale, CRUD admin.",
|
||
]),
|
||
("1 module AU CHOIX (retenu)", BLUE, [
|
||
"Tracker d'émotions : journal, saisies, rapports périodiques.",
|
||
"Hiérarchie : 7 émotions de base + 28 sous-émotions.",
|
||
]),
|
||
("Modules ÉCARTÉS", GREY, [
|
||
"Diagnostic de stress.",
|
||
"Cohérence cardiaque.",
|
||
"Activités de détente.",
|
||
]),
|
||
]
|
||
for i, (t, col, items) in enumerate(cols):
|
||
x = Inches(0.7 + i * 4.05)
|
||
rect(s, x, Inches(1.5), Inches(3.85), Inches(0.6), col)
|
||
tb, tf = textbox(s, x + Inches(0.1), Inches(1.53), Inches(3.65), Inches(0.55), MSO_ANCHOR.MIDDLE)
|
||
set_par(tf.paragraphs[0], t, 13, WHITE, bold=True, space_after=0)
|
||
bullets(s, items, x + Inches(0.05), Inches(2.3), Inches(3.85), Inches(3.5), size=13, gap=8)
|
||
tb, tf = textbox(s, Inches(0.7), Inches(6.0), Inches(11.9), Inches(0.8))
|
||
set_par(tf.paragraphs[0],
|
||
"Périmètre maîtrisé : 3 modules livrés en qualité plutôt que 6 modules superficiels.",
|
||
15, GREEN_D, bold=True, align=PP_ALIGN.CENTER, space_after=0)
|
||
footer(s)
|
||
|
||
# ════════════════════════════════════════════════════════════════════
|
||
# Slide 6 — Priorisation
|
||
# ════════════════════════════════════════════════════════════════════
|
||
s = prs.slides.add_slide(BLANK)
|
||
band_title(s, "Critères de priorisation et pondération", 3)
|
||
bullets(s, [
|
||
"5 critères pondérés, total sur 15 : Complexité, Valeur Ministère, Valeur Utilisateur, Nécessité, Interdépendance.",
|
||
], Inches(0.8), Inches(1.35), Inches(11.7), Inches(0.8), size=16)
|
||
# mini-tableau top priorités
|
||
rows = [
|
||
("Fonctionnalité", "Score /15", True),
|
||
("Journal d'émotions · Rapports · Diagnostic de stress", "10", False),
|
||
("Cryptage · Inscription · Cohérence cardiaque · Ajout tracker", "9", False),
|
||
("Conformité RGPD", "8", False),
|
||
("Réinitialisation mot de passe", "7", False),
|
||
]
|
||
ty = 2.3
|
||
for i, (a, b, head) in enumerate(rows):
|
||
col = BLUE if head else (BLUE_L if i % 2 else WHITE)
|
||
rect(s, Inches(0.8), Inches(ty + i * 0.62), Inches(9.4), Inches(0.62), col,
|
||
line=RGBColor(210, 220, 230))
|
||
rect(s, Inches(10.2), Inches(ty + i * 0.62), Inches(2.3), Inches(0.62), col,
|
||
line=RGBColor(210, 220, 230))
|
||
tb, tf = textbox(s, Inches(0.95), Inches(ty + i * 0.62), Inches(9.1), Inches(0.62), MSO_ANCHOR.MIDDLE)
|
||
set_par(tf.paragraphs[0], a, 13, WHITE if head else INK, bold=head, space_after=0)
|
||
tb, tf = textbox(s, Inches(10.2), Inches(ty + i * 0.62), Inches(2.3), Inches(0.62), MSO_ANCHOR.MIDDLE)
|
||
set_par(tf.paragraphs[0], b, 13, WHITE if head else GREEN_D, bold=True,
|
||
align=PP_ALIGN.CENTER, space_after=0)
|
||
tb, tf = textbox(s, Inches(0.8), Inches(6.05), Inches(11.9), Inches(0.8))
|
||
set_par(tf.paragraphs[0],
|
||
"→ Le Tracker cumule 4 fonctionnalités notées 9-10/15 : module au choix le plus rentable.",
|
||
15, GREEN_D, bold=True, space_after=0)
|
||
footer(s)
|
||
|
||
# ════════════════════════════════════════════════════════════════════
|
||
# Slide 7 — Réponse fonctionnelle Comptes
|
||
# ════════════════════════════════════════════════════════════════════
|
||
s = prs.slides.add_slide(BLANK)
|
||
band_title(s, "Réponse fonctionnelle — Comptes utilisateurs", 4)
|
||
bullets(s, [
|
||
"Inscription et connexion sécurisées par JWT (tymon/jwt-auth).",
|
||
"Consentement RGPD obligatoire, non pré-coché, à l'inscription.",
|
||
"Gestion du profil : édition, changement de mot de passe.",
|
||
"Espace admin : CRUD utilisateurs, activation/désactivation, rôles.",
|
||
"RGPD : export des données (JSON), anonymisation, soft delete + purge 30 j.",
|
||
"Mot de passe : bcrypt 12 rounds, min. 8 caractères.",
|
||
], Inches(0.8), Inches(1.5), Inches(8.0), Inches(5.2), size=17, gap=12)
|
||
image_fit(s, "mockup_inscription.png", Inches(9.1), Inches(1.4), Inches(3.7), Inches(5.2),
|
||
caption="Écran d'inscription (mobile)")
|
||
footer(s)
|
||
|
||
# ════════════════════════════════════════════════════════════════════
|
||
# Slide 8 — Réponse fonctionnelle Informations
|
||
# ════════════════════════════════════════════════════════════════════
|
||
s = prs.slides.add_slide(BLANK)
|
||
band_title(s, "Réponse fonctionnelle — Informations", 4)
|
||
bullets(s, [
|
||
"Pages de contenu santé mentale (feeds) accessibles au grand public.",
|
||
"Page liste (cartes : titre, extrait, image, date) et page détail (HTML rendu).",
|
||
"Back-office admin : création / édition / publication / suppression.",
|
||
"Slug unique généré automatiquement à partir du titre.",
|
||
"Champ booléen est_publie : brouillon (invisible) ou publié (visible).",
|
||
"Modération obligatoire : un administrateur valide avant publication.",
|
||
"Tri par ordre numérique pour mettre en avant un contenu.",
|
||
], Inches(0.8), Inches(1.5), Inches(11.7), Inches(5.0), size=18, gap=12)
|
||
footer(s)
|
||
|
||
# ════════════════════════════════════════════════════════════════════
|
||
# Slide 9 — Réponse fonctionnelle Tracker
|
||
# ════════════════════════════════════════════════════════════════════
|
||
s = prs.slides.add_slide(BLANK)
|
||
band_title(s, "Réponse fonctionnelle — Tracker d'émotions", 4)
|
||
bullets(s, [
|
||
"Wizard d'ajout en 3 étapes : émotion de base → sous-émotion + intensité → date + note.",
|
||
"Journal de bord : consultation, modification, suppression des saisies.",
|
||
"Rapports analytiques sur 4 périodes : semaine, mois, trimestre, année.",
|
||
"Statistiques : intensité moyenne, émotion dominante, répartition, évolution.",
|
||
"Hiérarchie configurable par l'admin : 7 émotions + 28 sous-émotions.",
|
||
"Web (graphiques Recharts) et mobile (Expo) synchronisés via l'API.",
|
||
], Inches(0.7), Inches(1.45), Inches(7.0), Inches(5.3), size=16, gap=10)
|
||
image_fit(s, "mockup_wizard.png", Inches(7.9), Inches(1.35), Inches(2.5), Inches(5.3))
|
||
image_fit(s, "mockup_rapports.png", Inches(10.5), Inches(2.4), Inches(2.7), Inches(3.2))
|
||
footer(s)
|
||
|
||
# ════════════════════════════════════════════════════════════════════
|
||
# Slide 10 — Cas d'usage UML
|
||
# ════════════════════════════════════════════════════════════════════
|
||
s = prs.slides.add_slide(BLANK)
|
||
band_title(s, "Modélisation — Cas d'usage UML", 5)
|
||
image_fit(s, "usecase.png", Inches(0.6), Inches(1.3), Inches(12.1), Inches(5.4),
|
||
caption="3 acteurs · cas d'usage groupés par module · modules écartés non représentés")
|
||
footer(s)
|
||
|
||
# ════════════════════════════════════════════════════════════════════
|
||
# Slide 11 — MCD
|
||
# ════════════════════════════════════════════════════════════════════
|
||
s = prs.slides.add_slide(BLANK)
|
||
band_title(s, "Modélisation — MCD (Merise)", 5)
|
||
image_fit(s, "mcd.png", Inches(0.6), Inches(1.3), Inches(12.1), Inches(5.4),
|
||
caption="8 entités · cardinalités Merise · hiérarchie d'émotions (auto-référence)")
|
||
footer(s)
|
||
|
||
# ════════════════════════════════════════════════════════════════════
|
||
# Slide 12 — Architecture MVC
|
||
# ════════════════════════════════════════════════════════════════════
|
||
s = prs.slides.add_slide(BLANK)
|
||
band_title(s, "Modélisation — Architecture MVC", 5)
|
||
image_fit(s, "mvc.png", Inches(0.6), Inches(1.25), Inches(8.6), Inches(5.5))
|
||
bullets(s, [
|
||
"API REST Laravel 11 = Contrôleur central.",
|
||
"Eloquent ORM + PostgreSQL 16 = Modèle.",
|
||
"Next.js (web) + React Native (mobile) = Vues.",
|
||
"Une seule logique métier, deux clients.",
|
||
"Sécurité centralisée : FormRequest, middleware, JWT.",
|
||
"Stack open-source, conteneurisée (Docker).",
|
||
], Inches(9.3), Inches(1.5), Inches(3.5), Inches(5.0), size=14, gap=12)
|
||
footer(s)
|
||
|
||
# ════════════════════════════════════════════════════════════════════
|
||
# Slide 13 — RGPD & données sensibles
|
||
# ════════════════════════════════════════════════════════════════════
|
||
s = prs.slides.add_slide(BLANK)
|
||
band_title(s, "Données personnelles et sensibles (RGPD)", 6)
|
||
bullets(s, [
|
||
"Données de santé mentale = données sensibles (RGPD art. 9).",
|
||
"Base légale : consentement explicite ; finalités documentées.",
|
||
"Droits CNIL implémentés : accès, rectification, effacement, portabilité (export JSON).",
|
||
"Anonymisation et soft delete + purge à 30 jours.",
|
||
], Inches(0.8), Inches(1.45), Inches(11.7), Inches(2.4), size=17, gap=10)
|
||
rect(s, Inches(0.8), Inches(4.1), Inches(11.7), Inches(2.5), BLUE_L)
|
||
rect(s, Inches(0.8), Inches(4.1), Inches(3.6), Inches(0.55), GREEN_D)
|
||
tb, tf = textbox(s, Inches(0.95), Inches(4.13), Inches(3.4), Inches(0.5), MSO_ANCHOR.MIDDLE)
|
||
set_par(tf.paragraphs[0], "Mesures de sécurité", 15, WHITE, bold=True, space_after=0)
|
||
bullets(s, [
|
||
"Chiffrement en transit (HTTPS/TLS 1.3) et au repos (AES-256).",
|
||
"Authentification JWT signée, expiration 60 min, throttle login.",
|
||
"Hébergement certifié HDS (Scaleway / OVH Healthcare).",
|
||
"Journalisation : table audits trace toute action admin sensible.",
|
||
], Inches(1.0), Inches(4.8), Inches(11.3), Inches(1.7), size=14, gap=6)
|
||
footer(s)
|
||
|
||
# ════════════════════════════════════════════════════════════════════
|
||
# Slide 14 — Prototype & perspectives
|
||
# ════════════════════════════════════════════════════════════════════
|
||
s = prs.slides.add_slide(BLANK)
|
||
band_title(s, "Prototype et perspectives", 7)
|
||
tb, tf = textbox(s, Inches(0.8), Inches(1.4), Inches(11.7), Inches(0.5))
|
||
set_par(tf.paragraphs[0], "Un prototype fonctionnel accompagne le dossier", 17, GREEN_D,
|
||
bold=True, space_after=0)
|
||
bullets(s, [
|
||
"Backend Laravel 11 (API REST JWT) + PostgreSQL 16.",
|
||
"Frontend web Next.js 16 et application mobile Expo (iOS / Android).",
|
||
"Conteneurisation Docker Compose, dépôt Git.",
|
||
], Inches(0.8), Inches(1.9), Inches(11.7), Inches(1.9), size=16)
|
||
tb, tf = textbox(s, Inches(0.8), Inches(4.0), Inches(11.7), Inches(0.5))
|
||
set_par(tf.paragraphs[0], "Perspectives d'évolution", 17, GREEN_D, bold=True, space_after=0)
|
||
bullets(s, [
|
||
"Activer les modules écartés (Diagnostic, Cohérence cardiaque, Activités) sans réécrire le modèle.",
|
||
"Open Data : base anonymisée pour études épidémiologiques.",
|
||
"Interopérabilité : Mon Espace Santé (FHIR) ; internationalisation (i18n).",
|
||
], Inches(0.8), Inches(4.5), Inches(11.7), Inches(1.7), size=16)
|
||
rect(s, 0, Inches(6.6), SW, Inches(0.9), BLUE)
|
||
tb, tf = textbox(s, Inches(0.8), Inches(6.62), Inches(11.7), Inches(0.85), MSO_ANCHOR.MIDDLE)
|
||
set_par(tf.paragraphs[0], "Merci de votre attention — Questions / Réponses", 20, WHITE,
|
||
bold=True, align=PP_ALIGN.CENTER, space_after=0)
|
||
|
||
# ════════════════════════════════════════════════════════════════════
|
||
# Annexes (backup Q/R) — séquences
|
||
# ════════════════════════════════════════════════════════════════════
|
||
for name, title, cap in [
|
||
("seq_login.png", "Annexe — Séquence d'authentification JWT",
|
||
"Validation · vérification bcrypt · émission du token JWT"),
|
||
("seq_saisie.png", "Annexe — Séquence d'ajout d'une saisie",
|
||
"Wizard 3 étapes · validation · persistance"),
|
||
]:
|
||
s = prs.slides.add_slide(BLANK)
|
||
band_title(s, title)
|
||
image_fit(s, name, Inches(0.6), Inches(1.3), Inches(12.1), Inches(5.4), caption=cap)
|
||
footer(s)
|
||
|
||
prs.save(OUT)
|
||
print(f"Support de soutenance généré : {OUT}")
|
||
print(f"Nombre de slides : {len(prs.slides._sldIdLst)}")
|