feat: add slide generation script for CESIZen project, covering deployment and security aspects
This commit is contained in:
@@ -0,0 +1,417 @@
|
||||
#!/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)}")
|
||||
Reference in New Issue
Block a user