236 lines
9.1 KiB
Python
236 lines
9.1 KiB
Python
# 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()
|
||
)
|