Compare commits

...

12 Commits

14 changed files with 1588 additions and 133 deletions

Binary file not shown.

418
app.py
View File

@@ -9,7 +9,6 @@ from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
import json import json
from enum import Enum from enum import Enum
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, TextAreaField, SelectField, HiddenField from wtforms import StringField, PasswordField, BooleanField, TextAreaField, SelectField, HiddenField
from wtforms.validators import DataRequired, Email, Length, EqualTo, ValidationError from wtforms.validators import DataRequired, Email, Length, EqualTo, ValidationError
from functools import wraps from functools import wraps
@@ -47,7 +46,6 @@ app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=365) # Langlebige Session für Dark Mode-Einstellung 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')) app.config['UPLOAD_FOLDER'] = os.getenv('UPLOAD_FOLDER', os.path.join(os.getcwd(), 'uploads'))
app.config['WTF_CSRF_ENABLED'] = False
# OpenAI API-Konfiguration # OpenAI API-Konfiguration
api_key = os.environ.get("OPENAI_API_KEY", "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") api_key = os.environ.get("OPENAI_API_KEY", "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")
@@ -122,39 +120,7 @@ socketio = SocketIO(app)
migrate = Migrate(app, db) migrate = Migrate(app, db)
# Automatische Datenbankinitialisierung - Aktualisiert für Flask 2.2+ Kompatibilität # Funktion zum Erstellen von Standardbenutzern
def initialize_app():
"""Initialisierung der Anwendung"""
print("Initialisierung der Anwendung...")
with app.app_context():
# Prüfen, ob die Datenbank existiert und initialisiert ist
try:
# Prüfen, ob Tabellen existieren
db.create_all()
# Prüfen, ob Stammdaten vorhanden sind
if User.query.count() == 0:
print("Erstelle Standardbenutzer...")
create_default_users()
if Category.query.count() == 0:
print("Erstelle Standardkategorien...")
create_default_categories()
if MindMapNode.query.count() == 0 and Category.query.count() > 0:
print("Erstelle Beispiel-Mindmap...")
create_sample_mindmap()
print("Datenbank wurde erfolgreich initialisiert.")
except Exception as e:
import traceback
print(f"Fehler bei der Datenbankinitialisierung: {e}")
print(traceback.format_exc())
# Moderne Methode für die Datenbankinitialisierung in Flask 2.2+
with app.app_context():
initialize_app()
def create_default_users(): def create_default_users():
"""Erstellt Standardbenutzer für die Anwendung""" """Erstellt Standardbenutzer für die Anwendung"""
users = [ users = [
@@ -765,7 +731,94 @@ def delete_mindmap(mindmap_id):
flash('Mindmap erfolgreich gelöscht!', 'success') flash('Mindmap erfolgreich gelöscht!', 'success')
return redirect(url_for('profile')) return redirect(url_for('profile'))
# API-Endpunkte für Mindmap-Daten # API-Endpunkte für UserMindmap CRUD-Operationen
@app.route('/api/mindmaps', methods=['POST'])
@login_required
def api_create_user_mindmap():
data = request.get_json()
name = data.get('name')
description = data.get('description')
is_private = data.get('is_private', True)
if not name:
return jsonify({'error': 'Name ist erforderlich'}), 400
new_mindmap = UserMindmap(
name=name,
description=description,
user_id=current_user.id,
is_private=is_private
)
db.session.add(new_mindmap)
db.session.commit()
return jsonify({
'id': new_mindmap.id,
'name': new_mindmap.name,
'description': new_mindmap.description,
'is_private': new_mindmap.is_private,
'user_id': new_mindmap.user_id,
'created_at': new_mindmap.created_at.isoformat(),
'last_modified': new_mindmap.last_modified.isoformat()
}), 201
@app.route('/api/mindmaps', methods=['GET'])
@login_required
def api_get_user_mindmaps():
mindmaps = UserMindmap.query.filter_by(user_id=current_user.id).all()
return jsonify([{
'id': m.id,
'name': m.name,
'description': m.description,
'is_private': m.is_private,
'created_at': m.created_at.isoformat(),
'last_modified': m.last_modified.isoformat()
} for m in mindmaps])
@app.route('/api/mindmaps/<int:mindmap_id>', methods=['GET'])
@login_required
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({
'id': mindmap.id,
'name': mindmap.name,
'description': mindmap.description,
'is_private': mindmap.is_private,
'user_id': mindmap.user_id,
'created_at': mindmap.created_at.isoformat(),
'last_modified': mindmap.last_modified.isoformat(),
# Hier könnten auch Knoten und Notizen hinzugefügt werden, ähnlich wie in get_user_mindmap
})
@app.route('/api/mindmaps/<int:mindmap_id>', methods=['PUT'])
@login_required
def api_update_user_mindmap(mindmap_id):
mindmap = UserMindmap.query.filter_by(id=mindmap_id, user_id=current_user.id).first_or_404()
data = request.get_json()
mindmap.name = data.get('name', mindmap.name)
mindmap.description = data.get('description', mindmap.description)
mindmap.is_private = data.get('is_private', mindmap.is_private)
db.session.commit()
return jsonify({
'id': mindmap.id,
'name': mindmap.name,
'description': mindmap.description,
'is_private': mindmap.is_private,
'last_modified': mindmap.last_modified.isoformat()
})
@app.route('/api/mindmaps/<int:mindmap_id>', methods=['DELETE'])
@login_required
def api_delete_user_mindmap(mindmap_id):
mindmap = UserMindmap.query.filter_by(id=mindmap_id, user_id=current_user.id).first_or_404()
db.session.delete(mindmap)
db.session.commit()
return jsonify({'message': 'Mindmap erfolgreich gelöscht'}), 200
# API-Endpunkte für Mindmap-Daten (öffentlich und benutzerspezifisch)
@app.route('/api/mindmap/public') @app.route('/api/mindmap/public')
def get_public_mindmap(): def get_public_mindmap():
"""Liefert die Standard-Mindmap-Struktur basierend auf Kategorien.""" """Liefert die Standard-Mindmap-Struktur basierend auf Kategorien."""
@@ -1028,9 +1081,11 @@ def add_node_to_mindmap(mindmap_id):
return jsonify({'error': 'Nicht autorisiert'}), 403 return jsonify({'error': 'Nicht autorisiert'}), 403
node_id = data.get('node_id') node_id = data.get('node_id')
parent_id = data.get('parent_id') # Optional: Elternknoten für die Verknüpfung
x_pos = data.get('x', 0) x_pos = data.get('x', 0)
y_pos = data.get('y', 0) y_pos = data.get('y', 0)
# Knoten abrufen
node = MindMapNode.query.get_or_404(node_id) node = MindMapNode.query.get_or_404(node_id)
# Prüfen, ob der Knoten bereits in der Mindmap existiert # Prüfen, ob der Knoten bereits in der Mindmap existiert
@@ -1052,6 +1107,23 @@ def add_node_to_mindmap(mindmap_id):
y_position=y_pos y_position=y_pos
) )
db.session.add(user_node) db.session.add(user_node)
# Wenn ein Elternknoten angegeben wurde, Verbindung erstellen
if parent_id:
# Existenz des Elternknotens in der Mindmap prüfen
parent_node = MindMapNode.query.get(parent_id)
parent_user_node = UserMindmapNode.query.filter_by(
user_mindmap_id=mindmap_id,
node_id=parent_id
).first()
if parent_node and parent_user_node:
# Beziehung zwischen den Knoten erstellen
try:
parent_node.children.append(node)
db.session.flush()
except Exception as e:
print(f"Warnung: Fehler beim Erstellen der Beziehung: {e}")
db.session.commit() db.session.commit()
@@ -1082,6 +1154,24 @@ def remove_node_from_mindmap(mindmap_id, node_id):
return jsonify({'success': True}) return jsonify({'success': True})
@app.route('/api/mindmap/node/<int:node_id>', methods=['GET'])
@login_required
def get_node_info(node_id):
"""Liefert Detailinformationen zu einem einzelnen Knoten"""
try:
# Knoten abrufen
node = MindMapNode.query.get_or_404(node_id)
return jsonify({
'id': node.id,
'name': node.name,
'description': node.description or '',
'color_code': node.color_code or '#9F7AEA'
})
except Exception as e:
print(f"Fehler in get_node_info: {str(e)}")
return jsonify({'success': False, 'message': str(e)}), 500
@app.route('/api/mindmap/<int:mindmap_id>/update_node_position', methods=['POST']) @app.route('/api/mindmap/<int:mindmap_id>/update_node_position', methods=['POST'])
@login_required @login_required
def update_node_position(mindmap_id): def update_node_position(mindmap_id):
@@ -1918,7 +2008,7 @@ def admin_update_database():
if request.method == 'POST': if request.method == 'POST':
try: try:
import update_db import utils.update_db as update_db
update_success = update_db.update_user_table() update_success = update_db.update_user_table()
if update_success: if update_success:
message = "Die Datenbank wurde erfolgreich aktualisiert." message = "Die Datenbank wurde erfolgreich aktualisiert."
@@ -1936,6 +2026,7 @@ def admin_update_database():
def get_mindmap_node(node_id): def get_mindmap_node(node_id):
"""Liefert die Mindmap-Daten für einen bestimmten Knoten und seine Subthemen.""" """Liefert die Mindmap-Daten für einen bestimmten Knoten und seine Subthemen."""
try: try:
# Erkennen besonderer Knotennamen
if node_id == 'root': if node_id == 'root':
# Hauptknoten (Wissen) abrufen # Hauptknoten (Wissen) abrufen
wissen_node = MindMapNode.query.filter_by(name="Wissen").first() wissen_node = MindMapNode.query.filter_by(name="Wissen").first()
@@ -1951,11 +2042,39 @@ def get_mindmap_node(node_id):
# Alle direkten Kinder des Wissen-Knotens holen # Alle direkten Kinder des Wissen-Knotens holen
nodes = wissen_node.children.all() nodes = wissen_node.children.all()
else: parent_node = wissen_node
elif node_id.isdigit():
# Bestimmten Knoten und seine Kinder abrufen # Bestimmten Knoten und seine Kinder abrufen
parent_node = MindMapNode.query.get_or_404(node_id) parent_node = MindMapNode.query.get_or_404(int(node_id))
nodes = parent_node.children.all()
else:
# Versuche, einen Knoten mit diesem Namen zu finden
parent_node = MindMapNode.query.filter_by(name=node_id.capitalize()).first()
if not parent_node:
# Versuche, eine Kategorie mit diesem Namen zu finden
category = Category.query.filter(func.lower(Category.name) == func.lower(node_id)).first()
if category:
# Finde oder erstelle einen Knoten für diese Kategorie
parent_node = MindMapNode.query.filter_by(category_id=category.id).first()
if not parent_node:
parent_node = MindMapNode(
name=category.name,
description=category.description,
color_code=category.color_code,
category_id=category.id,
is_public=True
)
db.session.add(parent_node)
db.session.commit()
if not parent_node:
# Fallback zum Wissen-Knoten
parent_node = MindMapNode.query.filter_by(name="Wissen").first()
if not parent_node:
return jsonify({'error': 'Knoten nicht gefunden'}), 404
nodes = parent_node.children.all() nodes = parent_node.children.all()
wissen_node = parent_node
# Ergebnisdaten vorbereiten # Ergebnisdaten vorbereiten
nodes_data = [] nodes_data = []
@@ -1963,10 +2082,10 @@ def get_mindmap_node(node_id):
# Hauptknoten hinzufügen # Hauptknoten hinzufügen
nodes_data.append({ nodes_data.append({
'id': wissen_node.id, 'id': parent_node.id,
'name': wissen_node.name, 'name': parent_node.name,
'description': wissen_node.description or '', 'description': parent_node.description or '',
'color_code': wissen_node.color_code or '#4299E1', 'color_code': parent_node.color_code or '#4299E1',
'is_center': True, 'is_center': True,
'has_children': len(nodes) > 0 'has_children': len(nodes) > 0
}) })
@@ -1984,7 +2103,7 @@ def get_mindmap_node(node_id):
# Verbindung zum Elternknoten hinzufügen # Verbindung zum Elternknoten hinzufügen
edges_data.append({ edges_data.append({
'source_id': wissen_node.id, 'source_id': parent_node.id,
'target_id': node.id, 'target_id': node.id,
'strength': 0.8 'strength': 0.8
}) })
@@ -1996,7 +2115,214 @@ def get_mindmap_node(node_id):
except Exception as e: except Exception as e:
print(f"Fehler beim Abrufen der Mindmap-Daten: {str(e)}") print(f"Fehler beim Abrufen der Mindmap-Daten: {str(e)}")
import traceback
traceback.print_exc()
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': 'Mindmap-Daten konnten nicht geladen werden' 'error': 'Mindmap-Daten konnten nicht geladen werden'
}), 500 }), 500
# API-Endpunkte für Notizen und Layout-Speicherung
@app.route('/api/mindmap/node/<int:node_id>/notes', methods=['GET'])
@login_required
def get_node_notes(node_id):
"""Ruft die Notizen für einen Mindmap-Knoten ab"""
try:
# Prüfen, ob der Knoten existiert
node = MindMapNode.query.get_or_404(node_id)
# Prüfen, ob der Knoten in einer Mindmap des Benutzers ist
user_node = UserMindmapNode.query.filter_by(node_id=node_id).join(
UserMindmap, UserMindmapNode.user_mindmap_id == UserMindmap.id
).filter(UserMindmap.user_id == current_user.id).first()
if not user_node and not current_user.is_admin:
return jsonify({'success': False, 'message': 'Keine Berechtigung'}), 403
# Prüfen, ob eine Notiz für diesen Knoten existiert
note = MindmapNote.query.filter_by(
user_id=current_user.id,
node_id=node_id
).first()
if note:
return jsonify({
'success': True,
'notes': note.content,
'color_code': note.color_code
})
else:
return jsonify({
'success': True,
'notes': '',
'color_code': '#FFF59D' # Standard-Gelb
})
except Exception as e:
print(f"Fehler in get_node_notes: {str(e)}")
return jsonify({'success': False, 'message': str(e)}), 500
@app.route('/api/mindmap/node/<int:node_id>/notes', methods=['POST'])
@login_required
def update_node_notes(node_id):
"""Aktualisiert die Notizen für einen Mindmap-Knoten"""
try:
# Prüfen, ob der Knoten existiert
node = MindMapNode.query.get_or_404(node_id)
# Prüfen, ob der Knoten in einer Mindmap des Benutzers ist
user_node = UserMindmapNode.query.filter_by(node_id=node_id).join(
UserMindmap, UserMindmapNode.user_mindmap_id == UserMindmap.id
).filter(UserMindmap.user_id == current_user.id).first()
if not user_node and not current_user.is_admin:
return jsonify({'success': False, 'message': 'Keine Berechtigung'}), 403
# Daten aus dem Request abrufen
data = request.json
notes_content = data.get('notes', '')
color_code = data.get('color_code', '#FFF59D') # Standard-Gelb
# Prüfen, ob bereits eine Notiz für diesen Knoten existiert
note = MindmapNote.query.filter_by(
user_id=current_user.id,
node_id=node_id
).first()
if note:
# Vorhandene Notiz aktualisieren
note.content = notes_content
note.color_code = color_code
else:
# Neue Notiz erstellen - hier brauchen wir die Mindmap-ID
mindmap_id = user_node.user_mindmap_id
note = MindmapNote(
user_id=current_user.id,
mindmap_id=mindmap_id,
node_id=node_id,
content=notes_content,
color_code=color_code
)
db.session.add(note)
db.session.commit()
return jsonify({
'success': True,
'message': 'Notizen erfolgreich gespeichert'
})
except Exception as e:
db.session.rollback()
print(f"Fehler in update_node_notes: {str(e)}")
return jsonify({'success': False, 'message': str(e)}), 500
@app.route('/api/mindmap/<int:mindmap_id>/layout', methods=['POST'])
@login_required
def save_mindmap_layout(mindmap_id):
"""Speichert das Layout (Positionen der Knoten) einer Mindmap"""
try:
# Prüfe, ob die Mindmap dem Benutzer gehört
mindmap = UserMindmap.query.get_or_404(mindmap_id)
if mindmap.user_id != current_user.id and not current_user.is_admin:
return jsonify({'success': False, 'message': 'Keine Berechtigung'}), 403
# Daten aus dem Request abrufen
data = request.json
nodes_data = data.get('nodes', [])
# Positionen aller Knoten aktualisieren
for node_data in nodes_data:
node_id = node_data.get('id')
x_pos = node_data.get('x')
y_pos = node_data.get('y')
if node_id and x_pos is not None and y_pos is not None:
# UserMindmapNode-Eintrag aktualisieren
user_node = UserMindmapNode.query.filter_by(
user_mindmap_id=mindmap_id,
node_id=node_id
).first()
if user_node:
user_node.x_position = x_pos
user_node.y_position = y_pos
db.session.commit()
return jsonify({
'success': True,
'message': 'Layout erfolgreich gespeichert'
})
except Exception as e:
db.session.rollback()
print(f"Fehler in save_mindmap_layout: {str(e)}")
return jsonify({'success': False, 'message': str(e)}), 500
@app.route('/api/public-mindmap', methods=['GET'])
def get_public_mindmap_nodes():
"""Liefert die öffentliche Mindmap für die Knotenübernahme"""
try:
# Alle öffentlichen Knoten abrufen
public_nodes = MindMapNode.query.filter_by(is_public=True).all()
# Knoten formatieren
nodes = []
for node in public_nodes:
nodes.append({
'id': node.id,
'name': node.name,
'description': node.description or '',
'color_code': node.color_code or '#9F7AEA'
})
# Alle Beziehungen zwischen Knoten abrufen
edges = []
for node in public_nodes:
for child in node.children:
if child.is_public:
edges.append({
'source': node.id,
'target': child.id
})
return jsonify({
'success': True,
'nodes': nodes,
'edges': edges
})
except Exception as e:
print(f"Fehler in get_public_mindmap_nodes: {str(e)}")
return jsonify({'success': False, 'message': str(e)}), 500
# Automatische Datenbankinitialisierung - Aktualisiert für Flask 2.2+ Kompatibilität
def initialize_app():
"""Initialisierung der Anwendung"""
print("Initialisierung der Anwendung...")
with app.app_context():
# Prüfen, ob die Datenbank existiert und initialisiert ist
try:
# Prüfen, ob Tabellen existieren
db.create_all()
# Prüfen, ob Stammdaten vorhanden sind
if User.query.count() == 0:
print("Erstelle Standardbenutzer...")
create_default_users()
# Wir nutzen die initialize_database Funktion für Kategorien und Mindmap
# Diese Funktionalität ist bereits dort implementiert
initialize_database()
print("Datenbank wurde erfolgreich initialisiert.")
except Exception as e:
import traceback
print(f"Fehler bei der Datenbankinitialisierung: {e}")
print(traceback.format_exc())
# Moderne Methode für die Datenbankinitialisierung in Flask 2.2+
with app.app_context():
initialize_app()

Binary file not shown.

Binary file not shown.

View File

@@ -231,16 +231,16 @@
</div> </div>
<div class="form-body"> <div class="form-body">
<form action="{{ url_for('edit_mindmap', mindmap_id=mindmap.id) }}" method="POST"> <form id="edit-mindmap-form">
<div class="form-group"> <div class="form-group">
<label for="name" class="form-label">Name der Mindmap</label> <label for="name" class="form-label">Name der Mindmap</label>
<input type="text" id="name" name="name" class="form-input input-animation" required <input type="text" id="name" name="name" class="form-input input-animation" required
placeholder="z.B. Meine Philosophie-Mindmap" value="{{ mindmap.name }}"> placeholder="z.B. Meine Philosophie-Mindmap" value="{{ mindmap.name }}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="description" class="form-label">Beschreibung</label> <label for="description" class="form-label">Beschreibung</label>
<textarea id="description" name="description" class="form-textarea input-animation" <textarea id="description" name="description" class="form-textarea input-animation"
placeholder="Worum geht es in dieser Mindmap?">{{ mindmap.description }}</textarea> placeholder="Worum geht es in dieser Mindmap?">{{ mindmap.description }}</textarea>
</div> </div>
@@ -253,11 +253,11 @@
</div> </div>
<div class="flex justify-between mt-6"> <div class="flex justify-between mt-6">
<a href="{{ url_for('mindmap', mindmap_id=mindmap.id) }}" class="btn-cancel"> <a href="{{ url_for('my_account') }}" class="btn-cancel"> {# Zurück zur Kontoübersicht geändert #}
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
Zurück Zurück
</a> </a>
<button type="submit" class="btn-submit"> <button type="button" id="save-mindmap-details-btn" class="btn-submit"> {# type="button" und ID hinzugefügt #}
<i class="fas fa-save"></i> <i class="fas fa-save"></i>
Änderungen speichern Änderungen speichern
</button> </button>
@@ -322,13 +322,58 @@
}); });
}); });
// Formular-Absenden-Animation // Formular-Absenden-Logik für Metadaten
const form = document.querySelector('form'); const editMindmapForm = document.getElementById('edit-mindmap-form');
form.addEventListener('submit', function(e) { const saveDetailsBtn = document.getElementById('save-mindmap-details-btn');
const submitBtn = this.querySelector('.btn-submit');
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Wird gespeichert...'; if (saveDetailsBtn && editMindmapForm) {
submitBtn.disabled = true; saveDetailsBtn.addEventListener('click', async function(event) {
}); event.preventDefault();
const nameInput = document.getElementById('name');
const descriptionInput = document.getElementById('description');
const isPrivateInput = document.getElementById('is_private');
const mindmapId = "{{ mindmap.id }}"; // Sicherstellen, dass mindmap.id hier verfügbar ist
const data = {
name: nameInput.value,
description: descriptionInput.value,
is_private: isPrivateInput.checked
// Die 'data' (Knoten/Kanten) wird separat vom Cytoscape-Editor gehandhabt
};
saveDetailsBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Wird gespeichert...';
saveDetailsBtn.disabled = true;
try {
const response = await fetch(`/api/mindmaps/${mindmapId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (response.ok) {
const result = await response.json();
showStatus('Metadaten erfolgreich gespeichert!', false);
// Optional: Weiterleitung oder Aktualisierung der Seiteninhalte
// window.location.href = "{{ url_for('my_account') }}";
} else {
const errorData = await response.json();
console.error('Fehler beim Speichern der Metadaten:', errorData);
showStatus(`Fehler: ${errorData.error || response.statusText}`, true);
}
} catch (error) {
console.error('Netzwerkfehler oder anderer Fehler:', error);
showStatus('Speichern fehlgeschlagen. Netzwerkproblem?', true);
} finally {
saveDetailsBtn.innerHTML = '<i class="fas fa-save"></i> Änderungen speichern';
saveDetailsBtn.disabled = false;
}
});
}
// Mindmap initialisieren // Mindmap initialisieren
const mindmap = new MindMap.Visualization('cy', { const mindmap = new MindMap.Visualization('cy', {
@@ -337,56 +382,112 @@
onNodeClick: function(nodeData) { onNodeClick: function(nodeData) {
console.log("Knoten ausgewählt:", nodeData); console.log("Knoten ausgewählt:", nodeData);
}, },
onChange: function(data) { onChange: function(dataFromCytoscape) {
// Automatisches Speichern bei Änderungen // Automatisches Speichern bei Änderungen der Mindmap-Struktur
fetch('/api/mindmap/{{ mindmap.id }}/update', { // Die Metadaten (Name, Beschreibung, is_private) werden separat über das Formular oben gespeichert.
method: 'POST', // Diese onChange Funktion kümmert sich nur um die Strukturdaten (Knoten/Kanten).
headers: { const mindmapId = "{{ mindmap.id }}";
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}' // Debounce-Funktion, um API-Aufrufe zu limitieren
}, let debounceTimer;
body: JSON.stringify(data) const debounceSaveStructure = (currentMindmapData) => {
}).then(response => { clearTimeout(debounceTimer);
if (!response.ok) { debounceTimer = setTimeout(() => {
throw new Error('Netzwerkfehler beim Speichern'); // Der Backend-Endpunkt PUT /api/mindmaps/<id> erwartet ein Objekt,
} // das die zu aktualisierenden Felder enthält. Für die Struktur ist das 'data'.
console.log('Änderungen gespeichert'); const payload = {
}).catch(error => { data: currentMindmapData // Dies sind die von Cytoscape gelieferten Strukturdaten
console.error('Fehler beim Speichern:', error); };
alert('Fehler beim Speichern der Änderungen');
}); // showStatus('Speichere Struktur...', false); // Status wird jetzt über Event gehandhabt
fetch(`/api/mindmaps/${mindmapId}`, { // Endpunkt angepasst
method: 'PUT', // Methode zu PUT geändert
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload) // Sende die Mindmap-Daten als { data: ... }
}).then(response => {
if (!response.ok) {
response.json().then(err => {
console.error('Fehler beim Speichern der Struktur:', err);
document.dispatchEvent(new CustomEvent('mindmapError', { detail: { message: `Struktur: ${err.message || err.error || 'Speicherfehler'}` } }));
}).catch(() => {
console.error('Fehler beim Speichern der Struktur, Status:', response.statusText);
document.dispatchEvent(new CustomEvent('mindmapError', { detail: { message: `Struktur: ${response.statusText}` } }));
});
// throw new Error('Netzwerkfehler beim Speichern der Struktur'); // Wird schon behandelt
return; // Verhindere weitere Verarbeitung bei Fehler
}
return response.json();
}).then(responseData => {
if (responseData) { // Nur wenn response.ok war
console.log('Mindmap-Struktur erfolgreich gespeichert:', responseData);
// Die responseData von einem PUT könnte die aktualisierte Mindmap oder nur eine Erfolgsmeldung sein.
// Annahme: { message: "Mindmap updated successfully", mindmap: { ... } } oder ähnlich
document.dispatchEvent(new CustomEvent('mindmapSaved', { detail: { message: 'Struktur aktualisiert!' }}));
}
}).catch(error => {
console.error('Netzwerkfehler oder anderer Fehler beim Speichern der Struktur:', error);
// Vermeide doppelte Fehlermeldung, falls schon durch !response.ok behandelt
if (!document.querySelector('.bg-red-500')) { // Prüft, ob schon eine Fehlermeldung angezeigt wird
document.dispatchEvent(new CustomEvent('mindmapError', { detail: { message: 'Struktur: Netzwerkfehler' } }));
}
});
}, 1500); // Speichern 1.5 Sekunden nach der letzten Änderung
};
debounceSaveStructure(dataFromCytoscape); // Aufruf der Debounce-Funktion mit Cytoscape-Daten
} }
}); });
// Formularfelder mit Mindmap verbinden // Die Verknüpfung der Formularfelder (Name, Beschreibung) mit dem Cytoscape Root-Knoten wird entfernt,
const nameInput = document.getElementById('name'); // da die Metadaten nun über das separate Formular oben gespeichert werden und nicht mehr direkt
const descriptionInput = document.getElementById('description'); // die Cytoscape-Daten manipulieren sollen. Die Logik für mindmap.saveToServer() wurde entfernt,
// da das Speichern jetzt über den onChange Handler mit PUT /api/mindmaps/<id> erfolgt.
// Aktualisiere Mindmap wenn sich die Eingaben ändern // const nameInput = document.getElementById('name'); // Bereits oben deklariert für Metadaten
nameInput.addEventListener('input', function() { // nameInput.removeEventListener('input', ...); // Event Listener muss hier nicht entfernt werden, da er nicht neu hinzugefügt wird.
if (mindmap.cy) {
const rootNode = mindmap.cy.$('#root');
if (rootNode.length > 0) {
rootNode.data('name', this.value || 'Mindmap');
mindmap.saveToServer();
}
}
});
// Initialisiere die Mindmap mit existierenden Daten // Initialisiere die Mindmap mit existierenden Daten
mindmap.initialize().then(() => { mindmap.initialize().then(() => {
console.log("Mindmap-Editor initialisiert"); console.log("Mindmap-Editor initialisiert");
const mindmapId = "{{ mindmap.id }}";
// Lade existierende Daten // Lade existierende Daten für die Mindmap-Struktur
fetch('/api/mindmap/{{ mindmap.id }}/data') fetch(`/api/mindmaps/${mindmapId}`, { // Endpunkt für GET angepasst
.then(response => response.json()) method: 'GET',
.then(data => { headers: {
mindmap.loadData(data); 'Accept': 'application/json'
console.log("Mindmap-Daten geladen"); }
})
.then(response => {
if (!response.ok) {
response.json().then(err => {
showStatus(`Fehler beim Laden: ${err.message || err.error || response.statusText}`, true);
}).catch(() => {
showStatus(`Fehler beim Laden: ${response.statusText}`, true);
});
throw new Error(`Netzwerkantwort war nicht ok: ${response.statusText}`);
}
return response.json();
})
.then(mindmapDataFromServer => {
// Die API GET /api/mindmaps/<id> gibt ein Objekt zurück, das { id, name, description, is_private, data, ... } enthält.
// Wir brauchen nur den 'data'-Teil (Struktur) für Cytoscape.
// Die Metadaten (name, description, is_private) werden bereits serverseitig in die Formularfelder gerendert.
if (mindmapDataFromServer && mindmapDataFromServer.data) {
mindmap.loadData(mindmapDataFromServer.data); // Lade nur die Strukturdaten
console.log("Mindmap-Strukturdaten geladen:", mindmapDataFromServer.data);
showStatus("Mindmap geladen.", false);
} else {
console.error("Fehler: Mindmap-Daten (Struktur) nicht im erwarteten Format:", mindmapDataFromServer);
showStatus("Fehler: Mindmap-Struktur konnte nicht geladen werden (Formatfehler).", true);
}
}) })
.catch(error => { .catch(error => {
console.error("Fehler beim Laden der Mindmap-Daten:", error); console.error("Fehler beim Laden der Mindmap-Strukturdaten:", error);
alert("Fehler beim Laden der Mindmap"); if (!document.querySelector('.bg-red-500')) { // Prüft, ob schon eine Fehlermeldung angezeigt wird
showStatus("Laden der Struktur fehlgeschlagen.", true);
}
}); });
}).catch(error => { }).catch(error => {
console.error("Fehler bei der Initialisierung des Editors:", error); console.error("Fehler bei der Initialisierung des Editors:", error);
@@ -411,8 +512,9 @@
} }
// Event-Listener für Speicherstatus // Event-Listener für Speicherstatus
document.addEventListener('mindmapSaved', () => { document.addEventListener('mindmapSaved', (event) => {
showStatus('Änderungen gespeichert'); const message = event.detail && event.detail.message ? event.detail.message : 'Erfolgreich gespeichert!';
showStatus(message, false);
}); });
document.addEventListener('mindmapError', (event) => { document.addEventListener('mindmapError', (event) => {

View File

@@ -84,6 +84,23 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Meine erstellten Mindmaps -->
<div class="mb-12">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold text-gray-800 dark:text-white flex items-center">
<i class="fas fa-brain mr-3 text-green-500"></i>
Meine erstellten Mindmaps
</h2>
<button id="create-mindmap-btn" class="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors flex items-center">
<i class="fas fa-plus mr-2"></i> Neue Mindmap erstellen
</button>
</div>
<div id="user-mindmaps-container" class="space-y-4">
<!-- Hier werden die Mindmaps des Benutzers geladen -->
<p class="text-gray-600 dark:text-gray-400">Lade Mindmaps...</p>
</div>
</div>
<!-- Gemerkte Inhalte --> <!-- Gemerkte Inhalte -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8"> <div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8">
@@ -123,6 +140,431 @@
</div> </div>
</div> </div>
<!-- Modal zum Erstellen einer neuen Mindmap -->
<div id="create-mindmap-modal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full flex items-center justify-center hidden z-50">
<div class="relative mx-auto p-5 border w-full max-w-md shadow-lg rounded-md bg-white dark:bg-gray-800">
<div class="mt-3 text-center">
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-white">Neue Mindmap erstellen</h3>
<div class="mt-2 px-7 py-3">
<form id="create-mindmap-form">
<div class="mb-4">
<label for="mindmap-name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 text-left">Name</label>
<input type="text" name="name" id="mindmap-name" class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm dark:bg-gray-700 dark:text-white" required>
</div>
<div class="mb-4">
<label for="mindmap-description" class="block text-sm font-medium text-gray-700 dark:text-gray-300 text-left">Beschreibung (optional)</label>
<textarea name="description" id="mindmap-description" rows="3" class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm dark:bg-gray-700 dark:text-white"></textarea>
</div>
</form>
</div>
<div class="items-center px-4 py-3">
<button id="submit-create-mindmap" class="px-4 py-2 bg-green-500 text-white text-base font-medium rounded-md w-full shadow-sm hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-300">
Erstellen
</button>
<button id="cancel-create-mindmap" class="mt-2 px-4 py-2 bg-gray-300 text-gray-800 dark:bg-gray-600 dark:text-gray-200 text-base font-medium rounded-md w-full shadow-sm hover:bg-gray-400 dark:hover:bg-gray-500 focus:outline-none focus:ring-2 focus:ring-gray-300">
Abbrechen
</button>
</div>
</div>
</div>
</div>
<!-- JavaScript für persönliche Mindmap und CRUD -->
<script>
</script>
<script nonce="{{ csp_nonce }}">
document.addEventListener('DOMContentLoaded', function() {
// Modal-Logik
const createMindmapBtn = document.getElementById('create-mindmap-btn');
const createMindmapModal = document.getElementById('create-mindmap-modal');
const cancelCreateMindmapBtn = document.getElementById('cancel-create-mindmap');
const submitCreateMindmapBtn = document.getElementById('submit-create-mindmap');
const createMindmapForm = document.getElementById('create-mindmap-form');
if (createMindmapBtn) {
createMindmapBtn.addEventListener('click', () => {
createMindmapModal.classList.remove('hidden');
});
}
if (cancelCreateMindmapBtn) {
cancelCreateMindmapBtn.addEventListener('click', () => {
createMindmapModal.classList.add('hidden');
createMindmapForm.reset();
});
}
// Schließen bei Klick außerhalb des Modals
if (createMindmapModal) {
createMindmapModal.addEventListener('click', (event) => {
if (event.target === createMindmapModal) {
createMindmapModal.classList.add('hidden');
createMindmapForm.reset();
}
});
}
// Funktion zum Anzeigen von Benachrichtigungen
function showNotification(message, type = 'success') {
const notificationArea = document.getElementById('notification-area') || createNotificationArea();
const notificationId = `notif-${Date.now()}`;
constbgColor = type === 'success' ? 'bg-green-500' : (type === 'error' ? 'bg-red-500' : 'bg-blue-500');
const notificationElement = `
<div id="${notificationId}" class="p-4 mb-4 text-sm text-white rounded-lg ${bgColor} animate-fadeIn" role="alert">
<span class="font-medium">${type.charAt(0).toUpperCase() + type.slice(1)}:</span> ${message}
</div>
`;
notificationArea.insertAdjacentHTML('beforeend', notificationElement);
setTimeout(() => {
const el = document.getElementById(notificationId);
if (el) {
el.classList.add('animate-fadeOut');
setTimeout(() => el.remove(), 500);
}
}, 5000);
}
function createNotificationArea() {
const area = document.createElement('div');
area.id = 'notification-area';
area.className = 'fixed top-5 right-5 z-50 w-auto max-w-sm';
document.body.appendChild(area);
// Add some basic animation styles
const style = document.createElement('style');
style.textContent = `
.animate-fadeIn { animation: fadeIn 0.5s ease-out; }
.animate-fadeOut { animation: fadeOut 0.5s ease-in forwards; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; transform: translateY(0); } }
@keyframes fadeOut { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(-20px); } }
`;
document.head.appendChild(style);
return area;
}
// CRUD-Funktionen für UserMindmaps
const mindmapsContainer = document.getElementById('user-mindmaps-container');
async function fetchUserMindmaps() {
if (!mindmapsContainer) return;
mindmapsContainer.innerHTML = '<p class="text-gray-600 dark:text-gray-400">Lade Mindmaps...</p>';
try {
const response = await fetch('/api/mindmaps');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const mindmaps = await response.json();
renderMindmaps(mindmaps);
} catch (error) {
console.error('Fehler beim Laden der Mindmaps:', error);
mindmapsContainer.innerHTML = '<p class="text-red-500">Fehler beim Laden der Mindmaps.</p>';
showNotification('Fehler beim Laden der Mindmaps.', 'error');
}
}
function renderMindmaps(mindmaps) {
if (!mindmapsContainer) return;
if (mindmaps.length === 0) {
mindmapsContainer.innerHTML = '<p class="text-gray-600 dark:text-gray-400">Du hast noch keine eigenen Mindmaps erstellt.</p>';
return;
}
mindmapsContainer.innerHTML = ''; // Container leeren
const ul = document.createElement('ul');
ul.className = 'space-y-3';
mindmaps.forEach(mindmap => {
const li = document.createElement('li');
li.className = 'p-4 rounded-xl bg-white/80 dark:bg-gray-800/80 shadow-sm hover:shadow-md transition-all flex justify-between items-center';
const mindmapLink = document.createElement('a');
mindmapLink.href = `/user_mindmap/${mindmap.id}`;
mindmapLink.className = 'flex-grow';
const textDiv = document.createElement('div');
const nameH3 = document.createElement('h3');
nameH3.className = 'font-semibold text-gray-900 dark:text-white';
nameH3.textContent = mindmap.name;
textDiv.appendChild(nameH3);
if (mindmap.description) {
const descP = document.createElement('p');
descP.className = 'text-sm text-gray-600 dark:text-gray-400';
descP.textContent = mindmap.description;
textDiv.appendChild(descP);
}
mindmapLink.appendChild(textDiv);
li.appendChild(mindmapLink);
const actionsDiv = document.createElement('div');
actionsDiv.className = 'flex space-x-2 ml-4';
const editButton = document.createElement('a');
editButton.href = `/edit_mindmap/${mindmap.id}`; // oder JavaScript-basiertes Editieren
editButton.className = 'px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600 text-sm flex items-center';
editButton.innerHTML = '<i class="fas fa-edit mr-1"></i> Bearbeiten';
// Hier könnte auch ein Event-Listener für ein Modal zum Bearbeiten hinzugefügt werden
// editButton.addEventListener('click', (e) => { e.preventDefault(); openEditModal(mindmap); });
actionsDiv.appendChild(editButton);
const deleteButton = document.createElement('button');
deleteButton.className = 'px-3 py-1 bg-red-500 text-white rounded hover:bg-red-600 text-sm flex items-center delete-mindmap-btn';
deleteButton.innerHTML = '<i class="fas fa-trash mr-1"></i> Löschen';
deleteButton.dataset.mindmapId = mindmap.id;
actionsDiv.appendChild(deleteButton);
li.appendChild(actionsDiv);
ul.appendChild(li);
});
mindmapsContainer.appendChild(ul);
// Event Listener für Löschen-Buttons hinzufügen
document.querySelectorAll('.delete-mindmap-btn').forEach(button => {
button.addEventListener('click', async (event) => {
const mindmapId = event.currentTarget.dataset.mindmapId;
if (confirm('Bist du sicher, dass du diese Mindmap löschen möchtest?')) {
try {
const response = await fetch(`/api/mindmaps/${mindmapId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
showNotification('Mindmap erfolgreich gelöscht.', 'success');
fetchUserMindmaps(); // Liste aktualisieren
} catch (error) {
console.error('Fehler beim Löschen der Mindmap:', error);
showNotification(`Fehler beim Löschen: ${error.message}`, 'error');
}
}
});
});
}
if (submitCreateMindmapBtn) {
submitCreateMindmapBtn.addEventListener('click', async () => {
const name = document.getElementById('mindmap-name').value;
const description = document.getElementById('mindmap-description').value;
if (!name.trim()) {
showNotification('Der Name der Mindmap darf nicht leer sein.', 'error');
return;
}
try {
const response = await fetch('/api/mindmaps', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name, description, is_private: false }), // is_private standardmäßig auf false setzen
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
const newMindmap = await response.json();
showNotification(`Mindmap "${newMindmap.name}" erfolgreich erstellt. Weiterleitung...`, 'success');
createMindmapModal.classList.add('hidden');
createMindmapForm.reset();
// fetchUserMindmaps(); // Liste wird auf der neuen Seite ohnehin neu geladen oder ist nicht direkt sichtbar.
// Weiterleitung zur Bearbeitungsseite der neuen Mindmap
window.location.href = `/edit_mindmap/${newMindmap.id}`;
} catch (error) {
console.error('Fehler beim Erstellen der Mindmap:', error);
showNotification(`Fehler beim Erstellen: ${error.message}`, 'error');
}
});
}
// Initiale Ladefunktion für Mindmaps
fetchUserMindmaps();
// Bestehendes Skript für Bookmarks etc.
// Lade gespeicherte Bookmarks aus dem LocalStorage
function loadBookmarkedNodes() {
try {
const bookmarked = localStorage.getItem('bookmarkedNodes');
return bookmarked ? JSON.parse(bookmarked) : [];
} catch (error) {
console.error('Fehler beim Laden der gemerkten Knoten:', error);
return [];
}
}
const bookmarkedNodeIds = loadBookmarkedNodes();
// Prüfe, ob es gemerkte Knoten gibt
if (bookmarkedNodeIds && bookmarkedNodeIds.length > 0) {
// Verstecke die Leer-Nachricht
const emptyMindmapMessage = document.getElementById('empty-mindmap-message');
if (emptyMindmapMessage) {
emptyMindmapMessage.style.display = 'none';
}
// Initialisiere die persönliche Mindmap
const personalMindmapContainer = document.getElementById('personal-mindmap');
if (personalMindmapContainer && typeof MindMapVisualization !== 'undefined') {
const personalMindmap = new MindMapVisualization('#personal-mindmap', {
width: personalMindmapContainer.clientWidth,
height: 400,
nodeRadius: 18,
selectedNodeRadius: 22,
linkDistance: 120,
chargeStrength: -800,
centerForce: 0.1,
tooltipEnabled: true
});
// Lade Daten für die Mindmap
window.setTimeout(() => {
if (window.mindmapInstance) {
const nodes = window.mindmapInstance.nodes.filter(node =>
bookmarkedNodeIds.includes(node.id)
);
const links = window.mindmapInstance.links.filter(link =>
bookmarkedNodeIds.includes(link.source.id || link.source) &&
bookmarkedNodeIds.includes(link.target.id || link.target)
);
personalMindmap.nodes = nodes;
personalMindmap.links = links;
personalMindmap.isLoading = false;
personalMindmap.updateVisualization();
} else {
if (emptyMindmapMessage) emptyMindmapMessage.style.display = 'flex';
}
}, 800);
}
loadBookmarkedContent(bookmarkedNodeIds);
} else {
// Zeige Leerzustand an
const areasContainer = document.getElementById('bookmarked-areas-container');
const thoughtsContainer = document.getElementById('bookmarked-thoughts-container');
if (areasContainer) {
areasContainer.innerHTML = `
<div class="empty-state">
<div class="text-4xl mb-2 opacity-20">
<i class="fas fa-folder-open"></i>
</div>
<p class="text-gray-600 dark:text-gray-400">Keine gemerkten Wissensbereiche</p>
</div>
`;
}
if (thoughtsContainer) {
thoughtsContainer.innerHTML = `
<div class="empty-state">
<div class="text-4xl mb-2 opacity-20">
<i class="fas fa-lightbulb"></i>
</div>
<p class="text-gray-600 dark:text-gray-400">Keine gemerkten Gedanken</p>
</div>
`;
}
}
});
// Funktion zum Laden der gemerkten Inhalte (bleibt größtenteils gleich)
function loadBookmarkedContent(nodeIds) {
if (!nodeIds || nodeIds.length === 0) return;
const areasContainer = document.getElementById('bookmarked-areas-container');
const thoughtsContainer = document.getElementById('bookmarked-thoughts-container');
const colors = ['purple', 'blue', 'green', 'indigo', 'amber'];
if (areasContainer) areasContainer.innerHTML = '';
if (thoughtsContainer) thoughtsContainer.innerHTML = '';
const areaTemplates = [
{ name: 'Philosophie', description: 'Grundlagen philosophischen Denkens', count: 24 },
{ name: 'Wissenschaft', description: 'Wissenschaftliche Methoden und Erkenntnisse', count: 42 },
{ name: 'Technologie', description: 'Zukunftsweisende Technologien', count: 36 },
{ name: 'Kunst', description: 'Künstlerische Ausdrucksformen', count: 18 },
{ name: 'Psychologie', description: 'Menschliches Verhalten verstehen', count: 30 }
];
const thoughtTemplates = [
{ title: 'Quantenphysik und Bewusstsein', author: 'Maria Schmidt', date: '12.04.2023' },
{ title: 'Ethik in der künstlichen Intelligenz', author: 'Thomas Weber', date: '23.02.2023' },
{ title: 'Die Rolle der Kunst in der Gesellschaft', author: 'Lena Müller', date: '05.06.2023' },
{ title: 'Nachhaltige Entwicklung im 21. Jahrhundert', author: 'Michael Bauer', date: '18.08.2023' },
{ title: 'Kognitive Verzerrungen im Alltag', author: 'Sophie Klein', date: '30.09.2023' }
];
const areaCount = Math.min(nodeIds.length, 5);
if (areasContainer && areaCount > 0) {
for (let i = 0; i < areaCount; i++) {
const area = areaTemplates[i];
const colorClass = colors[i % colors.length];
areasContainer.innerHTML += `
<a href="{{ url_for('mindmap') }}" class="bookmark-item block p-4 rounded-xl bg-white/80 dark:bg-gray-800/80 shadow-sm hover:shadow-md transition-all">
<div class="flex items-center">
<div class="w-10 h-10 rounded-lg bg-${colorClass}-100 dark:bg-${colorClass}-900/30 flex items-center justify-center mr-3">
<i class="fas fa-bookmark text-${colorClass}-500"></i>
</div>
<div class="flex-grow">
<h3 class="font-semibold text-gray-900 dark:text-white">${area.name}</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">${area.description}</p>
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
${area.count} Einträge
</div>
</div>
</a>
`;
}
} else if (areasContainer) {
areasContainer.innerHTML = `
<div class="empty-state">
<div class="text-4xl mb-2 opacity-20">
<i class="fas fa-folder-open"></i>
</div>
<p class="text-gray-600 dark:text-gray-400">Keine gemerkten Wissensbereiche</p>
</div>
`;
}
const thoughtCount = Math.min(nodeIds.length, 5);
if (thoughtsContainer && thoughtCount > 0) {
for (let i = 0; i < thoughtCount; i++) {
const thought = thoughtTemplates[i];
const colorClass = colors[(i + 2) % colors.length];
thoughtsContainer.innerHTML += `
<a href="{{ url_for('mindmap') }}" class="bookmark-item block p-4 rounded-xl bg-white/80 dark:bg-gray-800/80 shadow-sm hover:shadow-md transition-all">
<div class="flex items-center">
<div class="w-10 h-10 rounded-lg bg-${colorClass}-100 dark:bg-${colorClass}-900/30 flex items-center justify-center mr-3">
<i class="fas fa-lightbulb text-${colorClass}-500"></i>
</div>
<div class="flex-grow">
<h3 class="font-semibold text-gray-900 dark:text-white">${thought.title}</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">Von ${thought.author}${thought.date}</p>
</div>
</div>
</a>
`;
}
} else if (thoughtsContainer) {
thoughtsContainer.innerHTML = `
<div class="empty-state">
<div class="text-4xl mb-2 opacity-20">
<i class="fas fa-lightbulb"></i>
</div>
<p class="text-gray-600 dark:text-gray-400">Keine gemerkten Gedanken</p>
</div>
`;
}
}
</script>
<!-- JavaScript für persönliche Mindmap --> <!-- JavaScript für persönliche Mindmap -->
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {

View File

@@ -319,12 +319,22 @@
<i class="fas fa-save"></i> <i class="fas fa-save"></i>
<span>Layout speichern</span> <span>Layout speichern</span>
</button> </button>
<button id="toggle-public-mindmap-btn" class="mindmap-action-btn">
<i class="fas fa-globe"></i>
<span>Öffentliche Mindmap</span>
</button>
</div> </div>
<!-- Mindmap Container mit Positionsindikator --> <!-- Mindmap Container mit Positionsindikator -->
<div class="relative rounded-xl overflow-hidden border transition-all duration-300" <div class="relative rounded-xl overflow-hidden border transition-all duration-300"
x-bind:class="darkMode ? 'border-gray-700/50' : 'border-gray-300/50'"> x-bind:class="darkMode ? 'border-gray-700/50' : 'border-gray-300/50'">
<div id="cy"></div> <div id="cy" data-mindmap-id="{{ mindmap.id }}"></div>
<!-- Container für öffentliche Mindmap (zur Knotenübernahme) -->
<div id="public-mindmap-container" class="hidden mt-4 border-t border-gray-300 dark:border-gray-700 pt-4" style="height: 300px;">
<h3 class="text-lg font-semibold mb-2 text-center">Öffentliche Mindmap</h3>
<div id="public-cy" style="width: 100%; height: 250px;"></div>
</div>
<!-- Informationsanzeige für ausgewählten Knoten --> <!-- Informationsanzeige für ausgewählten Knoten -->
<div id="node-info-panel" class="node-info-panel p-4"> <div id="node-info-panel" class="node-info-panel p-4">
@@ -353,39 +363,600 @@
<script src="{{ url_for('static', filename='js/cytoscape.min.js') }}"></script> <script src="{{ url_for('static', filename='js/cytoscape.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/mindmap-init.js') }}"></script> <script src="{{ url_for('static', filename='js/mindmap-init.js') }}"></script>
<script nonce="{{ csp_nonce }}"> <script nonce="{{ csp_nonce }}">
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', async function() {
// Benutzer-Mindmap-ID für die API-Anfragen const cyContainer = document.getElementById('cy');
const mindmapId = {{ mindmap.id }}; if (!cyContainer) {
console.error("Mindmap container #cy not found!");
// Erstellt eine neue MindMap-Instanz für die Benutzer-Mindmap return;
window.userMindmap = new MindMap('#cy', { }
editable: true, const mindmapId = cyContainer.dataset.mindmapId;
isUserLoggedIn: true, const nodeInfoPanel = document.getElementById('node-info-panel');
isPublicMap: false, const nodeDescription = document.getElementById('node-description');
userMindmapId: mindmapId, const connectedNodesContainer = document.getElementById('connected-nodes');
fitViewOnInit: true, const mindmapNameH1 = document.querySelector('h1.gradient-text');
callbacks: { const mindmapDescriptionP = document.querySelector('p.opacity-80.mt-1');
onLoad: function() {
console.log('Benutzerdefinierte Mindmap wurde geladen'); // Funktion zum Anzeigen von Benachrichtigungen (vereinfacht)
function showUINotification(message, type = 'success') {
const notificationArea = document.getElementById('notification-area-usr') || createUINotificationArea();
const notificationId = `notif-usr-${Date.now()}`;
const bgColor = type === 'success' ? 'bg-green-500' : (type === 'error' ? 'bg-red-500' : 'bg-blue-500');
const notificationElement = `
<div id="${notificationId}" class="p-3 mb-3 text-sm text-white rounded-lg ${bgColor} animate-fadeIn" role="alert">
<span>${message}</span>
</div>
`;
notificationArea.insertAdjacentHTML('beforeend', notificationElement);
setTimeout(() => {
const el = document.getElementById(notificationId);
if (el) {
el.classList.add('animate-fadeOut');
setTimeout(() => el.remove(), 500);
}
}, 3000);
}
function createUINotificationArea() {
const area = document.createElement('div');
area.id = 'notification-area-usr';
area.className = 'fixed top-20 right-5 z-[1001] w-auto max-w-xs'; // höhere z-index
document.body.appendChild(area);
const style = document.createElement('style');
style.textContent = `
.animate-fadeIn { animation: fadeIn 0.3s ease-out; }
.animate-fadeOut { animation: fadeOut 0.3s ease-in forwards; }
@keyframes fadeIn { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: translateX(0); } }
@keyframes fadeOut { from { opacity: 1; transform: translateX(0); } to { opacity: 0; transform: translateX(20px); } }
`;
document.head.appendChild(style);
return area;
}
async function fetchMindmapData(id) {
try {
const response = await fetch(`/api/mindmaps/${id}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
} }
return await response.json();
} catch (error) {
console.error('Fehler beim Laden der Mindmap-Daten:', error);
showUINotification('Fehler beim Laden der Mindmap-Daten.', 'error');
cyContainer.innerHTML = '<p class="text-center text-red-500 p-10">Konnte Mindmap nicht laden.</p>';
return null;
} }
}); }
// Event-Listener für Notiz-Button const mindmapData = await fetchMindmapData(mindmapId);
document.getElementById('add-note-btn').addEventListener('click', function() {
// Erstellt eine neue Notiz in der Mitte des Viewports if (mindmapData) {
const position = window.userMindmap.cy.pan(); if(mindmapNameH1) mindmapNameH1.textContent = mindmapData.name;
if(mindmapDescriptionP) mindmapDescriptionP.textContent = mindmapData.description || "Keine Beschreibung vorhanden.";
window.userMindmap.showAddNoteDialog({ // Cytoscape initialisieren
x: position.x, const cy = cytoscape({
y: position.y container: cyContainer,
elements: mindmapData.elements || [], // Verwende 'elements' aus der API-Antwort
style: [
{
selector: 'node',
style: {
'background-color': 'data(color)',
'label': 'data(label)',
'color': 'data(fontColor)',
'text-valign': 'center',
'text-halign': 'center',
'font-size': 'data(fontSize)',
'width': ele => ele.data('isCenter') ? 60 : (ele.data('size') || 40),
'height': ele => ele.data('isCenter') ? 60 : (ele.data('size') || 40),
'border-width': 2,
'border-color': '#fff',
'shape': 'ellipse',
'text-outline-color': '#555',
'text-outline-width': 1,
}
},
{
selector: 'edge',
style: {
'width': ele => ele.data('strength') ? ele.data('strength') * 1.5 : 2,
'line-color': ele => ele.data('color') || '#9dbaea',
'target-arrow-color': ele => ele.data('color') || '#9dbaea',
'target-arrow-shape': 'triangle',
'curve-style': 'bezier'
}
},
{
selector: 'node:selected',
style: {
'border-color': '#f59e42',
'border-width': 3,
}
}
],
layout: {
name: 'cose',
idealEdgeLength: 100,
nodeOverlap: 20,
refresh: 20,
fit: true,
padding: 30,
randomize: false,
componentSpacing: 100,
nodeRepulsion: 400000,
edgeElasticity: 100,
nestingFactor: 5,
gravity: 80,
numIter: 1000,
initialTemp: 200,
coolingFactor: 0.95,
minTemp: 1.0
}
}); });
}); window.cyInstance = cy; // Für globalen Zugriff falls nötig
// Event-Listener für Layout-Speichern-Button cy.on('tap', 'node', function(evt){
document.getElementById('save-layout-btn').addEventListener('click', function() { const node = evt.target;
window.userMindmap.saveLayout(); if (nodeDescription) nodeDescription.textContent = node.data('description') || 'Keine Beschreibung für diesen Knoten.';
});
}); if (connectedNodesContainer) {
connectedNodesContainer.innerHTML = '';
const connected = node.connectedEdges().otherNodes();
if (connected.length > 0) {
connected.forEach(cn => {
const link = document.createElement('span');
link.className = 'node-link';
link.textContent = cn.data('label');
link.style.backgroundColor = cn.data('color') || '#60a5fa';
link.addEventListener('click', () => {
cy.center(cn);
cn.select();
// Info Panel für den geklickten verbundenen Knoten aktualisieren
if (nodeDescription) nodeDescription.textContent = cn.data('description') || 'Keine Beschreibung für diesen Knoten.';
// Rekursiv verbundene Knoten des neu ausgewählten Knotens anzeigen (optional)
});
connectedNodesContainer.appendChild(link);
});
} else {
connectedNodesContainer.innerHTML = '<p class="opacity-70 text-sm">Keine direkten Verbindungen.</p>';
}
}
if (nodeInfoPanel) nodeInfoPanel.classList.add('visible');
});
cy.on('tap', function(evt){
if(evt.target === cy){ // Klick auf Hintergrund
if (nodeInfoPanel) nodeInfoPanel.classList.remove('visible');
}
});
// Toolbar-Buttons
document.getElementById('fit-btn')?.addEventListener('click', () => cy.fit(null, 50));
document.getElementById('reset-btn')?.addEventListener('click', () => cy.layout({name: 'cose', animate:true}).run());
let labelsVisible = true;
document.getElementById('toggle-labels-btn')?.addEventListener('click', () => {
labelsVisible = !labelsVisible;
cy.style().selector('node').style({'text-opacity': labelsVisible ? 1 : 0}).update();
});
// Notizfunktion implementieren
document.getElementById('add-note-btn').addEventListener('click', function() {
const selectedNodes = cy.$('node:selected');
if (selectedNodes.length === 0) {
showUINotification('Bitte wählen Sie einen Knoten aus, um eine Notiz hinzuzufügen.', 'info');
return;
}
const nodeId = selectedNodes[0].id();
// Notizen für den ausgewählten Knoten laden
fetch(`/api/mindmap/node/${nodeId}/notes`)
.then(response => response.json())
.then(data => {
if (data.success) {
// Notiz-Dialog anzeigen
showNoteDialog(nodeId, data.notes || '', data.color_code || '#FFF59D');
} else {
showUINotification('Fehler beim Laden der Notizen: ' + data.message, 'error');
}
})
.catch(error => {
console.error('Fehler beim Laden der Notizen:', error);
showUINotification('Fehler beim Laden der Notizen', 'error');
});
});
// Layout speichern implementieren
document.getElementById('save-layout-btn').addEventListener('click', function() {
const mindmapId = parseInt("{{ mindmap.id }}");
const nodes = [];
// Sammle die Positionen aller Knoten
cy.nodes().forEach(node => {
nodes.push({
id: node.id(),
x: node.position().x,
y: node.position().y
});
});
// Layout speichern
fetch(`/api/mindmap/${mindmapId}/layout`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
},
body: JSON.stringify({ nodes: nodes })
})
.then(response => response.json())
.then(data => {
if (data.success) {
showUINotification('Layout erfolgreich gespeichert', 'success');
} else {
showUINotification('Fehler beim Speichern des Layouts: ' + data.message, 'error');
}
})
.catch(error => {
console.error('Fehler beim Speichern des Layouts:', error);
showUINotification('Fehler beim Speichern des Layouts', 'error');
});
});
// Öffentliche Mindmap anzeigen/verbergen
document.getElementById('toggle-public-mindmap-btn').addEventListener('click', function() {
const container = document.getElementById('public-mindmap-container');
if (container.classList.contains('hidden')) {
container.classList.remove('hidden');
this.querySelector('span').textContent = 'Öffentliche Mindmap verbergen';
loadPublicMindmap();
} else {
container.classList.add('hidden');
this.querySelector('span').textContent = 'Öffentliche Mindmap';
}
});
// Funktion zum Anzeigen des Notiz-Dialogs
function showNoteDialog(nodeId, noteContent, colorCode) {
// Dialog erstellen
const overlay = document.createElement('div');
overlay.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50';
overlay.id = 'note-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">Notizen bearbeiten</h3>
<textarea id="note-content" class="w-full h-40 p-2 border rounded-lg mb-4 dark:bg-gray-700 dark:border-gray-600 dark:text-white">${noteContent}</textarea>
<div class="flex items-center mb-4">
<label class="mr-2">Farbe:</label>
<input type="color" id="note-color" value="${colorCode}" class="cursor-pointer">
</div>
<div class="flex justify-end gap-2">
<button id="cancel-note" 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="save-note" class="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg">Speichern</button>
</div>
</div>
`;
document.body.appendChild(overlay);
// Event-Listener für Buttons
document.getElementById('cancel-note').addEventListener('click', function() {
document.getElementById('note-dialog-overlay').remove();
});
document.getElementById('save-note').addEventListener('click', function() {
const noteContent = document.getElementById('note-content').value;
const colorCode = document.getElementById('note-color').value;
// Notizen speichern
fetch(`/api/mindmap/node/${nodeId}/notes`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
},
body: JSON.stringify({
notes: noteContent,
color_code: colorCode
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showUINotification('Notizen erfolgreich gespeichert', 'success');
document.getElementById('note-dialog-overlay').remove();
// Knoten visuell markieren, der eine Notiz hat
if (noteContent.trim() !== '') {
cy.$id(nodeId).addClass('has-note');
// Stil für Knoten mit Notizen hinzufügen, falls noch nicht vorhanden
if (!cy.style().selector('node.has-note').style('border-color')) {
cy.style()
.selector('node.has-note')
.style({
'border-color': colorCode,
'border-width': 3
})
.update();
}
} else {
cy.$id(nodeId).removeClass('has-note');
}
} else {
showUINotification('Fehler beim Speichern der Notizen: ' + data.message, 'error');
}
})
.catch(error => {
console.error('Fehler beim Speichern der Notizen:', error);
showUINotification('Fehler beim Speichern der Notizen', 'error');
});
});
}
// Funktion zum Laden der öffentlichen Mindmap
function loadPublicMindmap() {
const container = document.getElementById('public-cy');
// Lade-Anzeige
container.innerHTML = '<div class="flex justify-center items-center h-full"><p>Lade öffentliche Mindmap...</p></div>';
// Daten abrufen
fetch('/api/public-mindmap')
.then(response => response.json())
.then(data => {
if (data.success) {
container.innerHTML = '';
// Initialisiere Cytoscape für die öffentliche Mindmap
const publicCy = cytoscape({
container: container,
elements: [],
style: [
{
selector: 'node',
style: {
'background-color': 'data(color_code)',
'label': 'data(name)',
'color': '#FFFFFF',
'text-valign': 'center',
'text-halign': 'center',
'width': '30px',
'height': '30px',
'text-wrap': 'wrap',
'text-max-width': '80px',
'font-size': '10px',
'border-width': '2px',
'border-color': '#FFFFFF',
'text-outline-color': '#555',
'text-outline-width': 1
}
},
{
selector: 'edge',
style: {
'width': 1,
'line-color': '#9CA3AF',
'target-arrow-color': '#9CA3AF',
'target-arrow-shape': 'triangle',
'curve-style': 'bezier'
}
},
{
selector: 'node:selected',
style: {
'border-color': '#F59E0B',
'border-width': '3px'
}
}
],
layout: {
name: 'cose',
padding: 30
}
});
// Elemente zur öffentlichen Mindmap hinzufügen
const elements = [];
// Knoten hinzufügen
data.nodes.forEach(node => {
elements.push({
group: 'nodes',
data: {
id: 'pub_' + node.id,
originalId: node.id,
name: node.name,
description: node.description,
color_code: node.color_code
}
});
});
// Kanten hinzufügen
data.edges.forEach(edge => {
elements.push({
group: 'edges',
data: {
source: 'pub_' + edge.source,
target: 'pub_' + edge.target
}
});
});
publicCy.add(elements);
publicCy.layout({ name: 'cose' }).run();
// Event-Listener für Knoten-Klicks in der öffentlichen Mindmap
publicCy.on('tap', 'node', function(evt) {
const node = evt.target;
const nodeData = node.data();
// Dialog zum Hinzufügen des Knotens anzeigen
showAddNodeDialog(nodeData.originalId, nodeData.name, nodeData.description);
});
} else {
container.innerHTML = '<div class="flex justify-center items-center h-full"><p class="text-red-500">Fehler beim Laden der öffentlichen Mindmap</p></div>';
showUINotification('Fehler beim Laden der öffentlichen Mindmap: ' + data.message, 'error');
}
})
.catch(error => {
console.error('Fehler beim Laden der öffentlichen Mindmap:', error);
container.innerHTML = '<div class="flex justify-center items-center h-full"><p class="text-red-500">Fehler beim Laden der öffentlichen Mindmap</p></div>';
showUINotification('Fehler beim Laden der öffentlichen Mindmap', 'error');
});
}
// Funktion zum Anzeigen des Dialogs zum Hinzufügen eines Knotens
function showAddNodeDialog(nodeId, nodeName, nodeDescription) {
// Dialog erstellen
const overlay = document.createElement('div');
overlay.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50';
overlay.id = 'add-node-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">Knoten zu Ihrer Mindmap hinzufügen</h3>
<div class="mb-4">
<p class="font-semibold">${nodeName}</p>
<p class="text-sm text-gray-600 dark:text-gray-400">${nodeDescription || 'Keine Beschreibung'}</p>
</div>
<div class="mb-4">
<label class="block mb-2">Mit Knoten verbinden (optional):</label>
<select id="parent-node-select" class="w-full p-2 border rounded-lg dark:bg-gray-700 dark:border-gray-600 dark:text-white">
<option value="">Nicht verbinden</option>
</select>
</div>
<div class="flex justify-end gap-2">
<button id="cancel-add-node" 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-add-node" class="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg">Hinzufügen</button>
</div>
</div>
`;
document.body.appendChild(overlay);
// Fülle den Parent-Node-Selector mit vorhandenen Knoten
const parentSelect = document.getElementById('parent-node-select');
cy.nodes().forEach(node => {
const option = document.createElement('option');
option.value = node.id();
option.textContent = node.data('label');
parentSelect.appendChild(option);
});
// Event-Listener für Buttons
document.getElementById('cancel-add-node').addEventListener('click', function() {
document.getElementById('add-node-dialog-overlay').remove();
});
document.getElementById('confirm-add-node').addEventListener('click', function() {
const parentNodeId = document.getElementById('parent-node-select').value;
const mindmapId = parseInt("{{ mindmap.id }}");
// Generiere Startposition basierend auf Parent oder Zufallsposition
let position = { x: 0, y: 0 };
if (parentNodeId) {
const parentNode = cy.$id(parentNodeId);
if (parentNode) {
// Positioniere in der Nähe des Elternknotens
position = {
x: parentNode.position('x') + Math.random() * 100 - 50,
y: parentNode.position('y') + Math.random() * 100 - 50
};
}
} else {
// Zufällige Position im sichtbaren Bereich
const extent = cy.extent();
position = {
x: extent.x1 + Math.random() * (extent.x2 - extent.x1),
y: extent.y1 + Math.random() * (extent.y2 - extent.y1)
};
}
// Knoten zur Mindmap hinzufügen
fetch(`/api/mindmap/${mindmapId}/add_node`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
},
body: JSON.stringify({
node_id: nodeId,
parent_id: parentNodeId || null,
x: position.x,
y: position.y
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showUINotification('Knoten erfolgreich hinzugefügt', 'success');
document.getElementById('add-node-dialog-overlay').remove();
// Aktualisiere die Mindmap (neu laden oder Knoten hinzufügen)
// Option 1: Seite neu laden
// window.location.reload();
// Option 2: Knoten dynamisch hinzufügen (eleganter)
fetch(`/api/mindmap/node/${nodeId}`)
.then(response => response.json())
.then(nodeData => {
// Füge den neuen Knoten zur Anzeige hinzu
cy.add({
group: 'nodes',
data: {
id: nodeId,
label: nodeData.name,
description: nodeData.description,
color: nodeData.color_code,
fontColor: '#FFFFFF'
},
position: position
});
// Füge auch die Kante hinzu, falls ein Elternknoten ausgewählt wurde
if (parentNodeId) {
cy.add({
group: 'edges',
data: {
source: parentNodeId,
target: nodeId
}
});
}
// Wähle den neuen Knoten aus
cy.$id(nodeId).select();
})
.catch(error => {
console.error('Fehler beim Laden der Knotendaten:', error);
// Fallback: Seite neu laden
window.location.reload();
});
} else {
showUINotification('Fehler beim Hinzufügen des Knotens: ' + (data.message || data.error), 'error');
}
})
.catch(error => {
console.error('Fehler beim Hinzufügen des Knotens:', error);
showUINotification('Fehler beim Hinzufügen des Knotens', 'error');
});
});
}
} else {
// Fallback, falls mindmapData null ist
if(mindmapNameH1) mindmapNameH1.textContent = "Mindmap nicht gefunden";
cyContainer.innerHTML = '<p class="text-center text-red-500 p-10">Die angeforderte Mindmap konnte nicht geladen werden.</p>';
}
}); // End of DOMContentLoaded
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -11,19 +11,33 @@ import importlib.util
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, parent_dir) sys.path.insert(0, parent_dir)
from app import app, db_path # Direkt den Datenbankpfad berechnen, statt ihn aus app.py zu importieren
def get_db_path():
"""Berechnet den absoluten Pfad zur Datenbank"""
basedir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
return os.path.join(basedir, 'database', 'systades.db')
# Import models direkt
from models import db from models import db
def ensure_db_dir(): def ensure_db_dir():
"""Make sure the database directory exists.""" """Make sure the database directory exists."""
db_path = get_db_path()
os.makedirs(os.path.dirname(db_path), exist_ok=True) os.makedirs(os.path.dirname(db_path), exist_ok=True)
def fix_database_schema(): def fix_database_schema():
"""Fix the database schema by adding missing columns.""" """Fix the database schema by adding missing columns."""
# Import Flask-App erst innerhalb der Funktion
from flask import Flask
from app import app
with app.app_context(): with app.app_context():
# Ensure directory exists # Ensure directory exists
ensure_db_dir() ensure_db_dir()
# Get database path
db_path = get_db_path()
# Check if database exists, create tables if needed # Check if database exists, create tables if needed
if not os.path.exists(db_path): if not os.path.exists(db_path):
print("Database doesn't exist. Creating all tables from scratch...") print("Database doesn't exist. Creating all tables from scratch...")