import csv import io from flask import Blueprint, request, render_template, redirect, flash from flask_login import login_required, current_user from werkzeug.utils import secure_filename from app.neo4j_utils import get_neo4j_handler from plugins.plant.models import db, Plant, PlantCommon, PlantScientific bp = Blueprint("importer", __name__, template_folder="templates") REQUIRED_HEADERS = {"uuid", "plant_type", "name", "scientific_name", "mother_uuid"} REQUIRED_FIELDS = {"uuid", "plant_type", "name"} @bp.route("/import/", methods=["GET", "POST"]) @login_required def upload(): if request.method == "POST": file = request.files.get("file") if not file: flash("No file uploaded.", "error") return redirect(request.url) try: decoded = file.read().decode("utf-8-sig") stream = io.StringIO(decoded) reader = csv.DictReader(stream) # Validate headers if reader.fieldnames is None: flash("Invalid CSV file: No headers found.", "error") return redirect(request.url) headers = set(h.strip() for h in reader.fieldnames) missing = REQUIRED_HEADERS - headers if missing: flash(f"Missing required column(s): {', '.join(missing)}", "error") return redirect(request.url) neo = get_neo4j_handler() added_count = 0 unknown_scientific_count = 0 for i, row in enumerate(reader, start=2): uuid = row.get("uuid", "").strip() name = row.get("name", "").strip() plant_type = row.get("plant_type", "").strip() sci_name = row.get("scientific_name", "").strip() mother_uuid = row.get("mother_uuid", "").strip() # Ensure required fields are present if not all([uuid, name, plant_type]): flash(f"Row {i} skipped: missing required data (uuid, name, or plant_type).", "warning") continue # Common Name common = PlantCommon.query.filter_by(name=name).first() if not common: common = PlantCommon(name=name) db.session.add(common) db.session.flush() # Scientific Name (fallback or assign 'Unknown') scientific = None if sci_name: scientific = PlantScientific.query.filter_by(name=sci_name).first() if not scientific: # Try resolving from existing records by common_id scientific = PlantScientific.query.filter_by(common_id=common.id).first() if not scientific: # Fallback to 'Unknown' unknown = PlantScientific.query.filter_by(name="Unknown").first() if not unknown: unknown = PlantScientific(name="Unknown", common_id=common.id) db.session.add(unknown) db.session.flush() scientific = unknown unknown_scientific_count += 1 # Plant plant = Plant.query.filter_by(uuid=uuid).first() if not plant: plant = Plant( uuid=uuid, common_id=common.id, scientific_id=scientific.id, plant_type=plant_type, owner_id=current_user.id ) db.session.add(plant) added_count += 1 # Neo4j neo.create_plant_node(uuid, name) if mother_uuid: neo.create_plant_node(mother_uuid, "Parent") neo.create_lineage(uuid, mother_uuid) db.session.commit() neo.close() msg = f"CSV imported successfully: {added_count} plants added." if unknown_scientific_count > 0: msg += f" {unknown_scientific_count} assigned 'Unknown' as scientific name." flash(msg, "success") except Exception as e: flash(f"Import failed: {str(e)}", "error") return redirect(request.url) return render_template("importer/upload.html")