#!/usr/bin/env python # -*- coding: utf-8 -*- import os import logging import traceback from datetime import datetime, timedelta, timezone from functools import wraps from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, session, g from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user from flask_sqlalchemy import SQLAlchemy from werkzeug.security import generate_password_hash, check_password_hash import json from enum import Enum from wtforms import StringField, PasswordField, BooleanField, TextAreaField, SelectField, HiddenField from wtforms.validators import DataRequired, Email, Length, EqualTo, ValidationError from functools import wraps import secrets from sqlalchemy.sql import func import openai from openai import OpenAI from dotenv import load_dotenv from flask_socketio import SocketIO, emit from flask_migrate import Migrate import sqlalchemy import ssl import certifi import os from sqlalchemy.orm import joinedload # Modelle importieren from models import ( db, User, Thought, Comment, MindMapNode, ThoughtRelation, ThoughtRating, RelationType, Category, UserMindmap, UserMindmapNode, MindmapNote, node_thought_association, user_thought_bookmark, node_relationship, MindmapShare, PermissionType ) # Lade .env-Datei load_dotenv() # force=True erzwingt die Synchronisierung # Bestimme den absoluten Pfad zur Datenbank basedir = os.path.abspath(os.path.dirname(__file__)) db_path = os.path.join(basedir, 'database', 'systades.db') # Stellen Sie sicher, dass das Verzeichnis existiert os.makedirs(os.path.dirname(db_path), exist_ok=True) app = Flask(__name__) app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'default-dev-key') app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=365) # Langlebige Session für Dark Mode-Einstellung app.config['UPLOAD_FOLDER'] = os.getenv('UPLOAD_FOLDER', os.path.join(os.getcwd(), 'uploads')) # Logger-Konfiguration log_level = os.environ.get('LOG_LEVEL', 'INFO').upper() log_dir = os.path.join(basedir, 'logs') os.makedirs(log_dir, exist_ok=True) log_file = os.path.join(log_dir, 'app.log') # Handler für Datei-Logs file_handler = logging.FileHandler(log_file) file_handler.setFormatter(logging.Formatter( '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]' )) file_handler.setLevel(getattr(logging, log_level)) # Handler für Konsolen-Logs console_handler = logging.StreamHandler() console_handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s')) console_handler.setLevel(getattr(logging, log_level)) # Konfiguriere App-Logger app.logger.addHandler(file_handler) app.logger.addHandler(console_handler) app.logger.setLevel(getattr(logging, log_level)) app.logger.info('Anwendung gestartet') # Einheitliches Fehlerbehandlungssystem class ErrorHandler: """Zentralisierte Fehlerbehandlung für die gesamte Anwendung""" @staticmethod def log_exception(e, endpoint=None, code=500): """Protokolliert eine Ausnahme mit Kontext-Informationen""" # Stack-Trace für Debugging trace = traceback.format_exc() # Benutzer-Informationen (wenn verfügbar) user_info = f"User: {current_user.id} ({current_user.username})" if current_user and current_user.is_authenticated else "Nicht angemeldet" # Request-Informationen request_info = f"Endpoint: {endpoint or request.path}, Method: {request.method}, IP: {request.remote_addr}" # Vollständige Log-Nachricht app.logger.error(f"Fehler {code}: {str(e)}\n{request_info}\n{user_info}\n{trace}") @staticmethod def api_error(message, code=400, details=None): """Erstellt eine standardisierte API-Fehlerantwort""" response = { 'success': False, 'error': { 'code': code, 'message': message } } if details: response['error']['details'] = details return jsonify(response), code @staticmethod def handle_exception(e, is_api_request=False): """Behandelt eine Ausnahme basierend auf ihrem Typ""" # Bestimme, ob es sich um eine API-Anfrage handelt if is_api_request is None: is_api_request = request.path.startswith('/api/') or request.headers.get('Accept') == 'application/json' # Protokolliere die Ausnahme ErrorHandler.log_exception(e) # Erstelle benutzerfreundliche Fehlermeldung user_message = "Ein unerwarteter Fehler ist aufgetreten." if is_api_request: # API-Antwort für Ausnahme return ErrorHandler.api_error(user_message, 500) else: # Web-Antwort für Ausnahme (mit Flash-Nachricht) flash(user_message, 'error') return render_template('errors/500.html'), 500 # Globaler Exception-Handler @app.errorhandler(Exception) def handle_unhandled_exception(e): """Fängt alle unbehandelten Ausnahmen ab""" return ErrorHandler.handle_exception(e) # OpenAI API-Konfiguration api_key = os.environ.get("OPENAI_API_KEY", "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") # Variable zur Überprüfung, ob OpenAI verfügbar ist openai_available = True # Client mit SSL-Verify-Deaktivierung für problematische Umgebungen try: # Versuche, den OpenAI-Client zu initialisieren und zu testen client = OpenAI(api_key=api_key) # Deaktiviere SSL-Verifizierung, falls in Windows-Umgebungen Probleme auftreten client._client.proxies = {} client._client.verify = False # Testanfrage, um zu prüfen, ob die API funktioniert try: # Einfache Testanfrage resp = client.models.list() openai_available = True print("OpenAI API-Verbindung erfolgreich hergestellt.") except Exception as e: print(f"OpenAI API-Verbindungstest fehlgeschlagen: {e}") openai_available = False except Exception as e: print(f"Fehler bei der Initialisierung des OpenAI-Clients: {e}") openai_available = False client = None # Dark Mode Einstellung in Session speichern @app.before_request def handle_dark_mode(): if 'dark_mode' not in session: session['dark_mode'] = False # Standardmäßig Light Mode # Context processor für Dark Mode @app.context_processor def inject_dark_mode(): return {'dark_mode': session.get('dark_mode', False)} # Route zum Umschalten des Dark Mode @app.route('/toggle-dark-mode', methods=['POST']) def toggle_dark_mode(): session['dark_mode'] = not session.get('dark_mode', False) return jsonify({'success': True, 'dark_mode': session['dark_mode']}) # Context processor für globale Template-Variablen @app.context_processor def inject_globals(): """Inject global variables into all templates.""" return { 'current_year': datetime.now().year } # Context-Prozessor für alle Templates @app.context_processor def inject_current_year(): return {'current_year': datetime.now().year} # Initialisiere die Datenbank db.init_app(app) # Initialisiere den Login-Manager login_manager = LoginManager(app) login_manager.login_view = 'login' # Erst nach der App-Initialisierung die DB-Check-Funktionen importieren from utils.db_check import check_db_connection, initialize_db_if_needed # SocketIO initialisieren socketio = SocketIO(app) migrate = Migrate(app, db) # Funktion zum Erstellen von Standardbenutzern def create_default_users(): """Erstellt Standardbenutzer für die Anwendung""" users = [ { 'username': 'admin', 'email': 'admin@example.com', 'password': 'admin', 'role': 'admin' }, { 'username': 'user', 'email': 'user@example.com', 'password': 'user', 'role': 'user' } ] for user_data in users: password = user_data.pop('password') user = User(**user_data) user.set_password(password) db.session.add(user) db.session.commit() print(f"{len(users)} Benutzer wurden erstellt.") def create_default_categories(): """Erstellt die Standardkategorien für die Mindmap""" # Hauptkategorien main_categories = [ { "name": "Philosophie", "description": "Philosophisches Denken und Konzepte", "color_code": "#9F7AEA", "icon": "fa-brain", "subcategories": [ {"name": "Ethik", "description": "Moralische Grundsätze", "icon": "fa-balance-scale"}, {"name": "Logik", "description": "Gesetze des Denkens", "icon": "fa-project-diagram"}, {"name": "Erkenntnistheorie", "description": "Natur des Wissens", "icon": "fa-lightbulb"} ] }, { "name": "Wissenschaft", "description": "Wissenschaftliche Disziplinen und Forschung", "color_code": "#48BB78", "icon": "fa-flask", "subcategories": [ {"name": "Physik", "description": "Gesetze der Materie und Energie", "icon": "fa-atom"}, {"name": "Biologie", "description": "Wissenschaft des Lebens", "icon": "fa-dna"}, {"name": "Mathematik", "description": "Abstrakte Strukturen", "icon": "fa-calculator"}, {"name": "Informatik", "description": "Wissenschaft der Datenverarbeitung", "icon": "fa-laptop-code"} ] }, { "name": "Technologie", "description": "Technologische Entwicklungen und Anwendungen", "color_code": "#ED8936", "icon": "fa-microchip", "subcategories": [ {"name": "Künstliche Intelligenz", "description": "Intelligente Maschinen", "icon": "fa-robot"}, {"name": "Programmierung", "description": "Softwareentwicklung", "icon": "fa-code"}, {"name": "Elektronik", "description": "Elektronische Systeme", "icon": "fa-memory"} ] }, { "name": "Künste", "description": "Kunstformen und kulturelle Ausdrucksweisen", "color_code": "#ED64A6", "icon": "fa-palette", "subcategories": [ {"name": "Literatur", "description": "Schriftliche Werke", "icon": "fa-book"}, {"name": "Musik", "description": "Klangkunst", "icon": "fa-musik"}, {"name": "Bildende Kunst", "description": "Visuelle Kunstformen", "icon": "fa-paint-brush"} ] }, { "name": "Psychologie", "description": "Menschliches Verhalten und Geist", "color_code": "#4299E1", "icon": "fa-comments", "subcategories": [ {"name": "Kognition", "description": "Denken und Wahrnehmen", "icon": "fa-brain"}, {"name": "Emotionen", "description": "Gefühlswelt", "icon": "fa-heart"}, {"name": "Persönlichkeit", "description": "Charaktereigenschaften", "icon": "fa-user"} ] } ] # Kategorien erstellen for main_cat_data in main_categories: # Prüfen, ob die Kategorie bereits existiert existing_cat = Category.query.filter_by(name=main_cat_data["name"]).first() if existing_cat: continue # Hauptkategorie erstellen main_category = Category( name=main_cat_data["name"], description=main_cat_data["description"], color_code=main_cat_data["color_code"], icon=main_cat_data["icon"] ) db.session.add(main_category) db.session.flush() # Um die ID zu generieren # Unterkategorien erstellen for sub_cat_data in main_cat_data.get("subcategories", []): sub_category = Category( name=sub_cat_data["name"], description=sub_cat_data["description"], color_code=main_cat_data["color_code"], icon=sub_cat_data.get("icon", main_cat_data["icon"]), parent_id=main_category.id ) db.session.add(sub_category) db.session.commit() print("Standard-Kategorien wurden erstellt!") def initialize_database(): """Initialisiert die Datenbank mit Grunddaten, falls diese leer ist""" try: print("Initialisiere die Datenbank...") # Erstelle alle Tabellen db.create_all() # Prüfen, ob bereits Kategorien existieren categories_count = Category.query.count() users_count = User.query.count() # Erstelle Standarddaten, wenn es keine Kategorien gibt if categories_count == 0: create_default_categories() # Admin-Benutzer erstellen, wenn keine Benutzer vorhanden sind if users_count == 0: admin_user = User( username="admin", email="admin@example.com", role="admin", is_active=True ) admin_user.set_password("admin123") # Sicheres Passwort in der Produktion verwenden! db.session.add(admin_user) db.session.commit() print("Admin-Benutzer wurde erstellt!") # Prüfe, ob der "Wissen"-Knoten existiert, falls nicht, erstelle ihn wissen_node = MindMapNode.query.filter_by(name="Wissen").first() if not wissen_node: wissen_node = MindMapNode( name="Wissen", description="Zentrale Wissensbasis", color_code="#4299E1", is_public=True ) db.session.add(wissen_node) db.session.commit() print("'Wissen'-Knoten wurde erstellt") return True except Exception as e: print(f"Fehler bei Datenbank-Initialisierung: {e}") return False # Instead of before_first_request, which is deprecated in newer Flask versions # Use a function to initialize the database that will be called during app creation def init_app_database(app_instance): """Initialisiert die Datenbank für die Flask-App""" with app_instance.app_context(): # Überprüfe und initialisiere die Datenbank bei Bedarf if not initialize_db_if_needed(db, initialize_database): print("WARNUNG: Datenbankinitialisierung fehlgeschlagen. Einige Funktionen könnten eingeschränkt sein.") # Call the function to initialize the database init_app_database(app) # Benutzerdefinierter Decorator für Admin-Zugriff def admin_required(f): @wraps(f) @login_required def decorated_function(*args, **kwargs): if not current_user.is_admin: flash('Zugriff verweigert. Nur Administratoren dürfen diese Seite aufrufen.', 'error') return redirect(url_for('index')) return f(*args, **kwargs) return decorated_function # Hilfsfunktion zur Fehlerbehandlung in API-Endpunkten def handle_api_exception(func): """Decorator für API-Endpunkte zur einheitlichen Fehlerbehandlung""" @wraps(func) def wrapper(*args, **kwargs): try: return func(*args, **kwargs) except Exception as e: # Log und API-Fehler zurückgeben return ErrorHandler.handle_exception(e, is_api_request=True) return wrapper @login_manager.user_loader def load_user(id): # Verwende session.get() anstelle von query.get() für SQLAlchemy 2.0 Kompatibilität return db.session.get(User, int(id)) # Routes for authentication @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': username = request.form.get('username') password = request.form.get('password') user = User.query.filter_by(username=username).first() if user and user.check_password(password): login_user(user) # Aktualisiere letzten Login-Zeitpunkt user.last_login = datetime.now(timezone.utc) db.session.commit() next_page = request.args.get('next') return redirect(next_page or url_for('index')) flash('Ungültiger Benutzername oder Passwort') return render_template('login.html') @app.route('/register', methods=['GET', 'POST']) def register(): if request.method == 'POST': username = request.form.get('username') email = request.form.get('email') password = request.form.get('password') if User.query.filter_by(username=username).first(): flash('Benutzername existiert bereits') return redirect(url_for('register')) if User.query.filter_by(email=email).first(): flash('E-Mail ist bereits registriert') return redirect(url_for('register')) user = User(username=username, email=email) user.set_password(password) db.session.add(user) db.session.commit() # Commit, um eine ID für den Benutzer zu erhalten # Erstelle eine Standard-Mindmap für den neuen Benutzer try: default_mindmap = UserMindmap( name='Meine Mindmap', description='Meine persönliche Wissenslandschaft', user_id=user.id ) db.session.add(default_mindmap) db.session.commit() except Exception as e: print(f"Fehler beim Erstellen der Standard-Mindmap: {e}") # Stelle sicher, dass wir trotzdem weitermachen können db.session.rollback() login_user(user) flash('Dein Konto wurde erfolgreich erstellt!', 'success') return redirect(url_for('index')) return render_template('register.html') @app.route('/logout') @login_required def logout(): logout_user() return redirect(url_for('index')) # Route for the homepage @app.route('/') def index(): return render_template('index.html') # Route for the mindmap page @app.route('/mindmap') def mindmap(): """Zeigt die Mindmap-Seite an.""" # Benutzer-Mindmaps, falls angemeldet user_mindmaps = [] if current_user.is_authenticated: user_mindmaps = UserMindmap.query.filter_by(user_id=current_user.id).all() # Stelle sicher, dass der "Wissen"-Knoten existiert wissen_node = MindMapNode.query.filter_by(name="Wissen").first() if not wissen_node: wissen_node = MindMapNode( name="Wissen", description="Zentrale Wissensbasis", color_code="#4299E1", is_public=True ) db.session.add(wissen_node) db.session.commit() print("'Wissen'-Knoten wurde erstellt") # Überprüfe, ob es Kategorien gibt, sonst erstelle sie if Category.query.count() == 0: create_default_categories() print("Kategorien wurden erstellt") # Stelle sicher, dass die Route für statische Dateien korrekt ist mindmap_js_path = url_for('static', filename='js/mindmap-init.js') return render_template('mindmap.html', user_mindmaps=user_mindmaps, mindmap_js_path=mindmap_js_path) # Route for user profile @app.route('/profile') @login_required def profile(): try: # Versuche auf die neue Benutzermodellstruktur zuzugreifen _ = current_user.bio # Dies wird fehlschlagen, wenn die Spalte nicht existiert # Wenn keine Ausnahme, fahre mit normalem Profil fort # Lade Benutzer-Mindmaps user_mindmaps = UserMindmap.query.filter_by(user_id=current_user.id).all() # Prüfe, ob der Benutzer eine Standard-Mindmap hat, sonst erstelle eine if not user_mindmaps: try: default_mindmap = UserMindmap( name='Meine Mindmap', description='Meine persönliche Wissenslandschaft', user_id=current_user.id ) db.session.add(default_mindmap) db.session.commit() # Aktualisiere die Liste nach dem Erstellen user_mindmaps = [default_mindmap] except Exception as e: print(f"Fehler beim Erstellen der Standard-Mindmap in Profil: {e}") # Flash-Nachricht für den Benutzer flash('Es gab ein Problem beim Laden deiner Mindmaps. Bitte versuche es später erneut.', 'warning') # Lade Statistiken thought_count = Thought.query.filter_by(user_id=current_user.id).count() bookmark_count = db.session.query(user_thought_bookmark).filter( user_thought_bookmark.c.user_id == current_user.id).count() # Berechne tatsächliche Werte für Benutzerstatistiken contributions_count = Comment.query.filter_by(user_id=current_user.id).count() # Berechne Verbindungen (Anzahl der Gedankenverknüpfungen) connections_count = ThoughtRelation.query.filter( (ThoughtRelation.source_id.in_( db.session.query(Thought.id).filter_by(user_id=current_user.id) )) | (ThoughtRelation.target_id.in_( db.session.query(Thought.id).filter_by(user_id=current_user.id) )) ).count() # Berechne durchschnittliche Bewertung der Gedanken des Benutzers avg_rating = db.session.query(func.avg(ThoughtRating.relevance_score)).join( Thought, Thought.id == ThoughtRating.thought_id ).filter(Thought.user_id == current_user.id).scalar() or 0 # Sammle alle Statistiken in einem Wörterbuch stats = { 'thought_count': thought_count, 'bookmark_count': bookmark_count, 'connections_count': connections_count, 'contributions_count': contributions_count, 'followers_count': 0, # Platzhalter für zukünftige Funktionalität 'rating': round(avg_rating, 1) } # Hole die letzten Gedanken des Benutzers thoughts = Thought.query.filter_by(user_id=current_user.id).order_by(Thought.created_at.desc()).limit(5).all() # Hole den Standort des Benutzers aus der Datenbank, falls vorhanden location = "Deutschland" # Standardwert return render_template('profile.html', user=current_user, user_mindmaps=user_mindmaps, stats=stats, thoughts=thoughts, location=location) except (AttributeError, sqlalchemy.exc.OperationalError) as e: # Die Spalte existiert nicht, verwende stattdessen das einfache Profil print(f"Verwende einfaches Profil wegen Datenbankfehler: {e}") flash('Dein Profil wird im einfachen Modus angezeigt, bis die Datenbank aktualisiert wird.', 'warning') # Lade nur die grundlegenden Informationen user_mindmaps = UserMindmap.query.filter_by(user_id=current_user.id).all() thoughts = Thought.query.filter_by(user_id=current_user.id).order_by(Thought.created_at.desc()).limit(5).all() return render_template('simple_profile.html', user=current_user, user_mindmaps=user_mindmaps, thoughts=thoughts) except Exception as e: # Eine andere Ausnahme ist aufgetreten print(f"Fehler beim Laden des Profils: {e}") flash('Beim Laden Ihres Benutzerprofils ist ein Fehler aufgetreten. Wir zeigen Ihnen die Standard-Ansicht.', 'error') # Erstelle grundlegende stats stats = { 'thought_count': 0, 'bookmark_count': 0, 'connections_count': 0, 'contributions_count': 0, 'followers_count': 0, 'rating': 0.0 } # Leere Listen für Mindmaps und Gedanken user_mindmaps = [] thoughts = [] location = "Deutschland" # Zeige das normale Profil mit minimalen Daten an return render_template('profile.html', user=current_user, user_mindmaps=user_mindmaps, stats=stats, thoughts=thoughts, location=location) # Route für Benutzereinstellungen @app.route('/settings', methods=['GET', 'POST']) @login_required def settings(): if request.method == 'POST': action = request.form.get('action') # Bestimme, ob es eine AJAX-Anfrage ist is_ajax = request.headers.get('X-Requested-With') == 'XMLHttpRequest' or request.content_type and 'multipart/form-data' in request.content_type if action == 'update_profile': try: current_user.bio = request.form.get('bio', '') current_user.location = request.form.get('location', '') current_user.website = request.form.get('website', '') # Update avatar if provided avatar_url = request.form.get('avatar_url') if avatar_url: current_user.avatar = avatar_url db.session.commit() if is_ajax: return jsonify({ 'success': True, 'message': 'Profil erfolgreich aktualisiert!' }) else: flash('Profil erfolgreich aktualisiert!', 'success') except Exception as e: db.session.rollback() app.logger.error(f"Fehler beim Aktualisieren des Profils: {str(e)}") if is_ajax: return jsonify({ 'success': False, 'message': 'Fehler beim Aktualisieren des Profils' }), 500 else: flash('Fehler beim Aktualisieren des Profils', 'error') elif action == 'update_password': try: current_password = request.form.get('current_password') new_password = request.form.get('new_password') confirm_password = request.form.get('confirm_password') if not current_user.check_password(current_password): if is_ajax: return jsonify({ 'success': False, 'message': 'Aktuelles Passwort ist nicht korrekt' }), 400 else: flash('Aktuelles Passwort ist nicht korrekt', 'error') elif new_password != confirm_password: if is_ajax: return jsonify({ 'success': False, 'message': 'Neue Passwörter stimmen nicht überein' }), 400 else: flash('Neue Passwörter stimmen nicht überein', 'error') else: current_user.set_password(new_password) db.session.commit() if is_ajax: return jsonify({ 'success': True, 'message': 'Passwort erfolgreich aktualisiert!' }) else: flash('Passwort erfolgreich aktualisiert!', 'success') except Exception as e: db.session.rollback() app.logger.error(f"Fehler beim Aktualisieren des Passworts: {str(e)}") if is_ajax: return jsonify({ 'success': False, 'message': 'Fehler beim Aktualisieren des Passworts' }), 500 else: flash('Fehler beim Aktualisieren des Passworts', 'error') if not is_ajax: return redirect(url_for('settings')) else: # Standardantwort für AJAX, falls keine spezifische Antwort zurückgegeben wurde return jsonify({ 'success': True, 'message': 'Einstellungen aktualisiert' }) return render_template('settings.html') # API-Endpunkt für Flash-Nachrichten @app.route('/api/get_flash_messages') def get_flash_messages(): """Liefert aktuelle Flash-Nachrichten für API/JS-Clients.""" # Hole alle gespeicherten Flash-Nachrichten messages = [] flashed_messages = session.get('_flashes', []) # Formatierung der Nachrichten für die API-Antwort for category, message in flashed_messages: messages.append({ 'category': category, 'message': message }) # Lösche die Nachrichten aus der Session, nachdem sie abgerufen wurden session.pop('_flashes', None) return jsonify(messages) # Routes für rechtliche Seiten @app.route('/impressum/') def impressum(): return render_template('impressum.html') @app.route('/datenschutz/') def datenschutz(): return render_template('datenschutz.html') @app.route('/agb/') def agb(): return render_template('agb.html') @app.route('/ueber-uns/') def ueber_uns(): return render_template('ueber_uns.html') # Benutzer-Mindmap-Funktionalität @app.route('/my-mindmap/') @login_required def user_mindmap(mindmap_id): mindmap = UserMindmap.query.get_or_404(mindmap_id) # Sicherheitscheck: Nur eigene Mindmaps oder öffentliche Mindmaps if mindmap.user_id != current_user.id and mindmap.is_private: flash("Du hast keinen Zugriff auf diese Mindmap.", "error") return redirect(url_for('profile')) return render_template('user_mindmap.html', mindmap=mindmap) @app.route('/mindmap/create', methods=['GET', 'POST']) @login_required def create_mindmap(): if request.method == 'POST': name = request.form.get('name') description = request.form.get('description') is_private = request.form.get('is_private') == 'on' new_mindmap = UserMindmap( name=name, description=description, user_id=current_user.id, is_private=is_private ) db.session.add(new_mindmap) db.session.commit() flash('Neue Mindmap erfolgreich erstellt!', 'success') return redirect(url_for('user_mindmap', mindmap_id=new_mindmap.id)) return render_template('create_mindmap.html') @app.route('/mindmap//edit', methods=['GET', 'POST']) @login_required def edit_mindmap(mindmap_id): mindmap = UserMindmap.query.get_or_404(mindmap_id) # Sicherheitscheck if mindmap.user_id != current_user.id: flash("Du kannst nur deine eigenen Mindmaps bearbeiten.", "error") return redirect(url_for('profile')) if request.method == 'POST': mindmap.name = request.form.get('name') mindmap.description = request.form.get('description') mindmap.is_private = request.form.get('is_private') == 'on' db.session.commit() flash('Mindmap erfolgreich aktualisiert!', 'success') return redirect(url_for('user_mindmap', mindmap_id=mindmap.id)) return render_template('edit_mindmap.html', mindmap=mindmap) @app.route('/mindmap//delete', methods=['POST']) @login_required def delete_mindmap(mindmap_id): mindmap = UserMindmap.query.get_or_404(mindmap_id) # Sicherheitscheck if mindmap.user_id != current_user.id: flash("Du kannst nur deine eigenen Mindmaps löschen.", "error") return redirect(url_for('profile')) db.session.delete(mindmap) db.session.commit() flash('Mindmap erfolgreich gelöscht!', 'success') return redirect(url_for('profile')) # API-Endpunkte für UserMindmap CRUD-Operationen @app.route('/api/mindmaps', methods=['POST']) @login_required def api_create_user_mindmap(): data = request.get_json() name = data.get('name') description = data.get('description') is_private = data.get('is_private', True) if not name: return jsonify({'error': 'Name ist erforderlich'}), 400 new_mindmap = UserMindmap( name=name, description=description, user_id=current_user.id, is_private=is_private ) db.session.add(new_mindmap) db.session.commit() return jsonify({ 'id': new_mindmap.id, 'name': new_mindmap.name, 'description': new_mindmap.description, 'is_private': new_mindmap.is_private, 'user_id': new_mindmap.user_id, 'created_at': new_mindmap.created_at.isoformat(), 'last_modified': new_mindmap.last_modified.isoformat() }), 201 @app.route('/api/mindmaps', methods=['GET']) @login_required def api_get_user_mindmaps(): mindmaps = UserMindmap.query.filter_by(user_id=current_user.id).all() return jsonify([{ 'id': m.id, 'name': m.name, 'description': m.description, 'is_private': m.is_private, 'created_at': m.created_at.isoformat(), 'last_modified': m.last_modified.isoformat() } for m in mindmaps]) @app.route('/api/mindmaps/', methods=['GET']) @login_required @handle_api_exception def api_get_user_mindmap_detail(mindmap_id): # Berechtigungsprüfung (mindestens READ-Zugriff) has_permission, error_msg = check_mindmap_permission(mindmap_id, "READ") if not has_permission: return ErrorHandler.api_error(error_msg, 403) mindmap = UserMindmap.query.get_or_404(mindmap_id) # Logik für eine detaillierte Ansicht result = { 'id': mindmap.id, 'name': mindmap.name, 'description': mindmap.description, 'is_private': mindmap.is_private, 'user_id': mindmap.user_id, 'created_at': mindmap.created_at.isoformat(), 'last_modified': mindmap.last_modified.isoformat(), 'is_owner': mindmap.user_id == current_user.id } # Berechtigungsinformationen hinzufügen, falls nicht der Eigentümer if mindmap.user_id != current_user.id: share = MindmapShare.query.filter_by( mindmap_id=mindmap_id, shared_with_id=current_user.id ).first() if share: result['permission'] = share.permission_type.name result['owner'] = { 'id': mindmap.user_id, 'username': User.query.get(mindmap.user_id).username } return jsonify(result) @app.route('/api/mindmaps/', methods=['PUT']) @login_required @handle_api_exception def api_update_user_mindmap(mindmap_id): # Berechtigungsprüfung (mindestens EDIT-Zugriff) has_permission, error_msg = check_mindmap_permission(mindmap_id, "EDIT") if not has_permission: return ErrorHandler.api_error(error_msg, 403) mindmap = UserMindmap.query.get_or_404(mindmap_id) data = request.get_json() # Grundlegende Informationen aktualisieren mindmap.name = data.get('name', mindmap.name) mindmap.description = data.get('description', mindmap.description) # is_private kann nur vom Eigentümer geändert werden if mindmap.user_id == current_user.id and 'is_private' in data: mindmap.is_private = data.get('is_private') db.session.commit() return jsonify({ 'id': mindmap.id, 'name': mindmap.name, 'description': mindmap.description, 'is_private': mindmap.is_private, 'last_modified': mindmap.last_modified.isoformat(), 'success': True }) @app.route('/api/mindmaps/', methods=['DELETE']) @login_required @handle_api_exception def api_delete_user_mindmap(mindmap_id): # Nur der Eigentümer kann eine Mindmap löschen mindmap = UserMindmap.query.get_or_404(mindmap_id) if mindmap.user_id != current_user.id: return ErrorHandler.api_error("Nur der Eigentümer kann eine Mindmap löschen.", 403) # Alle Freigaben löschen MindmapShare.query.filter_by(mindmap_id=mindmap_id).delete() # Mindmap löschen db.session.delete(mindmap) db.session.commit() return jsonify({ 'success': True, 'message': 'Mindmap erfolgreich gelöscht' }), 200 # API-Endpunkte für Mindmap-Daten (öffentlich und benutzerspezifisch) @app.route('/api/mindmap/public') def get_public_mindmap(): """Liefert die Standard-Mindmap-Struktur basierend auf Kategorien.""" try: # Hole alle Hauptkategorien categories = Category.query.filter_by(parent_id=None).all() # Transformiere zu einer Baumstruktur category_tree = [build_category_tree(cat) for cat in categories] return jsonify(category_tree) except Exception as e: print(f"Fehler beim Abrufen der Mindmap: {str(e)}") return jsonify({ 'success': False, 'error': 'Mindmap konnte nicht geladen werden' }), 500 @app.route('/api/mindmap/public/add_node', methods=['POST']) @login_required def add_node_to_public_mindmap(): """Fügt einen neuen Knoten zur öffentlichen Mindmap hinzu.""" try: data = request.json name = data.get('name') description = data.get('description', '') color_code = data.get('color_code', '#8b5cf6') x_position = data.get('x_position', 0) y_position = data.get('y_position', 0) if not name: return jsonify({ 'success': False, 'error': 'Knotenname ist erforderlich' }), 400 # Neuen Knoten erstellen new_node = MindMapNode( name=name, description=description, color_code=color_code ) db.session.add(new_node) db.session.flush() # ID generieren # Als Beitrag des aktuellen Benutzers markieren new_node.contributed_by = current_user.id db.session.commit() return jsonify({ 'success': True, 'node_id': new_node.id, 'message': 'Knoten erfolgreich hinzugefügt' }) except Exception as e: db.session.rollback() print(f"Fehler beim Hinzufügen des Knotens: {str(e)}") return jsonify({ 'success': False, 'error': f'Fehler beim Hinzufügen des Knotens: {str(e)}' }), 500 @app.route('/api/mindmap/public/update_node/', methods=['PUT']) @login_required def update_public_node(node_id): """Aktualisiert einen Knoten in der öffentlichen Mindmap.""" try: node = MindMapNode.query.get_or_404(node_id) data = request.json # Aktualisiere Knotendaten if 'name' in data: node.name = data['name'] if 'description' in data: node.description = data['description'] if 'color_code' in data: node.color_code = data['color_code'] # Als bearbeitet markieren node.last_modified = datetime.now(timezone.utc) node.last_modified_by = current_user.id db.session.commit() return jsonify({ 'success': True, 'message': 'Knoten erfolgreich aktualisiert' }) except Exception as e: db.session.rollback() print(f"Fehler beim Aktualisieren des Knotens: {str(e)}") return jsonify({ 'success': False, 'error': f'Fehler beim Aktualisieren des Knotens: {str(e)}' }), 500 @app.route('/api/mindmap/public/remove_node/', methods=['DELETE']) @login_required def remove_node_from_public_mindmap(node_id): """Entfernt einen Knoten aus der öffentlichen Mindmap.""" try: node = MindMapNode.query.get_or_404(node_id) # Lösche den Knoten db.session.delete(node) db.session.commit() return jsonify({ 'success': True, 'message': 'Knoten erfolgreich entfernt' }) except Exception as e: db.session.rollback() print(f"Fehler beim Entfernen des Knotens: {str(e)}") return jsonify({ 'success': False, 'error': f'Fehler beim Entfernen des Knotens: {str(e)}' }), 500 @app.route('/api/mindmap/public/update_layout', methods=['POST']) @login_required def update_public_layout(): """Aktualisiert die Positionen der Knoten in der öffentlichen Mindmap.""" try: data = request.json positions = data.get('positions', []) for pos in positions: node_id = pos.get('node_id') node = MindMapNode.query.get(node_id) if node: # Position aktualisieren node.x_position = pos.get('x_position', 0) node.y_position = pos.get('y_position', 0) db.session.commit() return jsonify({ 'success': True, 'message': 'Layout erfolgreich aktualisiert' }) except Exception as e: db.session.rollback() print(f"Fehler beim Aktualisieren des Layouts: {str(e)}") return jsonify({ 'success': False, 'error': f'Fehler beim Aktualisieren des Layouts: {str(e)}' }), 500 def build_category_tree(category): """ Erstellt eine Baumstruktur für eine Kategorie mit all ihren Unterkategorien und dazugehörigen Knoten Args: category: Ein Category-Objekt Returns: dict: Eine JSON-serialisierbare Darstellung der Kategoriestruktur """ # Kategorie-Basisinformationen category_dict = { 'id': category.id, 'name': category.name, 'description': category.description, 'color_code': category.color_code, 'icon': category.icon, 'nodes': [], 'children': [] } # Knoten zur Kategorie hinzufügen if category.nodes: for node in category.nodes: category_dict['nodes'].append({ 'id': node.id, 'name': node.name, 'description': node.description or '', 'color_code': node.color_code or '#9F7AEA', 'thought_count': len(node.thoughts) if hasattr(node, 'thoughts') else 0 }) # Rekursiv Unterkategorien hinzufügen if category.children: for child in category.children: category_dict['children'].append(build_category_tree(child)) return category_dict @app.route('/api/mindmap/user/') @login_required def get_user_mindmap(mindmap_id): """Liefert die benutzerdefinierte Mindmap-Struktur.""" mindmap = UserMindmap.query.get_or_404(mindmap_id) # Sicherheitscheck if mindmap.user_id != current_user.id and mindmap.is_private: return jsonify({'error': 'Nicht autorisiert'}), 403 # Hole alle verknüpften Knoten mit Position nodes_data = db.session.query( MindMapNode, UserMindmapNode ).join( UserMindmapNode, UserMindmapNode.node_id == MindMapNode.id ).filter( UserMindmapNode.user_mindmap_id == mindmap_id ).all() # Knoten formatieren nodes = [] for node, user_node in nodes_data: nodes.append({ 'id': node.id, 'name': node.name, 'description': node.description, 'color_code': node.color_code, 'x': user_node.x_position, 'y': user_node.y_position, 'scale': user_node.scale, 'thought_count': len(node.thoughts) }) # Hole Notizen zu dieser Mindmap notes = MindmapNote.query.filter_by( mindmap_id=mindmap_id, user_id=current_user.id ).all() notes_data = [{ 'id': note.id, 'content': note.content, 'node_id': note.node_id, 'thought_id': note.thought_id, 'color_code': note.color_code, 'created_at': note.created_at.isoformat() } for note in notes] return jsonify({ 'id': mindmap.id, 'name': mindmap.name, 'description': mindmap.description, 'is_private': mindmap.is_private, 'nodes': nodes, 'notes': notes_data }) @app.route('/api/mindmap/id//add_node', methods=['POST']) @login_required def add_node_to_mindmap(mindmap_id): """Fügt einen öffentlichen Knoten zur Benutzer-Mindmap hinzu.""" data = request.json mindmap = UserMindmap.query.get_or_404(mindmap_id) # Sicherheitscheck if mindmap.user_id != current_user.id: return jsonify({'error': 'Nicht autorisiert'}), 403 node_id = data.get('node_id') parent_id = data.get('parent_id') # Optional: Elternknoten für die Verknüpfung x_pos = data.get('x', 0) y_pos = data.get('y', 0) # Knoten abrufen node = MindMapNode.query.get_or_404(node_id) # Prüfen, ob der Knoten bereits in der Mindmap existiert existing = UserMindmapNode.query.filter_by( user_mindmap_id=mindmap_id, node_id=node_id ).first() if existing: # Update Position existing.x_position = x_pos existing.y_position = y_pos else: # Neuen Knoten hinzufügen user_node = UserMindmapNode( user_mindmap_id=mindmap_id, node_id=node_id, x_position=x_pos, y_position=y_pos ) db.session.add(user_node) # Wenn ein Elternknoten angegeben wurde, Verbindung erstellen if parent_id: # Existenz des Elternknotens in der Mindmap prüfen parent_node = MindMapNode.query.get(parent_id) parent_user_node = UserMindmapNode.query.filter_by( user_mindmap_id=mindmap_id, node_id=parent_id ).first() if parent_node and parent_user_node: # Beziehung zwischen den Knoten erstellen try: parent_node.children.append(node) db.session.flush() except Exception as e: print(f"Warnung: Fehler beim Erstellen der Beziehung: {e}") db.session.commit() return jsonify({ 'success': True, 'node_id': node_id, 'x': x_pos, 'y': y_pos }) @app.route('/api/mindmap/id//remove_node/', methods=['DELETE']) @login_required def remove_node_from_mindmap(mindmap_id, node_id): """Entfernt einen Knoten aus der Benutzer-Mindmap.""" mindmap = UserMindmap.query.get_or_404(mindmap_id) # Sicherheitscheck if mindmap.user_id != current_user.id: return jsonify({'error': 'Nicht autorisiert'}), 403 user_node = UserMindmapNode.query.filter_by( user_mindmap_id=mindmap_id, node_id=node_id ).first_or_404() db.session.delete(user_node) db.session.commit() return jsonify({'success': True}) @app.route('/api/mindmap/node/', methods=['GET']) @login_required def get_node_info(node_id): """Liefert Detailinformationen zu einem einzelnen Knoten""" try: # Knoten abrufen node = MindMapNode.query.get_or_404(node_id) return jsonify({ 'id': node.id, 'name': node.name, 'description': node.description or '', 'color_code': node.color_code or '#9F7AEA' }) except Exception as e: print(f"Fehler in get_node_info: {str(e)}") return jsonify({'success': False, 'message': str(e)}), 500 @app.route('/api/mindmap/id//update_node_position', methods=['POST']) @login_required def update_node_position(mindmap_id): """Aktualisiert die Position eines Knotens in der Benutzer-Mindmap.""" data = request.json mindmap = UserMindmap.query.get_or_404(mindmap_id) # Sicherheitscheck if mindmap.user_id != current_user.id: return jsonify({'error': 'Nicht autorisiert'}), 403 node_id = data.get('node_id') x_pos = data.get('x') y_pos = data.get('y') scale = data.get('scale') user_node = UserMindmapNode.query.filter_by( user_mindmap_id=mindmap_id, node_id=node_id ).first_or_404() if x_pos is not None: user_node.x_position = x_pos if y_pos is not None: user_node.y_position = y_pos if scale is not None: user_node.scale = scale db.session.commit() return jsonify({'success': True}) # Notizen-Funktionalität @app.route('/api/mindmap//notes', methods=['GET']) @login_required def get_mindmap_notes(mindmap_id): """Liefert alle Notizen zu einer Mindmap.""" mindmap = UserMindmap.query.get_or_404(mindmap_id) # Sicherheitscheck if mindmap.user_id != current_user.id: return jsonify({'error': 'Nicht autorisiert'}), 403 notes = MindmapNote.query.filter_by( mindmap_id=mindmap_id, user_id=current_user.id ).all() return jsonify([{ 'id': note.id, 'content': note.content, 'node_id': note.node_id, 'thought_id': note.thought_id, 'color_code': note.color_code, 'created_at': note.created_at.isoformat(), 'last_modified': note.last_modified.isoformat() } for note in notes]) @app.route('/api/mindmap//notes', methods=['POST']) @login_required def add_mindmap_note(mindmap_id): """Fügt eine neue Notiz zur Mindmap hinzu.""" data = request.json mindmap = UserMindmap.query.get_or_404(mindmap_id) # Sicherheitscheck if mindmap.user_id != current_user.id: return jsonify({'error': 'Nicht autorisiert'}), 403 content = data.get('content') node_id = data.get('node_id') thought_id = data.get('thought_id') color_code = data.get('color_code', "#FFF59D") # Gelber Standard note = MindmapNote( user_id=current_user.id, mindmap_id=mindmap_id, node_id=node_id, thought_id=thought_id, content=content, color_code=color_code ) db.session.add(note) db.session.commit() return jsonify({ 'id': note.id, 'content': note.content, 'node_id': note.node_id, 'thought_id': note.thought_id, 'color_code': note.color_code, 'created_at': note.created_at.isoformat(), 'last_modified': note.last_modified.isoformat() }) @app.route('/api/notes/', methods=['PUT']) @login_required def update_note(note_id): """Aktualisiert eine bestehende Notiz.""" data = request.json note = MindmapNote.query.get_or_404(note_id) # Sicherheitscheck if note.user_id != current_user.id: return jsonify({'error': 'Nicht autorisiert'}), 403 content = data.get('content') color_code = data.get('color_code') if content: note.content = content if color_code: note.color_code = color_code note.last_modified = datetime.now(timezone.utc) db.session.commit() return jsonify({ 'id': note.id, 'content': note.content, 'color_code': note.color_code, 'last_modified': note.last_modified.isoformat() }) @app.route('/api/notes/', methods=['DELETE']) @login_required def delete_note(note_id): """Löscht eine Notiz.""" note = MindmapNote.query.get_or_404(note_id) # Sicherheitscheck if note.user_id != current_user.id: return jsonify({'error': 'Nicht autorisiert'}), 403 db.session.delete(note) db.session.commit() return jsonify({'success': True}) # API routes for mindmap and thoughts @app.route('/api/mindmap') def get_mindmap(): """Gibt die Mindmap-Struktur zurück""" try: # Hauptknoten abrufen (Root-Knoten) wissen_node = MindMapNode.query.filter_by(name="Wissen").first() if not wissen_node: # Wissen-Knoten erstellen, falls nicht vorhanden wissen_node = MindMapNode( name="Wissen", description="Zentrale Wissensbasis", color_code="#4299E1", is_public=True ) db.session.add(wissen_node) db.session.commit() # Alle anderen aktiven Knoten holen nodes = MindMapNode.query.filter(MindMapNode.is_public == True).all() # Ergebnisdaten vorbereiten nodes_data = [] edges_data = [] # Knoten hinzufügen for node in nodes: nodes_data.append({ 'id': node.id, 'name': node.name, 'description': node.description or '', 'color_code': node.color_code or '#9F7AEA' }) # Wenn es nicht der Wissen-Knoten ist, Verbindung zum Wissen-Knoten hinzufügen if node.id != wissen_node.id: edges_data.append({ 'source': wissen_node.id, 'target': node.id }) # Beziehungen zwischen Knoten abfragen und hinzufügen relationships = db.session.query(node_relationship).all() for rel in relationships: if rel.parent_id != wissen_node.id: # Doppelte Kanten vermeiden edges_data.append({ 'source': rel.parent_id, 'target': rel.child_id }) return jsonify({ 'nodes': nodes_data, 'edges': edges_data }) except Exception as e: print(f"Fehler beim Abrufen der Mindmap: {str(e)}") return jsonify({ 'success': False, 'error': 'Mindmap konnte nicht geladen werden' }), 500 @app.route('/api/nodes//thoughts') def get_node_thoughts(node_id): """Liefert alle Gedanken, die mit einem Knoten verknüpft sind.""" node = MindMapNode.query.get_or_404(node_id) thoughts = [] for thought in node.thoughts: author = thought.author thoughts.append({ 'id': thought.id, 'title': thought.title, 'abstract': thought.abstract, 'content': thought.content[:200] + '...' if len(thought.content) > 200 else thought.content, 'keywords': thought.keywords, 'author': { 'id': author.id, 'username': author.username }, 'created_at': thought.created_at.isoformat(), 'color_code': thought.color_code, 'avg_rating': thought.average_rating, 'bookmarked': current_user.is_authenticated and thought in current_user.bookmarked_thoughts }) return jsonify(thoughts) @app.route('/api/nodes//thoughts', methods=['POST']) @login_required def add_node_thought(node_id): """Fügt einen neuen Gedanken zu einem Knoten hinzu.""" data = request.json node = MindMapNode.query.get_or_404(node_id) title = data.get('title') content = data.get('content') abstract = data.get('abstract', '') keywords = data.get('keywords', '') color_code = data.get('color_code', '#B39DDB') # Standard-Lila # Kategorie des Knotens bestimmen category_name = node.category.name if node.category else "Allgemein" thought = Thought( title=title, content=content, abstract=abstract, keywords=keywords, color_code=color_code, branch=category_name, user_id=current_user.id, source_type='User Input' ) node.thoughts.append(thought) db.session.add(thought) db.session.commit() return jsonify({ 'id': thought.id, 'title': thought.title, 'success': True }) @app.route('/api/thoughts/', methods=['GET']) def get_thought(thought_id): """Liefert Details zu einem Gedanken.""" thought = Thought.query.get_or_404(thought_id) author = thought.author is_bookmarked = False if current_user.is_authenticated: is_bookmarked = thought in current_user.bookmarked_thoughts # Verknüpfte Knoten abrufen nodes = [{ 'id': node.id, 'name': node.name, 'category': node.category.name if node.category else None } for node in thought.nodes] return jsonify({ 'id': thought.id, 'title': thought.title, 'content': thought.content, 'abstract': thought.abstract, 'keywords': thought.keywords, 'branch': thought.branch, 'color_code': thought.color_code, 'created_at': thought.created_at.isoformat(), 'author': { 'id': author.id, 'username': author.username, 'avatar': author.avatar }, 'avg_rating': thought.average_rating, 'bookmarked': is_bookmarked, 'nodes': nodes, 'source_type': thought.source_type }) @app.route('/api/thoughts', methods=['POST']) @login_required def add_thought(): """Erstellt einen neuen Gedanken.""" data = request.json title = data.get('title') content = data.get('content') abstract = data.get('abstract', '') keywords = data.get('keywords', '') branch = data.get('branch', 'Allgemein') color_code = data.get('color_code', '#B39DDB') thought = Thought( title=title, content=content, abstract=abstract, keywords=keywords, branch=branch, color_code=color_code, user_id=current_user.id, source_type='User Input' ) # Knoten-IDs, falls vorhanden node_ids = data.get('node_ids', []) for node_id in node_ids: node = MindMapNode.query.get(node_id) if node: thought.nodes.append(node) db.session.add(thought) db.session.commit() return jsonify({ 'id': thought.id, 'title': thought.title, 'success': True }) @app.route('/api/thoughts/', methods=['PUT']) @login_required def update_thought(thought_id): """Aktualisiert einen bestehenden Gedanken.""" thought = Thought.query.get_or_404(thought_id) # Sicherheitscheck if thought.user_id != current_user.id and not current_user.is_admin: return jsonify({'error': 'Nicht autorisiert'}), 403 data = request.json # Aktualisiere Felder, die gesendet wurden if 'title' in data: thought.title = data['title'] if 'content' in data: thought.content = data['content'] if 'abstract' in data: thought.abstract = data['abstract'] if 'keywords' in data: thought.keywords = data['keywords'] if 'color_code' in data: thought.color_code = data['color_code'] thought.last_modified = datetime.now(timezone.utc) db.session.commit() return jsonify({ 'id': thought.id, 'success': True }) @app.route('/api/thoughts/', methods=['DELETE']) @login_required def delete_thought(thought_id): """Löscht einen Gedanken.""" thought = Thought.query.get_or_404(thought_id) # Sicherheitscheck if thought.user_id != current_user.id and not current_user.is_admin: return jsonify({'error': 'Nicht autorisiert'}), 403 db.session.delete(thought) db.session.commit() return jsonify({'success': True}) @app.route('/api/thoughts//bookmark', methods=['POST']) @login_required def bookmark_thought(thought_id): """Fügt einen Gedanken zu den Bookmarks des Benutzers hinzu oder entfernt ihn.""" thought = Thought.query.get_or_404(thought_id) # Toggle Bookmark-Status if thought in current_user.bookmarked_thoughts: current_user.bookmarked_thoughts.remove(thought) action = 'removed' else: current_user.bookmarked_thoughts.append(thought) action = 'added' db.session.commit() return jsonify({ 'success': True, 'action': action }) @app.route('/api/categories') def get_categories(): """API-Endpunkt, der alle Kategorien als hierarchische Struktur zurückgibt""" try: # Hole alle Kategorien der obersten Ebene categories = Category.query.filter_by(parent_id=None).all() # Transformiere zu einer Baumstruktur category_tree = [build_category_tree(cat) for cat in categories] return jsonify(category_tree) except Exception as e: print(f"Fehler beim Abrufen der Kategorien: {str(e)}") return jsonify({ 'success': False, 'error': 'Kategorien konnten nicht geladen werden' }), 500 @app.route('/api/set_dark_mode', methods=['POST']) def set_dark_mode(): """Speichert die Dark Mode-Einstellung in der Session.""" data = request.json dark_mode = data.get('darkMode', False) session['dark_mode'] = 'true' if dark_mode else 'false' session.permanent = True return jsonify({ 'success': True, 'darkMode': dark_mode }) @app.route('/api/get_dark_mode', methods=['GET']) def get_dark_mode(): """Liefert die aktuelle Dark Mode-Einstellung.""" dark_mode = session.get('dark_mode', 'true') # Standard: Dark Mode aktiviert return jsonify({ 'success': True, 'darkMode': dark_mode }) # Fehlerhandler @app.errorhandler(404) def page_not_found(e): """404 Fehler - Seite nicht gefunden""" ErrorHandler.log_exception(e, code=404) is_api_request = request.path.startswith('/api/') if is_api_request: return ErrorHandler.api_error("Die angeforderte Ressource wurde nicht gefunden.", 404) return render_template('errors/404.html'), 404 @app.errorhandler(403) def forbidden(e): """403 Fehler - Zugriff verweigert""" ErrorHandler.log_exception(e, code=403) is_api_request = request.path.startswith('/api/') if is_api_request: return ErrorHandler.api_error("Sie haben keine Berechtigung, auf diese Ressource zuzugreifen.", 403) return render_template('errors/403.html'), 403 @app.errorhandler(500) def internal_server_error(e): """500 Fehler - Interner Serverfehler""" ErrorHandler.log_exception(e, code=500) is_api_request = request.path.startswith('/api/') if is_api_request: return ErrorHandler.api_error("Ein interner Serverfehler ist aufgetreten.", 500) return render_template('errors/500.html'), 500 @app.errorhandler(429) def too_many_requests(e): """429 Fehler - Zu viele Anfragen""" ErrorHandler.log_exception(e, code=429) is_api_request = request.path.startswith('/api/') if is_api_request: return ErrorHandler.api_error("Zu viele Anfragen. Bitte versuchen Sie es später erneut.", 429) return render_template('errors/429.html'), 429 @app.errorhandler(400) def bad_request(e): """400 Fehler - Ungültige Anfrage""" ErrorHandler.log_exception(e, code=400) is_api_request = request.path.startswith('/api/') if is_api_request: return ErrorHandler.api_error("Die Anfrage konnte nicht verarbeitet werden.", 400) flash("Die Anfrage konnte nicht verarbeitet werden. Bitte überprüfen Sie Ihre Eingaben.", "error") return render_template('errors/400.html', error=str(e)), 400 @app.errorhandler(401) def unauthorized(e): """401 Fehler - Nicht autorisiert""" ErrorHandler.log_exception(e, code=401) is_api_request = request.path.startswith('/api/') if is_api_request: return ErrorHandler.api_error("Authentifizierung erforderlich.", 401) flash("Sie müssen sich anmelden, um auf diese Seite zuzugreifen.", "error") return redirect(url_for('login')) # OpenAI-Integration für KI-Assistenz @app.route('/api/assistant', methods=['POST']) def chat_with_assistant(): """Chat mit dem KI-Assistenten""" data = request.json user_message = data.get('message', '') # Überprüfe, ob die OpenAI-API verfügbar ist if not openai_available or client is None: # Fallback-Antwort, wenn OpenAI nicht verfügbar ist fallback_message = { "response": "Der KI-Assistent ist derzeit nicht verfügbar. Bitte versuchen Sie es später erneut oder kontaktieren Sie den Administrator.", "thoughts": "Leider konnte keine Verbindung zur OpenAI-API hergestellt werden. Dies kann an SSL-Zertifikatsproblemen, Netzwerkproblemen oder API-Schlüsselproblemen liegen." } return jsonify(fallback_message) # Versuche, eine Antwort von OpenAI zu erhalten try: # Check, ob es eine Datenbankanfrage ist is_db_query, db_query_result = check_database_query(user_message) if is_db_query: return jsonify({ "response": db_query_result, "thoughts": "Ihre Anfrage wurde als Datenbankanfrage erkannt und direkt beantwortet." }) system_message = """Du bist SysTades, ein intelligenter Assistent in einer Wissensmanagement-Anwendung. Deine Aufgabe ist es, tiefe, reflektierte Antworten auf Fragen der Benutzer zu geben. Beziehe dich auf vorhandene Konzepte und betrachte Fragen stets aus mehreren Perspektiven. Vermeide pauschale Antworten und beziehe immer verschiedene Denkansätze ein. Antworte stets auf Deutsch und in einem nachdenklichen, philosophischen Ton. """ response = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": system_message}, {"role": "user", "content": user_message} ], temperature=0.7, timeout=60 # Erhöht auf 60 Sekunden für bessere Zuverlässigkeit ) # Extrahiere die Antwort assistant_response = response.choices[0].message.content # Generiere zusätzliche "Gedanken" für verbesserte UX thoughts_response = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": "Gib kurze Einblicke in deine Gedankenprozesse zu dieser Frage. Schreibe 1-3 Sätze zur Reflexion über die Frage und die wichtigsten Aspekte deiner Antwort. Bleibe dabei informell und persönlich. Schreibe auf Deutsch."}, {"role": "user", "content": user_message}, {"role": "assistant", "content": assistant_response} ], temperature=0.7, max_tokens=150 ) thoughts = thoughts_response.choices[0].message.content return jsonify({ "response": assistant_response, "thoughts": thoughts }) except Exception as e: print(f"Fehler bei der OpenAI-Anfrage: {e}") import traceback print(f"Stack Trace: {traceback.format_exc()}") # Fallback-Antwort bei Fehler fallback_message = { "response": "Es tut mir leid, aber ich konnte Ihre Anfrage nicht verarbeiten. Bitte versuchen Sie es später erneut.", "thoughts": "Ein technisches Problem ist aufgetreten. Dies könnte an Netzwerkproblemen, API-Grenzen oder Serverauslastung liegen." } return jsonify(fallback_message) def check_database_query(user_message): """ Prüft, ob die Benutzeranfrage nach Datenbankinformationen sucht und gibt relevante Informationen aus der Datenbank zurück. Returns: (bool, str): Tupel aus (ist_db_anfrage, db_antwort) """ user_message = user_message.lower() # Schlüsselwörter, die auf eine Datenbankanfrage hindeuten db_keywords = [ 'wie viele', 'wieviele', 'anzahl', 'liste', 'zeige', 'zeig mir', 'datenbank', 'statistik', 'stats', 'benutzer', 'gedanken', 'kategorien', 'mindmap', 'neueste', 'kürzlich', 'gespeichert', 'zähle', 'suche nach', 'finde' ] # Überprüfen, ob die Nachricht eine Datenbankanfrage sein könnte is_db_query = any(keyword in user_message for keyword in db_keywords) if not is_db_query: return False, "" # Datenbank-Antwort vorbereiten response = "Hier sind Informationen aus der Datenbank:\n\n" try: # 1. Benutzer-Statistiken if any(kw in user_message for kw in ['benutzer', 'user', 'nutzer']): user_count = User.query.count() active_users = User.query.filter_by(is_active=True).count() admins = User.query.filter_by(role='admin').count() response += f"**Benutzer-Statistiken:**\n" response += f"- Gesamt: {user_count} Benutzer\n" response += f"- Aktiv: {active_users} Benutzer\n" response += f"- Administratoren: {admins} Benutzer\n\n" # 2. Gedanken-Statistiken if any(kw in user_message for kw in ['gedanken', 'thought', 'inhalt']): thought_count = Thought.query.count() if thought_count > 0: avg_rating = db.session.query(func.avg(ThoughtRating.relevance_score)).scalar() or 0 avg_rating = round(avg_rating, 1) recent_thoughts = Thought.query.order_by(Thought.created_at.desc()).limit(5).all() response += f"**Gedanken-Statistiken:**\n" response += f"- Gesamt: {thought_count} Gedanken\n" response += f"- Durchschnittliche Bewertung: {avg_rating}/5\n\n" if recent_thoughts: response += "**Neueste Gedanken:**\n" for thought in recent_thoughts: response += f"- {thought.title} (von {thought.author.username})\n" response += "\n" else: response += "Es sind noch keine Gedanken in der Datenbank gespeichert.\n\n" # 3. Kategorien-Statistiken if any(kw in user_message for kw in ['kategorie', 'category', 'thema']): category_count = Category.query.filter_by(parent_id=None).count() subcategory_count = Category.query.filter(Category.parent_id != None).count() response += f"**Kategorien-Statistiken:**\n" response += f"- Hauptkategorien: {category_count}\n" response += f"- Unterkategorien: {subcategory_count}\n\n" main_categories = Category.query.filter_by(parent_id=None).all() if main_categories: response += "**Hauptkategorien:**\n" for category in main_categories: subcats = Category.query.filter_by(parent_id=category.id).count() response += f"- {category.name} ({subcats} Unterkategorien)\n" response += "\n" # 4. Mindmap-Statistiken if any(kw in user_message for kw in ['mindmap', 'karte', 'map', 'knoten']): public_nodes = MindMapNode.query.count() user_mindmaps = UserMindmap.query.count() response += f"**Mindmap-Statistiken:**\n" response += f"- Öffentliche Knoten: {public_nodes}\n" response += f"- Benutzerdefinierte Mindmaps: {user_mindmaps}\n\n" if user_mindmaps > 0: recent_mindmaps = UserMindmap.query.order_by(UserMindmap.created_at.desc()).limit(3).all() if recent_mindmaps: response += "**Neueste Mindmaps:**\n" for mindmap in recent_mindmaps: response += f"- {mindmap.name} (von {mindmap.user.username})\n" response += "\n" # Wenn keine spezifischen Daten angefordert wurden, allgemeine Übersicht geben if not any(kw in user_message for kw in ['benutzer', 'user', 'gedanken', 'thought', 'kategorie', 'category', 'mindmap']): users = User.query.count() thoughts = Thought.query.count() categories = Category.query.count() nodes = MindMapNode.query.count() user_maps = UserMindmap.query.count() response += f"**Übersicht über die Datenbank:**\n" response += f"- Benutzer: {users}\n" response += f"- Gedanken: {thoughts}\n" response += f"- Kategorien: {categories}\n" response += f"- Mindmap-Knoten: {nodes}\n" response += f"- Benutzerdefinierte Mindmaps: {user_maps}\n" return True, response except Exception as e: import traceback print(f"Fehler bei der Datenbankabfrage: {e}") print(traceback.format_exc()) return True, "Es ist ein Fehler bei der Abfrage der Datenbank aufgetreten. Bitte versuchen Sie es später erneut." @app.route('/search') def search_thoughts_page(): """Seite zur Gedankensuche anzeigen.""" return render_template('search.html') @app.route('/my_account') def my_account(): """Zeigt die persönliche Merkliste an.""" if not current_user.is_authenticated: flash('Bitte melde dich an, um auf deine Merkliste zuzugreifen.', 'warning') return redirect(url_for('login')) # Hole die Lesezeichen des Benutzers bookmarked_thoughts = current_user.bookmarked_thoughts return render_template('my_account.html', bookmarked_thoughts=bookmarked_thoughts) # Dummy-Route, um 404-Fehler für fehlende Netzwerk-Hintergrundbilder zu vermeiden @app.route('/static/network-bg.jpg') @app.route('/static/network-bg.svg') def dummy_network_bg(): """Leere Antwort für die nicht mehr verwendeten Netzwerk-Hintergrundbilder.""" return '', 200 # Route zum expliziten Neu-Laden der Umgebungsvariablen @app.route('/admin/reload-env', methods=['POST']) @admin_required def reload_env(): """Lädt die Umgebungsvariablen aus der .env-Datei neu.""" try: # Erzwinge das Neuladen der .env-Datei load_dotenv(override=True, force=True) # OpenAI API-Key ist bereits fest kodiert # client wurde bereits mit festem API-Key initialisiert # Weitere Umgebungsvariablen hier aktualisieren, falls nötig app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', app.config['SECRET_KEY']) return jsonify({ 'success': True, 'message': 'Umgebungsvariablen wurden erfolgreich neu geladen.' }) except Exception as e: return jsonify({ 'success': False, 'message': f'Fehler beim Neuladen der Umgebungsvariablen: {str(e)}' }), 500 # Berechtigungsverwaltung für Mindmaps @app.route('/api/mindmap//shares', methods=['GET']) @login_required @handle_api_exception def get_mindmap_shares(mindmap_id): """Listet alle Benutzer auf, mit denen eine Mindmap geteilt wurde.""" # Überprüfen, ob die Mindmap dem Benutzer gehört mindmap = UserMindmap.query.get_or_404(mindmap_id) if mindmap.user_id != current_user.id: return ErrorHandler.api_error("Sie haben keine Berechtigung, die Freigaben dieser Mindmap einzusehen.", 403) # Alle Freigaben für diese Mindmap abrufen shares = MindmapShare.query.filter_by(mindmap_id=mindmap_id).all() result = [] for share in shares: # Benutzerinformationen abrufen shared_with_user = User.query.get(share.shared_with_id) if shared_with_user: result.append({ 'id': share.id, 'user_id': shared_with_user.id, 'username': shared_with_user.username, 'email': shared_with_user.email, 'permission': share.permission_type.name, 'created_at': share.created_at.isoformat(), 'last_accessed': share.last_accessed.isoformat() if share.last_accessed else None }) return jsonify({ 'success': True, 'shares': result }) @app.route('/api/mindmap//share', methods=['POST']) @login_required @handle_api_exception def share_mindmap(mindmap_id): """Teilt eine Mindmap mit einem anderen Benutzer.""" # Überprüfen, ob die Mindmap dem Benutzer gehört mindmap = UserMindmap.query.get_or_404(mindmap_id) if mindmap.user_id != current_user.id: return ErrorHandler.api_error("Sie haben keine Berechtigung, diese Mindmap zu teilen.", 403) data = request.json if not data or 'email' not in data or 'permission' not in data: return ErrorHandler.api_error("E-Mail-Adresse und Berechtigungstyp sind erforderlich.", 400) # Benutzer anhand der E-Mail-Adresse finden user_to_share_with = User.query.filter_by(email=data['email']).first() if not user_to_share_with: return ErrorHandler.api_error("Kein Benutzer mit dieser E-Mail-Adresse gefunden.", 404) # Prüfen, ob der Benutzer versucht, mit sich selbst zu teilen if user_to_share_with.id == current_user.id: return ErrorHandler.api_error("Sie können die Mindmap nicht mit sich selbst teilen.", 400) # Prüfen, ob die Mindmap bereits mit diesem Benutzer geteilt wurde existing_share = MindmapShare.query.filter_by( mindmap_id=mindmap_id, shared_with_id=user_to_share_with.id ).first() # Berechtigungstyp validieren und konvertieren try: permission_type = PermissionType[data['permission']] except (KeyError, ValueError): return ErrorHandler.api_error( "Ungültiger Berechtigungstyp. Erlaubte Werte sind: READ, EDIT, ADMIN.", 400 ) if existing_share: # Wenn bereits geteilt, aktualisiere die Berechtigungen existing_share.permission_type = permission_type message = "Berechtigungen erfolgreich aktualisiert." else: # Wenn noch nicht geteilt, erstelle eine neue Freigabe new_share = MindmapShare( mindmap_id=mindmap_id, shared_by_id=current_user.id, shared_with_id=user_to_share_with.id, permission_type=permission_type ) db.session.add(new_share) message = "Mindmap erfolgreich geteilt." # Wenn die Mindmap bisher privat war, mache sie jetzt nicht mehr privat if mindmap.is_private: mindmap.is_private = False db.session.commit() return jsonify({ 'success': True, 'message': message }) @app.route('/api/mindmap/shares/', methods=['PUT']) @login_required @handle_api_exception def update_mindmap_share(share_id): """Aktualisiert die Berechtigungen für eine geteilte Mindmap.""" # Freigabe finden share = MindmapShare.query.get_or_404(share_id) # Prüfen, ob der Benutzer der Eigentümer der Mindmap ist mindmap = UserMindmap.query.get(share.mindmap_id) if not mindmap or mindmap.user_id != current_user.id: return ErrorHandler.api_error("Sie haben keine Berechtigung, diese Freigabe zu ändern.", 403) data = request.json if not data or 'permission' not in data: return ErrorHandler.api_error("Berechtigungstyp ist erforderlich.", 400) # Berechtigungstyp validieren und konvertieren try: permission_type = PermissionType[data['permission']] except (KeyError, ValueError): return ErrorHandler.api_error( "Ungültiger Berechtigungstyp. Erlaubte Werte sind: READ, EDIT, ADMIN.", 400 ) # Berechtigungen aktualisieren share.permission_type = permission_type db.session.commit() return jsonify({ 'success': True, 'message': "Berechtigungen erfolgreich aktualisiert." }) @app.route('/api/mindmap/shares/', methods=['DELETE']) @login_required @handle_api_exception def revoke_mindmap_share(share_id): """Widerruft die Freigabe einer Mindmap für einen Benutzer.""" # Freigabe finden share = MindmapShare.query.get_or_404(share_id) # Prüfen, ob der Benutzer der Eigentümer der Mindmap ist mindmap = UserMindmap.query.get(share.mindmap_id) if not mindmap or mindmap.user_id != current_user.id: return ErrorHandler.api_error("Sie haben keine Berechtigung, diese Freigabe zu widerrufen.", 403) # Freigabe löschen db.session.delete(share) # Prüfen, ob dies die letzte Freigabe war und ggf. Mindmap wieder privat setzen remaining_shares = MindmapShare.query.filter_by(mindmap_id=mindmap.id).count() if remaining_shares == 0: mindmap.is_private = True db.session.commit() return jsonify({ 'success': True, 'message': "Freigabe erfolgreich widerrufen." }) @app.route('/api/mindmaps/shared-with-me', methods=['GET']) @login_required @handle_api_exception def get_shared_mindmaps(): """Listet alle Mindmaps auf, die mit dem aktuellen Benutzer geteilt wurden.""" shares = MindmapShare.query.filter_by(shared_with_id=current_user.id).all() result = [] for share in shares: mindmap = UserMindmap.query.get(share.mindmap_id) owner = User.query.get(mindmap.user_id) if mindmap and owner: # Zugriffsdatum aktualisieren share.last_accessed = datetime.now(timezone.utc) result.append({ 'id': mindmap.id, 'name': mindmap.name, 'description': mindmap.description, 'owner': { 'id': owner.id, 'username': owner.username }, 'created_at': mindmap.created_at.isoformat(), 'last_modified': mindmap.last_modified.isoformat(), 'permission': share.permission_type.name }) db.session.commit() # Speichere die aktualisierten Zugriffsdaten return jsonify({ 'success': True, 'mindmaps': result }) # Hilfsfunktion zur Überprüfung der Berechtigungen def check_mindmap_permission(mindmap_id, permission_type=None): """ Überprüft, ob der aktuelle Benutzer Zugriff auf eine Mindmap hat. Args: mindmap_id: ID der Mindmap permission_type: Mindestberechtigung, die erforderlich ist (READ, EDIT, ADMIN) Wenn None, wird nur der Zugriff überprüft. Returns: (bool, str): Tupel aus (hat_berechtigung, fehlermeldung) """ mindmap = UserMindmap.query.get(mindmap_id) if not mindmap: return False, "Mindmap nicht gefunden." # Wenn der Benutzer der Eigentümer ist, hat er vollen Zugriff if mindmap.user_id == current_user.id: return True, "" # Wenn die Mindmap privat ist und der Benutzer nicht der Eigentümer ist if mindmap.is_private: share = MindmapShare.query.filter_by( mindmap_id=mindmap_id, shared_with_id=current_user.id ).first() if not share: return False, "Sie haben keinen Zugriff auf diese Mindmap." # Wenn eine bestimmte Berechtigungsstufe erforderlich ist if permission_type: required_level = PermissionType[permission_type].value user_level = share.permission_type.value if required_level not in [p.value for p in PermissionType]: return False, f"Ungültiger Berechtigungstyp: {permission_type}" # Berechtigungshierarchie prüfen permission_hierarchy = { PermissionType.READ.name: 1, PermissionType.EDIT.name: 2, PermissionType.ADMIN.name: 3 } if permission_hierarchy[share.permission_type.name] < permission_hierarchy[permission_type]: return False, f"Sie benötigen {permission_type}-Berechtigungen für diese Aktion." return True, "" # Flask starten if __name__ == '__main__': with app.app_context(): # Make sure tables exist db.create_all() socketio.run(app, debug=True, host='0.0.0.0') def get_category_mindmap_data(category_name): """Generische Funktion zum Abrufen der Mindmap-Daten für eine Kategorie.""" try: # Kategorie mit allen Unterkategorien in einer Abfrage laden category = Category.query.filter_by(name=category_name).options( joinedload(Category.children) ).first_or_404() # Basis-Knoten erstellen nodes = [{ 'id': f'cat_{category.id}', 'name': category.name, 'description': category.description or '', 'color_code': category.color_code or '#9F7AEA', 'is_center': True, 'has_children': bool(category.children), 'icon': category.icon or 'fa-solid fa-circle' }] # Unterkategorien hinzufügen for subcat in category.children: nodes.append({ 'id': f'cat_{subcat.id}', 'name': subcat.name, 'description': subcat.description or '', 'color_code': subcat.color_code or '#9F7AEA', 'category': category_name, 'has_children': bool(subcat.children), 'icon': subcat.icon or 'fa-solid fa-circle' }) # Kanten erstellen (vereinheitlichte Schlüssel) edges = [{ 'source': f'cat_{category.id}', 'target': f'cat_{subcat.id}', 'strength': 0.8 } for subcat in category.children] return jsonify({ 'success': True, 'nodes': nodes, 'edges': edges }) except Exception as e: print(f"Fehler beim Abrufen der {category_name}-Mindmap: {str(e)}") return jsonify({ 'success': False, 'error': f'{category_name}-Mindmap konnte nicht geladen werden', 'details': str(e) }), 500 @app.route('/api/mindmap/root') def get_root_mindmap_data(): """Liefert die Daten für die Root-Mindmap.""" try: # Hauptkategorien mit Unterkategorien in einer Abfrage laden categories = Category.query.filter_by(parent_id=None).options( joinedload(Category.children) ).all() # Überprüfen, ob Kategorien vorhanden sind if not categories: print("Keine Hauptkategorien gefunden") return jsonify({ 'success': False, 'error': 'Keine Hauptkategorien gefunden', 'details': 'Bitte führen Sie das Datenbank-Initialisierungsskript aus' }), 404 print(f"Gefundene Hauptkategorien: {[cat.name for cat in categories]}") # Basis-Knoten erstellen nodes = [{ 'id': 'root', 'name': 'Wissen', 'description': 'Zentrale Wissensbasis', 'color_code': '#4299E1', 'is_center': True, 'has_children': bool(categories), 'icon': 'fa-solid fa-circle' }] # Kategorien als Knoten hinzufügen for category in categories: nodes.append({ 'id': f'cat_{category.id}', 'name': category.name, 'description': category.description or '', 'color_code': category.color_code or '#9F7AEA', 'category': category.name, 'has_children': bool(category.children.count() > 0), 'icon': category.icon or 'fa-solid fa-circle' }) # Kanten erstellen (vereinheitlichte Schlüssel) edges = [{ 'source': 'root', 'target': f'cat_{category.id}', 'strength': 0.8 } for category in categories] return jsonify({ 'success': True, 'nodes': nodes, 'edges': edges }) except Exception as e: print(f"Fehler beim Abrufen der Root-Mindmap: {str(e)}") traceback.print_exc() return jsonify({ 'success': False, 'error': 'Root-Mindmap konnte nicht geladen werden', 'details': str(e) }), 500 # Spezifische Routen für Kategorien @app.route('/api/mindmap/philosophy') def get_philosophy_mindmap_data(): return get_category_mindmap_data('Philosophie') @app.route('/api/mindmap/science') def get_science_mindmap_data(): return get_category_mindmap_data('Wissenschaft') @app.route('/api/mindmap/technology') def get_technology_mindmap_data(): return get_category_mindmap_data('Technologie') @app.route('/api/mindmap/arts') def get_arts_mindmap_data(): return get_category_mindmap_data('Künste') # Generische Route für spezifische Knoten @app.route('/api/mindmap/') def get_mindmap_data(node_id): """Liefert die Daten für einen spezifischen Mindmap-Knoten.""" try: # Prüfen, ob es sich um eine spezielle Route handelt if node_id in ['root', 'philosophy', 'science', 'technology', 'arts']: return jsonify({ 'success': False, 'error': 'Ungültige Knoten-ID', 'details': 'Diese ID ist für spezielle Routen reserviert' }), 400 # Knoten mit Unterknoten in einer Abfrage laden node = MindMapNode.query.options( joinedload(MindMapNode.children) ).get_or_404(node_id) # Basis-Knoten erstellen nodes = [{ 'id': str(node.id), 'name': node.name, 'description': node.description or '', 'color_code': node.color_code or '#9F7AEA', 'is_center': True, 'has_children': bool(node.children), 'icon': node.icon or 'fa-solid fa-circle' }] # Unterknoten hinzufügen for child in node.children: nodes.append({ 'id': str(child.id), 'name': child.name, 'description': child.description or '', 'color_code': child.color_code or '#9F7AEA', 'category': node.name, 'has_children': bool(child.children), 'icon': child.icon or 'fa-solid fa-circle' }) # Kanten erstellen (vereinheitlichte Schlüssel) edges = [{ 'source': str(node.id), 'target': str(child.id), 'strength': 0.8 } for child in node.children] return jsonify({ 'success': True, 'nodes': nodes, 'edges': edges }) except Exception as e: print(f"Fehler beim Abrufen der Mindmap-Daten für Knoten {node_id}: {str(e)}") return jsonify({ 'success': False, 'error': 'Mindmap-Daten konnten nicht geladen werden', 'details': str(e) }), 500