impolementation of standardised time zone
Release Inventarsystem / release-docker (push) Successful in 2m17s

This commit is contained in:
2026-08-23 11:28:53 +02:00
parent 6f9994c4ce
commit 65fadf7f5f
+405 -175
View File
@@ -95,6 +95,7 @@ import Web.modules.database.settings as cfg
from Web.modules.database.settings import MongoClient from Web.modules.database.settings import MongoClient
from tenant import get_tenant_context, get_tenant_db, get_tenant_trial_status, purge_expired_trial_tenants from tenant import get_tenant_context, get_tenant_db, get_tenant_trial_status, purge_expired_trial_tenants
from pymongo.errors import DuplicateKeyError from pymongo.errors import DuplicateKeyError
from zoneinfo import ZoneInfo
app = Flask(__name__, static_folder='static') # Correctly set static folder app = Flask(__name__, static_folder='static') # Correctly set static folder
@@ -199,7 +200,7 @@ def rollover_student_card_classes(dry_run=False, *, max_class=None, graduate_lab
client = MongoClient(MONGODB_HOST, MONGODB_PORT) client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB] db = client[MONGODB_DB]
col = db['student_cards'] col = db['student_cards']
now = datetime.datetime.now() now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
cursor = list(col.find({}, {'Klasse': 1})) cursor = list(col.find({}, {'Klasse': 1}))
for doc in cursor: for doc in cursor:
@@ -269,7 +270,7 @@ def api_library_return_by_code():
return jsonify({'ok': False, 'message': 'Kein Bibliotheksmedium für diesen Code gefunden.'}), 404 return jsonify({'ok': False, 'message': 'Kein Bibliotheksmedium für diesen Code gefunden.'}), 404
item_id = str(item_doc['_id']) item_id = str(item_doc['_id'])
now = datetime.datetime.now() now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
# If item already available -> nothing to return # If item already available -> nothing to return
if item_doc.get('Verfuegbar', True): if item_doc.get('Verfuegbar', True):
@@ -1025,7 +1026,7 @@ def _prepare_invoice_pdf_payload(invoice_data, borrow_doc=None, item_doc=None):
item_id = str(item_doc.get('_id')) item_id = str(item_doc.get('_id'))
return { return {
'invoice_number': invoice_data.get('invoice_number') or _build_invoice_number(borrow_doc.get('_id', ''), datetime.datetime.now()), 'invoice_number': invoice_data.get('invoice_number') or _build_invoice_number(borrow_doc.get('_id', ''), datetime.datetime.now(ZoneInfo("Europe/Berlin"))),
'created_at': created_at_raw, 'created_at': created_at_raw,
'created_at_display': created_at_display, 'created_at_display': created_at_display,
'borrower': invoice_data.get('borrower') or borrow_doc.get('User', '-'), 'borrower': invoice_data.get('borrower') or borrow_doc.get('User', '-'),
@@ -1048,7 +1049,7 @@ def _create_notification(db, *, audience, notif_type, title, message, target_use
if existing: if existing:
return False return False
now = datetime.datetime.now() now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
payload = { payload = {
'Audience': audience, 'Audience': audience,
'Type': notif_type, 'Type': notif_type,
@@ -1278,7 +1279,7 @@ def _build_reminder_message(item_name, start_dt=None, end_dt=None):
def create_return_reminders(): def create_return_reminders():
"""Create one-time reminders for day-1 and planned-end events.""" """Create one-time reminders for day-1 and planned-end events."""
now = datetime.datetime.now() now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
one_day_ago = now - datetime.timedelta(days=1) one_day_ago = now - datetime.timedelta(days=1)
client = None client = None
@@ -1456,7 +1457,7 @@ def update_appointment_statuses():
- Geplante Termine, die aktiviert werden sollten - Geplante Termine, die aktiviert werden sollten
- Aktive Termine, die beendet werden sollten - Aktive Termine, die beendet werden sollten
""" """
current_time = datetime.datetime.now(datetime.timezone.utc) current_time = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
try: try:
# Hole alle Termine mit Status 'planned' oder 'active' # Hole alle Termine mit Status 'planned' oder 'active'
@@ -1589,137 +1590,7 @@ def update_appointment_statuses():
f"Failed to create activation notification for {appointment.get('_id')}: {notif_err}" f"Failed to create activation notification for {appointment.get('_id')}: {notif_err}"
) )
elif it.is_library_item(appointment.get('Item')): elif it.is_library_item(appointment.get('Item')):
create_return_reminders() current_time = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
client.close()
if updated_count > 0:
app.logger.warning(
f"Appointment status update finished: {updated_count} changed ({activated_count} active, {completed_count} completed)"
)
except Exception as e:
app.logger.error(f"Automatic appointment status update failed: {e}")
# Initialize scheduler instances
scheduler = BackgroundScheduler()
_scheduler_initialized = False
_scheduler_worker_id = None # Tracks the unique ID of the worker holding the lock
def _initialize_scheduler():
"""Initialize the background scheduler safely using an atomic MongoDB lock."""
global _scheduler_initialized, _scheduler_worker_id
if _scheduler_initialized or not getattr(cfg, 'SCHEDULER_ENABLED', False):
return
try:
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB]
locks_col = db['system_locks']
# 1. Generate a unique ID for this specific worker process
_scheduler_worker_id = str(uuid.uuid4())
now = datetime.datetime.now(datetime.timezone.utc)
# 2. Ensure the lock document exists (initialize if missing)
try:
locks_col.insert_one({
'_id': 'scheduler_lock',
'locked_at': datetime.datetime.min.replace(tzinfo=datetime.timezone.utc),
'worker_id': None
})
except DuplicateKeyError:
pass # Document already exists, which is expected
# 3. Try to acquire the lock atomically
# We only acquire if the current lock is older than 5 minutes (stale/crashed worker)
# or if it was explicitly released (1970 init date)
lock_timeout = now - datetime.timedelta(minutes=5)
acquired = locks_col.find_one_and_update(
{
'_id': 'scheduler_lock',
'locked_at': {'$lt': lock_timeout}
},
{
'$set': {
'locked_at': now,
'worker_id': _scheduler_worker_id
}
}
)
if acquired is not None:
# 4. Lock acquired successfully - this is the master worker
scheduler.add_job(func=create_daily_backup, trigger="interval", hours=cfg.BACKUP_INTERVAL_HOURS)
scheduler.add_job(func=update_appointment_statuses, trigger="interval", minutes=cfg.SCHEDULER_INTERVAL_MIN)
scheduler.add_job(func=create_return_reminders, trigger="interval", minutes=cfg.SCHEDULER_INTERVAL_MIN)
scheduler.add_job(func=cleanup_expired_trial_tenants, trigger="interval", hours=1)
# 5. Add a heartbeat job to keep the lock alive
def renew_lock_heartbeat():
try:
hb_client = MongoClient(MONGODB_HOST, MONGODB_PORT)
hb_db = hb_client[MONGODB_DB]
hb_db['system_locks'].update_one(
{'_id': 'scheduler_lock', 'worker_id': _scheduler_worker_id},
{'$set': {'locked_at': datetime.datetime.now(datetime.timezone.utc)}}
)
hb_client.close()
except Exception as e:
app.logger.error(f"Scheduler heartbeat failed: {e}")
# Run heartbeat every 2 minutes (safely below the 5-minute timeout)
scheduler.add_job(func=renew_lock_heartbeat, trigger="interval", minutes=2)
scheduler.start()
_scheduler_initialized = True
app.logger.info(f"Scheduler started successfully (Worker ID: {_scheduler_worker_id})")
else:
app.logger.info("Scheduler skipped - another active worker holds the MongoDB lock")
client.close()
except Exception as e:
app.logger.error(f"Failed to initialize scheduler with MongoDB lock: {e}")
_scheduler_initialized = False
# Initialize scheduler on app startup
_initialize_scheduler()
def _shutdown_scheduler():
"""Gracefully shut down the scheduler and release the MongoDB lock."""
global _scheduler_initialized, _scheduler_worker_id
if getattr(cfg, 'SCHEDULER_ENABLED', False) and _scheduler_initialized:
try:
scheduler.shutdown(wait=False)
# Release the lock so a newly spawned worker can immediately take over
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB]
db['system_locks'].update_one(
{'_id': 'scheduler_lock', 'worker_id': _scheduler_worker_id},
{'$set': {'locked_at': datetime.datetime.min.replace(tzinfo=datetime.timezone.utc)}}
)
client.close()
app.logger.info(f"Scheduler shut down and lock released (Worker ID: {_scheduler_worker_id})")
except Exception as e:
app.logger.error(f"Error during scheduler shutdown: {e}")
atexit.register(_shutdown_scheduler)
def create_return_reminders():
"""
Prüft aktive Ausleihungen auf Überschreitung der Rückgabefrist,
erhöht Mahnstufen, versendet E-Mails/Benachrichtigungen und sperrt Nutzer.
"""
current_time = datetime.datetime.now(datetime.timezone.utc)
try: try:
client = MongoClient(MONGODB_HOST, MONGODB_PORT) client = MongoClient(MONGODB_HOST, MONGODB_PORT)
@@ -1811,13 +1682,137 @@ def create_return_reminders():
f"Dies ist ein automatischer Bericht über deinen Status: <b>Dein Konto wurde soeben für neue Reservierungen gesperrt.</b><br><br>" f"Dies ist ein automatischer Bericht über deinen Status: <b>Dein Konto wurde soeben für neue Reservierungen gesperrt.</b><br><br>"
f"Die Sperre wird erst aufgehoben, sobald der Artikel im System als zurückgegeben gemeldet wurde.") f"Die Sperre wird erst aufgehoben, sobald der Artikel im System als zurückgegeben gemeldet wurde.")
send(email=user_email, subject=subject, note=note, sender="Bibliotheksverwaltung") send(email=user_email, subject=subject, note=note, sender="Bibliotheksverwaltung")
app.logger.warning(f"Nutzer {target_user} gesperrt. 2. Mahnung an {user_email} gesendet.") app.logger.warning(
f"Nutzer {target_user} gesperrt. 2. Mahnung an {user_email} gesendet.")
client.close() client.close()
except Exception as e: except Exception as e:
app.logger.error(f"Fehler bei der automatischen Mahnlauf-Prüfung: {e}") app.logger.error(f"Fehler bei der automatischen Mahnlauf-Prüfung: {e}")
client.close()
if updated_count > 0:
app.logger.warning(
f"Appointment status update finished: {updated_count} changed ({activated_count} active, {completed_count} completed)"
)
except Exception as e:
app.logger.error(f"Automatic appointment status update failed: {e}")
# Initialize scheduler instances
scheduler = BackgroundScheduler()
_scheduler_initialized = False
_scheduler_worker_id = None # Tracks the unique ID of the worker holding the lock
def _initialize_scheduler():
"""Initialize the background scheduler safely using an atomic MongoDB lock."""
global _scheduler_initialized, _scheduler_worker_id
if _scheduler_initialized or not getattr(cfg, 'SCHEDULER_ENABLED', False):
return
try:
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB]
locks_col = db['system_locks']
# 1. Generate a unique ID for this specific worker process
_scheduler_worker_id = str(uuid.uuid4())
now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
# 2. Ensure the lock document exists (initialize if missing)
try:
locks_col.insert_one({
'_id': 'scheduler_lock',
'locked_at': datetime.datetime.min.replace(tzinfo=datetime.timezone.utc),
'worker_id': None
})
except DuplicateKeyError:
pass # Document already exists, which is expected
# 3. Try to acquire the lock atomically
# We only acquire if the current lock is older than 5 minutes (stale/crashed worker)
# or if it was explicitly released (1970 init date)
lock_timeout = now - datetime.timedelta(minutes=5)
acquired = locks_col.find_one_and_update(
{
'_id': 'scheduler_lock',
'locked_at': {'$lt': lock_timeout}
},
{
'$set': {
'locked_at': now,
'worker_id': _scheduler_worker_id
}
}
)
if acquired is not None:
# 4. Lock acquired successfully - this is the master worker
scheduler.add_job(func=create_daily_backup, trigger="interval", hours=cfg.BACKUP_INTERVAL_HOURS)
scheduler.add_job(func=update_appointment_statuses, trigger="interval", minutes=cfg.SCHEDULER_INTERVAL_MIN)
scheduler.add_job(func=create_return_reminders, trigger="interval", minutes=cfg.SCHEDULER_INTERVAL_MIN)
scheduler.add_job(func=cleanup_expired_trial_tenants, trigger="interval", hours=1)
# 5. Add a heartbeat job to keep the lock alive
def renew_lock_heartbeat():
try:
hb_client = MongoClient(MONGODB_HOST, MONGODB_PORT)
hb_db = hb_client[MONGODB_DB]
hb_db['system_locks'].update_one(
{'_id': 'scheduler_lock', 'worker_id': _scheduler_worker_id},
{'$set': {'locked_at': datetime.datetime.now(ZoneInfo("Europe/Berlin"))}}
)
hb_client.close()
except Exception as e:
app.logger.error(f"Scheduler heartbeat failed: {e}")
# Run heartbeat every 2 minutes (safely below the 5-minute timeout)
scheduler.add_job(func=renew_lock_heartbeat, trigger="interval", minutes=2)
scheduler.start()
_scheduler_initialized = True
app.logger.info(f"Scheduler started successfully (Worker ID: {_scheduler_worker_id})")
else:
app.logger.info("Scheduler skipped - another active worker holds the MongoDB lock")
client.close()
except Exception as e:
app.logger.error(f"Failed to initialize scheduler with MongoDB lock: {e}")
_scheduler_initialized = False
# Initialize scheduler on app startup
_initialize_scheduler()
def _shutdown_scheduler():
"""Gracefully shut down the scheduler and release the MongoDB lock."""
global _scheduler_initialized, _scheduler_worker_id
if getattr(cfg, 'SCHEDULER_ENABLED', False) and _scheduler_initialized:
try:
scheduler.shutdown(wait=False)
# Release the lock so a newly spawned worker can immediately take over
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB]
db['system_locks'].update_one(
{'_id': 'scheduler_lock', 'worker_id': _scheduler_worker_id},
{'$set': {'locked_at': datetime.datetime.min.replace(tzinfo=datetime.timezone.utc)}}
)
client.close()
app.logger.info(f"Scheduler shut down and lock released (Worker ID: {_scheduler_worker_id})")
except Exception as e:
app.logger.error(f"Error during scheduler shutdown: {e}")
atexit.register(_shutdown_scheduler)
"""-------------------------------------------------------------File Upload Validation----------------------------------------------------------------------------- """ """-------------------------------------------------------------File Upload Validation----------------------------------------------------------------------------- """
def allowed_file(filename, file_content=None, max_size_mb=cfg.MAX_UPLOAD_MB): def allowed_file(filename, file_content=None, max_size_mb=cfg.MAX_UPLOAD_MB):
@@ -2501,7 +2496,7 @@ def _upload_student_cards_excel():
student_cards_col.insert_one({ student_cards_col.insert_one({
'AusweisId': row['ausweis_id'], 'AusweisId': row['ausweis_id'],
'StandardAusleihdauer': int(row['default_borrow_days']), 'StandardAusleihdauer': int(row['default_borrow_days']),
'Erstellt': datetime.datetime.now(), 'Erstellt': datetime.datetime.now(ZoneInfo("Europe/Berlin")),
**encrypted_payload, **encrypted_payload,
}) })
created_total += 1 created_total += 1
@@ -3635,10 +3630,246 @@ def library_loans_admin():
client.close() client.close()
def generate_test_ausleihen():
"""
Generiert Testdaten für die Überprüfung der Mahnungs- und Sperrlogik.
Erstellt Dummy-Nutzer, Dummy-Items und Ausleihungen mit manipulierten Daten (DueDate).
"""
current_time = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
try:
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB]
ausleihungen = db['ausleihungen']
users_col = db['users']
items_col = db['items']
print("Generiere Testdaten...")
# 1. Test-Nutzer erstellen oder aktualisieren (upsert)
test_users = [
{"username": "test_user_stufe1", "email": "stufe1@test.local", "is_blocked": False},
{"username": "test_user_stufe2", "email": "stufe2@test.local", "is_blocked": False},
{"username": "test_user_frisch", "email": "frisch@test.local", "is_blocked": False}
]
for u in test_users:
users_col.update_one(
{'username': u['username']},
{'$set': u},
upsert=True
)
# 2. Test-Items (Gegenstände) erstellen
item1_res = items_col.insert_one({"Name": "Biologie Buch (Test Stufe 1)"})
item2_res = items_col.insert_one({"Name": "iPad Pro (Test Stufe 2)"})
item3_res = items_col.insert_one({"Name": "Taschenrechner (Test Frisch)"})
# 3. Test-Ausleihungen generieren
# Fall 1: 20 Tage drüber, Mahnstufe 0 -> Sollte Stufe 1 triggern
due_date_stufe1 = current_time - datetime.timedelta(days=20)
# Fall 2: 30 Tage drüber, Mahnstufe 1 -> Sollte Stufe 2 triggern & Nutzer sperren
due_date_stufe2 = current_time - datetime.timedelta(days=30)
# Fall 3: 5 Tage drüber, Mahnstufe 0 -> Sollte NICHTS tun (da < 14 Tage)
due_date_frisch = current_time - datetime.timedelta(days=5)
test_ausleihungen = [
{
"User": encrypt_text("test_user_stufe1"),
"Item": str(item1_res.inserted_id),
"Status": "active",
"DueDate": due_date_stufe1,
"Mahnstufe": 0,
"LastUpdated": current_time
},
{
"User": "test_user_stufe2",
"Item": str(item2_res.inserted_id),
"Status": "active",
"DueDate": due_date_stufe2,
"Mahnstufe": 1, # Hat bereits Stufe 1
"LastUpdated": due_date_stufe2 # Wurde in der Vergangenheit gemahnt
},
{
"User": "test_user_frisch",
"Item": str(item3_res.inserted_id),
"Status": "active",
"DueDate": due_date_frisch,
"Mahnstufe": 0,
"LastUpdated": current_time
}
]
# In die Datenbank einfügen
ausleihungen.insert_many(test_ausleihungen)
print("Erfolgreich! Folgende Testdaten wurden in die DB geschrieben:")
print(
f"- Fall 1 (Mahnstufe 1 Trigger): Nutzer 'test_user_stufe1', fällig war am {due_date_stufe1.strftime('%Y-%m-%d')}")
print(
f"- Fall 2 (Mahnstufe 2 & Sperre Trigger): Nutzer 'test_user_stufe2', fällig war am {due_date_stufe2.strftime('%Y-%m-%d')}")
print(
f"- Fall 3 (Ignorieren - zu früh): Nutzer 'test_user_frisch', fällig war am {due_date_frisch.strftime('%Y-%m-%d')}")
except Exception as e:
print(f"Fehler beim Erstellen der Testdaten: {e}")
finally:
client.close()
@app.route('/test_mahnungen') @app.route('/test_mahnungen')
def test_mahnungen(): def test_mahnungen():
# Testdaten (Mock-Daten), um alle if/else Bedingungen im HTML zu testen # Testdaten (Mock-Daten), um alle if/else Bedingungen im HTML zu testen
test_overdue_list = [ generate_test_ausleihen()
create_return_reminders()
"""Admin overview for overdue library items (Mahnungen)."""
if 'username' not in session:
flash(
'Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!',
'error')
return redirect(url_for('login'))
current_permissions = us.get_effective_permissions(session['username'])
# Hier nutzen wir beispielhaft die library_loans_admin Berechtigung
if not current_permissions['pages'].get('library_loans_admin', False):
flash(
'Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!',
'error')
return redirect(url_for('library_view'))
if not cfg.MODULES.is_enabled('library'):
flash('Bibliotheks-Modul ist deaktiviert.', 'error')
return redirect(url_for('home_admin'))
def fmt_dt(dt):
try:
return dt.strftime('%d.%m.%Y') if dt else 'Unbekannt'
except Exception:
return str(dt) if dt else 'Unbekannt'
def safe_decrypt(val):
if not val or not isinstance(val, str):
return val or ''
try:
return decrypt_text(val)
except Exception:
return val
current_time = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
client = None
try:
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB]
ausleihungen_col = db['ausleihungen']
items_col = db['items']
users_col = db['users']
student_cards_col = db['student_cards']
# Überfällige Ausleihen abrufen (Status 'active' und DueDate in der Vergangenheit)
overdue_records = list(ausleihungen_col.find({
'Status': 'active',
'DueDate': {'$lt': current_time}
}).sort('DueDate', 1))
overdue_list = []
if overdue_records:
# Bulk-Lookups zur Vermeidung von N+1 Queries
item_ids = []
for r in overdue_records:
i_id = r.get('Item')
if i_id:
try:
item_ids.append(ObjectId(str(i_id)))
except Exception:
item_ids.append(str(i_id))
items_cursor = items_col.find({
'_id': {'$in': item_ids}
}, {'Name': 1, 'Code_4': 1})
item_map = {str(item['_id']): item for item in items_cursor}
raw_users = [r.get('User') for r in overdue_records if r.get('User')]
users_cursor = users_col.find({'username': {'$in': raw_users}}, {'username': 1, 'is_blocked': 1})
user_block_map = {u.get('username'): u.get('is_blocked', False) for u in users_cursor}
class_map = {}
all_cards = list(student_cards_col.find({}, {'SchülerName': 1, 'Klasse': 1, 'AusweisId': 1}))
for card in all_cards:
raw_cls = card.get('Klasse')
dec_cls = safe_decrypt(raw_cls)
if not dec_cls:
continue
raw_name = card.get('SchülerName')
dec_name = safe_decrypt(raw_name)
ausweis_id = card.get('AusweisId')
if dec_name:
class_map[dec_name.strip().lower()] = dec_cls
if raw_name:
class_map[raw_name] = dec_cls
if ausweis_id:
class_map[str(ausweis_id).strip().lower()] = dec_cls
# Datenaufbereitung für das Template
for record in overdue_records:
item_id = str(record.get('Item') or '')
item_doc = item_map.get(item_id, {})
item_name = item_doc.get('Name', item_id)
item_code = item_doc.get('Code_4', '')
if item_code:
item_name = f"{item_name} ({item_code})"
raw_user = record.get('User', '')
decrypted_user = safe_decrypt(raw_user)
display_user = decrypted_user if decrypted_user else (raw_user or 'Unbekannt')
user_class = (
safe_decrypt(record.get('Klasse'))
or safe_decrypt(record.get('Class'))
or safe_decrypt(record.get('school_class'))
or class_map.get(raw_user, '')
or class_map.get(decrypted_user, '')
or class_map.get(display_user.strip().lower(), '')
or '—'
)
due_date_obj = record.get('DueDate')
days_overdue = (current_time - due_date_obj).days if due_date_obj else 0
is_blocked = user_block_map.get(raw_user, False)
overdue_list.append({
'id': str(record.get('_id')),
'item_name': item_name,
'user': display_user,
'klasse': user_class,
'due_date': fmt_dt(due_date_obj),
'days_overdue': days_overdue,
'mahnstufe': record.get('Mahnstufe', 0),
'is_blocked': is_blocked
})
return render_template(
'mahnungen_admin.html',
overdue_list=overdue_list,
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
)
except Exception as e:
app.logger.error(f"Error loading mahnungen admin view: {e}")
flash('Fehler beim Laden der Mahnungsverwaltung.', 'error')
return redirect(url_for('home_admin'))
finally:
if client:
client.close()
""" test_overdue_list = [
{ {
"user": "Max Mustermann", "user": "Max Mustermann",
"klasse": "10A", "klasse": "10A",
@@ -3669,7 +3900,7 @@ def test_mahnungen():
] ]
# Render das Template und übergebe die Testdaten # Render das Template und übergebe die Testdaten
return render_template('mahnungen_admin.html', overdue_list=test_overdue_list, APP_VERSION="1.0.0") return render_template('mahnungen_admin.html', overdue_list=test_overdue_list, APP_VERSION="1.0.0")"""
@app.route('/mahnungen_admin') @app.route('/mahnungen_admin')
@@ -3708,7 +3939,7 @@ def mahnungen_admin():
except Exception: except Exception:
return val return val
current_time = datetime.datetime.now(datetime.timezone.utc) current_time = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
client = None client = None
try: try:
@@ -4119,7 +4350,7 @@ def api_library_scan_action():
item_id = str(item_doc['_id']) item_id = str(item_doc['_id'])
borrower_name = card_doc.get('SchülerName') or f"Ausweis {student_card_id}" borrower_name = card_doc.get('SchülerName') or f"Ausweis {student_card_id}"
now = datetime.datetime.now() now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
if item_doc.get('Verfuegbar', True): if item_doc.get('Verfuegbar', True):
borrow_duration_days = None borrow_duration_days = None
@@ -4142,9 +4373,8 @@ def api_library_scan_action():
card_default = cfg.STUDENT_DEFAULT_BORROW_DAYS card_default = cfg.STUDENT_DEFAULT_BORROW_DAYS
borrow_duration_days = max(1, min(card_default, cfg.STUDENT_MAX_BORROW_DAYS)) borrow_duration_days = max(1, min(card_default, cfg.STUDENT_MAX_BORROW_DAYS))
end_date = now + datetime.timedelta(days=borrow_duration_days) if borrow_duration_days else None
it.update_item_status(item_id, False, borrower_name) it.update_item_status(item_id, False, borrower_name)
au.add_ausleihung(item_id, borrower_name, now, end_date=end_date) au.add_ausleihung(item_id, borrower_name, now)
_append_audit_event_standalone( _append_audit_event_standalone(
event_type='ausleihung_borrowed', event_type='ausleihung_borrowed',
@@ -4406,7 +4636,7 @@ def api_library_item_update(item_id):
'Autor': author, 'Autor': author,
'Code_4': code_4, 'Code_4': code_4,
'ItemType': media_type, 'ItemType': media_type,
'LastUpdated': datetime.datetime.now() 'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin"))
} }
if normalized_isbn: if normalized_isbn:
@@ -4600,7 +4830,7 @@ def student_cards_admin():
{'$set': { {'$set': {
'AusweisId': ausweis_id, 'AusweisId': ausweis_id,
'StandardAusleihdauer': int(default_borrow_days), 'StandardAusleihdauer': int(default_borrow_days),
'Aktualisiert': datetime.datetime.now(), 'Aktualisiert': datetime.datetime.now(ZoneInfo("Europe/Berlin")),
**encrypted_payload **encrypted_payload
}} }}
) )
@@ -4639,7 +4869,7 @@ def student_cards_admin():
student_cards.insert_one({ student_cards.insert_one({
'AusweisId': ausweis_id, 'AusweisId': ausweis_id,
'StandardAusleihdauer': int(default_borrow_days), 'StandardAusleihdauer': int(default_borrow_days),
'Erstellt': datetime.datetime.now(), 'Erstellt': datetime.datetime.now(ZoneInfo("Europe/Berlin")),
**encrypted_payload, **encrypted_payload,
}) })
flash('Neuer Ausweis wurde hinzugefügt.', 'success') flash('Neuer Ausweis wurde hinzugefügt.', 'success')
@@ -4696,7 +4926,7 @@ def student_cards_print():
return render_template( return render_template(
'student_cards_print.html', 'student_cards_print.html',
student_cards=all_cards, student_cards=all_cards,
current_datetime=datetime.datetime.now() current_datetime=datetime.datetime.now(ZoneInfo("Europe/Berlin"))
) )
@@ -4730,7 +4960,7 @@ def student_card_barcode_print():
return render_template( return render_template(
'student_card_barcode_print.html', 'student_card_barcode_print.html',
student_cards=all_cards, student_cards=all_cards,
current_datetime=datetime.datetime.now(), current_datetime=datetime.datetime.now(ZoneInfo("Europe/Berlin")),
download_link=url_for('student_card_barcode_download') download_link=url_for('student_card_barcode_download')
) )
@@ -4926,7 +5156,7 @@ def student_card_barcode_download():
pdf_buffer, pdf_buffer,
mimetype='application/pdf', mimetype='application/pdf',
as_attachment=True, as_attachment=True,
download_name=f'schuelerausweise_all_{datetime.datetime.now().strftime("%Y%m%d_%H%M%S")}.pdf' download_name=f'schuelerausweise_all_{datetime.datetime.now(ZoneInfo("Europe/Berlin")).strftime("%Y%m%d_%H%M%S")}.pdf'
) )
except Exception as e: except Exception as e:
app.logger.error(f"Error occurred while generating PDF for card {card['AusweisId']}: {e}") app.logger.error(f"Error occurred while generating PDF for card {card['AusweisId']}: {e}")
@@ -5112,7 +5342,7 @@ def student_card_class_barcode_download():
c.save() c.save()
pdf_buffer.seek(0) pdf_buffer.seek(0)
filename = f'ausweise_klasse_{class_name.replace(" ", "_")}_{datetime.datetime.now().strftime("%Y%m%d")}.pdf' filename = f'ausweise_klasse_{class_name.replace(" ", "_")}_{datetime.datetime.now(ZoneInfo("Europe/Berlin")).strftime("%Y%m%d")}.pdf'
return send_file( return send_file(
pdf_buffer, pdf_buffer,
@@ -6395,7 +6625,7 @@ def upload_item():
def _soft_delete_item_groups(db, root_item_ids, username): def _soft_delete_item_groups(db, root_item_ids, username):
"""Soft-delete one or more item groups and their borrow records.""" """Soft-delete one or more item groups and their borrow records."""
now = datetime.datetime.now() now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
unique_group_item_ids = [] unique_group_item_ids = []
seen_ids = set() seen_ids = set()
@@ -6916,7 +7146,7 @@ def update_group():
# 1. Shared Fields (Group Logic) # 1. Shared Fields (Group Logic)
# These apply to every item in the group # These apply to every item in the group
shared_update = {'LastUpdated': datetime.datetime.now()} shared_update = {'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin"))}
for source_key, target_key in ( for source_key, target_key in (
('name', 'Name'), ('name', 'Name'),
('ort', 'Ort'), ('ort', 'Ort'),
@@ -6991,7 +7221,7 @@ def report_damage(id):
if not item_doc: if not item_doc:
return jsonify({'success': False, 'message': 'Objekt nicht gefunden.'}), 404 return jsonify({'success': False, 'message': 'Objekt nicht gefunden.'}), 404
now = datetime.datetime.now() now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
damage_entry = { damage_entry = {
'description': description, 'description': description,
'reported_by': session['username'], 'reported_by': session['username'],
@@ -7095,7 +7325,7 @@ def mark_damage_repaired(id):
return jsonify({'success': False, 'message': 'Keine offenen Schäden vorhanden.'}), 400 return jsonify({'success': False, 'message': 'Keine offenen Schäden vorhanden.'}), 400
active_borrow = ausleihungen_col.find_one({'Item': str(id), 'Status': 'active'}, {'_id': 1}) active_borrow = ausleihungen_col.find_one({'Item': str(id), 'Status': 'active'}, {'_id': 1})
now = datetime.datetime.now() now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
repair_entry = { repair_entry = {
'repaired_by': session['username'], 'repaired_by': session['username'],
'repaired_at': now, 'repaired_at': now,
@@ -7272,7 +7502,7 @@ def ausleihen(id):
card_default = cfg.STUDENT_DEFAULT_BORROW_DAYS card_default = cfg.STUDENT_DEFAULT_BORROW_DAYS
borrow_duration_days = max(1, min(card_default, cfg.STUDENT_MAX_BORROW_DAYS)) borrow_duration_days = max(1, min(card_default, cfg.STUDENT_MAX_BORROW_DAYS))
start_date = datetime.datetime.now() start_date = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
end_date = None end_date = None
if borrow_duration_days: if borrow_duration_days:
end_date = start_date + datetime.timedelta(days=borrow_duration_days) end_date = start_date + datetime.timedelta(days=borrow_duration_days)
@@ -7361,7 +7591,7 @@ def ausleihen(id):
# Before borrowing, block if there's a conflicting planned booking # Before borrowing, block if there's a conflicting planned booking
try: try:
now = datetime.datetime.now() now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
# Fetch planned bookings for this item from DB # Fetch planned bookings for this item from DB
planned = au.get_planned_ausleihungen() planned = au.get_planned_ausleihungen()
# Count relevant upcoming planned bookings for today or ongoing # Count relevant upcoming planned bookings for today or ongoing
@@ -7422,12 +7652,12 @@ def ausleihen(id):
return redirect(url_for(redirect_target)) return redirect(url_for(redirect_target))
# If we reach here, we can borrow the requested number of exemplars # If we reach here, we can borrow the requested number of exemplars
current_date = datetime.datetime.now().strftime('%d.%m.%Y %H:%M') current_date = datetime.datetime.now(ZoneInfo("Europe/Berlin")).strftime('%d.%m.%Y %H:%M')
# If the item doesn't use exemplars (single item) # If the item doesn't use exemplars (single item)
if total_exemplare <= 1: if total_exemplare <= 1:
it.update_item_status(id, False, effective_borrower) it.update_item_status(id, False, effective_borrower)
start_date = datetime.datetime.now() start_date = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
au.add_ausleihung(id, effective_borrower, start_date, end_date=end_date) au.add_ausleihung(id, effective_borrower, start_date, end_date=end_date)
_append_audit_event_standalone( _append_audit_event_standalone(
event_type='ausleihung_returned', event_type='ausleihung_returned',
@@ -7470,7 +7700,7 @@ def ausleihen(id):
it.update_item_status(id, False, username) it.update_item_status(id, False, username)
# Create ausleihung records for each borrowed exemplar # Create ausleihung records for each borrowed exemplar
start_date = datetime.datetime.now() start_date = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
for exemplar in new_borrowed_exemplars: for exemplar in new_borrowed_exemplars:
exemplar_id = f"{id}_{exemplar['number']}" exemplar_id = f"{id}_{exemplar['number']}"
au.add_ausleihung(exemplar_id, effective_borrower, start_date, end_date=end_date, exemplar_data={ au.add_ausleihung(exemplar_id, effective_borrower, start_date, end_date=end_date, exemplar_data={
@@ -7532,7 +7762,7 @@ def zurueckgeben(id):
'Status': 'active' 'Status': 'active'
}) })
end_date = datetime.datetime.now() end_date = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
original_user = item.get('User', username) original_user = item.get('User', username)
updated_count = 0 updated_count = 0
@@ -7544,7 +7774,7 @@ def zurueckgeben(id):
{'$set': { {'$set': {
'Status': 'completed', 'Status': 'completed',
'End': end_date, 'End': end_date,
'LastUpdated': datetime.datetime.now() 'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin"))
}} }}
) )
@@ -7771,7 +8001,7 @@ def check_availability():
# Also include current availability if checking today and item is borrowed now # Also include current availability if checking today and item is borrowed now
item_doc = items_col.find_one({'_id': ObjectId(item_id)}) item_doc = items_col.find_one({'_id': ObjectId(item_id)})
if item_doc and not item_doc.get('Verfuegbar', True): if item_doc and not item_doc.get('Verfuegbar', True):
now = datetime.datetime.now() now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
if req_start.date() == now.date(): if req_start.date() == now.date():
conflicts.append({'status': 'active', 'user': item_doc.get('User'), 'start': None, 'end': None, 'period': None, 'id': None}) conflicts.append({'status': 'active', 'user': item_doc.get('User'), 'start': None, 'end': None, 'period': None, 'id': None})
@@ -8464,7 +8694,7 @@ def delete_user():
items_col = db['items'] items_col = db['items']
users_col = db['users'] # Direkter Zugriff auf die User-Collection users_col = db['users'] # Direkter Zugriff auf die User-Collection
now = datetime.datetime.now() now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
# 1. Aktive Ausleihen abschließen # 1. Aktive Ausleihen abschließen
ausleihungen.update_many( ausleihungen.update_many(
@@ -8906,7 +9136,7 @@ def admin_reset_borrowing(borrow_id):
item_id = rec.get('Item') item_id = rec.get('Item')
user = rec.get('User') user = rec.get('User')
now = datetime.datetime.now() now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
if status == 'active': if status == 'active':
ausleihungen.update_one({'_id': rec['_id']}, {'$set': {'Status': 'completed', 'End': now, 'LastUpdated': now}}) ausleihungen.update_one({'_id': rec['_id']}, {'$set': {'Status': 'completed', 'End': now, 'LastUpdated': now}})
# Free the item # Free the item
@@ -9004,7 +9234,7 @@ def admin_create_invoice(borrow_id):
mark_destroyed = request.form.get('mark_destroyed') == 'on' mark_destroyed = request.form.get('mark_destroyed') == 'on'
close_borrowing = request.form.get('close_borrowing') == 'on' close_borrowing = request.form.get('close_borrowing') == 'on'
now = datetime.datetime.now() now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
existing_invoice = borrow_doc.get('InvoiceData') or {} existing_invoice = borrow_doc.get('InvoiceData') or {}
if existing_invoice.get('invoice_number'): if existing_invoice.get('invoice_number'):
@@ -9173,7 +9403,7 @@ def admin_mark_invoice_paid(borrow_id):
flash('Rechnung ist bereits als bezahlt markiert.', 'info') flash('Rechnung ist bereits als bezahlt markiert.', 'info')
return redirect(url_for('admin_borrowings')) return redirect(url_for('admin_borrowings'))
now = datetime.datetime.now() now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
result = ausleihungen.update_one( result = ausleihungen.update_one(
{'_id': borrow_doc['_id']}, {'_id': borrow_doc['_id']},
{ {
@@ -9252,7 +9482,7 @@ def admin_pay_invoice_extended(borrow_id):
flash('Für diese Ausleihung existiert keine Rechnung.', 'warning') flash('Für diese Ausleihung existiert keine Rechnung.', 'warning')
return redirect(request.referrer or url_for('library_loans_admin')) return redirect(request.referrer or url_for('library_loans_admin'))
now = datetime.datetime.now() now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
update_fields = {'LastUpdated': now} update_fields = {'LastUpdated': now}
if invoice_data.get('paid') is not True: if invoice_data.get('paid') is not True:
@@ -9322,7 +9552,7 @@ def resolve_repaired_item(item_id):
flash('Element nicht gefunden.', 'error') flash('Element nicht gefunden.', 'error')
return redirect(request.referrer or url_for('library_loans_admin')) return redirect(request.referrer or url_for('library_loans_admin'))
now = datetime.datetime.now() now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
open_reports = item_doc.get('DamageReports', []) or [] open_reports = item_doc.get('DamageReports', []) or []
item_update = { item_update = {
@@ -9459,7 +9689,7 @@ def admin_add_invoice_correction(borrow_id):
flash('Ungültiger Korrekturbetrag.', 'error') flash('Ungültiger Korrekturbetrag.', 'error')
return redirect(url_for('admin_borrowings')) return redirect(url_for('admin_borrowings'))
now = datetime.datetime.now() now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
correction_number = f"CORR-{now.strftime('%Y%m%d-%H%M%S')}-{str(borrow_doc.get('_id'))[-6:].upper()}" correction_number = f"CORR-{now.strftime('%Y%m%d-%H%M%S')}-{str(borrow_doc.get('_id'))[-6:].upper()}"
correction_entry = { correction_entry = {
@@ -9549,7 +9779,7 @@ def resolve_repaired_item_funct(item_id, action, new_code_4="", current_user="ad
return False, "Item nicht in der Datenbank gefunden." return False, "Item nicht in der Datenbank gefunden."
series_group_id = item.get('SeriesGroupId') series_group_id = item.get('SeriesGroupId')
now = datetime.datetime.now() now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
legacy_damage_unset = { legacy_damage_unset = {
'has_damage': "", 'has_damage': "",
@@ -10060,7 +10290,7 @@ def admin_anonymize_names():
result = student_cards_col.update_one( result = student_cards_col.update_one(
{'_id': card_doc['_id']}, {'_id': card_doc['_id']},
{'$set': {'Aktualisiert': datetime.datetime.now(), **encrypted_payload}} {'$set': {'Aktualisiert': datetime.datetime.now(ZoneInfo("Europe/Berlin")), **encrypted_payload}}
) )
if result.modified_count > 0: if result.modified_count > 0:
cards_updated += 1 cards_updated += 1
@@ -11174,7 +11404,7 @@ def mark_notification_read(notification_id):
{'_id': ObjectId(notification_id)}, {'_id': ObjectId(notification_id)},
{ {
'$addToSet': {'ReadBy': username}, '$addToSet': {'ReadBy': username},
'$set': {'UpdatedAt': datetime.datetime.now()} '$set': {'UpdatedAt': datetime.datetime.now(ZoneInfo("Europe/Berlin"))}
} }
) )
if result.modified_count > 0: if result.modified_count > 0:
@@ -11814,7 +12044,7 @@ def schedule_appointment():
return jsonify({'success': False, 'message': f'Fehler beim Prüfen der Verfügbarkeit'}), 500 return jsonify({'success': False, 'message': f'Fehler beim Prüfen der Verfügbarkeit'}), 500
# Check if the appointment should already be active # Check if the appointment should already be active
now = datetime.datetime.now() now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
initial_status = 'active' if start_datetime <= now else 'planned' initial_status = 'active' if start_datetime <= now else 'planned'
# Create the appointment # Create the appointment