# plugins/importer/routes.py import csv import io import difflib from flask import Blueprint, request, render_template, redirect, flash, session, url_for from flask_login import login_required, current_user from flask_wtf.csrf import generate_csrf from app.neo4j_utils import get_neo4j_handler from plugins.plant.models import ( db, Plant, PlantCommonName, PlantScientificName, PlantOwnershipLog ) bp = Blueprint("importer", __name__, template_folder="templates", url_prefix="/import") REQUIRED_HEADERS = {"uuid", "plant_type", "name"} @bp.route("/", 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) headers = set(reader.fieldnames or []) missing = REQUIRED_HEADERS - headers if missing: flash(f"Missing required CSV headers: {missing}", "error") return redirect(request.url) session["pending_rows"] = [] review_list = [] # Preload existing common/scientific names all_common = {c.name.lower(): c for c in PlantCommonName.query.all()} all_scientific = {s.name.lower(): s for s in PlantScientificName.query.all()} for row in reader: uuid_raw = row.get("uuid", "") uuid = uuid_raw.strip().strip('"') name_raw = row.get("name", "") name = name_raw.strip() sci_raw = row.get("scientific_name", "") sci_name = sci_raw.strip() plant_type = row.get("plant_type", "").strip() or "plant" mother_raw = row.get("mother_uuid", "") mother_uuid = mother_raw.strip().strip('"') # If any required field is missing, skip if not (uuid and name and plant_type): continue # Try fuzzy‐matching scientific names if needed suggested_match = None original_sci = sci_name name_lc = name.lower() sci_lc = sci_name.lower() if sci_lc and sci_lc not in all_scientific: close = difflib.get_close_matches(sci_lc, all_scientific.keys(), n=1, cutoff=0.85) if close: suggested_match = all_scientific[close[0]].name if not sci_lc and name_lc in all_common: sci_obj = PlantScientificName.query.filter_by(common_id=all_common[name_lc].id).first() if sci_obj: sci_name = sci_obj.name elif not sci_lc: close_common = difflib.get_close_matches(name_lc, all_common.keys(), n=1, cutoff=0.85) if close_common: match_name = close_common[0] sci_obj = PlantScientificName.query.filter_by(common_id=all_common[match_name].id).first() if sci_obj: suggested_match = sci_obj.name sci_name = sci_obj.name session["pending_rows"].append({ "uuid": uuid, "name": name, "sci_name": sci_name, "original_sci_name": original_sci, "plant_type": plant_type, "mother_uuid": mother_uuid, "suggested_scientific_name": suggested_match, }) if suggested_match and suggested_match != original_sci: review_list.append({ "uuid": uuid, "common_name": name, "user_input": original_sci or "(blank)", "suggested_name": suggested_match }) session["review_list"] = review_list return redirect(url_for("importer.review")) except Exception as e: flash(f"Import failed: {e}", "error") return redirect(request.url) return render_template("importer/upload.html", csrf_token=generate_csrf()) @bp.route("/review", methods=["GET", "POST"]) @login_required def review(): rows = session.get("pending_rows", []) review_list = session.get("review_list", []) if request.method == "POST": neo = get_neo4j_handler() added = 0 # ————————————————————————————————————————————— # (1) CREATE MySQL records & MERGE every Neo4j node # ————————————————————————————————————————————— for row in rows: uuid_raw = row["uuid"] uuid = uuid_raw.strip().strip('"') name_raw = row["name"] name = name_raw.strip() sci_raw = row["sci_name"] sci_name = sci_raw.strip() plant_type = row["plant_type"].strip() mother_raw = row["mother_uuid"] mother_uuid = mother_raw.strip().strip('"') suggested = row.get("suggested_scientific_name") # ——— MySQL: PlantCommonName ——— common = PlantCommonName.query.filter_by(name=name).first() if not common: common = PlantCommonName(name=name) db.session.add(common) db.session.flush() # ——— MySQL: PlantScientificName ——— accepted = request.form.get(f"confirm_{uuid}") sci_to_use = suggested if (suggested and accepted) else sci_name scientific = PlantScientificName.query.filter_by(name=sci_to_use).first() if not scientific: scientific = PlantScientificName(name=sci_to_use, common_id=common.id) db.session.add(scientific) db.session.flush() # ——— MySQL: Plant row ——— 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, is_verified=bool(accepted) ) db.session.add(plant) db.session.flush() # so plant.id is available immediately added += 1 # ——— MySQL: Create initial ownership log entry ——— log = PlantOwnershipLog( plant_id = plant.id, user_id = current_user.id, date_acquired = datetime.utcnow(), transferred = False, is_verified = bool(accepted) ) db.session.add(log) # ——— Neo4j: ensure a node exists for this plant UUID ——— neo.create_plant_node(uuid, name) # Commit MySQL so that all Plant/OwnershipLog rows exist db.session.commit() # ————————————————————————————————————————————— # (2) CREATE Neo4j LINEAGE relationships (child → parent). (Unchanged) # ————————————————————————————————————————————— for row in rows: child_raw = row.get("uuid", "") child_uuid = child_raw.strip().strip('"') mother_raw = row.get("mother_uuid", "") mother_uuid = mother_raw.strip().strip('"') print( f"[DEBUG] row → child_raw={child_raw!r}, child_uuid={child_uuid!r}; " f"mother_raw={mother_raw!r}, mother_uuid={mother_uuid!r}" ) if mother_uuid: neo.create_plant_node(mother_uuid, name="Unknown") neo.create_lineage(child_uuid, mother_uuid) else: print(f"[DEBUG] Skipping LINEAGE creation for child {child_uuid!r} (no mother_uuid)") # (Optional) Check two known UUIDs neo.debug_check_node("8b1059c8-8dd3-487a-af19-1eb548788e87") neo.debug_check_node("2ee2e0e7-69de-4d8f-abfe-4ed973c3d760") neo.close() flash(f"{added} plants added (MySQL) + Neo4j nodes/relations created.", "success") session.pop("pending_rows", None) session.pop("review_list", None) return redirect(url_for("importer.upload")) return render_template( "importer/review.html", review_list=review_list, csrf_token=generate_csrf() )