Files
open-school/backend/tools/migrate_program_content_v2.py
2026-05-12 20:39:35 +02:00

227 lines
8.9 KiB
Python

import html
import re
import shutil
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
LESSON_RELATIVE = Path(
"contenus_pedagogiques/cycle_3/mathematiques/cm1/"
"01_nombres_calcul_resolution/01B_fractions"
)
MIRROR_RELATIVE = Path(
"Programme/contenus_pedagogiques/cycle_3/mathematiques/cm1/"
"01_nombres_calcul_resolution/01B_fractions"
)
LESSON_DIRS = [ROOT / "backend" / LESSON_RELATIVE, ROOT / MIRROR_RELATIVE]
def title_from_slug(slug: str) -> str:
parts = slug.split("_")
if parts and parts[0][:2].isdigit():
parts = parts[1:]
return " ".join(part.capitalize() for part in parts if part)
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) -> 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\b(?P<attrs>[^>]*)/?>'
r'(?P<body>.*?)</g>',
re.DOTALL,
)
for match in pattern.finditer(text):
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 "")
sections.append(
{
"index": len(sections),
"title": html.unescape(raw_title).strip() or f"Card {len(sections) + 1}",
"view_box": [
max(float(match.group("x")) - 28, 0),
max(float(match.group("y")) - 28, 0),
width + 56,
height + 56,
],
}
)
return sections
def render_svg_section(svg_path: Path, section: dict) -> str:
x, y, width, height = section["view_box"]
text = svg_path.read_text(encoding="utf-8", errors="ignore")
text = re.sub(r'\swidth="[^"]+"', f' width="{int(width)}"', text, count=1)
text = re.sub(r'\sheight="[^"]+"', f' height="{int(height)}"', text, count=1)
text = re.sub(
r'\sviewBox="[^"]+"',
f' viewBox="{x:g} {y:g} {width:g} {height:g}"',
text,
count=1,
)
return text
def move_file(source: Path, target: Path) -> None:
if not source.exists():
return
target.parent.mkdir(parents=True, exist_ok=True)
if target.exists():
source.unlink()
return
shutil.move(str(source), str(target))
def write_if_missing(path: Path, text: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
if not path.exists():
path.write_text(text, encoding="utf-8")
def build_lesson_markdown(lesson_dir: Path) -> None:
readme = lesson_dir / "README.md"
source_text = readme.read_text(encoding="utf-8", errors="ignore").strip() if readme.exists() else ""
fiche_parts = []
for fiche_md in sorted((lesson_dir / "fiches").glob("*/fiche.md")):
fiche_parts.append(fiche_md.read_text(encoding="utf-8", errors="ignore").strip())
body = "\n\n---\n\n".join(part for part in [source_text, *fiche_parts] if part)
text = (
"# Lecon globale - CM1 - Fractions\n\n"
"Cette lecon globale sert de contexte general a Professeur TOP.\n\n"
"## Sources\n\n"
"- Source principale locale : `Programme/pdf_collecteur/state/synthese_cycle3_cm1_mathematiques.md`.\n"
"- Documents officiels de reference : PDF du programme de mathematiques cycle 3 conserves dans `Programme/Cycle 3/Math/`.\n"
"- Niveau : cycle 3, CM1, mathematiques, nombres/calcul/resolution, fractions.\n\n"
"## Contenu consolide\n\n"
f"{body}\n"
)
(lesson_dir / "lecon.md").write_text(text, encoding="utf-8")
def migrate_lesson(lesson_dir: Path) -> None:
if not lesson_dir.exists():
return
old_svg_dir = lesson_dir / "svg"
old_card_dir = lesson_dir / "card_contexts"
old_exercise_svg_dir = lesson_dir / "exercices_svg"
old_kit_dir = lesson_dir / "kit_adaptatif"
fiche_slugs = sorted(
{
path.stem
for path in list(lesson_dir.glob("[0-9][0-9]_*.md"))
+ list(old_svg_dir.glob("*.svg"))
+ list(old_exercise_svg_dir.glob("*.svg"))
}
)
for index, slug in enumerate(fiche_slugs, start=1):
fiche_dir = lesson_dir / "fiches" / slug
move_file(lesson_dir / f"{slug}.md", fiche_dir / "fiche.md")
move_file(old_svg_dir / f"{slug}.svg", fiche_dir / "fiche.svg")
if (fiche_dir / "fiche.md").exists():
text = (fiche_dir / "fiche.md").read_text(encoding="utf-8", errors="ignore")
if "## Sources" not in text:
text = (
text.rstrip()
+ "\n\n## Sources\n\n"
+ "- Source principale locale : `Programme/pdf_collecteur/state/synthese_cycle3_cm1_mathematiques.md`.\n"
+ "- Documents officiels de reference : PDF du programme de mathematiques cycle 3 conserves dans `Programme/Cycle 3/Math/`.\n"
)
(fiche_dir / "fiche.md").write_text(text + "\n", encoding="utf-8")
old_context_subdir = old_card_dir / slug
card_subdir = lesson_dir / "cards" / slug
fiche_svg = fiche_dir / "fiche.svg"
sections = extract_svg_sections(fiche_svg) if fiche_svg.exists() else []
for section in sections:
card_number = section["index"] + 1
legacy_names = [
old_context_subdir / f"Fiche{index}Card{card_number}.md",
old_context_subdir / f"card_{card_number:02d}.md",
]
target_md = card_subdir / f"card_{card_number:02d}.md"
for legacy in legacy_names:
if legacy.exists():
move_file(legacy, target_md)
break
write_if_missing(
target_md,
(
f"# {section['title']}\n\n"
"Contexte de card a completer.\n\n"
"## Sources\n\n"
"- Source principale locale : `Programme/pdf_collecteur/state/synthese_cycle3_cm1_mathematiques.md`.\n"
),
)
target_svg = card_subdir / f"card_{card_number:02d}.svg"
if not target_svg.exists():
target_svg.write_text(render_svg_section(fiche_svg, section), encoding="utf-8")
exercise_dir = lesson_dir / "exercices" / slug
move_file(old_exercise_svg_dir / f"{slug}.svg", exercise_dir / "exercices.svg")
exercise_md = exercise_dir / "exercices.md"
if exercise_md.exists():
text = exercise_md.read_text(encoding="utf-8", errors="ignore")
if "## Sources" not in text:
text = (
text.rstrip()
+ "\n\n## Sources\n\n"
+ "- Source principale locale : `Programme/pdf_collecteur/state/synthese_cycle3_cm1_mathematiques.md`.\n"
+ "- Documents officiels de reference : PDF du programme de mathematiques cycle 3 conserves dans `Programme/Cycle 3/Math/`.\n"
)
exercise_md.write_text(text + "\n", encoding="utf-8")
if old_kit_dir.exists():
tests_dir = lesson_dir / "tests"
for md_path in sorted((old_kit_dir / "test_diagnostic").glob("*.md")):
if md_path.name == "README.md":
continue
move_file(md_path, tests_dir / md_path.name)
move_file(old_kit_dir / "00_principe_adaptatif.md", tests_dir / "00_principe_adaptatif.md")
move_file(old_kit_dir / "routage.md", tests_dir / "routage.md")
move_file(old_kit_dir / "svg" / "test_diagnostic_fraction.svg", tests_dir / "test_diagnostic_fraction.svg")
parcours_dir = lesson_dir / "exercices" / "parcours_adaptes"
for md_path in sorted((old_kit_dir / "parcours_profils").glob("*.md")):
if md_path.name == "README.md":
continue
move_file(md_path, parcours_dir / md_path.name)
move_file(old_kit_dir / "svg" / "parcours_fraction_support.svg", parcours_dir / "parcours_fraction_support.svg")
build_lesson_markdown(lesson_dir)
for legacy_dir in [old_svg_dir, old_card_dir, old_exercise_svg_dir, old_kit_dir]:
if legacy_dir.exists():
shutil.rmtree(legacy_dir)
def main() -> None:
for lesson_dir in LESSON_DIRS:
migrate_lesson(lesson_dir)
print(f"migrated: {lesson_dir}")
if __name__ == "__main__":
main()