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))
|
||||
|
||||
|
||||
def pick(d: dict, *keys, default=""):
|
||||
"""Accès tolérant : retourne la première clé non vide trouvée."""
|
||||
def pick(d, *keys, default=""):
|
||||
"""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:
|
||||
v = d.get(k)
|
||||
if v not in (None, ""):
|
||||
@@ -47,6 +53,16 @@ def pick(d: dict, *keys, 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:
|
||||
"""Hauteur estimée d'un texte (cm) pour une largeur donnée."""
|
||||
if not text:
|
||||
@@ -475,6 +491,8 @@ class RenderEngineV2:
|
||||
y = self._cy(tot)
|
||||
bd = self.components["badge"]["sizes"]["l"]
|
||||
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._badge(slide, self.MX + 1.02 + bd / 2, y + rh / 2, bd,
|
||||
st.get("numero", i + 1), font_size=28)
|
||||
@@ -665,7 +683,8 @@ class RenderEngineV2:
|
||||
HDR_H = 0.80
|
||||
WS_H = 0.68
|
||||
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
|
||||
y0 = self._cy(total_h)
|
||||
gx = self.MX + LBL_W + GAP_COL # grid x origin
|
||||
@@ -687,6 +706,8 @@ class RenderEngineV2:
|
||||
|
||||
cur_y = y0 + HDR_H
|
||||
for ws_idx, ws in enumerate(workstreams):
|
||||
if not isinstance(ws, dict):
|
||||
continue
|
||||
ws_col = self.cycle[ws_idx % len(self.cycle)]
|
||||
# Workstream row
|
||||
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)
|
||||
cur_y += WS_H
|
||||
for task in ws.get("tasks", []):
|
||||
if not isinstance(task, dict):
|
||||
continue
|
||||
start = task.get("start", 0)
|
||||
end = task.get("end", start + 1)
|
||||
bg = self.C["white"]
|
||||
@@ -837,6 +860,8 @@ class RenderEngineV2:
|
||||
|
||||
# Rows
|
||||
for i, task in enumerate(tasks):
|
||||
if not isinstance(task, dict):
|
||||
continue
|
||||
ry = y + HDR_H + i * ROW_H
|
||||
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)
|
||||
@@ -907,12 +932,25 @@ class RenderEngineV2:
|
||||
# ── org_chart ─────────────────────────────────────────────────────────────
|
||||
def _render_org_chart(self, slide, d):
|
||||
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", {})
|
||||
if isinstance(root, str):
|
||||
root = {"label": root, "children": []}
|
||||
if not root:
|
||||
return
|
||||
root = _node(root)
|
||||
BOX_W, BOX_H = 3.81, 1.0
|
||||
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)
|
||||
# grandchildren count per child (max)
|
||||
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)
|
||||
|
||||
# Grandchildren — sizing dynamique pour tenir dans la colonne
|
||||
grandchildren = child.get("children", [])
|
||||
grandchildren = [_node(g) for g in child.get("children", [])]
|
||||
if grandchildren:
|
||||
gd_y = child_y + BOX_H + GAP_V
|
||||
n_gd = len(grandchildren)
|
||||
@@ -1077,6 +1115,8 @@ class RenderEngineV2:
|
||||
|
||||
# Items as numbered circles
|
||||
for i, item in enumerate(items):
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
ix = item.get("x", 50) # 0-100
|
||||
iy = item.get("y", 50) # 0-100
|
||||
px = mx0 + (ix / 100) * MAT_W
|
||||
@@ -1099,7 +1139,7 @@ class RenderEngineV2:
|
||||
size=9, bold=True, color=self.C["white"],
|
||||
align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
|
||||
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