Compare commits
9 Commits
8440b7c30d
...
d0f32a8355
| Author | SHA1 | Date | |
|---|---|---|---|
| d0f32a8355 | |||
| 02d1801fc9 | |||
| c51a8e23ca | |||
| 1600647bc4 | |||
| 82d03f6c48 | |||
| d1352286b7 | |||
| e7b3374c53 | |||
| 4bf046c657 | |||
| 892a1212d9 |
Binary file not shown.
Binary file not shown.
855
app.py
855
app.py
@@ -2,7 +2,10 @@
|
||||
# -*- 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
|
||||
@@ -26,9 +29,10 @@ import os
|
||||
|
||||
# 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
|
||||
db, User, Thought, Comment, MindMapNode, ThoughtRelation, ThoughtRating,
|
||||
RelationType, Category, UserMindmap, UserMindmapNode, MindmapNote,
|
||||
node_thought_association, user_thought_bookmark, node_relationship,
|
||||
MindmapShare, PermissionType
|
||||
)
|
||||
|
||||
# Lade .env-Datei
|
||||
@@ -47,6 +51,87 @@ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=365) # Langlebige Session für Dark Mode-Einstellung
|
||||
app.config['UPLOAD_FOLDER'] = os.getenv('UPLOAD_FOLDER', os.path.join(os.getcwd(), 'uploads'))
|
||||
|
||||
# Logger-Konfiguration
|
||||
log_level = os.environ.get('LOG_LEVEL', 'INFO').upper()
|
||||
log_dir = os.path.join(basedir, 'logs')
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
log_file = os.path.join(log_dir, 'app.log')
|
||||
|
||||
# Handler für Datei-Logs
|
||||
file_handler = logging.FileHandler(log_file)
|
||||
file_handler.setFormatter(logging.Formatter(
|
||||
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
|
||||
))
|
||||
file_handler.setLevel(getattr(logging, log_level))
|
||||
|
||||
# Handler für Konsolen-Logs
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
|
||||
console_handler.setLevel(getattr(logging, log_level))
|
||||
|
||||
# Konfiguriere App-Logger
|
||||
app.logger.addHandler(file_handler)
|
||||
app.logger.addHandler(console_handler)
|
||||
app.logger.setLevel(getattr(logging, log_level))
|
||||
app.logger.info('Anwendung gestartet')
|
||||
|
||||
# Einheitliches Fehlerbehandlungssystem
|
||||
class ErrorHandler:
|
||||
"""Zentralisierte Fehlerbehandlung für die gesamte Anwendung"""
|
||||
|
||||
@staticmethod
|
||||
def log_exception(e, endpoint=None, code=500):
|
||||
"""Protokolliert eine Ausnahme mit Kontext-Informationen"""
|
||||
# Stack-Trace für Debugging
|
||||
trace = traceback.format_exc()
|
||||
# Benutzer-Informationen (wenn verfügbar)
|
||||
user_info = f"User: {current_user.id} ({current_user.username})" if current_user and current_user.is_authenticated else "Nicht angemeldet"
|
||||
# Request-Informationen
|
||||
request_info = f"Endpoint: {endpoint or request.path}, Method: {request.method}, IP: {request.remote_addr}"
|
||||
# Vollständige Log-Nachricht
|
||||
app.logger.error(f"Fehler {code}: {str(e)}\n{request_info}\n{user_info}\n{trace}")
|
||||
|
||||
@staticmethod
|
||||
def api_error(message, code=400, details=None):
|
||||
"""Erstellt eine standardisierte API-Fehlerantwort"""
|
||||
response = {
|
||||
'success': False,
|
||||
'error': {
|
||||
'code': code,
|
||||
'message': message
|
||||
}
|
||||
}
|
||||
if details:
|
||||
response['error']['details'] = details
|
||||
return jsonify(response), code
|
||||
|
||||
@staticmethod
|
||||
def handle_exception(e, is_api_request=False):
|
||||
"""Behandelt eine Ausnahme basierend auf ihrem Typ"""
|
||||
# Bestimme, ob es sich um eine API-Anfrage handelt
|
||||
if is_api_request is None:
|
||||
is_api_request = request.path.startswith('/api/') or request.headers.get('Accept') == 'application/json'
|
||||
|
||||
# Protokolliere die Ausnahme
|
||||
ErrorHandler.log_exception(e)
|
||||
|
||||
# Erstelle benutzerfreundliche Fehlermeldung
|
||||
user_message = "Ein unerwarteter Fehler ist aufgetreten."
|
||||
|
||||
if is_api_request:
|
||||
# API-Antwort für Ausnahme
|
||||
return ErrorHandler.api_error(user_message, 500)
|
||||
else:
|
||||
# Web-Antwort für Ausnahme (mit Flash-Nachricht)
|
||||
flash(user_message, 'error')
|
||||
return render_template('errors/500.html'), 500
|
||||
|
||||
# Globaler Exception-Handler
|
||||
@app.errorhandler(Exception)
|
||||
def handle_unhandled_exception(e):
|
||||
"""Fängt alle unbehandelten Ausnahmen ab"""
|
||||
return ErrorHandler.handle_exception(e)
|
||||
|
||||
# OpenAI API-Konfiguration
|
||||
api_key = os.environ.get("OPENAI_API_KEY", "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")
|
||||
|
||||
@@ -776,11 +861,17 @@ def api_get_user_mindmaps():
|
||||
|
||||
@app.route('/api/mindmaps/<int:mindmap_id>', methods=['GET'])
|
||||
@login_required
|
||||
@handle_api_exception
|
||||
def api_get_user_mindmap_detail(mindmap_id):
|
||||
mindmap = UserMindmap.query.filter_by(id=mindmap_id, user_id=current_user.id).first_or_404()
|
||||
# Bestehende Logik von get_user_mindmap kann hier wiederverwendet oder angepasst werden
|
||||
# Für eine einfache Detailansicht:
|
||||
return jsonify({
|
||||
# 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,
|
||||
@@ -788,18 +879,44 @@ def api_get_user_mindmap_detail(mindmap_id):
|
||||
'user_id': mindmap.user_id,
|
||||
'created_at': mindmap.created_at.isoformat(),
|
||||
'last_modified': mindmap.last_modified.isoformat(),
|
||||
# Hier könnten auch Knoten und Notizen hinzugefügt werden, ähnlich wie in get_user_mindmap
|
||||
})
|
||||
'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):
|
||||
mindmap = UserMindmap.query.filter_by(id=mindmap_id, user_id=current_user.id).first_or_404()
|
||||
# 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)
|
||||
mindmap.is_private = data.get('is_private', mindmap.is_private)
|
||||
|
||||
# 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({
|
||||
@@ -807,16 +924,31 @@ def api_update_user_mindmap(mindmap_id):
|
||||
'name': mindmap.name,
|
||||
'description': mindmap.description,
|
||||
'is_private': mindmap.is_private,
|
||||
'last_modified': mindmap.last_modified.isoformat()
|
||||
'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):
|
||||
mindmap = UserMindmap.query.filter_by(id=mindmap_id, user_id=current_user.id).first_or_404()
|
||||
# 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({'message': 'Mindmap erfolgreich gelöscht'}), 200
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Mindmap erfolgreich gelöscht'
|
||||
}), 200
|
||||
|
||||
# API-Endpunkte für Mindmap-Daten (öffentlich und benutzerspezifisch)
|
||||
@app.route('/api/mindmap/public')
|
||||
@@ -1627,20 +1759,78 @@ def get_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'))
|
||||
|
||||
# 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
|
||||
|
||||
# OpenAI-Integration für KI-Assistenz
|
||||
@app.route('/api/assistant', methods=['POST'])
|
||||
def chat_with_assistant():
|
||||
@@ -1883,6 +2073,255 @@ def reload_env():
|
||||
'message': f'Fehler beim Neuladen der Umgebungsvariablen: {str(e)}'
|
||||
}), 500
|
||||
|
||||
# Berechtigungsverwaltung für Mindmaps
|
||||
@app.route('/api/mindmap/<int:mindmap_id>/shares', methods=['GET'])
|
||||
@login_required
|
||||
@handle_api_exception
|
||||
def get_mindmap_shares(mindmap_id):
|
||||
"""Listet alle Benutzer auf, mit denen eine Mindmap geteilt wurde."""
|
||||
# Überprüfen, ob die Mindmap dem Benutzer gehört
|
||||
mindmap = UserMindmap.query.get_or_404(mindmap_id)
|
||||
if mindmap.user_id != current_user.id:
|
||||
return ErrorHandler.api_error("Sie haben keine Berechtigung, die Freigaben dieser Mindmap einzusehen.", 403)
|
||||
|
||||
# Alle Freigaben für diese Mindmap abrufen
|
||||
shares = MindmapShare.query.filter_by(mindmap_id=mindmap_id).all()
|
||||
|
||||
result = []
|
||||
for share in shares:
|
||||
# Benutzerinformationen abrufen
|
||||
shared_with_user = User.query.get(share.shared_with_id)
|
||||
|
||||
if shared_with_user:
|
||||
result.append({
|
||||
'id': share.id,
|
||||
'user_id': shared_with_user.id,
|
||||
'username': shared_with_user.username,
|
||||
'email': shared_with_user.email,
|
||||
'permission': share.permission_type.name,
|
||||
'created_at': share.created_at.isoformat(),
|
||||
'last_accessed': share.last_accessed.isoformat() if share.last_accessed else None
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'shares': result
|
||||
})
|
||||
|
||||
@app.route('/api/mindmap/<int:mindmap_id>/share', methods=['POST'])
|
||||
@login_required
|
||||
@handle_api_exception
|
||||
def share_mindmap(mindmap_id):
|
||||
"""Teilt eine Mindmap mit einem anderen Benutzer."""
|
||||
# Überprüfen, ob die Mindmap dem Benutzer gehört
|
||||
mindmap = UserMindmap.query.get_or_404(mindmap_id)
|
||||
if mindmap.user_id != current_user.id:
|
||||
return ErrorHandler.api_error("Sie haben keine Berechtigung, diese Mindmap zu teilen.", 403)
|
||||
|
||||
data = request.json
|
||||
if not data or 'email' not in data or 'permission' not in data:
|
||||
return ErrorHandler.api_error("E-Mail-Adresse und Berechtigungstyp sind erforderlich.", 400)
|
||||
|
||||
# Benutzer anhand der E-Mail-Adresse finden
|
||||
user_to_share_with = User.query.filter_by(email=data['email']).first()
|
||||
if not user_to_share_with:
|
||||
return ErrorHandler.api_error("Kein Benutzer mit dieser E-Mail-Adresse gefunden.", 404)
|
||||
|
||||
# Prüfen, ob der Benutzer versucht, mit sich selbst zu teilen
|
||||
if user_to_share_with.id == current_user.id:
|
||||
return ErrorHandler.api_error("Sie können die Mindmap nicht mit sich selbst teilen.", 400)
|
||||
|
||||
# Prüfen, ob die Mindmap bereits mit diesem Benutzer geteilt wurde
|
||||
existing_share = MindmapShare.query.filter_by(
|
||||
mindmap_id=mindmap_id,
|
||||
shared_with_id=user_to_share_with.id
|
||||
).first()
|
||||
|
||||
# Berechtigungstyp validieren und konvertieren
|
||||
try:
|
||||
permission_type = PermissionType[data['permission']]
|
||||
except (KeyError, ValueError):
|
||||
return ErrorHandler.api_error(
|
||||
"Ungültiger Berechtigungstyp. Erlaubte Werte sind: READ, EDIT, ADMIN.",
|
||||
400
|
||||
)
|
||||
|
||||
if existing_share:
|
||||
# Wenn bereits geteilt, aktualisiere die Berechtigungen
|
||||
existing_share.permission_type = permission_type
|
||||
message = "Berechtigungen erfolgreich aktualisiert."
|
||||
else:
|
||||
# Wenn noch nicht geteilt, erstelle eine neue Freigabe
|
||||
new_share = MindmapShare(
|
||||
mindmap_id=mindmap_id,
|
||||
shared_by_id=current_user.id,
|
||||
shared_with_id=user_to_share_with.id,
|
||||
permission_type=permission_type
|
||||
)
|
||||
db.session.add(new_share)
|
||||
message = "Mindmap erfolgreich geteilt."
|
||||
|
||||
# Wenn die Mindmap bisher privat war, mache sie jetzt nicht mehr privat
|
||||
if mindmap.is_private:
|
||||
mindmap.is_private = False
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': message
|
||||
})
|
||||
|
||||
@app.route('/api/mindmap/shares/<int:share_id>', methods=['PUT'])
|
||||
@login_required
|
||||
@handle_api_exception
|
||||
def update_mindmap_share(share_id):
|
||||
"""Aktualisiert die Berechtigungen für eine geteilte Mindmap."""
|
||||
# Freigabe finden
|
||||
share = MindmapShare.query.get_or_404(share_id)
|
||||
|
||||
# Prüfen, ob der Benutzer der Eigentümer der Mindmap ist
|
||||
mindmap = UserMindmap.query.get(share.mindmap_id)
|
||||
if not mindmap or mindmap.user_id != current_user.id:
|
||||
return ErrorHandler.api_error("Sie haben keine Berechtigung, diese Freigabe zu ändern.", 403)
|
||||
|
||||
data = request.json
|
||||
if not data or 'permission' not in data:
|
||||
return ErrorHandler.api_error("Berechtigungstyp ist erforderlich.", 400)
|
||||
|
||||
# Berechtigungstyp validieren und konvertieren
|
||||
try:
|
||||
permission_type = PermissionType[data['permission']]
|
||||
except (KeyError, ValueError):
|
||||
return ErrorHandler.api_error(
|
||||
"Ungültiger Berechtigungstyp. Erlaubte Werte sind: READ, EDIT, ADMIN.",
|
||||
400
|
||||
)
|
||||
|
||||
# Berechtigungen aktualisieren
|
||||
share.permission_type = permission_type
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': "Berechtigungen erfolgreich aktualisiert."
|
||||
})
|
||||
|
||||
@app.route('/api/mindmap/shares/<int:share_id>', methods=['DELETE'])
|
||||
@login_required
|
||||
@handle_api_exception
|
||||
def revoke_mindmap_share(share_id):
|
||||
"""Widerruft die Freigabe einer Mindmap für einen Benutzer."""
|
||||
# Freigabe finden
|
||||
share = MindmapShare.query.get_or_404(share_id)
|
||||
|
||||
# Prüfen, ob der Benutzer der Eigentümer der Mindmap ist
|
||||
mindmap = UserMindmap.query.get(share.mindmap_id)
|
||||
if not mindmap or mindmap.user_id != current_user.id:
|
||||
return ErrorHandler.api_error("Sie haben keine Berechtigung, diese Freigabe zu widerrufen.", 403)
|
||||
|
||||
# Freigabe löschen
|
||||
db.session.delete(share)
|
||||
|
||||
# Prüfen, ob dies die letzte Freigabe war und ggf. Mindmap wieder privat setzen
|
||||
remaining_shares = MindmapShare.query.filter_by(mindmap_id=mindmap.id).count()
|
||||
if remaining_shares == 0:
|
||||
mindmap.is_private = True
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': "Freigabe erfolgreich widerrufen."
|
||||
})
|
||||
|
||||
@app.route('/api/mindmaps/shared-with-me', methods=['GET'])
|
||||
@login_required
|
||||
@handle_api_exception
|
||||
def get_shared_mindmaps():
|
||||
"""Listet alle Mindmaps auf, die mit dem aktuellen Benutzer geteilt wurden."""
|
||||
shares = MindmapShare.query.filter_by(shared_with_id=current_user.id).all()
|
||||
|
||||
result = []
|
||||
for share in shares:
|
||||
mindmap = UserMindmap.query.get(share.mindmap_id)
|
||||
owner = User.query.get(mindmap.user_id)
|
||||
|
||||
if mindmap and owner:
|
||||
# Zugriffsdatum aktualisieren
|
||||
share.last_accessed = datetime.now(timezone.utc)
|
||||
|
||||
result.append({
|
||||
'id': mindmap.id,
|
||||
'name': mindmap.name,
|
||||
'description': mindmap.description,
|
||||
'owner': {
|
||||
'id': owner.id,
|
||||
'username': owner.username
|
||||
},
|
||||
'created_at': mindmap.created_at.isoformat(),
|
||||
'last_modified': mindmap.last_modified.isoformat(),
|
||||
'permission': share.permission_type.name
|
||||
})
|
||||
|
||||
db.session.commit() # Speichere die aktualisierten Zugriffsdaten
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'mindmaps': result
|
||||
})
|
||||
|
||||
# Hilfsfunktion zur Überprüfung der Berechtigungen
|
||||
def check_mindmap_permission(mindmap_id, permission_type=None):
|
||||
"""
|
||||
Überprüft, ob der aktuelle Benutzer Zugriff auf eine Mindmap hat.
|
||||
|
||||
Args:
|
||||
mindmap_id: ID der Mindmap
|
||||
permission_type: Mindestberechtigung, die erforderlich ist (READ, EDIT, ADMIN)
|
||||
Wenn None, wird nur der Zugriff überprüft.
|
||||
|
||||
Returns:
|
||||
(bool, str): Tupel aus (hat_berechtigung, fehlermeldung)
|
||||
"""
|
||||
mindmap = UserMindmap.query.get(mindmap_id)
|
||||
if not mindmap:
|
||||
return False, "Mindmap nicht gefunden."
|
||||
|
||||
# Wenn der Benutzer der Eigentümer ist, hat er vollen Zugriff
|
||||
if mindmap.user_id == current_user.id:
|
||||
return True, ""
|
||||
|
||||
# Wenn die Mindmap privat ist und der Benutzer nicht der Eigentümer ist
|
||||
if mindmap.is_private:
|
||||
share = MindmapShare.query.filter_by(
|
||||
mindmap_id=mindmap_id,
|
||||
shared_with_id=current_user.id
|
||||
).first()
|
||||
|
||||
if not share:
|
||||
return False, "Sie haben keinen Zugriff auf diese Mindmap."
|
||||
|
||||
# Wenn eine bestimmte Berechtigungsstufe erforderlich ist
|
||||
if permission_type:
|
||||
required_level = PermissionType[permission_type].value
|
||||
user_level = share.permission_type.value
|
||||
|
||||
if required_level not in [p.value for p in PermissionType]:
|
||||
return False, f"Ungültiger Berechtigungstyp: {permission_type}"
|
||||
|
||||
# Berechtigungshierarchie prüfen
|
||||
permission_hierarchy = {
|
||||
PermissionType.READ.name: 1,
|
||||
PermissionType.EDIT.name: 2,
|
||||
PermissionType.ADMIN.name: 3
|
||||
}
|
||||
|
||||
if permission_hierarchy[share.permission_type.name] < permission_hierarchy[permission_type]:
|
||||
return False, f"Sie benötigen {permission_type}-Berechtigungen für diese Aktion."
|
||||
|
||||
return True, ""
|
||||
|
||||
# Flask starten
|
||||
if __name__ == '__main__':
|
||||
with app.app_context():
|
||||
@@ -2298,6 +2737,394 @@ def get_public_mindmap_nodes():
|
||||
print(f"Fehler in get_public_mindmap_nodes: {str(e)}")
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
# Suchfunktion für Mindmap-Knoten
|
||||
@app.route('/api/search/mindmap', methods=['GET'])
|
||||
@handle_api_exception
|
||||
def search_mindmap_nodes():
|
||||
"""
|
||||
Durchsucht Mindmap-Knoten nach einem Suchbegriff.
|
||||
|
||||
Query-Parameter:
|
||||
- q: Suchbegriff
|
||||
- user_only: Wenn "true", werden nur Knoten aus den Mindmaps des Benutzers durchsucht
|
||||
- include_public: Wenn "true", werden auch öffentliche Knoten einbezogen (Standard: true)
|
||||
"""
|
||||
try:
|
||||
# Parameter auslesen
|
||||
query = request.args.get('q', '').strip()
|
||||
user_only = request.args.get('user_only', 'false').lower() == 'true'
|
||||
include_public = request.args.get('include_public', 'true').lower() == 'true'
|
||||
|
||||
if not query:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': 'Kein Suchbegriff angegeben',
|
||||
'results': []
|
||||
}), 400
|
||||
|
||||
# Basisfunktion für die Suche
|
||||
results = []
|
||||
|
||||
# Erstelle Such-Pattern (beide Suchbegriffe werden verwendet)
|
||||
search_pattern = f"%{query}%"
|
||||
|
||||
# 1. Knoten aus Benutzer-Mindmaps suchen, falls Benutzer angemeldet ist
|
||||
if current_user.is_authenticated:
|
||||
# Suche in allen Knoten, die in den Mindmaps des Benutzers sind
|
||||
user_nodes_query = db.session.query(
|
||||
MindMapNode, UserMindmapNode
|
||||
).join(
|
||||
UserMindmapNode, UserMindmapNode.node_id == MindMapNode.id
|
||||
).join(
|
||||
UserMindmap, UserMindmapNode.user_mindmap_id == UserMindmap.id
|
||||
).filter(
|
||||
UserMindmap.user_id == current_user.id,
|
||||
db.or_(
|
||||
MindMapNode.name.ilike(search_pattern),
|
||||
MindMapNode.description.ilike(search_pattern)
|
||||
)
|
||||
).all()
|
||||
|
||||
# Formatiere die Ergebnisse
|
||||
for node, user_node in user_nodes_query:
|
||||
# Hole den Mindmap-Namen für diesen Knoten
|
||||
mindmap = UserMindmap.query.get(user_node.user_mindmap_id)
|
||||
|
||||
results.append({
|
||||
'id': node.id,
|
||||
'name': node.name,
|
||||
'description': node.description or '',
|
||||
'color_code': node.color_code or '#9F7AEA',
|
||||
'source': 'user_mindmap',
|
||||
'mindmap_id': user_node.user_mindmap_id,
|
||||
'mindmap_name': mindmap.name if mindmap else 'Unbekannte Mindmap',
|
||||
'position': {
|
||||
'x': user_node.x_position,
|
||||
'y': user_node.y_position
|
||||
}
|
||||
})
|
||||
|
||||
# 2. Öffentliche Knoten suchen, falls gewünscht und nicht nur Benutzer-Mindmaps
|
||||
if include_public and not user_only:
|
||||
public_nodes_query = MindMapNode.query.filter(
|
||||
MindMapNode.is_public == True,
|
||||
db.or_(
|
||||
MindMapNode.name.ilike(search_pattern),
|
||||
MindMapNode.description.ilike(search_pattern)
|
||||
)
|
||||
).all()
|
||||
|
||||
# Prüfen, ob die öffentlichen Knoten bereits in den Ergebnissen sind
|
||||
existing_node_ids = [node['id'] for node in results]
|
||||
|
||||
for node in public_nodes_query:
|
||||
if node.id not in existing_node_ids:
|
||||
results.append({
|
||||
'id': node.id,
|
||||
'name': node.name,
|
||||
'description': node.description or '',
|
||||
'color_code': node.color_code or '#9F7AEA',
|
||||
'source': 'public',
|
||||
'mindmap_id': None,
|
||||
'mindmap_name': 'Öffentliche Mindmap'
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'{len(results)} Ergebnisse gefunden',
|
||||
'results': results
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"Fehler bei der Mindmap-Suche: {str(e)}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'Fehler bei der Suche: {str(e)}',
|
||||
'results': []
|
||||
}), 500
|
||||
|
||||
# Export/Import-Funktionen für Mindmaps
|
||||
@app.route('/api/mindmap/<int:mindmap_id>/export', methods=['GET'])
|
||||
@login_required
|
||||
@handle_api_exception
|
||||
def export_mindmap(mindmap_id):
|
||||
"""
|
||||
Exportiert eine Mindmap im angegebenen Format.
|
||||
|
||||
Query-Parameter:
|
||||
- format: Format der Exportdatei (json, xml, csv)
|
||||
"""
|
||||
try:
|
||||
# Sicherheitscheck: Nur eigene Mindmaps oder Mindmaps, auf die der Benutzer Zugriff hat
|
||||
mindmap = UserMindmap.query.get_or_404(mindmap_id)
|
||||
|
||||
# Prüfen, ob der Benutzer Zugriff auf diese Mindmap hat
|
||||
can_access = mindmap.user_id == current_user.id
|
||||
|
||||
if not can_access and mindmap.is_private:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': 'Keine Berechtigung für den Zugriff auf diese Mindmap'
|
||||
}), 403
|
||||
|
||||
# Format aus Query-Parameter holen
|
||||
export_format = request.args.get('format', 'json')
|
||||
|
||||
# Alle Knoten und ihre Positionen in dieser Mindmap holen
|
||||
nodes_data = db.session.query(
|
||||
MindMapNode, UserMindmapNode
|
||||
).join(
|
||||
UserMindmapNode, UserMindmapNode.node_id == MindMapNode.id
|
||||
).filter(
|
||||
UserMindmapNode.user_mindmap_id == mindmap_id
|
||||
).all()
|
||||
|
||||
# Beziehungen zwischen Knoten holen
|
||||
relationships = []
|
||||
for node1, user_node1 in nodes_data:
|
||||
for node2, user_node2 in nodes_data:
|
||||
if node1.id != node2.id and node2 in node1.children:
|
||||
relationships.append({
|
||||
'source': node1.id,
|
||||
'target': node2.id
|
||||
})
|
||||
|
||||
# Exportdaten vorbereiten
|
||||
export_data = {
|
||||
'mindmap': {
|
||||
'id': mindmap.id,
|
||||
'name': mindmap.name,
|
||||
'description': mindmap.description,
|
||||
'created_at': mindmap.created_at.isoformat(),
|
||||
'last_modified': mindmap.last_modified.isoformat()
|
||||
},
|
||||
'nodes': [{
|
||||
'id': node.id,
|
||||
'name': node.name,
|
||||
'description': node.description or '',
|
||||
'color_code': node.color_code or '#9F7AEA',
|
||||
'x_position': user_node.x_position,
|
||||
'y_position': user_node.y_position,
|
||||
'scale': user_node.scale or 1.0
|
||||
} for node, user_node in nodes_data],
|
||||
'relationships': relationships
|
||||
}
|
||||
|
||||
# Exportieren im angeforderten Format
|
||||
if export_format == 'json':
|
||||
response = app.response_class(
|
||||
response=json.dumps(export_data, indent=2),
|
||||
status=200,
|
||||
mimetype='application/json'
|
||||
)
|
||||
response.headers["Content-Disposition"] = f"attachment; filename=mindmap_{mindmap_id}.json"
|
||||
return response
|
||||
|
||||
elif export_format == 'xml':
|
||||
import dicttoxml
|
||||
xml_data = dicttoxml.dicttoxml(export_data)
|
||||
response = app.response_class(
|
||||
response=xml_data,
|
||||
status=200,
|
||||
mimetype='application/xml'
|
||||
)
|
||||
response.headers["Content-Disposition"] = f"attachment; filename=mindmap_{mindmap_id}.xml"
|
||||
return response
|
||||
|
||||
elif export_format == 'csv':
|
||||
import io
|
||||
import csv
|
||||
|
||||
# CSV kann nicht die gesamte Struktur darstellen, daher nur die Knotenliste
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
|
||||
# Schreibe Header
|
||||
writer.writerow(['id', 'name', 'description', 'color_code', 'x_position', 'y_position', 'scale'])
|
||||
|
||||
# Schreibe Knotendaten
|
||||
for node, user_node in nodes_data:
|
||||
writer.writerow([
|
||||
node.id,
|
||||
node.name,
|
||||
node.description or '',
|
||||
node.color_code or '#9F7AEA',
|
||||
user_node.x_position,
|
||||
user_node.y_position,
|
||||
user_node.scale or 1.0
|
||||
])
|
||||
|
||||
output.seek(0)
|
||||
response = app.response_class(
|
||||
response=output.getvalue(),
|
||||
status=200,
|
||||
mimetype='text/csv'
|
||||
)
|
||||
response.headers["Content-Disposition"] = f"attachment; filename=mindmap_{mindmap_id}_nodes.csv"
|
||||
return response
|
||||
|
||||
else:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'Nicht unterstütztes Format: {export_format}'
|
||||
}), 400
|
||||
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Exportieren der Mindmap: {str(e)}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'Fehler beim Exportieren: {str(e)}'
|
||||
}), 500
|
||||
|
||||
@app.route('/api/mindmap/<int:mindmap_id>/import', methods=['POST'])
|
||||
@login_required
|
||||
@handle_api_exception
|
||||
def import_mindmap(mindmap_id):
|
||||
"""
|
||||
Importiert Daten in eine bestehende Mindmap.
|
||||
|
||||
Die Daten können in verschiedenen Formaten (JSON, XML, CSV) hochgeladen werden.
|
||||
"""
|
||||
try:
|
||||
# Sicherheitscheck: Nur eigene Mindmaps können bearbeitet werden
|
||||
mindmap = UserMindmap.query.get_or_404(mindmap_id)
|
||||
|
||||
if mindmap.user_id != current_user.id:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': 'Keine Berechtigung zum Bearbeiten dieser Mindmap'
|
||||
}), 403
|
||||
|
||||
# Prüfen, ob eine Datei hochgeladen wurde
|
||||
if 'file' not in request.files:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': 'Keine Datei ausgewählt'
|
||||
}), 400
|
||||
|
||||
file = request.files['file']
|
||||
|
||||
if file.filename == '':
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': 'Keine Datei ausgewählt'
|
||||
}), 400
|
||||
|
||||
# Format anhand der Dateiendung erkennen
|
||||
file_ext = file.filename.rsplit('.', 1)[1].lower() if '.' in file.filename else None
|
||||
|
||||
if file_ext not in ['json', 'xml', 'csv']:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'Nicht unterstütztes Dateiformat: {file_ext}'
|
||||
}), 400
|
||||
|
||||
# Datei einlesen
|
||||
import_data = None
|
||||
|
||||
if file_ext == 'json':
|
||||
import_data = json.loads(file.read().decode('utf-8'))
|
||||
elif file_ext == 'xml':
|
||||
import xml.etree.ElementTree as ET
|
||||
import xmltodict
|
||||
xml_data = file.read().decode('utf-8')
|
||||
import_data = xmltodict.parse(xml_data)
|
||||
elif file_ext == 'csv':
|
||||
import io
|
||||
import csv
|
||||
csv_data = file.read().decode('utf-8')
|
||||
reader = csv.DictReader(io.StringIO(csv_data))
|
||||
nodes = []
|
||||
for row in reader:
|
||||
nodes.append(row)
|
||||
import_data = {'nodes': nodes}
|
||||
|
||||
# Daten in die Mindmap importieren
|
||||
if 'nodes' in import_data:
|
||||
# Bestehende Knoten in der Mindmap für Referenz
|
||||
existing_nodes = db.session.query(
|
||||
UserMindmapNode.node_id
|
||||
).filter_by(
|
||||
user_mindmap_id=mindmap_id
|
||||
).all()
|
||||
existing_node_ids = [n[0] for n in existing_nodes]
|
||||
|
||||
# Mapping von alten zu neuen Knoten-IDs für importierte Knoten
|
||||
id_mapping = {}
|
||||
|
||||
# Knoten importieren
|
||||
for node_data in import_data.get('nodes', []):
|
||||
# Prüfen, ob es sich um Stringkeys (aus CSV) oder Dict (aus JSON) handelt
|
||||
if isinstance(node_data, dict):
|
||||
node_name = node_data.get('name')
|
||||
node_desc = node_data.get('description', '')
|
||||
node_color = node_data.get('color_code', '#9F7AEA')
|
||||
x_pos = float(node_data.get('x_position', 0))
|
||||
y_pos = float(node_data.get('y_position', 0))
|
||||
node_scale = float(node_data.get('scale', 1.0))
|
||||
old_id = node_data.get('id')
|
||||
else:
|
||||
# Fallback für andere Formate
|
||||
continue
|
||||
|
||||
# Neuen Knoten erstellen, wenn nötig
|
||||
new_node = MindMapNode(
|
||||
name=node_name,
|
||||
description=node_desc,
|
||||
color_code=node_color
|
||||
)
|
||||
db.session.add(new_node)
|
||||
db.session.flush() # ID generieren
|
||||
|
||||
# Verknüpfung zur Mindmap erstellen
|
||||
user_node = UserMindmapNode(
|
||||
user_mindmap_id=mindmap_id,
|
||||
node_id=new_node.id,
|
||||
x_position=x_pos,
|
||||
y_position=y_pos,
|
||||
scale=node_scale
|
||||
)
|
||||
db.session.add(user_node)
|
||||
|
||||
# ID-Mapping für Beziehungen speichern
|
||||
if old_id:
|
||||
id_mapping[old_id] = new_node.id
|
||||
|
||||
# Beziehungen zwischen Knoten importieren
|
||||
for rel in import_data.get('relationships', []):
|
||||
source_id = rel.get('source')
|
||||
target_id = rel.get('target')
|
||||
|
||||
if source_id in id_mapping and target_id in id_mapping:
|
||||
# Knoten-Objekte holen
|
||||
source_node = MindMapNode.query.get(id_mapping[source_id])
|
||||
target_node = MindMapNode.query.get(id_mapping[target_id])
|
||||
|
||||
if source_node and target_node:
|
||||
# Beziehung erstellen
|
||||
source_node.children.append(target_node)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'{len(import_data.get("nodes", []))} Knoten erfolgreich importiert'
|
||||
})
|
||||
|
||||
else:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': 'Keine Knotendaten in der Importdatei gefunden'
|
||||
}), 400
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
print(f"Fehler beim Importieren der Mindmap: {str(e)}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'Fehler beim Importieren: {str(e)}'
|
||||
}), 500
|
||||
|
||||
# Automatische Datenbankinitialisierung - Aktualisiert für Flask 2.2+ Kompatibilität
|
||||
def initialize_app():
|
||||
"""Initialisierung der Anwendung"""
|
||||
|
||||
Binary file not shown.
27
logs/app.log
Normal file
27
logs/app.log
Normal file
@@ -0,0 +1,27 @@
|
||||
2025-05-10 23:12:44,110 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:74]
|
||||
2025-05-10 23:12:45,854 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:74]
|
||||
2025-05-10 23:12:45,854 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:74]
|
||||
2025-05-10 23:13:27,379 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:74]
|
||||
2025-05-10 23:13:29,289 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:74]
|
||||
2025-05-10 23:13:29,289 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:74]
|
||||
2025-05-10 23:13:35,686 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:75]
|
||||
2025-05-10 23:13:37,640 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:75]
|
||||
2025-05-10 23:13:37,640 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:75]
|
||||
2025-05-10 23:14:35,907 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:75]
|
||||
2025-05-10 23:14:37,804 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:75]
|
||||
2025-05-10 23:14:37,804 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:75]
|
||||
2025-05-10 23:14:44,251 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:75]
|
||||
2025-05-10 23:14:46,088 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:75]
|
||||
2025-05-10 23:14:46,088 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:75]
|
||||
2025-05-10 23:15:14,106 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:75]
|
||||
2025-05-10 23:15:15,855 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:75]
|
||||
2025-05-10 23:15:15,855 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:75]
|
||||
2025-05-10 23:15:30,739 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||
2025-05-10 23:15:32,667 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||
2025-05-10 23:15:32,667 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||
2025-05-10 23:16:55,581 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||
2025-05-10 23:16:57,283 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||
2025-05-10 23:16:57,283 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||
2025-05-10 23:17:04,727 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||
2025-05-10 23:17:06,698 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||
2025-05-10 23:17:06,698 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:76]
|
||||
31
models.py
31
models.py
@@ -359,4 +359,33 @@ class ForumPost(db.Model):
|
||||
replies = db.relationship('ForumPost', backref=db.backref('parent', remote_side=[id]), lazy=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<ForumPost {self.title}>'
|
||||
return f'<ForumPost {self.title}>'
|
||||
|
||||
# Berechtigungstypen für Mindmap-Freigaben
|
||||
class PermissionType(Enum):
|
||||
READ = "Nur-Lesen"
|
||||
EDIT = "Bearbeiten"
|
||||
ADMIN = "Administrator"
|
||||
|
||||
# Freigabemodell für Mindmaps
|
||||
class MindmapShare(db.Model):
|
||||
"""Speichert Informationen über freigegebene Mindmaps und Berechtigungen"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
mindmap_id = db.Column(db.Integer, db.ForeignKey('user_mindmap.id'), nullable=False)
|
||||
shared_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
shared_with_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
permission_type = db.Column(db.Enum(PermissionType), nullable=False, default=PermissionType.READ)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
last_accessed = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
# Beziehungen
|
||||
mindmap = db.relationship('UserMindmap', backref=db.backref('shares', lazy='dynamic'))
|
||||
shared_by = db.relationship('User', foreign_keys=[shared_by_id], backref=db.backref('shared_mindmaps', lazy='dynamic'))
|
||||
shared_with = db.relationship('User', foreign_keys=[shared_with_id], backref=db.backref('accessible_mindmaps', lazy='dynamic'))
|
||||
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint('mindmap_id', 'shared_with_id', name='unique_mindmap_share'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<MindmapShare: {self.mindmap_id} - {self.shared_with_id} - {self.permission_type.name}>'
|
||||
38
templates/errors/400.html
Normal file
38
templates/errors/400.html
Normal file
@@ -0,0 +1,38 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}400 - Ungültige Anfrage{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="min-h-[75vh] flex flex-col items-center justify-center px-4 py-12 bg-gradient-to-b from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
|
||||
<div class="glass-effect max-w-2xl w-full p-6 md:p-10 rounded-xl border border-gray-300/20 dark:border-gray-700/30 shadow-xl transform transition-all duration-300 hover:shadow-2xl">
|
||||
<div class="text-center">
|
||||
<div class="flex justify-center mb-6">
|
||||
<div class="relative">
|
||||
<h1 class="text-7xl md:text-8xl font-extrabold text-primary-600 dark:text-primary-400 opacity-90">400</h1>
|
||||
<div class="absolute -top-4 -right-4 w-12 h-12 bg-red-500 rounded-full flex items-center justify-center animate-pulse">
|
||||
<i class="fa-solid fa-exclamation text-white text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="text-2xl md:text-3xl font-semibold mb-4 text-gray-800 dark:text-gray-200">Ungültige Anfrage</h2>
|
||||
<p class="text-gray-600 dark:text-gray-300 mb-8 max-w-lg mx-auto text-base md:text-lg">Die Anfrage konnte nicht verarbeitet werden. Bitte überprüfen Sie Ihre Eingaben und versuchen Sie es erneut.</p>
|
||||
{% if error %}
|
||||
<div class="mb-6 p-4 bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-200 rounded-lg text-sm">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<a href="{{ url_for('index') }}" class="btn-primary transform transition-transform duration-300 hover:scale-105 px-6 py-3 rounded-lg">
|
||||
<i class="fa-solid fa-home mr-2"></i>Zur Startseite
|
||||
</a>
|
||||
<a href="javascript:history.back()" class="btn-secondary transform transition-transform duration-300 hover:scale-105 px-6 py-3 rounded-lg">
|
||||
<i class="fa-solid fa-arrow-left mr-2"></i>Zurück
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-8 text-sm text-gray-500 dark:text-gray-400">
|
||||
<p>Benötigen Sie Hilfe? <a href="#" class="text-primary-600 dark:text-primary-400 hover:underline">Kontaktieren Sie uns</a></p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -229,6 +229,87 @@
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Suchergebnisse Container */
|
||||
.search-results-container {
|
||||
position: absolute;
|
||||
top: 5rem;
|
||||
left: 1rem;
|
||||
width: 350px;
|
||||
max-height: calc(100vh - 16rem);
|
||||
overflow-y: auto;
|
||||
border-radius: 1rem;
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
pointer-events: none;
|
||||
transition: all 0.3s ease;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.search-results-container.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
body.dark .search-results-container {
|
||||
background-color: rgba(15, 23, 42, 0.95);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
body:not(.dark) .search-results-container {
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
/* Suchergebnis-Item */
|
||||
.search-result-item {
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid rgba(127, 127, 127, 0.2);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.search-result-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
body.dark .search-result-item:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
body:not(.dark) .search-result-item:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.search-result-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-result-color {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
border-radius: 50%;
|
||||
margin-right: 0.5rem;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.search-result-desc {
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.search-result-source {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.6;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
body.dark .menu-item:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
@@ -299,6 +380,16 @@
|
||||
|
||||
<!-- Toolbar -->
|
||||
<div class="mindmap-toolbar mb-4">
|
||||
<!-- Suchleiste -->
|
||||
<div class="search-container mr-2">
|
||||
<div class="relative">
|
||||
<input type="text" id="mindmap-search" placeholder="Knoten suchen..."
|
||||
class="px-3 py-2 pr-10 rounded-lg text-sm border dark:bg-gray-700 dark:text-white dark:border-gray-600">
|
||||
<button id="search-btn" class="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-500 dark:text-gray-400">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button id="fit-btn" class="mindmap-action-btn">
|
||||
<i class="fas fa-expand"></i>
|
||||
<span>Alles anzeigen</span>
|
||||
@@ -319,6 +410,20 @@
|
||||
<i class="fas fa-save"></i>
|
||||
<span>Layout speichern</span>
|
||||
</button>
|
||||
{% if mindmap.user_id == current_user.id %}
|
||||
<button id="share-btn" class="mindmap-action-btn">
|
||||
<i class="fas fa-share-alt"></i>
|
||||
<span>Freigeben</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
<button id="export-btn" class="mindmap-action-btn">
|
||||
<i class="fas fa-file-export"></i>
|
||||
<span>Exportieren</span>
|
||||
</button>
|
||||
<button id="import-btn" class="mindmap-action-btn">
|
||||
<i class="fas fa-file-import"></i>
|
||||
<span>Importieren</span>
|
||||
</button>
|
||||
<button id="toggle-public-mindmap-btn" class="mindmap-action-btn">
|
||||
<i class="fas fa-globe"></i>
|
||||
<span>Öffentliche Mindmap</span>
|
||||
@@ -336,6 +441,19 @@
|
||||
<div id="public-cy" style="width: 100%; height: 250px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Suchergebnisse Container -->
|
||||
<div id="search-results-container" class="search-results-container p-4">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<h3 class="text-lg font-semibold">Suchergebnisse</h3>
|
||||
<button id="close-search" class="text-sm opacity-70 hover:opacity-100">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="search-results-list" class="space-y-1">
|
||||
<!-- Suchergebnisse werden hier dynamisch hinzugefügt -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Informationsanzeige für ausgewählten Knoten -->
|
||||
<div id="node-info-panel" class="node-info-panel p-4">
|
||||
<h3 class="text-xl font-bold gradient-text mb-2">Knotendetails</h3>
|
||||
@@ -952,6 +1070,505 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Suchfunktionalität implementieren
|
||||
const searchInput = document.getElementById('mindmap-search');
|
||||
const searchBtn = document.getElementById('search-btn');
|
||||
const searchResultsContainer = document.getElementById('search-results-container');
|
||||
const searchResultsList = document.getElementById('search-results-list');
|
||||
const closeSearchBtn = document.getElementById('close-search');
|
||||
|
||||
// Funktion zum Schließen der Suchergebnisse
|
||||
function closeSearchResults() {
|
||||
searchResultsContainer.classList.remove('visible');
|
||||
searchInput.value = '';
|
||||
}
|
||||
|
||||
// Event-Listener für die Suche
|
||||
searchBtn.addEventListener('click', performSearch);
|
||||
searchInput.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
performSearch();
|
||||
}
|
||||
});
|
||||
|
||||
closeSearchBtn.addEventListener('click', closeSearchResults);
|
||||
|
||||
// Funktion für die Suche
|
||||
function performSearch() {
|
||||
const query = searchInput.value.trim();
|
||||
if (!query) {
|
||||
showUINotification('Bitte geben Sie einen Suchbegriff ein.', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
// Lade-Animation im Suchergebnisbereich anzeigen
|
||||
searchResultsList.innerHTML = '<div class="p-4 text-center"><i class="fas fa-spinner fa-spin mr-2"></i> Suche läuft...</div>';
|
||||
searchResultsContainer.classList.add('visible');
|
||||
|
||||
// API-Anfrage für die Suche
|
||||
fetch(`/api/search/mindmap?q=${encodeURIComponent(query)}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
displaySearchResults(data.results, query);
|
||||
} else {
|
||||
searchResultsList.innerHTML = `<div class="p-4 text-center text-red-500">Fehler: ${data.message}</div>`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler bei der Suche:', error);
|
||||
searchResultsList.innerHTML = '<div class="p-4 text-center text-red-500">Ein Fehler ist aufgetreten.</div>';
|
||||
});
|
||||
}
|
||||
|
||||
// Funktion zum Anzeigen der Suchergebnisse
|
||||
function displaySearchResults(results, query) {
|
||||
searchResultsList.innerHTML = '';
|
||||
|
||||
if (results.length === 0) {
|
||||
searchResultsList.innerHTML = '<div class="p-4 text-center">Keine Ergebnisse gefunden.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Hervorheben-Funktion für den Suchbegriff
|
||||
function highlightText(text, query) {
|
||||
if (!text) return '';
|
||||
|
||||
const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
|
||||
return text.replace(regex, '<span class="bg-yellow-200 dark:bg-yellow-800">$1</span>');
|
||||
}
|
||||
|
||||
// Ergebnisse anzeigen
|
||||
results.forEach(result => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'search-result-item';
|
||||
|
||||
// Inhalte mit hervorgehobenem Suchbegriff
|
||||
const highlightedName = highlightText(result.name, query);
|
||||
const highlightedDesc = highlightText(result.description, query);
|
||||
|
||||
item.innerHTML = `
|
||||
<div class="search-result-title">
|
||||
<span class="search-result-color" style="background-color: ${result.color_code}"></span>
|
||||
<span>${highlightedName}</span>
|
||||
</div>
|
||||
<div class="search-result-desc">${highlightedDesc}</div>
|
||||
<div class="search-result-source">
|
||||
<i class="fas fa-map-marked-alt mr-1"></i> ${result.mindmap_name}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Event-Listener zum Springen zum Knoten
|
||||
item.addEventListener('click', () => {
|
||||
if (result.source === 'user_mindmap') {
|
||||
// Knoten ist bereits in der aktuellen Mindmap
|
||||
const node = cy.$id(result.id);
|
||||
if (node.length > 0) {
|
||||
// Knoten auswählen und zentrieren
|
||||
node.select();
|
||||
cy.animate({
|
||||
center: { eles: node },
|
||||
zoom: 1.5,
|
||||
duration: 500
|
||||
});
|
||||
|
||||
// Aktualisiere das Info-Panel
|
||||
nodeDescription.textContent = result.description || 'Keine Beschreibung für diesen Knoten.';
|
||||
nodeInfoPanel.classList.add('visible');
|
||||
|
||||
// Suchergebnisse schließen
|
||||
closeSearchResults();
|
||||
} else {
|
||||
// Knoten ist in einer anderen Mindmap des Benutzers
|
||||
if (result.mindmap_id && result.mindmap_id !== parseInt("{{ mindmap.id }}")) {
|
||||
if (confirm(`Dieser Knoten befindet sich in der Mindmap "${result.mindmap_name}". Möchten Sie zu dieser Mindmap wechseln?`)) {
|
||||
window.location.href = `/my-mindmap/${result.mindmap_id}`;
|
||||
}
|
||||
} else {
|
||||
showUINotification('Knoten konnte nicht gefunden werden.', 'error');
|
||||
}
|
||||
}
|
||||
} else if (result.source === 'public') {
|
||||
// Knoten ist in der öffentlichen Mindmap, frage ob er hinzugefügt werden soll
|
||||
if (confirm(`Möchten Sie den Knoten "${result.name}" aus der öffentlichen Mindmap zu Ihrer Mindmap hinzufügen?`)) {
|
||||
// Dialog zum Hinzufügen des Knotens anzeigen
|
||||
showAddNodeDialog(result.id, result.name, result.description);
|
||||
closeSearchResults();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
searchResultsList.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
// Export-Funktionalität implementieren
|
||||
document.getElementById('export-btn').addEventListener('click', function() {
|
||||
const mindmapId = parseInt("{{ mindmap.id }}");
|
||||
|
||||
// Dialog für Export-Format anzeigen
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50';
|
||||
overlay.id = 'export-dialog-overlay';
|
||||
|
||||
overlay.innerHTML = `
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4">
|
||||
<h3 class="text-xl font-bold mb-4">Mindmap exportieren</h3>
|
||||
<div class="mb-4">
|
||||
<label class="block mb-2">Format auswählen:</label>
|
||||
<select id="export-format" class="w-full p-2 border rounded-lg dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
||||
<option value="json">JSON</option>
|
||||
<option value="xml">XML</option>
|
||||
<option value="csv">CSV</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button id="cancel-export" class="px-4 py-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-lg">Abbrechen</button>
|
||||
<button id="confirm-export" class="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg">Exportieren</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// Event-Listener für Export-Buttons
|
||||
document.getElementById('cancel-export').addEventListener('click', function() {
|
||||
document.getElementById('export-dialog-overlay').remove();
|
||||
});
|
||||
|
||||
document.getElementById('confirm-export').addEventListener('click', function() {
|
||||
const format = document.getElementById('export-format').value;
|
||||
|
||||
// API-Anfrage für den Export
|
||||
fetch(`/api/mindmap/${mindmapId}/export?format=${format}`)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.blob();
|
||||
})
|
||||
.then(blob => {
|
||||
// Download der Export-Datei
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `mindmap_${mindmapId}.${format}`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.getElementById('export-dialog-overlay').remove();
|
||||
showUINotification(`Mindmap wurde erfolgreich als ${format.toUpperCase()} exportiert.`, 'success');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Exportieren der Mindmap:', error);
|
||||
showUINotification('Fehler beim Exportieren der Mindmap.', 'error');
|
||||
document.getElementById('export-dialog-overlay').remove();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Import-Funktionalität implementieren
|
||||
document.getElementById('import-btn').addEventListener('click', function() {
|
||||
const mindmapId = parseInt("{{ mindmap.id }}");
|
||||
|
||||
// Dialog für Import-Format anzeigen
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50';
|
||||
overlay.id = 'import-dialog-overlay';
|
||||
|
||||
overlay.innerHTML = `
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4">
|
||||
<h3 class="text-xl font-bold mb-4">Mindmap importieren</h3>
|
||||
<div class="mb-4">
|
||||
<label class="block mb-2">Datei auswählen:</label>
|
||||
<input type="file" id="import-file" class="w-full p-2 border rounded-lg dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
||||
accept=".json,.xml,.csv">
|
||||
</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button id="cancel-import" class="px-4 py-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-lg">Abbrechen</button>
|
||||
<button id="confirm-import" class="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg">Importieren</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// Event-Listener für Import-Buttons
|
||||
document.getElementById('cancel-import').addEventListener('click', function() {
|
||||
document.getElementById('import-dialog-overlay').remove();
|
||||
});
|
||||
|
||||
document.getElementById('confirm-import').addEventListener('click', function() {
|
||||
const fileInput = document.getElementById('import-file');
|
||||
|
||||
if (!fileInput.files || fileInput.files.length === 0) {
|
||||
showUINotification('Bitte wählen Sie eine Datei aus.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const file = fileInput.files[0];
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
// API-Anfrage für den Import
|
||||
fetch(`/api/mindmap/${mindmapId}/import`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
document.getElementById('import-dialog-overlay').remove();
|
||||
showUINotification('Mindmap wurde erfolgreich importiert.', 'success');
|
||||
|
||||
// Seite nach kurzer Verzögerung neu laden, um die importierten Daten anzuzeigen
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1500);
|
||||
} else {
|
||||
showUINotification(`Fehler beim Importieren: ${data.message}`, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Importieren der Mindmap:', error);
|
||||
showUINotification('Fehler beim Importieren der Mindmap.', 'error');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Freigabe-Funktionalität implementieren
|
||||
if (document.getElementById('share-btn')) {
|
||||
document.getElementById('share-btn').addEventListener('click', function() {
|
||||
const mindmapId = parseInt("{{ mindmap.id }}");
|
||||
|
||||
// Dialog für Mindmap-Freigabe anzeigen
|
||||
showShareDialog(mindmapId);
|
||||
});
|
||||
}
|
||||
|
||||
// Funktion zum Anzeigen des Freigabe-Dialogs
|
||||
function showShareDialog(mindmapId) {
|
||||
// Dialog erstellen
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50';
|
||||
overlay.id = 'share-dialog-overlay';
|
||||
|
||||
overlay.innerHTML = `
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-lg w-full mx-4">
|
||||
<h3 class="text-xl font-bold mb-4">Mindmap freigeben</h3>
|
||||
|
||||
<div class="mb-6">
|
||||
<div class="flex flex-col space-y-4">
|
||||
<div>
|
||||
<label class="block mb-2 text-sm font-medium">E-Mail-Adresse des Benutzers:</label>
|
||||
<input type="email" id="share-email" class="w-full p-2 border rounded-lg dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
||||
placeholder="beispiel@mail.com">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block mb-2 text-sm font-medium">Berechtigungen:</label>
|
||||
<select id="share-permission" class="w-full p-2 border rounded-lg dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
||||
<option value="READ">Nur-Lesen</option>
|
||||
<option value="EDIT">Bearbeiten</option>
|
||||
<option value="ADMIN">Administrator</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button id="add-share-btn" class="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg">
|
||||
<i class="fas fa-plus mr-1"></i> Hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h4 class="font-semibold mb-2">Aktuelle Freigaben</h4>
|
||||
<div id="share-list" class="max-h-60 overflow-y-auto border rounded-lg p-2 dark:border-gray-700">
|
||||
<div class="text-center text-sm opacity-70 p-4">Lade Freigaben...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2 mt-4">
|
||||
<button id="close-share-dialog" class="px-4 py-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-lg">Schließen</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// Aktuelle Freigaben laden
|
||||
loadShares(mindmapId);
|
||||
|
||||
// Event-Listener für Buttons
|
||||
document.getElementById('close-share-dialog').addEventListener('click', function() {
|
||||
document.getElementById('share-dialog-overlay').remove();
|
||||
});
|
||||
|
||||
document.getElementById('add-share-btn').addEventListener('click', function() {
|
||||
const email = document.getElementById('share-email').value.trim();
|
||||
const permission = document.getElementById('share-permission').value;
|
||||
|
||||
if (!email) {
|
||||
showUINotification('Bitte geben Sie eine E-Mail-Adresse ein.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Mindmap freigeben
|
||||
fetch(`/api/mindmap/${mindmapId}/share`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token() }}'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: email,
|
||||
permission: permission
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showUINotification(data.message, 'success');
|
||||
document.getElementById('share-email').value = '';
|
||||
loadShares(mindmapId); // Freigaben neu laden
|
||||
} else {
|
||||
showUINotification(data.error, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Freigeben der Mindmap:', error);
|
||||
showUINotification('Fehler beim Freigeben der Mindmap.', 'error');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Funktion zum Laden der aktuellen Freigaben
|
||||
function loadShares(mindmapId) {
|
||||
const shareList = document.getElementById('share-list');
|
||||
if (!shareList) return;
|
||||
|
||||
shareList.innerHTML = '<div class="text-center text-sm opacity-70 p-4">Lade Freigaben...</div>';
|
||||
|
||||
fetch(`/api/mindmap/${mindmapId}/shares`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const shares = data.shares;
|
||||
|
||||
if (shares.length === 0) {
|
||||
shareList.innerHTML = '<div class="text-center text-sm opacity-70 p-4">Keine Freigaben vorhanden.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
shareList.innerHTML = '';
|
||||
|
||||
shares.forEach(share => {
|
||||
const shareItem = document.createElement('div');
|
||||
shareItem.className = 'flex justify-between items-center p-2 border-b dark:border-gray-700 last:border-0';
|
||||
|
||||
const permissionClass = {
|
||||
'READ': 'text-blue-500 dark:text-blue-400',
|
||||
'EDIT': 'text-green-500 dark:text-green-400',
|
||||
'ADMIN': 'text-purple-500 dark:text-purple-400'
|
||||
}[share.permission] || 'text-gray-500';
|
||||
|
||||
shareItem.innerHTML = `
|
||||
<div>
|
||||
<div class="font-medium">${share.username}</div>
|
||||
<div class="text-xs opacity-70">${share.email}</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<select class="share-permission-select text-sm p-1 rounded ${permissionClass}" data-share-id="${share.id}">
|
||||
<option value="READ" ${share.permission === 'READ' ? 'selected' : ''}>Nur-Lesen</option>
|
||||
<option value="EDIT" ${share.permission === 'EDIT' ? 'selected' : ''}>Bearbeiten</option>
|
||||
<option value="ADMIN" ${share.permission === 'ADMIN' ? 'selected' : ''}>Administrator</option>
|
||||
</select>
|
||||
<button class="text-red-500 hover:text-red-700" data-share-id="${share.id}">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Event-Listener zum Aktualisieren der Berechtigungen
|
||||
const permissionSelect = shareItem.querySelector('.share-permission-select');
|
||||
permissionSelect.addEventListener('change', function() {
|
||||
const shareId = this.dataset.shareId;
|
||||
const newPermission = this.value;
|
||||
|
||||
updateSharePermission(shareId, newPermission);
|
||||
});
|
||||
|
||||
// Event-Listener zum Löschen der Freigabe
|
||||
const deleteButton = shareItem.querySelector('button[data-share-id]');
|
||||
deleteButton.addEventListener('click', function() {
|
||||
const shareId = this.dataset.shareId;
|
||||
|
||||
if (confirm('Möchten Sie diese Freigabe wirklich widerrufen?')) {
|
||||
revokeShare(shareId, mindmapId);
|
||||
}
|
||||
});
|
||||
|
||||
shareList.appendChild(shareItem);
|
||||
});
|
||||
|
||||
} else {
|
||||
shareList.innerHTML = '<div class="text-center text-sm text-red-500 p-4">Fehler beim Laden der Freigaben.</div>';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Laden der Freigaben:', error);
|
||||
shareList.innerHTML = '<div class="text-center text-sm text-red-500 p-4">Fehler beim Laden der Freigaben.</div>';
|
||||
});
|
||||
}
|
||||
|
||||
// Funktion zum Aktualisieren der Berechtigungen einer Freigabe
|
||||
function updateSharePermission(shareId, permission) {
|
||||
fetch(`/api/mindmap/shares/${shareId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token() }}'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
permission: permission
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showUINotification(data.message, 'success');
|
||||
} else {
|
||||
showUINotification(data.error, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Aktualisieren der Berechtigungen:', error);
|
||||
showUINotification('Fehler beim Aktualisieren der Berechtigungen.', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Funktion zum Widerrufen einer Freigabe
|
||||
function revokeShare(shareId, mindmapId) {
|
||||
fetch(`/api/mindmap/shares/${shareId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRFToken': '{{ csrf_token() }}'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showUINotification(data.message, 'success');
|
||||
loadShares(mindmapId); // Freigaben neu laden
|
||||
} else {
|
||||
showUINotification(data.error, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Widerrufen der Freigabe:', error);
|
||||
showUINotification('Fehler beim Widerrufen der Freigabe.', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
} else {
|
||||
// Fallback, falls mindmapData null ist
|
||||
if(mindmapNameH1) mindmapNameH1.textContent = "Mindmap nicht gefunden";
|
||||
|
||||
Reference in New Issue
Block a user