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