fix: org_chart et renderers tolerent noeuds string ou dict (anti-crash)
This commit is contained in:
+46
-6
@@ -38,8 +38,14 @@ def hex_to_rgb(h: str) -> RGBColor:
|
|||||||
return RGBColor(int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16))
|
return RGBColor(int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16))
|
||||||
|
|
||||||
|
|
||||||
def pick(d: dict, *keys, default=""):
|
def pick(d, *keys, default=""):
|
||||||
"""Accès tolérant : retourne la première clé non vide trouvée."""
|
"""Accès tolérant : première clé non vide trouvée.
|
||||||
|
Si d est une chaîne (l'agent a produit 'Texte' au lieu de {label:'Texte'}),
|
||||||
|
on la retourne directement — évite les crashs 'str has no attribute get'."""
|
||||||
|
if isinstance(d, str):
|
||||||
|
return d
|
||||||
|
if not isinstance(d, dict):
|
||||||
|
return default
|
||||||
for k in keys:
|
for k in keys:
|
||||||
v = d.get(k)
|
v = d.get(k)
|
||||||
if v not in (None, ""):
|
if v not in (None, ""):
|
||||||
@@ -47,6 +53,16 @@ def pick(d: dict, *keys, default=""):
|
|||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def as_label(item, *keys, default=""):
|
||||||
|
"""Normalise un élément de liste en texte : 'Texte' ou {label:'Texte'}."""
|
||||||
|
if isinstance(item, str):
|
||||||
|
return item
|
||||||
|
if isinstance(item, dict):
|
||||||
|
return pick(item, *(keys or ("label", "texte", "titre", "title")),
|
||||||
|
default=default)
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
def estimate_text_height(text: str, size_pt: int, width_cm: float) -> float:
|
def estimate_text_height(text: str, size_pt: int, width_cm: float) -> float:
|
||||||
"""Hauteur estimée d'un texte (cm) pour une largeur donnée."""
|
"""Hauteur estimée d'un texte (cm) pour une largeur donnée."""
|
||||||
if not text:
|
if not text:
|
||||||
@@ -475,6 +491,8 @@ class RenderEngineV2:
|
|||||||
y = self._cy(tot)
|
y = self._cy(tot)
|
||||||
bd = self.components["badge"]["sizes"]["l"]
|
bd = self.components["badge"]["sizes"]["l"]
|
||||||
for i, st in enumerate(steps):
|
for i, st in enumerate(steps):
|
||||||
|
if isinstance(st, str):
|
||||||
|
st = {"titre": st}
|
||||||
self._card(slide, self.MX, y, self.SLIDE_W - 2 * self.MX, rh)
|
self._card(slide, self.MX, y, self.SLIDE_W - 2 * self.MX, rh)
|
||||||
self._badge(slide, self.MX + 1.02 + bd / 2, y + rh / 2, bd,
|
self._badge(slide, self.MX + 1.02 + bd / 2, y + rh / 2, bd,
|
||||||
st.get("numero", i + 1), font_size=28)
|
st.get("numero", i + 1), font_size=28)
|
||||||
@@ -665,7 +683,8 @@ class RenderEngineV2:
|
|||||||
HDR_H = 0.80
|
HDR_H = 0.80
|
||||||
WS_H = 0.68
|
WS_H = 0.68
|
||||||
TASK_H = 0.78
|
TASK_H = 0.78
|
||||||
n_tasks = sum(len(ws.get("tasks", [])) for ws in workstreams)
|
n_tasks = sum(len(ws.get("tasks", [])) for ws in workstreams
|
||||||
|
if isinstance(ws, dict))
|
||||||
total_h = HDR_H + len(workstreams)*WS_H + n_tasks*TASK_H
|
total_h = HDR_H + len(workstreams)*WS_H + n_tasks*TASK_H
|
||||||
y0 = self._cy(total_h)
|
y0 = self._cy(total_h)
|
||||||
gx = self.MX + LBL_W + GAP_COL # grid x origin
|
gx = self.MX + LBL_W + GAP_COL # grid x origin
|
||||||
@@ -687,6 +706,8 @@ class RenderEngineV2:
|
|||||||
|
|
||||||
cur_y = y0 + HDR_H
|
cur_y = y0 + HDR_H
|
||||||
for ws_idx, ws in enumerate(workstreams):
|
for ws_idx, ws in enumerate(workstreams):
|
||||||
|
if not isinstance(ws, dict):
|
||||||
|
continue
|
||||||
ws_col = self.cycle[ws_idx % len(self.cycle)]
|
ws_col = self.cycle[ws_idx % len(self.cycle)]
|
||||||
# Workstream row
|
# Workstream row
|
||||||
self._rect(slide, self.MX, cur_y, self.SLIDE_W - 2*self.MX, WS_H, "#EEECEA")
|
self._rect(slide, self.MX, cur_y, self.SLIDE_W - 2*self.MX, WS_H, "#EEECEA")
|
||||||
@@ -696,6 +717,8 @@ class RenderEngineV2:
|
|||||||
color=self.C["navy"], anchor=MSO_ANCHOR.MIDDLE)
|
color=self.C["navy"], anchor=MSO_ANCHOR.MIDDLE)
|
||||||
cur_y += WS_H
|
cur_y += WS_H
|
||||||
for task in ws.get("tasks", []):
|
for task in ws.get("tasks", []):
|
||||||
|
if not isinstance(task, dict):
|
||||||
|
continue
|
||||||
start = task.get("start", 0)
|
start = task.get("start", 0)
|
||||||
end = task.get("end", start + 1)
|
end = task.get("end", start + 1)
|
||||||
bg = self.C["white"]
|
bg = self.C["white"]
|
||||||
@@ -837,6 +860,8 @@ class RenderEngineV2:
|
|||||||
|
|
||||||
# Rows
|
# Rows
|
||||||
for i, task in enumerate(tasks):
|
for i, task in enumerate(tasks):
|
||||||
|
if not isinstance(task, dict):
|
||||||
|
continue
|
||||||
ry = y + HDR_H + i * ROW_H
|
ry = y + HDR_H + i * ROW_H
|
||||||
bg = self.C["card"] if i % 2 == 0 else self.C["white"]
|
bg = self.C["card"] if i % 2 == 0 else self.C["white"]
|
||||||
self._rect(slide, self.MX, ry, self.SLIDE_W - 2*self.MX, ROW_H, bg)
|
self._rect(slide, self.MX, ry, self.SLIDE_W - 2*self.MX, ROW_H, bg)
|
||||||
@@ -907,12 +932,25 @@ class RenderEngineV2:
|
|||||||
# ── org_chart ─────────────────────────────────────────────────────────────
|
# ── org_chart ─────────────────────────────────────────────────────────────
|
||||||
def _render_org_chart(self, slide, d):
|
def _render_org_chart(self, slide, d):
|
||||||
self._title(slide, pick(d, "titre", "title"))
|
self._title(slide, pick(d, "titre", "title"))
|
||||||
|
|
||||||
|
def _node(n):
|
||||||
|
"""Normalise un nœud : 'Texte' → {label:'Texte'}, dict inchangé."""
|
||||||
|
if isinstance(n, str):
|
||||||
|
return {"label": n, "children": []}
|
||||||
|
if isinstance(n, dict):
|
||||||
|
return {"label": pick(n, "label", "titre", "title", "nom"),
|
||||||
|
"children": n.get("children", []) or []}
|
||||||
|
return {"label": str(n), "children": []}
|
||||||
|
|
||||||
root = d.get("root", {})
|
root = d.get("root", {})
|
||||||
|
if isinstance(root, str):
|
||||||
|
root = {"label": root, "children": []}
|
||||||
if not root:
|
if not root:
|
||||||
return
|
return
|
||||||
|
root = _node(root)
|
||||||
BOX_W, BOX_H = 3.81, 1.0
|
BOX_W, BOX_H = 3.81, 1.0
|
||||||
GAP_V, GAP_H = 1.27, 0.64
|
GAP_V, GAP_H = 1.27, 0.64
|
||||||
children = root.get("children", [])
|
children = [_node(c) for c in root.get("children", [])]
|
||||||
n_children = len(children)
|
n_children = len(children)
|
||||||
# grandchildren count per child (max)
|
# grandchildren count per child (max)
|
||||||
max_gd = max((len(c.get("children", [])) for c in children), default=0)
|
max_gd = max((len(c.get("children", [])) for c in children), default=0)
|
||||||
@@ -971,7 +1009,7 @@ class RenderEngineV2:
|
|||||||
ln3.line.width = Pt(1.0)
|
ln3.line.width = Pt(1.0)
|
||||||
|
|
||||||
# Grandchildren — sizing dynamique pour tenir dans la colonne
|
# Grandchildren — sizing dynamique pour tenir dans la colonne
|
||||||
grandchildren = child.get("children", [])
|
grandchildren = [_node(g) for g in child.get("children", [])]
|
||||||
if grandchildren:
|
if grandchildren:
|
||||||
gd_y = child_y + BOX_H + GAP_V
|
gd_y = child_y + BOX_H + GAP_V
|
||||||
n_gd = len(grandchildren)
|
n_gd = len(grandchildren)
|
||||||
@@ -1077,6 +1115,8 @@ class RenderEngineV2:
|
|||||||
|
|
||||||
# Items as numbered circles
|
# Items as numbered circles
|
||||||
for i, item in enumerate(items):
|
for i, item in enumerate(items):
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
continue
|
||||||
ix = item.get("x", 50) # 0-100
|
ix = item.get("x", 50) # 0-100
|
||||||
iy = item.get("y", 50) # 0-100
|
iy = item.get("y", 50) # 0-100
|
||||||
px = mx0 + (ix / 100) * MAT_W
|
px = mx0 + (ix / 100) * MAT_W
|
||||||
@@ -1099,7 +1139,7 @@ class RenderEngineV2:
|
|||||||
size=9, bold=True, color=self.C["white"],
|
size=9, bold=True, color=self.C["white"],
|
||||||
align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
|
align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
|
||||||
self._text(slide, lx + 0.6, leg_y - 0.35, 2.8, 0.6,
|
self._text(slide, lx + 0.6, leg_y - 0.35, 2.8, 0.6,
|
||||||
item.get("label", ""), size=10, color=self.C["body"])
|
as_label(item), size=10, color=self.C["body"])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user