Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 140fb5f743 | |||
| 9ea0ae6157 | |||
| bb89b434ed | |||
| ac4d125d73 | |||
| 3f6830e8c8 | |||
| 0ea5d2db26 |
+58
-20
@@ -16,6 +16,7 @@ Features:
|
||||
- History logging of item usage
|
||||
- Booking and reservation of items
|
||||
"""
|
||||
from random import random
|
||||
|
||||
from flask import Flask, render_template, request, redirect, url_for, session, flash, send_from_directory, get_flashed_messages, jsonify, Response, make_response, send_file, abort
|
||||
from werkzeug.utils import secure_filename
|
||||
@@ -2101,6 +2102,20 @@ def _build_student_card_excel_id(student_name, class_name, row_number, used_ids)
|
||||
return candidate
|
||||
|
||||
|
||||
def generate_ausweis_id_excel(existing_ids_set):
|
||||
"""Generates a unique ID and checks against DB and current import queue."""
|
||||
while True:
|
||||
random_digits = "".join(random.choices(string.digits, k=6))
|
||||
new_id = f"ID_{random_digits}"
|
||||
|
||||
# Prüfe sowohl in den bereits in dieser Session generierten IDs als auch in der DB
|
||||
if new_id.upper() not in existing_ids_set and not us.get_user_by_student_card(new_id):
|
||||
print(f"Generated unique ID: {new_id}")
|
||||
return new_id
|
||||
else:
|
||||
print(f"Already found: {new_id}, trying another...")
|
||||
|
||||
|
||||
def _upload_student_cards_excel():
|
||||
"""Bulk import student cards from Excel with automatic name/class mapping."""
|
||||
if 'username' not in session:
|
||||
@@ -2141,12 +2156,12 @@ def _upload_student_cards_excel():
|
||||
|
||||
synonyms = {
|
||||
'ausweis_id': ['ausweis_id', 'ausweisid', 'ausweis-id', 'karte', 'kartennummer', 'card_id', 'id'],
|
||||
'student_name': ['student_name', 'schuelername', 'schülername', 'schueler', 'schüler', 'name', 'vollname', 'vorname_nachname', 'nachname_vorname'],
|
||||
'first_name': ['vorname', 'first_name', 'firstname', 'rufname'],
|
||||
'last_name': ['nachname', 'last_name', 'lastname'],
|
||||
'class_name': ['klasse', 'class', 'class_name', 'jahrgang', 'jahrgangsstufe', 'stufe', 'gruppe', 'asv_klasse'],
|
||||
'notes': ['notizen', 'notes', 'bemerkungen', 'bemerkung', 'hinweis', 'hinweise'],
|
||||
'default_borrow_days': ['standard_ausleihdauer', 'ausleihdauer', 'borrow_days', 'tage', 'leihtage', 'max_borrow_days'],
|
||||
'default_borrow_days': ['standard_ausleihdauer', 'ausleihdauer', 'borrow_days', 'tage', 'leihtage',
|
||||
'max_borrow_days'],
|
||||
}
|
||||
|
||||
def col_index(key):
|
||||
@@ -2158,7 +2173,6 @@ def _upload_student_cards_excel():
|
||||
|
||||
mapped_indices = {
|
||||
'ausweis_id': col_index('ausweis_id'),
|
||||
'student_name': col_index('student_name'),
|
||||
'first_name': col_index('first_name'),
|
||||
'last_name': col_index('last_name'),
|
||||
'class_name': col_index('class_name'),
|
||||
@@ -2177,10 +2191,11 @@ def _upload_student_cards_excel():
|
||||
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||
try:
|
||||
db = client[cfg.MONGODB_DB]
|
||||
student_cards = list(db['student_cards'].find())
|
||||
|
||||
student_cards_cursor = db['student_cards'].find({}, {'AusweisId': 1})
|
||||
existing_ids.update(
|
||||
str(card.get('AusweisId', '')).strip().upper()
|
||||
for card in student_cards.find({}, {'AusweisId': 1})
|
||||
for card in student_cards_cursor
|
||||
if card.get('AusweisId')
|
||||
)
|
||||
|
||||
@@ -2198,27 +2213,29 @@ def _upload_student_cards_excel():
|
||||
return row_values[idx]
|
||||
|
||||
ausweis_id = sanitize_form_value(val('ausweis_id'))
|
||||
student_name = sanitize_form_value(val('student_name'))
|
||||
first_name = sanitize_form_value(val('first_name'))
|
||||
last_name = sanitize_form_value(val('last_name'))
|
||||
class_name = sanitize_form_value(val('class_name'))
|
||||
notes = sanitize_form_value(val('notes'))
|
||||
default_borrow_days = _excel_int(val('default_borrow_days')) or cfg.STUDENT_DEFAULT_BORROW_DAYS
|
||||
|
||||
if not student_name and first_name and last_name:
|
||||
student_name = f'{first_name} {last_name}'.strip()
|
||||
validation_warnings.append((row_number, 'Schülername wurde aus Vorname und Nachname zusammengesetzt'))
|
||||
default_borrow_days = _excel_int(val('default_borrow_days'))
|
||||
if not default_borrow_days:
|
||||
default_borrow_days = 14
|
||||
|
||||
first_name = sanitize_form_value(val('first_name')) or ""
|
||||
last_name = sanitize_form_value(val('last_name')) or ""
|
||||
student_name = f"{first_name} {last_name}".strip()
|
||||
|
||||
if not ausweis_id and not student_name and not class_name:
|
||||
continue
|
||||
|
||||
row_errors = []
|
||||
|
||||
if not student_name:
|
||||
row_errors.append('Schülername fehlt')
|
||||
row_errors.append('Vorname und Nachname fehlen')
|
||||
|
||||
if not ausweis_id and student_name:
|
||||
ausweis_id = _build_student_card_excel_id(student_name, class_name, row_number, existing_ids)
|
||||
ausweis_id = generate_ausweis_id_excel(existing_ids)
|
||||
validation_warnings.append((row_number, f'Ausweis-ID wurde automatisch erzeugt: {ausweis_id}'))
|
||||
existing_ids.add(ausweis_id.upper())
|
||||
elif ausweis_id:
|
||||
ausweis_id = str(ausweis_id).strip().upper()
|
||||
if ausweis_id in existing_ids:
|
||||
@@ -3826,11 +3843,14 @@ def api_item_detail(item_id):
|
||||
{f'<p><strong>Ausgeliehen von:</strong> {html.escape(str(borrower_value))}</p>' if borrower_value and status_label == 'Ausgeliehen' else ''}
|
||||
{borrows_html}
|
||||
"""
|
||||
|
||||
ctx = get_tenant_context()
|
||||
current_tenant_id = ctx.tenant_id if ctx else None
|
||||
|
||||
client.close()
|
||||
return jsonify({
|
||||
'html': detail_html,
|
||||
'images': item.get('Images', item.get('Bilder', []))
|
||||
'images': item.get('Images', item.get('Bilder', [])),
|
||||
'tenant': str(current_tenant_id)
|
||||
}), 200
|
||||
except Exception as e:
|
||||
app.logger.error(f"Error fetching item detail: {e}")
|
||||
@@ -3982,6 +4002,19 @@ def library_admin():
|
||||
back_target='library'
|
||||
)
|
||||
|
||||
def generate_ausweis_id():
|
||||
import random
|
||||
import string
|
||||
|
||||
while True:
|
||||
random_digits = "".join(random.choices(string.digits, k=6))
|
||||
new_id = f"ID_{random_digits}"
|
||||
|
||||
if not us.get_user_by_student_card(new_id):
|
||||
print(f"Generated unique ID: {new_id}")
|
||||
return new_id
|
||||
else:
|
||||
print(f"Already found: {new_id}, trying another...")
|
||||
|
||||
@app.route('/student_cards_admin', methods=['GET', 'POST'])
|
||||
def student_cards_admin():
|
||||
@@ -4082,13 +4115,18 @@ def student_cards_admin():
|
||||
flash('Fehler beim Aktualisieren des Ausweises.', 'error')
|
||||
|
||||
elif action == 'add':
|
||||
if not ausweis_id or not student_name:
|
||||
flash('Bitte Ausweis-ID und Schülername angeben.', 'error')
|
||||
if not student_name:
|
||||
flash('Bitte Schülername angeben.', 'error')
|
||||
else:
|
||||
# Check if ID already exists
|
||||
existing = student_cards.find_one({'AusweisId': ausweis_id})
|
||||
if ausweis_id:
|
||||
existing = student_cards.find_one({'AusweisId': ausweis_id})
|
||||
else:
|
||||
ausweis_id = generate_ausweis_id()
|
||||
existing = False
|
||||
|
||||
if existing:
|
||||
flash('Diese Ausweis-ID existiert bereits.', 'error')
|
||||
flash('Diese ID existiert bereits.', 'error')
|
||||
else:
|
||||
try:
|
||||
encrypted_payload = encrypt_document_fields(
|
||||
|
||||
@@ -1359,9 +1359,11 @@
|
||||
const detailContent = document.getElementById('detailContent');
|
||||
const detailModal = document.getElementById('detailModal');
|
||||
|
||||
// Lade-Status anzeigen und Modal öffnen
|
||||
detailContent.innerHTML = '<p>Lade Details...</p>';
|
||||
detailModal.style.display = 'flex';
|
||||
|
||||
// Daten vom Backend-API-Endpoint abrufen
|
||||
fetch(`/api/item_detail/${itemId}`)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
@@ -1373,18 +1375,40 @@
|
||||
detailContent.innerHTML = data.html;
|
||||
|
||||
const imageArray = data.images || [];
|
||||
const thumbnailInfoMap = data.thumbnailInfo || [];
|
||||
|
||||
if (Array.isArray(imageArray) && imageArray.length > 0) {
|
||||
const imagesHtml = imageArray.map(image => {
|
||||
const imageSrc = image.startsWith('/uploads/') || image.startsWith('http')
|
||||
? image
|
||||
: `/uploads/${image}`;
|
||||
const imagesHtml = imageArray.map((image, index) => {
|
||||
const isVideo = typeof isVideoFile === 'function' ? isVideoFile(image) : /\.(mp4|webm|ogg|mov)$/i.test(image);
|
||||
const thumbnailInfo = thumbnailInfoMap[index];
|
||||
|
||||
return `
|
||||
if (isVideo) {
|
||||
const videoSrc = image.startsWith('/uploads/') || image.startsWith('http')
|
||||
? image
|
||||
: `/uploads/${image}`;
|
||||
|
||||
return `
|
||||
<div class="item-image-wrapper" style="width: 100%; height: auto;">
|
||||
<video src="${videoSrc}" class="item-image ${index === 0 ? 'active-image' : ''}" id="modal-image-${index}" controls preload="metadata" style="width: 100%; height: auto; max-height: 300px;"></video>
|
||||
</div>`;
|
||||
} else {
|
||||
const imageSrc = thumbnailInfo && thumbnailInfo.has_preview
|
||||
? thumbnailInfo.preview_url
|
||||
: (image.startsWith('/uploads/') || image.startsWith('http')
|
||||
? image
|
||||
: `/uploads/${image}`);
|
||||
|
||||
return `
|
||||
<div class="item-image-wrapper">
|
||||
<img src="${imageSrc}" alt="Buchcover / Bild" class="item-image" loading="lazy">
|
||||
</div>
|
||||
`;
|
||||
<img src="${imageSrc}"
|
||||
alt="Buchcover / Bild"
|
||||
class="item-image ${index === 0 ? 'active-image' : ''}"
|
||||
id="modal-image-${index}"
|
||||
loading="lazy"
|
||||
onload="console.log('Bild geladen:', '${imageSrc}')"
|
||||
onerror="console.error('FEHLER beim Laden des Bildes im DOM:', '${imageSrc}')">
|
||||
</div>`;
|
||||
}
|
||||
}).join('');
|
||||
|
||||
const mediaHtml = `<div class="detail-gallery-container">${imagesHtml}</div>`;
|
||||
|
||||
@@ -249,7 +249,7 @@
|
||||
|
||||
<div style="border:1px solid #dbe4ee; border-radius:8px; padding:14px; margin-bottom:16px; background:#f8fbff;">
|
||||
<h3 style="margin:0 0 8px 0;">Excel-Import Bibliotheksausweise</h3>
|
||||
<p style="margin:0 0 10px 0; color:#555;">Laden Sie eine <strong>.xlsx</strong>- oder <strong>.csv</strong>-Datei hoch, zum Beispiel aus <strong>ASV (Amtliche Schuldaten)</strong>. Erkannt werden automatisch Spalten wie <strong>Name</strong>, <strong>Klasse</strong>, <strong>Ausweis-ID</strong>, <strong>Notizen</strong> und <strong>Standard-Ausleihdauer</strong>. Fehlt die Ausweis-ID, wird sie automatisch aus Name und Klasse erzeugt.</p>
|
||||
<p style="margin:0 0 10px 0; color:#555;">Laden Sie eine <strong>.xlsx</strong>- oder <strong>.csv</strong>-Datei hoch, zum Beispiel aus <strong>ASV (Amtliche Schuldaten)</strong>. Erkannt werden automatisch Spalten wie <strong>Name</strong>, <strong>Nachname</strong, <strong>Klasse</strong>, <strong>Ausweis-ID (optional)</strong>, <strong>Notizen (optional)</strong> und <strong>Standard-Ausleihdauer (optional)</strong>.</p>
|
||||
<form method="POST" action="{{ url_for('upload_student_cards_excel') }}" enctype="multipart/form-data" style="display:flex; gap:10px; flex-wrap:wrap; align-items:center;">
|
||||
<input type="file" name="student_cards_excel" accept=".xlsx,.csv" required>
|
||||
<button type="submit" class="btn btn-secondary" name="excel_action" value="validate">Nur validieren</button>
|
||||
@@ -272,7 +272,7 @@
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="ausweis_id">Ausweis-ID *</label>
|
||||
<input type="text" id="ausweis_id" name="ausweis_id" required
|
||||
<input type="text" id="ausweis_id" name="ausweis_id"
|
||||
value="{{ form_data.get('ausweis_id', '') }}"
|
||||
placeholder="z.B. SIS2024001">
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user