- New template: user-template-dictionary with Flask backend - Features: Add/Edit/Delete words, SQLite database per user - Persistent storage: Docker Volumes mount to /data/ - Modern HTML/CSS/JS Frontend with error handling - REST API: GET/POST/PUT/DELETE endpoints - Health checks and comprehensive logging - Comprehensive documentation in docs/templates/DICTIONARY_TEMPLATE.md - Updated templates.json and .env.example Files: - user-template-dictionary/Dockerfile - user-template-dictionary/app.py - user-template-dictionary/requirements.txt - user-template-dictionary/templates/index.html - docs/templates/DICTIONARY_TEMPLATE.md - templates.json (updated) - .env.example (updated)
268 lines
7.9 KiB
Python
268 lines
7.9 KiB
Python
"""
|
|
Persönliches Wörterbuch - Flask Backend mit SQLite
|
|
Speichert Wörter und Bedeutungen in einer persistenten Datenbank pro Benutzer
|
|
"""
|
|
|
|
from flask import Flask, render_template, request, jsonify
|
|
from datetime import datetime
|
|
import sqlite3
|
|
import os
|
|
import logging
|
|
|
|
app = Flask(__name__)
|
|
|
|
# Logging konfigurieren
|
|
logging.basicConfig(level=logging.INFO)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Pfad zur persistenten Datenbank (wird als Docker Volume gemountet)
|
|
DB_PATH = "/data/app.db"
|
|
os.makedirs("/data", exist_ok=True)
|
|
|
|
logger.info(f"[DICTIONARY] Database path: {DB_PATH}")
|
|
|
|
|
|
def get_db():
|
|
"""Verbindung zur SQLite-Datenbank"""
|
|
conn = sqlite3.connect(DB_PATH)
|
|
conn.row_factory = sqlite3.Row
|
|
return conn
|
|
|
|
|
|
def init_db():
|
|
"""Erstelle Tabelle beim Start (falls noch nicht vorhanden)"""
|
|
conn = get_db()
|
|
cursor = conn.cursor()
|
|
|
|
# Prüfe ob Tabelle bereits existiert
|
|
cursor.execute("""
|
|
SELECT name FROM sqlite_master
|
|
WHERE type='table' AND name='words'
|
|
""")
|
|
|
|
if not cursor.fetchone():
|
|
logger.info("[DICTIONARY] Creating 'words' table...")
|
|
cursor.execute('''
|
|
CREATE TABLE words (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
word TEXT NOT NULL UNIQUE,
|
|
meaning TEXT NOT NULL,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
''')
|
|
conn.commit()
|
|
logger.info("[DICTIONARY] Table 'words' created successfully")
|
|
else:
|
|
logger.info("[DICTIONARY] Table 'words' already exists")
|
|
|
|
conn.close()
|
|
|
|
|
|
@app.route('/')
|
|
def index():
|
|
"""Hauptseite mit HTML-Interface"""
|
|
return render_template('index.html')
|
|
|
|
|
|
@app.route('/health')
|
|
def health():
|
|
"""Health Check Endpoint für Docker"""
|
|
try:
|
|
conn = get_db()
|
|
cursor = conn.cursor()
|
|
cursor.execute("SELECT 1")
|
|
conn.close()
|
|
return {'status': 'ok', 'database': 'connected'}, 200
|
|
except Exception as e:
|
|
logger.error(f"[DICTIONARY] Health check failed: {e}")
|
|
return {'status': 'error', 'message': str(e)}, 500
|
|
|
|
|
|
@app.route('/api/words', methods=['GET'])
|
|
def get_words():
|
|
"""Alle gespeicherten Wörter abrufen"""
|
|
try:
|
|
conn = get_db()
|
|
cursor = conn.cursor()
|
|
cursor.execute('SELECT id, word, meaning, created_at FROM words ORDER BY created_at DESC')
|
|
words = [dict(row) for row in cursor.fetchall()]
|
|
conn.close()
|
|
|
|
logger.info(f"[DICTIONARY] Retrieved {len(words)} words")
|
|
return jsonify({
|
|
'words': words,
|
|
'count': len(words)
|
|
}), 200
|
|
except Exception as e:
|
|
logger.error(f"[DICTIONARY] Error retrieving words: {e}")
|
|
return jsonify({'error': 'Fehler beim Abrufen der Wörter'}), 500
|
|
|
|
|
|
@app.route('/api/words', methods=['POST'])
|
|
def add_word():
|
|
"""Neues Wort hinzufügen"""
|
|
try:
|
|
data = request.get_json()
|
|
|
|
if not data:
|
|
return jsonify({'error': 'Keine Daten empfangen'}), 400
|
|
|
|
word = data.get('word', '').strip()
|
|
meaning = data.get('meaning', '').strip()
|
|
|
|
# Validierung
|
|
if not word or not meaning:
|
|
return jsonify({'error': 'Wort und Bedeutung sind erforderlich'}), 400
|
|
|
|
if len(word) > 255:
|
|
return jsonify({'error': 'Wort ist zu lang (max. 255 Zeichen)'}), 400
|
|
|
|
if len(meaning) > 2000:
|
|
return jsonify({'error': 'Bedeutung ist zu lang (max. 2000 Zeichen)'}), 400
|
|
|
|
conn = get_db()
|
|
cursor = conn.cursor()
|
|
|
|
try:
|
|
cursor.execute(
|
|
'INSERT INTO words (word, meaning) VALUES (?, ?)',
|
|
(word, meaning)
|
|
)
|
|
conn.commit()
|
|
word_id = cursor.lastrowid
|
|
|
|
# Neuen Eintrag zurück
|
|
cursor.execute('SELECT id, word, meaning, created_at FROM words WHERE id = ?', (word_id,))
|
|
new_word = dict(cursor.fetchone())
|
|
conn.close()
|
|
|
|
logger.info(f"[DICTIONARY] Word added: '{word}'")
|
|
return jsonify(new_word), 201
|
|
|
|
except sqlite3.IntegrityError:
|
|
conn.close()
|
|
logger.warning(f"[DICTIONARY] Duplicate word: '{word}'")
|
|
return jsonify({'error': f'Das Wort "{word}" existiert bereits'}), 409
|
|
|
|
except Exception as e:
|
|
logger.error(f"[DICTIONARY] Error adding word: {e}")
|
|
return jsonify({'error': 'Fehler beim Speichern des Wortes'}), 500
|
|
|
|
|
|
@app.route('/api/words/<int:word_id>', methods=['PUT'])
|
|
def update_word(word_id):
|
|
"""Wort aktualisieren"""
|
|
try:
|
|
data = request.get_json()
|
|
|
|
if not data:
|
|
return jsonify({'error': 'Keine Daten empfangen'}), 400
|
|
|
|
word = data.get('word', '').strip()
|
|
meaning = data.get('meaning', '').strip()
|
|
|
|
# Validierung
|
|
if not word or not meaning:
|
|
return jsonify({'error': 'Wort und Bedeutung sind erforderlich'}), 400
|
|
|
|
conn = get_db()
|
|
cursor = conn.cursor()
|
|
|
|
try:
|
|
cursor.execute(
|
|
'UPDATE words SET word = ?, meaning = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
|
(word, meaning, word_id)
|
|
)
|
|
conn.commit()
|
|
|
|
if cursor.rowcount == 0:
|
|
conn.close()
|
|
return jsonify({'error': 'Wort nicht gefunden'}), 404
|
|
|
|
# Aktualisiertes Wort zurück
|
|
cursor.execute('SELECT id, word, meaning, created_at FROM words WHERE id = ?', (word_id,))
|
|
updated_word = dict(cursor.fetchone())
|
|
conn.close()
|
|
|
|
logger.info(f"[DICTIONARY] Word updated: ID {word_id}")
|
|
return jsonify(updated_word), 200
|
|
|
|
except sqlite3.IntegrityError:
|
|
conn.close()
|
|
logger.warning(f"[DICTIONARY] Duplicate word on update: '{word}'")
|
|
return jsonify({'error': f'Das Wort "{word}" existiert bereits'}), 409
|
|
|
|
except Exception as e:
|
|
logger.error(f"[DICTIONARY] Error updating word: {e}")
|
|
return jsonify({'error': 'Fehler beim Aktualisieren des Wortes'}), 500
|
|
|
|
|
|
@app.route('/api/words/<int:word_id>', methods=['DELETE'])
|
|
def delete_word(word_id):
|
|
"""Wort löschen"""
|
|
try:
|
|
conn = get_db()
|
|
cursor = conn.cursor()
|
|
cursor.execute('DELETE FROM words WHERE id = ?', (word_id,))
|
|
conn.commit()
|
|
|
|
if cursor.rowcount == 0:
|
|
conn.close()
|
|
return jsonify({'error': 'Wort nicht gefunden'}), 404
|
|
|
|
conn.close()
|
|
logger.info(f"[DICTIONARY] Word deleted: ID {word_id}")
|
|
return '', 204
|
|
|
|
except Exception as e:
|
|
logger.error(f"[DICTIONARY] Error deleting word: {e}")
|
|
return jsonify({'error': 'Fehler beim Löschen des Wortes'}), 500
|
|
|
|
|
|
@app.route('/api/stats')
|
|
def get_stats():
|
|
"""Statistiken über die Wörterbuch-Datenbank"""
|
|
try:
|
|
conn = get_db()
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute('SELECT COUNT(*) as total FROM words')
|
|
total = cursor.fetchone()['total']
|
|
|
|
cursor.execute('SELECT MAX(created_at) as last_added FROM words')
|
|
last_added = cursor.fetchone()['last_added']
|
|
|
|
conn.close()
|
|
|
|
return jsonify({
|
|
'total_words': total,
|
|
'last_added': last_added,
|
|
'database': 'sqlite3',
|
|
'storage': '/data/app.db'
|
|
}), 200
|
|
|
|
except Exception as e:
|
|
logger.error(f"[DICTIONARY] Error getting stats: {e}")
|
|
return jsonify({'error': 'Fehler beim Abrufen der Statistiken'}), 500
|
|
|
|
|
|
@app.errorhandler(404)
|
|
def not_found(error):
|
|
"""404 Handler"""
|
|
return jsonify({'error': 'Endpoint nicht gefunden'}), 404
|
|
|
|
|
|
@app.errorhandler(500)
|
|
def server_error(error):
|
|
"""500 Handler"""
|
|
logger.error(f"[DICTIONARY] Server error: {error}")
|
|
return jsonify({'error': 'Interner Fehler'}), 500
|
|
|
|
|
|
if __name__ == '__main__':
|
|
logger.info("[DICTIONARY] Starting Flask application...")
|
|
init_db()
|
|
logger.info("[DICTIONARY] Database initialized")
|
|
app.run(host='0.0.0.0', port=8080, debug=False)
|