Files
website/app.py

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
)