tons of fun
This commit is contained in:
@ -13,3 +13,7 @@ MYSQL_USER=plant_user
|
|||||||
MYSQL_PASSWORD=plant_pass
|
MYSQL_PASSWORD=plant_pass
|
||||||
MYSQL_ROOT_PASSWORD=supersecret
|
MYSQL_ROOT_PASSWORD=supersecret
|
||||||
|
|
||||||
|
|
||||||
|
NEO4J_URI=bolt://neo4j:7687
|
||||||
|
NEO4J_USER=neo4j
|
||||||
|
NEO4J_PASSWORD=your_secure_password
|
||||||
|
@ -24,3 +24,7 @@ class Config:
|
|||||||
# Optional toggles
|
# Optional toggles
|
||||||
ENABLE_DB_SEEDING = os.environ.get('ENABLE_DB_SEEDING', '0') == '1'
|
ENABLE_DB_SEEDING = os.environ.get('ENABLE_DB_SEEDING', '0') == '1'
|
||||||
DOCKER_ENV = os.environ.get('FLASK_ENV', 'production')
|
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')
|
||||||
|
32
app/neo4j_utils.py
Normal file
32
app/neo4j_utils.py
Normal file
@ -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)
|
@ -54,5 +54,17 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- 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:
|
volumes:
|
||||||
plant_price_tracker_mysql_data:
|
plant_price_tracker_mysql_data:
|
||||||
|
neo4j_data:
|
||||||
|
@ -5,6 +5,7 @@ from app import db
|
|||||||
|
|
||||||
class User(db.Model, UserMixin):
|
class User(db.Model, UserMixin):
|
||||||
__tablename__ = 'users'
|
__tablename__ = 'users'
|
||||||
|
__table_args__ = {'extend_existing': True}
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
email = db.Column(db.String(120), unique=True, nullable=False)
|
email = db.Column(db.String(120), unique=True, nullable=False)
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
<ul class="navbar-nav me-auto">
|
<ul class="navbar-nav me-auto">
|
||||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('core_ui.home') }}">Home</a></li>
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('core_ui.home') }}">Home</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('plant.index') }}">Plants</a></li>
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('plant.index') }}">Plants</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('importer.upload_csv') }}">Import</a></li>
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('importer.upload') }}">Import</a></li>
|
||||||
{% if current_user.is_authenticated and current_user.role == 'admin' %}
|
{% if current_user.is_authenticated and current_user.role == 'admin' %}
|
||||||
<li class="nav-item"><a class="nav-link text-danger" href="{{ url_for('admin.admin_dashboard') }}">Admin Dashboard</a></li>
|
<li class="nav-item"><a class="nav-link text-danger" href="{{ url_for('admin.admin_dashboard') }}">Admin Dashboard</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from app import db
|
from app import db
|
||||||
|
from plugins.plant.models import Plant
|
||||||
|
|
||||||
|
|
||||||
class GrowLog(db.Model):
|
class GrowLog(db.Model):
|
||||||
__tablename__ = 'grow_logs'
|
__tablename__ = 'grow_logs'
|
||||||
|
__table_args__ = {'extend_existing': True}
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=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)
|
title = db.Column(db.String(255), nullable=False)
|
||||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
@ -18,7 +20,7 @@ class PlantUpdate(db.Model):
|
|||||||
__table_args__ = {'extend_existing': True}
|
__table_args__ = {'extend_existing': True}
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=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)
|
growlog_id = db.Column(db.Integer, db.ForeignKey('grow_logs.id'), nullable=True)
|
||||||
update_type = db.Column(db.String(50), nullable=False)
|
update_type = db.Column(db.String(50), nullable=False)
|
||||||
description = db.Column(db.Text, nullable=True)
|
description = db.Column(db.Text, nullable=True)
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
def register_commands(app):
|
||||||
|
pass
|
||||||
|
@ -1,174 +1,77 @@
|
|||||||
# plugins/importer/routes.py
|
|
||||||
|
|
||||||
import csv
|
import csv
|
||||||
import io
|
import io
|
||||||
import re
|
from flask import Blueprint, request, render_template, redirect, flash
|
||||||
import uuid
|
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 (
|
bp = Blueprint("importer", __name__, template_folder="templates")
|
||||||
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.route("/import/", methods=["GET", "POST"])
|
||||||
|
def upload():
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
file = request.files.get("file")
|
file = request.files.get("file")
|
||||||
if not file or not file.filename.endswith(".csv"):
|
if not file:
|
||||||
flash("Please upload a valid CSV file.", "danger")
|
flash("No file uploaded.", "error")
|
||||||
return redirect(request.url)
|
return redirect(request.url)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
stream = io.StringIO(file.stream.read().decode("UTF-8"))
|
# Handle UTF-8 BOM (Byte Order Mark) if present
|
||||||
csv_reader = csv.reader(stream)
|
decoded = file.read().decode("utf-8-sig")
|
||||||
data = list(csv_reader)
|
stream = io.StringIO(decoded)
|
||||||
|
reader = csv.DictReader(stream)
|
||||||
|
|
||||||
if not data or data[0] != headers:
|
neo = get_neo4j_handler()
|
||||||
flash("CSV headers must exactly match: " + ", ".join(headers), "danger")
|
|
||||||
return redirect(request.url)
|
|
||||||
|
|
||||||
# Skip header row
|
for row in reader:
|
||||||
rows = data[1:]
|
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
|
if not all([uuid, name, sci_name]):
|
||||||
for row in rows:
|
continue # skip incomplete rows
|
||||||
name, sci_name, plant_type, plant_status, desc = row
|
|
||||||
|
|
||||||
# Get or create scientific name entry
|
# Common Name
|
||||||
sci = PlantScientificName.query.filter_by(name=sci_name.strip()).first()
|
common = PlantCommon.query.filter_by(name=name).first()
|
||||||
if not sci:
|
if not common:
|
||||||
sci = PlantScientificName(name=sci_name.strip())
|
common = PlantCommon(name=name)
|
||||||
db.session.add(sci)
|
db.session.add(common)
|
||||||
db.session.flush() # ensure sci.id is available
|
db.session.flush()
|
||||||
|
|
||||||
# Create plant entry
|
# Scientific Name
|
||||||
new_plant = Plant(
|
scientific = PlantScientific.query.filter_by(name=sci_name).first()
|
||||||
uuid=str(uuid4()),
|
if not scientific:
|
||||||
name=name.strip(),
|
scientific = PlantScientific(name=sci_name, common_id=common.id)
|
||||||
scientific_name_id=sci.id,
|
db.session.add(scientific)
|
||||||
type=plant_type.strip(),
|
db.session.flush()
|
||||||
status=plant_status.strip(),
|
|
||||||
description=desc.strip()
|
# 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.add(plant)
|
||||||
db.session.flush() # get new_plant.id for ownership log
|
|
||||||
|
|
||||||
# Log ownership
|
# Neo4j
|
||||||
log = PlantOwnershipLog(
|
neo.create_plant_node(uuid, name)
|
||||||
plant_id=new_plant.id,
|
if mother_uuid and mother_uuid.strip():
|
||||||
user_id=current_user.id,
|
neo.create_plant_node(mother_uuid.strip(), "Parent")
|
||||||
date_acquired=datetime.utcnow()
|
neo.create_lineage(uuid, mother_uuid.strip())
|
||||||
)
|
|
||||||
db.session.add(log)
|
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(f"Successfully imported {len(rows)} plants.", "success")
|
neo.close()
|
||||||
return redirect(url_for("importer.upload_csv"))
|
|
||||||
|
flash("CSV imported successfully with lineage.", "success")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
flash(f"Import failed: {str(e)}", "error")
|
||||||
flash(f"Error importing CSV: {str(e)}", "danger")
|
|
||||||
return redirect(request.url)
|
|
||||||
|
|
||||||
return render_template("importer/upload.html", headers=", ".join(headers))
|
return redirect(request.url)
|
||||||
|
|
||||||
|
return render_template("importer/upload.html")
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from app import db
|
from app import db
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from plugins.plant.models import Plant
|
||||||
|
|
||||||
class Media(db.Model):
|
class Media(db.Model):
|
||||||
__tablename__ = 'media'
|
__tablename__ = 'media'
|
||||||
@ -10,7 +10,7 @@ class Media(db.Model):
|
|||||||
file_url = db.Column(db.String(256), nullable=False)
|
file_url = db.Column(db.String(256), nullable=False)
|
||||||
uploaded_at = db.Column(db.DateTime, default=datetime.utcnow)
|
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)
|
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)
|
update_id = db.Column(db.Integer, db.ForeignKey('plant_updates.id'), nullable=True)
|
||||||
|
|
||||||
|
@ -1,59 +1,84 @@
|
|||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
import uuid as uuid_lib
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from app import db
|
from app import db
|
||||||
from plugins.search.models import Tag, plant_tags
|
|
||||||
from plugins.growlog.models import PlantUpdate
|
|
||||||
|
|
||||||
class PlantCommonName(db.Model):
|
# Association table for tags
|
||||||
__tablename__ = 'plants_common'
|
plant_tags = db.Table(
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
'plant_tags',
|
||||||
name = db.Column(db.String(120), unique=True, nullable=False)
|
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):
|
class Tag(db.Model):
|
||||||
__tablename__ = 'plants_scientific'
|
__tablename__ = 'tag'
|
||||||
|
__table_args__ = {'extend_existing': True}
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
name = db.Column(db.String(255), unique=True, nullable=False)
|
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):
|
class PlantLineage(db.Model):
|
||||||
__tablename__ = 'plant_lineage'
|
__tablename__ = 'plant_lineage'
|
||||||
|
__table_args__ = {'extend_existing': True}
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=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('plant.id'), nullable=False)
|
||||||
child_plant_id = db.Column(db.Integer, db.ForeignKey('plants.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):
|
class PlantOwnershipLog(db.Model):
|
||||||
__tablename__ = 'plant_ownership_log'
|
__tablename__ = 'plant_ownership_log'
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
__table_args__ = {'extend_existing': 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'))
|
|
||||||
|
|
||||||
class Plant(db.Model):
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
uuid = db.Column(db.String(36), unique=True, nullable=False, default=lambda: str(uuid4()))
|
plant_id = db.Column(db.Integer, db.ForeignKey('plant.id'), nullable=False)
|
||||||
mother_uuid = db.Column(db.String(36), nullable=True) # Optional parent reference
|
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||||
name_id = db.Column(db.Integer, db.ForeignKey("plant_common_name.id"), nullable=True)
|
start_time = db.Column(db.DateTime, nullable=False)
|
||||||
scientific_name_id = db.Column(db.Integer, db.ForeignKey("plant_scientific_name.id"), nullable=True)
|
end_time = db.Column(db.DateTime, nullable=True)
|
||||||
plant_type = db.Column(db.String(50), nullable=False)
|
transfer_note = db.Column(db.Text, nullable=True)
|
||||||
status = db.Column(db.String(50), nullable=False)
|
|
||||||
notes = db.Column(db.Text)
|
|
||||||
|
|
||||||
|
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
|
# Relationships
|
||||||
updates = db.relationship('PlantUpdate', backref='growlog', lazy=True)
|
updates = db.relationship('PlantUpdate', backref='growlog', lazy=True)
|
||||||
lineage = db.relationship('PlantLineage', backref='child', lazy=True, foreign_keys='PlantLineage.child_plant_id')
|
lineage = db.relationship('PlantLineage', backref='child', lazy=True, foreign_keys='PlantLineage.child_plant_id')
|
||||||
tags = db.relationship('Tag', secondary=plant_tags, backref='plants')
|
tags = db.relationship('Tag', secondary=plant_tags, backref='plants')
|
||||||
|
|
||||||
# → relationships so we can pull in the actual names:
|
|
||||||
common_name = db.relationship(
|
common_name = db.relationship(
|
||||||
'PlantCommonName',
|
'PlantCommonName',
|
||||||
backref=db.backref('plants', lazy='dynamic'),
|
backref=db.backref('plants', lazy='dynamic'),
|
||||||
@ -63,4 +88,7 @@ class Plant(db.Model):
|
|||||||
'PlantScientificName',
|
'PlantScientificName',
|
||||||
backref=db.backref('plants', lazy='dynamic'),
|
backref=db.backref('plants', lazy='dynamic'),
|
||||||
lazy=True
|
lazy=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
PlantCommon = PlantCommonName
|
||||||
|
PlantScientific = PlantScientificName
|
||||||
|
@ -7,7 +7,7 @@ bp = Blueprint('plant', __name__, template_folder='templates')
|
|||||||
|
|
||||||
@bp.route('/plants')
|
@bp.route('/plants')
|
||||||
def index():
|
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)
|
return render_template('plant/index.html', plants=plants)
|
||||||
|
|
||||||
@bp.route('/plants/<int:plant_id>')
|
@bp.route('/plants/<int:plant_id>')
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<dl class="row">
|
<dl class="row">
|
||||||
<dt class="col-sm-3">Date Added</dt>
|
<dt class="col-sm-3">Date Added</dt>
|
||||||
<dd class="col-sm-9">{{ plant.date_added.strftime('%Y-%m-%d') }}</dd>
|
<dd class="col-sm-9">{{ plant.created_at.strftime('%Y-%m-%d') }}</dd>
|
||||||
|
|
||||||
<dt class="col-sm-3">Status</dt>
|
<dt class="col-sm-3">Status</dt>
|
||||||
<dd class="col-sm-9">
|
<dd class="col-sm-9">
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
from app import db
|
from app import db
|
||||||
|
#from plugins.plant.models import Plant
|
||||||
|
|
||||||
plant_tags = db.Table(
|
# plant_tags = db.Table(
|
||||||
'plant_tags',
|
# 'plant_tags',
|
||||||
db.Column('plant_id', db.Integer, db.ForeignKey('plants.id'), primary_key=True),
|
# db.metadata,
|
||||||
db.Column('tag_id', db.Integer, db.ForeignKey('tags.id'), primary_key=True)
|
# 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):
|
# class Tag(db.Model):
|
||||||
__tablename__ = 'tags'
|
# __tablename__ = 'tags'
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
# __table_args__ = {'extend_existing': True}
|
||||||
name = db.Column(db.String(100), unique=True, nullable=False)
|
|
||||||
|
# id = db.Column(db.Integer, primary_key=True)
|
||||||
|
# name = db.Column(db.String(100), unique=True, nullable=False)
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
from flask import Blueprint, render_template, request, jsonify
|
from flask import Blueprint, render_template, request, jsonify
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
from app import db
|
from app import db
|
||||||
from .models import Tag
|
|
||||||
from .forms import SearchForm
|
from .forms import SearchForm
|
||||||
from plugins.plant.models import Plant
|
from plugins.plant.models import Plant, Tag
|
||||||
|
|
||||||
bp = Blueprint('search', __name__, template_folder='templates')
|
bp = Blueprint('search', __name__, template_folder='templates')
|
||||||
|
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from app import db
|
from app import db
|
||||||
|
from plugins.plant.models import Plant
|
||||||
|
|
||||||
|
|
||||||
class Submission(db.Model):
|
class Submission(db.Model):
|
||||||
__tablename__ = 'submissions'
|
__tablename__ = 'submissions'
|
||||||
|
__table_args__ = {'extend_existing': True}
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
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)
|
common_name = db.Column(db.String(120), nullable=False)
|
||||||
scientific_name = db.Column(db.String(120))
|
scientific_name = db.Column(db.String(120))
|
||||||
@ -29,6 +31,7 @@ class Submission(db.Model):
|
|||||||
|
|
||||||
class SubmissionImage(db.Model):
|
class SubmissionImage(db.Model):
|
||||||
__tablename__ = 'submission_images'
|
__tablename__ = 'submission_images'
|
||||||
|
__table_args__ = {'extend_existing': True}
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
submission_id = db.Column(db.Integer, db.ForeignKey('submissions.id'), nullable=False)
|
submission_id = db.Column(db.Integer, db.ForeignKey('submissions.id'), nullable=False)
|
||||||
|
@ -8,3 +8,5 @@ Werkzeug>=2.3.0
|
|||||||
pymysql
|
pymysql
|
||||||
python-dotenv
|
python-dotenv
|
||||||
cryptography
|
cryptography
|
||||||
|
|
||||||
|
neo4j>=5.18.0
|
||||||
|
Reference in New Issue
Block a user