new admin
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import base64
|
||||
import html
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote
|
||||
@@ -26,6 +28,7 @@ def default_content_root() -> Path:
|
||||
|
||||
CONTENT_ROOT = Path(os.getenv("PROGRAM_CONTENT_ROOT", default_content_root())).resolve()
|
||||
LESSON_BACKGROUND = "#f8fafc"
|
||||
REVIEW_STATE_PATH = Path(os.getenv("PROGRAM_REVIEW_STATE_PATH", CONTENT_ROOT / ".program_review_state.json"))
|
||||
|
||||
GRADE_ALIASES = {
|
||||
"CM1": "cm1",
|
||||
@@ -90,19 +93,31 @@ def find_section_context_path(svg_path: Path, asset_number: int, section_number:
|
||||
return next((candidate for candidate in candidates if candidate.exists()), None)
|
||||
|
||||
|
||||
def svg_attr(attrs: str, name: str, default: float | None = None) -> float | None:
|
||||
match = re.search(rf'\b{name}="(?P<value>[-\d.]+)"', attrs)
|
||||
if match:
|
||||
return float(match.group("value"))
|
||||
return default
|
||||
|
||||
|
||||
def extract_svg_sections(svg_path: Path, asset_number: int | None = None) -> list[dict]:
|
||||
text = svg_path.read_text(encoding="utf-8", errors="ignore")
|
||||
sections: list[dict] = []
|
||||
pattern = re.compile(
|
||||
r'<g\s+transform="translate\((?P<x>[-\d.]+)\s+(?P<y>[-\d.]+)\)">\s*'
|
||||
r'<rect\s+x="0"\s+y="0"\s+width="(?P<w>[-\d.]+)"\s+height="(?P<h>[-\d.]+)"[^>]*>'
|
||||
r'<rect\b(?P<attrs>[^>]*)/?>'
|
||||
r'(?P<body>.*?)</g>',
|
||||
re.DOTALL,
|
||||
)
|
||||
for match in pattern.finditer(text):
|
||||
width = float(match.group("w"))
|
||||
height = float(match.group("h"))
|
||||
if width < 250 or height < 120:
|
||||
attrs = match.group("attrs")
|
||||
rect_x = svg_attr(attrs, "x", 0)
|
||||
rect_y = svg_attr(attrs, "y", 0)
|
||||
width = svg_attr(attrs, "width")
|
||||
height = svg_attr(attrs, "height")
|
||||
if rect_x != 0 or rect_y != 0 or width is None or height is None:
|
||||
continue
|
||||
if width < 250 or height < 100:
|
||||
continue
|
||||
title_match = re.search(r'<text[^>]*>(?P<title>.*?)</text>', match.group("body"), re.DOTALL)
|
||||
raw_title = re.sub(r"<[^>]+>", "", title_match.group("title") if title_match else "")
|
||||
@@ -261,3 +276,105 @@ def get_content_status() -> dict:
|
||||
|
||||
def get_lesson(lesson_id: str) -> dict | None:
|
||||
return next((lesson for lesson in list_lessons() if lesson["id"] == lesson_id), None)
|
||||
|
||||
|
||||
def load_review_state() -> dict:
|
||||
if not REVIEW_STATE_PATH.exists():
|
||||
return {}
|
||||
try:
|
||||
return json.loads(REVIEW_STATE_PATH.read_text(encoding="utf-8"))
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
|
||||
|
||||
def save_review_state(state: dict) -> None:
|
||||
REVIEW_STATE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
REVIEW_STATE_PATH.write_text(
|
||||
json.dumps(state, ensure_ascii=False, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def collect_lesson_status(lesson: dict) -> dict:
|
||||
lesson_dir = CONTENT_ROOT / lesson["id"]
|
||||
return {
|
||||
"micro_fiches": len([path for path in lesson_dir.glob("*.md") if path.name.startswith(tuple("0123456789"))]),
|
||||
"lesson_svg": len(lesson.get("assets") or []),
|
||||
"cards": sum(len(asset.get("sections") or []) for asset in lesson.get("assets") or []),
|
||||
"contexts": len(list((lesson_dir / "card_contexts").rglob("Fiche*Card*.md"))),
|
||||
"text_exercises": len(list((lesson_dir / "exercices").rglob("exercices.md"))),
|
||||
"exercise_svg": len(list((lesson_dir / "exercices_svg").glob("*.svg"))),
|
||||
"validations": sorted(path.stem for path in lesson_dir.glob("VALIDATION_PHASE_*.md")),
|
||||
}
|
||||
|
||||
|
||||
def append_tree_child(parent: dict, key: str, label: str, node_type: str, path: str = "") -> dict:
|
||||
children = parent.setdefault("children", [])
|
||||
for child in children:
|
||||
if child["key"] == key:
|
||||
return child
|
||||
child = {"key": key, "label": label, "type": node_type, "path": path, "children": []}
|
||||
children.append(child)
|
||||
return child
|
||||
|
||||
|
||||
def build_program_tree() -> dict:
|
||||
reviews = load_review_state()
|
||||
root = {
|
||||
"key": "program",
|
||||
"label": "Programme",
|
||||
"type": "root",
|
||||
"path": "",
|
||||
"children": [],
|
||||
"review": reviews.get("program", {}),
|
||||
}
|
||||
for lesson in list_lessons():
|
||||
parts = lesson["id"].split("/")
|
||||
cycle, subject, grade = parts[0], parts[1], parts[2]
|
||||
theme_parts = parts[3:-1]
|
||||
lesson_slug = parts[-1]
|
||||
|
||||
cycle_node = append_tree_child(root, cycle, title_from_slug(cycle), "cycle", cycle)
|
||||
grade_key = f"{cycle}/{grade}"
|
||||
grade_node = append_tree_child(cycle_node, grade_key, grade.upper(), "grade", grade_key)
|
||||
subject_key = f"{grade_key}/{subject}"
|
||||
subject_node = append_tree_child(grade_node, subject_key, title_from_slug(subject), "subject", subject_key)
|
||||
parent = subject_node
|
||||
current_path = f"{cycle}/{subject}/{grade}"
|
||||
for theme_slug in theme_parts:
|
||||
current_path = f"{current_path}/{theme_slug}"
|
||||
parent = append_tree_child(parent, current_path, title_from_slug(theme_slug), "theme", current_path)
|
||||
|
||||
lesson_node = append_tree_child(parent, lesson["id"], lesson["title"], "lesson", lesson["id"])
|
||||
lesson_node["status"] = collect_lesson_status(lesson)
|
||||
lesson_node["review"] = reviews.get(lesson["id"], {})
|
||||
lesson_node["assets"] = lesson.get("assets") or []
|
||||
for asset in lesson.get("assets") or []:
|
||||
asset_key = asset["path"]
|
||||
asset_node = append_tree_child(lesson_node, asset_key, asset["title"], "fiche", asset_key)
|
||||
asset_node["url"] = asset.get("url")
|
||||
asset_node["review"] = reviews.get(asset_key, {})
|
||||
for section in asset.get("sections") or []:
|
||||
section_key = f"{asset_key}#{section.get('context_key') or section.get('index')}"
|
||||
section_node = append_tree_child(
|
||||
asset_node,
|
||||
section_key,
|
||||
section.get("title") or section_key,
|
||||
"card",
|
||||
section_key,
|
||||
)
|
||||
section_node["url"] = section.get("url")
|
||||
section_node["review"] = reviews.get(section_key, {})
|
||||
return root
|
||||
|
||||
|
||||
def update_program_review(node_key: str, status: str, comment: str, reviewer: str) -> dict:
|
||||
state = load_review_state()
|
||||
state[node_key] = {
|
||||
"status": status,
|
||||
"comment": comment,
|
||||
"reviewer": reviewer,
|
||||
"updated_at": datetime.utcnow().isoformat(timespec="seconds") + "Z",
|
||||
}
|
||||
save_review_state(state)
|
||||
return state[node_key]
|
||||
|
||||
Reference in New Issue
Block a user