From bf31ee2d16c12edbce463cb49194e09d63f850ef Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Mon, 3 Aug 2026 00:06:48 +0200 Subject: [PATCH] feat: add batch CSV and image upload logic - Created '/upload_csv_batch' endpoint to handle CSV parsing and multiple image uploads - Added automatic WebP conversion and GridFS storage for batch images - Implemented grouping logic via 'series_group_id' based on item name - Created '/batch_upload' route and 'upload_batch.html' for a seamless async frontend --- Web/app.py | 210 ++++++++++++++++++++++++++++++++ Web/requirements.txt | 3 +- Web/templates/upload_batch.html | 205 +++++++++++++++++++++++++++++++ requirements.txt | 3 +- 4 files changed, 419 insertions(+), 2 deletions(-) create mode 100644 Web/templates/upload_batch.html diff --git a/Web/app.py b/Web/app.py index be3a9ff..52dcb12 100755 --- a/Web/app.py +++ b/Web/app.py @@ -11845,3 +11845,213 @@ def test_push_notification(): except Exception as e: app.logger.error(f'Error sending test push: {e}') return jsonify({'success': False}), 500 + + +@app.route('/batch_upload', methods=['GET']) +def batch_upload_page(): + """ + Serves the HTML frontend for the batch CSV and image upload. + """ + # Check permissions if necessary, similar to your other routes + if 'username' not in session: + flash('Bitte melden Sie sich an.', 'error') + return redirect(url_for('login')) + + return render_template('upload_batch.html') + + +@app.route('/upload_csv_batch', methods=['POST']) +def upload_csv_batch(): + """ + Route for batch adding new items to the inventory via CSV. + Handles CSV parsing, bulk image upload (conversion to WebP), GridFS storage, + and groups identical items based on their Name. + """ + import pandas as pd + import ast + if 'username' not in session: + return jsonify({'success': False, 'message': 'Nicht angemeldet'}), 401 + + username = session['username'] + # permissions = _get_current_user_permissions() ... (anpassen wie in Original) + # if not _action_access_allowed(permissions, 'can_insert'): + # return jsonify({'success': False, 'message': 'Einfüge-Rechte erforderlich'}), 403 + + fs = get_gridfs() + upload_session_id = str(uuid.uuid4())[:8] + app.logger.info(f"Starting CSV Batch upload session {upload_session_id} - User: {username}") + + # 1. Dateien aus dem Request empfangen + if 'csv_file' not in request.files: + return jsonify({"success": False, "message": "Keine CSV-Datei hochgeladen"}), 400 + + csv_file = request.files['csv_file'] + uploaded_images = request.files.getlist('images') + + # 2. CSV Einlesen und Validieren + try: + df = pd.read_csv(csv_file) + except Exception as e: + app.logger.error(f"[Upload {upload_session_id}] Fehler beim Lesen der CSV: {str(e)}") + return jsonify({"success": False, "message": f"Fehler beim Lesen der CSV: {str(e)}"}), 400 + + if 'Name' not in df.columns: + return jsonify({"success": False, "message": "Die CSV muss zwingend eine 'Name' Spalte enthalten."}), 400 + + # 3. Bilder verarbeiten, nach WebP konvertieren und in GridFS speichern + # Mapping: Original-Dateiname (ohne Pfad/Erweiterung) -> GridFS Filename (.webp) + image_mapping = {} + processed_count = 0 + error_count = 0 + + for index, image in enumerate(uploaded_images): + if not image or not image.filename: + continue + + original_secure_name = secure_filename(image.filename) + base_name_no_ext = os.path.splitext(original_secure_name)[0] + image_log_prefix = f"[Upload {upload_session_id}][Image {index + 1}/{len(uploaded_images)}]" + + try: + # Annahme: is_allowed, error_message = allowed_file(...) + + image.seek(0) + image_bytes = image.read() + if not image_bytes: + error_count += 1 + continue + + optimized_io = io.BytesIO() + with Image.open(io.BytesIO(image_bytes)) as img: + if img.mode not in ('RGB', 'RGBA'): + img = img.convert('RGBA') + + max_width = 500 + if img.width > max_width: + ratio = max_width / img.width + new_size = (max_width, int(img.height * ratio)) + img = img.resize(new_size, Image.Resampling.LANCZOS) + + img.save(optimized_io, format='WEBP', quality=85, optimize=True) + + optimized_io.seek(0) + new_filename = f"{uuid.uuid4().hex}_{int(time.time())}.webp" + + # Speichern in GridFS analog zu upload_item + file_id = fs.put( + optimized_io, + filename=new_filename, + content_type='image/webp', + metadata={ + 'original_filename': original_secure_name, + 'upload_session': upload_session_id, + 'batch_upload': True + } + ) + + # Im Mapping speichern (damit wir sie später der CSV zuordnen können) + image_mapping[base_name_no_ext] = new_filename + processed_count += 1 + + except Exception as e: + app.logger.error(f"{image_log_prefix} Processing failed: {str(e)}") + error_count += 1 + + # 4. Items gruppieren (Analog zu series_group_id aus upload_item) + # Gruppierung über den Namen: Alle Zeilen mit demselben Namen gehören zur selben Serie + df['Name'] = df['Name'].fillna('Unbenannt').astype(str) + + # Optional: Fülle NaN Werte in der CSV mit sinnvollen Defaults für die Datenbank + df = df.fillna({ + 'Ort': 'Unbekannt', + 'Beschreibung': '', + 'Code_4': '', + 'Anschaffungsjahr': '', + 'Anschaffungskosten': '' + }) + + created_item_ids = [] + + grouped_items = df.groupby('Name') + + for name, group in grouped_items: + item_count = len(group) + series_group_id = str(uuid.uuid4()) if item_count > 1 else None + parent_item_id = None + + for position, (index, row) in enumerate(group.iterrows(), start=1): + + # Bilder aus der CSV-Zeile extrahieren und über das image_mapping mappen + item_image_filenames = [] + if 'Images' in row and pd.notna(row['Images']): + try: + # Aus "['Bild1.JPG', 'Bild2.JPG']" wird eine Liste + img_list = ast.literal_eval(str(row['Images'])) + if isinstance(img_list, list): + for img_name in img_list: + base_img_name = os.path.splitext(img_name)[0] + # Falls das Bild hochgeladen wurde, die WebP GridFS ID/Name nehmen + if base_img_name in image_mapping: + item_image_filenames.append(image_mapping[base_img_name]) + else: + app.logger.warning(f"Bild {img_name} in CSV definiert, aber nicht hochgeladen.") + except (ValueError, SyntaxError): + pass + + # Filter extrahieren (falls vorhanden, erwarte string list wie "['HSU', '', '', '']") + def parse_filter_col(col_data): + try: + res = ast.literal_eval(str(col_data)) + return res if isinstance(res, list) else [] + except: + return [] + + filter_upload = parse_filter_col(row.get('Filter', '[]')) + filter_upload2 = parse_filter_col(row.get('Filter2', '[]')) + filter_upload3 = parse_filter_col(row.get('Filter3', '[]')) + + reservierbar = bool(row.get('Reservierbar', False)) + + # DB Insert Funktion aufrufen (orientiert an deiner upload_item) + item_id = it.add_item( + name=row['Name'], + ort=row['Ort'], + beschreibung=row['Beschreibung'], + image_filenames=item_image_filenames, + filter_upload=filter_upload, + filter_upload2=filter_upload2, + filter_upload3=filter_upload3, + anschaffungs_jahr=str(row['Anschaffungsjahr']) if row['Anschaffungsjahr'] else None, + anschaffungs_kosten=str(row['Anschaffungskosten']) if row['Anschaffungskosten'] else None, + code_4=str(row['Code_4']) if row['Code_4'] else None, + reservierbar=reservierbar, + series_group_id=series_group_id, + series_count=item_count, + series_position=position, + is_grouped_sub_item=(position > 1), + parent_item_id=parent_item_id, + # Default Werte, falls keine Bibliotheks-CSV + isbn='', + item_type='other', + library_category='', + is_library=False + ) + + if item_id: + created_item_ids.append(item_id) + # Das erste Item in einer Serie wird der Parent für die restlichen + if position == 1: + parent_item_id = str(item_id) + else: + app.logger.error(f"Fehler beim Erstellen von Item: {row['Name']} (Index {index})") + + app.logger.info( + f"Batch Upload abgeschlossen: {len(created_item_ids)} Items erstellt. {processed_count} Bilder verarbeitet.") + + return jsonify({ + "success": True, + "message": f"Upload erfolgreich. {len(created_item_ids)} Items importiert und {processed_count} Bilder konvertiert.", + "created_count": len(created_item_ids), + "images_processed": processed_count, + "images_failed": error_count + }), 200 \ No newline at end of file diff --git a/Web/requirements.txt b/Web/requirements.txt index 5c7a4e2..2f12fd7 100755 --- a/Web/requirements.txt +++ b/Web/requirements.txt @@ -17,4 +17,5 @@ cryptography>=42.0.0 pywebpush py-vapid>=1.9.0 beautifulsoup4 -pywebpush \ No newline at end of file +pywebpush +pandas \ No newline at end of file diff --git a/Web/templates/upload_batch.html b/Web/templates/upload_batch.html new file mode 100644 index 0000000..3408be9 --- /dev/null +++ b/Web/templates/upload_batch.html @@ -0,0 +1,205 @@ + + + + + + Batch Upload - CSV & Bilder + + + + +
+

Inventar Batch Upload

+ +
+
+ + + +
+ +
+ + + + Du kannst mehrere Bilder markieren (Strg/Cmd gedrückt halten). +
+ + +
+ +
+
+ + + + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e30870e..da3a2e1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,4 +17,5 @@ cryptography>=42.0.0 pywebpush py-vapid>=1.9.0 beautifulsoup4 -pywebpush \ No newline at end of file +pywebpush +pandas \ No newline at end of file