1195 lines
39 KiB
Python
Executable File
1195 lines
39 KiB
Python
Executable File
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
import os
|
|
from datetime import datetime, timedelta
|
|
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, session
|
|
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 flask_wtf import FlaskForm
|
|
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
|
|
from openai import OpenAI
|
|
from dotenv import load_dotenv
|
|
|
|
# Modelle importieren
|
|
from models import (
|
|
db, User, Thought, Comment, MindMapNode, ThoughtRelation, ThoughtRating,
|
|
RelationType, Category, UserMindmap, UserMindmapNode, MindmapNote,
|
|
node_thought_association, user_thought_bookmark
|
|
)
|
|
|
|
# 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
|
|
|
|
# OpenAI API-Konfiguration
|
|
client = OpenAI(api_key="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")
|
|
|
|
# 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
|
|
}
|
|
|
|
# Kontext-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'
|
|
|
|
# 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
|
|
|
|
@login_manager.user_loader
|
|
def load_user(id):
|
|
return User.query.get(int(id))
|
|
|
|
# Routes for authentication
|
|
@app.route('/login', methods=['GET', 'POST'])
|
|
def login():
|
|
if request.method == 'POST':
|
|
username = request.form.get('username')
|
|
password = request.form.get('password')
|
|
|
|
user = User.query.filter_by(username=username).first()
|
|
if user and user.check_password(password):
|
|
login_user(user)
|
|
# Aktualisiere letzten Login-Zeitpunkt
|
|
user.last_login = datetime.utcnow()
|
|
db.session.commit()
|
|
|
|
next_page = request.args.get('next')
|
|
return redirect(next_page or url_for('index'))
|
|
flash('Ungültiger Benutzername oder Passwort')
|
|
return render_template('login.html')
|
|
|
|
@app.route('/register', methods=['GET', 'POST'])
|
|
def register():
|
|
if request.method == 'POST':
|
|
username = request.form.get('username')
|
|
email = request.form.get('email')
|
|
password = request.form.get('password')
|
|
|
|
if User.query.filter_by(username=username).first():
|
|
flash('Benutzername existiert bereits')
|
|
return redirect(url_for('register'))
|
|
|
|
if User.query.filter_by(email=email).first():
|
|
flash('E-Mail ist bereits registriert')
|
|
return redirect(url_for('register'))
|
|
|
|
user = User(username=username, email=email)
|
|
user.set_password(password)
|
|
db.session.add(user)
|
|
|
|
# Erstelle eine Standard-Mindmap für den neuen Benutzer
|
|
default_mindmap = UserMindmap(
|
|
name='Meine Mindmap',
|
|
description='Meine persönliche Wissenslandschaft',
|
|
user=user
|
|
)
|
|
db.session.add(default_mindmap)
|
|
db.session.commit()
|
|
|
|
login_user(user)
|
|
flash('Dein Konto wurde erfolgreich erstellt!', 'success')
|
|
return redirect(url_for('index'))
|
|
return render_template('register.html')
|
|
|
|
@app.route('/logout')
|
|
@login_required
|
|
def logout():
|
|
logout_user()
|
|
return redirect(url_for('index'))
|
|
|
|
# Route for the homepage
|
|
@app.route('/')
|
|
def index():
|
|
return render_template('index.html')
|
|
|
|
# Route for the mindmap page
|
|
@app.route('/mindmap')
|
|
def mindmap():
|
|
"""Zeigt die öffentliche Mindmap an."""
|
|
# Sicherstellen, dass wir Kategorien haben
|
|
with app.app_context():
|
|
if Category.query.count() == 0:
|
|
create_default_categories()
|
|
|
|
# Hole alle Kategorien der obersten Ebene
|
|
categories = Category.query.filter_by(parent_id=None).all()
|
|
return render_template('mindmap.html', categories=categories)
|
|
|
|
# Route for user profile
|
|
@app.route('/profile')
|
|
@login_required
|
|
def profile():
|
|
# Lade Benutzer-Mindmaps
|
|
user_mindmaps = UserMindmap.query.filter_by(user_id=current_user.id).all()
|
|
# Lade Statistiken
|
|
thought_count = Thought.query.filter_by(user_id=current_user.id).count()
|
|
bookmark_count = db.session.query(func.count()).select_from(user_thought_bookmark).filter(
|
|
user_thought_bookmark.c.user_id == current_user.id
|
|
).scalar()
|
|
|
|
return render_template('profile.html',
|
|
user_mindmaps=user_mindmaps,
|
|
thought_count=thought_count,
|
|
bookmark_count=bookmark_count)
|
|
|
|
# Route für Benutzereinstellungen
|
|
@app.route('/settings', methods=['GET', 'POST'])
|
|
@login_required
|
|
def settings():
|
|
if request.method == 'POST':
|
|
action = request.form.get('action')
|
|
|
|
if action == 'update_profile':
|
|
current_user.bio = request.form.get('bio')
|
|
|
|
# Update avatar if provided
|
|
avatar_url = request.form.get('avatar_url')
|
|
if avatar_url:
|
|
current_user.avatar = avatar_url
|
|
|
|
db.session.commit()
|
|
flash('Profil erfolgreich aktualisiert!', 'success')
|
|
|
|
elif action == 'update_password':
|
|
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):
|
|
flash('Aktuelles Passwort ist nicht korrekt', 'error')
|
|
elif new_password != confirm_password:
|
|
flash('Neue Passwörter stimmen nicht überein', 'error')
|
|
else:
|
|
current_user.set_password(new_password)
|
|
db.session.commit()
|
|
flash('Passwort erfolgreich aktualisiert!', 'success')
|
|
|
|
return redirect(url_for('settings'))
|
|
|
|
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')
|
|
|
|
# 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 Mindmap-Daten
|
|
@app.route('/api/mindmap/public')
|
|
def get_public_mindmap():
|
|
"""Liefert die öffentliche Mindmap-Struktur."""
|
|
# Hole alle Kategorien der obersten Ebene
|
|
root_categories = Category.query.filter_by(parent_id=None).all()
|
|
|
|
# Baue Baumstruktur auf
|
|
result = []
|
|
for category in root_categories:
|
|
result.append(build_category_tree(category))
|
|
|
|
return jsonify(result)
|
|
|
|
def build_category_tree(category):
|
|
"""Rekursive Funktion zum Aufbau der Kategoriestruktur."""
|
|
nodes = []
|
|
# Hole alle Knoten in dieser Kategorie
|
|
for node in category.nodes:
|
|
if node.is_public:
|
|
nodes.append({
|
|
'id': node.id,
|
|
'name': node.name,
|
|
'description': node.description,
|
|
'color_code': node.color_code,
|
|
'thought_count': len(node.thoughts)
|
|
})
|
|
|
|
# Rekursiv durch Unterkaterorien
|
|
children = []
|
|
for child in category.children:
|
|
children.append(build_category_tree(child))
|
|
|
|
return {
|
|
'id': category.id,
|
|
'name': category.name,
|
|
'description': category.description,
|
|
'color_code': category.color_code,
|
|
'icon': category.icon,
|
|
'nodes': nodes,
|
|
'children': children
|
|
}
|
|
|
|
@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/<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')
|
|
x_pos = data.get('x', 0)
|
|
y_pos = data.get('y', 0)
|
|
|
|
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)
|
|
|
|
db.session.commit()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'node_id': node_id,
|
|
'x': x_pos,
|
|
'y': y_pos
|
|
})
|
|
|
|
@app.route('/api/mindmap/<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/<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.utcnow()
|
|
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():
|
|
"""API-Endpunkt zur Bereitstellung der Mindmap-Daten in hierarchischer Form."""
|
|
# Alle root-Nodes (ohne parent) abrufen
|
|
root_nodes = MindMapNode.query.filter_by(parent_id=None).all()
|
|
|
|
if not root_nodes:
|
|
# Wenn keine Nodes existieren, rufen wir initialize_database direkt auf
|
|
# anstatt create_sample_mindmap zu verwenden
|
|
with app.app_context():
|
|
initialize_database()
|
|
root_nodes = MindMapNode.query.filter_by(parent_id=None).all()
|
|
|
|
# Ergebnisse in hierarchischer Struktur zurückgeben
|
|
result = []
|
|
|
|
for node in root_nodes:
|
|
node_data = build_node_tree(node)
|
|
result.append(node_data)
|
|
|
|
return jsonify({"nodes": result})
|
|
|
|
def build_node_tree(node):
|
|
"""Erzeugt eine hierarchische Darstellung eines Knotens inkl. seiner Kindknoten."""
|
|
# Gedankenzähler abrufen von der many-to-many Beziehung
|
|
thought_count = len(node.thoughts)
|
|
|
|
# Daten für aktuellen Knoten
|
|
node_data = {
|
|
"id": node.id,
|
|
"name": node.name,
|
|
"description": f"Knoten mit {thought_count} Gedanken",
|
|
"thought_count": thought_count,
|
|
"children": []
|
|
}
|
|
|
|
# Rekursiv Kinder hinzufügen
|
|
child_nodes = MindMapNode.query.filter_by(parent_id=node.id).all()
|
|
for child_node in child_nodes:
|
|
child_data = build_node_tree(child_node)
|
|
node_data["children"].append(child_data)
|
|
|
|
return node_data
|
|
|
|
@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.utcnow()
|
|
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():
|
|
"""Liefert alle verfügbaren Kategorien."""
|
|
categories = Category.query.all()
|
|
|
|
return jsonify([{
|
|
'id': category.id,
|
|
'name': category.name,
|
|
'description': category.description,
|
|
'color_code': category.color_code,
|
|
'icon': category.icon,
|
|
'parent_id': category.parent_id
|
|
} for category in categories])
|
|
|
|
@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):
|
|
return render_template('errors/404.html'), 404
|
|
|
|
@app.errorhandler(403)
|
|
def forbidden(e):
|
|
return render_template('errors/403.html'), 403
|
|
|
|
@app.errorhandler(500)
|
|
def internal_server_error(e):
|
|
return render_template('errors/500.html'), 500
|
|
|
|
@app.errorhandler(429)
|
|
def too_many_requests(e):
|
|
return render_template('errors/429.html'), 429
|
|
|
|
# OpenAI-Integration für KI-Assistenz
|
|
@app.route('/api/assistant', methods=['POST'])
|
|
def chat_with_assistant():
|
|
"""Chatbot-API mit OpenAI Integration."""
|
|
data = request.json
|
|
|
|
# Prüfen, ob wir ein einzelnes Prompt oder ein messages-Array haben
|
|
if 'messages' in data:
|
|
messages = data.get('messages', [])
|
|
if not messages:
|
|
return jsonify({
|
|
'error': 'Keine Nachrichten vorhanden.'
|
|
}), 400
|
|
|
|
# Extrahiere Systemnachricht falls vorhanden, sonst Standard-Systemnachricht
|
|
system_message = next((msg['content'] for msg in messages if msg['role'] == 'system'),
|
|
"Du bist ein hilfreicher Assistent, der Menschen dabei hilft, "
|
|
"Wissen zu organisieren und zu verknüpfen. Liefere informative, "
|
|
"sachliche und gut strukturierte Antworten.")
|
|
|
|
# Formatiere Nachrichten für OpenAI API
|
|
api_messages = [{"role": "system", "content": system_message}]
|
|
|
|
# Füge Benutzer- und Assistenten-Nachrichten hinzu
|
|
for msg in messages:
|
|
if msg['role'] in ['user', 'assistant']:
|
|
api_messages.append({"role": msg['role'], "content": msg['content']})
|
|
else:
|
|
# Alte Implementierung für direktes Prompt
|
|
prompt = data.get('prompt', '')
|
|
context = data.get('context', '')
|
|
|
|
if not prompt:
|
|
return jsonify({
|
|
'error': 'Prompt darf nicht leer sein.'
|
|
}), 400
|
|
|
|
# Zusammenfassen mehrerer Gedanken oder Analyse anfordern
|
|
system_message = (
|
|
"Du bist ein hilfreicher Assistent, der Menschen dabei hilft, "
|
|
"Wissen zu organisieren und zu verknüpfen. Liefere informative, "
|
|
"sachliche und gut strukturierte Antworten."
|
|
)
|
|
|
|
if context:
|
|
system_message += f"\n\nKontext: {context}"
|
|
|
|
api_messages = [
|
|
{"role": "system", "content": system_message},
|
|
{"role": "user", "content": prompt}
|
|
]
|
|
|
|
try:
|
|
response = client.chat.completions.create(
|
|
model="gpt-4o-mini",
|
|
messages=api_messages,
|
|
max_tokens=300,
|
|
temperature=0.7
|
|
)
|
|
|
|
answer = response.choices[0].message.content
|
|
|
|
# Für das neue Format erwarten wir response statt answer
|
|
return jsonify({
|
|
'response': answer
|
|
})
|
|
|
|
except Exception as e:
|
|
return jsonify({
|
|
'error': f'Fehler bei der OpenAI-Anfrage: {str(e)}'
|
|
}), 500
|
|
|
|
# App-Kontext-Funktion für Initialisierung der Datenbank
|
|
def create_default_categories():
|
|
"""Erstellt die Standard-Kategorien und wissenschaftlichen Bereiche."""
|
|
categories = [
|
|
{
|
|
'name': 'Naturwissenschaften',
|
|
'description': 'Empirische Untersuchung und Erklärung natürlicher Phänomene',
|
|
'color_code': '#4CAF50',
|
|
'icon': 'flask',
|
|
'children': [
|
|
{
|
|
'name': 'Physik',
|
|
'description': 'Studium der Materie, Energie und deren Wechselwirkungen',
|
|
'color_code': '#81C784',
|
|
'icon': 'atom'
|
|
},
|
|
{
|
|
'name': 'Biologie',
|
|
'description': 'Wissenschaft des Lebens und lebender Organismen',
|
|
'color_code': '#66BB6A',
|
|
'icon': 'leaf'
|
|
},
|
|
{
|
|
'name': 'Chemie',
|
|
'description': 'Wissenschaft der Materie, ihrer Eigenschaften und Reaktionen',
|
|
'color_code': '#A5D6A7',
|
|
'icon': 'vial'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
'name': 'Sozialwissenschaften',
|
|
'description': 'Untersuchung von Gesellschaft und menschlichem Verhalten',
|
|
'color_code': '#2196F3',
|
|
'icon': 'users',
|
|
'children': [
|
|
{
|
|
'name': 'Psychologie',
|
|
'description': 'Wissenschaftliches Studium des Geistes und Verhaltens',
|
|
'color_code': '#64B5F6',
|
|
'icon': 'brain'
|
|
},
|
|
{
|
|
'name': 'Soziologie',
|
|
'description': 'Studium sozialer Beziehungen und Institutionen',
|
|
'color_code': '#42A5F5',
|
|
'icon': 'network-wired'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
'name': 'Geisteswissenschaften',
|
|
'description': 'Studium menschlicher Kultur und Kreativität',
|
|
'color_code': '#9C27B0',
|
|
'icon': 'book',
|
|
'children': [
|
|
{
|
|
'name': 'Philosophie',
|
|
'description': 'Untersuchung grundlegender Fragen über Existenz, Wissen und Ethik',
|
|
'color_code': '#BA68C8',
|
|
'icon': 'lightbulb'
|
|
},
|
|
{
|
|
'name': 'Geschichte',
|
|
'description': 'Studium der Vergangenheit und ihres Einflusses auf die Gegenwart',
|
|
'color_code': '#AB47BC',
|
|
'icon': 'landmark'
|
|
},
|
|
{
|
|
'name': 'Literatur',
|
|
'description': 'Studium literarischer Werke und ihrer Bedeutung',
|
|
'color_code': '#CE93D8',
|
|
'icon': 'feather'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
'name': 'Technologie',
|
|
'description': 'Anwendung wissenschaftlicher Erkenntnisse für praktische Zwecke',
|
|
'color_code': '#FF9800',
|
|
'icon': 'microchip',
|
|
'children': [
|
|
{
|
|
'name': 'Informatik',
|
|
'description': 'Studium von Computern und Berechnungssystemen',
|
|
'color_code': '#FFB74D',
|
|
'icon': 'laptop-code'
|
|
},
|
|
{
|
|
'name': 'Künstliche Intelligenz',
|
|
'description': 'Entwicklung intelligenter Maschinen und Software',
|
|
'color_code': '#FFA726',
|
|
'icon': 'robot'
|
|
}
|
|
]
|
|
}
|
|
]
|
|
|
|
# Kategorien in die Datenbank einfügen
|
|
for category_data in categories:
|
|
children_data = category_data.pop('children', [])
|
|
category = Category(**category_data)
|
|
db.session.add(category)
|
|
db.session.flush() # Um die ID zu generieren
|
|
|
|
# Unterkategorien hinzufügen
|
|
for child_data in children_data:
|
|
child = Category(**child_data, parent_id=category.id)
|
|
db.session.add(child)
|
|
|
|
db.session.commit()
|
|
print("Standard-Kategorien wurden erstellt!")
|
|
|
|
def initialize_database():
|
|
"""Initialisiert die Datenbank, falls sie noch nicht existiert."""
|
|
db.create_all()
|
|
|
|
# Überprüfe, ob bereits Kategorien existieren
|
|
if Category.query.count() == 0:
|
|
create_default_categories()
|
|
|
|
# Führe die Datenbankinitialisierung beim Starten der App aus
|
|
with app.app_context():
|
|
initialize_database()
|
|
|
|
@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():
|
|
"""Stellt einen Fallback für alte Netzwerk-Hintergrund-Anfragen bereit."""
|
|
return redirect(url_for('static', filename='img/backgrounds/network-bg.jpg'))
|
|
|
|
@app.route('/static/css/src/cybernetwork-bg.css')
|
|
def serve_cybernetwork_css():
|
|
"""Stellt das CSS für den cybertechnischen Netzwerk-Hintergrund bereit."""
|
|
return app.send_static_file('css/src/cybernetwork-bg.css')
|
|
|
|
@app.route('/static/js/modules/cyber-network.js')
|
|
def serve_cybernetwork_js():
|
|
"""Stellt das JavaScript-Modul für den cybertechnischen Netzwerk-Hintergrund bereit."""
|
|
return app.send_static_file('js/modules/cyber-network.js')
|
|
|
|
@app.route('/static/js/modules/cyber-network-init.js')
|
|
def serve_cybernetwork_init_js():
|
|
"""Stellt das Initialisierungs-JavaScript für den cybertechnischen Netzwerk-Hintergrund bereit."""
|
|
return app.send_static_file('js/modules/cyber-network-init.js')
|
|
|
|
@app.route('/admin/reload-env', methods=['POST'])
|
|
@admin_required
|
|
def reload_env():
|
|
"""Lädt die Umgebungsvariablen aus der .env-Datei neu."""
|
|
try:
|
|
# Erzwinge das Neuladen der .env-Datei
|
|
load_dotenv(override=True, force=True)
|
|
|
|
# OpenAI API-Key ist bereits fest kodiert
|
|
# client wurde bereits mit festem API-Key initialisiert
|
|
|
|
# Weitere Umgebungsvariablen hier aktualisieren, falls nötig
|
|
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', app.config['SECRET_KEY'])
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': 'Umgebungsvariablen wurden erfolgreich neu geladen.'
|
|
})
|
|
except Exception as e:
|
|
return jsonify({
|
|
'success': False,
|
|
'message': f'Fehler beim Neuladen der Umgebungsvariablen: {str(e)}'
|
|
}), 500
|
|
|
|
# Flask starten
|
|
if __name__ == '__main__':
|
|
with app.app_context():
|
|
# Make sure tables exist
|
|
db.create_all()
|
|
app.run(host="0.0.0.0", debug=True) |