" feat: Integrate calendar functionality into blueprint system "

This commit is contained in:
2025-05-29 09:49:06 +02:00
parent 8b9ae6b451
commit f525bfc0a6
6 changed files with 2270 additions and 0 deletions

View File

@@ -0,0 +1,280 @@
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_
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
@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')
if not all([title, start, end, printer_id]):
return jsonify({"error": "Titel, Start, Ende und Drucker 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:
# Drucker prüfen
printer = db_session.query(Printer).filter_by(id=printer_id).first()
if not printer:
return jsonify({"error": "Drucker nicht gefunden"}), 404
# 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()
logger.info(f"Neuer Kalendereintrag erstellt: ID {job.id}, Name: {title}")
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 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

View File

@@ -0,0 +1,349 @@
import json
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 functools import wraps
from sqlalchemy import desc
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")
# 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'])
def guest_request_form():
"""Formular für Gastanfragen anzeigen."""
with get_cached_session() as db_session:
printers = db_session.query(Printer).filter_by(active=True).all()
return render_template('guest_request.html', printers=printers)
@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 = db_session.query(GuestRequest).filter_by(id=request_id).first()
if not guest_request:
abort(404, "Anfrage nicht gefunden")
# Nur wenn Status "approved" ist, OTP generieren und anzeigen
otp_code = None
if guest_request.status == "approved" and not guest_request.otp_code:
otp_code = guest_request.generate_otp()
db_session.commit()
# 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()
return render_template('guest_status.html',
request=guest_request,
job=job,
otp_code=otp_code)
# 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/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/requests/<int:request_id>/approve', methods=['POST'])
@approver_required
def api_approve_request(request_id):
"""Gastanfrage genehmigen."""
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
if guest_request.status != "pending":
return jsonify({"error": "Anfrage wurde bereits bearbeitet"}), 400
# Anfrage genehmigen
guest_request.status = "approved"
# 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(role="admin").first()
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 Benutzer {current_user.id}")
return jsonify({
"success": True,
"status": "approved",
"job_id": job.id,
"otp": otp_plain # Nur in dieser Antwort wird der OTP-Klartext zurückgegeben
})
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 {}
reason = data.get('reason', '')
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"
db_session.commit()
logger.info(f"Gastanfrage {request_id} abgelehnt von Benutzer {current_user.id}")
return jsonify({
"success": True,
"status": "denied"
})
except Exception as e:
logger.error(f"Fehler beim Ablehnen der Gastanfrage: {str(e)}")
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
@guest_blueprint.route('/api/jobs/start/<string:otp>', methods=['POST'])
def api_start_job_with_otp(otp):
"""Job mit OTP-Code starten."""
if not otp:
return jsonify({"error": "Kein OTP-Code angegeben"}), 400
try:
with get_cached_session() as db_session:
# Alle Gastanfragen mit approved-Status durchsuchen
guest_requests = db_session.query(GuestRequest).filter_by(status="approved").all()
valid_request = None
for req in guest_requests:
if req.verify_otp(otp):
valid_request = req
break
if not valid_request:
return jsonify({"error": "Ungültiger OTP-Code"}), 400
# Zugehörigen Job laden
job = db_session.query(Job).filter_by(id=valid_request.job_id).first()
if not job:
return jsonify({"error": "Kein Job für diese Anfrage gefunden"}), 404
# Grace-Period prüfen (5 Minuten nach geplantem Start)
now = datetime.now()
grace_end = job.start_at + timedelta(minutes=5)
if now > job.end_at:
return jsonify({"error": "Der Job ist bereits abgelaufen"}), 400
# Job starten
job.status = "running"
# OTP-Code nach Verwendung löschen
valid_request.otp_code = None
db_session.commit()
logger.info(f"Job {job.id} mit OTP-Code gestartet")
return jsonify({
"success": True,
"job_id": job.id,
"status": "running"
})
except Exception as e:
logger.error(f"Fehler beim Starten des Jobs mit OTP: {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

View File

@@ -0,0 +1 @@