Compare commits

...

8 Commits

4 changed files with 138 additions and 109 deletions
+25 -9
View File
@@ -11862,25 +11862,39 @@ def batch_upload_page():
return render_template('upload_batch.html')
def clean_db_field(val):
"""Bereinigt Werte, die fälschlicherweise als String-Listen aus der CSV kommen."""
"""Bereinigt Werte, die fälschlicherweise als String-Listen oder mit Klammern aus der CSV kommen."""
import ast
import pandas as pd
if not val or pd.isna(val):
return None
val_str = str(val).strip()
# Erkennt und entpackt String-Listen wie "['100465']" oder '["100465"]'
if (val_str.startswith("['") and val_str.endswith("']")) or (val_str.startswith('["') and val_str.endswith('"]')):
inner = val_str[2:-2].strip()
return inner if inner and inner != "''" and inner != '""' else None
# Wenn es wie eine Liste aussieht (z.B. "['100177']" oder "['']")
if val_str.startswith("[") and val_str.endswith("]"):
try:
parsed = ast.literal_eval(val_str)
if isinstance(parsed, list):
# Nimm das erste Element der Liste, wenn vorhanden
for item in parsed:
cleaned_item = str(item).strip()
if cleaned_item and cleaned_item not in ("", "''", '""', "None", "nan"):
return cleaned_item
return None
except Exception:
# Fallback bei Syntaxfehlern
inner = val_str[1:-1].strip().replace("'", "").replace('"', '')
return inner if inner and inner not in ("''", '""') else None
if val_str in ("[]", "['']", '[""]', "nan", "None"):
if val_str in ("[]", "['']", '[""]', "nan", "None", "''", '""'):
return None
return val_str
@app.route('/upload_csv_batch', methods=['POST'])
def upload_csv_batch():
"""
@@ -12064,8 +12078,10 @@ def upload_csv_batch():
reservierbar = bool(row.get('Reservierbar', False))
# Code_4 Behandlung
row_code = clean_db_field(row.get('Code_4', ''))
# Code_4 / Barcode sauber extrahieren und bereinigen
raw_code = row.get('Code_4') or row.get('Barcode') or ''
row_code = clean_db_field(raw_code)
if row_code:
unique_code = row_code
elif item_count > 1:
+52
View File
@@ -497,6 +497,58 @@ def check_nm_pwd(username, password):
finally:
client.close()
def add_admin(
username,
password,
name='',
last_name='',
is_student=False,
permission_preset='full_access',
action_permissions=None,
page_permissions=None,
):
"""
Add a new user to the database.
"""
if not check_password_strength(password):
return False
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
try:
db = _get_tenant_db(client)
users = db['users']
permission_defaults = build_default_permission_payload(permission_preset)
if isinstance(action_permissions, dict):
for key, value in action_permissions.items():
permission_defaults['actions'][str(key)] = bool(value)
if isinstance(page_permissions, dict):
for key, value in page_permissions.items():
permission_defaults['pages'][str(key)] = bool(value)
safe_name = name.strip() if name else ''
safe_last_name = last_name.strip() if last_name else ''
user_doc = {
'Username': username,
'Password': hashing(password),
'Admin': True,
'active_ausleihung': None,
'name': dp.encrypt_text(safe_name) if safe_name else '',
'last_name': dp.encrypt_text(safe_last_name) if safe_last_name else '',
'IsStudent': bool(is_student),
'PermissionPreset': permission_defaults['preset'],
'ActionPermissions': permission_defaults['actions'],
'PagePermissions': permission_defaults['pages'],
}
users.insert_one(user_doc)
return True
finally:
client.close()
def add_user(
username,
+57 -48
View File
@@ -48,7 +48,7 @@
</div>
<script>
// 1. Ein echter CSV-Parser, der Zeilenumbrüche und Kommas in Texten korrekt ignoriert
// 1. Robuster CSV-Parser, der Zeilenumbrüche und Kommas in Texten korrekt ignoriert
function parseCSV(csvString) {
const rows = [];
let currentRow = [];
@@ -70,7 +70,6 @@ function parseCSV(csvString) {
} else if ((char === '\n' || char === '\r') && !insideQuotes) {
if (char === '\r' && nextChar === '\n') i++; // Windows Umbrüche überspringen
currentRow.push(currentCell);
// Leere Zeilen ignorieren
if (currentRow.length > 1 || currentRow[0] !== '') {
rows.push(currentRow);
}
@@ -94,7 +93,6 @@ function rowToCSV(rowArray) {
return rowArray.map(cell => {
if (cell === null || cell === undefined) return '';
let cellStr = String(cell);
// Wenn kritische Zeichen drin sind, sauber in Anführungszeichen verpacken
if (cellStr.includes(',') || cellStr.includes('"') || cellStr.includes('\n') || cellStr.includes('\r')) {
return '"' + cellStr.replace(/"/g, '""') + '"';
}
@@ -102,7 +100,7 @@ function rowToCSV(rowArray) {
}).join(',');
}
// 3. Bildnamen extrahieren (bleibt wie es war)
// 3. Bildnamen extrahieren
function extractImageNames(cellValue) {
if (!cellValue) return [];
let raw = String(cellValue).replace(/^["']|["']$/g, '').trim();
@@ -155,11 +153,10 @@ document.getElementById('batchUploadForm').addEventListener('submit', async func
imageMap.set(file.name.toLowerCase(), file);
});
const BATCH_SIZE = 50;
const BATCH_SIZE = 20;
try {
const csvText = await csvFile.text();
// Hier rufen wir jetzt unseren sicheren Parser auf!
const allRows = parseCSV(csvText);
if (allRows.length <= 1) {
@@ -171,10 +168,8 @@ document.getElementById('batchUploadForm').addEventListener('submit', async func
log(`${dataRows.length} Einträge gefunden. Bereite Batches vor...`);
// Spalten-Index von "Images" sicher ermitteln
const imagesColIndex = headerRow.findIndex(h => h.toLowerCase().trim() === 'images');
// In Batches aufteilen
const batches = [];
for (let i = 0; i < dataRows.length; i += BATCH_SIZE) {
batches.push(dataRows.slice(i, i + BATCH_SIZE));
@@ -183,63 +178,77 @@ document.getElementById('batchUploadForm').addEventListener('submit', async func
progressBar.max = batches.length;
progressBar.value = 0;
// Sequenzieller Upload mit Fehlertoleranz pro Batch
for (let b = 0; b < batches.length; b++) {
const batchRows = batches[b];
progressText.textContent = `Lade Batch ${b + 1} von ${batches.length} hoch...`;
const requiredImagesForBatch = new Set();
try {
const requiredImagesForBatch = new Set();
if (imagesColIndex !== -1) {
batchRows.forEach(rowCols => {
if (rowCols[imagesColIndex]) {
const imgNames = extractImageNames(rowCols[imagesColIndex]);
imgNames.forEach(name => {
const fileMatch = imageMap.get(name.toLowerCase());
if (fileMatch) {
requiredImagesForBatch.add(fileMatch);
}
});
if (imagesColIndex !== -1) {
batchRows.forEach(rowCols => {
if (rowCols[imagesColIndex]) {
const imgNames = extractImageNames(rowCols[imagesColIndex]);
imgNames.forEach(name => {
const fileMatch = imageMap.get(name.toLowerCase());
if (fileMatch) {
requiredImagesForBatch.add(fileMatch);
}
});
}
});
}
const batchCsvArray = [headerRow, ...batchRows];
const batchCsvText = batchCsvArray.map(rowToCSV).join('\n');
const batchCsvBlob = new Blob([batchCsvText], { type: 'text/csv' });
const formData = new FormData();
formData.append('csv_file', batchCsvBlob, `batch_${b + 1}.csv`);
if (csrfToken) {
formData.append('csrf_token', csrfToken);
}
requiredImagesForBatch.forEach(imgFile => {
formData.append('images', imgFile);
});
log(`Batch ${b + 1}: ${batchRows.length} Items & ${requiredImagesForBatch.size} zugehörige Bilder.`);
const response = await fetch('/upload_csv_batch', {
method: 'POST',
body: formData,
headers: {
'X-CSRFToken': csrfToken || ''
}
});
}
// Sicherer Zusammenbau des CSV-Batches
const batchCsvArray = [headerRow, ...batchRows];
const batchCsvText = batchCsvArray.map(rowToCSV).join('\n');
const batchCsvBlob = new Blob([batchCsvText], { type: 'text/csv' });
const responseText = await response.text();
let result;
const formData = new FormData();
formData.append('csv_file', batchCsvBlob, `batch_${b + 1}.csv`);
if (csrfToken) {
formData.append('csrf_token', csrfToken);
}
requiredImagesForBatch.forEach(imgFile => {
formData.append('images', imgFile);
});
log(`Batch ${b + 1}: ${batchRows.length} Items & ${requiredImagesForBatch.size} zugehörige Bilder.`);
const response = await fetch('/upload_csv_batch', {
method: 'POST',
body: formData,
headers: {
'X-CSRFToken': csrfToken || ''
try {
result = JSON.parse(responseText);
} catch (parseErr) {
throw new Error(`Server-Fehler (Status ${response.status}). HTML statt JSON erhalten.`);
}
});
const result = await response.json();
if (!response.ok || !result.success) {
throw new Error(result.message || `Server-Fehler ${response.status}`);
}
if (!response.ok || !result.success) {
throw new Error(result.message || `Server-Fehler ${response.status}`);
log(`Batch ${b + 1} erfolgreich abgeschlossen.`);
} catch (batchErr) {
log(`⚠️ FEHLER in Batch ${b + 1}: ${batchErr.message}. Überspringe und fahre fort...`);
console.error(`Batch ${b + 1} fehlgeschlagen:`, batchErr);
}
log(`Batch ${b + 1} abgeschlossen.`);
progressBar.value = b + 1;
}
progressText.textContent = "Upload erfolgreich beendet! Keine verschobenen Spalten mehr.";
progressText.textContent = "Upload-Prozess beendet!";
uploadBtn.disabled = false;
} catch (err) {
+4 -52
View File
@@ -201,63 +201,15 @@ sys.path.insert(0, "/app/Web")
from Web.modules.database import settings
from pymongo import MongoClient
import Web.modules.inventarsystem.data_protection as dp
import Web.modules.database.user as us
tenant_id = sys.argv[1].lower()
mode = sys.argv[2]
sanitized = "".join(c for c in tenant_id if c.isalnum() or c == "_")
db_name = f"inventar_{sanitized}"
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
db = client[db_name]
pw_bytes = "admin123".encode("utf-8")
random_salt = os.urandom(16)
hashed = hashlib.scrypt(pw_bytes, salt=random_salt, n=16384, r=8, p=1)
hashed_pw_string = f"v1${random_salt.hex()}${hashed.hex()}"
action_permissions = {
"can_borrow": True,
"can_insert": True,
"can_edit": True,
"can_delete": True,
"can_manage_users": True,
"can_manage_settings": True,
"can_view_logs": True,
}
page_permissions = {
"home": True,
"tutorial_page": True,
"my_borrowed_items": True,
"notifications_view": True,
"impressum": True,
"license": True,
"library_view": True,
"terminplan": True,
"home_admin": True,
"upload_admin": True,
"library_admin": True,
"admin_borrowings": True,
"library_loans_admin": True,
"admin_damaged_items": True,
"admin_audit_dashboard": True,
"logs": True,
"manage_filters": True,
"manage_locations": True,
}
if db.users.count_documents({"Username": dp.encrypt_text("admin")}) == 0:
db.users.insert_one({
"Username": dp.encrypt_text("admin"),
"Password": hashed_pw_string,
"Admin": True,
"active_ausleihung": None,
"name": dp.encrypt_text("Admin"),
"last_name": dp.encrypt_text("User"),
"IsStudent": False,
"PermissionPreset": "full_access",
"ActionPermissions": action_permissions,
"PagePermissions": page_permissions,
})
us.add_admin("admin", "admin123", "admin", "admin")
us.make_admin("admin")
print("Fallback successfully applied")
if mode == "trial":
db.settings.update_one(