🎉 Refactor & Update Backend Files, Documentation 📚
This commit is contained in:
@@ -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:
|
||||
|
Reference in New Issue
Block a user