diff --git a/.env.example b/.env.example
index aef1722..fb8019c 100644
--- a/.env.example
+++ b/.env.example
@@ -13,3 +13,7 @@ MYSQL_USER=plant_user
MYSQL_PASSWORD=plant_pass
MYSQL_ROOT_PASSWORD=supersecret
+
+NEO4J_URI=bolt://neo4j:7687
+NEO4J_USER=neo4j
+NEO4J_PASSWORD=your_secure_password
diff --git a/app/config.py b/app/config.py
index 4a37c40..aa16cfd 100644
--- a/app/config.py
+++ b/app/config.py
@@ -24,3 +24,7 @@ class Config:
# Optional toggles
ENABLE_DB_SEEDING = os.environ.get('ENABLE_DB_SEEDING', '0') == '1'
DOCKER_ENV = os.environ.get('FLASK_ENV', 'production')
+
+ NEO4J_URI = os.getenv('NEO4J_URI', 'bolt://neo4j:7687')
+ NEO4J_USER = os.getenv('NEO4J_USER', 'neo4j')
+ NEO4J_PASSWORD = os.getenv('NEO4J_PASSWORD', 'your_secure_password')
diff --git a/app/neo4j_utils.py b/app/neo4j_utils.py
new file mode 100644
index 0000000..defc7c8
--- /dev/null
+++ b/app/neo4j_utils.py
@@ -0,0 +1,32 @@
+
+from neo4j import GraphDatabase
+from flask import current_app
+
+class Neo4jHandler:
+ def __init__(self, uri, user, password):
+ self.driver = GraphDatabase.driver(uri, auth=(user, password))
+
+ def close(self):
+ self.driver.close()
+
+ def create_plant_node(self, uuid, name):
+ with self.driver.session() as session:
+ session.run(
+ "MERGE (p:Plant {uuid: $uuid}) "
+ "SET p.name = $name",
+ uuid=uuid, name=name
+ )
+
+ def create_lineage(self, child_uuid, parent_uuid):
+ with self.driver.session() as session:
+ session.run(
+ "MATCH (child:Plant {uuid: $child_uuid}), (parent:Plant {uuid: $parent_uuid}) "
+ "MERGE (parent)-[:PARENT_OF]->(child)",
+ child_uuid=child_uuid, parent_uuid=parent_uuid
+ )
+
+def get_neo4j_handler():
+ uri = current_app.config['NEO4J_URI']
+ user = current_app.config['NEO4J_USER']
+ password = current_app.config['NEO4J_PASSWORD']
+ return Neo4jHandler(uri, user, password)
diff --git a/docker-compose.yml b/docker-compose.yml
index b64cabe..b971469 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -54,5 +54,17 @@ services:
depends_on:
- db
+ neo4j:
+ image: neo4j:5.18
+ container_name: nip_neo4j
+ ports:
+ - '7474:7474'
+ - '7687:7687'
+ environment:
+ - NEO4J_AUTH=neo4j/your_secure_password
+ volumes:
+ - neo4j_data:/data
+
volumes:
plant_price_tracker_mysql_data:
+ neo4j_data:
diff --git a/main.zip b/main.zip
index fc737e0..f045fce 100644
Binary files a/main.zip and b/main.zip differ
diff --git a/plugins/auth/models.py b/plugins/auth/models.py
index c8dfc82..6570644 100644
--- a/plugins/auth/models.py
+++ b/plugins/auth/models.py
@@ -5,6 +5,7 @@ from app import db
class User(db.Model, UserMixin):
__tablename__ = 'users'
+ __table_args__ = {'extend_existing': True}
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(120), unique=True, nullable=False)
diff --git a/plugins/core_ui/templates/core_ui/base.html b/plugins/core_ui/templates/core_ui/base.html
index f970cd2..7f32fac 100644
--- a/plugins/core_ui/templates/core_ui/base.html
+++ b/plugins/core_ui/templates/core_ui/base.html
@@ -31,7 +31,7 @@
- Home
- Plants
- - Import
+ - Import
{% if current_user.is_authenticated and current_user.role == 'admin' %}
- Admin Dashboard
{% endif %}
diff --git a/plugins/growlog/models.py b/plugins/growlog/models.py
index b9b02b2..d5ee187 100644
--- a/plugins/growlog/models.py
+++ b/plugins/growlog/models.py
@@ -1,12 +1,14 @@
from datetime import datetime
from app import db
+from plugins.plant.models import Plant
class GrowLog(db.Model):
__tablename__ = 'grow_logs'
+ __table_args__ = {'extend_existing': True}
id = db.Column(db.Integer, primary_key=True)
- plant_id = db.Column(db.Integer, db.ForeignKey('plants.id'), nullable=False)
+ plant_id = db.Column(db.Integer, db.ForeignKey('plant.id'), nullable=False)
title = db.Column(db.String(255), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
@@ -18,7 +20,7 @@ class PlantUpdate(db.Model):
__table_args__ = {'extend_existing': True}
id = db.Column(db.Integer, primary_key=True)
- plant_id = db.Column(db.Integer, db.ForeignKey('plants.id'), nullable=False)
+ plant_id = db.Column(db.Integer, db.ForeignKey('plant.id'), nullable=False)
growlog_id = db.Column(db.Integer, db.ForeignKey('grow_logs.id'), nullable=True)
update_type = db.Column(db.String(50), nullable=False)
description = db.Column(db.Text, nullable=True)
diff --git a/plugins/importer/__init__.py b/plugins/importer/__init__.py
index e69de29..1951bb2 100644
--- a/plugins/importer/__init__.py
+++ b/plugins/importer/__init__.py
@@ -0,0 +1,2 @@
+def register_commands(app):
+ pass
diff --git a/plugins/importer/routes.py b/plugins/importer/routes.py
index 4eca2b3..f95552f 100644
--- a/plugins/importer/routes.py
+++ b/plugins/importer/routes.py
@@ -1,174 +1,77 @@
-# plugins/importer/routes.py
-
import csv
import io
-import re
-import uuid
+from flask import Blueprint, request, render_template, redirect, flash
+from werkzeug.utils import secure_filename
+from app.neo4j_utils import get_neo4j_handler
+from plugins.plant.models import db, Plant, PlantCommon, PlantScientific
-from flask import (
- Blueprint, flash, redirect, render_template, request, url_for
-)
-from flask_login import login_required, current_user
-from sqlalchemy.exc import SQLAlchemyError
-
-from app import db
-from plugins.plant.models import (
- Plant, PlantCommonName, PlantScientificName, PlantLineage
-)
-
-# Blueprint setup
-bp = Blueprint(
- 'importer',
- __name__,
- template_folder='templates',
- url_prefix='/import'
-)
-
-# Expected CSV headers (exact, in this order)
-EXPECTED_HEADERS = ['uuid', 'plant_type', 'name', 'scientific_name', 'mother_uuid']
-
-# Strict regex for UUIDv4
-UUID_REGEX = re.compile(
- r'^[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}$'
-)
-
-def is_valid_uuid(u: str) -> bool:
- """Return True if 'u' matches UUIDv4 format exactly."""
- return bool(UUID_REGEX.match(u))
-
-def sanitize_field(field_value: str) -> str:
- """
- Prevent CSV-injection by prefixing any cell that starts with
- '=', '+', '-', or '@' with a space.
- """
- if isinstance(field_value, str) and field_value and field_value[0] in ['=', '+', '-', '@']:
- return ' ' + field_value
- return field_value
-
-def validate_row(row: dict, line_number: int):
- """
- Validate a single CSV row. Returns (cleaned_row_dict, errors_list).
- If errors_list is not empty, the row is invalid.
- """
- errors = []
- cleaned = {}
-
- # 1) uuid: if provided, must be valid; otherwise generate a new one
- raw_uuid = row.get('uuid', '').strip()
- if raw_uuid:
- if not is_valid_uuid(raw_uuid):
- errors.append(f"Line {line_number}: Invalid UUID format '{raw_uuid}'.")
- else:
- cleaned['uuid'] = raw_uuid
- else:
- # auto-generate
- cleaned['uuid'] = str(uuid.uuid4())
-
- # 2) plant_type: required, <= 50 chars
- plant_type = row.get('plant_type', '').strip()
- if not plant_type:
- errors.append(f"Line {line_number}: 'plant_type' is required.")
- elif len(plant_type) > 50:
- errors.append(f"Line {line_number}: 'plant_type' exceeds 50 characters.")
- else:
- cleaned['plant_type'] = sanitize_field(plant_type)
-
- # 3) name: required, <= 100 chars
- name = row.get('name', '').strip()
- if not name:
- errors.append(f"Line {line_number}: 'name' (common name) is required.")
- elif len(name) > 100:
- errors.append(f"Line {line_number}: 'name' exceeds 100 characters.")
- else:
- cleaned['name'] = sanitize_field(name)
-
- # 4) scientific_name: required, <= 100 chars
- sci = row.get('scientific_name', '').strip()
- if not sci:
- errors.append(f"Line {line_number}: 'scientific_name' is required.")
- elif len(sci) > 100:
- errors.append(f"Line {line_number}: 'scientific_name' exceeds 100 characters.")
- else:
- cleaned['scientific_name'] = sanitize_field(sci)
-
- # 5) mother_uuid: optional. If present (and not 'N/A'), must be valid
- raw_mother = row.get('mother_uuid', '').strip()
- if raw_mother and raw_mother.upper() != 'N/A':
- if not is_valid_uuid(raw_mother):
- errors.append(f"Line {line_number}: 'mother_uuid' has invalid UUID '{raw_mother}'.")
- else:
- cleaned['mother_uuid'] = raw_mother
- else:
- cleaned['mother_uuid'] = None
-
- if errors:
- return None, errors
-
- return cleaned, None
-
-@bp.route("/", methods=["GET", "POST"])
-@login_required
-def upload_csv():
- headers = [
- "name", "scientific_name", "type", "status", "description"
- ]
+bp = Blueprint("importer", __name__, template_folder="templates")
+@bp.route("/import/", methods=["GET", "POST"])
+def upload():
if request.method == "POST":
file = request.files.get("file")
- if not file or not file.filename.endswith(".csv"):
- flash("Please upload a valid CSV file.", "danger")
+ if not file:
+ flash("No file uploaded.", "error")
return redirect(request.url)
try:
- stream = io.StringIO(file.stream.read().decode("UTF-8"))
- csv_reader = csv.reader(stream)
- data = list(csv_reader)
+ # Handle UTF-8 BOM (Byte Order Mark) if present
+ decoded = file.read().decode("utf-8-sig")
+ stream = io.StringIO(decoded)
+ reader = csv.DictReader(stream)
- if not data or data[0] != headers:
- flash("CSV headers must exactly match: " + ", ".join(headers), "danger")
- return redirect(request.url)
+ neo = get_neo4j_handler()
- # Skip header row
- rows = data[1:]
+ 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")
- with db.session.begin_nested(): # rollback safe transaction
- for row in rows:
- name, sci_name, plant_type, plant_status, desc = row
+ if not all([uuid, name, sci_name]):
+ continue # skip incomplete rows
- # Get or create scientific name entry
- sci = PlantScientificName.query.filter_by(name=sci_name.strip()).first()
- if not sci:
- sci = PlantScientificName(name=sci_name.strip())
- db.session.add(sci)
- db.session.flush() # ensure sci.id is available
+ # Common Name
+ common = PlantCommon.query.filter_by(name=name).first()
+ if not common:
+ common = PlantCommon(name=name)
+ db.session.add(common)
+ db.session.flush()
- # Create plant entry
- new_plant = Plant(
- uuid=str(uuid4()),
- name=name.strip(),
- scientific_name_id=sci.id,
- type=plant_type.strip(),
- status=plant_status.strip(),
- description=desc.strip()
+ # Scientific 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()
+
+ # Plant
+ 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
)
- db.session.add(new_plant)
- db.session.flush() # get new_plant.id for ownership log
+ db.session.add(plant)
- # Log ownership
- log = PlantOwnershipLog(
- plant_id=new_plant.id,
- user_id=current_user.id,
- date_acquired=datetime.utcnow()
- )
- db.session.add(log)
+ # 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())
- db.session.commit()
- flash(f"Successfully imported {len(rows)} plants.", "success")
- return redirect(url_for("importer.upload_csv"))
+ db.session.commit()
+ neo.close()
+
+ flash("CSV imported successfully with lineage.", "success")
except Exception as e:
- db.session.rollback()
- flash(f"Error importing CSV: {str(e)}", "danger")
- return redirect(request.url)
+ flash(f"Import failed: {str(e)}", "error")
- return render_template("importer/upload.html", headers=", ".join(headers))
+ return redirect(request.url)
+
+ return render_template("importer/upload.html")
diff --git a/plugins/media/models.py b/plugins/media/models.py
index fe4d3bf..8662ba4 100644
--- a/plugins/media/models.py
+++ b/plugins/media/models.py
@@ -1,6 +1,6 @@
from app import db
from datetime import datetime
-
+from plugins.plant.models import Plant
class Media(db.Model):
__tablename__ = 'media'
@@ -10,7 +10,7 @@ class Media(db.Model):
file_url = db.Column(db.String(256), nullable=False)
uploaded_at = db.Column(db.DateTime, default=datetime.utcnow)
- plant_id = db.Column(db.Integer, db.ForeignKey('plants.id'), nullable=True)
+ plant_id = db.Column(db.Integer, db.ForeignKey('plant.id'), nullable=True)
growlog_id = db.Column(db.Integer, db.ForeignKey('grow_logs.id'), nullable=True)
update_id = db.Column(db.Integer, db.ForeignKey('plant_updates.id'), nullable=True)
diff --git a/plugins/plant/models.py b/plugins/plant/models.py
index 24766e0..c0cf245 100644
--- a/plugins/plant/models.py
+++ b/plugins/plant/models.py
@@ -1,59 +1,84 @@
+from flask_sqlalchemy import SQLAlchemy
+import uuid as uuid_lib
from datetime import datetime
from app import db
-from plugins.search.models import Tag, plant_tags
-from plugins.growlog.models import PlantUpdate
-class PlantCommonName(db.Model):
- __tablename__ = 'plants_common'
- id = db.Column(db.Integer, primary_key=True)
- name = db.Column(db.String(120), unique=True, nullable=False)
+# Association table for tags
+plant_tags = db.Table(
+ 'plant_tags',
+ db.metadata,
+ db.Column('plant_id', db.Integer, db.ForeignKey('plant.id'), primary_key=True),
+ db.Column('tag_id', db.Integer, db.ForeignKey('tag.id'), primary_key=True),
+ extend_existing=True
+)
-class PlantScientificName(db.Model):
- __tablename__ = 'plants_scientific'
+class Tag(db.Model):
+ __tablename__ = 'tag'
+ __table_args__ = {'extend_existing': True}
+
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255), unique=True, nullable=False)
+class PlantCommonName(db.Model):
+ __tablename__ = 'plant_common_name'
+ __table_args__ = {'extend_existing': True}
+
+ id = db.Column(db.Integer, primary_key=True)
+ name = db.Column(db.String(255), unique=True, nullable=False)
+
+class PlantScientificName(db.Model):
+ __tablename__ = 'plant_scientific_name'
+ __table_args__ = {'extend_existing': True}
+
+ id = db.Column(db.Integer, primary_key=True)
+ name = db.Column(db.String(255), unique=True, nullable=False)
+ common_id = db.Column(db.Integer, db.ForeignKey('plant_common_name.id'), nullable=False)
+
class PlantLineage(db.Model):
__tablename__ = 'plant_lineage'
+ __table_args__ = {'extend_existing': True}
+
id = db.Column(db.Integer, primary_key=True)
- parent_plant_id = db.Column(db.Integer, db.ForeignKey('plants.id'), nullable=False)
- child_plant_id = db.Column(db.Integer, db.ForeignKey('plants.id'), nullable=False)
+ child_plant_id = db.Column(db.Integer, db.ForeignKey('plant.id'), nullable=False)
+ parent_plant_id = db.Column(db.Integer, db.ForeignKey('plant.id'), nullable=False)
+ type = db.Column(db.String(50), nullable=False) # cutting, seed, division
class PlantOwnershipLog(db.Model):
__tablename__ = 'plant_ownership_log'
- id = db.Column(db.Integer, primary_key=True)
- plant_id = db.Column(db.Integer, db.ForeignKey('plants.id'), nullable=False)
- user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
- date_acquired = db.Column(db.DateTime, default=datetime.utcnow)
- date_relinquished = db.Column(db.DateTime, nullable=True)
-
-class Plant(db.Model):
- __tablename__ = 'plants'
- id = db.Column(db.Integer, primary_key=True)
- common_name_id = db.Column(db.Integer, db.ForeignKey('plants_common.id'))
- scientific_name_id = db.Column(db.Integer, db.ForeignKey('plants_scientific.id'))
- parent_id = db.Column(db.Integer, db.ForeignKey('plants.id'), nullable=True)
- is_dead = db.Column(db.Boolean, default=False)
- date_added = db.Column(db.DateTime, default=datetime.utcnow)
- created_by_user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
+ __table_args__ = {'extend_existing': True}
-class Plant(db.Model):
id = db.Column(db.Integer, primary_key=True)
- uuid = db.Column(db.String(36), unique=True, nullable=False, default=lambda: str(uuid4()))
- mother_uuid = db.Column(db.String(36), nullable=True) # Optional parent reference
- name_id = db.Column(db.Integer, db.ForeignKey("plant_common_name.id"), nullable=True)
- scientific_name_id = db.Column(db.Integer, db.ForeignKey("plant_scientific_name.id"), nullable=True)
- plant_type = db.Column(db.String(50), nullable=False)
- status = db.Column(db.String(50), nullable=False)
- notes = db.Column(db.Text)
+ plant_id = db.Column(db.Integer, db.ForeignKey('plant.id'), nullable=False)
+ user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
+ start_time = db.Column(db.DateTime, nullable=False)
+ end_time = db.Column(db.DateTime, nullable=True)
+ transfer_note = db.Column(db.Text, nullable=True)
+class Plant(db.Model):
+ __tablename__ = 'plant'
+ __table_args__ = {'extend_existing': True}
+
+ id = db.Column(db.Integer, primary_key=True)
+ uuid = db.Column(db.String(36), default=lambda: str(uuid_lib.uuid4()), unique=True, nullable=False)
+ custom_slug = db.Column(db.String(255), unique=True, nullable=True)
+
+ owner_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
+ common_id = db.Column(db.Integer, db.ForeignKey('plant_common_name.id'), nullable=False)
+ scientific_id = db.Column(db.Integer, db.ForeignKey('plant_scientific_name.id'), nullable=False)
+
+ plant_type = db.Column(db.String(50), nullable=False)
+ status = db.Column(db.String(50), nullable=False, default='active')
+ notes = db.Column(db.Text, nullable=True)
+
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
+ transferred = db.Column(db.Boolean, default=False)
+ graph_node_id = db.Column(db.String(255), nullable=True)
# Relationships
updates = db.relationship('PlantUpdate', backref='growlog', lazy=True)
lineage = db.relationship('PlantLineage', backref='child', lazy=True, foreign_keys='PlantLineage.child_plant_id')
tags = db.relationship('Tag', secondary=plant_tags, backref='plants')
-
- # → relationships so we can pull in the actual names:
+
common_name = db.relationship(
'PlantCommonName',
backref=db.backref('plants', lazy='dynamic'),
@@ -63,4 +88,7 @@ class Plant(db.Model):
'PlantScientificName',
backref=db.backref('plants', lazy='dynamic'),
lazy=True
- )
\ No newline at end of file
+ )
+
+PlantCommon = PlantCommonName
+PlantScientific = PlantScientificName
diff --git a/plugins/plant/routes.py b/plugins/plant/routes.py
index 98b6d0e..482394a 100644
--- a/plugins/plant/routes.py
+++ b/plugins/plant/routes.py
@@ -7,7 +7,7 @@ bp = Blueprint('plant', __name__, template_folder='templates')
@bp.route('/plants')
def index():
- plants = Plant.query.order_by(Plant.date_added.desc()).all()
+ plants = Plant.query.order_by(Plant.created_at.desc()).all()
return render_template('plant/index.html', plants=plants)
@bp.route('/plants/')
diff --git a/plugins/plant/templates/plant/detail.html b/plugins/plant/templates/plant/detail.html
index 6140ce3..f5a907e 100644
--- a/plugins/plant/templates/plant/detail.html
+++ b/plugins/plant/templates/plant/detail.html
@@ -16,7 +16,7 @@
{% endif %}
- Date Added
- - {{ plant.date_added.strftime('%Y-%m-%d') }}
+ - {{ plant.created_at.strftime('%Y-%m-%d') }}
- Status
-
diff --git a/plugins/search/models.py b/plugins/search/models.py
index 7b18cbc..647a7a3 100644
--- a/plugins/search/models.py
+++ b/plugins/search/models.py
@@ -1,12 +1,17 @@
from app import db
+#from plugins.plant.models import Plant
-plant_tags = db.Table(
- 'plant_tags',
- db.Column('plant_id', db.Integer, db.ForeignKey('plants.id'), primary_key=True),
- db.Column('tag_id', db.Integer, db.ForeignKey('tags.id'), primary_key=True)
-)
+# plant_tags = db.Table(
+# 'plant_tags',
+# db.metadata,
+# db.Column('plant_id', db.Integer, db.ForeignKey('plant.id'), primary_key=True),
+# db.Column('tag_id', db.Integer, db.ForeignKey('tags.id'), primary_key=True),
+# extend_existing=True
+# )
-class Tag(db.Model):
- __tablename__ = 'tags'
- id = db.Column(db.Integer, primary_key=True)
- name = db.Column(db.String(100), unique=True, nullable=False)
+# class Tag(db.Model):
+# __tablename__ = 'tags'
+# __table_args__ = {'extend_existing': True}
+
+# id = db.Column(db.Integer, primary_key=True)
+# name = db.Column(db.String(100), unique=True, nullable=False)
diff --git a/plugins/search/routes.py b/plugins/search/routes.py
index 987b79f..def2b9c 100644
--- a/plugins/search/routes.py
+++ b/plugins/search/routes.py
@@ -1,9 +1,8 @@
from flask import Blueprint, render_template, request, jsonify
from flask_login import login_required, current_user
from app import db
-from .models import Tag
from .forms import SearchForm
-from plugins.plant.models import Plant
+from plugins.plant.models import Plant, Tag
bp = Blueprint('search', __name__, template_folder='templates')
diff --git a/plugins/submission/models.py b/plugins/submission/models.py
index 1f5ca13..fc83ee2 100644
--- a/plugins/submission/models.py
+++ b/plugins/submission/models.py
@@ -1,13 +1,15 @@
from datetime import datetime
from app import db
+from plugins.plant.models import Plant
class Submission(db.Model):
__tablename__ = 'submissions'
+ __table_args__ = {'extend_existing': True}
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
- plant_id = db.Column(db.Integer, db.ForeignKey('plants.id'), nullable=True)
+ plant_id = db.Column(db.Integer, db.ForeignKey('plant.id'), nullable=True)
common_name = db.Column(db.String(120), nullable=False)
scientific_name = db.Column(db.String(120))
@@ -29,6 +31,7 @@ class Submission(db.Model):
class SubmissionImage(db.Model):
__tablename__ = 'submission_images'
+ __table_args__ = {'extend_existing': True}
id = db.Column(db.Integer, primary_key=True)
submission_id = db.Column(db.Integer, db.ForeignKey('submissions.id'), nullable=False)
diff --git a/requirements.txt b/requirements.txt
index 71b8d71..8cf5ae6 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -8,3 +8,5 @@ Werkzeug>=2.3.0
pymysql
python-dotenv
cryptography
+
+neo4j>=5.18.0