admin mode
This commit is contained in:
138
backend/app/program_content.py
Normal file
138
backend/app/program_content.py
Normal file
@@ -0,0 +1,138 @@
|
||||
import base64
|
||||
import os
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote
|
||||
|
||||
|
||||
def default_content_root() -> Path:
|
||||
current = Path(__file__).resolve()
|
||||
candidates = [
|
||||
parent / "Programme" / "contenus_pedagogiques"
|
||||
for parent in current.parents
|
||||
]
|
||||
candidates.extend(
|
||||
[
|
||||
Path("/programme/contenus_pedagogiques"),
|
||||
Path("/app/Programme/contenus_pedagogiques"),
|
||||
]
|
||||
)
|
||||
return next((candidate for candidate in candidates if candidate.exists()), candidates[0])
|
||||
|
||||
|
||||
CONTENT_ROOT = Path(os.getenv("PROGRAM_CONTENT_ROOT", default_content_root())).resolve()
|
||||
|
||||
GRADE_ALIASES = {
|
||||
"CM1": "cm1",
|
||||
"CM2": "cm2",
|
||||
"6e": "6e",
|
||||
"6eme": "6e",
|
||||
"6ieme": "6e",
|
||||
"5e": "5e",
|
||||
"5eme": "5e",
|
||||
"5ieme": "5e",
|
||||
"4e": "4e",
|
||||
"4eme": "4e",
|
||||
"4ieme": "4e",
|
||||
"3e": "3e",
|
||||
"3eme": "3e",
|
||||
"3ieme": "3e",
|
||||
}
|
||||
|
||||
|
||||
def normalize_grade(grade: str) -> str:
|
||||
compact = (grade or "").strip().replace("è", "e").replace("é", "e")
|
||||
return GRADE_ALIASES.get(compact, compact.lower())
|
||||
|
||||
|
||||
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 encode_asset_path(relative_path: str) -> str:
|
||||
payload = relative_path.encode("utf-8")
|
||||
return base64.urlsafe_b64encode(payload).decode("ascii").rstrip("=")
|
||||
|
||||
|
||||
def decode_asset_token(token: str) -> Path:
|
||||
padding = "=" * (-len(token) % 4)
|
||||
relative = base64.urlsafe_b64decode(f"{token}{padding}").decode("utf-8")
|
||||
path = (CONTENT_ROOT / relative).resolve()
|
||||
if CONTENT_ROOT not in path.parents:
|
||||
raise ValueError("Chemin de ressource invalide")
|
||||
return path
|
||||
|
||||
|
||||
def build_asset_url(relative_path: str) -> str:
|
||||
return f"/api/program/assets/{quote(encode_asset_path(relative_path))}"
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def list_lessons() -> list[dict]:
|
||||
lessons: list[dict] = []
|
||||
if not CONTENT_ROOT.exists():
|
||||
return lessons
|
||||
|
||||
for svg_dir in sorted(CONTENT_ROOT.glob("cycle_*/*/*/**/svg")):
|
||||
if not svg_dir.is_dir():
|
||||
continue
|
||||
|
||||
svg_files = sorted(svg_dir.glob("*.svg"))
|
||||
if not svg_files:
|
||||
continue
|
||||
|
||||
lesson_dir = svg_dir.parent
|
||||
relative_lesson = lesson_dir.relative_to(CONTENT_ROOT).as_posix()
|
||||
parts = relative_lesson.split("/")
|
||||
if len(parts) < 4:
|
||||
continue
|
||||
|
||||
cycle, subject, grade = parts[0], parts[1], parts[2]
|
||||
title = title_from_slug(lesson_dir.name)
|
||||
readme = lesson_dir / "README.md"
|
||||
if readme.exists():
|
||||
first_heading = next(
|
||||
(
|
||||
line.lstrip("#").strip()
|
||||
for line in readme.read_text(encoding="utf-8", errors="ignore").splitlines()
|
||||
if line.startswith("#")
|
||||
),
|
||||
"",
|
||||
)
|
||||
title = first_heading or title
|
||||
|
||||
assets = [
|
||||
{
|
||||
"name": svg_path.stem,
|
||||
"title": title_from_slug(svg_path.stem),
|
||||
"path": svg_path.relative_to(CONTENT_ROOT).as_posix(),
|
||||
"url": build_asset_url(svg_path.relative_to(CONTENT_ROOT).as_posix()),
|
||||
}
|
||||
for svg_path in svg_files
|
||||
]
|
||||
|
||||
lessons.append(
|
||||
{
|
||||
"id": relative_lesson,
|
||||
"title": title,
|
||||
"subject": title_from_slug(subject),
|
||||
"grade": grade,
|
||||
"cycle": cycle,
|
||||
"asset_count": len(assets),
|
||||
"assets": assets,
|
||||
}
|
||||
)
|
||||
|
||||
return lessons
|
||||
|
||||
|
||||
def list_lessons_for_grade(grade: str) -> list[dict]:
|
||||
normalized = normalize_grade(grade)
|
||||
return [lesson for lesson in list_lessons() if lesson["grade"] == normalized]
|
||||
|
||||
|
||||
def get_lesson(lesson_id: str) -> dict | None:
|
||||
return next((lesson for lesson in list_lessons() if lesson["id"] == lesson_id), None)
|
||||
Reference in New Issue
Block a user