fix(ical): fiabiliser le parsing Pronote 2026

Revue Codex validée. Corrections de parsing iCal, conservation du contexte des devoirs et fixtures anonymisées.

Co-authored-by: Codex <codex@antoineve.me>
Co-committed-by: Codex <codex@antoineve.me>
This commit was merged in pull request #31.
This commit is contained in:
2026-09-12 14:54:55 +02:00
committed by Codex
parent f194ed985d
commit d45d38d365
9 changed files with 399 additions and 79 deletions
+115 -5
View File
@@ -294,8 +294,8 @@ def test_collect_homeworks_dedup() -> None:
id="lesson2",
start=datetime(2026, 9, 10, 10, 0),
end=datetime(2026, 9, 10, 11, 0),
subject="Physique",
teachers=("M. Martin",),
subject="Math",
teachers=("M. Dupont",),
rooms=("205",),
group=None,
status=LessonStatus.NORMAL,
@@ -343,8 +343,8 @@ def test_collect_homeworks_id_stability() -> None:
id="lesson2",
start=datetime(2026, 9, 10, 10, 0),
end=datetime(2026, 9, 10, 11, 0),
subject="Physique",
teachers=("M. Martin",),
subject="Math",
teachers=("M. Dupont",),
rooms=("205",),
group=None,
status=LessonStatus.NORMAL,
@@ -361,7 +361,117 @@ def test_collect_homeworks_id_stability() -> None:
homeworks = collect_homeworks([lesson1, lesson2], target_date=date(2026, 9, 10))
assert len(homeworks) == 1
assert homeworks[0].id == generate_homework_id(date(2026, 9, 10), "devoir commun")
assert homeworks[0].id == generate_homework_id(
date(2026, 9, 10), "devoir commun", "Math", ("M. Dupont",)
)
def test_collect_homeworks_keeps_distinct_subjects() -> None:
"""Deux matières différentes conservent deux devoirs homonymes.
:return: None
"""
lesson = Lesson(
id="lesson1",
start=datetime(2026, 9, 10, 8, 0),
end=datetime(2026, 9, 10, 9, 0),
subject="Math",
teachers=("M. Dupont",),
group=None,
content=None,
homework_blocks=(
HomeworkBlock(
kind="due",
date=date(2026, 9, 10),
text="Devoir commun",
html="<p>Devoir commun</p>",
),
),
)
other_lesson = lesson.model_copy(
update={"id": "lesson2", "subject": "Physique", "teachers": ("M. Martin",)}
)
homeworks = collect_homeworks([lesson, other_lesson], target_date=date(2026, 9, 10))
assert len(homeworks) == 2
assert {homework.subject for homework in homeworks} == {"Math", "Physique"}
def test_collect_homeworks_deduplicates_teacher_order_variants() -> None:
"""Deux ordres d'enseignants équivalents ne créent pas de doublon.
:return: None
"""
homework_block = HomeworkBlock(
kind="due",
date=date(2026, 9, 10),
text="Devoir commun",
html="<p>Devoir commun</p>",
)
lesson = Lesson(
id="lesson1",
start=datetime(2026, 9, 10, 8, 0),
end=datetime(2026, 9, 10, 9, 0),
subject="Math",
teachers=("M. Martin", "M. Dupont"),
group=None,
content=None,
homework_blocks=(homework_block,),
)
other_lesson = lesson.model_copy(
update={"id": "lesson2", "teachers": ("M. Dupont", "M. Martin")}
)
homeworks = collect_homeworks([lesson, other_lesson], target_date=date(2026, 9, 10))
assert len(homeworks) == 1
def test_parse_body_accepts_html_variants_and_sanitizes() -> None:
"""Les attributs HTML, les espaces et le contenu actif sont traités correctement.
:return: None
"""
body = (
'<strong class="label"> Contenu pédagogique : </strong><p>Leçon &amp; exemple</p>'
'<strong data-kind="homework"> Pour le 10/09/2026 : </strong>'
'<p onclick="evil()">Lire <em>le chapitre</em></p><script>alert(1)</script>'
"<strong> Donné le 05/09/2026 : </strong><p>Noter &amp; relire</p>"
)
content, due_blocks, assigned_blocks = parse_body(body)
assert content == "Leçon & exemple"
assert due_blocks[0].date == date(2026, 9, 10)
assert due_blocks[0].text == "Lire le chapitre"
assert "<em>le chapitre</em>" in due_blocks[0].html
assert "onclick" not in due_blocks[0].html
assert "script" not in due_blocks[0].html.lower()
assert assigned_blocks[0].date == date(2026, 9, 5)
def test_parse_anonymized_variants_fixture() -> None:
"""Vérifie le corpus iCal anonymisé sur les statuts et le contexte des devoirs.
:return: None
"""
fixture_path = Path(__file__).parent.parent / "fixtures" / "pronote-variants.ics"
lessons, _, school_events = parse_ical(fixture_path.read_text(encoding="utf-8"))
assert lessons[0].class_part == "Groupe 1"
assert lessons[0].homework_blocks[0].html == "<p>Lire le chapitre</p>"
assert any(lesson.status == LessonStatus.MOVED for lesson in lessons)
assert any(event.kind.value == "public_holiday" for event in school_events)
assert any(
event.from_date == date(2026, 10, 20) and event.to_date == date(2026, 10, 22)
for event in school_events
)
homeworks = collect_homeworks(lessons, target_date=date(2026, 9, 15))
assert len(homeworks) == 2
assert {homework.subject for homework in homeworks} == {"Mathématiques", "Sciences"}
def test_collect_homeworks_sorted() -> None: