diff --git a/app_new/core/services/room_planning.py b/app_new/core/services/room_planning.py index 76db9b5..a26cc40 100644 --- a/app_new/core/services/room_planning.py +++ b/app_new/core/services/room_planning.py @@ -79,7 +79,7 @@ def available_slots(room_id, day, window_start=time(8), window_end=time(17), dur return slots -def sync_pronote_schedules(room_id, entries, *, synced_at=None): +def sync_pronote_schedules(room_id, entries, *, synced_at=None, week_start=None): """Applique un lot Pronote sans toucher aux créneaux manuels protégés. ``entries`` est une liste de dictionnaires normalisés par l'intégration @@ -87,6 +87,9 @@ def sync_pronote_schedules(room_id, entries, *, synced_at=None): libellés optionnels). Cette fonction ne contacte jamais Pronote elle-même. """ synced_at = synced_at or datetime.utcnow() + synced_weeks = {entry.get("week_start") for entry in entries if entry.get("week_start")} + if week_start: + synced_weeks.add(week_start) seen = set() for entry in entries: external_id = str(entry.get("external_id") or "").strip() @@ -140,7 +143,10 @@ def sync_pronote_schedules(room_id, entries, *, synced_at=None): note = "Conflit avec un créneau manuel protégé" manual.conflict_note = note row.conflict_note = note - existing = RoomSchedule.query.filter_by(room_id=room_id, source="pronote").all() + existing_query = RoomSchedule.query.filter_by(room_id=room_id, source="pronote") + if synced_weeks: + existing_query = existing_query.filter(RoomSchedule.week_start.in_(synced_weeks)) + existing = existing_query.all() for row in existing: if row.external_id not in seen: row.resolution_status = "disabled" diff --git a/app_new/pronote/routes.py b/app_new/pronote/routes.py index 839e190..a6c8771 100644 --- a/app_new/pronote/routes.py +++ b/app_new/pronote/routes.py @@ -207,7 +207,7 @@ def import_room_planning(room_id): try: lessons = get_lessons_for_room(client, room.name, monday, monday + timedelta(days=6), strict=True) entries = normalize_pronote_lessons(lessons, monday) - sync_pronote_schedules(room.id, entries) + sync_pronote_schedules(room.id, entries, week_start=monday) flash(f'{len(entries)} créneaux Pronote synchronisés. Les protections locales ont été conservées.', 'success') except Exception: db.session.rollback() diff --git a/tests/integration/test_phase1_documents_planning.py b/tests/integration/test_phase1_documents_planning.py index 2345e7a..8bcfa77 100644 --- a/tests/integration/test_phase1_documents_planning.py +++ b/tests/integration/test_phase1_documents_planning.py @@ -172,6 +172,30 @@ def test_pronote_sync_change_removal_ambiguity_and_freshness(app): assert db.session.get(RoomSchedule, row.id) is not None +def test_pronote_sync_removal_is_scoped_to_imported_week(app): + with app.app_context(): + building = Building(name="Phase15 scope building") + db.session.add(building) + db.session.flush() + room = Room(name="Phase15 scope room", building_id=building.id) + db.session.add(room) + db.session.flush() + current_week, next_week = date(2026, 9, 7), date(2026, 9, 14) + current = RoomSchedule(room_id=room.id, week_start=current_week, day_of_week=0, + start_time=time(8), end_time=time(9), source="pronote", + external_id="scope-current") + future = RoomSchedule(room_id=room.id, week_start=next_week, day_of_week=0, + start_time=time(8), end_time=time(9), source="pronote", + external_id="scope-future") + db.session.add_all([current, future]) + db.session.commit() + sync_pronote_schedules(room.id, [], week_start=current_week) + db.session.refresh(current) + db.session.refresh(future) + assert current.resolution_status == "disabled" + assert future.resolution_status == "active" + + def test_normalize_pronote_lessons_is_stable_and_skips_invalid_entries(): week = date(2026, 9, 7) lessons = [{