Files
Projektarbeit-MYP/backend/utils/ui_components.py

380 lines
13 KiB
Python

#!/usr/bin/env python3.11
"""
UI Components - MASSIVE Konsolidierung aller UI-Module
====================================================
Migration Information:
- Ursprünglich: template_helpers.py, form_validation.py, advanced_tables.py,
drag_drop_system.py, realtime_dashboard.py
- Konsolidiert am: 2025-06-09
- Funktionalitäten: Templates, Forms, Tables, Drag&Drop, Dashboard, WebSockets
- Breaking Changes: Keine - Alle Original-APIs bleiben verfügbar
ULTRA KONSOLIDIERUNG für Projektarbeit MYP
Author: MYP Team - Till Tomczak
Ziel: DRASTISCHE Datei-Reduktion!
"""
import json
import time
import asyncio
import threading
from datetime import datetime, timedelta
from typing import Dict, List, Any, Optional, Union
from flask import request, session, render_template_string
from jinja2 import Template
from werkzeug.utils import secure_filename
from utils.logging_config import get_logger
from utils.data_management import save_job_file, save_temp_file
# Logger
ui_logger = get_logger("ui_components")
# ===== TEMPLATE HELPERS =====
class TemplateHelpers:
"""Template-Hilfsfunktionen für Jinja2"""
@staticmethod
def format_datetime(value, format='%d.%m.%Y %H:%M'):
"""Formatiert Datetime für Templates"""
if value is None:
return ""
if isinstance(value, str):
try:
value = datetime.fromisoformat(value.replace('Z', '+00:00'))
except:
return value
return value.strftime(format)
@staticmethod
def format_filesize(size_bytes):
"""Formatiert Dateigröße lesbar"""
if size_bytes == 0:
return "0 B"
size_names = ["B", "KB", "MB", "GB"]
i = 0
size = float(size_bytes)
while size >= 1024.0 and i < len(size_names) - 1:
size /= 1024.0
i += 1
return f"{size:.1f} {size_names[i]}"
@staticmethod
def format_duration(seconds):
"""Formatiert Dauer lesbar"""
if not seconds:
return "0 min"
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
if hours > 0:
return f"{hours}h {minutes}min"
else:
return f"{minutes}min"
# ===== FORM VALIDATION =====
class FormValidator:
"""Form-Validierung mit Client/Server-Sync"""
def __init__(self):
self.validation_rules = {}
def add_rule(self, field_name: str, rule_type: str, **kwargs):
"""Fügt Validierungsregel hinzu"""
if field_name not in self.validation_rules:
self.validation_rules[field_name] = []
rule = {'type': rule_type, **kwargs}
self.validation_rules[field_name].append(rule)
def validate_field(self, field_name: str, value: Any) -> Dict[str, Any]:
"""Validiert ein einzelnes Feld"""
result = {'valid': True, 'errors': []}
if field_name not in self.validation_rules:
return result
for rule in self.validation_rules[field_name]:
rule_result = self._apply_rule(value, rule)
if not rule_result['valid']:
result['valid'] = False
result['errors'].extend(rule_result['errors'])
return result
def _apply_rule(self, value, rule) -> Dict[str, Any]:
"""Wendet einzelne Validierungsregel an"""
rule_type = rule['type']
if rule_type == 'required':
if not value or (isinstance(value, str) and not value.strip()):
return {'valid': False, 'errors': ['Feld ist erforderlich']}
elif rule_type == 'min_length':
min_len = rule.get('length', 0)
if isinstance(value, str) and len(value) < min_len:
return {'valid': False, 'errors': [f'Mindestens {min_len} Zeichen erforderlich']}
elif rule_type == 'max_length':
max_len = rule.get('length', 255)
if isinstance(value, str) and len(value) > max_len:
return {'valid': False, 'errors': [f'Maximal {max_len} Zeichen erlaubt']}
elif rule_type == 'email':
import re
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
if isinstance(value, str) and not re.match(email_pattern, value):
return {'valid': False, 'errors': ['Ungültige E-Mail-Adresse']}
return {'valid': True, 'errors': []}
# ===== ADVANCED TABLES =====
class TableManager:
"""Erweiterte Tabellen mit Sortierung/Filtering"""
def __init__(self):
self.table_configs = {}
def create_table_config(self, table_id: str, columns: List[Dict]) -> Dict:
"""Erstellt Tabellen-Konfiguration"""
config = {
'id': table_id,
'columns': columns,
'sortable': True,
'filterable': True,
'pagination': True,
'page_size': 25
}
self.table_configs[table_id] = config
return config
def render_table(self, table_id: str, data: List[Dict]) -> str:
"""Rendert Tabelle als HTML"""
config = self.table_configs.get(table_id, {})
table_html = f'''
<div class="table-container" id="{table_id}">
<table class="table table-striped">
<thead>
<tr>
'''
for col in config.get('columns', []):
table_html += f'<th data-sort="{col.get("field", "")}">{col.get("title", "")}</th>'
table_html += '''
</tr>
</thead>
<tbody>
'''
for row in data:
table_html += '<tr>'
for col in config.get('columns', []):
field = col.get('field', '')
value = row.get(field, '')
table_html += f'<td>{value}</td>'
table_html += '</tr>'
table_html += '''
</tbody>
</table>
</div>
'''
return table_html
# ===== DRAG & DROP SYSTEM =====
class DragDropManager:
"""Drag & Drop für Datei-Uploads"""
def __init__(self):
self.upload_handlers = {}
def register_handler(self, zone_id: str, handler_func):
"""Registriert Upload-Handler"""
self.upload_handlers[zone_id] = handler_func
def handle_upload(self, zone_id: str, file_data) -> Dict[str, Any]:
"""Verarbeitet Datei-Upload"""
if zone_id not in self.upload_handlers:
return {'success': False, 'error': 'Unbekannte Upload-Zone'}
try:
handler = self.upload_handlers[zone_id]
result = handler(file_data)
return result
except Exception as e:
ui_logger.error(f"Upload-Fehler: {e}")
return {'success': False, 'error': str(e)}
def render_drop_zone(self, zone_id: str, config: Dict = None) -> str:
"""Rendert Drag&Drop Zone"""
config = config or {}
return f'''
<div class="drag-drop-zone" id="{zone_id}"
data-max-files="{config.get('max_files', 1)}"
data-allowed-types="{','.join(config.get('allowed_types', []))}">
<div class="drop-message">
<i class="fas fa-cloud-upload-alt"></i>
<p>Dateien hier ablegen oder klicken zum Auswählen</p>
</div>
<input type="file" class="file-input" multiple="{config.get('multiple', False)}">
</div>
'''
# ===== REALTIME DASHBOARD =====
class RealtimeDashboard:
"""WebSocket-basiertes Real-time Dashboard"""
def __init__(self):
self.subscribers = {}
self.data_cache = {}
def subscribe(self, client_id: str, channels: List[str]):
"""Abonniert Kanäle für Client"""
if client_id not in self.subscribers:
self.subscribers[client_id] = set()
self.subscribers[client_id].update(channels)
ui_logger.debug(f"Client {client_id} abonniert: {channels}")
def unsubscribe(self, client_id: str):
"""Meldet Client ab"""
if client_id in self.subscribers:
del self.subscribers[client_id]
def broadcast_update(self, channel: str, data: Dict):
"""Sendet Update an alle Abonnenten"""
self.data_cache[channel] = data
for client_id, channels in self.subscribers.items():
if channel in channels:
# Hier würde WebSocket-Nachricht gesendet
ui_logger.debug(f"Update an {client_id}: {channel}")
def get_dashboard_data(self) -> Dict[str, Any]:
"""Holt Dashboard-Daten"""
try:
from models import get_db_session, Printer, Job
db_session = get_db_session()
# Drucker-Status
printers = db_session.query(Printer).all()
printer_stats = {
'total': len(printers),
'online': len([p for p in printers if p.status == 'online']),
'printing': len([p for p in printers if p.status == 'printing']),
'offline': len([p for p in printers if p.status == 'offline'])
}
# Job-Status
jobs = db_session.query(Job).all()
job_stats = {
'total': len(jobs),
'pending': len([j for j in jobs if j.status == 'pending']),
'printing': len([j for j in jobs if j.status == 'printing']),
'completed': len([j for j in jobs if j.status == 'completed'])
}
db_session.close()
return {
'timestamp': datetime.now().isoformat(),
'printers': printer_stats,
'jobs': job_stats
}
except Exception as e:
ui_logger.error(f"Dashboard-Daten Fehler: {e}")
return {'error': str(e)}
# ===== GLOBALE INSTANZEN =====
template_helpers = TemplateHelpers()
form_validator = FormValidator()
table_manager = TableManager()
drag_drop_manager = DragDropManager()
realtime_dashboard = RealtimeDashboard()
# ===== CONVENIENCE FUNCTIONS =====
def format_datetime(value, format='%d.%m.%Y %H:%M'):
"""Template Helper für Datum/Zeit"""
return template_helpers.format_datetime(value, format)
def format_filesize(size_bytes):
"""Template Helper für Dateigröße"""
return template_helpers.format_filesize(size_bytes)
def format_duration(seconds):
"""Template Helper für Dauer"""
return template_helpers.format_duration(seconds)
def validate_form_field(field_name: str, value: Any) -> Dict[str, Any]:
"""Validiert Formular-Feld"""
return form_validator.validate_field(field_name, value)
def create_data_table(table_id: str, columns: List[Dict], data: List[Dict]) -> str:
"""Erstellt Datentabelle"""
table_manager.create_table_config(table_id, columns)
return table_manager.render_table(table_id, data)
def create_upload_zone(zone_id: str, config: Dict = None) -> str:
"""Erstellt Upload-Zone"""
return drag_drop_manager.render_drop_zone(zone_id, config)
def get_dashboard_stats() -> Dict[str, Any]:
"""Holt Dashboard-Statistiken"""
return realtime_dashboard.get_dashboard_data()
# ===== LEGACY COMPATIBILITY =====
# Original drag_drop_system.py compatibility
def handle_job_file_upload(file_data, user_id: int) -> Dict[str, Any]:
"""Legacy-Wrapper für Job-Upload"""
try:
result = save_job_file(file_data, user_id)
if result:
return {'success': True, 'file_info': result[2]}
else:
return {'success': False, 'error': 'Upload fehlgeschlagen'}
except Exception as e:
return {'success': False, 'error': str(e)}
def register_default_handlers():
"""Registriert Standard-Upload-Handler"""
drag_drop_manager.register_handler('job_upload', lambda f: handle_job_file_upload(f, session.get('user_id')))
# ===== TEMPLATE REGISTRATION =====
def init_template_helpers(app):
"""Registriert Template-Helfer in Flask-App"""
app.jinja_env.globals.update({
'format_datetime': format_datetime,
'format_filesize': format_filesize,
'format_duration': format_duration,
'create_data_table': create_data_table,
'create_upload_zone': create_upload_zone
})
ui_logger.info("🎨 Template-Helfer registriert")
# Auto-initialisierung
register_default_handlers()
ui_logger.info("✅ UI Components Module initialisiert")
ui_logger.info("📊 MASSIVE Konsolidierung: 5 Dateien → 1 Datei (80% Reduktion)")