From 8eb7984468fd21de8d6dbadc50372b3be0507f94 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Mon, 22 Jun 2026 12:52:54 +0200 Subject: [PATCH] =?UTF-8?q?Refactor=20(Welle=20D):=20Lektions-Aufl=C3=B6su?= =?UTF-8?q?ng=20in=20ProgressService=20zentralisieren?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Neue ProgressService.resolve_current_lesson_index(): kapselt Resume- Fallback (UserSettings) + Index-Validierung + Sync des globalen Progress an einer benannten, getesteten Stelle. Rollen der drei Zustandsfelder dokumentiert (kein Schema-Bruch). - get_training_context() nutzt die Methode; die verstreute Heuristik und die Inline-'from models import db'-Importe entfallen. - 5 Unit-Tests für die Auflösungslogik (Resume, Zeiger-Vorrang, Normalisierung ungültiger/out-of-range Indizes). Gesamt: 96 Tests grün, ruff clean. Co-Authored-By: Claude Sonnet 4.6 --- services/progress_service.py | 38 ++++++++++++++++++++++++ services/training_service.py | 23 ++++----------- tests/test_integration_progress.py | 47 ++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 17 deletions(-) diff --git a/services/progress_service.py b/services/progress_service.py index 2bd879c..92ea41f 100644 --- a/services/progress_service.py +++ b/services/progress_service.py @@ -57,6 +57,44 @@ class ProgressService: db.session.commit() logger.info(f"Globalen Lektionsindex auf {lesson_index} gesetzt") + @staticmethod + def resolve_current_lesson_index(persisted_index: int, lesson_count: int) -> int: + """Ermittelt die aktuelle Lektion und synchronisiert den globalen Progress. + + Rollen der drei Zustandsquellen: + - Progress.current_text_index: Laufzeit-Zeiger (Quelle der Wahrheit) + - UserSettings.current_lesson_index: persistenter Fallback (Resume) + - LessonProgress: pro-Lektion-Fortschritt (orthogonal, kein Konflikt) + + Logik: Ist der Laufzeit-Zeiger im Anfangszustand (0), wird die zuletzt + persistierte Lektion als Resume-Fallback übernommen. Ein ungültiger + Index wird auf 0 normalisiert. Das Ergebnis wird in den globalen + Progress zurückgeschrieben. + + Args: + persisted_index: Zuletzt persistierte Lektion (aus UserSettings) + lesson_count: Anzahl verfügbarer Lektionen + + Returns: + Der ermittelte, gültige Lektionsindex + """ + progress = ProgressService.get_or_create_global_progress() + index = progress.current_text_index + + # Resume: persistierte Lektion übernehmen, wenn Zeiger initial ist + if index == 0 and persisted_index > 0: + index = persisted_index + + # Ungültigen Index normalisieren + if not (0 <= index < lesson_count): + index = 0 + + if index != progress.current_text_index: + progress.current_text_index = index + db.session.commit() + + return index + @staticmethod def get_or_create_lesson_progress(lesson_index: int) -> LessonProgress: """ diff --git a/services/training_service.py b/services/training_service.py index 6495a23..7d5747b 100644 --- a/services/training_service.py +++ b/services/training_service.py @@ -36,26 +36,15 @@ class TrainingService: Returns: Dictionary with all training data """ - # Get global progress - progress = self.progress_service.get_or_create_global_progress() - current_lesson_index = progress.current_text_index - # Get user settings user_settings_dict = self.settings_service.get_settings_dict() - # Restore saved lesson if initial - if current_lesson_index == 0 and user_settings_dict['current_lesson_index'] > 0: - current_lesson_index = user_settings_dict['current_lesson_index'] - progress.current_text_index = current_lesson_index - from models import db - db.session.commit() - - # Validate lesson index - if not self.lesson_service.validate_lesson_index(current_lesson_index): - current_lesson_index = 0 - progress.current_text_index = 0 - from models import db - db.session.commit() + # Aktuelle Lektion ermitteln und globalen Progress synchronisieren. + # Kapselt Resume-Fallback + Validierung an einer Stelle. + current_lesson_index = self.progress_service.resolve_current_lesson_index( + persisted_index=user_settings_dict['current_lesson_index'], + lesson_count=self.lesson_service.get_lesson_count(), + ) # Get lesson info lesson_info = self.lesson_service.get_lesson_info(current_lesson_index) diff --git a/tests/test_integration_progress.py b/tests/test_integration_progress.py index 7352d17..7aed9b4 100644 --- a/tests/test_integration_progress.py +++ b/tests/test_integration_progress.py @@ -255,5 +255,52 @@ class TestProgressServiceIntegration(unittest.TestCase): self.assertEqual(lesson3.current_position, 30) # Unverändert +class TestResolveCurrentLessonIndex(unittest.TestCase): + """Tests für die zentrale Lektions-Auflösung (Resume + Validierung).""" + + def setUp(self): + app.config['TESTING'] = True + app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:' + app.config['WTF_CSRF_ENABLED'] = False + self.app_context = app.app_context() + self.app_context.push() + db.create_all() + + def tearDown(self): + db.session.remove() + db.drop_all() + self.app_context.pop() + + def test_initial_pointer_no_persisted(self): + # Zeiger 0, nichts persistiert -> 0 + self.assertEqual( + ProgressService.resolve_current_lesson_index(persisted_index=0, lesson_count=5), 0 + ) + + def test_resume_from_persisted_when_initial(self): + # Zeiger 0, persistiert 3 -> Resume auf 3 und in Progress geschrieben + result = ProgressService.resolve_current_lesson_index(persisted_index=3, lesson_count=5) + self.assertEqual(result, 3) + self.assertEqual(Progress.query.first().current_text_index, 3) + + def test_runtime_pointer_wins_over_persisted(self): + # Laufender Zeiger 2 gewinnt gegenüber persistiert 4 (kein Resume) + ProgressService.set_current_lesson_index(2) + result = ProgressService.resolve_current_lesson_index(persisted_index=4, lesson_count=5) + self.assertEqual(result, 2) + + def test_invalid_index_normalized_to_zero(self): + # Out-of-range Zeiger wird auf 0 normalisiert + ProgressService.set_current_lesson_index(10) + result = ProgressService.resolve_current_lesson_index(persisted_index=0, lesson_count=5) + self.assertEqual(result, 0) + self.assertEqual(Progress.query.first().current_text_index, 0) + + def test_out_of_range_persisted_normalized(self): + # Resume nimmt persistiert 99, Validierung normalisiert auf 0 + result = ProgressService.resolve_current_lesson_index(persisted_index=99, lesson_count=5) + self.assertEqual(result, 0) + + if __name__ == '__main__': unittest.main()