Revue Codex validée. Intégration dans main de la chaîne des PR #31, #32 et #33. Co-authored-by: Codex <codex@antoineve.me> Co-committed-by: Codex <codex@antoineve.me>
73 lines
2.9 KiB
Python
73 lines
2.9 KiB
Python
"""Règles communes de statut et de consolidation des cours Pronote."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import unicodedata
|
|
from collections import defaultdict
|
|
|
|
from ...models.agenda import Lesson, LessonStatus
|
|
|
|
|
|
def _normalize_status(value: str) -> str:
|
|
"""Normalise un statut Pronote pour une comparaison tolérante.
|
|
|
|
:param value: Statut brut fourni par Pronote.
|
|
:return: Statut minuscule sans accents.
|
|
:rtype: str
|
|
"""
|
|
decomposed = unicodedata.normalize("NFKD", value)
|
|
without_accents = "".join(char for char in decomposed if not unicodedata.combining(char))
|
|
return " ".join(without_accents.casefold().split())
|
|
|
|
|
|
def lesson_status(raw_status: object, canceled: bool = False) -> LessonStatus:
|
|
"""Convertit le statut Pronote en statut métier canonique.
|
|
|
|
L'indicateur d'annulation et les libellés annulé sont prioritaires sur un
|
|
éventuel libellé de déplacement. Les libellés inconnus restent normaux.
|
|
|
|
:param raw_status: Valeur du champ ``Lesson.status`` de pronotepy.
|
|
:param canceled: Indicateur ``Lesson.canceled`` de pronotepy.
|
|
:return: Statut métier canonique.
|
|
:rtype: LessonStatus
|
|
"""
|
|
status = _normalize_status(raw_status) if isinstance(raw_status, str) else ""
|
|
if canceled or "annul" in status:
|
|
return LessonStatus.CANCELLED
|
|
if any(token in status for token in ("deplac", "changement de salle", "modifi")):
|
|
return LessonStatus.MOVED
|
|
return LessonStatus.NORMAL
|
|
|
|
|
|
def collapse_replaced_lessons(lessons: list[Lesson]) -> list[Lesson]:
|
|
"""Supprime l'ancien exemplaire annulé lorsqu'un cours déplacé est présent.
|
|
|
|
Deux cours sont considérés comme les exemplaires d'une même modification
|
|
lorsqu'ils partagent les dates, la matière, les enseignants et le groupe,
|
|
et qu'une seule annulation et un seul déplacement se distinguent par leur
|
|
salle. Une annulation isolée ou ambiguë est toujours conservée.
|
|
|
|
:param lessons: Cours issus d'une source Pronote.
|
|
:return: Cours consolidés dans leur ordre d'origine.
|
|
:rtype: list[Lesson]
|
|
"""
|
|
grouped: dict[tuple[object, ...], list[tuple[int, Lesson]]] = defaultdict(list)
|
|
for index, lesson in enumerate(lessons):
|
|
key = (
|
|
lesson.start,
|
|
lesson.end,
|
|
lesson.subject.casefold(),
|
|
tuple(teacher.casefold() for teacher in lesson.teachers),
|
|
(lesson.group or "").casefold(),
|
|
)
|
|
grouped[key].append((index, lesson))
|
|
|
|
replaced_indexes: set[int] = set()
|
|
for siblings in grouped.values():
|
|
cancelled = [item for item in siblings if item[1].status == LessonStatus.CANCELLED]
|
|
moved = [item for item in siblings if item[1].status == LessonStatus.MOVED]
|
|
if len(cancelled) == 1 and len(moved) == 1 and cancelled[0][1].rooms != moved[0][1].rooms:
|
|
replaced_indexes.add(cancelled[0][0])
|
|
|
|
return [lesson for index, lesson in enumerate(lessons) if index not in replaced_indexes]
|