diff --git a/routes/statistics.py b/routes/statistics.py index 6c34e73..6e945cd 100644 --- a/routes/statistics.py +++ b/routes/statistics.py @@ -10,7 +10,7 @@ from io import StringIO from datetime import datetime from flask import Blueprint, render_template, request, jsonify, session, Response, make_response from models import db -from services.helpers import lessons +from services import helpers from services.statistics_service import StatisticsService from services.progress_service import ProgressService from services.lesson_service import LessonService @@ -22,7 +22,18 @@ statistics_bp = Blueprint('statistics', __name__) # Initialize services statistics_service = StatisticsService() progress_service = ProgressService() -lesson_service = LessonService(lessons) + +# LessonService lazy initialisieren, da helpers.lessons erst nach +# load_lessons() befüllt ist (sonst leere Liste zur Importzeit). +_lesson_service = None + + +def get_lesson_service() -> LessonService: + """Lazy-Initialisierung des LessonService mit aktuellen Lektionen""" + global _lesson_service + if _lesson_service is None: + _lesson_service = LessonService(helpers.lessons) + return _lesson_service @statistics_bp.route('/statistics') @@ -35,14 +46,14 @@ def statistics(): # Check which lessons have statistics lesson_has_data = [ statistics_service.has_lesson_statistics(i) - for i in range(lesson_service.get_lesson_count()) + for i in range(get_lesson_service().get_lesson_count()) ] # Get speed display setting speed_display = session.get('speed_display', 'zpm') return render_template('statistics.html', - lessons=lessons, + lessons=helpers.lessons, current_lesson_index=current_lesson_index, lesson_has_data=lesson_has_data, speed_display=speed_display) @@ -84,7 +95,7 @@ def reset_lesson_statistics(): except (ValueError, TypeError): return jsonify({'error': 'Ungültiger Lektionsindex'}), 400 - if not lesson_service.validate_lesson_index(lesson_index): + if not get_lesson_service().validate_lesson_index(lesson_index): return jsonify({'error': 'Lektionsindex außerhalb des gültigen Bereichs'}), 400 # Delete statistics @@ -119,7 +130,7 @@ def export_statistics_csv() -> Response: # Data for stat in stats: - lesson_title = lesson_service.get_lesson_title(stat.lesson_index) + lesson_title = get_lesson_service().get_lesson_title(stat.lesson_index) writer.writerow([ stat.created_at.strftime('%Y-%m-%d %H:%M:%S'), @@ -158,7 +169,7 @@ def export_statistics_json() -> Response: } for stat in stats: - lesson_title = lesson_service.get_lesson_title(stat.lesson_index) + lesson_title = get_lesson_service().get_lesson_title(stat.lesson_index) export_data['statistics'].append({ 'date': stat.created_at.strftime('%Y-%m-%d %H:%M:%S'), diff --git a/routes/utils.py b/routes/utils.py index fe48b30..5df1f97 100644 --- a/routes/utils.py +++ b/routes/utils.py @@ -150,10 +150,3 @@ def shutdown(): except Exception as e: logger.error(f"Fehler beim Shutdown: {e}", exc_info=True) return jsonify({'error': str(e)}), 500 - - -# Disable CSRF for shutdown route -@utils_bp.before_app_request -def exempt_shutdown_from_csrf(): - """Exempts shutdown route from CSRF protection""" - pass diff --git a/services/helpers.py b/services/helpers.py index 90e1c71..5d7ace1 100644 --- a/services/helpers.py +++ b/services/helpers.py @@ -8,8 +8,7 @@ import json import re import logging import secrets -from typing import Dict, List, Any, Optional -from datetime import date, timedelta +from typing import Dict, List, Any from sqlalchemy import inspect # Logging @@ -93,8 +92,12 @@ def repair_json(json_string: str) -> str: # Repariere fehlende Kommas am Ende von Objekten im Array repaired = re.sub(r'\}\s*\n\s*\]', '}\n]', repaired) - # Repariere fehlende Anführungszeichen bei Keys - repaired = re.sub(r'(\w+)\s*:', r'"\1":', repaired) + # Repariere fehlende Anführungszeichen bei Keys. + # Nur Bezeichner quoten, die an einer Key-Position stehen (direkt nach + # '{' oder ','). Dadurch werden bereits gequotete Keys und Doppelpunkte + # in Werten (z.B. Uhrzeiten "10:30", URLs) nicht zerstört; die Operation + # ist idempotent für bereits gültiges JSON. + repaired = re.sub(r'([{,]\s*)([A-Za-z_]\w*)(\s*:)', r'\1"\2"\3', repaired) # Repariere fehlende schließende Klammern brackets_open = repaired.count('[') + repaired.count('{') @@ -208,85 +211,6 @@ def create_sample_lessons() -> List[Dict[str, Any]]: return FALLBACK_LESSONS -def get_or_create_user_settings(db, UserSettings): - """Holt oder erstellt die Benutzereinstellungen""" - settings = UserSettings.query.first() - if not settings: - settings = UserSettings() - db.session.add(settings) - db.session.commit() - logger.info("Neue Benutzereinstellungen erstellt") - return settings - - -def get_or_create_lesson_progress(db, LessonProgress, lesson_index: int): - """Holt oder erstellt den Fortschritt für eine bestimmte Lektion""" - 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 - - -def save_user_settings( - db, - UserSettings, - current_lesson_index: int, - metronome_enabled: bool, - metronome_bpm: int, - metronome_sound: str, - metronome_mode: str, - metronome_speed: int, - target_error_rate: int, - max_bpm_speed: int, - speed_display: str -) -> None: - """Speichert Benutzereinstellungen in der Datenbank""" - settings = get_or_create_user_settings(db, UserSettings) - settings.current_lesson_index = current_lesson_index - settings.metronome_enabled = metronome_enabled - settings.metronome_bpm = metronome_bpm - settings.metronome_sound = metronome_sound - settings.metronome_mode = metronome_mode - settings.metronome_speed = metronome_speed - settings.target_error_rate = target_error_rate - settings.max_bpm_speed = max_bpm_speed - settings.speed_display = speed_display - db.session.commit() - logger.info("Benutzereinstellungen gespeichert") - - -def update_daily_practice(db, DailyPractice, minutes: float) -> None: - """Aktualisiert die tägliche Übungszeit""" - 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") - - -def get_last_30_days_practice(DailyPractice) -> List[Dict[str, Any]]: - """Holt die Übungszeiten der letzten 30 Tage""" - 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] - - def check_database_status(db_engine): """ Prüft ob die Datenbank initialisiert ist und ob Migrationen ausstehen. @@ -331,50 +255,3 @@ def check_database_status(db_engine): logger.error(f"Fehler bei Datenbank-Prüfung: {e}") logger.warning("Falls Datenbank nicht existiert, führe aus: alembic upgrade head") return False - - -def calculate_statistics(user_input: str, current_text: str, elapsed_time: float) -> Dict[str, Any]: - """Berechnet Tippstatistiken mit echter Zeitmessung""" - correct_chars = 0 - incorrect_chars = 0 - total_chars = len(user_input) - - # Zähle korrekte und falsche Zeichen - for i, char in enumerate(user_input): - if i < len(current_text) and char == current_text[i]: - correct_chars += 1 - else: - incorrect_chars += 1 - - # Berechne Metriken basierend auf echter Zeit - # Mindestzeit von 1 Sekunde, um unrealistische Werte zu vermeiden - if total_chars > 0 and elapsed_time >= 1.0: - # Zeichen pro Minute (ganzzahlig) - chars_per_minute = (total_chars / elapsed_time) * 60 - # Wörter pro Minute gemäß Formel: (Gesamtzahl der korrekt getippten Zeichen/5)/((Zeit in Sekunden)/60) - words_per_minute = (correct_chars / 5) / (elapsed_time / 60) - # Durchschnittliche Zeit pro Zeichen - duration_per_char = elapsed_time / total_chars - - # Begrenze auf realistische Maximalwerte (z.B. max 1000 ZPM) - 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 - - # Fehlerrate und Genauigkeit - 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), # Ganzzahlig - 'duration_per_char': round(duration_per_char, 3), - 'words_per_minute': round(words_per_minute, 2), # Auf zwei Nachkommastellen - 'accuracy': round(accuracy, 2) - }