🎉 Refactor & Update Backend Files, Documentation 📚

This commit is contained in:
2025-06-01 00:05:09 +02:00
parent 193164964e
commit b2bdc2d123
12 changed files with 4178 additions and 1868 deletions

View File

@@ -33,7 +33,7 @@ _cache_lock = threading.Lock()
_cache_ttl = {} # Time-to-live für Cache-Einträge
# Alle exportierten Modelle
__all__ = ['User', 'Printer', 'Job', 'Stats', 'SystemLog', 'Base', 'GuestRequest', 'UserPermission', 'Notification', 'init_db', 'init_database', 'create_initial_admin', 'get_db_session', 'get_cached_session', 'clear_cache', 'engine']
__all__ = ['User', 'Printer', 'Job', 'Stats', 'SystemLog', 'Base', 'GuestRequest', 'UserPermission', 'Notification', 'JobOrder', 'init_db', 'init_database', 'create_initial_admin', 'get_db_session', 'get_cached_session', 'clear_cache', 'engine']
# ===== DATENBANK-KONFIGURATION MIT WAL UND OPTIMIERUNGEN =====
@@ -819,25 +819,29 @@ class GuestRequest(Base):
rejected_by_user = relationship("User", foreign_keys=[rejected_by]) # Admin der abgelehnt hat
def to_dict(self) -> dict:
return {
# Cache-Key für GuestRequest-Dict
cache_key = get_cache_key("GuestRequest", self.id, "dict")
cached_result = get_cache(cache_key)
if cached_result is not None:
return cached_result
result = {
"id": self.id,
"name": self.name,
"email": self.email,
"reason": self.reason,
"duration_min": self.duration_min,
"duration_minutes": self.duration_minutes or self.duration_min, # Fallback auf duration_min
"file_name": self.file_name,
"file_path": self.file_path,
"copies": self.copies,
"duration_minutes": self.duration_minutes,
"created_at": self.created_at.isoformat() if self.created_at else None,
"status": self.status,
"printer_id": self.printer_id,
"assigned_printer_id": self.assigned_printer_id,
"otp_code": self.otp_code,
"otp_expires_at": self.otp_expires_at.isoformat() if self.otp_expires_at else None,
"otp_used_at": self.otp_used_at.isoformat() if self.otp_used_at else None,
"job_id": self.job_id,
"author_ip": self.author_ip,
"otp_used_at": self.otp_used_at.isoformat() if self.otp_used_at else None,
"file_name": self.file_name,
"file_path": self.file_path,
"copies": self.copies,
"processed_by": self.processed_by,
"processed_at": self.processed_at.isoformat() if self.processed_at else None,
"approval_notes": self.approval_notes,
@@ -847,60 +851,304 @@ class GuestRequest(Base):
"rejected_at": self.rejected_at.isoformat() if self.rejected_at else None,
"approved_by": self.approved_by,
"rejected_by": self.rejected_by,
"printer": self.printer.to_dict() if self.printer else None,
"assigned_printer": self.assigned_printer.to_dict() if self.assigned_printer else None,
"job": self.job.to_dict() if self.job else None,
"processed_by_user": self.processed_by_user.to_dict() if self.processed_by_user else None,
"approved_by_user": self.approved_by_user.to_dict() if self.approved_by_user else None,
"rejected_by_user": self.rejected_by_user.to_dict() if self.rejected_by_user else None
"otp_expires_at": self.otp_expires_at.isoformat() if self.otp_expires_at else None,
"assigned_printer_id": self.assigned_printer_id,
}
# Ergebnis cachen (5 Minuten)
set_cache(cache_key, result, 300)
return result
def generate_otp(self) -> str:
"""
Generiert einen einmaligen OTP-Code und speichert den Hash in der Datenbank.
Returns:
str: Der generierte OTP-Code im Klartext
Generiert einen neuen OTP-Code und speichert den Hash.
"""
# Generiere 6-stelligen Code (Großbuchstaben + Ziffern)
otp_plain = ''.join(secrets.choice('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ') for _ in range(6))
otp_plain = secrets.token_hex(8) # 16-stelliger hexadezimaler Code
# Hash für die Speicherung erstellen
# Hash des OTP-Codes speichern
otp_bytes = otp_plain.encode('utf-8')
salt = bcrypt.gensalt()
otp_hash = bcrypt.hashpw(otp_bytes, salt).decode('utf-8')
self.otp_code = bcrypt.hashpw(otp_bytes, salt).decode('utf-8')
# Hash in der Datenbank speichern
self.otp_code = otp_hash
logger.info(f"OTP generiert für Guest Request {self.id}")
# Cache invalidieren
invalidate_model_cache("GuestRequest", self.id)
return otp_plain
def verify_otp(self, otp_plain: str) -> bool:
"""
Verifiziert einen OTP-Code gegen den gespeicherten Hash.
Args:
otp_plain: Der zu prüfende OTP-Code im Klartext
Returns:
bool: True wenn der Code korrekt ist, False andernfalls
Verifiziert einen OTP-Code.
"""
if not self.otp_code or not otp_plain:
return False
try:
# Code normalisieren (Großbuchstaben)
otp_plain = otp_plain.upper().strip()
# Hash verifizieren
otp_bytes = otp_plain.encode('utf-8')
stored_hash = self.otp_code.encode('utf-8')
hash_bytes = self.otp_code.encode('utf-8')
return bcrypt.checkpw(otp_bytes, stored_hash)
except Exception:
is_valid = bcrypt.checkpw(otp_bytes, hash_bytes)
if is_valid:
self.otp_used_at = datetime.now()
logger.info(f"OTP erfolgreich verifiziert für Guest Request {self.id}")
# Cache invalidieren
invalidate_model_cache("GuestRequest", self.id)
else:
logger.warning(f"Ungültiger OTP-Code für Guest Request {self.id}")
return is_valid
except Exception as e:
logger.error(f"Fehler bei OTP-Verifizierung: {str(e)}")
return False
class JobOrder(Base):
"""
Job-Reihenfolge für Drucker im Drag & Drop System.
Speichert die benutzerdefinierte Reihenfolge der Jobs pro Drucker.
"""
__tablename__ = "job_orders"
id = Column(Integer, primary_key=True)
printer_id = Column(Integer, ForeignKey("printers.id"), nullable=False)
job_id = Column(Integer, ForeignKey("jobs.id"), nullable=False)
order_position = Column(Integer, nullable=False) # Position in der Reihenfolge (0-basiert)
created_at = Column(DateTime, default=datetime.now)
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
last_modified_by = Column(Integer, ForeignKey("users.id"), nullable=True) # Wer die Reihenfolge geändert hat
# Beziehungen
printer = relationship("Printer", foreign_keys=[printer_id])
job = relationship("Job", foreign_keys=[job_id])
modified_by_user = relationship("User", foreign_keys=[last_modified_by])
# Eindeutige Kombination: Ein Job kann nur eine Position pro Drucker haben
__table_args__ = (
# Sicherstellen, dass jeder Job nur einmal pro Drucker existiert
# und jede Position pro Drucker nur einmal vergeben wird
)
def to_dict(self) -> dict:
"""
Konvertiert JobOrder zu Dictionary.
"""
cache_key = get_cache_key("JobOrder", f"{self.printer_id}_{self.job_id}", "dict")
cached_result = get_cache(cache_key)
if cached_result is not None:
return cached_result
result = {
"id": self.id,
"printer_id": self.printer_id,
"job_id": self.job_id,
"order_position": self.order_position,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
"last_modified_by": self.last_modified_by
}
# Ergebnis cachen (2 Minuten)
set_cache(cache_key, result, 120)
return result
@classmethod
def get_order_for_printer(cls, printer_id: int) -> List['JobOrder']:
"""
Holt die Job-Reihenfolge für einen bestimmten Drucker.
"""
cache_key = get_cache_key("JobOrder", printer_id, "printer_order")
cached_orders = get_cache(cache_key)
if cached_orders is not None:
return cached_orders
with get_cached_session() as session:
orders = session.query(cls).filter(
cls.printer_id == printer_id
).order_by(cls.order_position).all()
# Ergebnis cachen (1 Minute für häufige Abfragen)
set_cache(cache_key, orders, 60)
return orders
@classmethod
def update_printer_order(cls, printer_id: int, job_ids: List[int],
modified_by_user_id: int = None) -> bool:
"""
Aktualisiert die komplette Job-Reihenfolge für einen Drucker.
Args:
printer_id: ID des Druckers
job_ids: Liste der Job-IDs in der gewünschten Reihenfolge
modified_by_user_id: ID des Users der die Änderung durchführt
Returns:
bool: True wenn erfolgreich, False bei Fehler
"""
try:
with get_cached_session() as session:
# Validiere dass alle Jobs existieren und zum Drucker gehören
valid_jobs = session.query(Job).filter(
Job.id.in_(job_ids),
Job.printer_id == printer_id,
Job.status.in_(['scheduled', 'paused'])
).all()
if len(valid_jobs) != len(job_ids):
logger.warning(f"Nicht alle Jobs gültig für Drucker {printer_id}. "
f"Erwartet: {len(job_ids)}, Gefunden: {len(valid_jobs)}")
return False
# Alte Reihenfolge-Einträge für diesen Drucker löschen
session.query(cls).filter(cls.printer_id == printer_id).delete()
# Neue Reihenfolge-Einträge erstellen
for position, job_id in enumerate(job_ids):
order_entry = cls(
printer_id=printer_id,
job_id=job_id,
order_position=position,
last_modified_by=modified_by_user_id
)
session.add(order_entry)
session.commit()
# Cache invalidieren
clear_cache(f"JobOrder:{printer_id}")
logger.info(f"Job-Reihenfolge für Drucker {printer_id} erfolgreich aktualisiert. "
f"Jobs: {job_ids}, Benutzer: {modified_by_user_id}")
return True
except Exception as e:
logger.error(f"Fehler beim Aktualisieren der Job-Reihenfolge für Drucker {printer_id}: {str(e)}")
return False
@classmethod
def get_ordered_job_ids(cls, printer_id: int) -> List[int]:
"""
Holt die Job-IDs in der korrekten Reihenfolge für einen Drucker.
Args:
printer_id: ID des Druckers
Returns:
List[int]: Liste der Job-IDs in der richtigen Reihenfolge
"""
cache_key = get_cache_key("JobOrder", printer_id, "job_ids")
cached_ids = get_cache(cache_key)
if cached_ids is not None:
return cached_ids
try:
with get_cached_session() as session:
orders = session.query(cls).filter(
cls.printer_id == printer_id
).order_by(cls.order_position).all()
job_ids = [order.job_id for order in orders]
# Ergebnis cachen (1 Minute)
set_cache(cache_key, job_ids, 60)
return job_ids
except Exception as e:
logger.error(f"Fehler beim Laden der Job-Reihenfolge für Drucker {printer_id}: {str(e)}")
return []
@classmethod
def remove_job_from_orders(cls, job_id: int):
"""
Entfernt einen Job aus allen Drucker-Reihenfolgen (z.B. wenn Job gelöscht wird).
Args:
job_id: ID des zu entfernenden Jobs
"""
try:
with get_cached_session() as session:
# Alle Order-Einträge für diesen Job finden
orders_to_remove = session.query(cls).filter(cls.job_id == job_id).all()
printer_ids = {order.printer_id for order in orders_to_remove}
# Order-Einträge löschen
session.query(cls).filter(cls.job_id == job_id).delete()
# Positionen neu ordnen für betroffene Drucker
for printer_id in printer_ids:
remaining_orders = session.query(cls).filter(
cls.printer_id == printer_id
).order_by(cls.order_position).all()
# Positionen neu setzen (lückenlos)
for new_position, order in enumerate(remaining_orders):
order.order_position = new_position
order.updated_at = datetime.now()
session.commit()
# Cache für betroffene Drucker invalidieren
for printer_id in printer_ids:
clear_cache(f"JobOrder:{printer_id}")
logger.info(f"Job {job_id} aus allen Drucker-Reihenfolgen entfernt. "
f"Betroffene Drucker: {list(printer_ids)}")
except Exception as e:
logger.error(f"Fehler beim Entfernen des Jobs {job_id} aus Reihenfolgen: {str(e)}")
@classmethod
def cleanup_invalid_orders(cls):
"""
Bereinigt ungültige Order-Einträge (Jobs die nicht mehr existieren oder abgeschlossen sind).
"""
try:
with get_cached_session() as session:
# Finde Order-Einträge mit nicht existierenden oder abgeschlossenen Jobs
invalid_orders = session.query(cls).join(Job).filter(
Job.status.in_(['finished', 'aborted', 'cancelled'])
).all()
printer_ids = {order.printer_id for order in invalid_orders}
# Ungültige Einträge löschen
session.query(cls).join(Job).filter(
Job.status.in_(['finished', 'aborted', 'cancelled'])
).delete(synchronize_session='fetch')
# Positionen für betroffene Drucker neu ordnen
for printer_id in printer_ids:
remaining_orders = session.query(cls).filter(
cls.printer_id == printer_id
).order_by(cls.order_position).all()
for new_position, order in enumerate(remaining_orders):
order.order_position = new_position
order.updated_at = datetime.now()
session.commit()
# Cache für betroffene Drucker invalidieren
for printer_id in printer_ids:
clear_cache(f"JobOrder:{printer_id}")
logger.info(f"Bereinigung der Job-Reihenfolgen abgeschlossen. "
f"Entfernte Einträge: {len(invalid_orders)}, "
f"Betroffene Drucker: {list(printer_ids)}")
except Exception as e:
logger.error(f"Fehler bei der Bereinigung der Job-Reihenfolgen: {str(e)}")
# ===== DATENBANK-INITIALISIERUNG MIT OPTIMIERUNGEN =====
def init_db() -> None: