Compare commits

...

2 Commits

Author SHA1 Message Date
Aiirondev_dev fd242a6a0a fix of the json upload format 2026-08-03 23:45:59 +02:00
Aiirondev_dev 2892024969 fix of the json upload format 2026-08-03 23:26:51 +02:00
2 changed files with 203 additions and 90 deletions
+24 -3
View File
@@ -417,8 +417,6 @@ def _is_csrf_exempt_request():
@app.before_request
def _enforce_csrf_protection():
if request.endpoint == 'upload_csv_batch':
return None
if _is_csrf_exempt_request():
_get_csrf_token()
return None
@@ -11883,6 +11881,20 @@ def upload_csv_batch():
username = session.get('username', 'System')
def generate_unique_batch_code(base_code, position):
"""
Generiert einen eindeutigen Code für einen Artikel innerhalb einer Serie (Batch).
:param base_code: Der Code des ersten Artikels in der Gruppe (String oder None).
:param position: Die Position des aktuellen Artikels in der Gruppe (Integer).
:return: Ein eindeutiger Code als String.
"""
if base_code:
return f"{base_code}-{position}"
else:
random_prefix = str(uuid.uuid4())[:6].upper()
return f"BATCH-{random_prefix}-{position}"
fs = get_gridfs()
upload_session_id = str(uuid.uuid4())[:8]
app.logger.info(f"Starting CSV Batch upload session {upload_session_id} - User: {username}")
@@ -12029,6 +12041,15 @@ def upload_csv_batch():
except (ValueError, SyntaxError):
pass
# --- NEU: BILDER-REFERENZEN PRO ARTIKEL DEDUPLIZIEREN ---
# Falls die CSV z.B. ['bild1.jpg', 'bild1.jpg'] enthält, filtern wir das hier heraus,
# damit die GridFS-Datei nicht doppelt als Referenz gespeichert wird.
unique_image_filenames = []
for img in item_image_filenames:
if img not in unique_image_filenames:
unique_image_filenames.append(img)
# --------------------------------------------------------
def parse_filter_col(col_data):
try:
res = ast.literal_eval(str(col_data))
@@ -12056,7 +12077,7 @@ def upload_csv_batch():
str(row['Name']), # 1. Name
ort_val, # 2. Ort
str(row['Beschreibung']), # 3. Beschreibung
item_image_filenames, # 4. Image Filenames (GridFS)
unique_image_filenames, # 4. Image Filenames (GridFS) -> HIER GEÄNDERT
filter_upload, # 5. Filter 1
filter_upload2, # 6. Filter 2
filter_upload3, # 7. Filter 3
+179 -87
View File
@@ -21,6 +21,8 @@
align-items: center;
min-height: 100vh;
margin: 0;
padding: 20px;
box-sizing: border-box;
}
.upload-container {
@@ -56,6 +58,7 @@
border-radius: var(--border-radius);
background: #fafafa;
cursor: pointer;
box-sizing: border-box;
}
.btn-submit {
@@ -80,47 +83,48 @@
cursor: not-allowed;
}
/* Status & Feedback Messages */
#status-message {
margin-top: 1rem;
padding: 1rem;
border-radius: var(--border-radius);
/* Fortschritts- und Log-Bereich */
#uploadProgress {
margin-top: 2rem;
display: none;
}
#progressText {
font-size: 1rem;
margin-bottom: 0.5rem;
color: var(--primary-color);
text-align: center;
}
.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
progress {
width: 100%;
height: 20px;
border-radius: var(--border-radius);
}
.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
#logList {
margin-top: 1rem;
padding: 10px;
font-size: 0.85rem;
color: #555;
max-height: 150px;
overflow-y: auto;
background: #fafafa;
border: 1px solid #ddd;
border-radius: var(--border-radius);
list-style-type: none;
}
.loading {
background-color: #e2e3e5;
color: #383d41;
border: 1px solid #d6d8db;
#logList li {
margin-bottom: 5px;
padding-bottom: 5px;
border-bottom: 1px solid #eee;
}
.spinner {
display: inline-block;
width: 1.5rem;
height: 1.5rem;
border: 3px solid rgba(0,0,0,0.1);
border-radius: 50%;
border-top-color: var(--primary-color);
animation: spin 1s ease-in-out infinite;
vertical-align: middle;
margin-right: 0.5rem;
}
@keyframes spin {
to { transform: rotate(360deg); }
#logList li:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
</style>
</head>
@@ -129,89 +133,177 @@
<div class="upload-container">
<h2>Inventar Batch Upload</h2>
<form id="uploadForm">
<!-- ID auf "batchUploadForm" geändert, damit das JS es findet -->
<form id="batchUploadForm">
<div class="form-group">
<label for="csv_file">1. items.csv Datei auswählen</label>
<!-- Akzeptiert nur CSV Dateien -->
<input type="file" id="csv_file" name="csv_file" accept=".csv" required>
</div>
<div class="form-group">
<label for="images">2. Bilder auswählen</label>
<!-- multiple erlaubt das Auswählen mehrerer Bilder gleichzeitig -->
<input type="file" id="images" name="images" accept="image/*" multiple required>
<small style="color: #666; display: block; margin-top: 5px;">Du kannst mehrere Bilder markieren (Strg/Cmd gedrückt halten).</small>
</div>
<button type="submit" id="submitBtn" class="btn-submit">Daten hochladen</button>
<!-- ID auf "uploadBtn" geändert -->
<button type="submit" id="uploadBtn" class="btn-submit">Daten hochladen</button>
</form>
<div id="status-message"></div>
<!-- Fehlender Container für den Fortschrittsbalken und Logs hinzugefügt -->
<div id="uploadProgress">
<div id="progressText">Starte Upload...</div>
<progress id="progressBar" value="0" max="100"></progress>
<ul id="logList"></ul>
</div>
</div>
<script>
document.getElementById('uploadForm').addEventListener('submit', async function(e) {
e.preventDefault();
document.getElementById('batchUploadForm').addEventListener('submit', async function(e) {
e.preventDefault();
const form = e.target;
const submitBtn = document.getElementById('submitBtn');
const statusDiv = document.getElementById('status-message');
const csvInput = document.getElementById('csv_file');
const imageInput = document.getElementById('images');
const uploadBtn = document.getElementById('uploadBtn');
const progressContainer = document.getElementById('uploadProgress');
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
const logList = document.getElementById('logList');
submitBtn.disabled = true;
submitBtn.innerText = 'Wird verarbeitet...';
statusDiv.className = 'loading';
statusDiv.style.display = 'block';
statusDiv.innerHTML = '<div class="spinner"></div> Lade Dateien hoch und verarbeite Bilder... Bitte warten.';
if (!csvInput.files.length) {
alert("Bitte wähle eine CSV-Datei aus.");
return;
}
const formData = new FormData(form);
uploadBtn.disabled = true;
progressContainer.style.display = 'block';
logList.innerHTML = '';
const fetchOptions = {
method: 'POST',
body: formData,
credentials: 'include',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
};
const log = (msg) => {
const li = document.createElement('li');
li.textContent = msg;
logList.appendChild(li);
logList.scrollTop = logList.scrollHeight; // Auto-scroll
};
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
const csvFile = csvInput.files[0];
const allImages = Array.from(imageInput.files);
const BATCH_SIZE = 50;
if (csrfToken) {
fetchOptions.headers = {
'X-CSRFToken': csrfToken
};
try {
// 1. CSV-Datei lesen
const csvText = await csvFile.text();
// 2. CSV in Zeilen aufteilen
let rows = csvText.split(/\r?\n/).filter(row => row.trim().length > 0);
if (rows.length <= 1) {
throw new Error("CSV-Datei ist leer oder enthält nur Kopfzeilen.");
}
const header = rows[0];
let dataRows = rows.slice(1);
// 3. Client-seitige Deduplizierung (Entfernt exakte Duplikat-Zeilen)
const uniqueRowsSet = new Set();
const uniqueDataRows = [];
let duplicateCount = 0;
for (const row of dataRows) {
if (uniqueRowsSet.has(row)) {
duplicateCount++;
} else {
uniqueRowsSet.add(row);
uniqueDataRows.push(row);
}
}
log(`${uniqueDataRows.length} einzigartige Einträge gefunden. ${duplicateCount} Duplikate entfernt.`);
// Den Index der "Images" Spalte finden
const headers = header.split(',');
const imagesColIndex = headers.findIndex(h => h.trim().replace(/['"]/g, '') === 'Images');
// 4. In Batches (Häppchen) aufteilen
const batches = [];
for (let i = 0; i < uniqueDataRows.length; i += BATCH_SIZE) {
batches.push(uniqueDataRows.slice(i, i + BATCH_SIZE));
}
progressBar.max = batches.length;
progressBar.value = 0;
// 5. Batches nacheinander hochladen
for (let b = 0; b < batches.length; b++) {
const batchRows = batches[b];
progressText.textContent = `Lade Batch ${b + 1} von ${batches.length} hoch...`;
log(`Bereite Batch ${b + 1} vor (${batchRows.length} Artikel)...`);
// CSV für diesen Batch neu zusammensetzen
const batchCsvText = [header, ...batchRows].join('\n');
const batchCsvBlob = new Blob([batchCsvText], { type: 'text/csv' });
// Benötigte Bilder für diesen Batch extrahieren
const requiredImageNames = new Set();
if (imagesColIndex !== -1) {
batchRows.forEach(row => {
const cols = row.split(',');
if (cols[imagesColIndex]) {
try {
let imgStr = cols[imagesColIndex].trim().replace(/^"|"$/g, '').replace(/'/g, '"');
if (imgStr.startsWith('[') && imgStr.endsWith(']')) {
const parsedImages = JSON.parse(imgStr);
parsedImages.forEach(img => requiredImageNames.add(img));
}
} catch (err) {
console.warn("Konnte Bild-Array nicht parsen in Zeile:", row);
}
}
});
}
// Bilder auf die für diesen Batch benötigten filtern
const batchImages = allImages.filter(img => requiredImageNames.has(img.name));
// FormData zusammenbauen
const formData = new FormData();
formData.append('csv_file', batchCsvBlob, `batch_${b+1}.csv`);
batchImages.forEach(img => {
formData.append('images', img);
});
// An Server senden
try {
const response = await fetch('/upload_csv_batch', fetchOptions);
const response = await fetch('/upload_csv_batch', {
method: 'POST',
body: formData
});
// Antwort einmalig als Text auslesen, um sowohl JSON als auch HTML-Fehler abzufangen
const responseText = await response.text();
const result = await response.json();
let result;
try {
result = JSON.parse(responseText);
} catch (jsonError) {
console.error("Server hat kein JSON gesendet. Antwort war:", responseText);
throw new Error("Der Server hat einen HTML-Fehler zurückgegeben (z.B. Nginx 413 Entity Too Large oder Server-Crash). Siehe F12 Konsole.");
if (!response.ok || !result.success) {
throw new Error(result.message || `Server antwortete mit Status ${response.status}`);
}
if (response.ok && result.success) {
statusDiv.className = 'success';
statusDiv.innerHTML = `<strong>Erfolg!</strong><br>${result.message}`;
form.reset();
} else {
statusDiv.className = 'error';
statusDiv.innerHTML = `<strong>Fehler:</strong> ${result.message || 'Ein unbekannter Fehler ist aufgetreten.'}`;
}
} catch (error) {
statusDiv.className = 'error';
statusDiv.innerHTML = `<strong>Fehler:</strong> ${error.message}`;
console.error('Upload Error:', error);
} finally {
submitBtn.disabled = false;
submitBtn.innerText = 'Daten hochladen';
log(`Batch ${b + 1} erfolgreich: ${result.message}`);
} catch (batchErr) {
log(`Fehler in Batch ${b + 1}: ${batchErr.message}`);
alert(`Upload wurde bei Batch ${b + 1} aufgrund eines Fehlers abgebrochen. Prüfe die Logs.`);
break; // Stoppt weitere Uploads, wenn einer fehlschlägt
}
});
</script>
progressBar.value = b + 1;
}
progressText.textContent = "Upload-Vorgang abgeschlossen!";
uploadBtn.disabled = false;
} catch (error) {
alert("Fehler bei der Verarbeitung des Uploads: " + error.message);
log("Fehler: " + error.message);
uploadBtn.disabled = false;
}
});
</script>
</body>
</html>