From 6575a5f60b054390ce58c73a20d86974f6e76848 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 12 Sep 2026 15:43:12 +0200 Subject: [PATCH] fix(caldav): rendre les devoirs transparents --- pronote_sync/sync/serialization.py | 23 ++++--- tests/integration/test_caldav_sync.py | 90 +++++++++++++++++++++++++-- tests/unit/test_sync_serialization.py | 13 ++-- 3 files changed, 108 insertions(+), 18 deletions(-) diff --git a/pronote_sync/sync/serialization.py b/pronote_sync/sync/serialization.py index 399bbeb..c3e63fa 100644 --- a/pronote_sync/sync/serialization.py +++ b/pronote_sync/sync/serialization.py @@ -14,7 +14,7 @@ l'outil et de ne jamais toucher aux événements étrangers du calendrier. from __future__ import annotations -from datetime import datetime, time +from datetime import datetime, timedelta from typing import cast from icalendar import Calendar, Component, Event, vDate, vDatetime @@ -33,7 +33,15 @@ MANAGED_VALUE = "v1" PRODID = "-//pronote-sync//NONSGML v1.0//EN" #: Propriétés prises en compte dans la signature sémantique d'un composant. -_SIGNATURE_KEYS: tuple[str, ...] = ("UID", "SUMMARY", "DTSTART", "DTEND", "STATUS", "DESCRIPTION") +_SIGNATURE_KEYS: tuple[str, ...] = ( + "UID", + "SUMMARY", + "DTSTART", + "DTEND", + "STATUS", + "TRANSP", + "DESCRIPTION", +) def lesson_to_vevent(lesson: Lesson) -> Event: @@ -88,8 +96,9 @@ def lesson_to_vevent(lesson: Lesson) -> Event: def homework_to_vevent(homework: Homework) -> Event: """Convertit un devoir Pronote en composant VEVENT iCalendar. - Le devoir est représenté comme une tâche (``STATUS:NEEDS-ACTION``) sur la - journée d'échéance, entre 08:00 et 18:00. + Le devoir est représenté comme un événement toute la journée à la date + d'échéance. Il est transparent pour ne pas bloquer les disponibilités ; + aucun statut de tâche ``VTODO`` n'est ajouté à ce ``VEVENT``. :param homework: Devoir Pronote à sérialiser. :return: Composant :class:`icalendar.Event` marqué comme géré par l'outil. @@ -98,10 +107,10 @@ def homework_to_vevent(homework: Homework) -> Event: event = Event() event.add("uid", f"homework-{homework.id}") event.add("summary", f"Devoir: {homework.subject}") - event.add("dtstart", vDatetime(datetime.combine(homework.due_on, time(8, 0)))) - event.add("dtend", vDatetime(datetime.combine(homework.due_on, time(18, 0)))) + event.add("dtstart", vDate(homework.due_on)) + event.add("dtend", vDate(homework.due_on + timedelta(days=1))) event.add("description", homework.text) - event.add("status", "NEEDS-ACTION") + event.add("transp", "TRANSPARENT") event.add("categories", ["Pronote", "Devoir"]) event.add(MANAGED_PROPERTY, MANAGED_VALUE) return event diff --git a/tests/integration/test_caldav_sync.py b/tests/integration/test_caldav_sync.py index 8f4f4e1..488b39e 100644 --- a/tests/integration/test_caldav_sync.py +++ b/tests/integration/test_caldav_sync.py @@ -7,7 +7,7 @@ ajouts/mises à jour/suppressions, et la préservation des événements non gér from __future__ import annotations -from datetime import datetime +from datetime import date, datetime from typing import TYPE_CHECKING, Any import pytest @@ -122,10 +122,16 @@ class FakeCalendar: event_start = raw_start.dt event_end = raw_end.dt if raw_end is not None else event_start overlaps = True - if start is not None: - overlaps = overlaps and event_end > start - if end is not None: - overlaps = overlaps and event_start < end + if isinstance(event_start, datetime): + if start is not None: + overlaps = overlaps and event_end > start + if end is not None: + overlaps = overlaps and event_start < end + else: + if start is not None: + overlaps = overlaps and event_end > start.date() + if end is not None: + overlaps = overlaps and event_start < end.date() if overlaps: results.append(FakeCalendarEvent(ical_text, uid=uid, server=self._server)) break @@ -686,6 +692,80 @@ class TestCalDAVSynchronize: assert result.added == 1 assert len(fake_caldav_server.get_events()) == 1 + def test_legacy_homework_vevent_is_migrated_idempotently_and_removed( + self, + fake_caldav_server: FakeCalDAVServer, + full_settings: Settings, + ) -> None: + """Migre un ancien devoir puis vérifie l'idempotence et la suppression.""" + fake_caldav_server._events["homework-hw-legacy"] = _create_vevent_text( + uid="homework-hw-legacy", + summary="Devoir: Histoire", + start=datetime(2026, 1, 20, 8, 0), + end=datetime(2026, 1, 20, 18, 0), + status="NEEDS-ACTION", + managed=True, + ) + homework = Homework( + id="hw-legacy", + subject="Histoire", + assigned_on=None, + due_on=date(2026, 1, 20), + text="Lire le chapitre 5", + ) + + def make_data(homeworks: list[Homework]) -> PronoteData: + """Construit les données de synchronisation du scénario.""" + return PronoteData( + lessons=[], + homeworks=homeworks, + school_events=[], + messages=[], + target_date=date(2026, 1, 20), + generated_at=datetime(2026, 1, 14, 0, 0), + ) + + first = synchronize( + pronote_data=make_data([homework]), + settings=full_settings, + client_factory=fake_caldav_server.client_factory, + now=datetime(2026, 1, 14, 12, 0), + ) + assert first.status == CalDAVSyncStatus.SUCCESS + assert first.added == 0 + assert first.updated == 1 + assert first.removed == 0 + + migrated = Calendar.from_ical(fake_caldav_server.get_events()["homework-hw-legacy"]) + event = migrated.walk("VEVENT")[0] + assert event.get("DTSTART").dt == date(2026, 1, 20) + assert event.get("DTEND").dt == date(2026, 1, 21) + assert event.get("STATUS") is None + assert str(event.get("TRANSP")) == "TRANSPARENT" + + second = synchronize( + pronote_data=make_data([homework]), + settings=full_settings, + client_factory=fake_caldav_server.client_factory, + now=datetime(2026, 1, 14, 12, 0), + ) + assert second.status == CalDAVSyncStatus.SKIPPED + assert second.added == 0 + assert second.updated == 0 + assert second.removed == 0 + + removed = synchronize( + pronote_data=make_data([]), + settings=full_settings, + client_factory=fake_caldav_server.client_factory, + now=datetime(2026, 1, 14, 12, 0), + ) + assert removed.status == CalDAVSyncStatus.SUCCESS + assert removed.added == 0 + assert removed.updated == 0 + assert removed.removed == 1 + assert fake_caldav_server.get_events() == {} + def test_school_event_sync( self, fake_caldav_server: FakeCalDAVServer, diff --git a/tests/unit/test_sync_serialization.py b/tests/unit/test_sync_serialization.py index 3a364a1..5c8b54f 100644 --- a/tests/unit/test_sync_serialization.py +++ b/tests/unit/test_sync_serialization.py @@ -239,15 +239,16 @@ def test_homework_to_vevent_uid_prefix() -> None: assert str(event.get("UID")) == "homework-HW-5678" -def test_homework_to_vevent_status() -> None: - """Vérifie qu'un devoir a STATUS=NEEDS-ACTION. +def test_homework_to_vevent_is_transparent_without_task_status() -> None: + """Vérifie qu'un devoir VEVENT est transparent et sans statut VTODO. :return: None """ homework = _make_homework() event = homework_to_vevent(homework) - assert str(event.get("STATUS")) == "NEEDS-ACTION" + assert event.get("STATUS") is None + assert str(event.get("TRANSP")) == "TRANSPARENT" def test_homework_to_vevent_categories() -> None: @@ -265,15 +266,15 @@ def test_homework_to_vevent_categories() -> None: def test_homework_to_vevent_dtstart_dtend() -> None: - """Vérifie que DTSTART et DTEND couvrent la journée d'échéance (08:00-18:00). + """Vérifie que DTSTART et DTEND encadrent la journée d'échéance. :return: None """ homework = _make_homework(due_on=date(2026, 1, 20)) event = homework_to_vevent(homework) - assert event.get("DTSTART").dt == datetime(2026, 1, 20, 8, 0) - assert event.get("DTEND").dt == datetime(2026, 1, 20, 18, 0) + assert event.get("DTSTART").dt == date(2026, 1, 20) + assert event.get("DTEND").dt == date(2026, 1, 21) def test_homework_to_vevent_summary() -> None: -- 2.47.3