Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 10ae98245c | |||
| 359a8f8ab1 | |||
| 3171455f55 | |||
| a35bb3e048 | |||
| a0409a03dc | |||
| 627b1b76c4 | |||
| cd199f105d | |||
| f77f7024dd | |||
| 5202c5e25e | |||
| 2b1b01f4c1 | |||
| 55c21ac851 | |||
| 8069c6011c | |||
| 4535800a34 | |||
| e3aea734bb | |||
| ad77514574 | |||
| 9870726c76 |
+133
-79
@@ -1161,7 +1161,7 @@ def _build_reminder_message(item_name, start_dt=None, end_dt=None):
|
||||
|
||||
|
||||
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 for admins."""
|
||||
now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
one_day_ago = now - datetime.timedelta(days=1)
|
||||
|
||||
@@ -1183,7 +1183,6 @@ def create_return_reminders():
|
||||
},
|
||||
{'User': 1, 'Item': 1, 'Status': 1, 'Start': 1, 'End': 1}
|
||||
))
|
||||
|
||||
item_ids = []
|
||||
for entry in candidates:
|
||||
item_id = entry.get('Item')
|
||||
@@ -1202,9 +1201,14 @@ def create_return_reminders():
|
||||
for borrow_doc in candidates:
|
||||
borrow_id = str(borrow_doc.get('_id'))
|
||||
username = str(borrow_doc.get('User', '')).strip()
|
||||
|
||||
if not borrow_id or not username:
|
||||
continue
|
||||
|
||||
# Falls der Benutzername verschlüsselt ist, hier entschlüsseln,
|
||||
# damit der Admin den echten Namen/Ausweis lesen kann.
|
||||
display_user = decrypt_text(username) if 'decrypt_text' in globals() else username
|
||||
|
||||
item_id = str(borrow_doc.get('Item', '')).strip()
|
||||
item_doc = item_docs.get(item_id, {})
|
||||
item_name = item_doc.get('Name') or f'Item {item_id}'
|
||||
@@ -1212,33 +1216,42 @@ def create_return_reminders():
|
||||
start_dt = borrow_doc.get('Start')
|
||||
end_dt = borrow_doc.get('End')
|
||||
|
||||
if isinstance(start_dt, datetime.datetime) and start_dt.tzinfo is None:
|
||||
start_dt = start_dt.replace(tzinfo=datetime.timezone.utc)
|
||||
|
||||
if isinstance(end_dt, datetime.datetime) and end_dt.tzinfo is None:
|
||||
end_dt = end_dt.replace(tzinfo=datetime.timezone.utc)
|
||||
|
||||
if isinstance(start_dt, datetime.datetime) and start_dt <= one_day_ago:
|
||||
_create_notification(
|
||||
db,
|
||||
audience='user',
|
||||
notif_type='return_day_1',
|
||||
title='Erinnerung: Rueckgabe nach 1 Tag',
|
||||
message=_build_reminder_message(item_name, start_dt=start_dt, end_dt=end_dt),
|
||||
target_user=username,
|
||||
reference={'borrow_id': borrow_id, 'item_id': item_id, 'event': 'day_1'},
|
||||
unique_key=f'reminder:day1:{borrow_id}',
|
||||
severity='warning',
|
||||
audience='admin',
|
||||
notif_type='admin_return_day_1',
|
||||
title='Admin-Info: Ausleihe über 1 Tag aktiv',
|
||||
message=f'Nutzer {display_user} hat das Medium "{item_name}" seit über einem Tag ausgeliehen.',
|
||||
target_user=None,
|
||||
reference={'borrow_id': borrow_id, 'item_id': item_id, 'user': username, 'event': 'day_1'},
|
||||
unique_key=f'admin_reminder:day1:{borrow_id}',
|
||||
severity='info',
|
||||
)
|
||||
|
||||
if isinstance(end_dt, datetime.datetime) and end_dt <= now:
|
||||
_create_notification(
|
||||
db,
|
||||
audience='user',
|
||||
notif_type='return_after_end',
|
||||
title='Erinnerung: Geplante Ausleihe ist beendet',
|
||||
message=_build_reminder_message(item_name, start_dt=start_dt, end_dt=end_dt),
|
||||
target_user=username,
|
||||
reference={'borrow_id': borrow_id, 'item_id': item_id, 'event': 'after_end'},
|
||||
unique_key=f'reminder:end:{borrow_id}',
|
||||
audience='admin',
|
||||
notif_type='admin_return_after_end',
|
||||
title='Admin-Info: Geplante Ausleihe überfällig',
|
||||
message=f'Die Ausleihe von "{item_name}" durch {display_user} hat das geplante Enddatum überschritten.',
|
||||
target_user=None,
|
||||
reference={'borrow_id': borrow_id, 'item_id': item_id, 'user': username, 'event': 'after_end'},
|
||||
unique_key=f'admin_reminder:end:{borrow_id}',
|
||||
severity='warning',
|
||||
)
|
||||
except Exception as exc:
|
||||
app.logger.warning(f"Reminder creation failed: {exc}")
|
||||
if 'app' in globals():
|
||||
app.logger.warning(f"Admin reminder creation failed: {exc}")
|
||||
else:
|
||||
print(f"Admin reminder creation failed: {exc}")
|
||||
finally:
|
||||
if client:
|
||||
client.close()
|
||||
@@ -1503,6 +1516,7 @@ def update_appointment_statuses():
|
||||
student_card = _decrypt_student_card_doc(card) if '_decrypt_student_card_doc' in globals() else card
|
||||
student_name = student_card.get('SchülerName', target_ausweis_id)
|
||||
student_class = student_card.get('Klasse', '—')
|
||||
student_ausleih_dauer = student_card.get('StandardAusleihdauer', 14)
|
||||
|
||||
# Gegenstandsdetails laden
|
||||
item_name = "Unbekannter Artikel"
|
||||
@@ -1517,8 +1531,8 @@ def update_appointment_statuses():
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# STUFE 2: >= 28 Tage überfällig -> Ausweis sperren, Stufe 2 setzen & Admins benachrichtigen
|
||||
if days_overdue >= 28 and mahnstufe < 2:
|
||||
# STUFE 2: >= 2 * student_ausleih_dauer Tage überfällig -> Ausweis sperren, Stufe 2 setzen & Admins benachrichtigen
|
||||
if days_overdue >= (int(student_ausleih_dauer) * 2) and mahnstufe < 2:
|
||||
ausleihungen.update_one(
|
||||
{'_id': appt['_id']},
|
||||
{'$set': {'Mahnstufe': 2, 'LastUpdated': current_time}}
|
||||
@@ -1551,16 +1565,16 @@ def update_appointment_statuses():
|
||||
app.logger.warning(f"Fehler beim Erstellen der Admin-Notif (Stufe 2): {n_err}")
|
||||
|
||||
# 2. Web-Push Notification für Admins
|
||||
if 'send_push_to_all_admins' in globals():
|
||||
if 'create_return_reminders' in globals():
|
||||
try:
|
||||
send_push_to_all_admins(title=title, body=body, url=target_url)
|
||||
create_return_reminders(title=title, body=body, url=target_url)
|
||||
except Exception as p_err:
|
||||
app.logger.error(f"Fehler beim Senden der Admin-Push (Stufe 2): {p_err}")
|
||||
|
||||
app.logger.warning(f"Mahnstufe 2 & Ausweis-Sperre für Schülerausweis '{student_name}' ({target_ausweis_id}) gesetzt.")
|
||||
|
||||
# STUFE 1: >= 14 Tage überfällig -> Stufe 1 setzen & Admins benachrichtigen
|
||||
elif days_overdue >= 14 and mahnstufe == 0:
|
||||
elif days_overdue >= int(student_ausleih_dauer) and mahnstufe == 0:
|
||||
ausleihungen.update_one(
|
||||
{'_id': appt['_id']},
|
||||
{'$set': {'Mahnstufe': 1, 'LastUpdated': current_time}}
|
||||
@@ -1582,9 +1596,9 @@ def update_appointment_statuses():
|
||||
app.logger.warning(f"Fehler beim Erstellen der Admin-Notif (Stufe 1): {n_err}")
|
||||
|
||||
# 2. Web-Push Notification für Admins
|
||||
if 'send_push_to_all_admins' in globals():
|
||||
if 'create_return_reminders' in globals():
|
||||
try:
|
||||
send_push_to_all_admins(title=title, body=body, url=target_url)
|
||||
create_return_reminders(title=title, body=body, url=target_url)
|
||||
except Exception as p_err:
|
||||
app.logger.error(f"Fehler beim Senden der Admin-Push (Stufe 1): {p_err}")
|
||||
|
||||
@@ -3534,7 +3548,7 @@ def library_loans_admin():
|
||||
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).
|
||||
Erstellt Dummy-Schülerausweise, Bibliotheks-Items und Ausleihungen mit manipulierten Daten (DueDate).
|
||||
"""
|
||||
current_time = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
|
||||
@@ -3542,43 +3556,86 @@ def generate_test_ausleihen():
|
||||
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
||||
db = client[MONGODB_DB]
|
||||
ausleihungen = db['ausleihungen']
|
||||
users_col = db['users']
|
||||
student_cards_col = db['student_cards']
|
||||
items_col = db['items']
|
||||
|
||||
print("Generiere Testdaten...")
|
||||
print("Generiere Testdaten für das Mahnungssystem...")
|
||||
|
||||
# 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}
|
||||
# 1. Test-Schülerausweise erstellen oder aktualisieren (upsert)
|
||||
# Wir nutzen AusweisIds, die im System als Identifikation dienen
|
||||
test_students = [
|
||||
{
|
||||
"AusweisId": "TEST-AUSWEIS-01",
|
||||
"SchülerName": encrypt_text("Max Mustermann (Stufe 1 Test)"),
|
||||
"Klasse": "9a",
|
||||
"is_blocked": False
|
||||
},
|
||||
{
|
||||
"AusweisId": "TEST-AUSWEIS-02",
|
||||
"SchülerName": encrypt_text("Erika Musterfrau (Stufe 2 Test)"),
|
||||
"Klasse": "10b",
|
||||
"is_blocked": False
|
||||
},
|
||||
{
|
||||
"AusweisId": "TEST-AUSWEIS-03",
|
||||
"SchülerName": encrypt_text("Tom Frisch (Keine Mahnung Test)"),
|
||||
"Klasse": "8c",
|
||||
"is_blocked": False
|
||||
}
|
||||
]
|
||||
|
||||
for u in test_users:
|
||||
users_col.update_one(
|
||||
{'username': u['username']},
|
||||
{'$set': u},
|
||||
for s in test_students:
|
||||
student_cards_col.update_one(
|
||||
{'AusweisId': s['AusweisId']},
|
||||
{'$set': s},
|
||||
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)"})
|
||||
# 2. Test-Items als Bibliotheksartikel erstellen (ItemType wird gesetzt, damit it.is_library_itemgreift)
|
||||
# Passe LIBRARY_ITEM_TYPES an deine verwendeten Typen an (hier beispielhaft 'Buch' / 'Medien')
|
||||
item_type = LIBRARY_ITEM_TYPES[0] if 'LIBRARY_ITEM_TYPES' in globals() and LIBRARY_ITEM_TYPES else 'Buch'
|
||||
|
||||
item1_res = items_col.insert_one({
|
||||
"Name": "Biologie Buch (Test Stufe 1)",
|
||||
"ItemType": item_type,
|
||||
"Verfuegbar": False,
|
||||
"Exemplare": 1,
|
||||
"Code_4": "TR-9988"
|
||||
})
|
||||
item2_res = items_col.insert_one({
|
||||
"Name": "iPad Pro (Test Stufe 2)",
|
||||
"ItemType": item_type,
|
||||
"Verfuegbar": False,
|
||||
"Exemplare": 1,
|
||||
"Code_4": "TR-9988"
|
||||
})
|
||||
item3_res = items_col.insert_one({
|
||||
"Name": "Taschenrechner (Test Frisch)",
|
||||
"ItemType": item_type,
|
||||
"Verfuegbar": False,
|
||||
"Exemplare": 1,
|
||||
"Code_4": "TR-9988"
|
||||
})
|
||||
|
||||
# 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 1: 15 Tage drüber, Mahnstufe 0 -> Sollte Stufe 1 triggern (>= 14 Tage)
|
||||
due_date_stufe1 = current_time - datetime.timedelta(days=15)
|
||||
|
||||
# Fall 2: 30 Tage drüber, Mahnstufe 1 -> Sollte Stufe 2 triggern & Nutzer sperren
|
||||
due_date_stufe2 = current_time - datetime.timedelta(days=30)
|
||||
# Fall 2: 29 Tage drüber, Mahnstufe 1 -> Sollte Stufe 2 triggern & Ausweis sperren (>= 28 Tage)
|
||||
due_date_stufe2 = current_time - datetime.timedelta(days=29)
|
||||
|
||||
# Fall 3: 5 Tage drüber, Mahnstufe 0 -> Sollte NICHTS tun (da < 14 Tage)
|
||||
due_date_frisch = current_time - datetime.timedelta(days=5)
|
||||
|
||||
# Je nachdem, ob dein System die User-Felder in Ausleihungen verschlüsselt speichert:
|
||||
# Hier wird encrypt_text verwendet, falls die Funktion im Scope liegt.
|
||||
user_val_1 = encrypt_text("TEST-AUSWEIS-01")
|
||||
user_val_2 = encrypt_text("TEST-AUSWEIS-02")
|
||||
user_val_3 = encrypt_text("TEST-AUSWEIS-03")
|
||||
|
||||
test_ausleihungen = [
|
||||
{
|
||||
"User": encrypt_text("test_user_stufe1"),
|
||||
"User": user_val_1,
|
||||
"Item": str(item1_res.inserted_id),
|
||||
"Status": "active",
|
||||
"DueDate": due_date_stufe1,
|
||||
@@ -3586,19 +3643,19 @@ def generate_test_ausleihen():
|
||||
"LastUpdated": current_time
|
||||
},
|
||||
{
|
||||
"User": "test_user_stufe2",
|
||||
"User": user_val_2,
|
||||
"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
|
||||
"Mahnstufe": 1, # Hat bereits Stufe 1, wartet auf Eskalation zu Stufe 2
|
||||
"LastUpdated": due_date_stufe2
|
||||
},
|
||||
{
|
||||
"User": "test_user_frisch",
|
||||
"User": user_val_3,
|
||||
"Item": str(item3_res.inserted_id),
|
||||
"Status": "active",
|
||||
"DueDate": due_date_frisch,
|
||||
"Mahnstufe": 0,
|
||||
"Mahnstufe": 2,
|
||||
"LastUpdated": current_time
|
||||
}
|
||||
]
|
||||
@@ -3606,24 +3663,23 @@ def generate_test_ausleihen():
|
||||
# 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')}")
|
||||
print("Erfolgreich! Folgende Testdaten wurden für das Mahnungssystem in die DB geschrieben:")
|
||||
print(f"- Fall 1 (Mahnstufe 1 Trigger): Ausweis 'TEST-AUSWEIS-01', fällig war am {due_date_stufe1.strftime('%Y-%m-%d')} (15 Tage überfällig)")
|
||||
print(f"- Fall 2 (Mahnstufe 2 & Sperre): Ausweis 'TEST-AUSWEIS-02', fällig war am {due_date_stufe2.strftime('%Y-%m-%d')} (29 Tage überfällig)")
|
||||
print(f"- Fall 3 (Zu früh / Ignorieren): Ausweis 'TEST-AUSWEIS-03', fällig war am {due_date_frisch.strftime('%Y-%m-%d')} (5 Tage überfällig)")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Erstellen der Testdaten: {e}")
|
||||
finally:
|
||||
client.close()
|
||||
if client:
|
||||
client.close()
|
||||
|
||||
@app.route('/test_mahnungen')
|
||||
def test_mahnungen():
|
||||
# Testdaten (Mock-Daten), um alle if/else Bedingungen im HTML zu testen
|
||||
generate_test_ausleihen()
|
||||
create_return_reminders()
|
||||
return redirect(url_for('mahnungen_admin'))
|
||||
|
||||
|
||||
@app.route('/mahnungen_admin')
|
||||
@@ -3695,8 +3751,6 @@ def mahnungen_admin():
|
||||
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 = str(record.get('User') or '')
|
||||
decrypted_user = decrypt_text(raw_user)
|
||||
@@ -3714,27 +3768,20 @@ def mahnungen_admin():
|
||||
days_overdue = (current_time_naive - due_date_naive).days
|
||||
else:
|
||||
days_overdue = 0
|
||||
|
||||
overdue_list.append({
|
||||
'id': str(record.get('_id')),
|
||||
'item_name': item_name,
|
||||
'ausweis_id': ausweis_id,
|
||||
'schueler_name': student_name,
|
||||
'klasse': student_class,
|
||||
'email': student_email,
|
||||
'due_date': fmt_dt(due_date_obj),
|
||||
'days_overdue': days_overdue,
|
||||
'mahnstufe': record.get('Mahnstufe', 0),
|
||||
'is_blocked': is_blocked
|
||||
'id': str(record.get('_id')),
|
||||
'item_name': item_name,
|
||||
'item_code': item_code,
|
||||
'ausweis_id': ausweis_id,
|
||||
'schueler_name': student_name,
|
||||
'klasse': student_class,
|
||||
'email': student_email,
|
||||
'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"Fehler beim Laden der Mahnungsverwaltung: {e}")
|
||||
flash('Fehler beim Laden der Mahnungsverwaltung.', 'error')
|
||||
@@ -3743,6 +3790,13 @@ def mahnungen_admin():
|
||||
if client:
|
||||
client.close()
|
||||
|
||||
return render_template(
|
||||
'mahnungen_admin.html',
|
||||
overdue_items=overdue_list,
|
||||
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
||||
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
|
||||
)
|
||||
|
||||
@app.route('/mahnungen_reset', methods=['POST'])
|
||||
def mahnungen_reset():
|
||||
"""Setzt die Mahnstufe einer Ausleihe zurück, verlängert die Frist und entsperrt den Schülerausweis."""
|
||||
|
||||
@@ -17,7 +17,7 @@ def _build_smtp_client():
|
||||
smtp.starttls()
|
||||
smtp.ehlo()
|
||||
if cfg.EMAIL_USERNAME:
|
||||
smtp.login(cfg.EMAIL_USERNAME, cfg.EMAIL_PASSWORD or "")
|
||||
smtp.login("no-reply@invario-software.de", "#,EATwIn,68" or "")
|
||||
return smtp
|
||||
|
||||
|
||||
@@ -68,14 +68,20 @@ def send(email: list | str, subject: str, note: str, sender: str) -> bool:
|
||||
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["Subject"] = str(subject)
|
||||
msg["From"] = f"{sender} <{cfg.EMAIL_USERNAME}>"
|
||||
msg["From"] = f"{sender} <no-reply@invario-software.de>"
|
||||
msg["To"] = str(recipient)
|
||||
|
||||
msg.attach(MIMEText(text_content, "plain"))
|
||||
msg.attach(MIMEText(html_content, "html"))
|
||||
|
||||
#smtp.sendmail(
|
||||
# from_addr=cfg.EMAIL_USERNAME,
|
||||
# to_addrs=[recipient],
|
||||
# msg=msg.as_string()
|
||||
#)
|
||||
|
||||
smtp.sendmail(
|
||||
from_addr=cfg.EMAIL_USERNAME,
|
||||
from_addr="no-reply@invario-software.de",
|
||||
to_addrs=[recipient],
|
||||
msg=msg.as_string()
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user