Files
natureinpots_community/plugins/importer/routes.py
2025-06-04 23:24:16 -05:00

236 lines
9.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 fuzzymatching 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()
)