diff --git a/app/app.py b/app/app.py new file mode 100644 index 0000000..f0585b9 --- /dev/null +++ b/app/app.py @@ -0,0 +1,206 @@ +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() + +# Start-Initialisierung +init_db() + +# --- HELPER FUNKTIONEN --- + +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): + """Prüft auf Unvollständigkeit (Komma-Trennung)""" + 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' + + 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, c).ratio() + if ratio > 0.85: return 'typo' + + return 'wrong' + +def check_string_match(user_in, correct): + u = user_in.strip().lower() + c = correct.strip().lower() + if u == c: return 'correct' + ratio = SequenceMatcher(None, u, c).ratio() + if ratio > 0.85: return 'typo' + return 'wrong' + +# --- ROUTEN --- + +@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] + + # Sortierung: Erst Zahlen, dann Strings, "Original" ganz hinten + 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 = [] + total_items = len(inputs) + correct_items = 0 + has_incomplete = False + + for inp in inputs: + user_val = inp.get('value', '') + correct_val = inp.get('correct', '') + field_type = inp.get('type', 'german') + + status = 'wrong' + + if field_type == 'middle': + status = check_middle_logic(user_val, correct_val) + else: + status = check_string_match(user_val, correct_val) + + res_entry = {'status': status, 'correct': correct_val} + + if status == 'incomplete': + res_entry['msg'] = 'Unvollständig! Da fehlt noch etwas.' + has_incomplete = True + + results.append(res_entry) + + if status in ['correct', 'typo']: + correct_items += 1 + + 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) \ No newline at end of file