Phase 2: Service Layer Pattern implementiert
Umfassende Refactorierung mit Service Layer für bessere Wartbarkeit:
Neue Service-Klassen (services/):
- lesson_service.py - Lektionsverwaltung und Validierung
- progress_service.py - Fortschrittsverwaltung (global + pro Lektion)
- statistics_service.py - Statistik-Berechnungen und -Speicherung
- settings_service.py - Einstellungsverwaltung mit Validierung
- training_service.py - High-Level Training-Orchestrierung
Refactorierte Blueprints (routes/):
- training.py: Von 485 auf 202 Zeilen (~58% Reduktion)
- statistics.py: Verwendet Services für alle Operationen
- settings.py: Verwendet Services mit Validierung
- session.py: Verwendet Services für Session-Management
Template-Fixes:
- index.html: url_for('main.hilfe') für Blueprint-Routing
- hilfe.html: url_for('training.train') für Blueprint-Routing
Vorteile der Service Layer:
- Business Logic wiederverwendbar und testbar
- Routes sind schlanker und fokussieren auf HTTP
- Klare Trennung: Routes = HTTP, Services = Business Logic
- Einfacheres Unit Testing möglich
- Bessere Code-Organisation und Wartbarkeit
Alle 23 Routes getestet und funktionieren korrekt.
Code-Reduktion in Routes: ~40% bei gleichzeitiger Verbesserung der Struktur.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
11403a4c9d
commit
2ef3809e42
11 changed files with 1174 additions and 393 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -4,121 +4,31 @@ Handles all training-related routes
|
|||
"""
|
||||
|
||||
import logging
|
||||
from flask import Blueprint, render_template, request, jsonify, session
|
||||
from models import db, Progress, LessonStatistic, LessonProgress, UserSettings
|
||||
from services.helpers import (
|
||||
lessons,
|
||||
get_or_create_user_settings,
|
||||
get_or_create_lesson_progress,
|
||||
calculate_statistics
|
||||
)
|
||||
from flask import Blueprint, render_template, request, jsonify
|
||||
from models import db
|
||||
from services.helpers import lessons
|
||||
from services.lesson_service import LessonService
|
||||
from services.training_service import TrainingService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
training_bp = Blueprint('training', __name__)
|
||||
|
||||
# Initialize services
|
||||
lesson_service = LessonService(lessons)
|
||||
training_service = TrainingService(lesson_service)
|
||||
|
||||
|
||||
@training_bp.route('/train')
|
||||
def train():
|
||||
"""Hauptseite des Tipptrainers"""
|
||||
try:
|
||||
# Hole den globalen Fortschritt aus der Datenbank (speichert nur current_text_index)
|
||||
progress = Progress.query.first()
|
||||
|
||||
if not progress:
|
||||
# Falls keine Daten vorhanden sind, initialisiere
|
||||
progress = Progress(
|
||||
current_text_index=0,
|
||||
current_position=0,
|
||||
last_text=""
|
||||
)
|
||||
db.session.add(progress)
|
||||
db.session.commit()
|
||||
logger.info("Neuen globalen Fortschritt initialisiert")
|
||||
|
||||
# Lade gespeicherte Benutzereinstellungen aus der Datenbank
|
||||
user_settings = get_or_create_user_settings(db, UserSettings)
|
||||
|
||||
# Bestimme die aktuelle Lektion
|
||||
current_lesson_index = progress.current_text_index
|
||||
|
||||
# Wenn eine gespeicherte Lektion in UserSettings existiert und der Progress noch initial ist,
|
||||
# verwende die gespeicherte Lektion
|
||||
if current_lesson_index == 0 and user_settings.current_lesson_index > 0:
|
||||
current_lesson_index = user_settings.current_lesson_index
|
||||
progress.current_text_index = current_lesson_index
|
||||
db.session.commit()
|
||||
|
||||
# Validiere Lektionsindex
|
||||
if not lessons or current_lesson_index >= len(lessons):
|
||||
current_lesson_index = 0
|
||||
progress.current_text_index = 0
|
||||
db.session.commit()
|
||||
|
||||
# Hole den aktuellen Text
|
||||
current_text = lessons[current_lesson_index]['text'] if lessons else "Keine Lektionen verfügbar"
|
||||
|
||||
# Lade lektionsspezifische Fortschrittsdaten
|
||||
lesson_progress = get_or_create_lesson_progress(db, LessonProgress, current_lesson_index)
|
||||
|
||||
# Daten aus LessonProgress
|
||||
user_input = lesson_progress.last_text
|
||||
cursor_position = lesson_progress.current_position
|
||||
total_elapsed_time = lesson_progress.total_elapsed_time or 0
|
||||
key_stroke_count = lesson_progress.key_stroke_count or 0
|
||||
is_paused = lesson_progress.is_paused if lesson_progress.is_paused is not None else True
|
||||
|
||||
# Netto-Trainingszeit (wird in JavaScript SessionManager verwaltet)
|
||||
net_training_time = total_elapsed_time
|
||||
|
||||
logger.debug(f"Lade Lektion {current_lesson_index}: Position={cursor_position}, Eingabe-Länge={len(user_input)}")
|
||||
|
||||
# Bestimme den Lektionstitel
|
||||
current_lesson_title = "Unbekannte Lektion"
|
||||
if lessons and current_lesson_index < len(lessons):
|
||||
lesson = lessons[current_lesson_index]
|
||||
current_lesson_title = lesson.get('title', f"Lektion {current_lesson_index + 1}")
|
||||
|
||||
# Metronom-Einstellungen aus Datenbank laden
|
||||
metronome_enabled = user_settings.metronome_enabled
|
||||
metronome_bpm = user_settings.metronome_bpm
|
||||
metronome_sound = user_settings.metronome_sound
|
||||
metronome_mode = user_settings.metronome_mode
|
||||
metronome_speed = user_settings.metronome_speed
|
||||
target_error_rate = user_settings.target_error_rate
|
||||
max_bpm_speed = getattr(user_settings, 'max_bpm_speed', 100) # Fallback für Kompatibilität
|
||||
speed_display = user_settings.speed_display
|
||||
|
||||
# Synchronisiere Session mit DB
|
||||
session['total_elapsed_time'] = total_elapsed_time
|
||||
session['net_training_time'] = net_training_time
|
||||
session['key_stroke_count'] = key_stroke_count
|
||||
session['is_paused'] = is_paused
|
||||
|
||||
return render_template('index.html',
|
||||
current_text=current_text,
|
||||
current_position=cursor_position,
|
||||
last_text=user_input,
|
||||
total_elapsed_time=total_elapsed_time,
|
||||
net_training_time=net_training_time,
|
||||
key_stroke_count=key_stroke_count,
|
||||
is_paused=is_paused,
|
||||
lessons=lessons,
|
||||
current_lesson_title=current_lesson_title,
|
||||
current_lesson_index=current_lesson_index,
|
||||
metronome_enabled=metronome_enabled,
|
||||
metronome_bpm=metronome_bpm,
|
||||
metronome_sound=metronome_sound,
|
||||
metronome_mode=metronome_mode,
|
||||
metronome_speed=metronome_speed,
|
||||
target_error_rate=target_error_rate,
|
||||
max_bpm_speed=max_bpm_speed,
|
||||
speed_display=speed_display)
|
||||
|
||||
context = training_service.get_training_context()
|
||||
return render_template('index.html', **context)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler in train(): {e}", exc_info=True)
|
||||
# Auch im Fehlerfall Metronom-Standardwerte übergeben
|
||||
# Fehlerfall mit Standardwerten
|
||||
return render_template('index.html',
|
||||
current_text="Fehler beim Laden der Lektionen",
|
||||
current_position=0,
|
||||
|
|
@ -160,69 +70,21 @@ def update_progress():
|
|||
|
||||
is_paused = bool(data.get('is_paused', False))
|
||||
|
||||
# Hole den globalen Fortschritt (für lesson_index)
|
||||
progress = Progress.query.first()
|
||||
|
||||
if not progress:
|
||||
return jsonify({'error': 'Progress nicht gefunden'}), 400
|
||||
|
||||
try:
|
||||
# Hole lektionsspezifischen Fortschritt
|
||||
lesson_index = progress.current_text_index
|
||||
lesson_progress = get_or_create_lesson_progress(db, LessonProgress, lesson_index)
|
||||
|
||||
# Aktualisiere lektionsspezifische Daten
|
||||
lesson_progress.current_position = cursor_position
|
||||
lesson_progress.last_text = user_input
|
||||
lesson_progress.total_elapsed_time = int(total_elapsed_time)
|
||||
lesson_progress.key_stroke_count = key_stroke_count
|
||||
lesson_progress.is_paused = is_paused
|
||||
|
||||
# Prüfe, ob die Lektion abgeschlossen ist
|
||||
lesson_completed = cursor_position >= len(current_text) and len(user_input) == len(current_text)
|
||||
|
||||
if lesson_completed:
|
||||
# Speichere Statistik für abgeschlossene Lektion
|
||||
logger.info(f"Lektion {lesson_index} abgeschlossen! Speichere Statistik...")
|
||||
|
||||
# Berechne finale Statistiken
|
||||
stats = calculate_statistics(user_input, current_text, elapsed_time)
|
||||
|
||||
# Speichere in LessonStatistic
|
||||
lesson_stat = LessonStatistic(
|
||||
lesson_index=lesson_index,
|
||||
chars_per_minute=stats['typing_speed'],
|
||||
error_rate=stats['error_rate'],
|
||||
wpm=stats['words_per_minute'],
|
||||
training_duration=total_elapsed_time / 1000.0 # Millisekunden zu Sekunden
|
||||
)
|
||||
db.session.add(lesson_stat)
|
||||
|
||||
# Setze LessonProgress zurück
|
||||
lesson_progress.current_position = 0
|
||||
lesson_progress.last_text = ""
|
||||
lesson_progress.total_elapsed_time = 0
|
||||
lesson_progress.key_stroke_count = 0
|
||||
lesson_progress.is_paused = True
|
||||
|
||||
logger.info(f"Lektion {lesson_index} zurückgesetzt")
|
||||
|
||||
# Speichere in der Datenbank
|
||||
db.session.commit()
|
||||
|
||||
# Speichere zusätzliche Zustandsdaten auch in der Session (für Kompatibilität)
|
||||
session['total_elapsed_time'] = total_elapsed_time
|
||||
session['net_training_time'] = net_training_time
|
||||
session['key_stroke_count'] = key_stroke_count
|
||||
session['is_paused'] = is_paused
|
||||
|
||||
# Berechne Statistiken mit echter Zeit
|
||||
stats = calculate_statistics(user_input, current_text, elapsed_time)
|
||||
|
||||
# Füge lesson_completed zu Response hinzu
|
||||
stats['lesson_completed'] = lesson_completed
|
||||
# Update progress and get statistics
|
||||
stats = training_service.update_training_progress(
|
||||
user_input=user_input,
|
||||
current_text=current_text,
|
||||
elapsed_time=elapsed_time,
|
||||
cursor_position=cursor_position,
|
||||
total_elapsed_time=total_elapsed_time,
|
||||
net_training_time=net_training_time,
|
||||
key_stroke_count=key_stroke_count,
|
||||
is_paused=is_paused
|
||||
)
|
||||
|
||||
return jsonify(stats)
|
||||
|
||||
except Exception as db_error:
|
||||
db.session.rollback()
|
||||
logger.error(f"Datenbank-Fehler beim Speichern des Fortschritts: {db_error}", exc_info=True)
|
||||
|
|
@ -237,52 +99,8 @@ def update_progress():
|
|||
def next_text():
|
||||
"""Wechselt zum nächsten Text"""
|
||||
try:
|
||||
progress = Progress.query.first()
|
||||
|
||||
if not progress:
|
||||
return jsonify({'error': 'Progress nicht gefunden'}), 400
|
||||
|
||||
if not lessons:
|
||||
return jsonify({'error': 'Keine Lektionen verfügbar'}), 400
|
||||
|
||||
# Wechsel zum nächsten Text
|
||||
old_lesson_index = progress.current_text_index
|
||||
|
||||
if progress.current_text_index < len(lessons) - 1:
|
||||
progress.current_text_index += 1
|
||||
else:
|
||||
# Wenn wir am Ende sind, starten wir von vorne
|
||||
progress.current_text_index = 0
|
||||
|
||||
new_lesson_index = progress.current_text_index
|
||||
db.session.commit()
|
||||
|
||||
# Lade LessonProgress für neue Lektion (wird automatisch erstellt falls nicht vorhanden)
|
||||
lesson_progress = get_or_create_lesson_progress(db, LessonProgress, new_lesson_index)
|
||||
|
||||
# Synchronisiere Session mit der neuen Lektion
|
||||
session['total_elapsed_time'] = lesson_progress.total_elapsed_time
|
||||
session['net_training_time'] = lesson_progress.total_elapsed_time
|
||||
session['key_stroke_count'] = lesson_progress.key_stroke_count
|
||||
session['is_paused'] = lesson_progress.is_paused
|
||||
|
||||
# Bestimme den Titel der neuen Lektion
|
||||
new_lesson = lessons[new_lesson_index]
|
||||
lesson_title = new_lesson.get('title', f"Lektion {new_lesson_index + 1}")
|
||||
|
||||
logger.info(f"Lektionswechsel: {old_lesson_index} -> {new_lesson_index}")
|
||||
|
||||
return jsonify({
|
||||
'text': new_lesson['text'],
|
||||
'index': new_lesson_index,
|
||||
'total': len(lessons),
|
||||
'title': lesson_title,
|
||||
'current_position': lesson_progress.current_position,
|
||||
'last_text': lesson_progress.last_text,
|
||||
'total_elapsed_time': lesson_progress.total_elapsed_time,
|
||||
'key_stroke_count': lesson_progress.key_stroke_count,
|
||||
'is_paused': lesson_progress.is_paused
|
||||
})
|
||||
result = training_service.switch_to_next_lesson()
|
||||
return jsonify(result)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler in next_text: {e}", exc_info=True)
|
||||
|
|
@ -299,43 +117,12 @@ def set_lesson():
|
|||
if lesson_index is None or not isinstance(lesson_index, int):
|
||||
return jsonify({'error': 'Ungültiger Lektionsindex'}), 400
|
||||
|
||||
if lesson_index < 0 or lesson_index >= len(lessons):
|
||||
return jsonify({'error': 'Lektionsindex außerhalb des gültigen Bereichs'}), 400
|
||||
success, result = training_service.switch_to_lesson(lesson_index)
|
||||
|
||||
progress = Progress.query.first()
|
||||
if not progress:
|
||||
return jsonify({'error': 'Progress nicht gefunden'}), 400
|
||||
if not success:
|
||||
return jsonify(result), 400
|
||||
|
||||
old_lesson_index = progress.current_text_index
|
||||
|
||||
# Setze neue Lektion
|
||||
progress.current_text_index = lesson_index
|
||||
db.session.commit()
|
||||
|
||||
# Lade LessonProgress für neue Lektion (wird automatisch erstellt falls nicht vorhanden)
|
||||
lesson_progress = get_or_create_lesson_progress(db, LessonProgress, lesson_index)
|
||||
|
||||
# Synchronisiere Session mit der neuen Lektion
|
||||
session['total_elapsed_time'] = lesson_progress.total_elapsed_time
|
||||
session['net_training_time'] = lesson_progress.total_elapsed_time
|
||||
session['key_stroke_count'] = lesson_progress.key_stroke_count
|
||||
session['is_paused'] = lesson_progress.is_paused
|
||||
|
||||
# Bestimme den Titel der neuen Lektion
|
||||
new_lesson = lessons[lesson_index]
|
||||
lesson_title = new_lesson.get('title', f"Lektion {lesson_index + 1}")
|
||||
|
||||
logger.info(f"Lektionswechsel: {old_lesson_index} -> {lesson_index}")
|
||||
|
||||
return jsonify({
|
||||
'text': new_lesson['text'],
|
||||
'title': lesson_title,
|
||||
'current_position': lesson_progress.current_position,
|
||||
'last_text': lesson_progress.last_text,
|
||||
'total_elapsed_time': lesson_progress.total_elapsed_time,
|
||||
'key_stroke_count': lesson_progress.key_stroke_count,
|
||||
'is_paused': lesson_progress.is_paused
|
||||
})
|
||||
return jsonify(result)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler in set_lesson: {e}", exc_info=True)
|
||||
|
|
@ -345,6 +132,7 @@ def set_lesson():
|
|||
@training_bp.route('/save_lesson_statistics', methods=['POST'])
|
||||
def save_lesson_statistics():
|
||||
"""Speichert die Statistiken für eine abgeschlossene Lektion"""
|
||||
from services.statistics_service import StatisticsService
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
|
|
@ -375,11 +163,8 @@ def save_lesson_statistics():
|
|||
if error_rate > 100:
|
||||
return jsonify({'error': 'Fehlerrate kann nicht über 100% sein'}), 400
|
||||
|
||||
logger.debug(f"Empfangene Statistik: lesson_index={lesson_index}, chars_per_minute={chars_per_minute}, error_rate={error_rate}, wpm={wpm}, training_duration={training_duration}")
|
||||
|
||||
# Neue Statistik für die Lektion erstellen
|
||||
try:
|
||||
lesson_stat = LessonStatistic(
|
||||
StatisticsService.save_lesson_statistics(
|
||||
lesson_index=lesson_index,
|
||||
chars_per_minute=chars_per_minute,
|
||||
error_rate=error_rate,
|
||||
|
|
@ -387,12 +172,8 @@ def save_lesson_statistics():
|
|||
training_duration=training_duration
|
||||
)
|
||||
|
||||
db.session.add(lesson_stat)
|
||||
db.session.commit()
|
||||
|
||||
logger.info("Statistik erfolgreich in Datenbank gespeichert")
|
||||
|
||||
return jsonify({'status': 'success', 'message': 'Lektionsstatistiken gespeichert'})
|
||||
|
||||
except Exception as db_error:
|
||||
db.session.rollback()
|
||||
logger.error(f"Datenbank-Fehler beim Speichern der Statistik: {db_error}", exc_info=True)
|
||||
|
|
@ -407,32 +188,12 @@ def save_lesson_statistics():
|
|||
def reset_lesson():
|
||||
"""Setzt den Fortschritt der aktuellen Lektion zurück"""
|
||||
try:
|
||||
# Hole den globalen Fortschritt
|
||||
progress = Progress.query.first()
|
||||
if not progress:
|
||||
return jsonify({'error': 'Progress nicht gefunden'}), 400
|
||||
success, result = training_service.reset_current_lesson()
|
||||
|
||||
lesson_index = progress.current_text_index
|
||||
if not success:
|
||||
return jsonify(result), 400
|
||||
|
||||
# Hole oder erstelle LessonProgress für aktuelle Lektion
|
||||
lesson_progress = get_or_create_lesson_progress(db, LessonProgress, lesson_index)
|
||||
|
||||
# Setze alle Werte zurück
|
||||
lesson_progress.current_position = 0
|
||||
lesson_progress.last_text = ""
|
||||
lesson_progress.total_elapsed_time = 0
|
||||
lesson_progress.key_stroke_count = 0
|
||||
lesson_progress.is_paused = True
|
||||
|
||||
db.session.commit()
|
||||
|
||||
logger.info(f"Lektion {lesson_index} wurde zurückgesetzt")
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': 'Lektion zurückgesetzt',
|
||||
'lesson_index': lesson_index
|
||||
})
|
||||
return jsonify(result)
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
|
|
|
|||
150
services/lesson_service.py
Normal file
150
services/lesson_service.py
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
"""
|
||||
Lesson Service
|
||||
Handles all lesson-related business logic
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LessonService:
|
||||
"""Service for managing lessons"""
|
||||
|
||||
def __init__(self, lessons: List[Dict[str, Any]]):
|
||||
"""
|
||||
Initialize LessonService with lessons data
|
||||
|
||||
Args:
|
||||
lessons: List of lesson dictionaries
|
||||
"""
|
||||
self.lessons = lessons
|
||||
|
||||
def get_all_lessons(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get all lessons
|
||||
|
||||
Returns:
|
||||
List of all lessons
|
||||
"""
|
||||
return self.lessons
|
||||
|
||||
def get_lesson_count(self) -> int:
|
||||
"""
|
||||
Get total number of lessons
|
||||
|
||||
Returns:
|
||||
Number of lessons
|
||||
"""
|
||||
return len(self.lessons)
|
||||
|
||||
def get_lesson_by_index(self, index: int) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get a specific lesson by index
|
||||
|
||||
Args:
|
||||
index: Lesson index
|
||||
|
||||
Returns:
|
||||
Lesson dictionary or None if invalid index
|
||||
"""
|
||||
if 0 <= index < len(self.lessons):
|
||||
return self.lessons[index]
|
||||
return None
|
||||
|
||||
def get_lesson_text(self, index: int) -> str:
|
||||
"""
|
||||
Get lesson text by index
|
||||
|
||||
Args:
|
||||
index: Lesson index
|
||||
|
||||
Returns:
|
||||
Lesson text or error message
|
||||
"""
|
||||
lesson = self.get_lesson_by_index(index)
|
||||
if lesson:
|
||||
return lesson.get('text', 'Kein Text verfügbar')
|
||||
return 'Lektion nicht gefunden'
|
||||
|
||||
def get_lesson_title(self, index: int) -> str:
|
||||
"""
|
||||
Get lesson title by index
|
||||
|
||||
Args:
|
||||
index: Lesson index
|
||||
|
||||
Returns:
|
||||
Lesson title or default
|
||||
"""
|
||||
lesson = self.get_lesson_by_index(index)
|
||||
if lesson:
|
||||
return lesson.get('title', f'Lektion {index + 1}')
|
||||
return 'Unbekannte Lektion'
|
||||
|
||||
def validate_lesson_index(self, index: int) -> bool:
|
||||
"""
|
||||
Validate if lesson index is valid
|
||||
|
||||
Args:
|
||||
index: Lesson index to validate
|
||||
|
||||
Returns:
|
||||
True if valid, False otherwise
|
||||
"""
|
||||
return 0 <= index < len(self.lessons)
|
||||
|
||||
def get_next_lesson_index(self, current_index: int) -> int:
|
||||
"""
|
||||
Get next lesson index (wraps around to 0 at end)
|
||||
|
||||
Args:
|
||||
current_index: Current lesson index
|
||||
|
||||
Returns:
|
||||
Next lesson index
|
||||
"""
|
||||
if current_index < len(self.lessons) - 1:
|
||||
return current_index + 1
|
||||
return 0
|
||||
|
||||
def get_previous_lesson_index(self, current_index: int) -> int:
|
||||
"""
|
||||
Get previous lesson index (wraps around to last at beginning)
|
||||
|
||||
Args:
|
||||
current_index: Current lesson index
|
||||
|
||||
Returns:
|
||||
Previous lesson index
|
||||
"""
|
||||
if current_index > 0:
|
||||
return current_index - 1
|
||||
return len(self.lessons) - 1
|
||||
|
||||
def get_lesson_info(self, index: int) -> Dict[str, Any]:
|
||||
"""
|
||||
Get complete lesson information
|
||||
|
||||
Args:
|
||||
index: Lesson index
|
||||
|
||||
Returns:
|
||||
Dictionary with lesson info
|
||||
"""
|
||||
lesson = self.get_lesson_by_index(index)
|
||||
if not lesson:
|
||||
return {
|
||||
'index': index,
|
||||
'text': 'Lektion nicht gefunden',
|
||||
'title': 'Unbekannte Lektion',
|
||||
'total': len(self.lessons)
|
||||
}
|
||||
|
||||
return {
|
||||
'index': index,
|
||||
'text': lesson.get('text', ''),
|
||||
'title': self.get_lesson_title(index),
|
||||
'total': len(self.lessons)
|
||||
}
|
||||
183
services/progress_service.py
Normal file
183
services/progress_service.py
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
"""
|
||||
Progress Service
|
||||
Handles all progress and lesson progress related business logic
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
from models import db, Progress, LessonProgress
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ProgressService:
|
||||
"""Service for managing user progress"""
|
||||
|
||||
@staticmethod
|
||||
def get_or_create_global_progress() -> Progress:
|
||||
"""
|
||||
Get or create global progress entry
|
||||
|
||||
Returns:
|
||||
Progress instance
|
||||
"""
|
||||
progress = Progress.query.first()
|
||||
if not progress:
|
||||
progress = Progress(
|
||||
current_text_index=0,
|
||||
current_position=0,
|
||||
last_text=""
|
||||
)
|
||||
db.session.add(progress)
|
||||
db.session.commit()
|
||||
logger.info("Neuen globalen Fortschritt initialisiert")
|
||||
return progress
|
||||
|
||||
@staticmethod
|
||||
def get_current_lesson_index() -> int:
|
||||
"""
|
||||
Get current lesson index from global progress
|
||||
|
||||
Returns:
|
||||
Current lesson index
|
||||
"""
|
||||
progress = ProgressService.get_or_create_global_progress()
|
||||
return progress.current_text_index
|
||||
|
||||
@staticmethod
|
||||
def set_current_lesson_index(lesson_index: int) -> None:
|
||||
"""
|
||||
Set current lesson index in global progress
|
||||
|
||||
Args:
|
||||
lesson_index: New lesson index
|
||||
"""
|
||||
progress = ProgressService.get_or_create_global_progress()
|
||||
progress.current_text_index = lesson_index
|
||||
db.session.commit()
|
||||
logger.info(f"Globalen Lektionsindex auf {lesson_index} gesetzt")
|
||||
|
||||
@staticmethod
|
||||
def get_or_create_lesson_progress(lesson_index: int) -> LessonProgress:
|
||||
"""
|
||||
Get or create lesson progress for specific lesson
|
||||
|
||||
Args:
|
||||
lesson_index: Lesson index
|
||||
|
||||
Returns:
|
||||
LessonProgress instance
|
||||
"""
|
||||
lesson_progress = LessonProgress.query.filter_by(lesson_index=lesson_index).first()
|
||||
if not lesson_progress:
|
||||
lesson_progress = LessonProgress(
|
||||
lesson_index=lesson_index,
|
||||
current_position=0,
|
||||
last_text="",
|
||||
total_elapsed_time=0,
|
||||
key_stroke_count=0,
|
||||
is_paused=True
|
||||
)
|
||||
db.session.add(lesson_progress)
|
||||
db.session.commit()
|
||||
logger.info(f"Neuen Lektions-Fortschritt für Lektion {lesson_index} erstellt")
|
||||
return lesson_progress
|
||||
|
||||
@staticmethod
|
||||
def get_lesson_progress_data(lesson_index: int) -> Dict[str, Any]:
|
||||
"""
|
||||
Get lesson progress data as dictionary
|
||||
|
||||
Args:
|
||||
lesson_index: Lesson index
|
||||
|
||||
Returns:
|
||||
Dictionary with progress data
|
||||
"""
|
||||
lesson_progress = ProgressService.get_or_create_lesson_progress(lesson_index)
|
||||
return {
|
||||
'current_position': lesson_progress.current_position,
|
||||
'last_text': lesson_progress.last_text,
|
||||
'total_elapsed_time': lesson_progress.total_elapsed_time,
|
||||
'key_stroke_count': lesson_progress.key_stroke_count,
|
||||
'is_paused': lesson_progress.is_paused if lesson_progress.is_paused is not None else True
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def update_lesson_progress(
|
||||
lesson_index: int,
|
||||
cursor_position: int,
|
||||
user_input: str,
|
||||
total_elapsed_time: int,
|
||||
key_stroke_count: int,
|
||||
is_paused: bool
|
||||
) -> LessonProgress:
|
||||
"""
|
||||
Update lesson progress with new data
|
||||
|
||||
Args:
|
||||
lesson_index: Lesson index
|
||||
cursor_position: Current cursor position
|
||||
user_input: User's typed text
|
||||
total_elapsed_time: Total elapsed time in milliseconds
|
||||
key_stroke_count: Number of keystrokes
|
||||
is_paused: Whether session is paused
|
||||
|
||||
Returns:
|
||||
Updated LessonProgress instance
|
||||
"""
|
||||
lesson_progress = ProgressService.get_or_create_lesson_progress(lesson_index)
|
||||
lesson_progress.current_position = cursor_position
|
||||
lesson_progress.last_text = user_input
|
||||
lesson_progress.total_elapsed_time = int(total_elapsed_time)
|
||||
lesson_progress.key_stroke_count = key_stroke_count
|
||||
lesson_progress.is_paused = is_paused
|
||||
db.session.commit()
|
||||
return lesson_progress
|
||||
|
||||
@staticmethod
|
||||
def reset_lesson_progress(lesson_index: int) -> None:
|
||||
"""
|
||||
Reset progress for a specific lesson
|
||||
|
||||
Args:
|
||||
lesson_index: Lesson index to reset
|
||||
"""
|
||||
lesson_progress = ProgressService.get_or_create_lesson_progress(lesson_index)
|
||||
lesson_progress.current_position = 0
|
||||
lesson_progress.last_text = ""
|
||||
lesson_progress.total_elapsed_time = 0
|
||||
lesson_progress.key_stroke_count = 0
|
||||
lesson_progress.is_paused = True
|
||||
db.session.commit()
|
||||
logger.info(f"Lektion {lesson_index} wurde zurückgesetzt")
|
||||
|
||||
@staticmethod
|
||||
def check_lesson_completed(
|
||||
cursor_position: int,
|
||||
user_input_length: int,
|
||||
lesson_text_length: int
|
||||
) -> bool:
|
||||
"""
|
||||
Check if lesson is completed
|
||||
|
||||
Args:
|
||||
cursor_position: Current cursor position
|
||||
user_input_length: Length of user input
|
||||
lesson_text_length: Length of lesson text
|
||||
|
||||
Returns:
|
||||
True if lesson is completed
|
||||
"""
|
||||
return cursor_position >= lesson_text_length and user_input_length == lesson_text_length
|
||||
|
||||
@staticmethod
|
||||
def complete_lesson(lesson_index: int) -> None:
|
||||
"""
|
||||
Mark lesson as completed and reset progress
|
||||
|
||||
Args:
|
||||
lesson_index: Lesson index
|
||||
"""
|
||||
ProgressService.reset_lesson_progress(lesson_index)
|
||||
logger.info(f"Lektion {lesson_index} abgeschlossen und zurückgesetzt")
|
||||
206
services/settings_service.py
Normal file
206
services/settings_service.py
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
"""
|
||||
Settings Service
|
||||
Handles all user settings related business logic
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Any
|
||||
from models import db, UserSettings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SettingsService:
|
||||
"""Service for managing user settings"""
|
||||
|
||||
@staticmethod
|
||||
def get_or_create_user_settings() -> UserSettings:
|
||||
"""
|
||||
Get or create user settings entry
|
||||
|
||||
Returns:
|
||||
UserSettings instance
|
||||
"""
|
||||
settings = UserSettings.query.first()
|
||||
if not settings:
|
||||
settings = UserSettings()
|
||||
db.session.add(settings)
|
||||
db.session.commit()
|
||||
logger.info("Neue Benutzereinstellungen erstellt")
|
||||
return settings
|
||||
|
||||
@staticmethod
|
||||
def get_settings_dict() -> Dict[str, Any]:
|
||||
"""
|
||||
Get user settings as dictionary
|
||||
|
||||
Returns:
|
||||
Dictionary with all settings
|
||||
"""
|
||||
settings = SettingsService.get_or_create_user_settings()
|
||||
return {
|
||||
'current_lesson_index': settings.current_lesson_index,
|
||||
'metronome_enabled': settings.metronome_enabled,
|
||||
'metronome_bpm': settings.metronome_bpm,
|
||||
'metronome_sound': settings.metronome_sound,
|
||||
'metronome_mode': settings.metronome_mode,
|
||||
'metronome_speed': settings.metronome_speed,
|
||||
'target_error_rate': settings.target_error_rate,
|
||||
'max_bpm_speed': getattr(settings, 'max_bpm_speed', 100),
|
||||
'speed_display': settings.speed_display
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def update_settings(
|
||||
current_lesson_index: int = None,
|
||||
metronome_enabled: bool = None,
|
||||
metronome_bpm: int = None,
|
||||
metronome_sound: str = None,
|
||||
metronome_mode: str = None,
|
||||
metronome_speed: int = None,
|
||||
target_error_rate: int = None,
|
||||
max_bpm_speed: int = None,
|
||||
speed_display: str = None
|
||||
) -> UserSettings:
|
||||
"""
|
||||
Update user settings
|
||||
|
||||
Args:
|
||||
current_lesson_index: Current lesson index
|
||||
metronome_enabled: Whether metronome is enabled
|
||||
metronome_bpm: Metronome BPM
|
||||
metronome_sound: Metronome sound type
|
||||
metronome_mode: Metronome mode (automatic/explicit)
|
||||
metronome_speed: Metronome speed
|
||||
target_error_rate: Target error rate percentage
|
||||
max_bpm_speed: Maximum BPM speed
|
||||
speed_display: Speed display mode (zpm/wpm)
|
||||
|
||||
Returns:
|
||||
Updated UserSettings instance
|
||||
"""
|
||||
settings = SettingsService.get_or_create_user_settings()
|
||||
|
||||
if current_lesson_index is not None:
|
||||
settings.current_lesson_index = current_lesson_index
|
||||
if metronome_enabled is not None:
|
||||
settings.metronome_enabled = metronome_enabled
|
||||
if metronome_bpm is not None:
|
||||
settings.metronome_bpm = metronome_bpm
|
||||
if metronome_sound is not None:
|
||||
settings.metronome_sound = metronome_sound
|
||||
if metronome_mode is not None:
|
||||
settings.metronome_mode = metronome_mode
|
||||
if metronome_speed is not None:
|
||||
settings.metronome_speed = metronome_speed
|
||||
if target_error_rate is not None:
|
||||
settings.target_error_rate = target_error_rate
|
||||
if max_bpm_speed is not None:
|
||||
settings.max_bpm_speed = max_bpm_speed
|
||||
if speed_display is not None:
|
||||
settings.speed_display = speed_display
|
||||
|
||||
db.session.commit()
|
||||
logger.info("Benutzereinstellungen aktualisiert")
|
||||
return settings
|
||||
|
||||
@staticmethod
|
||||
def save_all_settings(settings_data: Dict[str, Any]) -> UserSettings:
|
||||
"""
|
||||
Save all settings from dictionary
|
||||
|
||||
Args:
|
||||
settings_data: Dictionary with settings data
|
||||
|
||||
Returns:
|
||||
Updated UserSettings instance
|
||||
"""
|
||||
return SettingsService.update_settings(
|
||||
current_lesson_index=settings_data.get('current_lesson_index'),
|
||||
metronome_enabled=settings_data.get('metronome_enabled'),
|
||||
metronome_bpm=settings_data.get('metronome_bpm'),
|
||||
metronome_sound=settings_data.get('metronome_sound'),
|
||||
metronome_mode=settings_data.get('metronome_mode'),
|
||||
metronome_speed=settings_data.get('metronome_speed'),
|
||||
target_error_rate=settings_data.get('target_error_rate'),
|
||||
max_bpm_speed=settings_data.get('max_bpm_speed'),
|
||||
speed_display=settings_data.get('speed_display')
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def validate_metronome_mode(mode: str) -> bool:
|
||||
"""
|
||||
Validate metronome mode
|
||||
|
||||
Args:
|
||||
mode: Mode to validate
|
||||
|
||||
Returns:
|
||||
True if valid
|
||||
"""
|
||||
return mode in ['automatic', 'explicit']
|
||||
|
||||
@staticmethod
|
||||
def validate_metronome_speed(speed: int) -> bool:
|
||||
"""
|
||||
Validate metronome speed
|
||||
|
||||
Args:
|
||||
speed: Speed to validate
|
||||
|
||||
Returns:
|
||||
True if valid
|
||||
"""
|
||||
return 20 <= speed <= 300
|
||||
|
||||
@staticmethod
|
||||
def validate_target_error_rate(rate: int) -> bool:
|
||||
"""
|
||||
Validate target error rate
|
||||
|
||||
Args:
|
||||
rate: Error rate to validate
|
||||
|
||||
Returns:
|
||||
True if valid
|
||||
"""
|
||||
return 0 <= rate <= 100
|
||||
|
||||
@staticmethod
|
||||
def validate_max_bpm_speed(speed: int) -> bool:
|
||||
"""
|
||||
Validate max BPM speed
|
||||
|
||||
Args:
|
||||
speed: Speed to validate
|
||||
|
||||
Returns:
|
||||
True if valid
|
||||
"""
|
||||
return 40 <= speed <= 200
|
||||
|
||||
@staticmethod
|
||||
def validate_speed_display(display: str) -> bool:
|
||||
"""
|
||||
Validate speed display mode
|
||||
|
||||
Args:
|
||||
display: Display mode to validate
|
||||
|
||||
Returns:
|
||||
True if valid
|
||||
"""
|
||||
return display in ['zpm', 'wpm']
|
||||
|
||||
@staticmethod
|
||||
def validate_metronome_sound(sound: str) -> bool:
|
||||
"""
|
||||
Validate metronome sound
|
||||
|
||||
Args:
|
||||
sound: Sound to validate
|
||||
|
||||
Returns:
|
||||
True if valid
|
||||
"""
|
||||
return sound in ['beep', 'click', 'woodblock']
|
||||
250
services/statistics_service.py
Normal file
250
services/statistics_service.py
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
"""
|
||||
Statistics Service
|
||||
Handles all statistics-related business logic
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Any, List
|
||||
from datetime import date, timedelta
|
||||
from models import db, LessonStatistic, DailyPractice
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StatisticsService:
|
||||
"""Service for managing statistics"""
|
||||
|
||||
@staticmethod
|
||||
def calculate_typing_statistics(
|
||||
user_input: str,
|
||||
current_text: str,
|
||||
elapsed_time: float
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Calculate typing statistics based on user input
|
||||
|
||||
Args:
|
||||
user_input: User's typed text
|
||||
current_text: Target text
|
||||
elapsed_time: Time elapsed in seconds
|
||||
|
||||
Returns:
|
||||
Dictionary with calculated statistics
|
||||
"""
|
||||
correct_chars = 0
|
||||
incorrect_chars = 0
|
||||
total_chars = len(user_input)
|
||||
|
||||
# Count correct and incorrect characters
|
||||
for i, char in enumerate(user_input):
|
||||
if i < len(current_text) and char == current_text[i]:
|
||||
correct_chars += 1
|
||||
else:
|
||||
incorrect_chars += 1
|
||||
|
||||
# Calculate metrics based on real time
|
||||
# Minimum 1 second to avoid unrealistic values
|
||||
if total_chars > 0 and elapsed_time >= 1.0:
|
||||
# Characters per minute (integer)
|
||||
chars_per_minute = (total_chars / elapsed_time) * 60
|
||||
# Words per minute: (correct characters / 5) / (time in minutes)
|
||||
words_per_minute = (correct_chars / 5) / (elapsed_time / 60)
|
||||
# Average time per character
|
||||
duration_per_char = elapsed_time / total_chars
|
||||
|
||||
# Limit to realistic maximum values
|
||||
chars_per_minute = min(chars_per_minute, 1000)
|
||||
words_per_minute = min(words_per_minute, 200)
|
||||
else:
|
||||
chars_per_minute = 0
|
||||
words_per_minute = 0
|
||||
duration_per_char = 0
|
||||
|
||||
# Error rate and accuracy
|
||||
error_rate = (incorrect_chars / total_chars * 100) if total_chars > 0 else 0
|
||||
accuracy = (correct_chars / total_chars * 100) if total_chars > 0 else 0
|
||||
|
||||
return {
|
||||
'correct_chars': correct_chars,
|
||||
'incorrect_chars': incorrect_chars,
|
||||
'total_chars': total_chars,
|
||||
'error_rate': round(error_rate, 1),
|
||||
'typing_speed': round(chars_per_minute),
|
||||
'duration_per_char': round(duration_per_char, 3),
|
||||
'words_per_minute': round(words_per_minute, 2),
|
||||
'accuracy': round(accuracy, 2)
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def save_lesson_statistics(
|
||||
lesson_index: int,
|
||||
chars_per_minute: float,
|
||||
error_rate: float,
|
||||
wpm: float,
|
||||
training_duration: float
|
||||
) -> LessonStatistic:
|
||||
"""
|
||||
Save statistics for a completed lesson
|
||||
|
||||
Args:
|
||||
lesson_index: Lesson index
|
||||
chars_per_minute: Characters per minute
|
||||
error_rate: Error rate percentage
|
||||
wpm: Words per minute
|
||||
training_duration: Training duration in seconds
|
||||
|
||||
Returns:
|
||||
Created LessonStatistic instance
|
||||
"""
|
||||
lesson_stat = LessonStatistic(
|
||||
lesson_index=lesson_index,
|
||||
chars_per_minute=chars_per_minute,
|
||||
error_rate=error_rate,
|
||||
wpm=wpm,
|
||||
training_duration=training_duration
|
||||
)
|
||||
db.session.add(lesson_stat)
|
||||
db.session.commit()
|
||||
logger.info(f"Statistik für Lektion {lesson_index} gespeichert")
|
||||
return lesson_stat
|
||||
|
||||
@staticmethod
|
||||
def get_lesson_statistics(lesson_index: int) -> List[LessonStatistic]:
|
||||
"""
|
||||
Get all statistics for a specific lesson
|
||||
|
||||
Args:
|
||||
lesson_index: Lesson index
|
||||
|
||||
Returns:
|
||||
List of LessonStatistic instances
|
||||
"""
|
||||
return LessonStatistic.query.filter_by(lesson_index=lesson_index)\
|
||||
.order_by(LessonStatistic.created_at.asc())\
|
||||
.all()
|
||||
|
||||
@staticmethod
|
||||
def get_lesson_statistics_data(lesson_index: int) -> Dict[str, List]:
|
||||
"""
|
||||
Get lesson statistics as formatted data for charts
|
||||
|
||||
Args:
|
||||
lesson_index: Lesson index
|
||||
|
||||
Returns:
|
||||
Dictionary with statistics data arrays
|
||||
"""
|
||||
stats = StatisticsService.get_lesson_statistics(lesson_index)
|
||||
return {
|
||||
'dates': [stat.created_at.strftime('%Y-%m-%d') for stat in stats],
|
||||
'chars_per_minute': [stat.chars_per_minute for stat in stats],
|
||||
'error_rate': [stat.error_rate for stat in stats],
|
||||
'wpm': [stat.wpm for stat in stats],
|
||||
'training_duration': [stat.training_duration for stat in stats]
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def has_lesson_statistics(lesson_index: int) -> bool:
|
||||
"""
|
||||
Check if lesson has any statistics
|
||||
|
||||
Args:
|
||||
lesson_index: Lesson index
|
||||
|
||||
Returns:
|
||||
True if lesson has statistics
|
||||
"""
|
||||
return LessonStatistic.query.filter_by(lesson_index=lesson_index).first() is not None
|
||||
|
||||
@staticmethod
|
||||
def delete_lesson_statistics(lesson_index: int) -> int:
|
||||
"""
|
||||
Delete all statistics for a lesson
|
||||
|
||||
Args:
|
||||
lesson_index: Lesson index
|
||||
|
||||
Returns:
|
||||
Number of deleted entries
|
||||
"""
|
||||
deleted_count = LessonStatistic.query.filter_by(lesson_index=lesson_index).delete()
|
||||
db.session.commit()
|
||||
logger.info(f"{deleted_count} Statistik-Einträge für Lektion {lesson_index} gelöscht")
|
||||
return deleted_count
|
||||
|
||||
@staticmethod
|
||||
def get_all_statistics() -> List[LessonStatistic]:
|
||||
"""
|
||||
Get all statistics ordered by lesson and creation date
|
||||
|
||||
Returns:
|
||||
List of all LessonStatistic instances
|
||||
"""
|
||||
return LessonStatistic.query.order_by(
|
||||
LessonStatistic.lesson_index.asc(),
|
||||
LessonStatistic.created_at.asc()
|
||||
).all()
|
||||
|
||||
@staticmethod
|
||||
def update_daily_practice(minutes: float) -> None:
|
||||
"""
|
||||
Update daily practice time
|
||||
|
||||
Args:
|
||||
minutes: Minutes practiced
|
||||
"""
|
||||
today = date.today()
|
||||
practice = DailyPractice.query.filter_by(date=today).first()
|
||||
|
||||
if practice:
|
||||
practice.minutes_practiced += minutes
|
||||
else:
|
||||
practice = DailyPractice(date=today, minutes_practiced=minutes)
|
||||
db.session.add(practice)
|
||||
|
||||
db.session.commit()
|
||||
logger.info(f"Tägliche Übungszeit aktualisiert: {minutes:.2f} Minuten")
|
||||
|
||||
@staticmethod
|
||||
def get_last_30_days_practice() -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get practice times for last 30 days
|
||||
|
||||
Returns:
|
||||
List of practice data dictionaries
|
||||
"""
|
||||
thirty_days_ago = date.today() - timedelta(days=29)
|
||||
practices = DailyPractice.query.filter(
|
||||
DailyPractice.date >= thirty_days_ago
|
||||
).order_by(DailyPractice.date.asc()).all()
|
||||
return [
|
||||
{'date': p.date.strftime('%Y-%m-%d'), 'minutes': p.minutes_practiced}
|
||||
for p in practices
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_full_30_days_practice() -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get complete 30 days of practice data (including days without practice)
|
||||
|
||||
Returns:
|
||||
List with all 30 days including zeros
|
||||
"""
|
||||
practice_data = StatisticsService.get_last_30_days_practice()
|
||||
today = date.today()
|
||||
full_data = []
|
||||
|
||||
for i in range(29, -1, -1): # From 29 days ago to today
|
||||
current_date = today - timedelta(days=i)
|
||||
date_str = current_date.strftime('%Y-%m-%d')
|
||||
|
||||
# Find data for this day
|
||||
matching = next((p for p in practice_data if p['date'] == date_str), None)
|
||||
|
||||
full_data.append({
|
||||
'date': date_str,
|
||||
'display_date': current_date.strftime('%d.%m'),
|
||||
'minutes': matching['minutes'] if matching else 0
|
||||
})
|
||||
|
||||
return full_data
|
||||
256
services/training_service.py
Normal file
256
services/training_service.py
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
"""
|
||||
Training Service
|
||||
Orchestrates training-related business logic using other services
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Any, Tuple
|
||||
from flask import session
|
||||
from services.lesson_service import LessonService
|
||||
from services.progress_service import ProgressService
|
||||
from services.statistics_service import StatisticsService
|
||||
from services.settings_service import SettingsService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TrainingService:
|
||||
"""High-level service for training operations"""
|
||||
|
||||
def __init__(self, lesson_service: LessonService):
|
||||
"""
|
||||
Initialize TrainingService
|
||||
|
||||
Args:
|
||||
lesson_service: LessonService instance
|
||||
"""
|
||||
self.lesson_service = lesson_service
|
||||
self.progress_service = ProgressService()
|
||||
self.statistics_service = StatisticsService()
|
||||
self.settings_service = SettingsService()
|
||||
|
||||
def get_training_context(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get complete training context for rendering training page
|
||||
|
||||
Returns:
|
||||
Dictionary with all training data
|
||||
"""
|
||||
# Get global progress
|
||||
progress = self.progress_service.get_or_create_global_progress()
|
||||
current_lesson_index = progress.current_text_index
|
||||
|
||||
# Get user settings
|
||||
user_settings_dict = self.settings_service.get_settings_dict()
|
||||
|
||||
# Restore saved lesson if initial
|
||||
if current_lesson_index == 0 and user_settings_dict['current_lesson_index'] > 0:
|
||||
current_lesson_index = user_settings_dict['current_lesson_index']
|
||||
progress.current_text_index = current_lesson_index
|
||||
from models import db
|
||||
db.session.commit()
|
||||
|
||||
# Validate lesson index
|
||||
if not self.lesson_service.validate_lesson_index(current_lesson_index):
|
||||
current_lesson_index = 0
|
||||
progress.current_text_index = 0
|
||||
from models import db
|
||||
db.session.commit()
|
||||
|
||||
# Get lesson info
|
||||
lesson_info = self.lesson_service.get_lesson_info(current_lesson_index)
|
||||
|
||||
# Get lesson progress
|
||||
progress_data = self.progress_service.get_lesson_progress_data(current_lesson_index)
|
||||
|
||||
# Synchronize session with DB
|
||||
session['total_elapsed_time'] = progress_data['total_elapsed_time']
|
||||
session['net_training_time'] = progress_data['total_elapsed_time']
|
||||
session['key_stroke_count'] = progress_data['key_stroke_count']
|
||||
session['is_paused'] = progress_data['is_paused']
|
||||
|
||||
return {
|
||||
'current_text': lesson_info['text'],
|
||||
'current_position': progress_data['current_position'],
|
||||
'last_text': progress_data['last_text'],
|
||||
'total_elapsed_time': progress_data['total_elapsed_time'],
|
||||
'net_training_time': progress_data['total_elapsed_time'],
|
||||
'key_stroke_count': progress_data['key_stroke_count'],
|
||||
'is_paused': progress_data['is_paused'],
|
||||
'lessons': self.lesson_service.get_all_lessons(),
|
||||
'current_lesson_title': lesson_info['title'],
|
||||
'current_lesson_index': current_lesson_index,
|
||||
'metronome_enabled': user_settings_dict['metronome_enabled'],
|
||||
'metronome_bpm': user_settings_dict['metronome_bpm'],
|
||||
'metronome_sound': user_settings_dict['metronome_sound'],
|
||||
'metronome_mode': user_settings_dict['metronome_mode'],
|
||||
'metronome_speed': user_settings_dict['metronome_speed'],
|
||||
'target_error_rate': user_settings_dict['target_error_rate'],
|
||||
'max_bpm_speed': user_settings_dict['max_bpm_speed'],
|
||||
'speed_display': user_settings_dict['speed_display']
|
||||
}
|
||||
|
||||
def update_training_progress(
|
||||
self,
|
||||
user_input: str,
|
||||
current_text: str,
|
||||
elapsed_time: float,
|
||||
cursor_position: int,
|
||||
total_elapsed_time: float,
|
||||
net_training_time: float,
|
||||
key_stroke_count: int,
|
||||
is_paused: bool
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Update training progress and calculate statistics
|
||||
|
||||
Args:
|
||||
user_input: User's typed text
|
||||
current_text: Target text
|
||||
elapsed_time: Time elapsed in seconds
|
||||
cursor_position: Current cursor position
|
||||
total_elapsed_time: Total elapsed time in milliseconds
|
||||
net_training_time: Net training time in milliseconds
|
||||
key_stroke_count: Number of keystrokes
|
||||
is_paused: Whether session is paused
|
||||
|
||||
Returns:
|
||||
Dictionary with statistics and completion status
|
||||
"""
|
||||
# Get current lesson
|
||||
lesson_index = self.progress_service.get_current_lesson_index()
|
||||
|
||||
# Update lesson progress
|
||||
self.progress_service.update_lesson_progress(
|
||||
lesson_index=lesson_index,
|
||||
cursor_position=cursor_position,
|
||||
user_input=user_input,
|
||||
total_elapsed_time=int(total_elapsed_time),
|
||||
key_stroke_count=key_stroke_count,
|
||||
is_paused=is_paused
|
||||
)
|
||||
|
||||
# Check if lesson is completed
|
||||
lesson_completed = self.progress_service.check_lesson_completed(
|
||||
cursor_position=cursor_position,
|
||||
user_input_length=len(user_input),
|
||||
lesson_text_length=len(current_text)
|
||||
)
|
||||
|
||||
if lesson_completed:
|
||||
logger.info(f"Lektion {lesson_index} abgeschlossen!")
|
||||
|
||||
# Calculate final statistics
|
||||
stats = self.statistics_service.calculate_typing_statistics(
|
||||
user_input, current_text, elapsed_time
|
||||
)
|
||||
|
||||
# Save statistics
|
||||
self.statistics_service.save_lesson_statistics(
|
||||
lesson_index=lesson_index,
|
||||
chars_per_minute=stats['typing_speed'],
|
||||
error_rate=stats['error_rate'],
|
||||
wpm=stats['words_per_minute'],
|
||||
training_duration=total_elapsed_time / 1000.0
|
||||
)
|
||||
|
||||
# Reset lesson progress
|
||||
self.progress_service.complete_lesson(lesson_index)
|
||||
|
||||
# Update session
|
||||
session['total_elapsed_time'] = total_elapsed_time
|
||||
session['net_training_time'] = net_training_time
|
||||
session['key_stroke_count'] = key_stroke_count
|
||||
session['is_paused'] = is_paused
|
||||
|
||||
# Calculate statistics
|
||||
stats = self.statistics_service.calculate_typing_statistics(
|
||||
user_input, current_text, elapsed_time
|
||||
)
|
||||
stats['lesson_completed'] = lesson_completed
|
||||
|
||||
return stats
|
||||
|
||||
def switch_to_next_lesson(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Switch to next lesson
|
||||
|
||||
Returns:
|
||||
Dictionary with new lesson data
|
||||
"""
|
||||
current_index = self.progress_service.get_current_lesson_index()
|
||||
new_index = self.lesson_service.get_next_lesson_index(current_index)
|
||||
|
||||
return self._switch_lesson(new_index, current_index)
|
||||
|
||||
def switch_to_lesson(self, lesson_index: int) -> Tuple[bool, Dict[str, Any]]:
|
||||
"""
|
||||
Switch to specific lesson
|
||||
|
||||
Args:
|
||||
lesson_index: Target lesson index
|
||||
|
||||
Returns:
|
||||
Tuple of (success, data dictionary)
|
||||
"""
|
||||
if not self.lesson_service.validate_lesson_index(lesson_index):
|
||||
return False, {'error': 'Ungültiger Lektionsindex'}
|
||||
|
||||
current_index = self.progress_service.get_current_lesson_index()
|
||||
return True, self._switch_lesson(lesson_index, current_index)
|
||||
|
||||
def _switch_lesson(self, new_index: int, old_index: int) -> Dict[str, Any]:
|
||||
"""
|
||||
Internal method to switch lessons
|
||||
|
||||
Args:
|
||||
new_index: New lesson index
|
||||
old_index: Old lesson index
|
||||
|
||||
Returns:
|
||||
Dictionary with new lesson data
|
||||
"""
|
||||
# Update global progress
|
||||
self.progress_service.set_current_lesson_index(new_index)
|
||||
|
||||
# Get new lesson progress
|
||||
progress_data = self.progress_service.get_lesson_progress_data(new_index)
|
||||
|
||||
# Synchronize session
|
||||
session['total_elapsed_time'] = progress_data['total_elapsed_time']
|
||||
session['net_training_time'] = progress_data['total_elapsed_time']
|
||||
session['key_stroke_count'] = progress_data['key_stroke_count']
|
||||
session['is_paused'] = progress_data['is_paused']
|
||||
|
||||
# Get lesson info
|
||||
lesson_info = self.lesson_service.get_lesson_info(new_index)
|
||||
|
||||
logger.info(f"Lektionswechsel: {old_index} -> {new_index}")
|
||||
|
||||
return {
|
||||
'text': lesson_info['text'],
|
||||
'index': new_index,
|
||||
'total': lesson_info['total'],
|
||||
'title': lesson_info['title'],
|
||||
'current_position': progress_data['current_position'],
|
||||
'last_text': progress_data['last_text'],
|
||||
'total_elapsed_time': progress_data['total_elapsed_time'],
|
||||
'key_stroke_count': progress_data['key_stroke_count'],
|
||||
'is_paused': progress_data['is_paused']
|
||||
}
|
||||
|
||||
def reset_current_lesson(self) -> Tuple[bool, Dict[str, Any]]:
|
||||
"""
|
||||
Reset current lesson progress
|
||||
|
||||
Returns:
|
||||
Tuple of (success, data dictionary)
|
||||
"""
|
||||
lesson_index = self.progress_service.get_current_lesson_index()
|
||||
self.progress_service.reset_lesson_progress(lesson_index)
|
||||
|
||||
return True, {
|
||||
'status': 'success',
|
||||
'message': 'Lektion zurückgesetzt',
|
||||
'lesson_index': lesson_index
|
||||
}
|
||||
|
|
@ -131,7 +131,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Zurück-Button -->
|
||||
<a href="{{ url_for('train') }}" class="btn btn-primary btn-lg back-button shadow">
|
||||
<a href="{{ url_for('training.train') }}" class="btn btn-primary btn-lg back-button shadow">
|
||||
← Zurück zum Trainer
|
||||
</a>
|
||||
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@
|
|||
<i data-lucide="pause" id="play-pause-pause" style="width: 24px; height: 24px; position: absolute; top: 0; left: 0;"></i>
|
||||
<i data-lucide="play" id="play-pause-play" style="width: 24px; height: 24px; position: absolute; top: 0; left: 0; display: none;"></i>
|
||||
</div>
|
||||
<a href="{{ url_for('hilfe') }}"
|
||||
<a href="{{ url_for('main.hilfe') }}"
|
||||
style="width: 24px; height: 24px; cursor: pointer; color: white; text-decoration: none; display: inline-block;"
|
||||
aria-label="Hilfe öffnen"
|
||||
title="Hilfe öffnen">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue