Refactor (Welle D): Lektions-Auflösung in ProgressService zentralisieren

- 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 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-22 12:52:54 +02:00
commit 8eb7984468
3 changed files with 91 additions and 17 deletions

View file

@ -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:
"""

View file

@ -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)

View file

@ -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()