diff --git a/routes/session.py b/routes/session.py index 42716ab..868ff14 100644 --- a/routes/session.py +++ b/routes/session.py @@ -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) diff --git a/routes/settings.py b/routes/settings.py index 310b371..adbd018 100644 --- a/routes/settings.py +++ b/routes/settings.py @@ -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 diff --git a/routes/statistics.py b/routes/statistics.py index 3aac765..6c34e73 100644 --- a/routes/statistics.py +++ b/routes/statistics.py @@ -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' diff --git a/routes/training.py b/routes/training.py index be4803b..2edc59d 100644 --- a/routes/training.py +++ b/routes/training.py @@ -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() diff --git a/services/lesson_service.py b/services/lesson_service.py new file mode 100644 index 0000000..e0b19dc --- /dev/null +++ b/services/lesson_service.py @@ -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) + } diff --git a/services/progress_service.py b/services/progress_service.py new file mode 100644 index 0000000..48a5f73 --- /dev/null +++ b/services/progress_service.py @@ -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") diff --git a/services/settings_service.py b/services/settings_service.py new file mode 100644 index 0000000..7b71f47 --- /dev/null +++ b/services/settings_service.py @@ -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'] diff --git a/services/statistics_service.py b/services/statistics_service.py new file mode 100644 index 0000000..971dd37 --- /dev/null +++ b/services/statistics_service.py @@ -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 diff --git a/services/training_service.py b/services/training_service.py new file mode 100644 index 0000000..d122cbb --- /dev/null +++ b/services/training_service.py @@ -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 + } diff --git a/templates/hilfe.html b/templates/hilfe.html index b0b2db7..fd07ccf 100644 --- a/templates/hilfe.html +++ b/templates/hilfe.html @@ -131,7 +131,7 @@ - + ← Zurück zum Trainer diff --git a/templates/index.html b/templates/index.html index 2d7f103..a0922cc 100644 --- a/templates/index.html +++ b/templates/index.html @@ -55,7 +55,7 @@ -