From cdf7b9c45d05461e497efd7cd10ee7eb2f679080 Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Wed, 12 Aug 2026 16:09:52 +0200 Subject: [PATCH] slight correction to also account for name and last_name --- Web/app.py | 171 +++++++++++++------- Web/templates/register.html | 315 +++++++++++++++++++++++------------- 2 files changed, 313 insertions(+), 173 deletions(-) diff --git a/Web/app.py b/Web/app.py index 209a970..9a021cb 100755 --- a/Web/app.py +++ b/Web/app.py @@ -7865,6 +7865,82 @@ def register(): permission_page_options=PERMISSION_PAGE_OPTIONS ) +def parse_csv_users(file_bytes): + """ + Liest CSV-Dateien extrem robust ein: + - Erkennt automatisch UTF-8, UTF-8-SIG und Latin-1 (für Excel-Umlaute). + - Erkennt automatisch das Trennzeichen (; oder , oder Tab). + - Erkennt Vor- und Nachname unabhängig von der Spaltenreihenfolge und Headernamen. + """ + # 1. Dekodierung mit Fallback für deutsche Excel-Dateien + try: + content = file_bytes.decode('utf-8-sig') + except UnicodeDecodeError: + content = file_bytes.decode('latin-1') + + lines = [line.strip() for line in content.splitlines() if line.strip()] + if not lines: + return [] + + # 2. Trennzeichen ermitteln (; , oder \t) + first_line = lines[0] + if first_line.count(';') >= first_line.count(','): + delimiter = ';' + elif first_line.count('\t') > first_line.count(','): + delimiter = '\t' + else: + delimiter = ',' + + stream = io.StringIO(content, newline=None) + reader = csv.DictReader(stream, delimiter=delimiter) + + parsed_users = [] + + # 3. Auswertung mit DictReader (wenn Header vorhanden sind) + if reader.fieldnames: + for row in reader: + # Keys normalisieren (kleingeschrieben, ohne Leerzeichen) + cleaned_row = {str(k).strip().lower(): str(v).strip() for k, v in row.items() if k and v} + + # Dynamisches Mapping für Vornamen + name = ( + cleaned_row.get('vorname') or + cleaned_row.get('first_name') or + cleaned_row.get('firstname') or + cleaned_row.get('name') or '' + ) + + # Dynamisches Mapping für Nachnamen + last_name = ( + cleaned_row.get('nachname') or + cleaned_row.get('last_name') or + cleaned_row.get('lastname') or + cleaned_row.get('surname') or + cleaned_row.get('familienname') or '' + ) + + # Falls Header nicht erkannt wurden, aber Werte vorhanden sind (z.B. CSV ohne Header) + if not name and not last_name: + vals = [str(v).strip() for v in row.values() if v] + if len(vals) >= 2: + name, last_name = vals[0], vals[1] + + if name or last_name: + parsed_users.append({'name': name, 'last_name': last_name}) + else: + # Fallback für dateien ohne Header + stream.seek(0) + raw_reader = csv.reader(stream, delimiter=delimiter) + for row in raw_reader: + clean_row = [str(cell).strip() for cell in row if str(cell).strip()] + if len(clean_row) >= 2: + # Header-Zeilen überspringen + if clean_row[0].lower() in ['vorname', 'first_name', 'name'] and clean_row[1].lower() in ['nachname', 'last_name']: + continue + parsed_users.append({'name': clean_row[0], 'last_name': clean_row[1]}) + + return parsed_users + def generate_compliant_password(length=16): lowers = string.ascii_lowercase uppers = string.ascii_uppercase @@ -7983,77 +8059,54 @@ def register_csv(): permission_preset = (request.form.get('permission_preset') or 'standard_user').strip() + # 1. CSV über die robuste Funktion einlesen + file_bytes = file.stream.read() + raw_users = parse_csv_users(file_bytes) + + if not raw_users: + flash('Keine gültigen Benutzer in der CSV-Datei gefunden. Bitte prüfen Sie das Format.', 'error') + return redirect(url_for('register')) + created_users = [] - try: - # Read file stream handling UTF-8 and BOM (Excel exports) - file_stream = io.StringIO(file.stream.read().decode("utf-8-sig"), newline=None) + for entry in raw_users: + name = entry['name'] + last_name = entry['last_name'] - # Sniff delimiter (comma or semicolon) - sample = file_stream.read(2048) - file_stream.seek(0) - delimiter = ';' if ';' in sample else ',' + if not name or not last_name: + continue - reader = csv.reader(file_stream, delimiter=delimiter) + # Benutzernamen & Passwort generieren + username = us.build_unique_username_from_name(name, last_name) + password = generate_compliant_password(16) - for row_num, row in enumerate(reader, start=1): - if not row or all(field.strip() == '' for field in row): - continue # Skip empty rows + # In DB speichern + success = us.add_user( + username=username, + password=password, + name=name, + last_name=last_name, + is_student=False, + student_card_id=None, + max_borrow_days=None, + permission_preset=permission_preset, + ) - # Clean entries - row = [field.strip() for field in row] - - # Header row detection (Skip row if it looks like "Name, Nachname") - if row_num == 1 and ('name' in row[0].lower() or 'vorname' in row[0].lower()): - continue - - if len(row) < 2: - continue - - name = row[0] - last_name = row[1] - - if not name or not last_name: - continue - - # 1. Generate Username via backend logic - username = us.build_unique_username_from_name(name, last_name) - - # 2. Generate Random Secure Password - password = generate_compliant_password(16) - - # 3. Add User to DB - success = us.add_user( - username=username, - password=password, - name=name, - last_name=last_name, - is_student=False, - student_card_id=None, - max_borrow_days=None, - permission_preset=permission_preset, - ) - - if success: - created_users.append({ - 'name': name, - 'last_name': last_name, - 'username': username, - 'password': password - }) - - except Exception as e: - flash(f'Fehler beim Verarbeiten der CSV-Datei: {str(e)}', 'error') - return redirect(url_for('register')) + if success: + created_users.append({ + 'name': name, + 'last_name': last_name, + 'username': username, + 'password': password + }) if not created_users: - flash('Keine gültigen Benutzer in der CSV-Datei gefunden.', 'error') + flash('Fehler beim Erstellen der Benutzer aus der CSV.', 'error') return redirect(url_for('register')) - # Generate PDF + # PDF mit Zugangsdaten generieren pdf_buffer = generate_credentials_pdf(created_users) - # Return downloadable PDF file return send_file( pdf_buffer, as_attachment=True, diff --git a/Web/templates/register.html b/Web/templates/register.html index ab3ceb8..5294c0d 100755 --- a/Web/templates/register.html +++ b/Web/templates/register.html @@ -321,11 +321,14 @@ document.addEventListener('DOMContentLoaded', function () { --primary-color: #3498db; --primary-dark: #2980b9; --success-color: #2ecc71; + --success-dark: #27ae60; + --csv-color: #059669; + --csv-dark: #047857; --error-color: #e74c3c; - --text-color: var(--ui-text); + --text-color: var(--ui-text, #1f2937); --light-bg: #f9f9f9; --border-radius: 8px; - --shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + --shadow: 0 4px 6px rgba(0, 0, 0, 0.05); --transition: all 0.3s ease; } @@ -358,37 +361,55 @@ body { } .subtitle { - font-size: 1.1rem; - color: #777; + font-size: 1.05rem; + color: #6b7280; margin-top: 0; } .content { - background-color: var(--ui-surface); + background-color: var(--ui-surface, #ffffff); padding: 2rem; border-radius: var(--border-radius); box-shadow: var(--shadow); margin-bottom: 2rem; } +/* Karten-Layout für Einzel- & Massenregistrierung */ .form-card { - max-width: 500px; - margin: 0 auto; + max-width: 620px; + margin: 0 auto 2.5rem auto; + padding: 1.5rem; + background: var(--ui-surface-soft, #f9fafb); + border: 1px solid #e5e7eb; + border-radius: var(--border-radius); +} + +.form-card:last-child { + margin-bottom: 0; +} + +.form-card h2 { + font-size: 1.4rem; + margin-top: 0; + margin-bottom: 0.5rem; + color: #111827; } .form-group { - margin-bottom: 1.5rem; + margin-bottom: 1.25rem; } .form-group label { display: block; - margin-bottom: 0.5rem; - font-weight: 500; - color: var(--primary-dark); + margin-bottom: 0.4rem; + font-weight: 600; + font-size: 0.95rem; + color: #374151; } .input-container { position: relative; + width: 100%; } .input-icon { @@ -396,24 +417,30 @@ body { left: 1rem; top: 50%; transform: translateY(-50%); - color: #aaa; - font-size: 1.2rem; + color: #9ca3af; + font-size: 1.1rem; + pointer-events: none; + z-index: 1; } +/* Eingabefelder & Selects */ input[type="text"], input[type="password"], +input[type="file"], .form-select { width: 100%; - padding: 0.8rem 1rem 0.8rem 3rem; - border: 1px solid #ddd; + padding: 0.75rem 1rem 0.75rem 2.8rem; + border: 1px solid #d1d5db; border-radius: var(--border-radius); - font-size: 1rem; + font-size: 0.95rem; + background-color: #ffffff; transition: var(--transition); box-sizing: border-box; } input[type="text"]:focus, input[type="password"]:focus, +input[type="file"]:focus, .form-select:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2); @@ -422,14 +449,47 @@ input[type="password"]:focus, .form-select { padding-left: 1rem; + cursor: pointer; +} + +/* Stylings für den CSV-Datei-Upload */ +input[type="file"] { + padding-left: 2.8rem; + cursor: pointer; +} + +input[type="file"]::file-selector-button { + padding: 0.35rem 0.75rem; + border-radius: 4px; + border: 1px solid #d1d5db; + background: #f3f4f6; + color: #374151; + cursor: pointer; + margin-right: 12px; + font-weight: 500; + transition: var(--transition); +} + +input[type="file"]::file-selector-button:hover { + background: #e5e7eb; } input::placeholder { - color: #aaa; + color: #9ca3af; } +code { + background: #e0f2fe; + color: #0369a1; + padding: 2px 6px; + border-radius: 4px; + font-family: monospace; + font-size: 0.9em; +} + +/* Buttons */ .form-actions { - margin-top: 2rem; + margin-top: 1.5rem; } .action-button { @@ -437,17 +497,21 @@ input::placeholder { color: white; border: none; padding: 0.8rem 1.5rem; - border-radius: 30px; + border-radius: var(--border-radius); cursor: pointer; font-weight: 600; transition: var(--transition); width: 100%; font-size: 1rem; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; } .action-button:hover { background-color: var(--primary-dark); - transform: translateY(-2px); + transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); } @@ -456,79 +520,48 @@ input::placeholder { } .register-button:hover { - background-color: #27ae60; + background-color: var(--success-dark); } -.flash-container { - margin-bottom: 1.5rem; +/* Toggle-Button für Passwort-Auge */ +.input-wrapper { + position: relative; + width: 100%; } -.flash { - display: flex; - align-items: center; - padding: 1rem; - margin-bottom: 1rem; - border-radius: var(--border-radius); - animation: fadeIn 0.3s ease-in-out; +.toggle-pw-btn { + position: absolute; + right: 0.75rem; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + cursor: pointer; + font-size: 1.1rem; + padding: 4px; + z-index: 2; + opacity: 0.7; + transition: opacity 0.2s; } -.flash-icon { - margin-right: 0.8rem; - font-size: 1.2rem; - display: flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - border-radius: 50%; - background-color: rgba(255, 255, 255, 0.3); -} - -.flash.success { - background-color: #d4edda; - color: #155724; - border-left: 4px solid var(--success-color); -} - -.flash.error { - background-color: #f8d7da; - color: #721c24; - border-left: 4px solid var(--error-color); -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(-10px); } - to { opacity: 1; transform: translateY(0); } -} - -@media (max-width: 768px) { - .container { - padding: 0 1rem; - margin: 1rem auto; - } - - .content { - padding: 1.5rem; - } - - .header-section h1 { - font-size: 2rem; - } +.toggle-pw-btn:hover { + opacity: 1; } +/* Passwort-Regeln Box */ .password-rules { margin-bottom: 10px; padding: 10px 12px; border: 1px solid #e5e7eb; border-radius: 8px; - background: var(--ui-surface-soft); + background: #ffffff; } .password-rules-title { - margin: 0 0 8px; + margin: 0 0 6px; font-weight: 700; color: #1f2937; - font-size: 0.92rem; + font-size: 0.88rem; } .password-rules ul { @@ -539,10 +572,10 @@ input::placeholder { .pw-rule { position: relative; - padding-left: 22px; - margin: 5px 0; - color: #b91c1c; - font-size: 0.9rem; + padding-left: 20px; + margin: 4px 0; + color: #dc2626; + font-size: 0.85rem; } .pw-rule::before { @@ -561,15 +594,39 @@ input::placeholder { content: '✓'; } +.pw-actions { + display: flex; + gap: 8px; + margin-top: 8px; +} + +.btn-secondary { + padding: 6px 12px; + border: 1px solid #d1d5db; + background-color: #ffffff; + color: #374151; + border-radius: 6px; + cursor: pointer; + font-size: 0.88rem; + font-weight: 500; + transition: var(--transition); +} + +.btn-secondary:hover { + background-color: #f3f4f6; + border-color: #9ca3af; +} + +/* Hinweise & Rechte-Panels */ .anonymize-hint { - margin-top: 10px; + margin-top: 8px; margin-bottom: 0; background: #f0f9ff; border: 1px solid #bae6fd; - border-radius: 8px; - padding: 10px 12px; - color: #0c4a6e; - font-size: 0.92rem; + border-radius: 6px; + padding: 8px 12px; + color: #0369a1; + font-size: 0.88rem; } .permission-panels { @@ -581,13 +638,13 @@ input::placeholder { .permission-panel { border: 1px solid #d1d5db; border-radius: 8px; - padding: 10px; - background: var(--ui-surface-soft); + padding: 12px; + background: #ffffff; } .permission-panel h4 { margin: 0 0 8px; - font-size: 1rem; + font-size: 0.95rem; color: #1f2937; } @@ -595,42 +652,72 @@ input::placeholder { display: flex; align-items: center; gap: 8px; - margin: 4px 0; - color: #1f2937; -} - -/* Neue Styles für die Passwort-Erweiterungen */ -.pw-actions { - display: flex; - gap: 8px; - margin-top: 8px; -} - -.btn-secondary { - padding: 8px 12px; - border: 1px solid #d1d5db; - background-color: #f3f4f6; - border-radius: 4px; + margin: 6px 0; + color: #374151; + font-size: 0.9rem; cursor: pointer; - font-size: 0.9em; - transition: background-color 0.2s; } -.btn-secondary:hover { - background-color: #e5e7eb; +/* Flash Nachrichten */ +.flash-container { + margin-bottom: 1.5rem; } -.input-wrapper { +.flash { display: flex; align-items: center; - gap: 8px; - width: 100%; + padding: 0.9rem 1rem; + margin-bottom: 1rem; + border-radius: var(--border-radius); + animation: fadeIn 0.3s ease-in-out; } -.input-wrapper .input-container { - flex-grow: 1; - margin: 0; +.flash-icon { + margin-right: 0.8rem; + font-size: 1rem; + display: flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + border-radius: 50%; + background-color: rgba(255, 255, 255, 0.4); +} + +.flash.success { + background-color: #d1fae5; + color: #065f46; + border-left: 4px solid var(--csv-color); +} + +.flash.error { + background-color: #fee2e2; + color: #991b1b; + border-left: 4px solid var(--error-color); +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(-8px); } + to { opacity: 1; transform: translateY(0); } +} + +@media (max-width: 768px) { + .container { + padding: 0 0.75rem; + margin: 1rem auto; + } + + .content { + padding: 1rem; + } + + .form-card { + padding: 1rem; + } + + .header-section h1 { + font-size: 1.8rem; + } } - {% endblock %} \ No newline at end of file