2527 lines
90 KiB
Python
2527 lines
90 KiB
Python
#!/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/<int:mindmap_id>')
|
|
@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/<int:mindmap_id>/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/<int:mindmap_id>/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/<int:mindmap_id>', 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/<int:mindmap_id>', 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/<int:mindmap_id>', 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/<int:node_id>', 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/<int:node_id>', 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/<int:mindmap_id>')
|
|
@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/<int: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/<int:mindmap_id>/remove_node/<int:node_id>', 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/<int:node_id>', 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/<int: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/<int:mindmap_id>/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/<int:mindmap_id>/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/<int:note_id>', 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/<int:note_id>', 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/<int:node_id>/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/<int:node_id>/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/<int:thought_id>', 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/<int:thought_id>', 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/<int:thought_id>', 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/<int:thought_id>/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/<int:mindmap_id>/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/<int:mindmap_id>/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/<int:share_id>', 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/<int:share_id>', 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()
|
|
|
|
# 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),
|
|
'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)}")
|
|
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/<node_id>')
|
|
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
|
|
|