latein6/app/app.py
2026-01-28 22:15:56 +00:00

234 lines
7.1 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 normalize_latin(text):
"""Entfernt Makrons für den toleranten Vergleich (ā -> a)"""
if not text: return ""
replacements = {
'ā': 'a', 'ē': 'e', 'ī': 'i', 'ō': 'o', 'ū': 'u',
'Ā': 'A', 'Ē': 'E', 'Ī': 'I', 'Ō': 'O', 'Ū': 'U',
'ȳ': 'y', 'Ȳ': 'Y'
}
t = text
for k, v in replacements.items():
t = t.replace(k, v)
return t
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):
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):
# 1. Normalisieren (Striche wegdenken für Vergleich)
u_norm = normalize_latin(user_in.strip().lower())
c_norm = normalize_latin(correct.strip().lower())
# 2. Exakter Abgleich (auf Basis der normalisierten Version)
if u_norm == c_norm: return 'correct'
def get_parts(s): return [x.strip() for x in s.split(',') if x.strip()]
u_parts = get_parts(u_norm)
c_parts = get_parts(c_norm)
if not u_parts: return 'wrong'
all_user_parts_correct = all(p in c_parts for p in u_parts)
if all_user_parts_correct:
if len(u_parts) < len(c_parts): return 'incomplete'
return 'correct'
ratio = SequenceMatcher(None, u_norm, c_norm).ratio()
if ratio > 0.85: return 'typo'
return 'wrong'
def check_string_match(user_in, correct):
u_norm = normalize_latin(user_in.strip().lower())
c_norm = normalize_latin(correct.strip().lower())
if not u_norm: return 'wrong'
if u_norm == c_norm: return 'correct'
ratio = SequenceMatcher(None, u_norm, c_norm).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)
results = [None] * len(inputs)
correct_items = 0
total_items = len(inputs)
has_incomplete = False
# 1. GRAMMATIK
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_user_inputs = []
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', ''))
used_pool_indices = set()
for u_item in german_user_inputs:
idx = u_item['index']
val = u_item['value']
found_match = False
for pool_i, pool_val in enumerate(german_correct_pool):
if pool_i in used_pool_indices: continue
status = check_string_match(val, pool_val)
if status in ['correct', 'typo']:
results[idx] = {'status': status, 'correct': pool_val}
used_pool_indices.add(pool_i)
correct_items += 1
found_match = True
break
if not found_match:
original_correct = inputs[idx].get('correct', '')
results[idx] = {'status': 'wrong', 'correct': original_correct}
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__':
app.run(host='0.0.0.0', port=5000)