Files
cesizen-individuel/generate_slides_bloc3.py
T

418 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Génère le support de soutenance CESIZen — Activité 3 « Déployer et sécuriser »
(PowerPoint 16:9 éditable). Réutilise la charte du support du Bloc 1.
Sortie : ./CESIZen_Soutenance_Bloc3.pptx
Schémas : ./assets_bloc3/*.png
"""
import os
from PIL import Image
from pptx import Presentation
from pptx.util import Inches, Pt
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)
ORANGE = RGBColor(228, 128, 28)
RED = RGBColor(200, 16, 46)
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_bloc3')
OUT = os.path.join(HERE, 'CESIZen_Soutenance_Bloc3.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 INFCDAAL3 — Déployer et sécuriser les applications informatiques · 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, 28, 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)
def cards(slide, items, y, h=1.8, top=0.55):
"""items = list of (titre, description, couleur)"""
n = len(items)
total_w = 12.0
gap = 0.25
cw = (total_w - gap * (n - 1)) / n
for i, (t, d, col) in enumerate(items):
x = Inches(0.7 + i * (cw + gap))
rect(slide, x, Inches(y), Inches(cw), Inches(h), BLUE_L)
rect(slide, x, Inches(y), Inches(cw), Inches(top), col)
tb, tf = textbox(slide, x + Inches(0.1), Inches(y + 0.03), Inches(cw - 0.2),
Inches(top), MSO_ANCHOR.MIDDLE)
set_par(tf.paragraphs[0], t, 13, WHITE, bold=True, space_after=0, align=PP_ALIGN.CENTER)
tb, tf = textbox(slide, x + Inches(0.15), Inches(y + top + 0.1), Inches(cw - 0.3),
Inches(h - top - 0.15))
for j, line in enumerate(d):
p = tf.paragraphs[0] if j == 0 else tf.add_paragraph()
set_par(p, "• " + line, 11.5, INK, space_after=5)
# ════════════════════════════════════════════════════════════════════
# 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.62), Inches(11.3), Inches(0.9))
set_par(tf.paragraphs[0], "L'application de votre santé mentale", 24, GREY_D,
align=PP_ALIGN.CENTER, space_after=0)
tb, tf = textbox(s, Inches(1), Inches(3.65), Inches(11.3), Inches(0.9))
set_par(tf.paragraphs[0], "Activité 3 — Déployer et sécuriser les applications informatiques",
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 INFCDAAL3 — Déployer et sécuriser les applications informatiques",
"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 et périmètre de l'Activité 3",
"Plan de déploiement : architecture et environnements",
"Versioning et intégration/livraison continues (Gitea · Coolify)",
"Plan de maintenance : ticketing, engagements de service, veille",
"Plan de sécurisation : vulnérabilités, risques, actions",
"Données personnelles, RGPD et gestion de crise",
"Démonstration des outils et conclusion",
], Inches(1.2), Inches(1.5), Inches(11), Inches(5.2), size=22, gap=13)
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 3 — Contexte & périmètre
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Contexte et périmètre de l'Activité 3", 1)
bullets(s, [
"CESIZen : plateforme grand public de santé mentale (Ministère).",
"Après la conception (Act. 1) et le développement/test (Act. 2), l'Activité 3 traite du déploiement et de la sécurisation.",
"Prototype déployé : API Laravel + web Next.js + mobile Expo + PostgreSQL.",
], Inches(0.8), Inches(1.4), Inches(11.7), Inches(2.0), size=18)
tb, tf = textbox(s, Inches(0.8), Inches(3.7), Inches(11.7), Inches(0.5))
set_par(tf.paragraphs[0], "3 livrables", 16, GREEN_D, bold=True, space_after=0)
cards(s, [
("Plan de déploiement", ["Environnements", "Versioning", "Automatisations"], GREEN),
("Plan de maintenance", ["Ticketing Gitea", "SLA & méthodologie", "Veille techno"], BLUE),
("Plan de sécurisation", ["Risques & criticité", "Actions correctives", "RGPD & crise"], GREEN_D),
], y=4.3, h=2.0)
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 4 — Architecture technique
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Architecture technique de la solution", 2)
bullets(s, [
"Architecture découplée : une API REST centrale, deux clients (web + mobile).",
("Back-end : Laravel 12 (PHP 8.4) — API REST JSON, authentification JWT.", 1),
("Base de données : PostgreSQL 16 (ACID) ; Redis en développement.", 1),
("Front-end web : Next.js 16 / React 19, design system de l'État (DSFR).", 1),
("Mobile : React Native / Expo 54 (iOS + Android, Mobile First).", 1),
"Conteneurisation Docker : environnements reproductibles et isolés.",
"Une seule logique métier → non-duplication, testabilité, évolutivité.",
], Inches(0.8), Inches(1.5), Inches(11.7), Inches(5.0), size=17, gap=9)
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 5 — Plan de déploiement : 3 environnements + chaîne
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Plan de déploiement — environnements et chaîne CI/CD", 2)
image_fit(s, "archi_deploiement.png", Inches(0.5), Inches(1.25), Inches(12.3), Inches(5.5),
caption="Développement (local) → Test/Intégration (Gitea Actions) → Production (Coolify, HTTPS)")
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 6 — Versioning & CI/CD
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Versioning et intégration/livraison continues", 3)
bullets(s, [
"Gitea : dépôt Git, ticketing et CI/CD réunis dans un outil souverain auto-hébergé.",
"Stratégie de branches : main (prod, protégée) · develop · feature/fix/hotfix.",
"Conventions : Conventional Commits, revue par pull request, tags de version.",
], Inches(0.8), Inches(1.4), Inches(11.7), Inches(1.9), size=17)
tb, tf = textbox(s, Inches(0.8), Inches(3.55), Inches(11.7), Inches(0.5))
set_par(tf.paragraphs[0], "Deux automatisations complémentaires", 16, GREEN_D, bold=True, space_after=0)
cards(s, [
("Intégration continue — Gitea Actions", [
"29 tests à chaque push",
"Lint (Pint, ESLint)",
"Merge bloqué si CI rouge"], ORANGE),
("Déploiement continu — Coolify", [
"Webhook sur merge main",
"Build images + migrations",
"HTTPS, health checks, restart"], BLUE),
], y=4.15, h=2.1)
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 7 — Maintenance : ticketing
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Plan de maintenance — ticketing et cycle de vie", 4)
image_fit(s, "cycle_ticket.png", Inches(0.5), Inches(1.25), Inches(12.3), Inches(5.5),
caption="Gitea Issues : gabarits incident/évolution, labels, jalons, Kanban · SLA du contrat")
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 8 — Maintenance : méthodo + veille
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Maintenance corrective, évolutive et veille", 4)
tb, tf = textbox(s, Inches(0.8), Inches(1.3), Inches(11.7), Inches(0.5))
set_par(tf.paragraphs[0], "Méthodologie", 16, GREEN_D, bold=True, space_after=0)
bullets(s, [
"Corrective : ticket → qualification (gravité) → SLA → correction → revue (PR + CI) → clôture validée.",
"Évolutive : analyse (documentation, délai, coût) → validation client → planification par version.",
"Pilotage : tableaux de bord Gitea (Kanban, jalons, indicateurs de délais).",
], Inches(0.8), Inches(1.8), Inches(11.7), Inches(2.3), size=16, gap=9)
tb, tf = textbox(s, Inches(0.8), Inches(4.2), Inches(11.7), Inches(0.5))
set_par(tf.paragraphs[0], "Veille technologique (pérennité)", 16, GREEN_D, bold=True, space_after=0)
bullets(s, [
"Sécurité : CVE, CERT-FR, OWASP, alertes de dépendances (composer/npm audit en CI).",
"Frameworks & dépendances : notes de version, robot de mises à jour testées par la CI.",
"Réglementation : CNIL, ANSSI (RGPD, HDS).",
], Inches(0.8), Inches(4.7), Inches(11.7), Inches(1.9), size=16, gap=9)
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 9 — Sécurisation : vulnérabilités OWASP
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Plan de sécurisation — vulnérabilités (OWASP)", 5)
bullets(s, [
"Contrôle d'accès : RBAC par middleware, garde-fous admin.",
"Cryptographie : HTTPS/TLS, bcrypt (coût 12), mots de passe masqués et hors logs.",
"Injection : ORM paramétré (Eloquent) + validation par Form Requests.",
"Authentification : JWT signé, TTL 60 min, blacklist, rate limiting différencié.",
"Journalisation : piste d'audit immuable (IP, anciennes/nouvelles valeurs).",
"Configuration : APP_DEBUG=false, secrets hors dépôt, CORS en liste blanche.",
], Inches(0.8), Inches(1.5), Inches(11.7), Inches(5.0), size=17, gap=11)
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 10 — Matrice de risques
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Sécurisation — matrice de criticité des risques", 5)
image_fit(s, "matrice_risques.png", Inches(2.4), Inches(1.25), Inches(8.5), Inches(5.5),
caption="Criticité = gravité × probabilité · priorisation des actions (R1, R7, R2 prioritaires)")
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 11 — Actions préventives / correctives
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Sécurisation — actions préventives et correctives", 5)
cards(s, [
("Prévention", [
"Validation & ORM (injection)",
"MFA admin, mots de passe robustes",
"audit dépendances en CI",
"Sauvegardes automatisées"], GREEN_D),
("Chiffrement", [
"HTTPS/TLS en transit",
"bcrypt + AES (APP_KEY)",
"Notes de santé chiffrées (prévu)",
"JWT signés"], BLUE),
("Correction", [
"Correctif prioritaire + test",
"Révocation / blacklist token",
"Redéploiement automatisé",
"Plan de reprise (PRA)"], ORANGE),
], y=1.6, h=4.6, top=0.6)
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 12 — RGPD
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Données personnelles et RGPD", 6)
bullets(s, [
"Données de santé mentale = données sensibles (RGPD art. 9).",
"Consentement explicite obligatoire à l'inscription, tracé en base.",
"Droits implémentés : accès et portabilité (export JSON), effacement (anonymisation + soft delete).",
"Minimisation des données ; hébergement UE, aucun transfert hors UE ; cible HDS en production.",
], Inches(0.8), Inches(1.45), Inches(11.7), Inches(2.4), size=17, gap=10)
rect(s, Inches(0.8), Inches(4.25), Inches(11.7), Inches(2.3), BLUE_L)
rect(s, Inches(0.8), Inches(4.25), Inches(3.6), Inches(0.55), GREEN_D)
tb, tf = textbox(s, Inches(0.95), Inches(4.28), Inches(3.4), Inches(0.5), MSO_ANCHOR.MIDDLE)
set_par(tf.paragraphs[0], "Traçabilité (art. 32)", 15, WHITE, bold=True, space_after=0)
bullets(s, [
"Piste d'audit immuable : action, IP, anciennes/nouvelles valeurs — le mot de passe n'est jamais journalisé.",
"Chiffrement en transit, hachage bcrypt, contrôle d'accès par rôle.",
"Export et anonymisation testés automatiquement (tests RGPD).",
], Inches(1.0), Inches(4.95), Inches(11.3), Inches(1.6), size=14, gap=6)
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 13 — Gestion de crise & escalade
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Gestion de crise et escalade d'information", 6)
levels = [
("N1 — Détection", "Astreinte technique", "Qualifier, contenir, ouvrir un ticket sécurité", GREEN),
("N2 — Confirmation", "Responsable technique", "Isoler, préserver les preuves, évaluer l'impact", YELLOW),
("N3 — Crise", "Chef de projet + DPO", "Notification CNIL < 72 h, information des personnes", ORANGE),
("N4 — Direction", "Direction + Ministère", "Communication officielle, coordination ANSSI", RED),
]
y = 1.55
for titre, resp, act, col in levels:
rect(s, Inches(0.8), Inches(y), Inches(11.7), Inches(1.15), BLUE_L)
rect(s, Inches(0.8), Inches(y), Inches(0.16), Inches(1.15), col)
tb, tf = textbox(s, Inches(1.1), Inches(y + 0.05), Inches(3.0), Inches(1.05), MSO_ANCHOR.MIDDLE)
set_par(tf.paragraphs[0], titre, 15, col if col != YELLOW else GREY_D, bold=True, space_after=2)
p = tf.add_paragraph(); set_par(p, resp, 12, GREY_D, space_after=0)
tb, tf = textbox(s, Inches(4.3), Inches(y + 0.05), Inches(8.0), Inches(1.05), MSO_ANCHOR.MIDDLE)
set_par(tf.paragraphs[0], act, 14, INK, space_after=0)
y += 1.32
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 14 — Démo & conclusion
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Démonstration et conclusion", 7)
tb, tf = textbox(s, Inches(0.8), Inches(1.35), Inches(11.7), Inches(0.5))
set_par(tf.paragraphs[0], "Démonstration des outils (en direct)", 17, GREEN_D, bold=True, space_after=0)
bullets(s, [
"Versioning : arbre des commits, branches et tags sur Gitea.",
"Intégration continue : pull request → 29 tests verts (Gitea Actions).",
"Ticketing : création d'une issue (gabarit incident), labels, jalon, Kanban.",
"Déploiement : merge → Coolify → site en ligne (cesizen.cleanows.fr).",
], Inches(0.8), Inches(1.85), Inches(11.7), Inches(2.4), size=16, gap=9)
tb, tf = textbox(s, Inches(0.8), Inches(4.35), Inches(11.7), Inches(0.5))
set_par(tf.paragraphs[0], "En synthèse", 17, GREEN_D, bold=True, space_after=0)
bullets(s, [
"Chaîne de déploiement complète, outillée et réellement en production.",
"Maintenance cadrée (ticketing, SLA, veille) et sécurité analysée par les risques + RGPD.",
], Inches(0.8), Inches(4.85), Inches(11.7), Inches(1.4), size=16, gap=8)
rect(s, 0, Inches(6.55), SW, Inches(0.95), BLUE)
tb, tf = textbox(s, Inches(0.8), Inches(6.57), Inches(11.7), Inches(0.9), 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)
# ─── Numéros de page (toutes les slides sauf la page de titre) ─────
for idx, slide in enumerate(prs.slides):
if idx == 0:
continue # pas de numéro sur la slide de titre
tb, tf = textbox(slide, Inches(12.4), Inches(7.05), Inches(0.7), Inches(0.35),
anchor=MSO_ANCHOR.MIDDLE)
set_par(tf.paragraphs[0], str(idx + 1), 10, GREY, bold=True,
align=PP_ALIGN.RIGHT, space_after=0)
prs.save(OUT)
print(f"Support de soutenance généré : {OUT}")
print(f"Nombre de slides : {len(prs.slides._sldIdLst)}")