Files
natureinpots_community/plugins/importer/routes.py
2025-06-06 02:00:05 -05:00

227 lines
9.4 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 datetime import datetime
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')
# ────────────────────────────────────────────────────────────────────────────────
# Redirect “/import/” → “/import/upload”
# ────────────────────────────────────────────────────────────────────────────────
@bp.route("/", methods=["GET"])
@login_required
def index():
# When someone hits /import, send them to /import/upload
return redirect(url_for("importer.upload"))
# ────────────────────────────────────────────────────────────────────────────────
# Required CSV headers for import
# ────────────────────────────────────────────────────────────────────────────────
REQUIRED_HEADERS = {"uuid", "plant_type", "name", "scientific_name", "mother_uuid"}
@bp.route("/upload", methods=["GET", "POST"])
@login_required
def upload():
if request.method == "POST":
file = request.files.get("file")
if not file:
flash("No file selected", "error")
return redirect(request.url)
# Decode as UTF-8-SIG to strip any BOM, then parse with csv.DictReader
try:
stream = io.StringIO(file.stream.read().decode("utf-8-sig"))
reader = csv.DictReader(stream)
except Exception:
flash("Failed to read CSV file. Ensure it is valid UTF-8.", "error")
return redirect(request.url)
headers = set(reader.fieldnames or [])
missing = REQUIRED_HEADERS - headers
if missing:
flash(f"Missing required CSV headers: {missing}", "error")
return redirect(request.url)
# Prepare session storage for the rows under review
session["pending_rows"] = []
review_list = []
# Preload existing common/scientific names (lowercased keys for fuzzy matching)
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('"')
# Skip any row where required fields are missing
if not (uuid and name and plant_type):
continue
# ─── If the scientific name doesnt match exactly, suggest a close match ─────
# Only suggest if the “closest key” differs from the raw input:
suggestions = difflib.get_close_matches(
sci_name.lower(),
list(all_scientific.keys()),
n=1,
cutoff=0.8
)
if suggestions and suggestions[0] != sci_name.lower():
suggested = all_scientific[suggestions[0]].name
else:
suggested = None
review_item = {
"uuid": uuid,
"name": name,
"sci_name": sci_name,
"suggested": suggested,
"plant_type": plant_type,
"mother_uuid": mother_uuid
}
review_list.append(review_item)
session["pending_rows"].append(review_item)
session["review_list"] = review_list
return redirect(url_for("importer.review"))
# GET → show upload form
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
# Re-load preload maps to avoid NameError if used below
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 rows:
uuid = row.get("uuid")
name = row.get("name")
sci_name = row.get("sci_name")
suggested = row.get("suggested")
plant_type = row.get("plant_type")
mother_uuid = row.get("mother_uuid")
# Check if user clicked "confirm" for a suggested scientific name
accepted_key = f"confirm_{uuid}"
accepted = request.form.get(accepted_key)
# ─── MySQL: PlantCommonName ────────────────────────────────────────────────
common = PlantCommonName.query.filter_by(name=name).first()
if not common:
common = PlantCommonName(name=name)
db.session.add(common)
db.session.flush()
all_common[common.name.lower()] = common
else:
all_common[common.name.lower()] = common
# ─── MySQL: PlantScientificName ───────────────────────────────────────────
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()
all_scientific[scientific.name.lower()] = scientific
else:
all_scientific[scientific.name.lower()] = scientific
# ─── Decide if this plants data is “verified” by the user ────────────────
data_verified = False
if (not suggested) or (suggested and accepted):
data_verified = True
# ─── MySQL: Plant record ─────────────────────────────────────────────────
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,
data_verified = data_verified
)
db.session.add(plant)
db.session.flush() # so plant.id is now available
log = PlantOwnershipLog(
plant_id = plant.id,
user_id = current_user.id,
date_acquired = datetime.utcnow(),
transferred = False,
is_verified = data_verified
)
db.session.add(log)
added += 1
else:
# Skip duplicates if the same UUID already exists
pass
# ─── Neo4j: ensure the Plant node exists ─────────────────────────────────
neo.create_plant_node(uuid, name)
# ─── Neo4j: create a LINEAGE relationship if mother_uuid was provided ─────
if mother_uuid:
# Replace the old call with the correct method name:
neo.create_lineage(child_uuid=uuid, parent_uuid=mother_uuid)
# Commit all MySQL changes at once
db.session.commit()
neo.close()
flash(f"{added} plants added (MySQL) and Neo4j nodes/relationships created.", "success")
session.pop("pending_rows", None)
session.pop("review_list", None)
return redirect(url_for("importer.upload"))
# GET → re-render the review page with the same review_list
return render_template(
"importer/review.html",
review_list=review_list,
csrf_token=generate_csrf()
)