import os import random import time import psycopg2 from flask import Flask, render_template, request, jsonify from difflib import SequenceMatcher app = Flask(__name__) # --- DB VERBINDUNG & RESET --- def get_db_connection(): max_retries = 10 for i in range(max_retries): try: conn = psycopg2.connect( host=os.environ.get('DB_HOST', 'postgres'), database=os.environ.get('DB_NAME', 'englisch6a'), user=os.environ.get('DB_USER', 'englisch6a'), password=os.environ.get('DB_PASS', 'englisch') ) return conn except psycopg2.OperationalError: time.sleep(2) continue raise Exception("DB Connection failed") def reset_db_on_startup(): try: conn = get_db_connection() cur = conn.cursor() if os.path.exists('init.sql'): with open('init.sql', 'r', encoding='utf-8') as f: cur.execute(f.read()) conn.commit() print("DB Reset erfolgreich.") cur.close() conn.close() except Exception as e: print(f"DB Reset Fehler: {e}") reset_db_on_startup() # --- HILFSFUNKTIONEN --- def clean_text(text): """Basis-Reinigung für Verben-Input""" return text.lower().strip() def clean_vocab_input(text): """ Spezielle Reinigung für Vokabeln: 1. Kleinbuchstaben & Leerzeichen weg. 2. Führendes 'to ' entfernen (damit 'go' == 'to go'). 3. Klammern entfernen (z.B. '(to)' -> weg). """ t = text.lower().strip() # Klammern entfernen t = t.replace('(', '').replace(')', '').replace('[', '').replace(']', '') t = t.strip() # Führendes "to " entfernen (nur wenn es ein ganzes Wort ist) if t.startswith('to '): t = t[3:].strip() return t def check_part(user_in, correct_str): variants = [x.strip().lower() for x in correct_str.split('/')] return user_in in variants # --- API ROUTEN --- @app.route('/') def index(): return render_template('index.html') @app.route('/api/pages') def get_pages(): mode_type = request.args.get('type', 'vocab') conn = get_db_connection() cur = conn.cursor() if mode_type == 'irregular': cur.execute("SELECT DISTINCT page FROM irregular_verbs ORDER BY page") else: cur.execute("SELECT DISTINCT page FROM vocabulary ORDER BY page") rows = cur.fetchall() cur.close() conn.close() pages = sorted([r[0] for r in rows]) return jsonify(pages) @app.route('/api/question', methods=['POST']) def get_question(): data = request.json main_mode = data.get('mainMode') sub_mode = data.get('subMode') pages = data.get('pages', []) conn = get_db_connection() cur = conn.cursor() if main_mode == 'irregular': query = "SELECT infinitive, simple_past, past_participle, german, page FROM irregular_verbs WHERE page = ANY(%s) ORDER BY RANDOM() LIMIT 1" cur.execute(query, (pages,)) row = cur.fetchone() cur.close() conn.close() if not row: return jsonify({'error': 'Keine Verben gefunden.'}) if sub_mode == 'start-german': return jsonify({ 'type': 'irregular_full', 'question': row[3], 'answer_infinitive': row[0], 'answer_simple': row[1], 'answer_participle': row[2], 'page': row[4] }) else: return jsonify({ 'type': 'irregular_standard', 'question': row[0], 'german_hint': row[3], 'answer_simple': row[1], 'answer_participle': row[2], 'page': row[4] }) else: query = "SELECT english, german, page FROM vocabulary WHERE page = ANY(%s) ORDER BY RANDOM() LIMIT 1" cur.execute(query, (pages,)) row = cur.fetchone() cur.close() conn.close() if not row: return jsonify({'error': 'Keine Vokabeln gefunden.'}) q_text, a_text = "", "" if sub_mode == 'de-en': q_text, a_text = row[1], row[0] elif sub_mode == 'en-de': q_text, a_text = row[0], row[1] else: if random.random() > 0.5: q_text, a_text = row[1], row[0] else: q_text, a_text = row[0], row[1] return jsonify({ 'type': 'vocab', 'question': q_text, 'answer': a_text, 'page': row[2] }) @app.route('/api/check', methods=['POST']) def check_answer(): data = request.json q_type = data.get('type') # --- 1. & 2. VERBEN (Keine Typos, strikt) --- if q_type.startswith('irregular'): is_correct = False msg = "" if q_type == 'irregular_full': u_inf = clean_text(data.get('infinitive', '')) u_simp = clean_text(data.get('simple', '')) u_part = clean_text(data.get('participle', '')) ok_inf = check_part(u_inf, data.get('correct_infinitive', '')) ok_simp = check_part(u_simp, data.get('correct_simple', '')) ok_part = check_part(u_part, data.get('correct_participle', '')) if ok_inf and ok_simp and ok_part: is_correct = True elif q_type == 'irregular_standard': u_simp = clean_text(data.get('simple', '')) u_part = clean_text(data.get('participle', '')) ok_simp = check_part(u_simp, data.get('correct_simple', '')) ok_part = check_part(u_part, data.get('correct_participle', '')) if ok_simp and ok_part: is_correct = True if is_correct: return jsonify({'status': 'correct', 'msg': 'Richtig!'}) else: # Bei Verben schicken wir KEINE Lösung im msg Feld, das macht das Frontend erst am Ende return jsonify({'status': 'wrong', 'msg': 'Leider nicht ganz richtig.'}) # --- 3. VOKABELN (Mit "to"-Logik und Typos) --- else: u_input_raw = data.get('input', '') correct_raw = data.get('correct', '') # Bereinigter Vergleich (ohne "to ", ohne Klammern) u_clean = clean_vocab_input(u_input_raw) # Antwortmöglichkeiten vorbereiten (Splitten bei ';') valid_answers = [] raw_parts = correct_raw.split(';') for p in raw_parts: # Original rein valid_answers.append(clean_vocab_input(p)) # Falls "(to) go" drin stand -> das wird durch clean_vocab_input schon zu "go" # Aber falls "to go" drin stand -> wird zu "go" # 1. Direkter Treffer if u_clean in valid_answers: return jsonify({'status': 'correct', 'msg': 'Richtig!'}) # 2. Fuzzy Match (Rechtschreibprüfung) best_ratio = 0 best_match = "" for v in valid_answers: ratio = SequenceMatcher(None, u_clean, v).ratio() if ratio > best_ratio: best_ratio = ratio best_match = v # Das bereinigte Wort als Vorschlag # Toleranz: 85% Übereinstimmung, bei kurzen Wörtern (<5 Zeichen) strenger (90%) threshold = 0.85 if len(best_match) < 5: threshold = 0.9 if best_ratio >= threshold: return jsonify({ 'status': 'typo', 'msg': f'Fast richtig! Meintest du: "{best_match}"? (Achte auf die Schreibweise)' }) # 3. Falsch return jsonify({'status': 'wrong', 'msg': 'Leider falsch.'}) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000)