import os import random import time import psycopg2 from flask import Flask, render_template, request, jsonify from difflib import SequenceMatcher app = Flask(__name__) # --- DATENBANK VERBINDUNG --- def get_db_connection(): """Stellt Verbindung zur Postgres-DB her, mit Warteschleife beim Start.""" 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: if i < max_retries - 1: print("Datenbank noch nicht bereit... warte 2 Sekunden.") time.sleep(2) continue else: print("FEHLER: Konnte keine Verbindung zur Datenbank herstellen.") raise # --- RESET LOGIK (STATELESS CONTAINER) --- def reset_db_on_startup(): """Löscht beim Start alles und lädt die init.sql neu.""" print("Starte Datenbank-Reset...") try: conn = get_db_connection() cur = conn.cursor() # Wir lesen die init.sql und führen sie aus. # Voraussetzung: In init.sql steht am Anfang 'DROP TABLE IF EXISTS ...' if os.path.exists('init.sql'): with open('init.sql', 'r', encoding='utf-8') as f: sql_commands = f.read() cur.execute(sql_commands) conn.commit() print("Datenbank erfolgreich neu aufgebaut (Clean State)!") else: print("WARNUNG: init.sql nicht gefunden! Datenbank könnte leer sein.") cur.close() conn.close() except Exception as e: print(f"KRITISCHER FEHLER beim DB-Reset: {e}") # Dies wird ausgeführt, sobald der Container startet (bevor der Webserver läuft) reset_db_on_startup() # --- HILFSFUNKTIONEN --- def expand_variants(text): """Zerlegt 'Wort1; Wort2' in eine Liste für den Vergleich.""" parts = text.split(';') results = [] for p in parts: clean = p.strip() if clean: results.append(clean) # Umgang mit Klammern z.B. "(to) go" -> "to go" und "go" final_res = [] for r in results: final_res.append(r) if '(' in r: without_parens = r.replace('(', '').replace(')', '').strip() final_res.append(without_parens) return final_res def clean_text_for_comparison(text): """Macht Text klein und entfernt Leerzeichen.""" return text.lower().strip() # --- ROUTEN --- @app.route('/') def index(): return render_template('index.html') @app.route('/api/pages') def get_pages(): """Liefert verfügbare Buchseiten.""" conn = get_db_connection() cur = conn.cursor() # Seiten aus der Vokabel-Tabelle cur.execute("SELECT DISTINCT page FROM vocabulary ORDER BY page") rows = cur.fetchall() # Seiten aus der Verben-Tabelle (falls vorhanden) cur.execute("SELECT DISTINCT page FROM irregular_verbs ORDER BY page") rows_verbs = cur.fetchall() cur.close() conn.close() # Listen zusammenführen und sortieren pages = set([r[0] for r in rows]) for r in rows_verbs: pages.add(r[0]) sorted_pages = sorted(list(pages)) return jsonify(sorted_pages) @app.route('/api/question', methods=['POST']) def get_question(): """Holt eine zufällige Frage basierend auf Modus und Seiten.""" data = request.json mode = data.get('mode', 'random') pages = data.get('pages', []) # Fallback: Wenn keine Seiten gewählt, nimm einfach Seite 206 (oder leer) if not pages: # Optional: Hier könnte man "alle" Logik einbauen, # aber Frontend sendet meistens was. pass conn = get_db_connection() cur = conn.cursor() # --- MODUS: UNREGELMÄSSIGE VERBEN --- if 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 auf diesen Seiten gefunden (Wähle 206 oder 207).'}) return jsonify({ 'type': 'irregular', 'question': row[0], # Infinitive (z.B. "go") 'german_hint': row[3], # Deutsch (z.B. "gehen") 'answer_simple': row[1], # Simple Past (z.B. "went") 'answer_participle': row[2], # Past Participle (z.B. "gone") 'page': row[4] }) # --- MODUS: NORMALE VOKABELN --- 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 auf den gewählten Seiten gefunden.'}) question_text = "" answer_text = "" if mode == 'de-en': question_text = row[1] # Deutsch answer_text = row[0] # Englisch elif mode == 'en-de': question_text = row[0] # Englisch answer_text = row[1] # Deutsch else: # random if random.random() > 0.5: question_text = row[1] answer_text = row[0] else: question_text = row[0] answer_text = row[1] return jsonify({ 'type': 'vocab', 'question': question_text, 'answer': answer_text, 'page': row[2] }) @app.route('/api/check', methods=['POST']) def check_answer(): """Prüft die Antwort des Nutzers.""" data = request.json # --- CHECK: UNREGELMÄSSIGE VERBEN --- if data.get('type') == 'irregular': user_simple = clean_text_for_comparison(data.get('input_simple', '')) user_participle = clean_text_for_comparison(data.get('input_participle', '')) correct_simple = data.get('correct_simple', '') correct_participle = data.get('correct_participle', '') # Hilfsfunktion: Prüft ob Eingabe in "was/were" enthalten ist def check_part(user_in, correct_str): # correct_str könnte "was/were" oder "burnt/burned" sein variants = [x.strip().lower() for x in correct_str.split('/')] return user_in in variants is_simple_ok = check_part(user_simple, correct_simple) is_participle_ok = check_part(user_participle, correct_participle) if is_simple_ok and is_participle_ok: return jsonify({'status': 'correct', 'msg': 'Perfekt! Beide Formen richtig.'}) else: # Detailliertes Feedback, was falsch war msg = "Leider falsch." if not is_simple_ok: msg += f" Simple Past war '{user_simple}' (Richtig: {correct_simple})." if not is_participle_ok: msg += f" Participle war '{user_participle}' (Richtig: {correct_participle})." return jsonify({ 'status': 'wrong', 'msg': f'Lösung: {correct_simple} | {correct_participle}' }) # --- CHECK: NORMALE VOKABELN --- else: user_input = data.get('input', '') db_answer = data.get('correct', '') clean_user = clean_text_for_comparison(user_input) valid_answers_raw = expand_variants(db_answer) valid_answers_clean = [clean_text_for_comparison(v) for v in valid_answers_raw] if clean_user in valid_answers_clean: return jsonify({'status': 'correct', 'msg': 'Richtig!'}) # Fuzzy Logic (Tippfehler erkennen) best_ratio = 0 best_match = "" for correct_option in valid_answers_clean: ratio = SequenceMatcher(None, clean_user, correct_option).ratio() if ratio > best_ratio: best_ratio = ratio best_match = correct_option threshold = 0.85 if len(best_match) <= 4: threshold = 0.9 # Bei kurzen Wörtern strenger sein if best_ratio >= threshold: return jsonify({ 'status': 'typo', 'msg': f'Fast richtig! Meintest du: "{best_match}"? (Es war "{user_input}")' }) return jsonify({'status': 'wrong', 'msg': f'Falsch. Lösung: {db_answer}'}) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000)