Fix & Cleanup: weiterer stale-Import-Bug + Code-Bereinigung
Fix: - routes/statistics.py hatte denselben stale-Import-Bug wie Bug 1: 'from services.helpers import lessons' band zur Importzeit an die leere Liste, und 'LessonService(lessons)' wurde eager auf Modulebene gebaut. Folge: Statistik-Seite zeigte keine Lektionen, lesson_has_data leer, CSV/JSON-Export lieferte stets "Unbekannte Lektion". Behoben via Lazy-Init (get_lesson_service) + helpers.lessons. Cleanup: - Tote Duplikat-Funktionen aus helpers.py entfernt (calculate_statistics, get_or_create_user_settings, save_user_settings, get_or_create_lesson_progress, update_daily_practice, get_last_30_days_practice) - alle in den Service-Klassen vorhanden; ungenutzte Imports (date, timedelta, Optional) entfernt - Funktionslosen before_app_request (no-op) aus utils.py entfernt - repair_json-Regex robuster: quotet nur echte Key-Positionen statt jeden Doppelpunkt-Vorgänger; idempotent, zerstört keine Werte (Uhrzeiten, URLs) mehr Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8c621b23cb
commit
f5751374ba
3 changed files with 25 additions and 144 deletions
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue