vocab-backend/deck_endpoints.py

530 lines
20 KiB
Python

# deck_endpoints.py
from flask import Blueprint, request, jsonify
import sqlite3
import os
import shutil
import logging
deck_bp = Blueprint('deck_bp', __name__)
DATABASE = 'mydatabase.db'
# Logger konfigurieren (angenommen, ocr_server3.py konfiguriert das Logging)
logger = logging.getLogger(__name__)
def get_db_connection():
conn = sqlite3.connect(DATABASE)
conn.row_factory = sqlite3.Row
return conn
# Erstellen der Tabelle Deck, falls sie nicht existiert
def init_db():
conn = get_db_connection()
cursor = conn.cursor()
# Tabelle Deck erstellen
cursor.execute('''
CREATE TABLE IF NOT EXISTS Deck (
id INTEGER PRIMARY KEY AUTOINCREMENT,
deckname TEXT NOT NULL,
bildname TEXT,
bildid TEXT,
iconindex INTEGER,
x1 REAL,
x2 REAL,
y1 REAL,
y2 REAL,
due INTEGER DEFAULT (CAST((julianday('now') - julianday('1970-01-01')) AS INTEGER)),
ivl REAL DEFAULT 0.0,
factor REAL DEFAULT 2.5,
reps INTEGER DEFAULT 0,
lapses INTEGER DEFAULT 0,
isGraduated INTEGER DEFAULT 0 -- Neue Spalte hinzugefügt
)
''')
conn.commit()
conn.close()
def clean_debug_directories():
"""
Löscht alle Verzeichnisse unter 'debug_images', die keinen Eintrag in der Deck-Tabelle mit bildname oder bildid haben.
"""
debug_base_dir = 'debug_images'
if not os.path.exists(debug_base_dir):
logger.info(f"Debug-Verzeichnis '{debug_base_dir}' existiert nicht. Nichts zu bereinigen.")
return
try:
conn = get_db_connection()
cursor = conn.cursor()
# Sammle alle bildname und bildid, die in der Deck-Tabelle vorhanden sind
cursor.execute('SELECT DISTINCT bildname FROM Deck WHERE bildname IS NOT NULL')
bildnames = {row['bildname'] for row in cursor.fetchall()}
cursor.execute('SELECT DISTINCT bildid FROM Deck WHERE bildid IS NOT NULL')
bildids = {row['bildid'] for row in cursor.fetchall()}
conn.close()
except sqlite3.Error as e:
logger.error(f"Fehler beim Abrufen der bildname und bildid aus der Datenbank: {e}")
return
# Durchlaufe alle Verzeichnisse unter 'debug_images'
for dir_name in os.listdir(debug_base_dir):
dir_path = os.path.join(debug_base_dir, dir_name)
if os.path.isdir(dir_path):
if dir_name not in bildnames and dir_name not in bildids:
try:
shutil.rmtree(dir_path)
logger.info(f"Nicht verwendetes Debug-Verzeichnis gelöscht: {dir_path}")
except Exception as e:
logger.error(f"Fehler beim Löschen des Verzeichnisses '{dir_path}': {e}")
else:
logger.debug(f"Debug-Verzeichnis behalten: {dir_path}")
logger.info("Bereinigung der Debug-Verzeichnisse abgeschlossen.")
def clean_db_entries():
"""
Löscht alle Einträge aus der Deck-Tabelle, für die es kein entsprechendes Verzeichnis unter 'debug_images' gibt.
"""
debug_base_dir = 'debug_images'
if not os.path.exists(debug_base_dir):
logger.info(f"Debug-Verzeichnis '{debug_base_dir}' existiert nicht. Keine DB-Einträge zu bereinigen.")
return
try:
# Liste der vorhandenen Verzeichnisse unter 'debug_images'
existing_dirs = {name for name in os.listdir(debug_base_dir)
if os.path.isdir(os.path.join(debug_base_dir, name))}
except Exception as e:
logger.error(f"Fehler beim Zugriff auf Verzeichnisse in '{debug_base_dir}': {e}")
return
try:
conn = get_db_connection()
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
# Alle bildid-Werte aus der Deck-Tabelle abrufen, die nicht NULL sind
cursor.execute('SELECT DISTINCT bildid FROM Deck WHERE bildid IS NOT NULL')
bildids = {row['bildid'] for row in cursor.fetchall()}
# bildid-Werte identifizieren, für die es kein Verzeichnis gibt
missing_dirs = bildids - existing_dirs
if missing_dirs:
# Einträge aus der Deck-Tabelle löschen, deren bildid keinen entsprechenden Ordner hat
placeholders = ','.join('?' for _ in missing_dirs)
sql = f'DELETE FROM Deck WHERE bildid IN ({placeholders})'
cursor.execute(sql, tuple(missing_dirs))
deleted_count = cursor.rowcount
conn.commit()
logger.info(f"{deleted_count} Einträge aus der Deck-Tabelle gelöscht, deren 'bildid'-Verzeichnisse fehlen.")
else:
logger.info("Keine DB-Einträge zu löschen; alle 'bildid'-Verzeichnisse existieren.")
conn.close()
except sqlite3.Error as e:
logger.error(f"Datenbankfehler beim Löschen von Einträgen aus der Deck-Tabelle: {e}")
return
# ------
# Endpoints
# ------
# ------
# Deck - POST, GET, DELETE
# ------
@deck_bp.route('/api/decks', methods=['POST'])
def create_deck():
data = request.get_json()
if not data or 'deckname' not in data:
return jsonify({'error': 'No deckname provided'}), 400
deckname = data['deckname']
conn = get_db_connection()
cursor = conn.cursor()
try:
# Ein neuer Eintrag für das Deck ohne Bilddaten
cursor.execute('INSERT INTO Deck (deckname) VALUES (?)', (deckname,))
conn.commit()
deck_id = cursor.lastrowid
conn.close()
return jsonify({'status': 'success', 'deck_id': deck_id}), 201
except sqlite3.Error as e:
conn.close()
logger.error(f"Datenbankfehler beim Erstellen des Decks: {e}")
return jsonify({'error': 'Database error', 'details': str(e)}), 500
@deck_bp.route('/api/decks', methods=['GET'])
def get_decks():
conn = get_db_connection()
cursor = conn.cursor()
# Alle Einträge abrufen
entries = cursor.execute('SELECT * FROM Deck').fetchall()
conn.close()
decks = {}
for entry in entries:
deckname = entry['deckname']
if deckname not in decks:
decks[deckname] = {
'name': deckname,
'images': []
}
if entry['bildname'] and entry['bildid']:
image = {
'name': entry['bildname'],
'id': entry['bildid'],
'iconindex': entry['iconindex'],
'boxid':entry['id'],
'x1': entry['x1'],
'x2': entry['x2'],
'y1': entry['y1'],
'y2': entry['y2'],
'due': entry['due'],
'ivl': entry['ivl'],
'factor': entry['factor'],
'reps': entry['reps'],
'lapses': entry['lapses'],
'isGraduated': bool(entry['isGraduated']) # isGraduated hinzufügen
}
decks[deckname]['images'].append(image)
deck_list = list(decks.values())
return jsonify(deck_list)
@deck_bp.route('/api/decks/<deckname>', methods=['DELETE'])
def delete_deck(deckname):
conn = get_db_connection()
cursor = conn.cursor()
try:
# Überprüfen, ob das Deck existiert
cursor.execute('SELECT COUNT(*) as count FROM Deck WHERE deckname = ?', (deckname,))
result = cursor.fetchone()
if result['count'] == 0:
conn.close()
return jsonify({'error': 'Deck not found'}), 404
# Löschen aller Einträge mit dem gegebenen deckname
cursor.execute('DELETE FROM Deck WHERE deckname = ?', (deckname,))
conn.commit()
conn.close()
return jsonify({'status': 'success'}), 200
except sqlite3.Error as e:
conn.close()
logger.error(f"Datenbankfehler beim Löschen des Decks '{deckname}': {e}")
return jsonify({'error': 'Database error', 'details': str(e)}), 500
@deck_bp.route('/api/decks/<old_deckname>/rename', methods=['PUT'])
def rename_deck(old_deckname):
conn = get_db_connection()
cursor = conn.cursor()
try:
# Überprüfen, ob das Deck existiert
cursor.execute('SELECT COUNT(*) as count FROM Deck WHERE deckname = ?', (old_deckname,))
result = cursor.fetchone()
if result['count'] == 0:
conn.close()
return jsonify({'error': 'Deck not found'}), 404
# Neuen Decknamen aus dem Request-Body holen
new_deckname = request.json.get('newDeckName')
if not new_deckname:
conn.close()
return jsonify({'error': 'New deck name is required'}), 400
# Überprüfen, ob der neue Deckname bereits existiert
cursor.execute('SELECT COUNT(*) as count FROM Deck WHERE deckname = ?', (new_deckname,))
result = cursor.fetchone()
if result['count'] > 0:
conn.close()
return jsonify({'error': 'Deck with the new name already exists'}), 409
# Deck umbenennen
cursor.execute('UPDATE Deck SET deckname = ? WHERE deckname = ?', (new_deckname, old_deckname))
conn.commit()
conn.close()
return jsonify({'status': 'success', 'message': 'Deck renamed successfully'}), 200
except sqlite3.Error as e:
conn.close()
logger.error(f"Datenbankfehler beim Umbenennen des Decks '{old_deckname}': {e}")
return jsonify({'error': 'Database error', 'details': str(e)}), 500
# ------
# Image - POST, GET, DELETE
# ------
@deck_bp.route('/api/decks/image', methods=['POST'])
def update_image():
data = request.get_json()
if not data:
return jsonify({'error': 'No data provided'}), 400
# Überprüfen, ob die erforderlichen Felder vorhanden sind
required_fields = ['deckname', 'bildname', 'bildid', 'boxes']
if not all(field in data for field in required_fields):
return jsonify({'error': 'Missing fields in data'}), 400
deckname = data['deckname']
bildname = data['bildname']
bildid = data['bildid']
boxes = data['boxes']
# Überprüfen, ob 'boxes' eine Liste ist und mindestens ein Box-Element enthält
if not isinstance(boxes, list) or len(boxes) == 0:
return jsonify({'error': "'boxes' must be a non-empty list"}), 400
# Verbindung zur Datenbank herstellen
conn = get_db_connection()
cursor = conn.cursor()
try:
# Überprüfen, ob das Deck existiert
cursor.execute('SELECT COUNT(*) as count FROM Deck WHERE deckname = ?', (deckname,))
result = cursor.fetchone()
if result['count'] == 0:
conn.close()
return jsonify({'error': 'Deck not found'}), 404
# 1. Lösche alle Einträge für das Deck ohne bildname
cursor.execute('DELETE FROM Deck WHERE deckname = ? AND bildid = ?', (deckname,bildid))
inserted_image_ids = []
# 2. Füge neue Image-Einträge hinzu
for index, box in enumerate(boxes):
# Überprüfen, ob alle erforderlichen Koordinaten vorhanden sind
box_fields = ['x1', 'x2', 'y1', 'y2']
if not all(field in box for field in box_fields):
conn.close()
return jsonify({'error': 'Missing fields in one of the boxes'}), 400
x1 = box['x1']
x2 = box['x2']
y1 = box['y1']
y2 = box['y2']
# Setzen des iconindex auf den aktuellen Index der Box
iconindex = index
cursor.execute('''
INSERT INTO Deck (deckname, bildname, bildid, iconindex, x1, x2, y1, y2)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
''', (deckname, bildname, bildid, iconindex, x1, x2, y1, y2))
inserted_image_ids.append(cursor.lastrowid)
conn.commit()
return jsonify({'status': 'success', 'inserted_image_ids': inserted_image_ids}), 201
except sqlite3.Error as e:
conn.rollback()
logger.error(f"Datenbankfehler beim Aktualisieren der Images für Deck '{deckname}': {e}")
return jsonify({'error': 'Database error', 'details': str(e)}), 500
finally:
conn.close()
# @deck_bp.route('/api/decks/image/<bildid>', methods=['GET'])
# def get_images_by_bildid(bildid):
# conn = get_db_connection()
# cursor = conn.cursor()
# images = cursor.execute('SELECT * FROM Deck WHERE bildid = ?', (bildid,)).fetchall()
# conn.close()
# image_list = [dict(image) for image in images]
# return jsonify(image_list)
@deck_bp.route('/api/decks/image/<bildid>', methods=['DELETE'])
def delete_images_by_bildid(bildid):
"""
Löscht alle Einträge in der Deck-Tabelle für die gegebene bildid.
Falls es das letzte Image für dieses Deck ist, wird ein neuer Deck-Eintrag erstellt.
Optional: Löscht das zugehörige Debug-Verzeichnis, wenn keine weiteren Einträge bestehen.
"""
try:
# Sicherheitsmaßnahme: Nur erlaubte Zeichen in bildid
if not all(c.isalnum() or c in ('_', '-') for c in bildid):
logger.warning(f"Ungültige bildid angefordert: {bildid}")
return jsonify({'error': 'Invalid image ID'}), 400
conn = get_db_connection()
cursor = conn.cursor()
# Start einer Transaktion
conn.execute('BEGIN')
# Schritt 1: Identifizieren der betroffenen Decks
cursor.execute('SELECT DISTINCT deckname FROM Deck WHERE bildid = ?', (bildid,))
affected_decks = {row['deckname'] for row in cursor.fetchall()}
if not affected_decks:
conn.close()
return jsonify({'error': 'No entries found for the given image ID'}), 404
# Schritt 2: Löschen der Image-Einträge
cursor.execute('DELETE FROM Deck WHERE bildid = ?', (bildid,))
logger.info(f"Alle Einträge für bildid '{bildid}' wurden gelöscht.")
# Schritt 3: Überprüfen und Wiederherstellen der Deck-Einträge
for deckname in affected_decks:
# Überprüfen, ob noch andere Image-Einträge für dieses Deck existieren
cursor.execute('SELECT COUNT(*) as count FROM Deck WHERE deckname = ? AND bildid IS NOT NULL', (deckname,))
result = cursor.fetchone()
image_count = result['count'] if result else 0
if image_count == 0:
# Überprüfen, ob bereits ein Deck-Eintrag existiert (ohne bildid)
cursor.execute('SELECT COUNT(*) as count FROM Deck WHERE deckname = ? AND bildid IS NULL', (deckname,))
deck_entry = cursor.fetchone()
if deck_entry['count'] == 0:
# Ein neuer Deck-Eintrag wird erstellt
cursor.execute('INSERT INTO Deck (deckname) VALUES (?)', (deckname,))
logger.info(f"Neuer Deck-Eintrag für '{deckname}' erstellt, da keine weiteren Images vorhanden sind.")
# Commit der Transaktion
conn.commit()
# Schritt 4: Optionales Löschen des Debug-Verzeichnisses
debug_dir = os.path.join('debug_images', bildid)
if os.path.exists(debug_dir):
try:
shutil.rmtree(debug_dir)
logger.info(f"Debug-Verzeichnis gelöscht: {debug_dir}")
except Exception as e:
logger.error(f"Fehler beim Löschen des Debug-Verzeichnisses '{debug_dir}': {e}")
# Eine Warnung wird zurückgegeben, aber die Anfrage wird als erfolgreich betrachtet
return jsonify({
'status': 'success',
'message': 'Database entries deleted, but failed to delete debug directory.',
'details': str(e)
}), 200
return jsonify({'status': 'success', 'message': f'All entries for image ID "{bildid}" have been deleted.'}), 200
except sqlite3.Error as e:
# Rollback der Transaktion bei einem Datenbankfehler
conn.rollback()
logger.error(f"Datenbankfehler beim Löschen der Image-Einträge für '{bildid}': {e}")
return jsonify({'error': 'Database error', 'details': str(e)}), 500
except Exception as e:
# Rollback der Transaktion bei einem unerwarteten Fehler
conn.rollback()
logger.error(f"Unerwarteter Fehler beim Löschen der Image-Einträge für '{bildid}': {e}")
return jsonify({'error': 'Failed to delete image entries', 'details': str(e)}), 500
finally:
conn.close()
@deck_bp.route('/api/decks/images/<bildid>/move', methods=['POST'])
def move_image(bildid):
"""
Verschiebt alle Einträge einer bildid von ihrem aktuellen Deck in ein neues Deck.
Das übergebene JSON sollte { "targetDeckId": "neuerDeckname" } sein.
"""
try:
data = request.get_json()
if not data or 'targetDeckId' not in data:
return jsonify({'error': 'No targetDeckId provided'}), 400
target_deck_id = data['targetDeckId']
# Sicherheitsmaßnahme: Nur erlaubte Zeichen im targetDeckId
if not all(c.isalnum() or c in ('_', '-') for c in target_deck_id):
logger.warning(f"Ungültiger targetDeckId angefordert: {target_deck_id}")
return jsonify({'error': 'Invalid targetDeckId'}), 400
conn = get_db_connection()
cursor = conn.cursor()
# Überprüfen, ob es Einträge mit der gegebenen bildid gibt
cursor.execute('SELECT COUNT(*) as count FROM Deck WHERE bildid = ?', (bildid,))
result = cursor.fetchone()
if result['count'] == 0:
conn.close()
return jsonify({'error': 'No entries found for the given image ID'}), 404
# Update deckname für alle Einträge mit der bildid
cursor.execute('UPDATE Deck SET deckname = ? WHERE bildid = ?', (target_deck_id, bildid))
updated_rows = cursor.rowcount
conn.commit()
conn.close()
logger.info(f"Bild mit bildid '{bildid}' wurde in das Deck '{target_deck_id}' verschoben. Aktualisierte Einträge: {updated_rows}")
return jsonify({'status': 'success', 'moved_entries': updated_rows}), 200
except sqlite3.Error as e:
if conn:
conn.rollback()
logger.error(f"Datenbankfehler beim Verschieben der Image-Einträge für '{bildid}': {e}")
return jsonify({'error': 'Database error', 'details': str(e)}), 500
except Exception as e:
if conn:
conn.rollback()
logger.error(f"Unerwarteter Fehler beim Verschieben der Image-Einträge für '{bildid}': {e}")
return jsonify({'error': 'Failed to move image entries', 'details': str(e)}), 500
finally:
if conn:
conn.close()
@deck_bp.route('/api/decks/boxes/<int:box_id>', methods=['PUT'])
def update_box(box_id):
data = request.get_json()
due = data.get('due', 0)
ivl = data.get('ivl', 0.0)
factor = data.get('factor', 2.5)
reps = data.get('reps', 0)
lapses = data.get('lapses', 0)
is_graduated = data.get('isGraduated') # Neues Feld aus den Daten extrahieren
# Validierung des isGraduated-Feldes
if is_graduated is not None:
if isinstance(is_graduated, bool):
is_graduated_int = int(is_graduated)
else:
return jsonify({'error': 'isGraduated muss ein Boolean-Wert sein'}), 400
else:
is_graduated_int = None # Kein Update für isGraduated
conn = get_db_connection()
cursor = conn.cursor()
try:
if is_graduated_int is not None:
cursor.execute("""
UPDATE Deck
SET due = ?, ivl = ?, factor = ?, reps = ?, lapses = ?, isGraduated = ?
WHERE id = ?
""", (due, ivl, factor, reps, lapses, is_graduated_int, box_id))
else:
cursor.execute("""
UPDATE Deck
SET due = ?, ivl = ?, factor = ?, reps = ?, lapses = ?
WHERE id = ?
""", (due, ivl, factor, reps, lapses, box_id))
if cursor.rowcount == 0:
conn.close()
return jsonify({'error': 'Box nicht gefunden'}), 404
conn.commit()
conn.close()
return jsonify({'status': 'success'}), 200
except sqlite3.Error as e:
conn.rollback()
conn.close()
logger.error(f"Datenbankfehler beim Aktualisieren der Box '{box_id}': {e}")
return jsonify({'error': 'Database error', 'details': str(e)}), 500
return jsonify({'status': 'success'}), 200
# Sicherstellen, dass die Datenbank existiert
if not os.path.exists(DATABASE):
init_db()
clean_debug_directories()
clean_db_entries()