3351 lines
117 KiB
Python
3351 lines
117 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
|
|
import uuid as uuid_pkg
|
|
|
|
# Custom Logger Integration
|
|
from utils.logger import get_logger, setup_logging, log_execution_time, log_api_call, performance_monitor, log_user_activity
|
|
|
|
# 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, SocialPost, SocialComment, Notification,
|
|
user_follows
|
|
)
|
|
|
|
# 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'))
|
|
|
|
# Initialisiere das erweiterte Logging-System
|
|
setup_logging(app, log_level=os.environ.get('LOG_LEVEL', 'INFO'))
|
|
logger = get_logger('SysTades')
|
|
|
|
# System-Start protokollieren
|
|
logger.system_startup(
|
|
version='1.0.0',
|
|
environment=os.environ.get('FLASK_ENV', 'development'),
|
|
port=5000
|
|
)
|
|
|
|
# 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
|
|
logger.error(
|
|
f"Fehler {code}: {str(e)}\n{request_info}\n{user_info}\n{trace}",
|
|
component='ERROR',
|
|
user=current_user.username if current_user and current_user.is_authenticated else None,
|
|
ip=request.remote_addr if request else None
|
|
)
|
|
|
|
@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
|
|
logger.info("OpenAI API-Verbindung erfolgreich hergestellt", component='SYSTEM')
|
|
except Exception as e:
|
|
logger.warning(f"OpenAI API-Verbindungstest fehlgeschlagen: {e}", component='ERROR')
|
|
openai_available = False
|
|
except Exception as e:
|
|
logger.error(f"Fehler bei der Initialisierung des OpenAI-Clients: {e}", component='ERROR')
|
|
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()
|
|
logger.info(f"{len(users)} Benutzer wurden erstellt", component='DB')
|
|
|
|
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()
|
|
logger.info("Standard-Kategorien wurden erstellt!", component='DB')
|
|
|
|
def initialize_database():
|
|
"""Initialisiert die Datenbank mit Grunddaten, falls diese leer ist"""
|
|
try:
|
|
logger.info("Initialisiere die Datenbank...", component='DB')
|
|
|
|
# Erstelle alle Tabellen
|
|
db.create_all()
|
|
|
|
# Prüfen, ob bereits Kategorien existieren
|
|
categories_count = Category.query.count()
|
|
users_count = User.query.count()
|
|
|
|
logger.info(
|
|
f"Datenbank-Status: {categories_count} Kategorien, {users_count} Benutzer",
|
|
component='DB'
|
|
)
|
|
|
|
# Erstelle Standarddaten, wenn es keine Kategorien gibt
|
|
if categories_count == 0:
|
|
logger.info("Erstelle Standard-Kategorien...", component='DB')
|
|
create_default_categories()
|
|
|
|
# Admin-Benutzer erstellen, wenn keine Benutzer vorhanden sind
|
|
if users_count == 0:
|
|
logger.info("Erstelle Admin-Benutzer...", component='DB')
|
|
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()
|
|
logger.info("Admin-Benutzer wurde erstellt!", component='DB')
|
|
|
|
# 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()
|
|
logger.info("'Wissen'-Knoten wurde erstellt", component='DB')
|
|
|
|
# Überprüfe, ob es Kategorien gibt, sonst erstelle sie
|
|
if Category.query.count() == 0:
|
|
create_default_categories()
|
|
logger.info("Kategorien wurden erstellt", component='DB')
|
|
|
|
logger.info("Datenbank-Initialisierung erfolgreich abgeschlossen", component='DB')
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"Fehler bei Datenbank-Initialisierung: {e}", component='ERROR')
|
|
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, app_instance):
|
|
print("WARNUNG: Datenbankinitialisierung fehlgeschlagen. Einige Funktionen könnten eingeschränkt sein.")
|
|
return False
|
|
return True
|
|
|
|
# Call the function to initialize the database
|
|
# init_app_database(app) # Commented out to prevent context error - will be moved to end
|
|
|
|
# If the initialization failed, log a warning
|
|
# if not initialize_db_if_needed(db, initialize_database): # Commented out to prevent context error
|
|
# logger.warning(
|
|
# "Datenbankinitialisierung fehlgeschlagen. Einige Funktionen könnten eingeschränkt sein.",
|
|
# component='ERROR'
|
|
# )
|
|
|
|
# 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'])
|
|
@log_execution_time(component='AUTH')
|
|
def login():
|
|
if request.method == 'POST':
|
|
# Handle form submission
|
|
username = request.form['username']
|
|
password = request.form['password']
|
|
remember_me = 'remember_me' in request.form
|
|
|
|
user = User.query.filter_by(username=username).first()
|
|
|
|
if user and user.check_password(password):
|
|
login_user(user, remember=remember_me)
|
|
user.last_login = datetime.utcnow()
|
|
db.session.commit()
|
|
|
|
logger.user_activity(
|
|
user_id=user.id,
|
|
action='login',
|
|
details=f'Login erfolgreich für {username}',
|
|
ip=request.remote_addr
|
|
)
|
|
|
|
flash('Erfolgreich angemeldet!', 'success')
|
|
return redirect(url_for('mindmap'))
|
|
else:
|
|
logger.user_activity(
|
|
user_id=None,
|
|
action='failed_login',
|
|
details=f'Fehlgeschlagener Login-Versuch für {username}',
|
|
ip=request.remote_addr
|
|
)
|
|
flash('Ungültiger Benutzername oder Passwort', 'error')
|
|
|
|
return render_template('login.html')
|
|
|
|
# Route für /auth/login für Kompatibilität
|
|
@app.route('/auth/login', methods=['GET', 'POST'])
|
|
@log_execution_time(component='AUTH')
|
|
def auth_login():
|
|
"""Redirect /auth/login to /login for compatibility"""
|
|
return redirect(url_for('login'))
|
|
|
|
@app.route('/register', methods=['GET', 'POST'])
|
|
@log_execution_time(component='AUTH')
|
|
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():
|
|
logger.warning(
|
|
f"Registrierung fehlgeschlagen: Benutzername '{username}' bereits vergeben",
|
|
component='AUTH',
|
|
ip=request.remote_addr
|
|
)
|
|
flash('Benutzername existiert bereits')
|
|
return redirect(url_for('register'))
|
|
|
|
if User.query.filter_by(email=email).first():
|
|
logger.warning(
|
|
f"Registrierung fehlgeschlagen: E-Mail '{email}' bereits registriert",
|
|
component='AUTH',
|
|
ip=request.remote_addr
|
|
)
|
|
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
|
|
|
|
# Erfolgreiche Registrierung protokollieren
|
|
logger.info(
|
|
f"Neuer Benutzer registriert: {username} ({email})",
|
|
component='AUTH',
|
|
ip=request.remote_addr
|
|
)
|
|
|
|
# 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()
|
|
|
|
logger.info(
|
|
f"Standard-Mindmap für Benutzer {username} erstellt",
|
|
component='DB',
|
|
user=username
|
|
)
|
|
except Exception as e:
|
|
logger.error(
|
|
f"Fehler beim Erstellen der Standard-Mindmap für {username}: {e}",
|
|
component='ERROR'
|
|
)
|
|
# 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
|
|
@log_execution_time(component='AUTH')
|
|
def logout():
|
|
username = current_user.username if current_user.is_authenticated else 'Unknown'
|
|
logout_user()
|
|
|
|
# Abmeldung protokollieren
|
|
logger.info(
|
|
f"Benutzer {username} abgemeldet",
|
|
component='AUTH',
|
|
ip=request.remote_addr
|
|
)
|
|
|
|
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()
|
|
logger.info("'Wissen'-Knoten wurde erstellt", component='DB')
|
|
|
|
# Überprüfe, ob es Kategorien gibt, sonst erstelle sie
|
|
if Category.query.count() == 0:
|
|
create_default_categories()
|
|
logger.info("Kategorien wurden erstellt", component='DB')
|
|
|
|
# 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
|
|
@log_api_call
|
|
@performance_monitor('mindmap_creation')
|
|
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()
|
|
|
|
logger.user_action(
|
|
username=current_user.username,
|
|
action='mindmap_created',
|
|
details=f"Neue Mindmap '{name}' erstellt"
|
|
)
|
|
|
|
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
|
|
@log_api_call
|
|
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'])
|
|
@log_api_call
|
|
@performance_monitor('ai_assistant')
|
|
def chat_with_assistant():
|
|
"""Chat mit dem KI-Assistenten"""
|
|
data = request.json
|
|
user_message = data.get('message', '')
|
|
|
|
user_name = current_user.username if current_user.is_authenticated else 'Anonymous'
|
|
|
|
logger.info(
|
|
f"KI-Assistent Anfrage von {user_name}: {user_message[:100]}...",
|
|
component='API',
|
|
user=user_name if current_user.is_authenticated else None
|
|
)
|
|
|
|
# Überprüfe, ob die OpenAI-API verfügbar ist
|
|
if not openai_available or client is None:
|
|
logger.warning(
|
|
"OpenAI API nicht verfügbar - Fallback-Antwort gesendet",
|
|
component='API'
|
|
)
|
|
# 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()}")
|
|
|
|
logger.error(
|
|
f"Fehler bei der OpenAI-Anfrage von {user_name}: {e}",
|
|
component='ERROR',
|
|
user=user_name if current_user.is_authenticated else None
|
|
)
|
|
|
|
# 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 .env-Datei neu"""
|
|
try:
|
|
# Lade die .env-Datei neu
|
|
from dotenv import load_dotenv
|
|
load_dotenv(override=True)
|
|
|
|
# Aktualisiere die App-Konfiguration
|
|
app.config['OPENAI_API_KEY'] = os.getenv('OPENAI_API_KEY')
|
|
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY')
|
|
app.config['DATABASE_URL'] = os.getenv('DATABASE_URL')
|
|
|
|
# Logge die Aktion
|
|
ErrorHandler.log_exception(
|
|
Exception("Environment reloaded"),
|
|
endpoint="admin/reload-env",
|
|
code=200
|
|
)
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': 'Umgebungsvariablen erfolgreich neu geladen'
|
|
})
|
|
except Exception as e:
|
|
ErrorHandler.log_exception(e, "admin/reload-env")
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Fehler beim Neuladen der Umgebungsvariablen',
|
|
'details': str(e)
|
|
}), 500
|
|
|
|
@app.route('/admin/api/dashboard-data', methods=['GET'])
|
|
@admin_required
|
|
def admin_dashboard_data():
|
|
"""Liefert Dashboard-Daten für den Admin-Bereich"""
|
|
try:
|
|
# Benutzerstatistiken
|
|
total_users = User.query.count()
|
|
active_users = User.query.filter_by(is_active=True).count()
|
|
admin_users = User.query.filter_by(role='admin').count()
|
|
|
|
# Mindmap-Statistiken
|
|
total_mindmaps = UserMindmap.query.count()
|
|
public_mindmaps = UserMindmap.query.filter_by(is_private=False).count()
|
|
|
|
# Gedanken-Statistiken
|
|
total_thoughts = Thought.query.count()
|
|
|
|
# Knoten-Statistiken
|
|
total_nodes = MindMapNode.query.count()
|
|
public_nodes = MindMapNode.query.filter_by(is_public=True).count()
|
|
|
|
# Social-Statistiken
|
|
total_posts = SocialPost.query.count()
|
|
total_comments = SocialComment.query.count()
|
|
total_notifications = Notification.query.count()
|
|
unread_notifications = Notification.query.filter_by(is_read=False).count()
|
|
|
|
# Kategorien-Statistiken
|
|
total_categories = Category.query.count()
|
|
|
|
# Neueste Aktivitäten (letzte 10)
|
|
recent_users = User.query.order_by(User.created_at.desc()).limit(5).all()
|
|
recent_thoughts = Thought.query.order_by(Thought.created_at.desc()).limit(5).all()
|
|
recent_mindmaps = UserMindmap.query.order_by(UserMindmap.created_at.desc()).limit(5).all()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'data': {
|
|
'users': {
|
|
'total': total_users,
|
|
'active': active_users,
|
|
'admins': admin_users,
|
|
'recent': [{'id': u.id, 'username': u.username, 'created_at': u.created_at.isoformat()} for u in recent_users]
|
|
},
|
|
'mindmaps': {
|
|
'total': total_mindmaps,
|
|
'public': public_mindmaps,
|
|
'private': total_mindmaps - public_mindmaps,
|
|
'recent': [{'id': m.id, 'name': m.name, 'created_at': m.created_at.isoformat()} for m in recent_mindmaps]
|
|
},
|
|
'thoughts': {
|
|
'total': total_thoughts,
|
|
'recent': [{'id': t.id, 'title': t.title, 'created_at': t.created_at.isoformat()} for t in recent_thoughts]
|
|
},
|
|
'nodes': {
|
|
'total': total_nodes,
|
|
'public': public_nodes
|
|
},
|
|
'social': {
|
|
'posts': total_posts,
|
|
'comments': total_comments,
|
|
'notifications': total_notifications,
|
|
'unread_notifications': unread_notifications
|
|
},
|
|
'categories': {
|
|
'total': total_categories
|
|
}
|
|
}
|
|
})
|
|
except Exception as e:
|
|
ErrorHandler.log_exception(e, "admin/api/dashboard-data")
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Fehler beim Laden der Dashboard-Daten',
|
|
'details': 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, ""
|
|
|
|
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
|
|
|
|
# Prüfen, ob es sich um eine Kategorie-ID handelt (cat_X Format)
|
|
if node_id.startswith('cat_'):
|
|
try:
|
|
category_id = int(node_id.replace('cat_', ''))
|
|
category = Category.query.get_or_404(category_id)
|
|
|
|
# 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
|
|
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 ValueError:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Ungültige Kategorie-ID',
|
|
'details': 'Kategorie-ID muss numerisch sein'
|
|
}), 400
|
|
|
|
# Normale MindMapNode-ID verarbeiten
|
|
try:
|
|
node_id_int = int(node_id)
|
|
except ValueError:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Ungültige Knoten-ID',
|
|
'details': 'Knoten-ID muss numerisch sein'
|
|
}), 400
|
|
|
|
# Knoten ohne Eager Loading laden (da children dynamic ist)
|
|
node = MindMapNode.query.get_or_404(node_id_int)
|
|
|
|
# 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': node.children.count() > 0, # Dynamic query verwenden
|
|
'icon': node.icon or 'fa-solid fa-circle'
|
|
}]
|
|
|
|
# Unterknoten hinzufügen (dynamic relationship verwenden)
|
|
children = node.children.all() # Alle Kinder laden
|
|
for child in 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': child.children.count() > 0, # Dynamic query verwenden
|
|
'icon': child.icon or 'fa-solid fa-circle'
|
|
})
|
|
|
|
# Kanten erstellen
|
|
edges = [{
|
|
'source': str(node.id),
|
|
'target': str(child.id),
|
|
'strength': 0.8
|
|
} for child in 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
|
|
|
|
# Social Feed Routes hinzufügen nach den bestehenden Routen
|
|
@app.route('/feed')
|
|
@login_required
|
|
@log_execution_time(component='SOCIAL')
|
|
def social_feed():
|
|
"""Hauptfeed-Seite mit Posts von gefolgten Benutzern"""
|
|
page = request.args.get('page', 1, type=int)
|
|
posts_per_page = 10
|
|
|
|
# Hole alle User-IDs von Benutzern, denen ich folge + meine eigene
|
|
followed_user_ids = [user.id for user in current_user.following]
|
|
all_user_ids = followed_user_ids + [current_user.id]
|
|
|
|
# Posts von diesen Benutzern direkt mit einer Abfrage holen
|
|
all_posts = SocialPost.query.filter(
|
|
SocialPost.user_id.in_(all_user_ids)
|
|
).order_by(SocialPost.created_at.desc())
|
|
|
|
posts = all_posts.paginate(
|
|
page=page, per_page=posts_per_page, error_out=False
|
|
)
|
|
|
|
return render_template('social/feed.html', posts=posts)
|
|
|
|
@app.route('/discover')
|
|
@login_required
|
|
@log_execution_time(component='SOCIAL')
|
|
def discover():
|
|
"""Entdecke neue Inhalte und Benutzer"""
|
|
# Trending Posts (basierend auf Likes und Kommentaren)
|
|
trending_posts = SocialPost.query.order_by(
|
|
(SocialPost.like_count + SocialPost.comment_count).desc()
|
|
).limit(20).all()
|
|
|
|
# Empfohlene Benutzer (die viele Follower haben und denen wir nicht folgen)
|
|
# Subquery um Benutzer zu finden, denen der aktuelle Benutzer folgt
|
|
following_subquery = db.session.query(user_follows.c.followed_id).filter(
|
|
user_follows.c.follower_id == current_user.id
|
|
).subquery()
|
|
|
|
suggested_users = User.query.filter(
|
|
User.id != current_user.id,
|
|
~User.id.in_(following_subquery)
|
|
).order_by(User.follower_count.desc()).limit(5).all()
|
|
|
|
# Community-Statistiken
|
|
from datetime import datetime, timedelta
|
|
today = datetime.utcnow().date()
|
|
|
|
active_users_count = User.query.filter(
|
|
User.last_login >= datetime.utcnow() - timedelta(days=7)
|
|
).count()
|
|
|
|
posts_today_count = SocialPost.query.filter(
|
|
db.func.date(SocialPost.created_at) == today
|
|
).count()
|
|
|
|
mindmaps_count = UserMindmap.query.count()
|
|
thoughts_count = Thought.query.count()
|
|
|
|
return render_template('social/discover.html',
|
|
trending_posts=trending_posts,
|
|
suggested_users=suggested_users,
|
|
active_users_count=active_users_count,
|
|
posts_today_count=posts_today_count,
|
|
mindmaps_count=mindmaps_count,
|
|
thoughts_count=thoughts_count)
|
|
|
|
@app.route('/profile/<username>')
|
|
@login_required
|
|
def user_profile(username):
|
|
"""Benutzerprofil anzeigen"""
|
|
user = User.query.filter_by(username=username).first_or_404()
|
|
posts = SocialPost.query.filter_by(user_id=user.id).order_by(
|
|
SocialPost.created_at.desc()
|
|
).limit(20).all()
|
|
|
|
is_following = current_user.is_following(user) if user != current_user else False
|
|
|
|
return render_template('social/profile.html',
|
|
user=user,
|
|
posts=posts,
|
|
is_following=is_following)
|
|
|
|
# API Routes für Social Features
|
|
@app.route('/api/posts', methods=['POST'])
|
|
@login_required
|
|
@handle_api_exception
|
|
def create_post():
|
|
"""Erstelle einen neuen Social Post"""
|
|
data = request.get_json()
|
|
|
|
if not data or 'content' not in data:
|
|
return ErrorHandler.api_error('Content ist erforderlich', 400)
|
|
|
|
content = data['content'].strip()
|
|
if not content:
|
|
return ErrorHandler.api_error('Content darf nicht leer sein', 400)
|
|
|
|
post = SocialPost(
|
|
content=content,
|
|
user_id=current_user.id,
|
|
post_type=data.get('post_type', 'text'),
|
|
visibility=data.get('visibility', 'public'),
|
|
image_url=data.get('image_url'),
|
|
video_url=data.get('video_url'),
|
|
link_url=data.get('link_url'),
|
|
shared_thought_id=data.get('shared_thought_id'),
|
|
shared_node_id=data.get('shared_node_id')
|
|
)
|
|
|
|
db.session.add(post)
|
|
current_user.post_count += 1
|
|
db.session.commit()
|
|
|
|
logger.info(f"Neuer Post erstellt von {current_user.username}",
|
|
component='SOCIAL', user=current_user.username)
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'post': post.to_dict(),
|
|
'message': 'Post erfolgreich erstellt'
|
|
})
|
|
|
|
@app.route('/api/posts/<int:post_id>/like', methods=['POST'])
|
|
@login_required
|
|
@handle_api_exception
|
|
def like_post(post_id):
|
|
"""Like/Unlike einen Post"""
|
|
post = SocialPost.query.get_or_404(post_id)
|
|
|
|
if current_user in post.liked_by:
|
|
# Unlike
|
|
post.liked_by.remove(current_user)
|
|
post.like_count = max(0, post.like_count - 1)
|
|
action = 'unliked'
|
|
else:
|
|
# Like
|
|
post.liked_by.append(current_user)
|
|
post.like_count += 1
|
|
action = 'liked'
|
|
|
|
# Benachrichtigung für den Autor (wenn nicht selbst)
|
|
if post.author != current_user:
|
|
notification = Notification(
|
|
user_id=post.user_id,
|
|
type='like',
|
|
message=f'{current_user.username} hat deinen Post geliked',
|
|
related_user_id=current_user.id,
|
|
related_post_id=post.id
|
|
)
|
|
db.session.add(notification)
|
|
|
|
db.session.commit()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'action': action,
|
|
'like_count': post.like_count,
|
|
'is_liked': current_user in post.liked_by
|
|
})
|
|
|
|
@app.route('/api/posts/<int:post_id>/comments', methods=['POST'])
|
|
@login_required
|
|
@handle_api_exception
|
|
def comment_on_post(post_id):
|
|
"""Kommentar zu einem Post hinzufügen"""
|
|
post = SocialPost.query.get_or_404(post_id)
|
|
data = request.get_json()
|
|
|
|
if not data or 'content' not in data:
|
|
return ErrorHandler.api_error('Content ist erforderlich', 400)
|
|
|
|
content = data['content'].strip()
|
|
if not content:
|
|
return ErrorHandler.api_error('Kommentar darf nicht leer sein', 400)
|
|
|
|
comment = SocialComment(
|
|
content=content,
|
|
user_id=current_user.id,
|
|
post_id=post.id,
|
|
parent_id=data.get('parent_id')
|
|
)
|
|
|
|
db.session.add(comment)
|
|
post.comment_count += 1
|
|
|
|
# Benachrichtigung für den Post-Autor
|
|
if post.author != current_user:
|
|
notification = Notification(
|
|
user_id=post.user_id,
|
|
type='comment',
|
|
message=f'{current_user.username} hat deinen Post kommentiert',
|
|
related_user_id=current_user.id,
|
|
related_post_id=post.id,
|
|
related_comment_id=comment.id
|
|
)
|
|
db.session.add(notification)
|
|
|
|
db.session.commit()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'comment': comment.to_dict(),
|
|
'message': 'Kommentar hinzugefügt'
|
|
})
|
|
|
|
@app.route('/api/users/<int:user_id>/follow', methods=['POST'])
|
|
@login_required
|
|
@handle_api_exception
|
|
def follow_user(user_id):
|
|
"""Einem Benutzer folgen/entfolgen"""
|
|
user = User.query.get_or_404(user_id)
|
|
|
|
if user == current_user:
|
|
return ErrorHandler.api_error('Du kannst dir nicht selbst folgen', 400)
|
|
|
|
if current_user.is_following(user):
|
|
# Entfolgen
|
|
current_user.unfollow(user)
|
|
action = 'unfollowed'
|
|
message = f'Du folgst {user.username} nicht mehr'
|
|
else:
|
|
# Folgen
|
|
current_user.follow(user)
|
|
action = 'followed'
|
|
message = f'Du folgst jetzt {user.username}'
|
|
|
|
db.session.commit()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'action': action,
|
|
'message': message,
|
|
'is_following': current_user.is_following(user),
|
|
'follower_count': user.follower_count
|
|
})
|
|
|
|
@app.route('/api/feed')
|
|
@login_required
|
|
@handle_api_exception
|
|
def get_feed_posts():
|
|
"""API für Feed-Posts"""
|
|
page = request.args.get('page', 1, type=int)
|
|
per_page = request.args.get('per_page', 10, type=int)
|
|
|
|
# Hole alle User-IDs von Benutzern, denen ich folge + meine eigene
|
|
followed_user_ids = [user.id for user in current_user.following]
|
|
all_user_ids = followed_user_ids + [current_user.id]
|
|
|
|
# Posts von diesen Benutzern direkt mit einer Abfrage holen
|
|
all_posts = SocialPost.query.filter(
|
|
SocialPost.user_id.in_(all_user_ids)
|
|
).order_by(SocialPost.created_at.desc())
|
|
|
|
posts = all_posts.paginate(
|
|
page=page, per_page=per_page, error_out=False
|
|
)
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'posts': [post.to_dict() for post in posts.items],
|
|
'has_next': posts.has_next,
|
|
'has_prev': posts.has_prev,
|
|
'page': posts.page,
|
|
'pages': posts.pages,
|
|
'total': posts.total
|
|
})
|
|
|
|
# Erweiterte Mindmap-API Routes
|
|
@app.route('/api/mindmaps/<int:mindmap_id>/collaborate', methods=['POST'])
|
|
@login_required
|
|
@handle_api_exception
|
|
def start_collaboration(mindmap_id):
|
|
"""Startet eine Kollaborationssitzung für eine Mindmap"""
|
|
mindmap = UserMindmap.query.get_or_404(mindmap_id)
|
|
|
|
# Berechtigung prüfen
|
|
if not check_mindmap_permission(mindmap_id, PermissionType.EDIT):
|
|
return ErrorHandler.api_error('Keine Berechtigung zum Bearbeiten', 403)
|
|
|
|
# Kollaborationssitzung starten (hier könnte man WebSocket-Integration hinzufügen)
|
|
return jsonify({
|
|
'success': True,
|
|
'message': 'Kollaborationssitzung gestartet',
|
|
'mindmap_id': mindmap_id,
|
|
'session_id': str(uuid_pkg.uuid4())
|
|
})
|
|
|
|
@app.route('/api/mindmaps/<int:mindmap_id>/export', methods=['GET'])
|
|
@login_required
|
|
@handle_api_exception
|
|
def export_mindmap(mindmap_id):
|
|
"""Exportiert eine Mindmap in verschiedenen Formaten"""
|
|
mindmap = UserMindmap.query.get_or_404(mindmap_id)
|
|
|
|
# Berechtigung prüfen
|
|
if not check_mindmap_permission(mindmap_id, PermissionType.READ):
|
|
return ErrorHandler.api_error('Keine Berechtigung zum Lesen', 403)
|
|
|
|
format_type = request.args.get('format', 'json')
|
|
|
|
# Mindmap-Daten zusammenstellen
|
|
mindmap_data = {
|
|
'id': mindmap.id,
|
|
'name': mindmap.name,
|
|
'description': mindmap.description,
|
|
'nodes': [],
|
|
'thoughts': [],
|
|
'notes': []
|
|
}
|
|
|
|
# Knoten hinzufügen
|
|
for node_rel in UserMindmapNode.query.filter_by(user_mindmap_id=mindmap.id).all():
|
|
node = node_rel.node
|
|
mindmap_data['nodes'].append({
|
|
'id': node.id,
|
|
'name': node.name,
|
|
'description': node.description,
|
|
'x_position': node_rel.x_position,
|
|
'y_position': node_rel.y_position,
|
|
'color_code': node.color_code
|
|
})
|
|
|
|
# Gedanken hinzufügen
|
|
for thought in mindmap.thoughts:
|
|
mindmap_data['thoughts'].append({
|
|
'id': thought.id,
|
|
'title': thought.title,
|
|
'content': thought.content,
|
|
'branch': thought.branch
|
|
})
|
|
|
|
# Notizen hinzufügen
|
|
for note in mindmap.notes:
|
|
mindmap_data['notes'].append({
|
|
'id': note.id,
|
|
'content': note.content,
|
|
'color_code': note.color_code
|
|
})
|
|
|
|
if format_type == 'json':
|
|
return jsonify({
|
|
'success': True,
|
|
'data': mindmap_data,
|
|
'format': 'json'
|
|
})
|
|
else:
|
|
return ErrorHandler.api_error('Unsupported format', 400)
|
|
|
|
@app.route('/api/posts/<int:post_id>/comments', methods=['GET'])
|
|
@login_required
|
|
@handle_api_exception
|
|
def get_post_comments(post_id):
|
|
"""Lade Kommentare für einen Post"""
|
|
post = SocialPost.query.get_or_404(post_id)
|
|
|
|
comments = SocialComment.query.filter_by(post_id=post.id).order_by(
|
|
SocialComment.created_at.asc()
|
|
).all()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'comments': [comment.to_dict() for comment in comments]
|
|
})
|
|
|
|
# Discover API Endpunkte
|
|
@app.route('/api/discover/users')
|
|
@login_required
|
|
@handle_api_exception
|
|
def discover_users():
|
|
"""API für Nutzer-Entdeckung"""
|
|
page = request.args.get('page', 1, type=int)
|
|
per_page = request.args.get('per_page', 20, type=int)
|
|
|
|
# Nutzer finden, die der aktuelle Nutzer noch nicht folgt
|
|
not_following_subquery = db.session.query(user_follows.c.followed_id).filter(
|
|
user_follows.c.follower_id == current_user.id
|
|
).subquery()
|
|
|
|
users = User.query.filter(
|
|
User.id != current_user.id,
|
|
~User.id.in_(not_following_subquery)
|
|
).order_by(User.created_at.desc()).limit(per_page).all()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'users': [{
|
|
'id': user.id,
|
|
'username': user.username,
|
|
'display_name': user.display_name,
|
|
'bio': user.bio or '',
|
|
'follower_count': user.follower_count,
|
|
'following_count': user.following_count,
|
|
'post_count': SocialPost.query.filter_by(user_id=user.id).count(),
|
|
'is_following': current_user.is_following(user),
|
|
'is_verified': False # Kann später erweitert werden
|
|
} for user in users]
|
|
})
|
|
|
|
@app.route('/api/discover/posts')
|
|
@login_required
|
|
@handle_api_exception
|
|
def discover_posts():
|
|
"""API für beliebte Posts"""
|
|
page = request.args.get('page', 1, type=int)
|
|
per_page = request.args.get('per_page', 20, type=int)
|
|
|
|
# Posts sortiert nach Likes und Kommentaren (Popularität)
|
|
posts = SocialPost.query.order_by(
|
|
(SocialPost.like_count + SocialPost.comment_count).desc(),
|
|
SocialPost.created_at.desc()
|
|
).limit(per_page).all()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'posts': [post.to_dict() for post in posts]
|
|
})
|
|
|
|
@app.route('/api/discover/trending')
|
|
@login_required
|
|
@handle_api_exception
|
|
def discover_trending():
|
|
"""API für Trending-Inhalte"""
|
|
# Beispiel-Trending-Daten (kann später mit echten Daten erweitert werden)
|
|
trending_items = [
|
|
{
|
|
'id': 1,
|
|
'title': '#KI',
|
|
'description': 'Diskussionen über Künstliche Intelligenz',
|
|
'count': 42,
|
|
'growth': 156
|
|
},
|
|
{
|
|
'id': 2,
|
|
'title': '#Wissenschaft',
|
|
'description': 'Neue wissenschaftliche Entdeckungen',
|
|
'count': 28,
|
|
'growth': 89
|
|
},
|
|
{
|
|
'id': 3,
|
|
'title': '#Innovation',
|
|
'description': 'Innovative Ideen und Technologien',
|
|
'count': 19,
|
|
'growth': 67
|
|
}
|
|
]
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'trending': trending_items
|
|
})
|
|
|
|
@app.route('/api/search/users')
|
|
@login_required
|
|
@handle_api_exception
|
|
def search_users():
|
|
"""API für Nutzer-Suche"""
|
|
query = request.args.get('q', '').strip()
|
|
|
|
if not query:
|
|
return jsonify({
|
|
'success': True,
|
|
'users': []
|
|
})
|
|
|
|
# Suche nach Nutzernamen und Anzeigenamen
|
|
users = User.query.filter(
|
|
db.or_(
|
|
User.username.ilike(f'%{query}%'),
|
|
User.display_name.ilike(f'%{query}%')
|
|
)
|
|
).filter(User.id != current_user.id).limit(10).all()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'users': [{
|
|
'id': user.id,
|
|
'username': user.username,
|
|
'display_name': user.display_name,
|
|
'bio': user.bio or '',
|
|
'follower_count': user.follower_count,
|
|
'is_following': current_user.is_following(user),
|
|
'is_verified': False
|
|
} for user in users]
|
|
})
|
|
|
|
@app.route('/api/notifications')
|
|
@login_required
|
|
@handle_api_exception
|
|
def get_notifications():
|
|
"""API für Benachrichtigungen"""
|
|
page = request.args.get('page', 1, type=int)
|
|
per_page = request.args.get('per_page', 20, type=int)
|
|
|
|
notifications = Notification.query.filter_by(
|
|
user_id=current_user.id
|
|
).order_by(Notification.created_at.desc()).paginate(
|
|
page=page, per_page=per_page, error_out=False
|
|
)
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'notifications': [notification.to_dict() for notification in notifications.items],
|
|
'has_next': notifications.has_next,
|
|
'has_prev': notifications.has_prev,
|
|
'page': notifications.page,
|
|
'pages': notifications.pages,
|
|
'total': notifications.total,
|
|
'unread_count': Notification.query.filter_by(
|
|
user_id=current_user.id, is_read=False
|
|
).count()
|
|
})
|
|
|
|
@app.route('/api/notifications/<int:notification_id>/read', methods=['POST'])
|
|
@login_required
|
|
@handle_api_exception
|
|
def mark_notification_read(notification_id):
|
|
"""Benachrichtigung als gelesen markieren"""
|
|
notification = Notification.query.filter_by(
|
|
id=notification_id, user_id=current_user.id
|
|
).first_or_404()
|
|
|
|
notification.is_read = True
|
|
db.session.commit()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': 'Benachrichtigung als gelesen markiert'
|
|
})
|
|
|
|
@app.route('/api/notifications/mark-all-read', methods=['POST'])
|
|
@login_required
|
|
@handle_api_exception
|
|
def mark_all_notifications_read():
|
|
"""Alle Benachrichtigungen als gelesen markieren"""
|
|
Notification.query.filter_by(
|
|
user_id=current_user.id, is_read=False
|
|
).update({'is_read': True})
|
|
|
|
db.session.commit()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': 'Alle Benachrichtigungen als gelesen markiert'
|
|
})
|
|
|
|
# ... existing code ...
|
|
|
|
# Flask starten
|
|
if __name__ == '__main__':
|
|
# Initialize database properly with app context
|
|
try:
|
|
success = init_app_database(app)
|
|
if success:
|
|
logger.info("Datenbank erfolgreich initialisiert", component='DB')
|
|
else:
|
|
logger.warning("Datenbankinitialisierung fehlgeschlagen. Einige Funktionen könnten eingeschränkt sein.", component='ERROR')
|
|
except Exception as e:
|
|
logger.error(f"Fehler bei der Datenbankinitialisierung: {e}", component='ERROR')
|
|
|
|
# Erstelle alle Datenbanktabellen
|
|
with app.app_context():
|
|
try:
|
|
db.create_all()
|
|
logger.info("Datenbanktabellen erstellt/aktualisiert", component='DB')
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Erstellen der Datenbanktabellen: {e}", component='ERROR')
|
|
|
|
# Starte den Flask-Entwicklungsserver
|
|
logger.info("Starte Flask-Entwicklungsserver auf http://localhost:5000", component='SYSTEM')
|
|
app.run(
|
|
debug=True,
|
|
host='0.0.0.0',
|
|
port=5000,
|
|
threaded=True
|
|
)
|