ehh
This commit is contained in:
@ -54,7 +54,7 @@ def create_app():
|
|||||||
mod = importlib.util.module_from_spec(spec)
|
mod = importlib.util.module_from_spec(spec)
|
||||||
spec.loader.exec_module(mod)
|
spec.loader.exec_module(mod)
|
||||||
if hasattr(mod, 'bp'):
|
if hasattr(mod, 'bp'):
|
||||||
app.register_blueprint(mod.bp)
|
app.register_blueprint(mod.bp, strict_slashes=False)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[⚠️] Failed to load routes from plugin '{plugin}': {e}")
|
print(f"[⚠️] Failed to load routes from plugin '{plugin}': {e}")
|
||||||
|
|
||||||
|
@ -1,13 +1,19 @@
|
|||||||
import csv
|
import csv
|
||||||
import io
|
import io
|
||||||
from flask import Blueprint, request, render_template, redirect, flash
|
from flask import Blueprint, request, render_template, redirect, flash
|
||||||
|
from flask_login import login_required, current_user
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
from app.neo4j_utils import get_neo4j_handler
|
from app.neo4j_utils import get_neo4j_handler
|
||||||
from plugins.plant.models import db, Plant, PlantCommon, PlantScientific
|
from plugins.plant.models import db, Plant, PlantCommon, PlantScientific
|
||||||
|
|
||||||
bp = Blueprint("importer", __name__, template_folder="templates")
|
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"])
|
@bp.route("/import/", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
def upload():
|
def upload():
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
file = request.files.get("file")
|
file = request.files.get("file")
|
||||||
@ -16,22 +22,36 @@ def upload():
|
|||||||
return redirect(request.url)
|
return redirect(request.url)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Handle UTF-8 BOM (Byte Order Mark) if present
|
|
||||||
decoded = file.read().decode("utf-8-sig")
|
decoded = file.read().decode("utf-8-sig")
|
||||||
stream = io.StringIO(decoded)
|
stream = io.StringIO(decoded)
|
||||||
reader = csv.DictReader(stream)
|
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()
|
neo = get_neo4j_handler()
|
||||||
|
added_count = 0
|
||||||
|
unknown_scientific_count = 0
|
||||||
|
|
||||||
for row in reader:
|
for i, row in enumerate(reader, start=2):
|
||||||
uuid = row.get("uuid")
|
uuid = row.get("uuid", "").strip()
|
||||||
name = row.get("name")
|
name = row.get("name", "").strip()
|
||||||
sci_name = row.get("scientific_name")
|
plant_type = row.get("plant_type", "").strip()
|
||||||
plant_type = row.get("plant_type", "plant")
|
sci_name = row.get("scientific_name", "").strip()
|
||||||
mother_uuid = row.get("mother_uuid")
|
mother_uuid = row.get("mother_uuid", "").strip()
|
||||||
|
|
||||||
if not all([uuid, name, sci_name]):
|
# Ensure required fields are present
|
||||||
continue # skip incomplete rows
|
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 Name
|
||||||
common = PlantCommon.query.filter_by(name=name).first()
|
common = PlantCommon.query.filter_by(name=name).first()
|
||||||
@ -40,12 +60,25 @@ def upload():
|
|||||||
db.session.add(common)
|
db.session.add(common)
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
|
|
||||||
# Scientific Name
|
# Scientific Name (fallback or assign 'Unknown')
|
||||||
|
scientific = None
|
||||||
|
|
||||||
|
if sci_name:
|
||||||
scientific = PlantScientific.query.filter_by(name=sci_name).first()
|
scientific = PlantScientific.query.filter_by(name=sci_name).first()
|
||||||
|
|
||||||
if not scientific:
|
if not scientific:
|
||||||
scientific = PlantScientific(name=sci_name, common_id=common.id)
|
# Try resolving from existing records by common_id
|
||||||
db.session.add(scientific)
|
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()
|
db.session.flush()
|
||||||
|
scientific = unknown
|
||||||
|
unknown_scientific_count += 1
|
||||||
|
|
||||||
# Plant
|
# Plant
|
||||||
plant = Plant.query.filter_by(uuid=uuid).first()
|
plant = Plant.query.filter_by(uuid=uuid).first()
|
||||||
@ -54,20 +87,26 @@ def upload():
|
|||||||
uuid=uuid,
|
uuid=uuid,
|
||||||
common_id=common.id,
|
common_id=common.id,
|
||||||
scientific_id=scientific.id,
|
scientific_id=scientific.id,
|
||||||
plant_type=plant_type
|
plant_type=plant_type,
|
||||||
|
owner_id=current_user.id
|
||||||
)
|
)
|
||||||
db.session.add(plant)
|
db.session.add(plant)
|
||||||
|
added_count += 1
|
||||||
|
|
||||||
# Neo4j
|
# Neo4j
|
||||||
neo.create_plant_node(uuid, name)
|
neo.create_plant_node(uuid, name)
|
||||||
if mother_uuid and mother_uuid.strip():
|
if mother_uuid:
|
||||||
neo.create_plant_node(mother_uuid.strip(), "Parent")
|
neo.create_plant_node(mother_uuid, "Parent")
|
||||||
neo.create_lineage(uuid, mother_uuid.strip())
|
neo.create_lineage(uuid, mother_uuid)
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
neo.close()
|
neo.close()
|
||||||
|
|
||||||
flash("CSV imported successfully with lineage.", "success")
|
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:
|
except Exception as e:
|
||||||
flash(f"Import failed: {str(e)}", "error")
|
flash(f"Import failed: {str(e)}", "error")
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
<div class="alert alert-info" role="alert">
|
<div class="alert alert-info" role="alert">
|
||||||
<strong>Expected CSV headers (in this exact order):</strong>
|
<strong>Expected CSV headers (in this exact order):</strong>
|
||||||
<code>{{ headers }}</code>
|
<code>uuid,plant_type,name,scientific_name,mother_uuid</code>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="POST" enctype="multipart/form-data" class="needs-validation" novalidate>
|
<form method="POST" enctype="multipart/form-data" class="needs-validation" novalidate>
|
||||||
|
@ -5,7 +5,7 @@ from .forms import PlantForm
|
|||||||
|
|
||||||
bp = Blueprint('plant', __name__, template_folder='templates')
|
bp = Blueprint('plant', __name__, template_folder='templates')
|
||||||
|
|
||||||
@bp.route('/plants')
|
@bp.route('/plants/')
|
||||||
def index():
|
def index():
|
||||||
plants = Plant.query.order_by(Plant.created_at.desc()).all()
|
plants = Plant.query.order_by(Plant.created_at.desc()).all()
|
||||||
return render_template('plant/index.html', plants=plants)
|
return render_template('plant/index.html', plants=plants)
|
||||||
|
Reference in New Issue
Block a user