"feat: Integrate new printer model functionality in backend/app/models.py and templates/printers.html"
This commit is contained in:
@@ -6,13 +6,14 @@ from datetime import datetime
|
|||||||
from typing import Optional, List, Dict, Any
|
from typing import Optional, List, Dict, Any
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
|
||||||
from sqlalchemy import create_engine, Column, Integer, String, Boolean, DateTime, ForeignKey, Float, event, text
|
from sqlalchemy import create_engine, Column, Integer, String, Boolean, DateTime, ForeignKey, Float, event, text, Text
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from sqlalchemy.orm import relationship, sessionmaker, Session, Mapped, mapped_column, scoped_session
|
from sqlalchemy.orm import relationship, sessionmaker, Session, Mapped, mapped_column, scoped_session
|
||||||
from sqlalchemy.pool import StaticPool, QueuePool
|
from sqlalchemy.pool import StaticPool, QueuePool
|
||||||
from sqlalchemy.engine import Engine
|
from sqlalchemy.engine import Engine
|
||||||
from flask_login import UserMixin
|
from flask_login import UserMixin
|
||||||
import bcrypt
|
import bcrypt
|
||||||
|
import secrets
|
||||||
|
|
||||||
from config.settings import DATABASE_PATH, ensure_database_directory
|
from config.settings import DATABASE_PATH, ensure_database_directory
|
||||||
from utils.logging_config import get_logger
|
from utils.logging_config import get_logger
|
||||||
@@ -32,7 +33,7 @@ _cache_lock = threading.Lock()
|
|||||||
_cache_ttl = {} # Time-to-live für Cache-Einträge
|
_cache_ttl = {} # Time-to-live für Cache-Einträge
|
||||||
|
|
||||||
# Alle exportierten Modelle
|
# Alle exportierten Modelle
|
||||||
__all__ = ['User', 'Printer', 'Job', 'Stats', 'SystemLog', 'Base', 'init_db', 'init_database', 'create_initial_admin', 'get_db_session', 'get_cached_session', 'clear_cache']
|
__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']
|
||||||
|
|
||||||
# ===== DATENBANK-KONFIGURATION MIT WAL UND OPTIMIERUNGEN =====
|
# ===== DATENBANK-KONFIGURATION MIT WAL UND OPTIMIERUNGEN =====
|
||||||
|
|
||||||
@@ -283,6 +284,8 @@ class User(UserMixin, Base):
|
|||||||
|
|
||||||
jobs = relationship("Job", back_populates="user", foreign_keys="Job.user_id", cascade="all, delete-orphan")
|
jobs = relationship("Job", back_populates="user", foreign_keys="Job.user_id", cascade="all, delete-orphan")
|
||||||
owned_jobs = relationship("Job", foreign_keys="Job.owner_id", overlaps="owner")
|
owned_jobs = relationship("Job", foreign_keys="Job.owner_id", overlaps="owner")
|
||||||
|
permissions = relationship("UserPermission", back_populates="user", uselist=False, cascade="all, delete-orphan")
|
||||||
|
notifications = relationship("Notification", back_populates="user", cascade="all, delete-orphan")
|
||||||
|
|
||||||
def set_password(self, password: str) -> None:
|
def set_password(self, password: str) -> None:
|
||||||
password_bytes = password.encode('utf-8')
|
password_bytes = password.encode('utf-8')
|
||||||
@@ -659,6 +662,167 @@ class SystemLog(Base):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UserPermission(Base):
|
||||||
|
"""
|
||||||
|
Berechtigungen für Benutzer.
|
||||||
|
"""
|
||||||
|
__tablename__ = "user_permissions"
|
||||||
|
|
||||||
|
user_id = Column(Integer, ForeignKey("users.id"), primary_key=True)
|
||||||
|
can_start_jobs = Column(Boolean, default=False)
|
||||||
|
needs_approval = Column(Boolean, default=True)
|
||||||
|
can_approve_jobs = Column(Boolean, default=False)
|
||||||
|
|
||||||
|
user = relationship("User", back_populates="permissions")
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
"""
|
||||||
|
Konvertiert die Benutzerberechtigungen in ein Dictionary.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"user_id": self.user_id,
|
||||||
|
"can_start_jobs": self.can_start_jobs,
|
||||||
|
"needs_approval": self.needs_approval,
|
||||||
|
"can_approve_jobs": self.can_approve_jobs
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Notification(Base):
|
||||||
|
"""
|
||||||
|
Benachrichtigungen für Benutzer.
|
||||||
|
"""
|
||||||
|
__tablename__ = "notifications"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||||
|
type = Column(String(50), nullable=False)
|
||||||
|
payload = Column(Text) # JSON-Daten als String
|
||||||
|
created_at = Column(DateTime, default=datetime.now)
|
||||||
|
read = Column(Boolean, default=False)
|
||||||
|
|
||||||
|
user = relationship("User", back_populates="notifications")
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
"""
|
||||||
|
Konvertiert die Benachrichtigung in ein Dictionary.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"id": self.id,
|
||||||
|
"user_id": self.user_id,
|
||||||
|
"type": self.type,
|
||||||
|
"payload": self.payload,
|
||||||
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||||
|
"read": self.read
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_for_approvers(cls, notification_type: str, payload: dict):
|
||||||
|
"""
|
||||||
|
Erstellt Benachrichtigungen für alle Benutzer mit can_approve_jobs-Berechtigung.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
notification_type: Art der Benachrichtigung
|
||||||
|
payload: Daten für die Benachrichtigung als Dictionary
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
payload_json = json.dumps(payload)
|
||||||
|
|
||||||
|
with get_cached_session() as session:
|
||||||
|
# Alle Benutzer mit can_approve_jobs-Berechtigung finden
|
||||||
|
approvers = session.query(User).join(UserPermission).filter(
|
||||||
|
UserPermission.can_approve_jobs == True
|
||||||
|
).all()
|
||||||
|
|
||||||
|
# Benachrichtigungen für alle Genehmiger erstellen
|
||||||
|
for approver in approvers:
|
||||||
|
notification = cls(
|
||||||
|
user_id=approver.id,
|
||||||
|
type=notification_type,
|
||||||
|
payload=payload_json
|
||||||
|
)
|
||||||
|
session.add(notification)
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
class GuestRequest(Base):
|
||||||
|
"""
|
||||||
|
Gastanfragen für Druckaufträge.
|
||||||
|
"""
|
||||||
|
__tablename__ = "guest_requests"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
name = Column(String(100), nullable=False)
|
||||||
|
email = Column(String(120))
|
||||||
|
reason = Column(Text)
|
||||||
|
duration_min = Column(Integer)
|
||||||
|
created_at = Column(DateTime, default=datetime.now)
|
||||||
|
status = Column(String(20), default="pending") # pending|approved|denied
|
||||||
|
printer_id = Column(Integer, ForeignKey("printers.id"))
|
||||||
|
otp_code = Column(String(100), nullable=True) # Hash des OTP-Codes
|
||||||
|
job_id = Column(Integer, ForeignKey("jobs.id"), nullable=True)
|
||||||
|
author_ip = Column(String(50))
|
||||||
|
|
||||||
|
printer = relationship("Printer")
|
||||||
|
job = relationship("Job")
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
"""
|
||||||
|
Konvertiert die Gastanfrage in ein Dictionary.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"id": self.id,
|
||||||
|
"name": self.name,
|
||||||
|
"email": self.email,
|
||||||
|
"reason": self.reason,
|
||||||
|
"duration_min": self.duration_min,
|
||||||
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||||
|
"status": self.status,
|
||||||
|
"printer_id": self.printer_id,
|
||||||
|
"job_id": self.job_id,
|
||||||
|
"printer": self.printer.to_dict() if self.printer else None,
|
||||||
|
"job": self.job.to_dict() if self.job else None
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
# Generiere 6-stelligen Code (Großbuchstaben + Ziffern)
|
||||||
|
otp_plain = ''.join(secrets.choice('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ') for _ in range(6))
|
||||||
|
|
||||||
|
# Hash für die Speicherung erstellen
|
||||||
|
otp_bytes = otp_plain.encode('utf-8')
|
||||||
|
salt = bcrypt.gensalt()
|
||||||
|
otp_hash = bcrypt.hashpw(otp_bytes, salt).decode('utf-8')
|
||||||
|
|
||||||
|
# Hash in der Datenbank speichern
|
||||||
|
self.otp_code = otp_hash
|
||||||
|
|
||||||
|
return otp_plain
|
||||||
|
|
||||||
|
def verify_otp(self, otp_plain: str) -> bool:
|
||||||
|
"""
|
||||||
|
Überprüft, ob der angegebene OTP-Code gültig ist.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
otp_plain: Der zu überprüfende OTP-Code im Klartext
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True, wenn der Code gültig ist, sonst False
|
||||||
|
"""
|
||||||
|
if not self.otp_code:
|
||||||
|
return False
|
||||||
|
|
||||||
|
otp_bytes = otp_plain.encode('utf-8')
|
||||||
|
hash_bytes = self.otp_code.encode('utf-8')
|
||||||
|
|
||||||
|
return bcrypt.checkpw(otp_bytes, hash_bytes)
|
||||||
|
|
||||||
|
|
||||||
# ===== DATENBANK-INITIALISIERUNG MIT OPTIMIERUNGEN =====
|
# ===== DATENBANK-INITIALISIERUNG MIT OPTIMIERUNGEN =====
|
||||||
|
|
||||||
def init_db() -> None:
|
def init_db() -> None:
|
||||||
|
@@ -460,6 +460,171 @@
|
|||||||
modal.setAttribute('aria-hidden', 'true');
|
modal.setAttribute('aria-hidden', 'true');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Der Rest des Codes bleibt unverändert
|
// Render printers grid mit Filter-Unterstützung
|
||||||
|
function renderPrinters() {
|
||||||
|
const grid = document.getElementById('printers-grid');
|
||||||
|
if (!grid) {
|
||||||
|
console.error('printers-grid Element nicht gefunden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter anwenden
|
||||||
|
let filteredPrinters = printers;
|
||||||
|
if (currentFilter === 'online') {
|
||||||
|
filteredPrinters = printers.filter(p => p.status === 'available' || p.is_online);
|
||||||
|
} else if (currentFilter === 'offline') {
|
||||||
|
filteredPrinters = printers.filter(p => p.status === 'offline' || !p.is_online);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status-Übersicht aktualisieren falls nicht bereits gesetzt
|
||||||
|
if (!document.getElementById('online-count').textContent || document.getElementById('online-count').textContent === '-') {
|
||||||
|
const onlineCount = printers.filter(p => p.status === 'available' || p.is_online).length;
|
||||||
|
const offlineCount = printers.length - onlineCount;
|
||||||
|
updateStatusOverview(onlineCount, offlineCount, printers.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filteredPrinters.length === 0) {
|
||||||
|
let emptyMessage = 'Keine Drucker vorhanden';
|
||||||
|
if (currentFilter === 'online') {
|
||||||
|
emptyMessage = 'Keine Online-Drucker gefunden';
|
||||||
|
} else if (currentFilter === 'offline') {
|
||||||
|
emptyMessage = 'Keine Offline-Drucker gefunden';
|
||||||
|
}
|
||||||
|
|
||||||
|
grid.innerHTML = `
|
||||||
|
<div class="col-span-full text-center py-8 sm:py-12">
|
||||||
|
<svg class="h-14 w-14 sm:h-16 sm:w-16 text-slate-300 dark:text-slate-600 mx-auto mb-4 sm:mb-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
|
||||||
|
</svg>
|
||||||
|
<p class="text-slate-700 dark:text-slate-300 text-base sm:text-lg font-medium">${emptyMessage}</p>
|
||||||
|
${currentFilter === 'all' && printers.length === 0 ? `
|
||||||
|
{% if current_user.is_admin %}
|
||||||
|
<button id="addFirstPrinterBtn" class="mt-4 sm:mt-5 action-btn-new success">
|
||||||
|
<svg class="h-4 w-4 sm:h-5 sm:w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||||
|
</svg>
|
||||||
|
<span>Ersten Drucker hinzufügen</span>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Event-Listener für den "Ersten Drucker hinzufügen" Button
|
||||||
|
const addFirstPrinterBtn = document.getElementById('addFirstPrinterBtn');
|
||||||
|
if (addFirstPrinterBtn) {
|
||||||
|
// Entferne vorherige Event-Listener, um doppelte Registrierung zu vermeiden
|
||||||
|
const clonedBtn = addFirstPrinterBtn.cloneNode(true);
|
||||||
|
addFirstPrinterBtn.parentNode.replaceChild(clonedBtn, addFirstPrinterBtn);
|
||||||
|
|
||||||
|
clonedBtn.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
showAddPrinterModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
grid.innerHTML = filteredPrinters.map(printer => {
|
||||||
|
const isOnline = printer.status === 'available' || printer.is_online;
|
||||||
|
const statusText = getPrinterStatusText(printer.status);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="printer-card-new ${isOnline ? 'online' : ''}">
|
||||||
|
${isOnline ? '<div class="online-indicator"></div>' : ''}
|
||||||
|
|
||||||
|
<div class="flex items-start justify-between mb-4">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-base sm:text-lg font-bold ${isOnline ? 'text-green-800 dark:text-green-300' : 'text-slate-900 dark:text-white'}">${printer.name}</h3>
|
||||||
|
<p class="text-xs sm:text-sm ${isOnline ? 'text-green-700 dark:text-green-400' : 'text-slate-600 dark:text-slate-400'}">${printer.model}</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-end">
|
||||||
|
<span class="status-badge-new ${isOnline ? 'online' : 'offline'}">
|
||||||
|
${isOnline ? '🟢' : '🔴'} ${statusText}
|
||||||
|
</span>
|
||||||
|
${printer.last_checked ?
|
||||||
|
`<span class="text-xs ${isOnline ? 'text-green-600 dark:text-green-400' : 'text-slate-500 dark:text-slate-400'} mt-1.5">
|
||||||
|
Geprüft: ${formatTime(printer.last_checked)}
|
||||||
|
</span>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2 mb-4">
|
||||||
|
<div class="printer-info-row">
|
||||||
|
<svg class="printer-info-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
</svg>
|
||||||
|
<span>${printer.location}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="printer-info-row">
|
||||||
|
<svg class="printer-info-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||||
|
</svg>
|
||||||
|
<span class="font-mono text-xs">${printer.mac_address}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="printer-info-row">
|
||||||
|
<svg class="printer-info-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9v-9m0-9v9" />
|
||||||
|
</svg>
|
||||||
|
<span class="font-mono text-xs">${printer.plug_ip}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button class="printer-detail-btn flex-1 action-btn-new primary text-xs" data-printer-id="${printer.id}">
|
||||||
|
Details
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{% if current_user.is_admin %}
|
||||||
|
<button class="delete-printer-btn flex-1 action-btn-new danger text-xs" data-printer-id="${printer.id}">
|
||||||
|
<svg class="h-3.5 w-3.5 sm:h-4 sm:w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||||
|
</svg>
|
||||||
|
Löschen
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
// Sichere Ereignisbehandlung: Alte Event-Listener entfernen, indem wir neue Listener hinzufügen
|
||||||
|
document.querySelectorAll('.printer-detail-btn').forEach(btn => {
|
||||||
|
// Entferne alte Listener durch Klon-Ersetzung
|
||||||
|
const clonedBtn = btn.cloneNode(true);
|
||||||
|
btn.parentNode.replaceChild(clonedBtn, btn);
|
||||||
|
|
||||||
|
// Füge neuen Listener hinzu
|
||||||
|
clonedBtn.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const printerId = parseInt(this.getAttribute('data-printer-id'));
|
||||||
|
if (printerId) {
|
||||||
|
showPrinterDetail(printerId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.delete-printer-btn').forEach(btn => {
|
||||||
|
// Entferne alte Listener durch Klon-Ersetzung
|
||||||
|
const clonedBtn = btn.cloneNode(true);
|
||||||
|
btn.parentNode.replaceChild(clonedBtn, btn);
|
||||||
|
|
||||||
|
// Füge neuen Listener hinzu
|
||||||
|
clonedBtn.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const printerId = parseInt(this.getAttribute('data-printer-id'));
|
||||||
|
if (printerId) {
|
||||||
|
deletePrinter(printerId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
Reference in New Issue
Block a user