Phase 2: Service Layer Pattern implementiert

Umfassende Refactorierung mit Service Layer für bessere Wartbarkeit:

Neue Service-Klassen (services/):
- lesson_service.py - Lektionsverwaltung und Validierung
- progress_service.py - Fortschrittsverwaltung (global + pro Lektion)
- statistics_service.py - Statistik-Berechnungen und -Speicherung
- settings_service.py - Einstellungsverwaltung mit Validierung
- training_service.py - High-Level Training-Orchestrierung

Refactorierte Blueprints (routes/):
- training.py: Von 485 auf 202 Zeilen (~58% Reduktion)
- statistics.py: Verwendet Services für alle Operationen
- settings.py: Verwendet Services mit Validierung
- session.py: Verwendet Services für Session-Management

Template-Fixes:
- index.html: url_for('main.hilfe') für Blueprint-Routing
- hilfe.html: url_for('training.train') für Blueprint-Routing

Vorteile der Service Layer:
- Business Logic wiederverwendbar und testbar
- Routes sind schlanker und fokussieren auf HTTP
- Klare Trennung: Routes = HTTP, Services = Business Logic
- Einfacheres Unit Testing möglich
- Bessere Code-Organisation und Wartbarkeit

Alle 23 Routes getestet und funktionieren korrekt.
Code-Reduktion in Routes: ~40% bei gleichzeitiger Verbesserung der Struktur.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
jamulix 2025-10-29 04:35:59 +01:00
commit 2ef3809e42
11 changed files with 1174 additions and 393 deletions

View file

@ -4,40 +4,42 @@ Handles session management routes
"""
import logging
from datetime import date, timedelta
from flask import Blueprint, render_template, request, jsonify
from models import db, Progress
from services.helpers import update_daily_practice, get_last_30_days_practice, save_user_settings
from services.statistics_service import StatisticsService
from services.settings_service import SettingsService
from services.progress_service import ProgressService
logger = logging.getLogger(__name__)
session_bp = Blueprint('session', __name__)
# Initialize services
statistics_service = StatisticsService()
settings_service = SettingsService()
progress_service = ProgressService()
@session_bp.route('/end_session', methods=['POST'])
def end_session():
"""Beendet eine Tipp-Session und speichert Statistiken"""
from models import UserSettings, DailyPractice
try:
data = request.get_json()
if not data:
return jsonify({'error': 'Keine Daten empfangen'}), 400
# Hole die aktuelle Session-Zeit (in Sekunden)
# Get session time
total_elapsed_time = float(data.get('total_elapsed_time', 0))
minutes_practiced = total_elapsed_time / 60.0
# Aktualisiere die tägliche Übungszeit
# Update daily practice
if minutes_practiced > 0:
update_daily_practice(db, DailyPractice, minutes_practiced)
statistics_service.update_daily_practice(minutes_practiced)
# Speichere alle aktuellen Einstellungen
progress = Progress.query.first()
current_lesson = progress.current_text_index if progress else 0
# Get current lesson
current_lesson = progress_service.get_current_lesson_index()
save_user_settings(
db,
UserSettings,
# Save all settings
settings_service.update_settings(
current_lesson_index=current_lesson,
metronome_enabled=data.get('metronome_enabled', False),
metronome_bpm=data.get('metronome_bpm', 60),
@ -55,6 +57,7 @@ def end_session():
'message': 'Session beendet',
'redirect': '/session_end'
})
except Exception as e:
logger.error(f"Fehler in end_session: {e}", exc_info=True)
return jsonify({'error': 'Interner Serverfehler'}), 500
@ -63,29 +66,11 @@ def end_session():
@session_bp.route('/session_end')
def session_end():
"""Zeigt die Endseite mit Übungsstatistiken der letzten 30 Tage"""
from models import DailyPractice
try:
# Hole die letzten 30 Tage
practice_data = get_last_30_days_practice(DailyPractice)
# Get full 30 days practice data
full_data = statistics_service.get_full_30_days_practice()
# Erstelle vollständige Daten für alle 30 Tage (auch Tage ohne Übung)
today = date.today()
full_data = []
for i in range(29, -1, -1): # Von 29 Tagen zurück bis heute
current_date = today - timedelta(days=i)
date_str = current_date.strftime('%Y-%m-%d')
# Suche nach Daten für diesen Tag
matching = next((p for p in practice_data if p['date'] == date_str), None)
full_data.append({
'date': date_str,
'display_date': current_date.strftime('%d.%m'),
'minutes': matching['minutes'] if matching else 0
})
# Berechne Gesamtstatistiken
# Calculate total statistics
total_minutes = sum(d['minutes'] for d in full_data)
days_practiced = sum(1 for d in full_data if d['minutes'] > 0)
@ -93,6 +78,10 @@ def session_end():
practice_data=full_data,
total_minutes=total_minutes,
days_practiced=days_practiced)
except Exception as e:
logger.error(f"Fehler in session_end: {e}", exc_info=True)
return render_template('session_end.html', practice_data=[], total_minutes=0, days_practiced=0)
return render_template('session_end.html',
practice_data=[],
total_minutes=0,
days_practiced=0)

View file

@ -5,26 +5,28 @@ Handles all settings-related routes
import logging
from flask import Blueprint, render_template, request, jsonify, session
from models import db, Progress
from services.helpers import get_or_create_user_settings, save_user_settings
from services.settings_service import SettingsService
from services.progress_service import ProgressService
logger = logging.getLogger(__name__)
settings_bp = Blueprint('settings', __name__)
# Initialize services
settings_service = SettingsService()
progress_service = ProgressService()
@settings_bp.route('/settings')
def settings():
"""Einstellungsseite"""
from models import UserSettings
# Lade Einstellungen aus Datenbank mit Defaults
user_settings = get_or_create_user_settings(db, UserSettings)
settings_dict = settings_service.get_settings_dict()
current_settings = {
'metronome_mode': user_settings.metronome_mode,
'metronome_speed': user_settings.metronome_speed,
'target_error_rate': user_settings.target_error_rate,
'max_bpm_speed': getattr(user_settings, 'max_bpm_speed', 100),
'speed_display': user_settings.speed_display
'metronome_mode': settings_dict['metronome_mode'],
'metronome_speed': settings_dict['metronome_speed'],
'target_error_rate': settings_dict['target_error_rate'],
'max_bpm_speed': settings_dict['max_bpm_speed'],
'speed_display': settings_dict['speed_display']
}
return render_template('settings.html', settings=current_settings)
@ -32,18 +34,17 @@ def settings():
@settings_bp.route('/save_settings', methods=['POST'])
def save_settings_route():
"""Speichert die Einstellungen persistent in der Datenbank"""
from models import UserSettings
try:
data = request.get_json()
if not data:
return jsonify({'error': 'Keine Daten empfangen'}), 400
# Validiere metronome_mode
# Extract and validate settings
metronome_mode = data.get('metronome_mode', 'automatic')
if metronome_mode not in ['automatic', 'explicit']:
if not settings_service.validate_metronome_mode(metronome_mode):
return jsonify({'error': 'Ungültiger Metronom-Modus'}), 400
# Validiere numerische Werte
# Validate numeric values
try:
metronome_speed = int(data.get('metronome_speed', 60))
target_error_rate = int(data.get('target_error_rate', 5))
@ -51,30 +52,28 @@ def save_settings_route():
except (ValueError, TypeError):
return jsonify({'error': 'Ungültige numerische Werte'}), 400
# Bereichsvalidierung
if not (20 <= metronome_speed <= 300):
# Validate ranges
if not settings_service.validate_metronome_speed(metronome_speed):
return jsonify({'error': 'Metronom-Geschwindigkeit muss zwischen 20 und 300 liegen'}), 400
if not (0 <= target_error_rate <= 100):
if not settings_service.validate_target_error_rate(target_error_rate):
return jsonify({'error': 'Fehlerrate muss zwischen 0 und 100 liegen'}), 400
if not (40 <= max_bpm_speed <= 200):
if not settings_service.validate_max_bpm_speed(max_bpm_speed):
return jsonify({'error': 'Maximale BPM-Geschwindigkeit muss zwischen 40 und 200 liegen'}), 400
# Validiere speed_display
# Validate speed display
speed_display = data.get('speed_display', 'zpm')
if speed_display not in ['zpm', 'wpm']:
if not settings_service.validate_speed_display(speed_display):
return jsonify({'error': 'Ungültige Geschwindigkeitsanzeige'}), 400
# Speichere in Datenbank
progress = Progress.query.first()
current_lesson = progress.current_text_index if progress else 0
# Get current lesson
current_lesson = progress_service.get_current_lesson_index()
save_user_settings(
db,
UserSettings,
# Update settings
settings_service.update_settings(
current_lesson_index=current_lesson,
metronome_enabled=True, # Metronome bleibt aktiviert
metronome_enabled=True,
metronome_bpm=metronome_speed,
metronome_sound='beep',
metronome_mode=metronome_mode,
@ -86,6 +85,7 @@ def save_settings_route():
logger.info(f"Einstellungen gespeichert: mode={metronome_mode}, speed={metronome_speed}, max_bpm={max_bpm_speed}")
return jsonify({'status': 'success'})
except Exception as e:
logger.error(f"Fehler in save_settings: {e}", exc_info=True)
return jsonify({'error': 'Interner Serverfehler'}), 500
@ -99,7 +99,7 @@ def set_metronome():
if not data:
return jsonify({'error': 'Keine Daten empfangen'}), 400
# Validierung
# Validation
enabled = bool(data.get('enabled', False))
try:
@ -107,20 +107,22 @@ def set_metronome():
except (ValueError, TypeError):
return jsonify({'error': 'Ungültiger BPM-Wert'}), 400
# Erweiterte Validierung mit detaillierten Fehlermeldungen
# Extended validation
if bpm < 20:
return jsonify({'error': 'BPM muss mindestens 20 sein'}), 400
if bpm > 300:
return jsonify({'error': 'BPM darf maximal 300 sein'}), 400
sound = data.get('sound', 'beep')
if sound not in ['beep', 'click', 'woodblock']:
sound = 'beep' # Fallback auf Standard
if not settings_service.validate_metronome_sound(sound):
sound = 'beep' # Fallback
session['metronome_enabled'] = enabled
session['metronome_bpm'] = bpm
session['metronome_sound'] = sound
return jsonify({'status': 'success'})
except Exception as e:
logger.error(f"Fehler in set_metronome: {e}", exc_info=True)
return jsonify({'error': 'Interner Serverfehler'}), 500

View file

@ -9,29 +9,36 @@ import json
from io import StringIO
from datetime import datetime
from flask import Blueprint, render_template, request, jsonify, session, Response, make_response
from models import db, Progress, LessonStatistic
from models import db
from services.helpers import lessons
from services.statistics_service import StatisticsService
from services.progress_service import ProgressService
from services.lesson_service import LessonService
logger = logging.getLogger(__name__)
statistics_bp = Blueprint('statistics', __name__)
# Initialize services
statistics_service = StatisticsService()
progress_service = ProgressService()
lesson_service = LessonService(lessons)
@statistics_bp.route('/statistics')
def statistics():
"""Zeigt die Statistik-Seite mit Diagrammen"""
try:
# Hole den aktuellen Fortschritt für die voreingestellte Lektion
progress = Progress.query.first()
current_lesson_index = progress.current_text_index if progress else 0
# Get current lesson index
current_lesson_index = progress_service.get_current_lesson_index()
# Prüfe für jede Lektion, ob Statistik-Daten vorhanden sind
lesson_has_data = []
for i in range(len(lessons)):
has_data = LessonStatistic.query.filter_by(lesson_index=i).first() is not None
lesson_has_data.append(has_data)
# Check which lessons have statistics
lesson_has_data = [
statistics_service.has_lesson_statistics(i)
for i in range(lesson_service.get_lesson_count())
]
# Hole die Einstellung für die Geschwindigkeitsanzeige aus der Session
# Get speed display setting
speed_display = session.get('speed_display', 'zpm')
return render_template('statistics.html',
@ -52,19 +59,9 @@ def statistics():
def get_lesson_statistics(lesson_index: int) -> Response:
"""Gibt die Statistiken für eine bestimmte Lektion zurück"""
try:
stats = LessonStatistic.query.filter_by(lesson_index=lesson_index)\
.order_by(LessonStatistic.created_at.asc())\
.all()
data = {
'dates': [stat.created_at.strftime('%Y-%m-%d') for stat in stats],
'chars_per_minute': [stat.chars_per_minute for stat in stats],
'error_rate': [stat.error_rate for stat in stats],
'wpm': [stat.wpm for stat in stats],
'training_duration': [stat.training_duration for stat in stats]
}
data = statistics_service.get_lesson_statistics_data(lesson_index)
return jsonify(data)
except Exception as e:
logger.error(f"Fehler in get_lesson_statistics: {e}", exc_info=True)
return jsonify({'error': 'Interner Serverfehler'}), 500
@ -87,14 +84,11 @@ def reset_lesson_statistics():
except (ValueError, TypeError):
return jsonify({'error': 'Ungültiger Lektionsindex'}), 400
if lesson_index < 0 or lesson_index >= len(lessons):
if not lesson_service.validate_lesson_index(lesson_index):
return jsonify({'error': 'Lektionsindex außerhalb des gültigen Bereichs'}), 400
# Lösche alle Statistiken für diese Lektion
deleted_count = LessonStatistic.query.filter_by(lesson_index=lesson_index).delete()
db.session.commit()
logger.info(f"{deleted_count} Statistik-Einträge für Lektion {lesson_index} gelöscht")
# Delete statistics
deleted_count = statistics_service.delete_lesson_statistics(lesson_index)
return jsonify({
'status': 'success',
@ -113,24 +107,19 @@ def reset_lesson_statistics():
def export_statistics_csv() -> Response:
"""Exportiert alle Statistiken als CSV-Datei"""
try:
# Hole alle Statistiken
stats = LessonStatistic.query.order_by(
LessonStatistic.lesson_index.asc(),
LessonStatistic.created_at.asc()
).all()
# Get all statistics
stats = statistics_service.get_all_statistics()
# CSV erstellen
# Create CSV
output = StringIO()
writer = csv.writer(output)
# Header
writer.writerow(['Datum', 'Lektion-Nr', 'Lektionstitel', 'Zeichen/Min', 'Fehlerrate %', 'Wörter/Min'])
# Daten
# Data
for stat in stats:
lesson_title = "Unbekannt"
if stat.lesson_index < len(lessons):
lesson_title = lessons[stat.lesson_index].get('title', f"Lektion {stat.lesson_index + 1}")
lesson_title = lesson_service.get_lesson_title(stat.lesson_index)
writer.writerow([
stat.created_at.strftime('%Y-%m-%d %H:%M:%S'),
@ -141,7 +130,7 @@ def export_statistics_csv() -> Response:
f"{stat.wpm:.2f}"
])
# Response erstellen
# Create response
response = make_response(output.getvalue())
response.headers['Content-Type'] = 'text/csv; charset=utf-8'
response.headers['Content-Disposition'] = 'attachment; filename=typewriter_statistiken.csv'
@ -158,13 +147,10 @@ def export_statistics_csv() -> Response:
def export_statistics_json() -> Response:
"""Exportiert alle Statistiken als JSON-Datei"""
try:
# Hole alle Statistiken
stats = LessonStatistic.query.order_by(
LessonStatistic.lesson_index.asc(),
LessonStatistic.created_at.asc()
).all()
# Get all statistics
stats = statistics_service.get_all_statistics()
# JSON-Daten erstellen
# Create JSON data
export_data = {
'export_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'total_entries': len(stats),
@ -172,9 +158,7 @@ def export_statistics_json() -> Response:
}
for stat in stats:
lesson_title = "Unbekannt"
if stat.lesson_index < len(lessons):
lesson_title = lessons[stat.lesson_index].get('title', f"Lektion {stat.lesson_index + 1}")
lesson_title = lesson_service.get_lesson_title(stat.lesson_index)
export_data['statistics'].append({
'date': stat.created_at.strftime('%Y-%m-%d %H:%M:%S'),
@ -185,7 +169,7 @@ def export_statistics_json() -> Response:
'words_per_minute': round(stat.wpm, 2)
})
# Response erstellen
# Create response
response = make_response(json.dumps(export_data, ensure_ascii=False, indent=2))
response.headers['Content-Type'] = 'application/json; charset=utf-8'
response.headers['Content-Disposition'] = 'attachment; filename=typewriter_statistiken.json'

View file

@ -4,121 +4,31 @@ Handles all training-related routes
"""
import logging
from flask import Blueprint, render_template, request, jsonify, session
from models import db, Progress, LessonStatistic, LessonProgress, UserSettings
from services.helpers import (
lessons,
get_or_create_user_settings,
get_or_create_lesson_progress,
calculate_statistics
)
from flask import Blueprint, render_template, request, jsonify
from models import db
from services.helpers import lessons
from services.lesson_service import LessonService
from services.training_service import TrainingService
logger = logging.getLogger(__name__)
training_bp = Blueprint('training', __name__)
# Initialize services
lesson_service = LessonService(lessons)
training_service = TrainingService(lesson_service)
@training_bp.route('/train')
def train():
"""Hauptseite des Tipptrainers"""
try:
# Hole den globalen Fortschritt aus der Datenbank (speichert nur current_text_index)
progress = Progress.query.first()
if not progress:
# Falls keine Daten vorhanden sind, initialisiere
progress = Progress(
current_text_index=0,
current_position=0,
last_text=""
)
db.session.add(progress)
db.session.commit()
logger.info("Neuen globalen Fortschritt initialisiert")
# Lade gespeicherte Benutzereinstellungen aus der Datenbank
user_settings = get_or_create_user_settings(db, UserSettings)
# Bestimme die aktuelle Lektion
current_lesson_index = progress.current_text_index
# Wenn eine gespeicherte Lektion in UserSettings existiert und der Progress noch initial ist,
# verwende die gespeicherte Lektion
if current_lesson_index == 0 and user_settings.current_lesson_index > 0:
current_lesson_index = user_settings.current_lesson_index
progress.current_text_index = current_lesson_index
db.session.commit()
# Validiere Lektionsindex
if not lessons or current_lesson_index >= len(lessons):
current_lesson_index = 0
progress.current_text_index = 0
db.session.commit()
# Hole den aktuellen Text
current_text = lessons[current_lesson_index]['text'] if lessons else "Keine Lektionen verfügbar"
# Lade lektionsspezifische Fortschrittsdaten
lesson_progress = get_or_create_lesson_progress(db, LessonProgress, current_lesson_index)
# Daten aus LessonProgress
user_input = lesson_progress.last_text
cursor_position = lesson_progress.current_position
total_elapsed_time = lesson_progress.total_elapsed_time or 0
key_stroke_count = lesson_progress.key_stroke_count or 0
is_paused = lesson_progress.is_paused if lesson_progress.is_paused is not None else True
# Netto-Trainingszeit (wird in JavaScript SessionManager verwaltet)
net_training_time = total_elapsed_time
logger.debug(f"Lade Lektion {current_lesson_index}: Position={cursor_position}, Eingabe-Länge={len(user_input)}")
# Bestimme den Lektionstitel
current_lesson_title = "Unbekannte Lektion"
if lessons and current_lesson_index < len(lessons):
lesson = lessons[current_lesson_index]
current_lesson_title = lesson.get('title', f"Lektion {current_lesson_index + 1}")
# Metronom-Einstellungen aus Datenbank laden
metronome_enabled = user_settings.metronome_enabled
metronome_bpm = user_settings.metronome_bpm
metronome_sound = user_settings.metronome_sound
metronome_mode = user_settings.metronome_mode
metronome_speed = user_settings.metronome_speed
target_error_rate = user_settings.target_error_rate
max_bpm_speed = getattr(user_settings, 'max_bpm_speed', 100) # Fallback für Kompatibilität
speed_display = user_settings.speed_display
# Synchronisiere Session mit DB
session['total_elapsed_time'] = total_elapsed_time
session['net_training_time'] = net_training_time
session['key_stroke_count'] = key_stroke_count
session['is_paused'] = is_paused
return render_template('index.html',
current_text=current_text,
current_position=cursor_position,
last_text=user_input,
total_elapsed_time=total_elapsed_time,
net_training_time=net_training_time,
key_stroke_count=key_stroke_count,
is_paused=is_paused,
lessons=lessons,
current_lesson_title=current_lesson_title,
current_lesson_index=current_lesson_index,
metronome_enabled=metronome_enabled,
metronome_bpm=metronome_bpm,
metronome_sound=metronome_sound,
metronome_mode=metronome_mode,
metronome_speed=metronome_speed,
target_error_rate=target_error_rate,
max_bpm_speed=max_bpm_speed,
speed_display=speed_display)
context = training_service.get_training_context()
return render_template('index.html', **context)
except Exception as e:
logger.error(f"Fehler in train(): {e}", exc_info=True)
# Auch im Fehlerfall Metronom-Standardwerte übergeben
# Fehlerfall mit Standardwerten
return render_template('index.html',
current_text="Fehler beim Laden der Lektionen",
current_position=0,
@ -160,69 +70,21 @@ def update_progress():
is_paused = bool(data.get('is_paused', False))
# Hole den globalen Fortschritt (für lesson_index)
progress = Progress.query.first()
if not progress:
return jsonify({'error': 'Progress nicht gefunden'}), 400
try:
# Hole lektionsspezifischen Fortschritt
lesson_index = progress.current_text_index
lesson_progress = get_or_create_lesson_progress(db, LessonProgress, lesson_index)
# Aktualisiere lektionsspezifische Daten
lesson_progress.current_position = cursor_position
lesson_progress.last_text = user_input
lesson_progress.total_elapsed_time = int(total_elapsed_time)
lesson_progress.key_stroke_count = key_stroke_count
lesson_progress.is_paused = is_paused
# Prüfe, ob die Lektion abgeschlossen ist
lesson_completed = cursor_position >= len(current_text) and len(user_input) == len(current_text)
if lesson_completed:
# Speichere Statistik für abgeschlossene Lektion
logger.info(f"Lektion {lesson_index} abgeschlossen! Speichere Statistik...")
# Berechne finale Statistiken
stats = calculate_statistics(user_input, current_text, elapsed_time)
# Speichere in LessonStatistic
lesson_stat = LessonStatistic(
lesson_index=lesson_index,
chars_per_minute=stats['typing_speed'],
error_rate=stats['error_rate'],
wpm=stats['words_per_minute'],
training_duration=total_elapsed_time / 1000.0 # Millisekunden zu Sekunden
)
db.session.add(lesson_stat)
# Setze LessonProgress zurück
lesson_progress.current_position = 0
lesson_progress.last_text = ""
lesson_progress.total_elapsed_time = 0
lesson_progress.key_stroke_count = 0
lesson_progress.is_paused = True
logger.info(f"Lektion {lesson_index} zurückgesetzt")
# Speichere in der Datenbank
db.session.commit()
# Speichere zusätzliche Zustandsdaten auch in der Session (für Kompatibilität)
session['total_elapsed_time'] = total_elapsed_time
session['net_training_time'] = net_training_time
session['key_stroke_count'] = key_stroke_count
session['is_paused'] = is_paused
# Berechne Statistiken mit echter Zeit
stats = calculate_statistics(user_input, current_text, elapsed_time)
# Füge lesson_completed zu Response hinzu
stats['lesson_completed'] = lesson_completed
# Update progress and get statistics
stats = training_service.update_training_progress(
user_input=user_input,
current_text=current_text,
elapsed_time=elapsed_time,
cursor_position=cursor_position,
total_elapsed_time=total_elapsed_time,
net_training_time=net_training_time,
key_stroke_count=key_stroke_count,
is_paused=is_paused
)
return jsonify(stats)
except Exception as db_error:
db.session.rollback()
logger.error(f"Datenbank-Fehler beim Speichern des Fortschritts: {db_error}", exc_info=True)
@ -237,52 +99,8 @@ def update_progress():
def next_text():
"""Wechselt zum nächsten Text"""
try:
progress = Progress.query.first()
if not progress:
return jsonify({'error': 'Progress nicht gefunden'}), 400
if not lessons:
return jsonify({'error': 'Keine Lektionen verfügbar'}), 400
# Wechsel zum nächsten Text
old_lesson_index = progress.current_text_index
if progress.current_text_index < len(lessons) - 1:
progress.current_text_index += 1
else:
# Wenn wir am Ende sind, starten wir von vorne
progress.current_text_index = 0
new_lesson_index = progress.current_text_index
db.session.commit()
# Lade LessonProgress für neue Lektion (wird automatisch erstellt falls nicht vorhanden)
lesson_progress = get_or_create_lesson_progress(db, LessonProgress, new_lesson_index)
# Synchronisiere Session mit der neuen Lektion
session['total_elapsed_time'] = lesson_progress.total_elapsed_time
session['net_training_time'] = lesson_progress.total_elapsed_time
session['key_stroke_count'] = lesson_progress.key_stroke_count
session['is_paused'] = lesson_progress.is_paused
# Bestimme den Titel der neuen Lektion
new_lesson = lessons[new_lesson_index]
lesson_title = new_lesson.get('title', f"Lektion {new_lesson_index + 1}")
logger.info(f"Lektionswechsel: {old_lesson_index} -> {new_lesson_index}")
return jsonify({
'text': new_lesson['text'],
'index': new_lesson_index,
'total': len(lessons),
'title': lesson_title,
'current_position': lesson_progress.current_position,
'last_text': lesson_progress.last_text,
'total_elapsed_time': lesson_progress.total_elapsed_time,
'key_stroke_count': lesson_progress.key_stroke_count,
'is_paused': lesson_progress.is_paused
})
result = training_service.switch_to_next_lesson()
return jsonify(result)
except Exception as e:
logger.error(f"Fehler in next_text: {e}", exc_info=True)
@ -299,43 +117,12 @@ def set_lesson():
if lesson_index is None or not isinstance(lesson_index, int):
return jsonify({'error': 'Ungültiger Lektionsindex'}), 400
if lesson_index < 0 or lesson_index >= len(lessons):
return jsonify({'error': 'Lektionsindex außerhalb des gültigen Bereichs'}), 400
success, result = training_service.switch_to_lesson(lesson_index)
progress = Progress.query.first()
if not progress:
return jsonify({'error': 'Progress nicht gefunden'}), 400
if not success:
return jsonify(result), 400
old_lesson_index = progress.current_text_index
# Setze neue Lektion
progress.current_text_index = lesson_index
db.session.commit()
# Lade LessonProgress für neue Lektion (wird automatisch erstellt falls nicht vorhanden)
lesson_progress = get_or_create_lesson_progress(db, LessonProgress, lesson_index)
# Synchronisiere Session mit der neuen Lektion
session['total_elapsed_time'] = lesson_progress.total_elapsed_time
session['net_training_time'] = lesson_progress.total_elapsed_time
session['key_stroke_count'] = lesson_progress.key_stroke_count
session['is_paused'] = lesson_progress.is_paused
# Bestimme den Titel der neuen Lektion
new_lesson = lessons[lesson_index]
lesson_title = new_lesson.get('title', f"Lektion {lesson_index + 1}")
logger.info(f"Lektionswechsel: {old_lesson_index} -> {lesson_index}")
return jsonify({
'text': new_lesson['text'],
'title': lesson_title,
'current_position': lesson_progress.current_position,
'last_text': lesson_progress.last_text,
'total_elapsed_time': lesson_progress.total_elapsed_time,
'key_stroke_count': lesson_progress.key_stroke_count,
'is_paused': lesson_progress.is_paused
})
return jsonify(result)
except Exception as e:
logger.error(f"Fehler in set_lesson: {e}", exc_info=True)
@ -345,6 +132,7 @@ def set_lesson():
@training_bp.route('/save_lesson_statistics', methods=['POST'])
def save_lesson_statistics():
"""Speichert die Statistiken für eine abgeschlossene Lektion"""
from services.statistics_service import StatisticsService
try:
data = request.get_json()
if not data:
@ -375,11 +163,8 @@ def save_lesson_statistics():
if error_rate > 100:
return jsonify({'error': 'Fehlerrate kann nicht über 100% sein'}), 400
logger.debug(f"Empfangene Statistik: lesson_index={lesson_index}, chars_per_minute={chars_per_minute}, error_rate={error_rate}, wpm={wpm}, training_duration={training_duration}")
# Neue Statistik für die Lektion erstellen
try:
lesson_stat = LessonStatistic(
StatisticsService.save_lesson_statistics(
lesson_index=lesson_index,
chars_per_minute=chars_per_minute,
error_rate=error_rate,
@ -387,12 +172,8 @@ def save_lesson_statistics():
training_duration=training_duration
)
db.session.add(lesson_stat)
db.session.commit()
logger.info("Statistik erfolgreich in Datenbank gespeichert")
return jsonify({'status': 'success', 'message': 'Lektionsstatistiken gespeichert'})
except Exception as db_error:
db.session.rollback()
logger.error(f"Datenbank-Fehler beim Speichern der Statistik: {db_error}", exc_info=True)
@ -407,32 +188,12 @@ def save_lesson_statistics():
def reset_lesson():
"""Setzt den Fortschritt der aktuellen Lektion zurück"""
try:
# Hole den globalen Fortschritt
progress = Progress.query.first()
if not progress:
return jsonify({'error': 'Progress nicht gefunden'}), 400
success, result = training_service.reset_current_lesson()
lesson_index = progress.current_text_index
if not success:
return jsonify(result), 400
# Hole oder erstelle LessonProgress für aktuelle Lektion
lesson_progress = get_or_create_lesson_progress(db, LessonProgress, lesson_index)
# Setze alle Werte zurück
lesson_progress.current_position = 0
lesson_progress.last_text = ""
lesson_progress.total_elapsed_time = 0
lesson_progress.key_stroke_count = 0
lesson_progress.is_paused = True
db.session.commit()
logger.info(f"Lektion {lesson_index} wurde zurückgesetzt")
return jsonify({
'status': 'success',
'message': 'Lektion zurückgesetzt',
'lesson_index': lesson_index
})
return jsonify(result)
except Exception as e:
db.session.rollback()

150
services/lesson_service.py Normal file
View file

@ -0,0 +1,150 @@
"""
Lesson Service
Handles all lesson-related business logic
"""
import logging
from typing import List, Dict, Any, Optional
logger = logging.getLogger(__name__)
class LessonService:
"""Service for managing lessons"""
def __init__(self, lessons: List[Dict[str, Any]]):
"""
Initialize LessonService with lessons data
Args:
lessons: List of lesson dictionaries
"""
self.lessons = lessons
def get_all_lessons(self) -> List[Dict[str, Any]]:
"""
Get all lessons
Returns:
List of all lessons
"""
return self.lessons
def get_lesson_count(self) -> int:
"""
Get total number of lessons
Returns:
Number of lessons
"""
return len(self.lessons)
def get_lesson_by_index(self, index: int) -> Optional[Dict[str, Any]]:
"""
Get a specific lesson by index
Args:
index: Lesson index
Returns:
Lesson dictionary or None if invalid index
"""
if 0 <= index < len(self.lessons):
return self.lessons[index]
return None
def get_lesson_text(self, index: int) -> str:
"""
Get lesson text by index
Args:
index: Lesson index
Returns:
Lesson text or error message
"""
lesson = self.get_lesson_by_index(index)
if lesson:
return lesson.get('text', 'Kein Text verfügbar')
return 'Lektion nicht gefunden'
def get_lesson_title(self, index: int) -> str:
"""
Get lesson title by index
Args:
index: Lesson index
Returns:
Lesson title or default
"""
lesson = self.get_lesson_by_index(index)
if lesson:
return lesson.get('title', f'Lektion {index + 1}')
return 'Unbekannte Lektion'
def validate_lesson_index(self, index: int) -> bool:
"""
Validate if lesson index is valid
Args:
index: Lesson index to validate
Returns:
True if valid, False otherwise
"""
return 0 <= index < len(self.lessons)
def get_next_lesson_index(self, current_index: int) -> int:
"""
Get next lesson index (wraps around to 0 at end)
Args:
current_index: Current lesson index
Returns:
Next lesson index
"""
if current_index < len(self.lessons) - 1:
return current_index + 1
return 0
def get_previous_lesson_index(self, current_index: int) -> int:
"""
Get previous lesson index (wraps around to last at beginning)
Args:
current_index: Current lesson index
Returns:
Previous lesson index
"""
if current_index > 0:
return current_index - 1
return len(self.lessons) - 1
def get_lesson_info(self, index: int) -> Dict[str, Any]:
"""
Get complete lesson information
Args:
index: Lesson index
Returns:
Dictionary with lesson info
"""
lesson = self.get_lesson_by_index(index)
if not lesson:
return {
'index': index,
'text': 'Lektion nicht gefunden',
'title': 'Unbekannte Lektion',
'total': len(self.lessons)
}
return {
'index': index,
'text': lesson.get('text', ''),
'title': self.get_lesson_title(index),
'total': len(self.lessons)
}

View file

@ -0,0 +1,183 @@
"""
Progress Service
Handles all progress and lesson progress related business logic
"""
import logging
from typing import Dict, Any, Optional
from models import db, Progress, LessonProgress
logger = logging.getLogger(__name__)
class ProgressService:
"""Service for managing user progress"""
@staticmethod
def get_or_create_global_progress() -> Progress:
"""
Get or create global progress entry
Returns:
Progress instance
"""
progress = Progress.query.first()
if not progress:
progress = Progress(
current_text_index=0,
current_position=0,
last_text=""
)
db.session.add(progress)
db.session.commit()
logger.info("Neuen globalen Fortschritt initialisiert")
return progress
@staticmethod
def get_current_lesson_index() -> int:
"""
Get current lesson index from global progress
Returns:
Current lesson index
"""
progress = ProgressService.get_or_create_global_progress()
return progress.current_text_index
@staticmethod
def set_current_lesson_index(lesson_index: int) -> None:
"""
Set current lesson index in global progress
Args:
lesson_index: New lesson index
"""
progress = ProgressService.get_or_create_global_progress()
progress.current_text_index = lesson_index
db.session.commit()
logger.info(f"Globalen Lektionsindex auf {lesson_index} gesetzt")
@staticmethod
def get_or_create_lesson_progress(lesson_index: int) -> LessonProgress:
"""
Get or create lesson progress for specific lesson
Args:
lesson_index: Lesson index
Returns:
LessonProgress instance
"""
lesson_progress = LessonProgress.query.filter_by(lesson_index=lesson_index).first()
if not lesson_progress:
lesson_progress = LessonProgress(
lesson_index=lesson_index,
current_position=0,
last_text="",
total_elapsed_time=0,
key_stroke_count=0,
is_paused=True
)
db.session.add(lesson_progress)
db.session.commit()
logger.info(f"Neuen Lektions-Fortschritt für Lektion {lesson_index} erstellt")
return lesson_progress
@staticmethod
def get_lesson_progress_data(lesson_index: int) -> Dict[str, Any]:
"""
Get lesson progress data as dictionary
Args:
lesson_index: Lesson index
Returns:
Dictionary with progress data
"""
lesson_progress = ProgressService.get_or_create_lesson_progress(lesson_index)
return {
'current_position': lesson_progress.current_position,
'last_text': lesson_progress.last_text,
'total_elapsed_time': lesson_progress.total_elapsed_time,
'key_stroke_count': lesson_progress.key_stroke_count,
'is_paused': lesson_progress.is_paused if lesson_progress.is_paused is not None else True
}
@staticmethod
def update_lesson_progress(
lesson_index: int,
cursor_position: int,
user_input: str,
total_elapsed_time: int,
key_stroke_count: int,
is_paused: bool
) -> LessonProgress:
"""
Update lesson progress with new data
Args:
lesson_index: Lesson index
cursor_position: Current cursor position
user_input: User's typed text
total_elapsed_time: Total elapsed time in milliseconds
key_stroke_count: Number of keystrokes
is_paused: Whether session is paused
Returns:
Updated LessonProgress instance
"""
lesson_progress = ProgressService.get_or_create_lesson_progress(lesson_index)
lesson_progress.current_position = cursor_position
lesson_progress.last_text = user_input
lesson_progress.total_elapsed_time = int(total_elapsed_time)
lesson_progress.key_stroke_count = key_stroke_count
lesson_progress.is_paused = is_paused
db.session.commit()
return lesson_progress
@staticmethod
def reset_lesson_progress(lesson_index: int) -> None:
"""
Reset progress for a specific lesson
Args:
lesson_index: Lesson index to reset
"""
lesson_progress = ProgressService.get_or_create_lesson_progress(lesson_index)
lesson_progress.current_position = 0
lesson_progress.last_text = ""
lesson_progress.total_elapsed_time = 0
lesson_progress.key_stroke_count = 0
lesson_progress.is_paused = True
db.session.commit()
logger.info(f"Lektion {lesson_index} wurde zurückgesetzt")
@staticmethod
def check_lesson_completed(
cursor_position: int,
user_input_length: int,
lesson_text_length: int
) -> bool:
"""
Check if lesson is completed
Args:
cursor_position: Current cursor position
user_input_length: Length of user input
lesson_text_length: Length of lesson text
Returns:
True if lesson is completed
"""
return cursor_position >= lesson_text_length and user_input_length == lesson_text_length
@staticmethod
def complete_lesson(lesson_index: int) -> None:
"""
Mark lesson as completed and reset progress
Args:
lesson_index: Lesson index
"""
ProgressService.reset_lesson_progress(lesson_index)
logger.info(f"Lektion {lesson_index} abgeschlossen und zurückgesetzt")

View file

@ -0,0 +1,206 @@
"""
Settings Service
Handles all user settings related business logic
"""
import logging
from typing import Dict, Any
from models import db, UserSettings
logger = logging.getLogger(__name__)
class SettingsService:
"""Service for managing user settings"""
@staticmethod
def get_or_create_user_settings() -> UserSettings:
"""
Get or create user settings entry
Returns:
UserSettings instance
"""
settings = UserSettings.query.first()
if not settings:
settings = UserSettings()
db.session.add(settings)
db.session.commit()
logger.info("Neue Benutzereinstellungen erstellt")
return settings
@staticmethod
def get_settings_dict() -> Dict[str, Any]:
"""
Get user settings as dictionary
Returns:
Dictionary with all settings
"""
settings = SettingsService.get_or_create_user_settings()
return {
'current_lesson_index': settings.current_lesson_index,
'metronome_enabled': settings.metronome_enabled,
'metronome_bpm': settings.metronome_bpm,
'metronome_sound': settings.metronome_sound,
'metronome_mode': settings.metronome_mode,
'metronome_speed': settings.metronome_speed,
'target_error_rate': settings.target_error_rate,
'max_bpm_speed': getattr(settings, 'max_bpm_speed', 100),
'speed_display': settings.speed_display
}
@staticmethod
def update_settings(
current_lesson_index: int = None,
metronome_enabled: bool = None,
metronome_bpm: int = None,
metronome_sound: str = None,
metronome_mode: str = None,
metronome_speed: int = None,
target_error_rate: int = None,
max_bpm_speed: int = None,
speed_display: str = None
) -> UserSettings:
"""
Update user settings
Args:
current_lesson_index: Current lesson index
metronome_enabled: Whether metronome is enabled
metronome_bpm: Metronome BPM
metronome_sound: Metronome sound type
metronome_mode: Metronome mode (automatic/explicit)
metronome_speed: Metronome speed
target_error_rate: Target error rate percentage
max_bpm_speed: Maximum BPM speed
speed_display: Speed display mode (zpm/wpm)
Returns:
Updated UserSettings instance
"""
settings = SettingsService.get_or_create_user_settings()
if current_lesson_index is not None:
settings.current_lesson_index = current_lesson_index
if metronome_enabled is not None:
settings.metronome_enabled = metronome_enabled
if metronome_bpm is not None:
settings.metronome_bpm = metronome_bpm
if metronome_sound is not None:
settings.metronome_sound = metronome_sound
if metronome_mode is not None:
settings.metronome_mode = metronome_mode
if metronome_speed is not None:
settings.metronome_speed = metronome_speed
if target_error_rate is not None:
settings.target_error_rate = target_error_rate
if max_bpm_speed is not None:
settings.max_bpm_speed = max_bpm_speed
if speed_display is not None:
settings.speed_display = speed_display
db.session.commit()
logger.info("Benutzereinstellungen aktualisiert")
return settings
@staticmethod
def save_all_settings(settings_data: Dict[str, Any]) -> UserSettings:
"""
Save all settings from dictionary
Args:
settings_data: Dictionary with settings data
Returns:
Updated UserSettings instance
"""
return SettingsService.update_settings(
current_lesson_index=settings_data.get('current_lesson_index'),
metronome_enabled=settings_data.get('metronome_enabled'),
metronome_bpm=settings_data.get('metronome_bpm'),
metronome_sound=settings_data.get('metronome_sound'),
metronome_mode=settings_data.get('metronome_mode'),
metronome_speed=settings_data.get('metronome_speed'),
target_error_rate=settings_data.get('target_error_rate'),
max_bpm_speed=settings_data.get('max_bpm_speed'),
speed_display=settings_data.get('speed_display')
)
@staticmethod
def validate_metronome_mode(mode: str) -> bool:
"""
Validate metronome mode
Args:
mode: Mode to validate
Returns:
True if valid
"""
return mode in ['automatic', 'explicit']
@staticmethod
def validate_metronome_speed(speed: int) -> bool:
"""
Validate metronome speed
Args:
speed: Speed to validate
Returns:
True if valid
"""
return 20 <= speed <= 300
@staticmethod
def validate_target_error_rate(rate: int) -> bool:
"""
Validate target error rate
Args:
rate: Error rate to validate
Returns:
True if valid
"""
return 0 <= rate <= 100
@staticmethod
def validate_max_bpm_speed(speed: int) -> bool:
"""
Validate max BPM speed
Args:
speed: Speed to validate
Returns:
True if valid
"""
return 40 <= speed <= 200
@staticmethod
def validate_speed_display(display: str) -> bool:
"""
Validate speed display mode
Args:
display: Display mode to validate
Returns:
True if valid
"""
return display in ['zpm', 'wpm']
@staticmethod
def validate_metronome_sound(sound: str) -> bool:
"""
Validate metronome sound
Args:
sound: Sound to validate
Returns:
True if valid
"""
return sound in ['beep', 'click', 'woodblock']

View file

@ -0,0 +1,250 @@
"""
Statistics Service
Handles all statistics-related business logic
"""
import logging
from typing import Dict, Any, List
from datetime import date, timedelta
from models import db, LessonStatistic, DailyPractice
logger = logging.getLogger(__name__)
class StatisticsService:
"""Service for managing statistics"""
@staticmethod
def calculate_typing_statistics(
user_input: str,
current_text: str,
elapsed_time: float
) -> Dict[str, Any]:
"""
Calculate typing statistics based on user input
Args:
user_input: User's typed text
current_text: Target text
elapsed_time: Time elapsed in seconds
Returns:
Dictionary with calculated statistics
"""
correct_chars = 0
incorrect_chars = 0
total_chars = len(user_input)
# Count correct and incorrect characters
for i, char in enumerate(user_input):
if i < len(current_text) and char == current_text[i]:
correct_chars += 1
else:
incorrect_chars += 1
# Calculate metrics based on real time
# Minimum 1 second to avoid unrealistic values
if total_chars > 0 and elapsed_time >= 1.0:
# Characters per minute (integer)
chars_per_minute = (total_chars / elapsed_time) * 60
# Words per minute: (correct characters / 5) / (time in minutes)
words_per_minute = (correct_chars / 5) / (elapsed_time / 60)
# Average time per character
duration_per_char = elapsed_time / total_chars
# Limit to realistic maximum values
chars_per_minute = min(chars_per_minute, 1000)
words_per_minute = min(words_per_minute, 200)
else:
chars_per_minute = 0
words_per_minute = 0
duration_per_char = 0
# Error rate and accuracy
error_rate = (incorrect_chars / total_chars * 100) if total_chars > 0 else 0
accuracy = (correct_chars / total_chars * 100) if total_chars > 0 else 0
return {
'correct_chars': correct_chars,
'incorrect_chars': incorrect_chars,
'total_chars': total_chars,
'error_rate': round(error_rate, 1),
'typing_speed': round(chars_per_minute),
'duration_per_char': round(duration_per_char, 3),
'words_per_minute': round(words_per_minute, 2),
'accuracy': round(accuracy, 2)
}
@staticmethod
def save_lesson_statistics(
lesson_index: int,
chars_per_minute: float,
error_rate: float,
wpm: float,
training_duration: float
) -> LessonStatistic:
"""
Save statistics for a completed lesson
Args:
lesson_index: Lesson index
chars_per_minute: Characters per minute
error_rate: Error rate percentage
wpm: Words per minute
training_duration: Training duration in seconds
Returns:
Created LessonStatistic instance
"""
lesson_stat = LessonStatistic(
lesson_index=lesson_index,
chars_per_minute=chars_per_minute,
error_rate=error_rate,
wpm=wpm,
training_duration=training_duration
)
db.session.add(lesson_stat)
db.session.commit()
logger.info(f"Statistik für Lektion {lesson_index} gespeichert")
return lesson_stat
@staticmethod
def get_lesson_statistics(lesson_index: int) -> List[LessonStatistic]:
"""
Get all statistics for a specific lesson
Args:
lesson_index: Lesson index
Returns:
List of LessonStatistic instances
"""
return LessonStatistic.query.filter_by(lesson_index=lesson_index)\
.order_by(LessonStatistic.created_at.asc())\
.all()
@staticmethod
def get_lesson_statistics_data(lesson_index: int) -> Dict[str, List]:
"""
Get lesson statistics as formatted data for charts
Args:
lesson_index: Lesson index
Returns:
Dictionary with statistics data arrays
"""
stats = StatisticsService.get_lesson_statistics(lesson_index)
return {
'dates': [stat.created_at.strftime('%Y-%m-%d') for stat in stats],
'chars_per_minute': [stat.chars_per_minute for stat in stats],
'error_rate': [stat.error_rate for stat in stats],
'wpm': [stat.wpm for stat in stats],
'training_duration': [stat.training_duration for stat in stats]
}
@staticmethod
def has_lesson_statistics(lesson_index: int) -> bool:
"""
Check if lesson has any statistics
Args:
lesson_index: Lesson index
Returns:
True if lesson has statistics
"""
return LessonStatistic.query.filter_by(lesson_index=lesson_index).first() is not None
@staticmethod
def delete_lesson_statistics(lesson_index: int) -> int:
"""
Delete all statistics for a lesson
Args:
lesson_index: Lesson index
Returns:
Number of deleted entries
"""
deleted_count = LessonStatistic.query.filter_by(lesson_index=lesson_index).delete()
db.session.commit()
logger.info(f"{deleted_count} Statistik-Einträge für Lektion {lesson_index} gelöscht")
return deleted_count
@staticmethod
def get_all_statistics() -> List[LessonStatistic]:
"""
Get all statistics ordered by lesson and creation date
Returns:
List of all LessonStatistic instances
"""
return LessonStatistic.query.order_by(
LessonStatistic.lesson_index.asc(),
LessonStatistic.created_at.asc()
).all()
@staticmethod
def update_daily_practice(minutes: float) -> None:
"""
Update daily practice time
Args:
minutes: Minutes practiced
"""
today = date.today()
practice = DailyPractice.query.filter_by(date=today).first()
if practice:
practice.minutes_practiced += minutes
else:
practice = DailyPractice(date=today, minutes_practiced=minutes)
db.session.add(practice)
db.session.commit()
logger.info(f"Tägliche Übungszeit aktualisiert: {minutes:.2f} Minuten")
@staticmethod
def get_last_30_days_practice() -> List[Dict[str, Any]]:
"""
Get practice times for last 30 days
Returns:
List of practice data dictionaries
"""
thirty_days_ago = date.today() - timedelta(days=29)
practices = DailyPractice.query.filter(
DailyPractice.date >= thirty_days_ago
).order_by(DailyPractice.date.asc()).all()
return [
{'date': p.date.strftime('%Y-%m-%d'), 'minutes': p.minutes_practiced}
for p in practices
]
@staticmethod
def get_full_30_days_practice() -> List[Dict[str, Any]]:
"""
Get complete 30 days of practice data (including days without practice)
Returns:
List with all 30 days including zeros
"""
practice_data = StatisticsService.get_last_30_days_practice()
today = date.today()
full_data = []
for i in range(29, -1, -1): # From 29 days ago to today
current_date = today - timedelta(days=i)
date_str = current_date.strftime('%Y-%m-%d')
# Find data for this day
matching = next((p for p in practice_data if p['date'] == date_str), None)
full_data.append({
'date': date_str,
'display_date': current_date.strftime('%d.%m'),
'minutes': matching['minutes'] if matching else 0
})
return full_data

View file

@ -0,0 +1,256 @@
"""
Training Service
Orchestrates training-related business logic using other services
"""
import logging
from typing import Dict, Any, Tuple
from flask import session
from services.lesson_service import LessonService
from services.progress_service import ProgressService
from services.statistics_service import StatisticsService
from services.settings_service import SettingsService
logger = logging.getLogger(__name__)
class TrainingService:
"""High-level service for training operations"""
def __init__(self, lesson_service: LessonService):
"""
Initialize TrainingService
Args:
lesson_service: LessonService instance
"""
self.lesson_service = lesson_service
self.progress_service = ProgressService()
self.statistics_service = StatisticsService()
self.settings_service = SettingsService()
def get_training_context(self) -> Dict[str, Any]:
"""
Get complete training context for rendering training page
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()
# Get lesson info
lesson_info = self.lesson_service.get_lesson_info(current_lesson_index)
# Get lesson progress
progress_data = self.progress_service.get_lesson_progress_data(current_lesson_index)
# Synchronize session with DB
session['total_elapsed_time'] = progress_data['total_elapsed_time']
session['net_training_time'] = progress_data['total_elapsed_time']
session['key_stroke_count'] = progress_data['key_stroke_count']
session['is_paused'] = progress_data['is_paused']
return {
'current_text': lesson_info['text'],
'current_position': progress_data['current_position'],
'last_text': progress_data['last_text'],
'total_elapsed_time': progress_data['total_elapsed_time'],
'net_training_time': progress_data['total_elapsed_time'],
'key_stroke_count': progress_data['key_stroke_count'],
'is_paused': progress_data['is_paused'],
'lessons': self.lesson_service.get_all_lessons(),
'current_lesson_title': lesson_info['title'],
'current_lesson_index': current_lesson_index,
'metronome_enabled': user_settings_dict['metronome_enabled'],
'metronome_bpm': user_settings_dict['metronome_bpm'],
'metronome_sound': user_settings_dict['metronome_sound'],
'metronome_mode': user_settings_dict['metronome_mode'],
'metronome_speed': user_settings_dict['metronome_speed'],
'target_error_rate': user_settings_dict['target_error_rate'],
'max_bpm_speed': user_settings_dict['max_bpm_speed'],
'speed_display': user_settings_dict['speed_display']
}
def update_training_progress(
self,
user_input: str,
current_text: str,
elapsed_time: float,
cursor_position: int,
total_elapsed_time: float,
net_training_time: float,
key_stroke_count: int,
is_paused: bool
) -> Dict[str, Any]:
"""
Update training progress and calculate statistics
Args:
user_input: User's typed text
current_text: Target text
elapsed_time: Time elapsed in seconds
cursor_position: Current cursor position
total_elapsed_time: Total elapsed time in milliseconds
net_training_time: Net training time in milliseconds
key_stroke_count: Number of keystrokes
is_paused: Whether session is paused
Returns:
Dictionary with statistics and completion status
"""
# Get current lesson
lesson_index = self.progress_service.get_current_lesson_index()
# Update lesson progress
self.progress_service.update_lesson_progress(
lesson_index=lesson_index,
cursor_position=cursor_position,
user_input=user_input,
total_elapsed_time=int(total_elapsed_time),
key_stroke_count=key_stroke_count,
is_paused=is_paused
)
# Check if lesson is completed
lesson_completed = self.progress_service.check_lesson_completed(
cursor_position=cursor_position,
user_input_length=len(user_input),
lesson_text_length=len(current_text)
)
if lesson_completed:
logger.info(f"Lektion {lesson_index} abgeschlossen!")
# Calculate final statistics
stats = self.statistics_service.calculate_typing_statistics(
user_input, current_text, elapsed_time
)
# Save statistics
self.statistics_service.save_lesson_statistics(
lesson_index=lesson_index,
chars_per_minute=stats['typing_speed'],
error_rate=stats['error_rate'],
wpm=stats['words_per_minute'],
training_duration=total_elapsed_time / 1000.0
)
# Reset lesson progress
self.progress_service.complete_lesson(lesson_index)
# Update session
session['total_elapsed_time'] = total_elapsed_time
session['net_training_time'] = net_training_time
session['key_stroke_count'] = key_stroke_count
session['is_paused'] = is_paused
# Calculate statistics
stats = self.statistics_service.calculate_typing_statistics(
user_input, current_text, elapsed_time
)
stats['lesson_completed'] = lesson_completed
return stats
def switch_to_next_lesson(self) -> Dict[str, Any]:
"""
Switch to next lesson
Returns:
Dictionary with new lesson data
"""
current_index = self.progress_service.get_current_lesson_index()
new_index = self.lesson_service.get_next_lesson_index(current_index)
return self._switch_lesson(new_index, current_index)
def switch_to_lesson(self, lesson_index: int) -> Tuple[bool, Dict[str, Any]]:
"""
Switch to specific lesson
Args:
lesson_index: Target lesson index
Returns:
Tuple of (success, data dictionary)
"""
if not self.lesson_service.validate_lesson_index(lesson_index):
return False, {'error': 'Ungültiger Lektionsindex'}
current_index = self.progress_service.get_current_lesson_index()
return True, self._switch_lesson(lesson_index, current_index)
def _switch_lesson(self, new_index: int, old_index: int) -> Dict[str, Any]:
"""
Internal method to switch lessons
Args:
new_index: New lesson index
old_index: Old lesson index
Returns:
Dictionary with new lesson data
"""
# Update global progress
self.progress_service.set_current_lesson_index(new_index)
# Get new lesson progress
progress_data = self.progress_service.get_lesson_progress_data(new_index)
# Synchronize session
session['total_elapsed_time'] = progress_data['total_elapsed_time']
session['net_training_time'] = progress_data['total_elapsed_time']
session['key_stroke_count'] = progress_data['key_stroke_count']
session['is_paused'] = progress_data['is_paused']
# Get lesson info
lesson_info = self.lesson_service.get_lesson_info(new_index)
logger.info(f"Lektionswechsel: {old_index} -> {new_index}")
return {
'text': lesson_info['text'],
'index': new_index,
'total': lesson_info['total'],
'title': lesson_info['title'],
'current_position': progress_data['current_position'],
'last_text': progress_data['last_text'],
'total_elapsed_time': progress_data['total_elapsed_time'],
'key_stroke_count': progress_data['key_stroke_count'],
'is_paused': progress_data['is_paused']
}
def reset_current_lesson(self) -> Tuple[bool, Dict[str, Any]]:
"""
Reset current lesson progress
Returns:
Tuple of (success, data dictionary)
"""
lesson_index = self.progress_service.get_current_lesson_index()
self.progress_service.reset_lesson_progress(lesson_index)
return True, {
'status': 'success',
'message': 'Lektion zurückgesetzt',
'lesson_index': lesson_index
}

View file

@ -131,7 +131,7 @@
</div>
<!-- Zurück-Button -->
<a href="{{ url_for('train') }}" class="btn btn-primary btn-lg back-button shadow">
<a href="{{ url_for('training.train') }}" class="btn btn-primary btn-lg back-button shadow">
← Zurück zum Trainer
</a>

View file

@ -55,7 +55,7 @@
<i data-lucide="pause" id="play-pause-pause" style="width: 24px; height: 24px; position: absolute; top: 0; left: 0;"></i>
<i data-lucide="play" id="play-pause-play" style="width: 24px; height: 24px; position: absolute; top: 0; left: 0; display: none;"></i>
</div>
<a href="{{ url_for('hilfe') }}"
<a href="{{ url_for('main.hilfe') }}"
style="width: 24px; height: 24px; cursor: pointer; color: white; text-decoration: none; display: inline-block;"
aria-label="Hilfe öffnen"
title="Hilfe öffnen">