📝 Commit Details:
This commit is contained in:
BIN
backend/blueprints/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
backend/blueprints/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/blueprints/__pycache__/auth.cpython-313.pyc
Normal file
BIN
backend/blueprints/__pycache__/auth.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/blueprints/__pycache__/calendar.cpython-311.pyc
Normal file
BIN
backend/blueprints/__pycache__/calendar.cpython-311.pyc
Normal file
Binary file not shown.
BIN
backend/blueprints/__pycache__/calendar.cpython-313.pyc
Normal file
BIN
backend/blueprints/__pycache__/calendar.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/blueprints/__pycache__/guest.cpython-311.pyc
Normal file
BIN
backend/blueprints/__pycache__/guest.cpython-311.pyc
Normal file
Binary file not shown.
BIN
backend/blueprints/__pycache__/guest.cpython-313.pyc
Normal file
BIN
backend/blueprints/__pycache__/guest.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/blueprints/__pycache__/kiosk_control.cpython-313.pyc
Normal file
BIN
backend/blueprints/__pycache__/kiosk_control.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/blueprints/__pycache__/printers.cpython-311.pyc
Normal file
BIN
backend/blueprints/__pycache__/printers.cpython-311.pyc
Normal file
Binary file not shown.
BIN
backend/blueprints/__pycache__/printers.cpython-313.pyc
Normal file
BIN
backend/blueprints/__pycache__/printers.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/blueprints/__pycache__/ticket_system.cpython-313.pyc
Normal file
BIN
backend/blueprints/__pycache__/ticket_system.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/blueprints/__pycache__/user.cpython-313.pyc
Normal file
BIN
backend/blueprints/__pycache__/user.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/blueprints/__pycache__/users.cpython-311.pyc
Normal file
BIN
backend/blueprints/__pycache__/users.cpython-311.pyc
Normal file
Binary file not shown.
BIN
backend/blueprints/__pycache__/users.cpython-313.pyc
Normal file
BIN
backend/blueprints/__pycache__/users.cpython-313.pyc
Normal file
Binary file not shown.
550
backend/blueprints/calendar.py
Normal file
550
backend/blueprints/calendar.py
Normal file
@ -0,0 +1,550 @@
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from flask import Blueprint, render_template, request, jsonify, redirect, url_for, abort
|
||||
from flask_login import current_user, login_required
|
||||
from sqlalchemy import and_, or_, func
|
||||
|
||||
from models import Job, Printer, User, UserPermission, get_cached_session
|
||||
from utils.logging_config import get_logger
|
||||
|
||||
calendar_blueprint = Blueprint('calendar', __name__)
|
||||
logger = get_logger("calendar")
|
||||
|
||||
def can_edit_events(user):
|
||||
"""Prüft, ob ein Benutzer Kalendereinträge bearbeiten darf."""
|
||||
if user.is_admin:
|
||||
return True
|
||||
|
||||
with get_cached_session() as db_session:
|
||||
permission = db_session.query(UserPermission).filter_by(user_id=user.id).first()
|
||||
if not permission:
|
||||
return False
|
||||
return permission.can_approve_jobs
|
||||
|
||||
def get_smart_printer_assignment(start_date, end_date, priority="normal", db_session=None):
|
||||
"""
|
||||
Intelligente Druckerzuweisung basierend auf verschiedenen Faktoren.
|
||||
|
||||
Args:
|
||||
start_date: Startzeit des Jobs
|
||||
end_date: Endzeit des Jobs
|
||||
priority: Prioritätsstufe ('urgent', 'high', 'normal', 'low')
|
||||
db_session: Datenbankverbindung
|
||||
|
||||
Returns:
|
||||
printer_id: ID des empfohlenen Druckers oder None
|
||||
"""
|
||||
if not db_session:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Verfügbare Drucker ermitteln
|
||||
available_printers = db_session.query(Printer).filter(
|
||||
Printer.active == True
|
||||
).all()
|
||||
|
||||
if not available_printers:
|
||||
logger.warning("Keine aktiven Drucker für automatische Zuweisung gefunden")
|
||||
return None
|
||||
|
||||
printer_scores = []
|
||||
|
||||
for printer in available_printers:
|
||||
score = 0
|
||||
|
||||
# 1. Verfügbarkeit prüfen - Jobs im gleichen Zeitraum
|
||||
conflicting_jobs = db_session.query(Job).filter(
|
||||
Job.printer_id == printer.id,
|
||||
Job.status.in_(["scheduled", "running"]),
|
||||
or_(
|
||||
and_(Job.start_at >= start_date, Job.start_at < end_date),
|
||||
and_(Job.end_at > start_date, Job.end_at <= end_date),
|
||||
and_(Job.start_at <= start_date, Job.end_at >= end_date)
|
||||
)
|
||||
).count()
|
||||
|
||||
if conflicting_jobs > 0:
|
||||
continue # Drucker ist nicht verfügbar
|
||||
|
||||
score += 100 # Grundpunkte für Verfügbarkeit
|
||||
|
||||
# 2. Auslastung in den letzten 24 Stunden bewerten
|
||||
last_24h = datetime.now() - timedelta(hours=24)
|
||||
recent_jobs = db_session.query(Job).filter(
|
||||
Job.printer_id == printer.id,
|
||||
Job.start_at >= last_24h,
|
||||
Job.status.in_(["scheduled", "running", "finished"])
|
||||
).count()
|
||||
|
||||
# Weniger Auslastung = höhere Punktzahl
|
||||
score += max(0, 50 - (recent_jobs * 10))
|
||||
|
||||
# 3. Prioritätsbasierte Zuweisung
|
||||
if priority == "urgent":
|
||||
# Für dringende Jobs: Express-Drucker bevorzugen
|
||||
if "express" in printer.name.lower() or "schnell" in printer.name.lower():
|
||||
score += 30
|
||||
elif priority == "high":
|
||||
# Für hohe Priorität: Weniger belastete Drucker
|
||||
if recent_jobs <= 2:
|
||||
score += 20
|
||||
|
||||
# 4. Zeitfenster-basierte Zuweisung
|
||||
start_hour = start_date.hour
|
||||
if start_hour >= 18 or start_hour <= 6: # Nachtschicht
|
||||
if "nacht" in printer.name.lower() or printer.location and "c" in printer.location.lower():
|
||||
score += 25
|
||||
elif start_hour >= 8 and start_hour <= 17: # Tagschicht
|
||||
if "tag" in printer.name.lower() or printer.location and "a" in printer.location.lower():
|
||||
score += 15
|
||||
|
||||
# 5. Standort-basierte Bewertung (Round-Robin ähnlich)
|
||||
if printer.location:
|
||||
location_penalty = hash(printer.location) % 10 # Verteilung basierend auf Standort
|
||||
score += location_penalty
|
||||
|
||||
# 6. Druckerdauer-Eignung
|
||||
job_duration_hours = (end_date - start_date).total_seconds() / 3600
|
||||
if job_duration_hours > 8: # Lange Jobs
|
||||
if "langzeit" in printer.name.lower() or "marathon" in printer.name.lower():
|
||||
score += 20
|
||||
elif job_duration_hours <= 2: # Kurze Jobs
|
||||
if "express" in printer.name.lower() or "schnell" in printer.name.lower():
|
||||
score += 15
|
||||
|
||||
# 7. Wartungszyklen berücksichtigen
|
||||
# Neuere Drucker (falls last_maintenance_date verfügbar) bevorzugen
|
||||
# TODO: Implementierung abhängig von Printer-Model-Erweiterungen
|
||||
|
||||
printer_scores.append({
|
||||
'printer': printer,
|
||||
'score': score,
|
||||
'conflicts': conflicting_jobs,
|
||||
'recent_load': recent_jobs
|
||||
})
|
||||
|
||||
# Nach Punktzahl sortieren und besten Drucker auswählen
|
||||
if not printer_scores:
|
||||
logger.warning("Keine verfügbaren Drucker für den gewünschten Zeitraum gefunden")
|
||||
return None
|
||||
|
||||
printer_scores.sort(key=lambda x: x['score'], reverse=True)
|
||||
best_printer = printer_scores[0]
|
||||
|
||||
logger.info(f"Automatische Druckerzuweisung: {best_printer['printer'].name} "
|
||||
f"(Score: {best_printer['score']}, Load: {best_printer['recent_load']})")
|
||||
|
||||
return best_printer['printer'].id
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei automatischer Druckerzuweisung: {str(e)}")
|
||||
return None
|
||||
|
||||
@calendar_blueprint.route('/calendar', methods=['GET'])
|
||||
@login_required
|
||||
def calendar_view():
|
||||
"""Kalender-Ansicht anzeigen."""
|
||||
can_edit = can_edit_events(current_user)
|
||||
|
||||
with get_cached_session() as db_session:
|
||||
printers = db_session.query(Printer).filter_by(active=True).all()
|
||||
|
||||
return render_template('calendar.html',
|
||||
printers=printers,
|
||||
can_edit=can_edit)
|
||||
|
||||
@calendar_blueprint.route('/api/calendar', methods=['GET'])
|
||||
@login_required
|
||||
def api_get_calendar_events():
|
||||
"""Kalendereinträge als JSON für FullCalendar zurückgeben."""
|
||||
try:
|
||||
# Datumsbereich aus Anfrage
|
||||
start_str = request.args.get('from')
|
||||
end_str = request.args.get('to')
|
||||
|
||||
if not start_str or not end_str:
|
||||
# Standardmäßig eine Woche anzeigen
|
||||
start_date = datetime.now()
|
||||
end_date = start_date + timedelta(days=7)
|
||||
else:
|
||||
try:
|
||||
start_date = datetime.fromisoformat(start_str)
|
||||
end_date = datetime.fromisoformat(end_str)
|
||||
except ValueError:
|
||||
return jsonify({"error": "Ungültiges Datumsformat"}), 400
|
||||
|
||||
# Optional: Filter nach Druckern
|
||||
printer_id = request.args.get('printer_id')
|
||||
|
||||
with get_cached_session() as db_session:
|
||||
# Jobs im angegebenen Zeitraum abfragen
|
||||
query = db_session.query(Job).filter(
|
||||
or_(
|
||||
# Jobs, die im Zeitraum beginnen
|
||||
and_(Job.start_at >= start_date, Job.start_at <= end_date),
|
||||
# Jobs, die im Zeitraum enden
|
||||
and_(Job.end_at >= start_date, Job.end_at <= end_date),
|
||||
# Jobs, die den Zeitraum komplett umfassen
|
||||
and_(Job.start_at <= start_date, Job.end_at >= end_date)
|
||||
)
|
||||
)
|
||||
|
||||
if printer_id:
|
||||
query = query.filter(Job.printer_id == printer_id)
|
||||
|
||||
jobs = query.all()
|
||||
|
||||
# Jobs in FullCalendar-Event-Format umwandeln
|
||||
events = []
|
||||
for job in jobs:
|
||||
# Farbe basierend auf Status bestimmen
|
||||
color = "#6B7280" # Grau für pending
|
||||
if job.status == "running":
|
||||
color = "#3B82F6" # Blau für running
|
||||
elif job.status == "finished":
|
||||
color = "#10B981" # Grün für finished
|
||||
elif job.status == "scheduled":
|
||||
color = "#10B981" # Grün für approved
|
||||
elif job.status == "cancelled" or job.status == "failed":
|
||||
color = "#EF4444" # Rot für abgebrochen/fehlgeschlagen
|
||||
|
||||
# Benutzerinformationen laden
|
||||
user = db_session.query(User).filter_by(id=job.user_id).first()
|
||||
user_name = user.name if user else "Unbekannt"
|
||||
|
||||
# Druckerinformationen laden
|
||||
printer = db_session.query(Printer).filter_by(id=job.printer_id).first()
|
||||
printer_name = printer.name if printer else "Unbekannt"
|
||||
|
||||
event = {
|
||||
"id": job.id,
|
||||
"title": job.name,
|
||||
"start": job.start_at.isoformat(),
|
||||
"end": job.end_at.isoformat(),
|
||||
"color": color,
|
||||
"extendedProps": {
|
||||
"status": job.status,
|
||||
"description": job.description,
|
||||
"userName": user_name,
|
||||
"printerId": job.printer_id,
|
||||
"printerName": printer_name
|
||||
}
|
||||
}
|
||||
|
||||
events.append(event)
|
||||
|
||||
return jsonify(events)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Abrufen der Kalendereinträge: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
||||
|
||||
@calendar_blueprint.route('/api/calendar/event', methods=['POST'])
|
||||
@login_required
|
||||
def api_create_calendar_event():
|
||||
"""Neuen Kalendereintrag (Job) erstellen."""
|
||||
# Nur Admins und Benutzer mit can_approve_jobs dürfen Einträge erstellen
|
||||
if not can_edit_events(current_user):
|
||||
return jsonify({"error": "Keine Berechtigung zum Erstellen von Kalendereinträgen"}), 403
|
||||
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({"error": "Keine Daten erhalten"}), 400
|
||||
|
||||
# Pflichtfelder prüfen
|
||||
title = data.get('title')
|
||||
start = data.get('start')
|
||||
end = data.get('end')
|
||||
printer_id = data.get('printerId') # Jetzt optional
|
||||
priority = data.get('priority', 'normal')
|
||||
|
||||
if not all([title, start, end]):
|
||||
return jsonify({"error": "Titel, Start und Ende sind erforderlich"}), 400
|
||||
|
||||
# Datumsfelder konvertieren
|
||||
try:
|
||||
start_date = datetime.fromisoformat(start)
|
||||
end_date = datetime.fromisoformat(end)
|
||||
except ValueError:
|
||||
return jsonify({"error": "Ungültiges Datumsformat"}), 400
|
||||
|
||||
# Dauer in Minuten berechnen
|
||||
duration_minutes = int((end_date - start_date).total_seconds() / 60)
|
||||
|
||||
with get_cached_session() as db_session:
|
||||
# Intelligente Druckerzuweisung falls kein Drucker angegeben
|
||||
if not printer_id:
|
||||
logger.info(f"Automatische Druckerzuweisung wird verwendet für Job '{title}'")
|
||||
printer_id = get_smart_printer_assignment(
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
priority=priority,
|
||||
db_session=db_session
|
||||
)
|
||||
|
||||
if not printer_id:
|
||||
return jsonify({
|
||||
"error": "Keine verfügbaren Drucker für den gewünschten Zeitraum gefunden. "
|
||||
"Bitte wählen Sie einen spezifischen Drucker oder einen anderen Zeitraum."
|
||||
}), 409
|
||||
|
||||
# Drucker prüfen/validieren
|
||||
printer = db_session.query(Printer).filter_by(id=printer_id).first()
|
||||
if not printer:
|
||||
return jsonify({"error": "Drucker nicht gefunden"}), 404
|
||||
|
||||
if not printer.active:
|
||||
return jsonify({"error": f"Drucker '{printer.name}' ist nicht aktiv"}), 400
|
||||
|
||||
# Nochmals Verfügbarkeit prüfen bei automatischer Zuweisung
|
||||
if not data.get('printerId'): # War automatische Zuweisung
|
||||
conflicting_jobs = db_session.query(Job).filter(
|
||||
Job.printer_id == printer_id,
|
||||
Job.status.in_(["scheduled", "running"]),
|
||||
or_(
|
||||
and_(Job.start_at >= start_date, Job.start_at < end_date),
|
||||
and_(Job.end_at > start_date, Job.end_at <= end_date),
|
||||
and_(Job.start_at <= start_date, Job.end_at >= end_date)
|
||||
)
|
||||
).first()
|
||||
|
||||
if conflicting_jobs:
|
||||
return jsonify({
|
||||
"error": f"Automatisch zugewiesener Drucker '{printer.name}' ist nicht mehr verfügbar. "
|
||||
"Bitte wählen Sie einen anderen Zeitraum oder spezifischen Drucker."
|
||||
}), 409
|
||||
|
||||
# Neuen Job erstellen
|
||||
job = Job(
|
||||
name=title,
|
||||
description=data.get('description', ''),
|
||||
user_id=current_user.id,
|
||||
printer_id=printer_id,
|
||||
start_at=start_date,
|
||||
end_at=end_date,
|
||||
status="scheduled",
|
||||
duration_minutes=duration_minutes,
|
||||
owner_id=current_user.id
|
||||
)
|
||||
|
||||
db_session.add(job)
|
||||
db_session.commit()
|
||||
|
||||
assignment_type = "automatisch" if not data.get('printerId') else "manuell"
|
||||
logger.info(f"Neuer Kalendereintrag erstellt: ID {job.id}, Name: {title}, "
|
||||
f"Drucker: {printer.name} ({assignment_type} zugewiesen)")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"id": job.id,
|
||||
"title": job.name,
|
||||
"start": job.start_at.isoformat(),
|
||||
"end": job.end_at.isoformat(),
|
||||
"status": job.status,
|
||||
"printer": {
|
||||
"id": printer.id,
|
||||
"name": printer.name,
|
||||
"location": printer.location,
|
||||
"assignment_type": assignment_type
|
||||
},
|
||||
"message": f"Auftrag erfolgreich erstellt und {assignment_type} dem Drucker '{printer.name}' zugewiesen."
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Erstellen des Kalendereintrags: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
||||
|
||||
@calendar_blueprint.route('/api/calendar/event/<int:event_id>', methods=['PUT'])
|
||||
@login_required
|
||||
def api_update_calendar_event(event_id):
|
||||
"""Kalendereintrag (Job) aktualisieren."""
|
||||
# Nur Admins und Benutzer mit can_approve_jobs dürfen Einträge bearbeiten
|
||||
if not can_edit_events(current_user):
|
||||
return jsonify({"error": "Keine Berechtigung zum Bearbeiten von Kalendereinträgen"}), 403
|
||||
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({"error": "Keine Daten erhalten"}), 400
|
||||
|
||||
with get_cached_session() as db_session:
|
||||
job = db_session.query(Job).filter_by(id=event_id).first()
|
||||
if not job:
|
||||
return jsonify({"error": "Kalendereintrag nicht gefunden"}), 404
|
||||
|
||||
# Felder aktualisieren, die im Request enthalten sind
|
||||
if 'title' in data:
|
||||
job.name = data['title']
|
||||
|
||||
if 'description' in data:
|
||||
job.description = data['description']
|
||||
|
||||
if 'start' in data and 'end' in data:
|
||||
try:
|
||||
start_date = datetime.fromisoformat(data['start'])
|
||||
end_date = datetime.fromisoformat(data['end'])
|
||||
|
||||
job.start_at = start_date
|
||||
job.end_at = end_date
|
||||
job.duration_minutes = int((end_date - start_date).total_seconds() / 60)
|
||||
except ValueError:
|
||||
return jsonify({"error": "Ungültiges Datumsformat"}), 400
|
||||
|
||||
if 'printerId' in data:
|
||||
printer = db_session.query(Printer).filter_by(id=data['printerId']).first()
|
||||
if not printer:
|
||||
return jsonify({"error": "Drucker nicht gefunden"}), 404
|
||||
job.printer_id = data['printerId']
|
||||
|
||||
if 'status' in data:
|
||||
# Status nur ändern, wenn er gültig ist
|
||||
valid_statuses = ["scheduled", "running", "finished", "cancelled"]
|
||||
if data['status'] in valid_statuses:
|
||||
job.status = data['status']
|
||||
|
||||
db_session.commit()
|
||||
|
||||
logger.info(f"Kalendereintrag aktualisiert: ID {job.id}")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"id": job.id,
|
||||
"title": job.name,
|
||||
"start": job.start_at.isoformat(),
|
||||
"end": job.end_at.isoformat(),
|
||||
"status": job.status
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Aktualisieren des Kalendereintrags: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
||||
|
||||
@calendar_blueprint.route('/api/calendar/event/<int:event_id>', methods=['DELETE'])
|
||||
@login_required
|
||||
def api_delete_calendar_event(event_id):
|
||||
"""Kalendereintrag (Job) löschen."""
|
||||
# Nur Admins und Benutzer mit can_approve_jobs dürfen Einträge löschen
|
||||
if not can_edit_events(current_user):
|
||||
return jsonify({"error": "Keine Berechtigung zum Löschen von Kalendereinträgen"}), 403
|
||||
|
||||
try:
|
||||
with get_cached_session() as db_session:
|
||||
job = db_session.query(Job).filter_by(id=event_id).first()
|
||||
if not job:
|
||||
return jsonify({"error": "Kalendereintrag nicht gefunden"}), 404
|
||||
|
||||
db_session.delete(job)
|
||||
db_session.commit()
|
||||
|
||||
logger.info(f"Kalendereintrag gelöscht: ID {event_id}")
|
||||
|
||||
return jsonify({"success": True})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Löschen des Kalendereintrags: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
||||
|
||||
@calendar_blueprint.route('/api/calendar/smart-recommendation', methods=['POST'])
|
||||
@login_required
|
||||
def api_get_smart_recommendation():
|
||||
"""Intelligente Druckerempfehlung für gegebenes Zeitfenster abrufen."""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({"error": "Keine Daten erhalten"}), 400
|
||||
|
||||
start = data.get('start')
|
||||
end = data.get('end')
|
||||
priority = data.get('priority', 'normal')
|
||||
|
||||
if not all([start, end]):
|
||||
return jsonify({"error": "Start und Ende sind erforderlich"}), 400
|
||||
|
||||
# Datumsfelder konvertieren
|
||||
try:
|
||||
start_date = datetime.fromisoformat(start)
|
||||
end_date = datetime.fromisoformat(end)
|
||||
except ValueError:
|
||||
return jsonify({"error": "Ungültiges Datumsformat"}), 400
|
||||
|
||||
with get_cached_session() as db_session:
|
||||
# Empfohlenen Drucker ermitteln
|
||||
recommended_printer_id = get_smart_printer_assignment(
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
priority=priority,
|
||||
db_session=db_session
|
||||
)
|
||||
|
||||
if not recommended_printer_id:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"message": "Keine verfügbaren Drucker für den gewünschten Zeitraum gefunden."
|
||||
})
|
||||
|
||||
# Drucker-Details abrufen
|
||||
printer = db_session.query(Printer).filter_by(id=recommended_printer_id).first()
|
||||
if not printer:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"message": "Empfohlener Drucker nicht mehr verfügbar."
|
||||
})
|
||||
|
||||
# Zusätzliche Statistiken für die Empfehlung berechnen
|
||||
last_24h = datetime.now() - timedelta(hours=24)
|
||||
recent_jobs_count = db_session.query(Job).filter(
|
||||
Job.printer_id == printer.id,
|
||||
Job.start_at >= last_24h,
|
||||
Job.status.in_(["scheduled", "running", "finished"])
|
||||
).count()
|
||||
|
||||
# Verfügbarkeit als Prozentsatz berechnen
|
||||
total_time_slots = 24 # Stunden pro Tag
|
||||
availability_percent = max(0, 100 - (recent_jobs_count * 4)) # Grobe Schätzung
|
||||
|
||||
# Auslastung berechnen
|
||||
utilization_percent = min(100, recent_jobs_count * 8) # Grobe Schätzung
|
||||
|
||||
# Eignung basierend auf Priorität und Zeitfenster bestimmen
|
||||
suitability = "Gut"
|
||||
if priority == "urgent" and ("express" in printer.name.lower() or "schnell" in printer.name.lower()):
|
||||
suitability = "Perfekt"
|
||||
elif priority == "high" and recent_jobs_count <= 2:
|
||||
suitability = "Ausgezeichnet"
|
||||
elif recent_jobs_count == 0:
|
||||
suitability = "Optimal"
|
||||
|
||||
# Begründung generieren
|
||||
reason = f"Optimale Verfügbarkeit und geringe Auslastung im gewählten Zeitraum"
|
||||
|
||||
job_duration_hours = (end_date - start_date).total_seconds() / 3600
|
||||
start_hour = start_date.hour
|
||||
|
||||
if priority == "urgent":
|
||||
reason = "Schnellster verfügbarer Drucker für dringende Aufträge"
|
||||
elif start_hour >= 18 or start_hour <= 6:
|
||||
reason = "Speziell für Nachtschichten optimiert"
|
||||
elif job_duration_hours > 8:
|
||||
reason = "Zuverlässig für lange Druckaufträge"
|
||||
elif job_duration_hours <= 2:
|
||||
reason = "Optimal für schnelle Druckaufträge"
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"recommendation": {
|
||||
"printer_id": printer.id,
|
||||
"printer_name": f"{printer.name}",
|
||||
"location": printer.location or "Haupthalle",
|
||||
"reason": reason,
|
||||
"availability": f"{availability_percent}%",
|
||||
"utilization": f"{utilization_percent}%",
|
||||
"suitability": suitability,
|
||||
"recent_jobs": recent_jobs_count,
|
||||
"priority_optimized": priority in ["urgent", "high"] and suitability in ["Perfekt", "Ausgezeichnet"]
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Abrufen der intelligenten Empfehlung: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
855
backend/blueprints/guest.py
Normal file
855
backend/blueprints/guest.py
Normal file
@ -0,0 +1,855 @@
|
||||
import json
|
||||
import secrets
|
||||
import bcrypt
|
||||
from datetime import datetime, timedelta
|
||||
from flask import Blueprint, render_template, request, jsonify, redirect, url_for, abort, session, flash
|
||||
from flask_login import current_user, login_required
|
||||
from flask_wtf import FlaskForm
|
||||
from flask_wtf.file import FileField, FileAllowed
|
||||
from wtforms import StringField, TextAreaField, IntegerField, SelectField
|
||||
from wtforms.validators import DataRequired, Email, Optional, NumberRange
|
||||
from functools import wraps
|
||||
from sqlalchemy import desc
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
from models import GuestRequest, Job, Printer, User, UserPermission, Notification, get_cached_session
|
||||
from utils.logging_config import get_logger
|
||||
|
||||
guest_blueprint = Blueprint('guest', __name__)
|
||||
logger = get_logger("guest")
|
||||
|
||||
# Flask-WTF Formular für Gastanfragen
|
||||
class GuestRequestForm(FlaskForm):
|
||||
name = StringField('Vollständiger Name', validators=[DataRequired()])
|
||||
email = StringField('E-Mail-Adresse', validators=[DataRequired(), Email()])
|
||||
printer_id = SelectField('Drucker auswählen', coerce=int, validators=[Optional()])
|
||||
duration_min = IntegerField('Geschätzte Dauer (Minuten)',
|
||||
validators=[DataRequired(), NumberRange(min=1, max=1440)],
|
||||
default=60)
|
||||
reason = TextAreaField('Projektbeschreibung', validators=[Optional()])
|
||||
file = FileField('3D-Datei hochladen',
|
||||
validators=[Optional(), FileAllowed(['stl', 'obj', '3mf', 'amf', 'gcode'],
|
||||
'3D-Dateien sind erlaubt!')])
|
||||
|
||||
# Hilfsfunktionen
|
||||
def can_approve_jobs(user_id):
|
||||
"""Prüft, ob ein Benutzer Anfragen genehmigen darf."""
|
||||
with get_cached_session() as db_session:
|
||||
permission = db_session.query(UserPermission).filter_by(user_id=user_id).first()
|
||||
if not permission:
|
||||
return False
|
||||
return permission.can_approve_jobs
|
||||
|
||||
def approver_required(f):
|
||||
"""Decorator zur Prüfung der Genehmigungsberechtigung."""
|
||||
@wraps(f)
|
||||
@login_required
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not can_approve_jobs(current_user.id):
|
||||
abort(403, "Keine Berechtigung zum Genehmigen von Anfragen")
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
# Gast-Routen
|
||||
@guest_blueprint.route('/request', methods=['GET', 'POST'])
|
||||
def guest_request_form():
|
||||
"""Formular für Gastanfragen anzeigen und verarbeiten."""
|
||||
with get_cached_session() as db_session:
|
||||
# Aktive Drucker für SelectField laden
|
||||
printers = db_session.query(Printer).filter_by(active=True).all()
|
||||
|
||||
# Formular erstellen
|
||||
form = GuestRequestForm()
|
||||
|
||||
# Drucker-Optionen für SelectField setzen
|
||||
printer_choices = [(0, 'Keinen spezifischen Drucker auswählen')]
|
||||
printer_choices.extend([(p.id, f"{p.name} ({p.location or 'Kein Standort'})") for p in printers])
|
||||
form.printer_id.choices = printer_choices
|
||||
|
||||
if form.validate_on_submit():
|
||||
try:
|
||||
# Daten aus dem Formular extrahieren
|
||||
name = form.name.data
|
||||
email = form.email.data
|
||||
reason = form.reason.data
|
||||
duration_min = form.duration_min.data
|
||||
printer_id = form.printer_id.data if form.printer_id.data != 0 else None
|
||||
|
||||
# IP-Adresse erfassen
|
||||
author_ip = request.remote_addr
|
||||
|
||||
# Drucker validieren, falls angegeben
|
||||
if printer_id:
|
||||
printer = db_session.query(Printer).filter_by(id=printer_id, active=True).first()
|
||||
if not printer:
|
||||
flash("Ungültiger Drucker ausgewählt.", "error")
|
||||
return render_template('guest_request.html', form=form, printers=printers)
|
||||
|
||||
# Neue Anfrage erstellen
|
||||
guest_request = GuestRequest(
|
||||
name=name,
|
||||
email=email,
|
||||
reason=reason,
|
||||
duration_min=duration_min,
|
||||
printer_id=printer_id,
|
||||
author_ip=author_ip
|
||||
)
|
||||
|
||||
db_session.add(guest_request)
|
||||
db_session.commit()
|
||||
|
||||
# Benachrichtigung für Genehmiger erstellen
|
||||
Notification.create_for_approvers(
|
||||
notification_type="guest_request",
|
||||
payload={
|
||||
"request_id": guest_request.id,
|
||||
"name": guest_request.name,
|
||||
"created_at": guest_request.created_at.isoformat(),
|
||||
"status": guest_request.status
|
||||
}
|
||||
)
|
||||
|
||||
logger.info(f"Neue Gastanfrage erstellt: ID {guest_request.id}, Name: {name}")
|
||||
flash("Ihr Antrag wurde erfolgreich eingereicht!", "success")
|
||||
|
||||
# Weiterleitung zur Status-Seite
|
||||
return redirect(url_for('guest.guest_request_status', request_id=guest_request.id))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Erstellen der Gastanfrage: {str(e)}")
|
||||
flash("Fehler beim Verarbeiten Ihres Antrags. Bitte versuchen Sie es erneut.", "error")
|
||||
|
||||
# Drucker-Liste von der Session trennen für Template-Verwendung
|
||||
db_session.expunge_all()
|
||||
|
||||
return render_template('guest_request.html', form=form, printers=printers)
|
||||
|
||||
@guest_blueprint.route('/start-job', methods=['GET'])
|
||||
def guest_start_job_form():
|
||||
"""Code-Eingabe-Formular für Gäste anzeigen."""
|
||||
return render_template('guest_start_job.html')
|
||||
|
||||
@guest_blueprint.route('/job/<int:job_id>/status', methods=['GET'])
|
||||
def guest_job_status(job_id):
|
||||
"""Job-Status-Seite für Gäste anzeigen."""
|
||||
with get_cached_session() as db_session:
|
||||
# Job mit eager loading des printer-Relationships laden
|
||||
job = db_session.query(Job).options(
|
||||
joinedload(Job.printer),
|
||||
joinedload(Job.user)
|
||||
).filter_by(id=job_id).first()
|
||||
|
||||
if not job:
|
||||
abort(404, "Job nicht gefunden")
|
||||
|
||||
# Zugehörige Gastanfrage finden
|
||||
guest_request = db_session.query(GuestRequest).filter_by(job_id=job_id).first()
|
||||
|
||||
# Objekte explizit von der Session trennen, um sie außerhalb verwenden zu können
|
||||
db_session.expunge(job)
|
||||
if guest_request:
|
||||
db_session.expunge(guest_request)
|
||||
|
||||
return render_template('guest_job_status.html',
|
||||
job=job,
|
||||
guest_request=guest_request)
|
||||
|
||||
@guest_blueprint.route('/requests/overview', methods=['GET'])
|
||||
def guest_requests_overview():
|
||||
"""Öffentliche Übersicht aller Druckanträge mit zensierten persönlichen Daten."""
|
||||
try:
|
||||
with get_cached_session() as db_session:
|
||||
# Alle Gastanfragen mit eager loading des printer-Relationships laden
|
||||
guest_requests = db_session.query(GuestRequest).options(
|
||||
joinedload(GuestRequest.printer)
|
||||
).order_by(desc(GuestRequest.created_at)).all()
|
||||
|
||||
# Daten für Gäste aufbereiten (persönliche Daten zensieren)
|
||||
public_requests = []
|
||||
for req in guest_requests:
|
||||
# Name zensieren: Nur ersten Buchstaben und letzten Buchstaben anzeigen
|
||||
censored_name = "***"
|
||||
if req.name and len(req.name) > 0:
|
||||
if len(req.name) == 1:
|
||||
censored_name = req.name[0] + "***"
|
||||
elif len(req.name) == 2:
|
||||
censored_name = req.name[0] + "***" + req.name[-1]
|
||||
else:
|
||||
censored_name = req.name[0] + "***" + req.name[-1]
|
||||
|
||||
# E-Mail zensieren
|
||||
censored_email = "***@***.***"
|
||||
if req.email and "@" in req.email:
|
||||
email_parts = req.email.split("@")
|
||||
if len(email_parts[0]) > 2:
|
||||
censored_email = email_parts[0][:2] + "***@" + email_parts[1]
|
||||
else:
|
||||
censored_email = "***@" + email_parts[1]
|
||||
|
||||
# Grund zensieren (nur erste 20 Zeichen anzeigen)
|
||||
censored_reason = "***"
|
||||
if req.reason and len(req.reason) > 20:
|
||||
censored_reason = req.reason[:20] + "..."
|
||||
elif req.reason:
|
||||
censored_reason = req.reason
|
||||
|
||||
public_requests.append({
|
||||
"id": req.id,
|
||||
"name": censored_name,
|
||||
"email": censored_email,
|
||||
"reason": censored_reason,
|
||||
"duration_min": req.duration_min,
|
||||
"created_at": req.created_at,
|
||||
"status": req.status,
|
||||
"printer": req.printer.to_dict() if req.printer else None
|
||||
})
|
||||
|
||||
# Objekte explizit von der Session trennen
|
||||
db_session.expunge_all()
|
||||
|
||||
return render_template('guest_requests_overview.html', requests=public_requests)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Laden der öffentlichen Gastanfragen: {str(e)}")
|
||||
return render_template('guest_requests_overview.html', requests=[], error="Fehler beim Laden der Anfragen")
|
||||
|
||||
@guest_blueprint.route('/request/<int:request_id>', methods=['GET'])
|
||||
def guest_request_status(request_id):
|
||||
"""Status einer Gastanfrage anzeigen."""
|
||||
with get_cached_session() as db_session:
|
||||
# Guest Request mit eager loading des printer-Relationships laden
|
||||
guest_request = db_session.query(GuestRequest).options(
|
||||
joinedload(GuestRequest.printer)
|
||||
).filter_by(id=request_id).first()
|
||||
|
||||
if not guest_request:
|
||||
abort(404, "Anfrage nicht gefunden")
|
||||
|
||||
# OTP-Code nur anzeigen, wenn Anfrage genehmigt wurde
|
||||
otp_code = None
|
||||
show_start_link = False
|
||||
|
||||
if guest_request.status == "approved":
|
||||
if not guest_request.otp_code:
|
||||
# OTP generieren falls noch nicht vorhanden
|
||||
otp_code = guest_request.generate_otp()
|
||||
db_session.commit()
|
||||
else:
|
||||
# OTP existiert bereits - prüfen ob noch nicht verwendet
|
||||
show_start_link = guest_request.otp_used_at is None
|
||||
|
||||
# Zugehörigen Job laden, falls vorhanden
|
||||
job = None
|
||||
if guest_request.job_id:
|
||||
job = db_session.query(Job).filter_by(id=guest_request.job_id).first()
|
||||
|
||||
# Objekte explizit von der Session trennen, um sie außerhalb verwenden zu können
|
||||
db_session.expunge(guest_request)
|
||||
if job:
|
||||
db_session.expunge(job)
|
||||
|
||||
return render_template('guest_status.html',
|
||||
request=guest_request,
|
||||
job=job,
|
||||
otp_code=otp_code,
|
||||
show_start_link=show_start_link)
|
||||
|
||||
# API-Endpunkte
|
||||
@guest_blueprint.route('/api/guest/requests', methods=['POST'])
|
||||
def api_create_guest_request():
|
||||
"""Neue Gastanfrage erstellen."""
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({"error": "Keine Daten erhalten"}), 400
|
||||
|
||||
# Pflichtfelder prüfen
|
||||
name = data.get('name')
|
||||
if not name:
|
||||
return jsonify({"error": "Name ist erforderlich"}), 400
|
||||
|
||||
# Optionale Felder
|
||||
email = data.get('email')
|
||||
reason = data.get('reason')
|
||||
duration_min = data.get('duration_min', 60) # Standard: 1 Stunde
|
||||
printer_id = data.get('printer_id')
|
||||
|
||||
# IP-Adresse erfassen
|
||||
author_ip = request.remote_addr
|
||||
|
||||
try:
|
||||
with get_cached_session() as db_session:
|
||||
# Drucker prüfen
|
||||
if printer_id:
|
||||
printer = db_session.query(Printer).filter_by(id=printer_id, active=True).first()
|
||||
if not printer:
|
||||
return jsonify({"error": "Ungültiger Drucker ausgewählt"}), 400
|
||||
|
||||
# Neue Anfrage erstellen
|
||||
guest_request = GuestRequest(
|
||||
name=name,
|
||||
email=email,
|
||||
reason=reason,
|
||||
duration_min=duration_min,
|
||||
printer_id=printer_id,
|
||||
author_ip=author_ip
|
||||
)
|
||||
|
||||
db_session.add(guest_request)
|
||||
db_session.commit()
|
||||
|
||||
# Benachrichtigung für Genehmiger erstellen
|
||||
Notification.create_for_approvers(
|
||||
notification_type="guest_request",
|
||||
payload={
|
||||
"request_id": guest_request.id,
|
||||
"name": guest_request.name,
|
||||
"created_at": guest_request.created_at.isoformat(),
|
||||
"status": guest_request.status
|
||||
}
|
||||
)
|
||||
|
||||
logger.info(f"Neue Gastanfrage erstellt: ID {guest_request.id}, Name: {name}")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"request_id": guest_request.id,
|
||||
"status": guest_request.status,
|
||||
"redirect_url": url_for('guest.guest_request_status', request_id=guest_request.id)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Erstellen der Gastanfrage: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
||||
|
||||
@guest_blueprint.route('/api/guest/start-job', methods=['POST'])
|
||||
def api_start_job_with_code():
|
||||
"""Job mit OTP-Code starten."""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data or 'code' not in data:
|
||||
return jsonify({"error": "Code ist erforderlich"}), 400
|
||||
|
||||
code = data['code'].strip().upper()
|
||||
if len(code) != 6:
|
||||
return jsonify({"error": "Code muss 6 Zeichen lang sein"}), 400
|
||||
|
||||
with get_cached_session() as db_session:
|
||||
# Alle genehmigten Gastanfragen mit OTP-Codes finden
|
||||
guest_requests = db_session.query(GuestRequest).filter(
|
||||
GuestRequest.status == "approved",
|
||||
GuestRequest.otp_code.isnot(None),
|
||||
GuestRequest.otp_used_at.is_(None) # Noch nicht verwendet
|
||||
).all()
|
||||
|
||||
matching_request = None
|
||||
for req in guest_requests:
|
||||
# Code validieren
|
||||
if req.verify_otp(code):
|
||||
matching_request = req
|
||||
break
|
||||
|
||||
if not matching_request:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Ungültiger oder bereits verwendeter Code"
|
||||
}), 400
|
||||
|
||||
# Prüfen ob zugehöriger Job existiert
|
||||
if not matching_request.job_id:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Kein zugehöriger Job gefunden"
|
||||
}), 400
|
||||
|
||||
job = db_session.query(Job).options(
|
||||
joinedload(Job.printer)
|
||||
).filter_by(id=matching_request.job_id).first()
|
||||
|
||||
if not job:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Job nicht gefunden"
|
||||
}), 400
|
||||
|
||||
# Prüfen ob Job noch startbar ist
|
||||
if job.status not in ["scheduled", "waiting_for_printer"]:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": f"Job kann im Status '{job.status}' nicht gestartet werden"
|
||||
}), 400
|
||||
|
||||
# Job starten
|
||||
now = datetime.now()
|
||||
job.status = "running"
|
||||
job.start_at = now
|
||||
job.end_at = now + timedelta(minutes=matching_request.duration_min)
|
||||
job.actual_start_time = now
|
||||
|
||||
# OTP als verwendet markieren
|
||||
matching_request.otp_used_at = now
|
||||
|
||||
# Drucker einschalten (falls implementiert)
|
||||
if job.printer and job.printer.plug_ip:
|
||||
try:
|
||||
from utils.job_scheduler import toggle_plug
|
||||
toggle_plug(job.printer_id, True)
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler beim Einschalten des Druckers: {str(e)}")
|
||||
|
||||
db_session.commit()
|
||||
|
||||
logger.info(f"Job {job.id} mit OTP-Code gestartet für Gastanfrage {matching_request.id}")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"job_id": job.id,
|
||||
"job_name": job.name,
|
||||
"start_time": job.start_at.strftime("%H:%M"),
|
||||
"end_time": job.end_at.strftime("%H:%M"),
|
||||
"duration_minutes": matching_request.duration_min,
|
||||
"printer_name": job.printer.name if job.printer else "Unbekannt",
|
||||
"message": f"Job '{job.name}' erfolgreich gestartet"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Starten des Jobs mit Code: {str(e)}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Fehler beim Starten des Jobs"
|
||||
}), 500
|
||||
|
||||
@guest_blueprint.route('/api/guest/requests/<int:request_id>', methods=['GET'])
|
||||
def api_get_guest_request(request_id):
|
||||
"""Status einer Gastanfrage abrufen."""
|
||||
try:
|
||||
with get_cached_session() as db_session:
|
||||
guest_request = db_session.query(GuestRequest).filter_by(id=request_id).first()
|
||||
if not guest_request:
|
||||
return jsonify({"error": "Anfrage nicht gefunden"}), 404
|
||||
|
||||
# OTP wird nie über die API zurückgegeben
|
||||
response_data = guest_request.to_dict()
|
||||
response_data.pop("otp_code", None)
|
||||
|
||||
return jsonify(response_data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Abrufen der Gastanfrage: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
||||
|
||||
@guest_blueprint.route('/api/guest/job/<int:job_id>/status', methods=['GET'])
|
||||
def api_get_guest_job_status(job_id):
|
||||
"""Job-Status für Gäste abrufen."""
|
||||
try:
|
||||
with get_cached_session() as db_session:
|
||||
# Job mit Drucker-Information laden
|
||||
job = db_session.query(Job).options(
|
||||
joinedload(Job.printer)
|
||||
).filter_by(id=job_id).first()
|
||||
|
||||
if not job:
|
||||
return jsonify({"error": "Job nicht gefunden"}), 404
|
||||
|
||||
# Zugehörige Gastanfrage prüfen
|
||||
guest_request = db_session.query(GuestRequest).filter_by(job_id=job_id).first()
|
||||
if not guest_request:
|
||||
return jsonify({"error": "Kein Gastjob"}), 403
|
||||
|
||||
# Aktuelle Zeit für Berechnungen
|
||||
now = datetime.now()
|
||||
|
||||
# Restzeit berechnen
|
||||
remaining_minutes = 0
|
||||
if job.status == "running" and job.end_at:
|
||||
remaining_seconds = (job.end_at - now).total_seconds()
|
||||
remaining_minutes = max(0, int(remaining_seconds / 60))
|
||||
|
||||
# Fortschritt berechnen
|
||||
progress_percent = 0
|
||||
if job.status == "running" and job.start_at and job.end_at:
|
||||
total_duration = (job.end_at - job.start_at).total_seconds()
|
||||
elapsed_duration = (now - job.start_at).total_seconds()
|
||||
progress_percent = min(100, max(0, int((elapsed_duration / total_duration) * 100)))
|
||||
elif job.status in ["completed", "finished"]:
|
||||
progress_percent = 100
|
||||
|
||||
job_data = {
|
||||
"id": job.id,
|
||||
"name": job.name,
|
||||
"status": job.status,
|
||||
"start_at": job.start_at.isoformat() if job.start_at else None,
|
||||
"end_at": job.end_at.isoformat() if job.end_at else None,
|
||||
"duration_minutes": job.duration_minutes,
|
||||
"remaining_minutes": remaining_minutes,
|
||||
"progress_percent": progress_percent,
|
||||
"printer": {
|
||||
"id": job.printer.id,
|
||||
"name": job.printer.name,
|
||||
"location": job.printer.location
|
||||
} if job.printer else None,
|
||||
"guest_request": {
|
||||
"id": guest_request.id,
|
||||
"name": guest_request.name,
|
||||
"created_at": guest_request.created_at.isoformat()
|
||||
},
|
||||
"is_active": job.status in ["scheduled", "running"],
|
||||
"is_completed": job.status in ["completed", "finished"],
|
||||
"is_failed": job.status in ["failed", "cancelled"]
|
||||
}
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"job": job_data
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Abrufen des Job-Status: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
||||
|
||||
@guest_blueprint.route('/api/notifications', methods=['GET'])
|
||||
@login_required
|
||||
def api_get_notifications():
|
||||
"""Benachrichtigungen für den aktuellen Benutzer abrufen."""
|
||||
try:
|
||||
# Zeitstempel für Filter (nur neue Benachrichtigungen)
|
||||
since = request.args.get('since')
|
||||
if since:
|
||||
try:
|
||||
since_date = datetime.fromisoformat(since)
|
||||
except ValueError:
|
||||
return jsonify({"error": "Ungültiges Datumsformat"}), 400
|
||||
else:
|
||||
since_date = None
|
||||
|
||||
with get_cached_session() as db_session:
|
||||
query = db_session.query(Notification).filter_by(
|
||||
user_id=current_user.id,
|
||||
read=False
|
||||
)
|
||||
|
||||
if since_date:
|
||||
query = query.filter(Notification.created_at > since_date)
|
||||
|
||||
notifications = query.order_by(desc(Notification.created_at)).all()
|
||||
|
||||
return jsonify({
|
||||
"count": len(notifications),
|
||||
"notifications": [n.to_dict() for n in notifications]
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Abrufen der Benachrichtigungen: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
||||
|
||||
@guest_blueprint.route('/api/notifications/<int:notification_id>/read', methods=['POST'])
|
||||
@login_required
|
||||
def api_mark_notification_read(notification_id):
|
||||
"""Benachrichtigung als gelesen markieren."""
|
||||
try:
|
||||
with get_cached_session() as db_session:
|
||||
notification = db_session.query(Notification).filter_by(
|
||||
id=notification_id,
|
||||
user_id=current_user.id
|
||||
).first()
|
||||
|
||||
if not notification:
|
||||
return jsonify({"error": "Benachrichtigung nicht gefunden"}), 404
|
||||
|
||||
notification.read = True
|
||||
db_session.commit()
|
||||
|
||||
return jsonify({"success": True})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Markieren der Benachrichtigung als gelesen: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
||||
|
||||
@guest_blueprint.route('/api/admin/requests', methods=['GET'])
|
||||
@approver_required
|
||||
def api_get_all_requests():
|
||||
"""Alle Gastanfragen für Admins abrufen."""
|
||||
try:
|
||||
# Filter-Parameter
|
||||
status_filter = request.args.get('status', 'all') # all, pending, approved, denied
|
||||
limit = int(request.args.get('limit', 50))
|
||||
offset = int(request.args.get('offset', 0))
|
||||
|
||||
with get_cached_session() as db_session:
|
||||
# Query mit eager loading
|
||||
query = db_session.query(GuestRequest).options(
|
||||
joinedload(GuestRequest.printer),
|
||||
joinedload(GuestRequest.job),
|
||||
joinedload(GuestRequest.processed_by_user)
|
||||
)
|
||||
|
||||
# Status-Filter anwenden
|
||||
if status_filter != 'all':
|
||||
query = query.filter(GuestRequest.status == status_filter)
|
||||
|
||||
# Sortierung: Pending zuerst, dann nach Erstellungsdatum
|
||||
query = query.order_by(
|
||||
desc(GuestRequest.status == 'pending'),
|
||||
desc(GuestRequest.created_at)
|
||||
)
|
||||
|
||||
# Pagination
|
||||
total_count = query.count()
|
||||
requests = query.offset(offset).limit(limit).all()
|
||||
|
||||
# Daten für Admin aufbereiten
|
||||
admin_requests = []
|
||||
for req in requests:
|
||||
request_data = req.to_dict()
|
||||
|
||||
# Zusätzliche Admin-Informationen
|
||||
request_data.update({
|
||||
"can_be_processed": req.status == "pending",
|
||||
"is_overdue": (
|
||||
req.status == "approved" and
|
||||
req.job and
|
||||
req.job.end_at < datetime.now()
|
||||
) if req.job else False,
|
||||
"time_since_creation": (datetime.now() - req.created_at).total_seconds() / 3600 if req.created_at else 0 # Stunden
|
||||
})
|
||||
|
||||
admin_requests.append(request_data)
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"requests": admin_requests,
|
||||
"pagination": {
|
||||
"total": total_count,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
"has_more": (offset + limit) < total_count
|
||||
},
|
||||
"stats": {
|
||||
"total": total_count,
|
||||
"pending": db_session.query(GuestRequest).filter_by(status='pending').count(),
|
||||
"approved": db_session.query(GuestRequest).filter_by(status='approved').count(),
|
||||
"denied": db_session.query(GuestRequest).filter_by(status='denied').count()
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Abrufen der Admin-Gastanfragen: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
||||
|
||||
@guest_blueprint.route('/api/admin/requests/<int:request_id>', methods=['GET'])
|
||||
@approver_required
|
||||
def api_get_request_details(request_id):
|
||||
"""Detaillierte Informationen zu einer Gastanfrage für Admins abrufen."""
|
||||
try:
|
||||
with get_cached_session() as db_session:
|
||||
guest_request = db_session.query(GuestRequest).options(
|
||||
joinedload(GuestRequest.printer),
|
||||
joinedload(GuestRequest.job),
|
||||
joinedload(GuestRequest.processed_by_user)
|
||||
).filter_by(id=request_id).first()
|
||||
|
||||
if not guest_request:
|
||||
return jsonify({"error": "Anfrage nicht gefunden"}), 404
|
||||
|
||||
# Vollständige Admin-Informationen
|
||||
request_data = guest_request.to_dict()
|
||||
|
||||
# Verfügbare Drucker für Zuweisung
|
||||
available_printers = db_session.query(Printer).filter_by(active=True).all()
|
||||
request_data["available_printers"] = [p.to_dict() for p in available_printers]
|
||||
|
||||
# Job-Historie falls vorhanden
|
||||
if guest_request.job:
|
||||
job_data = guest_request.job.to_dict()
|
||||
job_data["is_active"] = guest_request.job.status in ["scheduled", "running"]
|
||||
job_data["is_overdue"] = guest_request.job.end_at < datetime.now() if guest_request.job.end_at else False
|
||||
request_data["job_details"] = job_data
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"request": request_data
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Abrufen der Gastanfrage-Details: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
||||
|
||||
@guest_blueprint.route('/api/admin/requests/<int:request_id>/update', methods=['PUT'])
|
||||
@approver_required
|
||||
def api_update_request(request_id):
|
||||
"""Gastanfrage aktualisieren (nur für Admins)."""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({"error": "Keine Daten erhalten"}), 400
|
||||
|
||||
with get_cached_session() as db_session:
|
||||
guest_request = db_session.query(GuestRequest).filter_by(id=request_id).first()
|
||||
if not guest_request:
|
||||
return jsonify({"error": "Anfrage nicht gefunden"}), 404
|
||||
|
||||
# Erlaubte Felder für Updates
|
||||
allowed_fields = ['printer_id', 'duration_min', 'approval_notes', 'rejection_reason']
|
||||
changes_made = False
|
||||
|
||||
for field in allowed_fields:
|
||||
if field in data:
|
||||
if field == 'printer_id' and data[field]:
|
||||
# Drucker validieren
|
||||
printer = db_session.query(Printer).filter_by(id=data[field], active=True).first()
|
||||
if not printer:
|
||||
return jsonify({"error": "Ungültiger Drucker ausgewählt"}), 400
|
||||
|
||||
setattr(guest_request, field, data[field])
|
||||
changes_made = True
|
||||
|
||||
if changes_made:
|
||||
guest_request.processed_by = current_user.id
|
||||
guest_request.processed_at = datetime.now()
|
||||
db_session.commit()
|
||||
|
||||
logger.info(f"Gastanfrage {request_id} aktualisiert von Admin {current_user.id}")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": "Anfrage erfolgreich aktualisiert",
|
||||
"updated_by": current_user.name,
|
||||
"updated_at": guest_request.processed_at.isoformat()
|
||||
})
|
||||
else:
|
||||
return jsonify({"error": "Keine Änderungen vorgenommen"}), 400
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Aktualisieren der Gastanfrage: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
||||
|
||||
# Admin-Routen
|
||||
@guest_blueprint.route('/admin/requests', methods=['GET'])
|
||||
@approver_required
|
||||
def admin_requests_management():
|
||||
"""Admin-Oberfläche für die Verwaltung von Gastanfragen."""
|
||||
return render_template('admin_guest_requests.html')
|
||||
|
||||
@guest_blueprint.route('/api/requests/<int:request_id>/approve', methods=['POST'])
|
||||
@approver_required
|
||||
def api_approve_request(request_id):
|
||||
"""Gastanfrage genehmigen."""
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
approval_notes = data.get('notes', '')
|
||||
printer_id = data.get('printer_id') # Optional: Drucker zuweisen/ändern
|
||||
|
||||
with get_cached_session() as db_session:
|
||||
guest_request = db_session.query(GuestRequest).filter_by(id=request_id).first()
|
||||
if not guest_request:
|
||||
return jsonify({"error": "Anfrage nicht gefunden"}), 404
|
||||
|
||||
if guest_request.status != "pending":
|
||||
return jsonify({"error": "Anfrage wurde bereits bearbeitet"}), 400
|
||||
|
||||
# Drucker validieren, falls angegeben
|
||||
if printer_id:
|
||||
printer = db_session.query(Printer).filter_by(id=printer_id, active=True).first()
|
||||
if not printer:
|
||||
return jsonify({"error": "Ungültiger Drucker ausgewählt"}), 400
|
||||
guest_request.printer_id = printer_id
|
||||
|
||||
# Sicherstellen, dass ein Drucker zugewiesen ist
|
||||
if not guest_request.printer_id:
|
||||
return jsonify({"error": "Kein Drucker zugewiesen. Bitte wählen Sie einen Drucker aus."}), 400
|
||||
|
||||
# Anfrage genehmigen
|
||||
guest_request.status = "approved"
|
||||
guest_request.processed_by = current_user.id
|
||||
guest_request.processed_at = datetime.now()
|
||||
guest_request.approval_notes = approval_notes
|
||||
|
||||
# OTP generieren
|
||||
otp_plain = guest_request.generate_otp()
|
||||
|
||||
# Zugehörigen Job erstellen
|
||||
start_time = datetime.now() + timedelta(minutes=5) # Start in 5 Minuten
|
||||
end_time = start_time + timedelta(minutes=guest_request.duration_min)
|
||||
|
||||
# Admin-Benutzer als Eigentümer verwenden
|
||||
admin_user = db_session.query(User).filter_by(is_admin=True).first()
|
||||
if not admin_user:
|
||||
admin_user = current_user # Fallback auf aktuellen Benutzer
|
||||
|
||||
job = Job(
|
||||
name=f"Gastauftrag: {guest_request.name}",
|
||||
description=guest_request.reason or "Gastauftrag",
|
||||
user_id=admin_user.id,
|
||||
printer_id=guest_request.printer_id,
|
||||
start_at=start_time,
|
||||
end_at=end_time,
|
||||
status="scheduled",
|
||||
duration_minutes=guest_request.duration_min,
|
||||
owner_id=admin_user.id
|
||||
)
|
||||
|
||||
db_session.add(job)
|
||||
db_session.flush() # ID generieren
|
||||
|
||||
# Job-ID in Gastanfrage speichern
|
||||
guest_request.job_id = job.id
|
||||
|
||||
db_session.commit()
|
||||
|
||||
logger.info(f"Gastanfrage {request_id} genehmigt von Admin {current_user.id} ({current_user.username})")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"status": "approved",
|
||||
"job_id": job.id,
|
||||
"otp": otp_plain, # Nur in dieser Antwort wird der OTP-Klartext zurückgegeben
|
||||
"approved_by": current_user.username,
|
||||
"approved_at": guest_request.processed_at.isoformat(),
|
||||
"notes": approval_notes,
|
||||
"message": f"Anfrage genehmigt. Zugangscode: {otp_plain}"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Genehmigen der Gastanfrage: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
||||
|
||||
@guest_blueprint.route('/api/requests/<int:request_id>/deny', methods=['POST'])
|
||||
@approver_required
|
||||
def api_deny_request(request_id):
|
||||
"""Gastanfrage ablehnen."""
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
rejection_reason = data.get('reason', '')
|
||||
|
||||
if not rejection_reason.strip():
|
||||
return jsonify({"error": "Ablehnungsgrund ist erforderlich"}), 400
|
||||
|
||||
with get_cached_session() as db_session:
|
||||
guest_request = db_session.query(GuestRequest).filter_by(id=request_id).first()
|
||||
if not guest_request:
|
||||
return jsonify({"error": "Anfrage nicht gefunden"}), 404
|
||||
|
||||
if guest_request.status != "pending":
|
||||
return jsonify({"error": "Anfrage wurde bereits bearbeitet"}), 400
|
||||
|
||||
# Anfrage ablehnen
|
||||
guest_request.status = "denied"
|
||||
guest_request.processed_by = current_user.id
|
||||
guest_request.processed_at = datetime.now()
|
||||
guest_request.rejection_reason = rejection_reason
|
||||
|
||||
db_session.commit()
|
||||
|
||||
logger.info(f"Gastanfrage {request_id} abgelehnt von Admin {current_user.id} ({current_user.username}): {rejection_reason}")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"status": "denied",
|
||||
"rejected_by": current_user.username,
|
||||
"rejected_at": guest_request.processed_at.isoformat(),
|
||||
"reason": rejection_reason,
|
||||
"message": "Anfrage wurde abgelehnt"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Ablehnen der Gastanfrage: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
181
backend/blueprints/printers.py
Normal file
181
backend/blueprints/printers.py
Normal file
@ -0,0 +1,181 @@
|
||||
"""
|
||||
Drucker-Blueprint für MYP Platform
|
||||
Enthält alle Routen und Funktionen zur Druckerverwaltung, Statusüberwachung und Steuerung.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from flask import Blueprint, request, jsonify, current_app, abort, Response
|
||||
from flask_login import login_required, current_user
|
||||
from werkzeug.utils import secure_filename
|
||||
from werkzeug.exceptions import NotFound, BadRequest
|
||||
from sqlalchemy import func, desc, asc
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from typing import Dict, List, Tuple, Any, Optional
|
||||
|
||||
from models import Printer, User, Job, get_db_session
|
||||
from utils.logging_config import get_logger, measure_execution_time
|
||||
from utils.permissions import require_permission, Permission, check_permission
|
||||
from utils.printer_monitor import printer_monitor
|
||||
|
||||
# Logger initialisieren
|
||||
printers_logger = get_logger("printers")
|
||||
|
||||
# Blueprint erstellen
|
||||
printers_blueprint = Blueprint("printers", __name__, url_prefix="/api/printers")
|
||||
|
||||
@printers_blueprint.route("/monitor/live-status", methods=["GET"])
|
||||
@login_required
|
||||
@measure_execution_time(logger=printers_logger, task_name="API-Live-Drucker-Status-Abfrage")
|
||||
def get_live_printer_status():
|
||||
"""
|
||||
Liefert den aktuellen Live-Status aller Drucker.
|
||||
|
||||
Query-Parameter:
|
||||
- use_cache: ob Cache verwendet werden soll (default: true)
|
||||
|
||||
Returns:
|
||||
JSON mit Live-Status aller Drucker
|
||||
"""
|
||||
printers_logger.info(f"🔄 Live-Status-Abfrage von Benutzer {current_user.name} (ID: {current_user.id})")
|
||||
|
||||
# Parameter auslesen
|
||||
use_cache_param = request.args.get("use_cache", "true").lower()
|
||||
use_cache = use_cache_param == "true"
|
||||
|
||||
try:
|
||||
# Live-Status über den PrinterMonitor abrufen
|
||||
status_data = printer_monitor.get_live_printer_status(use_session_cache=use_cache)
|
||||
|
||||
# Zusammenfassung der Druckerstatus erstellen
|
||||
summary = printer_monitor.get_printer_summary()
|
||||
|
||||
# Antwort mit Status und Zusammenfassung
|
||||
response = {
|
||||
"success": True,
|
||||
"status": status_data,
|
||||
"summary": summary,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"cache_used": use_cache
|
||||
}
|
||||
|
||||
printers_logger.info(f"✅ Live-Status-Abfrage erfolgreich: {len(status_data)} Drucker")
|
||||
return jsonify(response)
|
||||
|
||||
except Exception as e:
|
||||
printers_logger.error(f"❌ Fehler bei Live-Status-Abfrage: {str(e)}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Fehler bei Abfrage des Druckerstatus",
|
||||
"message": str(e)
|
||||
}), 500
|
||||
|
||||
@printers_blueprint.route("/control/<int:printer_id>/power", methods=["POST"])
|
||||
@login_required
|
||||
@require_permission(Permission.CONTROL_PRINTER) # Verwende die bereits vorhandene Berechtigung
|
||||
@measure_execution_time(logger=printers_logger, task_name="API-Drucker-Stromversorgung-Steuerung")
|
||||
def control_printer_power(printer_id):
|
||||
"""
|
||||
Steuert die Stromversorgung eines Druckers (ein-/ausschalten).
|
||||
|
||||
Args:
|
||||
printer_id: ID des zu steuernden Druckers
|
||||
|
||||
JSON-Parameter:
|
||||
- action: "on" oder "off"
|
||||
|
||||
Returns:
|
||||
JSON mit Ergebnis der Steuerungsaktion
|
||||
"""
|
||||
printers_logger.info(f"🔌 Stromsteuerung für Drucker {printer_id} von Benutzer {current_user.name}")
|
||||
|
||||
# Parameter validieren
|
||||
data = request.get_json()
|
||||
if not data or "action" not in data:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Parameter 'action' fehlt"
|
||||
}), 400
|
||||
|
||||
action = data["action"]
|
||||
if action not in ["on", "off"]:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Ungültige Aktion. Erlaubt sind 'on' oder 'off'."
|
||||
}), 400
|
||||
|
||||
try:
|
||||
# Drucker aus Datenbank holen
|
||||
db_session = get_db_session()
|
||||
printer = db_session.query(Printer).filter(Printer.id == printer_id).first()
|
||||
|
||||
if not printer:
|
||||
db_session.close()
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": f"Drucker mit ID {printer_id} nicht gefunden"
|
||||
}), 404
|
||||
|
||||
# Prüfen, ob Drucker eine Steckdose konfiguriert hat
|
||||
if not printer.plug_ip or not printer.plug_username or not printer.plug_password:
|
||||
db_session.close()
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": f"Drucker {printer.name} hat keine Steckdose konfiguriert"
|
||||
}), 400
|
||||
|
||||
# Steckdose steuern
|
||||
from PyP100 import PyP110
|
||||
try:
|
||||
# TP-Link Tapo P110 Verbindung herstellen
|
||||
p110 = PyP110.P110(printer.plug_ip, printer.plug_username, printer.plug_password)
|
||||
p110.handshake() # Authentifizierung
|
||||
p110.login() # Login
|
||||
|
||||
# Steckdose ein- oder ausschalten
|
||||
if action == "on":
|
||||
p110.turnOn()
|
||||
success = True
|
||||
message = "Steckdose erfolgreich eingeschaltet"
|
||||
printer.status = "starting" # Status aktualisieren
|
||||
else:
|
||||
p110.turnOff()
|
||||
success = True
|
||||
message = "Steckdose erfolgreich ausgeschaltet"
|
||||
printer.status = "offline" # Status aktualisieren
|
||||
|
||||
# Zeitpunkt der letzten Prüfung aktualisieren
|
||||
printer.last_checked = datetime.now()
|
||||
db_session.commit()
|
||||
|
||||
# Cache leeren, damit neue Status-Abfragen aktuell sind
|
||||
printer_monitor.clear_all_caches()
|
||||
|
||||
printers_logger.info(f"✅ {action.upper()}: Drucker {printer.name} erfolgreich {message}")
|
||||
|
||||
except Exception as e:
|
||||
printers_logger.error(f"❌ Fehler bei Steckdosensteuerung für {printer.name}: {str(e)}")
|
||||
db_session.close()
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": f"Fehler bei Steckdosensteuerung: {str(e)}"
|
||||
}), 500
|
||||
|
||||
db_session.close()
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": message,
|
||||
"printer_id": printer_id,
|
||||
"printer_name": printer.name,
|
||||
"action": action,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
printers_logger.error(f"❌ Allgemeiner Fehler bei Stromsteuerung: {str(e)}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": f"Allgemeiner Fehler: {str(e)}"
|
||||
}), 500
|
168
backend/blueprints/users.py
Normal file
168
backend/blueprints/users.py
Normal file
@ -0,0 +1,168 @@
|
||||
from flask import Blueprint, render_template, request, jsonify, redirect, url_for, abort
|
||||
from flask_login import current_user, login_required
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from functools import wraps
|
||||
|
||||
from models import User, UserPermission, get_cached_session
|
||||
from utils.logging_config import get_logger
|
||||
|
||||
users_blueprint = Blueprint('users', __name__)
|
||||
logger = get_logger("users")
|
||||
|
||||
def users_admin_required(f):
|
||||
"""Decorator zur Prüfung der Admin-Berechtigung für Users Blueprint."""
|
||||
@wraps(f)
|
||||
@login_required
|
||||
def users_decorated_function(*args, **kwargs):
|
||||
if not current_user.is_admin:
|
||||
abort(403, "Nur Administratoren haben Zugriff auf diese Seite")
|
||||
return f(*args, **kwargs)
|
||||
return users_decorated_function
|
||||
|
||||
@users_blueprint.route('/admin/users/<int:user_id>/permissions', methods=['GET'])
|
||||
@users_admin_required
|
||||
def admin_user_permissions(user_id):
|
||||
"""Benutzerberechtigungen anzeigen und bearbeiten."""
|
||||
with get_cached_session() as db_session:
|
||||
user = db_session.query(User).filter_by(id=user_id).first()
|
||||
if not user:
|
||||
abort(404, "Benutzer nicht gefunden")
|
||||
|
||||
# Berechtigungen laden oder erstellen, falls nicht vorhanden
|
||||
permission = db_session.query(UserPermission).filter_by(user_id=user_id).first()
|
||||
if not permission:
|
||||
permission = UserPermission(user_id=user_id)
|
||||
db_session.add(permission)
|
||||
db_session.commit()
|
||||
|
||||
return render_template('admin_user_permissions.html', user=user, permission=permission)
|
||||
|
||||
@users_blueprint.route('/api/users/<int:user_id>/permissions', methods=['GET'])
|
||||
@login_required
|
||||
def api_get_user_permissions(user_id):
|
||||
"""Benutzerberechtigungen als JSON zurückgeben."""
|
||||
# Nur Admins oder der Benutzer selbst darf seine Berechtigungen sehen
|
||||
if not current_user.is_admin and current_user.id != user_id:
|
||||
return jsonify({"error": "Keine Berechtigung"}), 403
|
||||
|
||||
try:
|
||||
with get_cached_session() as db_session:
|
||||
permission = db_session.query(UserPermission).filter_by(user_id=user_id).first()
|
||||
|
||||
if not permission:
|
||||
# Falls keine Berechtigungen existieren, Standard-Werte zurückgeben
|
||||
return jsonify({
|
||||
"user_id": user_id,
|
||||
"can_start_jobs": False,
|
||||
"needs_approval": True,
|
||||
"can_approve_jobs": False
|
||||
})
|
||||
|
||||
return jsonify(permission.to_dict())
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Abrufen der Benutzerberechtigungen: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
||||
|
||||
@users_blueprint.route('/api/users/<int:user_id>/permissions', methods=['PUT'])
|
||||
@users_admin_required
|
||||
def api_update_user_permissions(user_id):
|
||||
"""Benutzerberechtigungen aktualisieren."""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({"error": "Keine Daten erhalten"}), 400
|
||||
|
||||
with get_cached_session() as db_session:
|
||||
# Benutzer prüfen
|
||||
user = db_session.query(User).filter_by(id=user_id).first()
|
||||
if not user:
|
||||
return jsonify({"error": "Benutzer nicht gefunden"}), 404
|
||||
|
||||
# Berechtigungen laden oder erstellen
|
||||
permission = db_session.query(UserPermission).filter_by(user_id=user_id).first()
|
||||
if not permission:
|
||||
permission = UserPermission(user_id=user_id)
|
||||
db_session.add(permission)
|
||||
|
||||
# Berechtigungen aktualisieren
|
||||
if 'can_start_jobs' in data:
|
||||
permission.can_start_jobs = bool(data['can_start_jobs'])
|
||||
|
||||
if 'needs_approval' in data:
|
||||
permission.needs_approval = bool(data['needs_approval'])
|
||||
|
||||
if 'can_approve_jobs' in data:
|
||||
permission.can_approve_jobs = bool(data['can_approve_jobs'])
|
||||
|
||||
db_session.commit()
|
||||
|
||||
logger.info(f"Berechtigungen für Benutzer {user_id} aktualisiert durch Admin {current_user.id}")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"permissions": permission.to_dict()
|
||||
})
|
||||
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Datenbankfehler beim Aktualisieren der Benutzerberechtigungen: {str(e)}")
|
||||
return jsonify({"error": "Datenbankfehler beim Verarbeiten der Anfrage"}), 500
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Aktualisieren der Benutzerberechtigungen: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
||||
|
||||
@users_blueprint.route('/admin/users/<int:user_id>/permissions/update', methods=['POST'])
|
||||
@users_admin_required
|
||||
def admin_update_user_permissions(user_id):
|
||||
"""Benutzerberechtigungen über Formular aktualisieren."""
|
||||
try:
|
||||
# Formularfelder auslesen
|
||||
can_start_jobs = request.form.get('can_start_jobs') == 'on'
|
||||
needs_approval = request.form.get('needs_approval') == 'on'
|
||||
can_approve_jobs = request.form.get('can_approve_jobs') == 'on'
|
||||
|
||||
with get_cached_session() as db_session:
|
||||
# Benutzer prüfen
|
||||
user = db_session.query(User).filter_by(id=user_id).first()
|
||||
if not user:
|
||||
abort(404, "Benutzer nicht gefunden")
|
||||
|
||||
# Berechtigungen laden oder erstellen
|
||||
permission = db_session.query(UserPermission).filter_by(user_id=user_id).first()
|
||||
if not permission:
|
||||
permission = UserPermission(user_id=user_id)
|
||||
db_session.add(permission)
|
||||
|
||||
# Berechtigungen aktualisieren
|
||||
permission.can_start_jobs = can_start_jobs
|
||||
permission.needs_approval = needs_approval
|
||||
permission.can_approve_jobs = can_approve_jobs
|
||||
|
||||
db_session.commit()
|
||||
|
||||
logger.info(f"Berechtigungen für Benutzer {user_id} aktualisiert durch Admin {current_user.id} (Formular)")
|
||||
|
||||
return redirect(url_for('users.admin_user_permissions', user_id=user_id))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Aktualisieren der Benutzerberechtigungen: {str(e)}")
|
||||
abort(500, "Fehler beim Verarbeiten der Anfrage")
|
||||
|
||||
# Erweiterung des bestehenden Benutzer-Bearbeitungsformulars
|
||||
@users_blueprint.route('/admin/users/<int:user_id>/edit/permissions', methods=['GET'])
|
||||
@users_admin_required
|
||||
def admin_edit_user_permissions_section(user_id):
|
||||
"""Rendert nur den Berechtigungsteil für das Benutzer-Edit-Formular."""
|
||||
with get_cached_session() as db_session:
|
||||
user = db_session.query(User).filter_by(id=user_id).first()
|
||||
if not user:
|
||||
abort(404, "Benutzer nicht gefunden")
|
||||
|
||||
# Berechtigungen laden oder erstellen, falls nicht vorhanden
|
||||
permission = db_session.query(UserPermission).filter_by(user_id=user_id).first()
|
||||
if not permission:
|
||||
permission = UserPermission(user_id=user_id)
|
||||
db_session.add(permission)
|
||||
db_session.commit()
|
||||
|
||||
return render_template('_user_permissions_form.html', user=user, permission=permission)
|
Reference in New Issue
Block a user