tons of fun
This commit is contained in:
@ -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
|
||||
|
@ -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')
|
||||
|
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:
|
||||
- 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:
|
||||
|
@ -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)
|
||||
|
@ -31,7 +31,7 @@
|
||||
<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('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' %}
|
||||
<li class="nav-item"><a class="nav-link text-danger" href="{{ url_for('admin.admin_dashboard') }}">Admin Dashboard</a></li>
|
||||
{% endif %}
|
||||
|
@ -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)
|
||||
|
@ -0,0 +1,2 @@
|
||||
def register_commands(app):
|
||||
pass
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
)
|
||||
)
|
||||
|
||||
PlantCommon = PlantCommonName
|
||||
PlantScientific = PlantScientificName
|
||||
|
@ -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/<int:plant_id>')
|
||||
|
@ -16,7 +16,7 @@
|
||||
{% endif %}
|
||||
<dl class="row">
|
||||
<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>
|
||||
<dd class="col-sm-9">
|
||||
|
@ -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)
|
||||
|
@ -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')
|
||||
|
||||
|
@ -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)
|
||||
|
@ -8,3 +8,5 @@ Werkzeug>=2.3.0
|
||||
pymysql
|
||||
python-dotenv
|
||||
cryptography
|
||||
|
||||
neo4j>=5.18.0
|
||||
|
Reference in New Issue
Block a user