Compare commits

...

16 Commits

Author SHA1 Message Date
Aiirondev_dev 2a6c40eb5a Fix for the CRSF Token implementation callability
Release Inventarsystem / release-docker (push) Successful in 2m16s
2026-08-24 12:35:16 +02:00
Aiirondev_dev 432d6da798 Fix for the CRSF Token implementation
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-24 12:12:55 +02:00
Aiirondev_dev 2cdf0d2169 implementation of the export for users
Release Inventarsystem / release-docker (push) Successful in 3m8s
2026-08-24 11:56:03 +02:00
Aiirondev_dev 9a4d10def7 implementation of the Email add on check
Release Inventarsystem / release-docker (push) Successful in 2m19s
2026-08-23 22:14:54 +02:00
Aiirondev_dev 10ae98245c Make the Mahnungen accueally dependent on the student_ausleih_dauer
Release Inventarsystem / release-docker (push) Successful in 2m17s
2026-08-23 21:59:16 +02:00
Aiirondev_dev 359a8f8ab1 Make the Mahnungen accueally dependent on the student_ausleih_dauer
Release Inventarsystem / release-docker (push) Successful in 2m19s
2026-08-23 21:45:02 +02:00
Aiirondev_dev 3171455f55 Fix of a logik error
Release Inventarsystem / release-docker (push) Successful in 2m17s
2026-08-23 21:14:04 +02:00
Aiirondev_dev a35bb3e048 Test data fixes
Release Inventarsystem / release-docker (push) Successful in 2m17s
2026-08-23 21:06:46 +02:00
Aiirondev_dev a0409a03dc Test data fixes
Release Inventarsystem / release-docker (push) Successful in 2m16s
2026-08-23 21:01:20 +02:00
Aiirondev_dev 627b1b76c4 Test data fixes
Release Inventarsystem / release-docker (push) Successful in 2m19s
2026-08-23 20:59:02 +02:00
Aiirondev_dev cd199f105d Test data
Release Inventarsystem / release-docker (push) Successful in 2m16s
2026-08-23 20:46:00 +02:00
Aiirondev_dev f77f7024dd Push notification fix
Release Inventarsystem / release-docker (push) Successful in 2m18s
2026-08-23 20:31:20 +02:00
Aiirondev_dev 5202c5e25e Push notification fix
Release Inventarsystem / release-docker (push) Successful in 2m16s
2026-08-23 20:19:21 +02:00
Aiirondev_dev 2b1b01f4c1 Fix of an dispalying issue
Release Inventarsystem / release-docker (push) Successful in 2m16s
2026-08-23 20:16:44 +02:00
Aiirondev_dev 55c21ac851 Test data error
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-23 19:57:05 +02:00
Aiirondev_dev 8069c6011c Change of a naming error
Release Inventarsystem / release-docker (push) Successful in 2m16s
2026-08-23 19:54:42 +02:00
4 changed files with 183 additions and 53 deletions
+110 -42
View File
@@ -1160,7 +1160,7 @@ def _build_reminder_message(item_name, start_dt=None, end_dt=None):
)
def create_return_reminders_admin():
def create_return_reminders():
"""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_admin():
},
{'User': 1, 'Item': 1, 'Status': 1, 'Start': 1, 'End': 1}
))
item_ids = []
for entry in candidates:
item_id = entry.get('Item')
@@ -1517,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"
@@ -1531,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}}
@@ -1565,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}}
@@ -1596,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}")
@@ -3566,19 +3566,19 @@ def generate_test_ausleihen():
test_students = [
{
"AusweisId": "TEST-AUSWEIS-01",
"SchülerName": "Max Mustermann (Stufe 1 Test)",
"SchülerName": encrypt_text("Max Mustermann (Stufe 1 Test)"),
"Klasse": "9a",
"is_blocked": False
},
{
"AusweisId": "TEST-AUSWEIS-02",
"SchülerName": "Erika Musterfrau (Stufe 2 Test)",
"SchülerName": encrypt_text("Erika Musterfrau (Stufe 2 Test)"),
"Klasse": "10b",
"is_blocked": False
},
{
"AusweisId": "TEST-AUSWEIS-03",
"SchülerName": "Tom Frisch (Keine Mahnung Test)",
"SchülerName": encrypt_text("Tom Frisch (Keine Mahnung Test)"),
"Klasse": "8c",
"is_blocked": False
}
@@ -3599,19 +3599,22 @@ def generate_test_ausleihen():
"Name": "Biologie Buch (Test Stufe 1)",
"ItemType": item_type,
"Verfuegbar": False,
"Exemplare": 1
"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
"Exemplare": 1,
"Code_4": "TR-9988"
})
item3_res = items_col.insert_one({
"Name": "Taschenrechner (Test Frisch)",
"ItemType": item_type,
"Verfuegbar": False,
"Exemplare": 1
"Exemplare": 1,
"Code_4": "TR-9988"
})
# 3. Test-Ausleihungen generieren
@@ -3626,13 +3629,13 @@ def generate_test_ausleihen():
# 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") if 'encrypt_text' in globals() else "TEST-AUSWEIS-01"
user_val_2 = encrypt_text("TEST-AUSWEIS-02") if 'encrypt_text' in globals() else "TEST-AUSWEIS-02"
user_val_3 = encrypt_text("TEST-AUSWEIS-03") if 'encrypt_text' in globals() else "TEST-AUSWEIS-03"
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(user_val_1),
"User": user_val_1,
"Item": str(item1_res.inserted_id),
"Status": "active",
"DueDate": due_date_stufe1,
@@ -3640,7 +3643,7 @@ def generate_test_ausleihen():
"LastUpdated": current_time
},
{
"User": encrypt_text(user_val_2),
"User": user_val_2,
"Item": str(item2_res.inserted_id),
"Status": "active",
"DueDate": due_date_stufe2,
@@ -3648,11 +3651,11 @@ def generate_test_ausleihen():
"LastUpdated": due_date_stufe2
},
{
"User": encrypt_text(user_val_3),
"User": user_val_3,
"Item": str(item3_res.inserted_id),
"Status": "active",
"DueDate": due_date_frisch,
"Mahnstufe": 0,
"Mahnstufe": 2,
"LastUpdated": current_time
}
]
@@ -3748,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)
@@ -3767,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')
@@ -3796,6 +3790,14 @@ 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'),
email_service_enabled=cfg.EMAIL_ENABLED
)
@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."""
@@ -8626,6 +8628,72 @@ def register_csv():
mimetype='application/pdf'
)
@app.route('/export_users_csv', methods=['POST'])
def export_users_csv():
"""
Exports a filtered list of users to a CSV file.
Receives a JSON array of usernames from the frontend.
"""
if 'username' not in session:
flash('Ihnen ist es nicht gestattet, diese Aktion auszuführen.', 'error')
return redirect(url_for('login'))
usernames_json = request.form.get('usernames')
if not usernames_json:
flash('Keine Benutzer zum Exportieren ausgewählt.', 'error')
return redirect(url_for('user_del'))
try:
target_usernames = json.loads(usernames_json)
except json.JSONDecodeError:
flash('Ungültige Daten beim Export übermittelt.', 'error')
return redirect(url_for('user_del'))
# Fetch all users
all_users = us.get_all_users()
# Create CSV in memory
si = io.StringIO()
# Delimiter set to semicolon (standard for Excel in German locales)
cw = csv.writer(si, delimiter=';')
# Write headers
cw.writerow(['Benutzername', 'Vorname', 'Nachname', 'Administrator', 'Rechte-Preset'])
for user in all_users:
encrypted_username = user.get('Username')
if not encrypted_username:
continue
uname = decrypt_text(encrypted_username)
# Only process users that were present in the frontend's filtered view
if uname in target_usernames:
try:
permissions_payload = us.get_effective_permissions(uname)
preset = permissions_payload.get('preset', 'standard_user')
except Exception:
preset = 'standard_user'
try:
name = us.get_name(uname) or ""
last_name = us.get_last_name(uname) or ""
except Exception:
name = ""
last_name = ""
admin_status = "Ja" if user.get('Admin', False) else "Nein"
cw.writerow([uname, name, last_name, admin_status, preset])
# Prefix with BOM (Byte Order Mark) to ensure Excel reads UTF-8 (Umlauts) correctly
output = '\ufeff' + si.getvalue()
return Response(
output,
mimetype="text/csv",
headers={"Content-disposition": "attachment; filename=benutzer_export.csv"}
)
@app.route('/user_del', methods=['GET'])
def user_del():
+9 -3
View File
@@ -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()
)
+2
View File
@@ -179,10 +179,12 @@
</td>
<td class="text-end">
<div class="btn-group" role="group">
{% if mail_service_enabled %}
<button type="button" class="btn btn-sm btn-outline-primary"
onclick="openEmailModal('{{ item.id }}', '{{ item.schueler_name }}', '{{ item.email }}')">
<i class="bi bi-envelope"></i> E-Mail
</button>
{% endif %}
<button type="button" class="btn btn-sm btn-outline-warning text-dark"
onclick="resetMahnung('{{ item.id }}', '{{ item.schueler_name }}')">
<i class="bi bi-arrow-counterclockwise"></i> Zurücksetzen
+62 -8
View File
@@ -53,8 +53,11 @@
<option value="desc">Absteigend</option>
</select>
</div>
<div class="col-md-2">
<button class="btn btn-secondary w-100" onclick="resetFilters()">Filter zurücksetzen</button>
<div class="col-md-3">
<div class="d-flex gap-2">
<button class="btn btn-secondary flex-grow-1" onclick="resetFilters()">Zurücksetzen</button>
<button class="btn btn-success flex-grow-1" onclick="exportFilteredUsers()">Exportieren</button>
</div>
</div>
</div>
<div class="mt-2 text-muted small" id="filter-count"></div>
@@ -82,19 +85,20 @@
<td>{{ permission_presets.get(user.permission_preset, {}).get('label', user.permission_preset) }}</td>
<td class="actions">
<form method="POST" action="{{ url_for('delete_user') }}" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="username" value="{{ user.username }}">
<button type="submit" class="btn btn-danger btn-sm" onclick="return confirm('Sind Sie sicher, dass Sie den Benutzer {{ user.username }} löschen möchten?')">
Löschen
</button>
</form>
<button type="button" class="btn btn-primary btn-sm"
<button type="button" class="btn btn-primary btn-sm"
data-username="{{ user.username }}"
data-firstname="{{ user.name if user.name else '' }}"
data-lastname="{{ user.last_name if user.last_name else '' }}"
onclick="openEditUserModal(this)">
Bearbeiten
</button>
<button type="button" class="btn btn-warning btn-sm"
<button type="button" class="btn btn-warning btn-sm"
onclick="openResetPasswordModal('{{ user.username }}')">
Passwort zurücksetzen
</button>
@@ -124,6 +128,7 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST" action="{{ url_for('admin_update_user_permissions') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="modal-body permissions-modal-body">
<input type="hidden" id="perm-username" name="username">
<div class="mb-3">
@@ -180,6 +185,7 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST" action="{{ url_for('admin_update_user_name') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="modal-body">
<input type="hidden" id="edit-username" name="username">
<div class="mb-3">
@@ -213,6 +219,7 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST" action="{{ url_for('admin_reset_user_password') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="modal-body">
<input type="hidden" id="reset-username" name="username">
<div class="mb-3">
@@ -313,6 +320,54 @@
applyFilters();
}
function exportFilteredUsers() {
var table = document.getElementById('user-table');
var tbody = table.querySelector('tbody');
var rows = Array.from(tbody.querySelectorAll('tr'));
var visibleUsernames = [];
// Sammle alle Benutzernamen, die durch den Filter aktuell NICHT versteckt sind
rows.forEach(function(row) {
if (row.style.display !== 'none') {
var cells = row.querySelectorAll('td');
if (cells.length > 0) {
visibleUsernames.push(cells[0].textContent.trim());
}
}
});
if (visibleUsernames.length === 0) {
alert('Keine Benutzer zum Exportieren vorhanden.');
return;
}
// Dynamisches Formular erstellen
var form = document.createElement('form');
form.method = 'POST';
form.action = "{{ url_for('export_users_csv') }}";
// 1. CSRF-Token als Wert (ohne Klammern) übergeben
var csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = 'csrf_token';
csrfInput.value = "{{ csrf_token }}";
form.appendChild(csrfInput);
// 2. Gefilterte Benutzernamen hinzufügen
var input = document.createElement('input');
input.type = 'hidden';
input.name = 'usernames';
input.value = JSON.stringify(visibleUsernames);
form.appendChild(input);
document.body.appendChild(form);
form.submit();
// Aufräumen
document.body.removeChild(form);
}
document.addEventListener('DOMContentLoaded', function() {
applyFilters();
document.getElementById('filter-search').addEventListener('input', applyFilters);
@@ -332,12 +387,12 @@
var username = button.getAttribute('data-username');
var name = button.getAttribute('data-firstname');
var lastName = button.getAttribute('data-lastname');
document.getElementById('edit-username').value = username;
document.getElementById('edit-username-display').value = username;
document.getElementById('edit-name').value = name;
document.getElementById('edit-last-name').value = lastName;
var modal = new bootstrap.Modal(document.getElementById('editUserModal'));
modal.show();
}
@@ -345,8 +400,7 @@
function openResetPasswordModal(username) {
document.getElementById('reset-username').value = username;
document.getElementById('username-display').value = username;
// Open modal using Bootstrap
var modal = new bootstrap.Modal(document.getElementById('resetPasswordModal'));
modal.show();
}