245 lines
7.7 KiB
Python
245 lines
7.7 KiB
Python
import os
|
|
import random
|
|
import time
|
|
import re
|
|
import psycopg2
|
|
from flask import Flask, render_template, request, jsonify
|
|
from difflib import SequenceMatcher
|
|
|
|
app = Flask(__name__)
|
|
|
|
def get_db_connection():
|
|
max_retries = 10
|
|
for i in range(max_retries):
|
|
try:
|
|
conn = psycopg2.connect(
|
|
host=os.environ.get('DB_HOST', 'postgres-lat'),
|
|
database=os.environ.get('DB_NAME', 'latein'),
|
|
user=os.environ.get('DB_USER', 'latein'),
|
|
password=os.environ.get('DB_PASS', 'latein')
|
|
)
|
|
return conn
|
|
except psycopg2.OperationalError:
|
|
time.sleep(2)
|
|
continue
|
|
raise Exception("DB Connection failed")
|
|
|
|
def init_db():
|
|
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()
|
|
cur.close()
|
|
conn.close()
|
|
|
|
init_db()
|
|
|
|
# --- HELPER ---
|
|
|
|
def is_middle_relevant(text):
|
|
if not text: return False
|
|
signals = [' m', ' f', ' n', 'm.', 'f.', 'n.', 'pl', 'sg', 'gen', 'dat', 'akk', 'abl', 'perf', 'ppp']
|
|
t = text.lower()
|
|
ignore = ['adv', 'präp', 'konj', 'subj', 'interj']
|
|
is_signal = any(s in t for s in signals)
|
|
is_ignore = any(i in t for i in ignore)
|
|
if is_ignore and not is_signal: return False
|
|
return True
|
|
|
|
def clean_german_answers(text):
|
|
# Alles in Klammern entfernen und splitten
|
|
text = re.sub(r'\(.*?\)', '', text)
|
|
text = re.sub(r'\[.*?\]', '', text)
|
|
parts = re.split(r'[;,]', text)
|
|
return [p.strip() for p in parts if p.strip()]
|
|
|
|
def check_middle_logic(user_in, correct):
|
|
"""Prüft Grammatik (erlaubt unvollständige Eingaben)"""
|
|
u = user_in.strip().lower()
|
|
c = correct.strip().lower()
|
|
|
|
if u == c: return 'correct'
|
|
|
|
def get_parts(s): return [x.strip() for x in s.split(',') if x.strip()]
|
|
|
|
u_parts = get_parts(u)
|
|
c_parts = get_parts(c)
|
|
|
|
if not u_parts: return 'wrong'
|
|
|
|
# Sind alle eingegebenen Teile in der Lösung vorhanden?
|
|
all_user_parts_correct = all(p in c_parts for p in u_parts)
|
|
|
|
if all_user_parts_correct:
|
|
# Wenn weniger Teile als nötig -> unvollständig
|
|
if len(u_parts) < len(c_parts):
|
|
return 'incomplete'
|
|
return 'correct'
|
|
|
|
# Tippfehler-Check
|
|
ratio = SequenceMatcher(None, u, c).ratio()
|
|
if ratio > 0.85: return 'typo'
|
|
|
|
return 'wrong'
|
|
|
|
def check_string_match(user_in, correct):
|
|
"""Standard String Vergleich"""
|
|
u = user_in.strip().lower()
|
|
c = correct.strip().lower()
|
|
if not u: return 'wrong'
|
|
|
|
if u == c: return 'correct'
|
|
|
|
ratio = SequenceMatcher(None, u, c).ratio()
|
|
if ratio > 0.85: return 'typo'
|
|
return 'wrong'
|
|
|
|
# --- ROUTES ---
|
|
|
|
@app.route('/')
|
|
def index():
|
|
return render_template('index.html')
|
|
|
|
@app.route('/api/lessons')
|
|
def get_lessons():
|
|
conn = get_db_connection()
|
|
cur = conn.cursor()
|
|
cur.execute("SELECT DISTINCT lesson FROM vocabulary")
|
|
rows = cur.fetchall()
|
|
cur.close()
|
|
conn.close()
|
|
lessons = [r[0] for r in rows]
|
|
def sort_key(val):
|
|
if val.isdigit(): return (0, int(val))
|
|
if val.startswith("Original"): return (2, val)
|
|
return (1, val)
|
|
lessons.sort(key=sort_key)
|
|
return jsonify(lessons)
|
|
|
|
@app.route('/api/question', methods=['POST'])
|
|
def get_question():
|
|
data = request.json
|
|
selected_lessons = data.get('lessons', [])
|
|
ask_middle_setting = data.get('askMiddle', True)
|
|
|
|
conn = get_db_connection()
|
|
cur = conn.cursor()
|
|
query = "SELECT id, lesson, latin, middle, german FROM vocabulary WHERE lesson = ANY(%s) ORDER BY RANDOM() LIMIT 1"
|
|
cur.execute(query, (selected_lessons,))
|
|
row = cur.fetchone()
|
|
cur.close()
|
|
conn.close()
|
|
|
|
if not row: return jsonify({'error': 'Keine Vokabeln gefunden'})
|
|
|
|
vid, lesson, latin, middle, german_raw = row
|
|
german_answers = clean_german_answers(german_raw)
|
|
|
|
middle_req = False
|
|
hint = ""
|
|
if middle:
|
|
if ask_middle_setting and is_middle_relevant(middle):
|
|
middle_req = True
|
|
else:
|
|
hint = middle
|
|
|
|
return jsonify({
|
|
'id': vid,
|
|
'latin': latin,
|
|
'hint': hint,
|
|
'middle_req': middle_req,
|
|
'middle_correct': middle if middle_req else "",
|
|
'german_count': len(german_answers),
|
|
'german_correct': german_answers
|
|
})
|
|
|
|
@app.route('/api/check', methods=['POST'])
|
|
def check():
|
|
data = request.json
|
|
inputs = data.get('inputs', [])
|
|
attempt_num = data.get('attempt', 1)
|
|
|
|
# Wir initialisieren die Ergebnis-Liste mit "falsch", damit wir sie später füllen können
|
|
results = [None] * len(inputs)
|
|
|
|
correct_items = 0
|
|
total_items = len(inputs)
|
|
has_incomplete = False
|
|
|
|
# 1. GRAMMATIK (Mitte) direkt prüfen (feste Position)
|
|
for i, inp in enumerate(inputs):
|
|
if inp.get('type') == 'middle':
|
|
status = check_middle_logic(inp.get('value', ''), inp.get('correct', ''))
|
|
msg = 'Unvollständig! Da fehlt noch etwas.' if status == 'incomplete' else ''
|
|
if status == 'incomplete': has_incomplete = True
|
|
|
|
results[i] = {'status': status, 'correct': inp.get('correct', ''), 'msg': msg}
|
|
if status in ['correct', 'typo']: correct_items += 1
|
|
|
|
# 2. DEUTSCH (German) - Pool-Logik für Reihenfolge-Unabhängigkeit
|
|
|
|
# Sammle alle deutschen User-Eingaben und ihre Indizes
|
|
german_user_inputs = [] # Liste von {'index': 1, 'value': '...'}
|
|
# Sammle alle korrekten deutschen Lösungen (Pool)
|
|
german_correct_pool = []
|
|
|
|
for i, inp in enumerate(inputs):
|
|
if inp.get('type') == 'german' or inp.get('type') is None:
|
|
german_user_inputs.append({'index': i, 'value': inp.get('value', '')})
|
|
german_correct_pool.append(inp.get('correct', ''))
|
|
|
|
# Welche Pool-Einträge wurden schon "verbraucht"?
|
|
used_pool_indices = set()
|
|
|
|
# Jetzt prüfen wir jede User-Eingabe gegen den GANZEN Pool
|
|
for u_item in german_user_inputs:
|
|
idx = u_item['index']
|
|
val = u_item['value']
|
|
found_match = False
|
|
matched_correct_val = ""
|
|
|
|
# Suche im Pool nach einem Match, der noch nicht benutzt wurde
|
|
for pool_i, pool_val in enumerate(german_correct_pool):
|
|
if pool_i in used_pool_indices:
|
|
continue
|
|
|
|
# Standard Prüfung für Deutsch
|
|
status = check_string_match(val, pool_val)
|
|
|
|
if status in ['correct', 'typo']:
|
|
# Treffer!
|
|
results[idx] = {'status': status, 'correct': pool_val}
|
|
used_pool_indices.add(pool_i) # Lösung als "benutzt" markieren
|
|
correct_items += 1
|
|
found_match = True
|
|
break
|
|
|
|
# Wenn KEIN Match im Pool gefunden wurde
|
|
if not found_match:
|
|
# Wir zeigen als Lösung einfach die an, die ursprünglich an dieser Position stand
|
|
# (auch wenn die Position eigentlich egal ist, irgendwas müssen wir anzeigen)
|
|
original_correct = inputs[idx].get('correct', '')
|
|
results[idx] = {'status': 'wrong', 'correct': original_correct}
|
|
|
|
# Punkteberechnung
|
|
score_fraction = 0
|
|
if total_items > 0:
|
|
base_score = correct_items / total_items
|
|
if attempt_num > 1:
|
|
base_score = base_score * 0.5
|
|
score_fraction = base_score
|
|
|
|
all_correct = (correct_items == total_items)
|
|
|
|
return jsonify({
|
|
'results': results,
|
|
'all_correct': all_correct,
|
|
'score': score_fraction,
|
|
'has_incomplete': has_incomplete
|
|
})
|
|
|
|
if __name__ == '__main__':
|
|
# Production settings
|
|
app.run(host='0.0.0.0', port=5000) |