diff --git a/app/__init__.py b/app/__init__.py index a98742e..91452c8 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -54,7 +54,7 @@ def create_app(): mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) if hasattr(mod, 'bp'): - app.register_blueprint(mod.bp) + app.register_blueprint(mod.bp, strict_slashes=False) except Exception as e: print(f"[⚠️] Failed to load routes from plugin '{plugin}': {e}") diff --git a/main.zip b/main.zip index f045fce..a2e894f 100644 Binary files a/main.zip and b/main.zip differ diff --git a/plugins/importer/routes.py b/plugins/importer/routes.py index f95552f..5daa311 100644 --- a/plugins/importer/routes.py +++ b/plugins/importer/routes.py @@ -1,13 +1,19 @@ import csv import io from flask import Blueprint, request, render_template, redirect, flash +from flask_login import login_required, current_user from werkzeug.utils import secure_filename from app.neo4j_utils import get_neo4j_handler from plugins.plant.models import db, Plant, PlantCommon, PlantScientific 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"]) +@login_required def upload(): if request.method == "POST": file = request.files.get("file") @@ -16,22 +22,36 @@ def upload(): return redirect(request.url) try: - # Handle UTF-8 BOM (Byte Order Mark) if present decoded = file.read().decode("utf-8-sig") stream = io.StringIO(decoded) 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() + added_count = 0 + unknown_scientific_count = 0 - for row in reader: - uuid = row.get("uuid") - name = row.get("name") - sci_name = row.get("scientific_name") - plant_type = row.get("plant_type", "plant") - mother_uuid = row.get("mother_uuid") + for i, row in enumerate(reader, start=2): + uuid = row.get("uuid", "").strip() + name = row.get("name", "").strip() + plant_type = row.get("plant_type", "").strip() + sci_name = row.get("scientific_name", "").strip() + mother_uuid = row.get("mother_uuid", "").strip() - if not all([uuid, name, sci_name]): - continue # skip incomplete rows + # Ensure required fields are present + 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 = PlantCommon.query.filter_by(name=name).first() @@ -40,12 +60,25 @@ def upload(): db.session.add(common) db.session.flush() - # Scientific Name - scientific = PlantScientific.query.filter_by(name=sci_name).first() + # Scientific Name (fallback or assign 'Unknown') + scientific = None + + if sci_name: + scientific = PlantScientific.query.filter_by(name=sci_name).first() + if not scientific: - scientific = PlantScientific(name=sci_name, common_id=common.id) - db.session.add(scientific) - db.session.flush() + # Try resolving from existing records by common_id + 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() + scientific = unknown + unknown_scientific_count += 1 # Plant plant = Plant.query.filter_by(uuid=uuid).first() @@ -54,20 +87,26 @@ def upload(): uuid=uuid, common_id=common.id, scientific_id=scientific.id, - plant_type=plant_type + plant_type=plant_type, + owner_id=current_user.id ) db.session.add(plant) + added_count += 1 # Neo4j neo.create_plant_node(uuid, name) - if mother_uuid and mother_uuid.strip(): - neo.create_plant_node(mother_uuid.strip(), "Parent") - neo.create_lineage(uuid, mother_uuid.strip()) + if mother_uuid: + neo.create_plant_node(mother_uuid, "Parent") + neo.create_lineage(uuid, mother_uuid) db.session.commit() 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: flash(f"Import failed: {str(e)}", "error") diff --git a/plugins/importer/templates/importer/upload.html b/plugins/importer/templates/importer/upload.html index 1da6414..1b3e944 100644 --- a/plugins/importer/templates/importer/upload.html +++ b/plugins/importer/templates/importer/upload.html @@ -5,7 +5,7 @@
diff --git a/plugins/plant/routes.py b/plugins/plant/routes.py index 482394a..c05b71d 100644 --- a/plugins/plant/routes.py +++ b/plugins/plant/routes.py @@ -5,7 +5,7 @@ from .forms import PlantForm bp = Blueprint('plant', __name__, template_folder='templates') -@bp.route('/plants') +@bp.route('/plants/') def index(): plants = Plant.query.order_by(Plant.created_at.desc()).all() return render_template('plant/index.html', plants=plants)