fix(ical): fiabiliser le parsing Pronote 2026

This commit is contained in:
2026-09-12 13:33:58 +02:00
parent f194ed985d
commit e63000230b
9 changed files with 368 additions and 79 deletions
+53
View File
@@ -0,0 +1,53 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//pronote-sync tests//FR
X-WR-CALNAME:Fixture anonymisée
BEGIN:VEVENT
UID:variant-normal
DTSTART:20260910T080000Z
DTEND:20260910T090000Z
SUMMARY:Mathématiques
CATEGORIES:Cours
DESCRIPTION:<div>Matière : Mathématiques Professeur(s) : Enseignant A Salle(s) : 101 Groupe : Classe entière Partie(s) de classe : Groupe 1 <strong data-section="content">Contenu pédagogique :</strong><p>Leçon &amp; exemple</p><strong>Pour le 15/09/2026 :</strong><p>Lire le chapitre</p><strong>Donné le 10/09/2026 :</strong><p>Lire le chapitre</p></div>
END:VEVENT
BEGIN:VEVENT
UID:variant-duplicate
DTSTART:20260910T100000Z
DTEND:20260910T110000Z
SUMMARY:Mathématiques
CATEGORIES:Cours
DESCRIPTION:<div>Matière : Mathématiques Professeur(s) : Enseignant A Salle(s) : 102 <strong>Pour le 15/09/2026 :</strong><p>Lire le chapitre</p></div>
END:VEVENT
BEGIN:VEVENT
UID:variant-same-text-other-context
DTSTART:20260910T120000Z
DTEND:20260910T130000Z
SUMMARY:Sciences
CATEGORIES:Cours
DESCRIPTION:<div>Matière : Sciences Professeur(s) : Enseignant B Salle(s) : 103 <strong>Pour le 15/09/2026 :</strong><p>Lire le chapitre</p></div>
END:VEVENT
BEGIN:VEVENT
UID:variant-moved
DTSTART:20260911T080000Z
DTEND:20260911T090000Z
SUMMARY:Mathématiques
CATEGORIES:Cours - Cours modifié
DESCRIPTION:<div>Matière : Mathématiques Professeur : Enseignant A Salle : 204</div>
END:VEVENT
BEGIN:VEVENT
UID:variant-public-holiday
DTSTART;VALUE=DATE:20260914
DTEND;VALUE=DATE:20260915
SUMMARY:Journée fériée anonymisée
CATEGORIES:Jours fériés
DESCRIPTION:Journée sans cours.
END:VEVENT
BEGIN:VEVENT
UID:variant-holiday
DTSTART;VALUE=DATE:20261020
DTEND;VALUE=DATE:20261022
SUMMARY:Vacances anonymisées
CATEGORIES:Congés
DESCRIPTION:Période de vacances.
END:VEVENT
END:VCALENDAR
+19
View File
@@ -0,0 +1,19 @@
{
"version": "fixture-1",
"source": "ical",
"pronote_version": "2026-unknown",
"anonymized": true,
"scenarios": {
"normal_and_headers": "variant-normal",
"duplicate_homework_same_context": ["variant-normal", "variant-duplicate"],
"same_text_distinct_context": "variant-same-text-other-context",
"moved_lesson": "variant-moved",
"public_holiday": "variant-public-holiday",
"holiday_exclusive_end": "variant-holiday"
},
"invariants": [
"Aucune valeur d'authentification ou identité réelle n'est présente.",
"Les bornes de date d'un événement scolaire sont début inclusif et fin exclusive.",
"Les devoirs sont vérifiés après parsing dans les modèles métier finaux."
]
}
+85 -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,87 @@ 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_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: