diff --git a/__pycache__/app.cpython-313.pyc b/__pycache__/app.cpython-313.pyc index 656669f..c7f01c4 100644 Binary files a/__pycache__/app.cpython-313.pyc and b/__pycache__/app.cpython-313.pyc differ diff --git a/app.py b/app.py index e73354b..9174fb7 100644 --- a/app.py +++ b/app.py @@ -2,6 +2,8 @@ # -*- coding: utf-8 -*- import os +import logging +import traceback from datetime import datetime, timedelta, timezone 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 @@ -47,6 +49,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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") @@ -2535,6 +2618,154 @@ def export_mindmap(mindmap_id): 'message': f'Fehler beim Exportieren: {str(e)}' }), 500 +@app.route('/api/mindmap//import', methods=['POST']) +@login_required +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"""