sort of working, more changes

This commit is contained in:
2025-06-09 05:45:58 -05:00
parent d442cad0bb
commit f0b1abd622
102 changed files with 1448 additions and 2264 deletions

View File

@ -17,3 +17,5 @@ MYSQL_ROOT_PASSWORD=supersecret
NEO4J_URI=bolt://neo4j:7687
NEO4J_USER=neo4j
NEO4J_PASSWORD=your_secure_password
STANDARD_IMG_SIZE=300x200

View File

@ -1,5 +1,8 @@
import os
# Define basedir so its available inside the Config class
basedir = os.path.abspath(os.path.dirname(__file__))
class Config:
SECRET_KEY = os.environ['SECRET_KEY']
UPLOAD_FOLDER = os.environ['UPLOAD_FOLDER']
@ -28,3 +31,11 @@ class Config:
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')
# Override or default upload folder
UPLOAD_FOLDER = os.path.join(basedir, "static", "uploads")
# Standard image size (for placeholders, etc.)
STANDARD_IMG_SIZE = tuple(
map(int, os.getenv('STANDARD_IMG_SIZE', '300x200').split('x'))
)

BIN
main-app-new.zip Normal file

Binary file not shown.

Binary file not shown.

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 0fcf1e150ae2
Revises: 58516c9892e9
Create Date: 2025-06-05 09:31:44.116783
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '0fcf1e150ae2'
down_revision = '58516c9892e9'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,8 +1,8 @@
"""auto
Revision ID: 07d152ee2ac2
Revises: 0171b270afc1
Create Date: 2025-06-04 06:24:51.986909
Revision ID: 13e8b68e0737
Revises: 3065b811b58f
Create Date: 2025-06-09 08:05:24.660884
"""
from alembic import op
@ -10,8 +10,8 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '07d152ee2ac2'
down_revision = '0171b270afc1'
revision = '13e8b68e0737'
down_revision = '3065b811b58f'
branch_labels = None
depends_on = None

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 1c7cef84b4ae
Revises: 26803929dc3e
Create Date: 2025-06-04 22:07:43.375613
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '1c7cef84b4ae'
down_revision = '26803929dc3e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('plant', sa.Column('is_verified', sa.Boolean(), nullable=False))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('plant', 'is_verified')
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 1cd7fa3f84ce
Revises: e0afd892f86e
Create Date: 2025-06-09 02:46:59.792016
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '1cd7fa3f84ce'
down_revision = 'e0afd892f86e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 1edc2e2c93cd
Revises: d11c2e8b173a
Create Date: 2025-06-09 04:36:51.035371
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '1edc2e2c93cd'
down_revision = 'd11c2e8b173a'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,8 +1,8 @@
"""auto
Revision ID: 00991befbf1d
Revises: 1cd7fa3f84ce
Create Date: 2025-06-09 02:54:23.747908
Revision ID: 1f5b1e0b6b05
Revises: 3426fe15f0ce
Create Date: 2025-06-09 09:41:17.949317
"""
from alembic import op
@ -10,8 +10,8 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '00991befbf1d'
down_revision = '1cd7fa3f84ce'
revision = '1f5b1e0b6b05'
down_revision = '3426fe15f0ce'
branch_labels = None
depends_on = None

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 246f5cce6c15
Revises: 90401e0dbe75
Create Date: 2025-06-09 05:22:52.461981
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '246f5cce6c15'
down_revision = '90401e0dbe75'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 263b128622a9
Revises: 00991befbf1d
Create Date: 2025-06-09 04:06:48.275595
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '263b128622a9'
down_revision = '00991befbf1d'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 26803929dc3e
Revises: 07d152ee2ac2
Create Date: 2025-06-04 06:38:27.377036
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '26803929dc3e'
down_revision = '07d152ee2ac2'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 27a65a4e055c
Revises: 48d93714beaf
Create Date: 2025-06-05 04:23:44.796455
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '27a65a4e055c'
down_revision = '48d93714beaf'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 2a0b02a42543
Revises: 93b893e47742
Create Date: 2025-06-05 02:41:56.741133
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '2a0b02a42543'
down_revision = '93b893e47742'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 2fa6feb17477
Revises: 9cff183551e1
Create Date: 2025-06-05 00:45:39.693560
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '2fa6feb17477'
down_revision = '9cff183551e1'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,8 +1,8 @@
"""auto
Revision ID: 0171b270afc1
Revises: 4d9859ada63b
Create Date: 2025-06-04 06:20:47.463202
Revision ID: 3065b811b58f
Revises: fa22b011d450
Create Date: 2025-06-09 07:41:07.546689
"""
from alembic import op
@ -10,8 +10,8 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '0171b270afc1'
down_revision = '4d9859ada63b'
revision = '3065b811b58f'
down_revision = 'fa22b011d450'
branch_labels = None
depends_on = None

View File

@ -1,8 +1,8 @@
"""auto
Revision ID: 01e2d617ae49
Revises: 460dbe73c1bc
Create Date: 2025-06-09 04:23:50.730127
Revision ID: 33e98411843d
Revises: c10353a20277
Create Date: 2025-06-09 09:30:42.712274
"""
from alembic import op
@ -10,8 +10,8 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '01e2d617ae49'
down_revision = '460dbe73c1bc'
revision = '33e98411843d'
down_revision = 'c10353a20277'
branch_labels = None
depends_on = None

View File

@ -0,0 +1,28 @@
"""auto
Revision ID: 3426fe15f0ce
Revises: 33e98411843d
Create Date: 2025-06-09 09:34:33.556990
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '3426fe15f0ce'
down_revision = '33e98411843d'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,70 +0,0 @@
"""auto
Revision ID: 373571dfe134
Revises: 0fcf1e150ae2
Create Date: 2025-06-05 09:38:55.414193
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '373571dfe134'
down_revision = '0fcf1e150ae2'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('submission_images', sa.Column('file_url', sa.String(length=256), nullable=False))
op.add_column('submission_images', sa.Column('uploaded_at', sa.DateTime(), nullable=True))
op.drop_column('submission_images', 'is_visible')
op.drop_column('submission_images', 'file_path')
op.add_column('submissions', sa.Column('submitted_at', sa.DateTime(), nullable=True))
op.add_column('submissions', sa.Column('plant_name', sa.String(length=100), nullable=False))
op.add_column('submissions', sa.Column('approved', sa.Boolean(), nullable=True))
op.add_column('submissions', sa.Column('approved_at', sa.DateTime(), nullable=True))
op.add_column('submissions', sa.Column('reviewed_by', sa.Integer(), nullable=True))
op.drop_constraint(op.f('submissions_ibfk_1'), 'submissions', type_='foreignkey')
op.create_foreign_key(None, 'submissions', 'users', ['reviewed_by'], ['id'])
op.drop_column('submissions', 'common_name')
op.drop_column('submissions', 'height')
op.drop_column('submissions', 'container_size')
op.drop_column('submissions', 'timestamp')
op.drop_column('submissions', 'price')
op.drop_column('submissions', 'plant_id')
op.drop_column('submissions', 'width')
op.drop_column('submissions', 'health_status')
op.drop_column('submissions', 'leaf_count')
op.drop_column('submissions', 'potting_mix')
op.drop_column('submissions', 'source')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('submissions', sa.Column('source', mysql.VARCHAR(length=120), nullable=True))
op.add_column('submissions', sa.Column('potting_mix', mysql.VARCHAR(length=255), nullable=True))
op.add_column('submissions', sa.Column('leaf_count', mysql.INTEGER(), autoincrement=False, nullable=True))
op.add_column('submissions', sa.Column('health_status', mysql.VARCHAR(length=50), nullable=True))
op.add_column('submissions', sa.Column('width', mysql.FLOAT(), nullable=True))
op.add_column('submissions', sa.Column('plant_id', mysql.INTEGER(), autoincrement=False, nullable=True))
op.add_column('submissions', sa.Column('price', mysql.FLOAT(), nullable=False))
op.add_column('submissions', sa.Column('timestamp', mysql.DATETIME(), nullable=True))
op.add_column('submissions', sa.Column('container_size', mysql.VARCHAR(length=120), nullable=True))
op.add_column('submissions', sa.Column('height', mysql.FLOAT(), nullable=True))
op.add_column('submissions', sa.Column('common_name', mysql.VARCHAR(length=120), nullable=False))
op.drop_constraint(None, 'submissions', type_='foreignkey')
op.create_foreign_key(op.f('submissions_ibfk_1'), 'submissions', 'plant', ['plant_id'], ['id'])
op.drop_column('submissions', 'reviewed_by')
op.drop_column('submissions', 'approved_at')
op.drop_column('submissions', 'approved')
op.drop_column('submissions', 'plant_name')
op.drop_column('submissions', 'submitted_at')
op.add_column('submission_images', sa.Column('file_path', mysql.VARCHAR(length=255), nullable=False))
op.add_column('submission_images', sa.Column('is_visible', mysql.TINYINT(display_width=1), autoincrement=False, nullable=True))
op.drop_column('submission_images', 'uploaded_at')
op.drop_column('submission_images', 'file_url')
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 39f714eda2bf
Revises: 6420e024f896
Create Date: 2025-06-06 09:50:29.954004
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '39f714eda2bf'
down_revision = '6420e024f896'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 3a0b96235583
Revises: 263b128622a9
Create Date: 2025-06-09 04:13:37.036064
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '3a0b96235583'
down_revision = '263b128622a9'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 401f262d79cc
Revises: 583fab3f9f80
Create Date: 2025-06-05 04:48:49.440383
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '401f262d79cc'
down_revision = '583fab3f9f80'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 408c432b5835
Revises: 77087ff2442e
Create Date: 2025-06-06 08:34:26.804782
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '408c432b5835'
down_revision = '77087ff2442e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,36 +0,0 @@
"""auto
Revision ID: 447ff559592b
Revises: 408c432b5835
Create Date: 2025-06-06 08:47:25.908940
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '447ff559592b'
down_revision = '408c432b5835'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('submissions', sa.Column('vendor_name', sa.String(length=255), nullable=True))
op.add_column('submissions', sa.Column('rating', sa.Integer(), nullable=True))
op.add_column('submissions', sa.Column('old_vendor', sa.String(length=255), nullable=True))
op.add_column('submissions', sa.Column('new_vendor', sa.String(length=255), nullable=True))
op.add_column('submissions', sa.Column('alias_reason', sa.Text(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('submissions', 'alias_reason')
op.drop_column('submissions', 'new_vendor')
op.drop_column('submissions', 'old_vendor')
op.drop_column('submissions', 'rating')
op.drop_column('submissions', 'vendor_name')
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 460dbe73c1bc
Revises: 3a0b96235583
Create Date: 2025-06-09 04:16:30.838677
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '460dbe73c1bc'
down_revision = '3a0b96235583'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 48d93714beaf
Revises: 761d0f8be3ff
Create Date: 2025-06-05 04:20:31.030479
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '48d93714beaf'
down_revision = '761d0f8be3ff'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 48fee8a8a3be
Revises: af76c66c9075
Create Date: 2025-06-05 00:25:55.439874
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '48fee8a8a3be'
down_revision = 'af76c66c9075'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 4bdec754b085
Revises: 27a65a4e055c
Create Date: 2025-06-05 04:34:19.085549
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '4bdec754b085'
down_revision = '27a65a4e055c'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 501b54868875
Revises: 401f262d79cc
Create Date: 2025-06-05 04:51:52.183453
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '501b54868875'
down_revision = '401f262d79cc'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 50d5ff358f96
Revises: 1c7cef84b4ae
Create Date: 2025-06-04 22:14:54.902029
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '50d5ff358f96'
down_revision = '1c7cef84b4ae'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -0,0 +1,28 @@
"""auto
Revision ID: 52e805a0163e
Revises: cb57ad0a3231
Create Date: 2025-06-09 09:48:19.311607
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '52e805a0163e'
down_revision = 'cb57ad0a3231'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -0,0 +1,28 @@
"""auto
Revision ID: 539c103a1ac4
Revises: b5b29b5b85ae
Create Date: 2025-06-09 10:32:37.666108
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '539c103a1ac4'
down_revision = 'b5b29b5b85ae'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 58022b5ab921
Revises: 50d5ff358f96
Create Date: 2025-06-04 22:32:06.203591
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '58022b5ab921'
down_revision = '50d5ff358f96'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 583fab3f9f80
Revises: 64ec4065d18d
Create Date: 2025-06-05 04:47:05.679772
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '583fab3f9f80'
down_revision = '64ec4065d18d'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 58516c9892e9
Revises: 85da58851d35
Create Date: 2025-06-05 05:28:30.947641
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '58516c9892e9'
down_revision = '85da58851d35'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 5c85ebc9451b
Revises: d8bfe4d4c083
Create Date: 2025-06-05 09:47:14.478039
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '5c85ebc9451b'
down_revision = 'd8bfe4d4c083'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 6420e024f896
Revises: 7d232205181b
Create Date: 2025-06-06 09:40:37.498453
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '6420e024f896'
down_revision = '7d232205181b'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,44 +0,0 @@
"""auto
Revision ID: 64c1927562cc
Revises: fb0243eaa7c3
Create Date: 2025-06-06 07:54:00.147383
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '64c1927562cc'
down_revision = 'fb0243eaa7c3'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('media', sa.Column('plant_id', sa.Integer(), nullable=True))
op.add_column('media', sa.Column('update_id', sa.Integer(), nullable=True))
op.drop_constraint(op.f('media_ibfk_2'), 'media', type_='foreignkey')
op.drop_constraint(op.f('media_ibfk_3'), 'media', type_='foreignkey')
op.create_foreign_key(None, 'media', 'plant', ['plant_id'], ['id'])
op.create_foreign_key(None, 'media', 'plant_updates', ['update_id'], ['id'])
op.drop_column('media', 'uploader_id')
op.drop_column('media', 'submission_id')
op.drop_column('media', 'caption')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('media', sa.Column('caption', mysql.VARCHAR(length=255), nullable=True))
op.add_column('media', sa.Column('submission_id', mysql.INTEGER(), autoincrement=False, nullable=True))
op.add_column('media', sa.Column('uploader_id', mysql.INTEGER(), autoincrement=False, nullable=False))
op.drop_constraint(None, 'media', type_='foreignkey')
op.drop_constraint(None, 'media', type_='foreignkey')
op.create_foreign_key(op.f('media_ibfk_3'), 'media', 'submissions', ['submission_id'], ['id'])
op.create_foreign_key(op.f('media_ibfk_2'), 'media', 'users', ['uploader_id'], ['id'])
op.drop_column('media', 'update_id')
op.drop_column('media', 'plant_id')
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 64ec4065d18d
Revises: 4bdec754b085
Create Date: 2025-06-05 04:40:02.186807
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '64ec4065d18d'
down_revision = '4bdec754b085'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 6539ef5f5419
Revises: 39f714eda2bf
Create Date: 2025-06-06 10:03:52.256341
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '6539ef5f5419'
down_revision = '39f714eda2bf'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 6cb1c3054071
Revises: f4987441cc85
Create Date: 2025-06-06 09:09:55.403015
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '6cb1c3054071'
down_revision = 'f4987441cc85'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -0,0 +1,28 @@
"""auto
Revision ID: 6fcf5e1ad9fa
Revises: a7883990430e
Create Date: 2025-06-09 10:27:44.541187
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '6fcf5e1ad9fa'
down_revision = 'a7883990430e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,46 +0,0 @@
"""auto
Revision ID: 72455429fdaf
Revises: 501b54868875
Create Date: 2025-06-05 05:07:43.605568
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '72455429fdaf'
down_revision = '501b54868875'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('transfer_request',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('plant_id', sa.Integer(), nullable=False),
sa.Column('seller_id', sa.Integer(), nullable=False),
sa.Column('buyer_id', sa.Integer(), nullable=False),
sa.Column('status', sa.String(length=20), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('seller_message', sa.String(length=512), nullable=True),
sa.Column('buyer_message', sa.String(length=512), nullable=True),
sa.ForeignKeyConstraint(['buyer_id'], ['users.id'], ),
sa.ForeignKeyConstraint(['plant_id'], ['plant.id'], ),
sa.ForeignKeyConstraint(['seller_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.add_column('plant', sa.Column('data_verified', sa.Boolean(), nullable=False))
op.drop_column('plant_ownership_log', 'graph_node_id')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('plant_ownership_log', sa.Column('graph_node_id', mysql.VARCHAR(length=255), nullable=True))
op.drop_column('plant', 'data_verified')
op.drop_table('transfer_request')
# ### end Alembic commands ###

View File

@ -1,93 +0,0 @@
"""auto
Revision ID: 761d0f8be3ff
Revises: ad9ea9d31b58
Create Date: 2025-06-05 04:18:09.403526
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '761d0f8be3ff'
down_revision = 'ad9ea9d31b58'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('plant_lineage')
op.add_column('plant', sa.Column('updated_at', sa.DateTime(), nullable=True))
op.drop_column('plant', 'transferred')
op.drop_column('plant', 'status')
op.drop_column('plant', 'is_verified')
op.drop_column('plant', 'graph_node_id')
op.drop_column('plant', 'notes')
op.add_column('plant_common_name', sa.Column('created_at', sa.DateTime(), nullable=True))
op.alter_column('plant_common_name', 'name',
existing_type=mysql.VARCHAR(length=255),
type_=sa.String(length=128),
existing_nullable=False)
op.add_column('plant_ownership_log', sa.Column('date_acquired', sa.DateTime(), nullable=True))
op.add_column('plant_ownership_log', sa.Column('transferred', sa.Boolean(), nullable=False))
op.add_column('plant_ownership_log', sa.Column('graph_node_id', sa.String(length=255), nullable=True))
op.add_column('plant_ownership_log', sa.Column('is_verified', sa.Boolean(), nullable=False))
op.drop_column('plant_ownership_log', 'start_time')
op.drop_column('plant_ownership_log', 'transfer_note')
op.drop_column('plant_ownership_log', 'end_time')
op.add_column('plant_scientific_name', sa.Column('created_at', sa.DateTime(), nullable=True))
op.alter_column('plant_scientific_name', 'name',
existing_type=mysql.VARCHAR(length=255),
type_=sa.String(length=256),
existing_nullable=False)
op.alter_column('tag', 'name',
existing_type=mysql.VARCHAR(length=255),
type_=sa.String(length=128),
existing_nullable=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('tag', 'name',
existing_type=sa.String(length=128),
type_=mysql.VARCHAR(length=255),
existing_nullable=False)
op.alter_column('plant_scientific_name', 'name',
existing_type=sa.String(length=256),
type_=mysql.VARCHAR(length=255),
existing_nullable=False)
op.drop_column('plant_scientific_name', 'created_at')
op.add_column('plant_ownership_log', sa.Column('end_time', mysql.DATETIME(), nullable=True))
op.add_column('plant_ownership_log', sa.Column('transfer_note', mysql.TEXT(), nullable=True))
op.add_column('plant_ownership_log', sa.Column('start_time', mysql.DATETIME(), nullable=False))
op.drop_column('plant_ownership_log', 'is_verified')
op.drop_column('plant_ownership_log', 'graph_node_id')
op.drop_column('plant_ownership_log', 'transferred')
op.drop_column('plant_ownership_log', 'date_acquired')
op.alter_column('plant_common_name', 'name',
existing_type=sa.String(length=128),
type_=mysql.VARCHAR(length=255),
existing_nullable=False)
op.drop_column('plant_common_name', 'created_at')
op.add_column('plant', sa.Column('notes', mysql.TEXT(), nullable=True))
op.add_column('plant', sa.Column('graph_node_id', mysql.VARCHAR(length=255), nullable=True))
op.add_column('plant', sa.Column('is_verified', mysql.TINYINT(display_width=1), autoincrement=False, nullable=False))
op.add_column('plant', sa.Column('status', mysql.VARCHAR(length=50), nullable=False))
op.add_column('plant', sa.Column('transferred', mysql.TINYINT(display_width=1), autoincrement=False, nullable=True))
op.drop_column('plant', 'updated_at')
op.create_table('plant_lineage',
sa.Column('id', mysql.INTEGER(), autoincrement=True, nullable=False),
sa.Column('child_plant_id', mysql.INTEGER(), autoincrement=False, nullable=False),
sa.Column('parent_plant_id', mysql.INTEGER(), autoincrement=False, nullable=False),
sa.Column('type', mysql.VARCHAR(length=50), nullable=False),
sa.ForeignKeyConstraint(['child_plant_id'], ['plant.id'], name=op.f('plant_lineage_ibfk_1')),
sa.ForeignKeyConstraint(['parent_plant_id'], ['plant.id'], name=op.f('plant_lineage_ibfk_2')),
sa.PrimaryKeyConstraint('id'),
mysql_collate='utf8mb4_0900_ai_ci',
mysql_default_charset='utf8mb4',
mysql_engine='InnoDB'
)
# ### end Alembic commands ###

View File

@ -1,38 +0,0 @@
"""auto
Revision ID: 77087ff2442e
Revises: 64c1927562cc
Create Date: 2025-06-06 08:10:58.028201
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '77087ff2442e'
down_revision = '64c1927562cc'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('media', sa.Column('uploader_id', sa.Integer(), nullable=False))
op.add_column('media', sa.Column('caption', sa.String(length=255), nullable=True))
op.create_foreign_key(None, 'media', 'users', ['uploader_id'], ['id'])
op.add_column('submissions', sa.Column('submission_type', sa.String(length=50), nullable=False))
op.add_column('submissions', sa.Column('price', sa.Float(), nullable=True))
op.add_column('submissions', sa.Column('source', sa.String(length=255), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('submissions', 'source')
op.drop_column('submissions', 'price')
op.drop_column('submissions', 'submission_type')
op.drop_constraint(None, 'media', type_='foreignkey')
op.drop_column('media', 'caption')
op.drop_column('media', 'uploader_id')
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 7d232205181b
Revises: fad6fe2b5e43
Create Date: 2025-06-06 09:38:39.786953
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '7d232205181b'
down_revision = 'fad6fe2b5e43'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -0,0 +1,28 @@
"""auto
Revision ID: 7d9fc95edc61
Revises: fa3de05c91fb
Create Date: 2025-06-09 10:10:23.833551
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '7d9fc95edc61'
down_revision = 'fa3de05c91fb'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 7dbb6d550055
Revises: 72455429fdaf
Create Date: 2025-06-05 05:10:43.392181
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '7dbb6d550055'
down_revision = '72455429fdaf'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 7dd0c2491053
Revises: 6539ef5f5419
Create Date: 2025-06-09 02:26:02.002280
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '7dd0c2491053'
down_revision = '6539ef5f5419'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 806e94a40aeb
Revises: e1cdc5f78f5e
Create Date: 2025-06-05 01:11:25.968741
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '806e94a40aeb'
down_revision = 'e1cdc5f78f5e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -0,0 +1,28 @@
"""auto
Revision ID: 80cf84342c5f
Revises: 539c103a1ac4
Create Date: 2025-06-09 10:35:15.685799
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '80cf84342c5f'
down_revision = '539c103a1ac4'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 85da58851d35
Revises: 8cd29b8fb6ec
Create Date: 2025-06-05 05:20:46.638884
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '85da58851d35'
down_revision = '8cd29b8fb6ec'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 8605e1ff50cd
Revises: cab9b8b4d05b
Create Date: 2025-06-09 04:53:58.830485
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '8605e1ff50cd'
down_revision = 'cab9b8b4d05b'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 8cd29b8fb6ec
Revises: 7dbb6d550055
Create Date: 2025-06-05 05:12:50.608338
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '8cd29b8fb6ec'
down_revision = '7dbb6d550055'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 90401e0dbe75
Revises: 8605e1ff50cd
Create Date: 2025-06-09 05:15:46.404716
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '90401e0dbe75'
down_revision = '8605e1ff50cd'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 93b893e47742
Revises: b783b3b43713
Create Date: 2025-06-05 02:37:12.714926
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '93b893e47742'
down_revision = 'b783b3b43713'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 9b93a2dffe81
Revises: c1a4158c8226
Create Date: 2025-06-05 01:29:51.402975
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '9b93a2dffe81'
down_revision = 'c1a4158c8226'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: 9cff183551e1
Revises: 48fee8a8a3be
Create Date: 2025-06-05 00:32:07.995675
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '9cff183551e1'
down_revision = '48fee8a8a3be'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -0,0 +1,28 @@
"""auto
Revision ID: 9d73ac427e40
Revises: b9234524f710
Create Date: 2025-06-09 08:23:23.453209
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '9d73ac427e40'
down_revision = 'b9234524f710'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -0,0 +1,28 @@
"""auto
Revision ID: a10cbbbeb3f6
Revises: 9d73ac427e40
Create Date: 2025-06-09 08:28:08.962286
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'a10cbbbeb3f6'
down_revision = '9d73ac427e40'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -0,0 +1,28 @@
"""auto
Revision ID: a7883990430e
Revises: 7d9fc95edc61
Create Date: 2025-06-09 10:13:51.730708
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'a7883990430e'
down_revision = '7d9fc95edc61'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -0,0 +1,28 @@
"""auto
Revision ID: ab1a71750f4e
Revises: 52e805a0163e
Create Date: 2025-06-09 09:50:46.848952
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'ab1a71750f4e'
down_revision = '52e805a0163e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: ad9ea9d31b58
Revises: 2a0b02a42543
Create Date: 2025-06-05 03:05:30.311725
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'ad9ea9d31b58'
down_revision = '2a0b02a42543'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: af76c66c9075
Revises: 58022b5ab921
Create Date: 2025-06-04 22:44:12.056714
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'af76c66c9075'
down_revision = '58022b5ab921'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -0,0 +1,28 @@
"""auto
Revision ID: b5b29b5b85ae
Revises: 6fcf5e1ad9fa
Create Date: 2025-06-09 10:30:56.308436
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'b5b29b5b85ae'
down_revision = '6fcf5e1ad9fa'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: b783b3b43713
Revises: bfc7a6bd8abc
Create Date: 2025-06-05 02:07:18.572162
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'b783b3b43713'
down_revision = 'bfc7a6bd8abc'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -0,0 +1,28 @@
"""auto
Revision ID: b9234524f710
Revises: 13e8b68e0737
Create Date: 2025-06-09 08:19:19.133720
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'b9234524f710'
down_revision = '13e8b68e0737'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: b9c03e1ae0bf
Revises: 9b93a2dffe81
Create Date: 2025-06-05 01:37:57.483736
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'b9c03e1ae0bf'
down_revision = '9b93a2dffe81'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: bfc7a6bd8abc
Revises: cc35036a6f94
Create Date: 2025-06-05 01:57:23.973531
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'bfc7a6bd8abc'
down_revision = 'cc35036a6f94'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -0,0 +1,28 @@
"""auto
Revision ID: c10353a20277
Revises: e4ece621c461
Create Date: 2025-06-09 09:13:23.016684
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'c10353a20277'
down_revision = 'e4ece621c461'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: c1a4158c8226
Revises: 806e94a40aeb
Create Date: 2025-06-05 01:16:54.451574
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'c1a4158c8226'
down_revision = '806e94a40aeb'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: c9495b058ab0
Revises: 373571dfe134
Create Date: 2025-06-05 09:42:35.228096
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'c9495b058ab0'
down_revision = '373571dfe134'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: cab9b8b4d05b
Revises: 1edc2e2c93cd
Create Date: 2025-06-09 04:42:01.258302
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'cab9b8b4d05b'
down_revision = '1edc2e2c93cd'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -0,0 +1,28 @@
"""auto
Revision ID: cb3ce762cabb
Revises: ab1a71750f4e
Create Date: 2025-06-09 09:57:52.586507
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'cb3ce762cabb'
down_revision = 'ab1a71750f4e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -0,0 +1,28 @@
"""auto
Revision ID: cb57ad0a3231
Revises: 1f5b1e0b6b05
Create Date: 2025-06-09 09:44:00.832472
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'cb57ad0a3231'
down_revision = '1f5b1e0b6b05'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: cc35036a6f94
Revises: b9c03e1ae0bf
Create Date: 2025-06-05 01:45:09.251040
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'cc35036a6f94'
down_revision = 'b9c03e1ae0bf'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: d11c2e8b173a
Revises: 01e2d617ae49
Create Date: 2025-06-09 04:31:08.076159
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'd11c2e8b173a'
down_revision = '01e2d617ae49'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: d8bfe4d4c083
Revises: c9495b058ab0
Create Date: 2025-06-05 09:44:47.740029
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'd8bfe4d4c083'
down_revision = 'c9495b058ab0'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,37 +0,0 @@
"""auto
Revision ID: e0afd892f86e
Revises: 7dd0c2491053
Create Date: 2025-06-09 02:37:06.190352
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'e0afd892f86e'
down_revision = '7dd0c2491053'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('import_batches',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('export_id', sa.String(length=64), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('imported_at', sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('export_id', 'user_id', name='uix_export_user')
)
op.create_index(op.f('ix_import_batches_user_id'), 'import_batches', ['user_id'], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_import_batches_user_id'), table_name='import_batches')
op.drop_table('import_batches')
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: e1cdc5f78f5e
Revises: 2fa6feb17477
Create Date: 2025-06-05 00:57:10.914714
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'e1cdc5f78f5e'
down_revision = '2fa6feb17477'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,70 +0,0 @@
"""auto
Revision ID: e34cff15a95e
Revises: 5c85ebc9451b
Create Date: 2025-06-06 07:34:33.699976
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = 'e34cff15a95e'
down_revision = '5c85ebc9451b'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('featured_images', sa.Column('media_id', sa.Integer(), nullable=False))
op.drop_constraint(op.f('featured_images_ibfk_1'), 'featured_images', type_='foreignkey')
op.create_foreign_key(None, 'featured_images', 'media', ['media_id'], ['id'])
op.drop_column('featured_images', 'submission_image_id')
op.add_column('image_hearts', sa.Column('media_id', sa.Integer(), nullable=False))
op.drop_constraint(op.f('image_hearts_ibfk_1'), 'image_hearts', type_='foreignkey')
op.create_foreign_key(None, 'image_hearts', 'media', ['media_id'], ['id'])
op.drop_column('image_hearts', 'submission_image_id')
op.add_column('media', sa.Column('uploader_id', sa.Integer(), nullable=False))
op.add_column('media', sa.Column('submission_id', sa.Integer(), nullable=True))
op.drop_constraint(op.f('media_ibfk_2'), 'media', type_='foreignkey')
op.drop_constraint(op.f('media_ibfk_3'), 'media', type_='foreignkey')
op.create_foreign_key(None, 'media', 'users', ['uploader_id'], ['id'])
op.create_foreign_key(None, 'media', 'submissions', ['submission_id'], ['id'])
op.drop_column('media', 'update_id')
op.drop_column('media', 'plant_id')
op.add_column('submissions', sa.Column('submission_type', sa.String(length=50), nullable=False))
op.add_column('submissions', sa.Column('price', sa.Float(), nullable=True))
op.add_column('submissions', sa.Column('source', sa.String(length=255), nullable=True))
op.alter_column('submissions', 'plant_name',
existing_type=mysql.VARCHAR(length=100),
nullable=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('submissions', 'plant_name',
existing_type=mysql.VARCHAR(length=100),
nullable=False)
op.drop_column('submissions', 'source')
op.drop_column('submissions', 'price')
op.drop_column('submissions', 'submission_type')
op.add_column('media', sa.Column('plant_id', mysql.INTEGER(), autoincrement=False, nullable=True))
op.add_column('media', sa.Column('update_id', mysql.INTEGER(), autoincrement=False, nullable=True))
op.drop_constraint(None, 'media', type_='foreignkey')
op.drop_constraint(None, 'media', type_='foreignkey')
op.create_foreign_key(op.f('media_ibfk_3'), 'media', 'plant_updates', ['update_id'], ['id'])
op.create_foreign_key(op.f('media_ibfk_2'), 'media', 'plant', ['plant_id'], ['id'])
op.drop_column('media', 'submission_id')
op.drop_column('media', 'uploader_id')
op.add_column('image_hearts', sa.Column('submission_image_id', mysql.INTEGER(), autoincrement=False, nullable=False))
op.drop_constraint(None, 'image_hearts', type_='foreignkey')
op.create_foreign_key(op.f('image_hearts_ibfk_1'), 'image_hearts', 'submission_images', ['submission_image_id'], ['id'])
op.drop_column('image_hearts', 'media_id')
op.add_column('featured_images', sa.Column('submission_image_id', mysql.INTEGER(), autoincrement=False, nullable=False))
op.drop_constraint(None, 'featured_images', type_='foreignkey')
op.create_foreign_key(op.f('featured_images_ibfk_1'), 'featured_images', 'submission_images', ['submission_image_id'], ['id'])
op.drop_column('featured_images', 'media_id')
# ### end Alembic commands ###

View File

@ -0,0 +1,28 @@
"""auto
Revision ID: e4ece621c461
Revises: a10cbbbeb3f6
Create Date: 2025-06-09 08:55:16.262879
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'e4ece621c461'
down_revision = 'a10cbbbeb3f6'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('media', sa.Column('created_at', sa.DateTime(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('media', 'created_at')
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""auto
Revision ID: f4987441cc85
Revises: 447ff559592b
Create Date: 2025-06-06 08:59:55.024371
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'f4987441cc85'
down_revision = '447ff559592b'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,8 +1,8 @@
"""auto
Revision ID: 4d9859ada63b
Revision ID: fa22b011d450
Revises:
Create Date: 2025-06-04 06:16:08.829142
Create Date: 2025-06-09 06:59:45.406606
"""
from alembic import op
@ -10,7 +10,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '4d9859ada63b'
revision = 'fa22b011d450'
down_revision = None
branch_labels = None
depends_on = None
@ -18,18 +18,26 @@ depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('import_batches',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('export_id', sa.String(length=64), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('imported_at', sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('export_id', 'user_id', name='uix_export_user')
)
op.create_index(op.f('ix_import_batches_user_id'), 'import_batches', ['user_id'], unique=False)
op.create_table('plant_common_name',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=255), nullable=False),
sa.Column('name', sa.String(length=128), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name'),
sa.UniqueConstraint('name')
)
op.create_table('tag',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=255), nullable=False),
sa.Column('name', sa.String(length=128), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name'),
sa.UniqueConstraint('name')
)
op.create_table('users',
@ -41,18 +49,39 @@ def upgrade():
sa.Column('excluded_from_analytics', sa.Boolean(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email'),
sa.UniqueConstraint('email')
)
op.create_table('plant_scientific_name',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=255), nullable=False),
sa.Column('name', sa.String(length=256), nullable=False),
sa.Column('common_id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['common_id'], ['plant_common_name.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name'),
sa.UniqueConstraint('name')
)
op.create_table('submissions',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('submitted_at', sa.DateTime(), nullable=True),
sa.Column('plant_name', sa.String(length=100), nullable=True),
sa.Column('scientific_name', sa.String(length=120), nullable=True),
sa.Column('notes', sa.Text(), nullable=True),
sa.Column('submission_type', sa.String(length=50), nullable=False),
sa.Column('price', sa.Float(), nullable=True),
sa.Column('source', sa.String(length=255), nullable=True),
sa.Column('vendor_name', sa.String(length=255), nullable=True),
sa.Column('rating', sa.Integer(), nullable=True),
sa.Column('old_vendor', sa.String(length=255), nullable=True),
sa.Column('new_vendor', sa.String(length=255), nullable=True),
sa.Column('alias_reason', sa.Text(), nullable=True),
sa.Column('approved', sa.Boolean(), nullable=True),
sa.Column('approved_at', sa.DateTime(), nullable=True),
sa.Column('reviewed_by', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['reviewed_by'], ['users.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('plant',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('uuid', sa.String(length=36), nullable=False),
@ -61,20 +90,24 @@ def upgrade():
sa.Column('common_id', sa.Integer(), nullable=False),
sa.Column('scientific_id', sa.Integer(), nullable=False),
sa.Column('plant_type', sa.String(length=50), nullable=False),
sa.Column('status', sa.String(length=50), nullable=False),
sa.Column('notes', sa.Text(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('transferred', sa.Boolean(), nullable=True),
sa.Column('graph_node_id', sa.String(length=255), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('data_verified', sa.Boolean(), nullable=False),
sa.ForeignKeyConstraint(['common_id'], ['plant_common_name.id'], ),
sa.ForeignKeyConstraint(['owner_id'], ['users.id'], ),
sa.ForeignKeyConstraint(['scientific_id'], ['plant_scientific_name.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('custom_slug'),
sa.UniqueConstraint('custom_slug'),
sa.UniqueConstraint('uuid'),
sa.UniqueConstraint('uuid')
)
op.create_table('submission_images',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('submission_id', sa.Integer(), nullable=False),
sa.Column('file_url', sa.String(length=256), nullable=False),
sa.Column('uploaded_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['submission_id'], ['submissions.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('grow_logs',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('plant_id', sa.Integer(), nullable=False),
@ -83,22 +116,13 @@ def upgrade():
sa.ForeignKeyConstraint(['plant_id'], ['plant.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('plant_lineage',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('child_plant_id', sa.Integer(), nullable=False),
sa.Column('parent_plant_id', sa.Integer(), nullable=False),
sa.Column('type', sa.String(length=50), nullable=False),
sa.ForeignKeyConstraint(['child_plant_id'], ['plant.id'], ),
sa.ForeignKeyConstraint(['parent_plant_id'], ['plant.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('plant_ownership_log',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('plant_id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('start_time', sa.DateTime(), nullable=False),
sa.Column('end_time', sa.DateTime(), nullable=True),
sa.Column('transfer_note', sa.Text(), nullable=True),
sa.Column('date_acquired', sa.DateTime(), nullable=True),
sa.Column('transferred', sa.Boolean(), nullable=False),
sa.Column('is_verified', sa.Boolean(), nullable=False),
sa.ForeignKeyConstraint(['plant_id'], ['plant.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
@ -110,24 +134,19 @@ def upgrade():
sa.ForeignKeyConstraint(['tag_id'], ['tag.id'], ),
sa.PrimaryKeyConstraint('plant_id', 'tag_id')
)
op.create_table('submissions',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('plant_id', sa.Integer(), nullable=True),
sa.Column('common_name', sa.String(length=120), nullable=False),
sa.Column('scientific_name', sa.String(length=120), nullable=True),
sa.Column('price', sa.Float(), nullable=False),
sa.Column('source', sa.String(length=120), nullable=True),
sa.Column('timestamp', sa.DateTime(), nullable=True),
sa.Column('height', sa.Float(), nullable=True),
sa.Column('width', sa.Float(), nullable=True),
sa.Column('leaf_count', sa.Integer(), nullable=True),
sa.Column('potting_mix', sa.String(length=255), nullable=True),
sa.Column('container_size', sa.String(length=120), nullable=True),
sa.Column('health_status', sa.String(length=50), nullable=True),
sa.Column('notes', sa.Text(), nullable=True),
op.create_table('transfer_request',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('plant_id', sa.Integer(), nullable=False),
sa.Column('seller_id', sa.Integer(), nullable=False),
sa.Column('buyer_id', sa.Integer(), nullable=False),
sa.Column('status', sa.String(length=20), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('seller_message', sa.String(length=512), nullable=True),
sa.Column('buyer_message', sa.String(length=512), nullable=True),
sa.ForeignKeyConstraint(['buyer_id'], ['users.id'], ),
sa.ForeignKeyConstraint(['plant_id'], ['plant.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.ForeignKeyConstraint(['seller_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('plant_updates',
@ -141,62 +160,58 @@ def upgrade():
sa.ForeignKeyConstraint(['plant_id'], ['plant.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('submission_images',
op.create_table('media',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('submission_id', sa.Integer(), nullable=False),
sa.Column('file_path', sa.String(length=255), nullable=False),
sa.Column('is_visible', sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(['submission_id'], ['submissions.id'], ),
sa.Column('file_url', sa.String(length=256), nullable=False),
sa.Column('uploaded_at', sa.DateTime(), nullable=True),
sa.Column('uploader_id', sa.Integer(), nullable=False),
sa.Column('caption', sa.String(length=255), nullable=True),
sa.Column('plant_id', sa.Integer(), nullable=True),
sa.Column('growlog_id', sa.Integer(), nullable=True),
sa.Column('update_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['growlog_id'], ['grow_logs.id'], ),
sa.ForeignKeyConstraint(['plant_id'], ['plant.id'], ),
sa.ForeignKeyConstraint(['update_id'], ['plant_updates.id'], ),
sa.ForeignKeyConstraint(['uploader_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('featured_images',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('submission_image_id', sa.Integer(), nullable=False),
sa.Column('media_id', sa.Integer(), nullable=False),
sa.Column('override_text', sa.String(length=255), nullable=True),
sa.Column('is_featured', sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(['submission_image_id'], ['submission_images.id'], ),
sa.ForeignKeyConstraint(['media_id'], ['media.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('image_hearts',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('submission_image_id', sa.Integer(), nullable=False),
sa.Column('media_id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['submission_image_id'], ['submission_images.id'], ),
sa.ForeignKeyConstraint(['media_id'], ['media.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('media',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('file_url', sa.String(length=256), nullable=False),
sa.Column('uploaded_at', sa.DateTime(), nullable=True),
sa.Column('plant_id', sa.Integer(), nullable=True),
sa.Column('growlog_id', sa.Integer(), nullable=True),
sa.Column('update_id', sa.Integer(), nullable=True),
sa.Column('caption', sa.String(length=255), nullable=True),
sa.ForeignKeyConstraint(['growlog_id'], ['grow_logs.id'], ),
sa.ForeignKeyConstraint(['plant_id'], ['plant.id'], ),
sa.ForeignKeyConstraint(['update_id'], ['plant_updates.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('media')
op.drop_table('image_hearts')
op.drop_table('featured_images')
op.drop_table('submission_images')
op.drop_table('media')
op.drop_table('plant_updates')
op.drop_table('submissions')
op.drop_table('transfer_request')
op.drop_table('plant_tags')
op.drop_table('plant_ownership_log')
op.drop_table('plant_lineage')
op.drop_table('grow_logs')
op.drop_table('submission_images')
op.drop_table('plant')
op.drop_table('submissions')
op.drop_table('plant_scientific_name')
op.drop_table('users')
op.drop_table('tag')
op.drop_table('plant_common_name')
op.drop_index(op.f('ix_import_batches_user_id'), table_name='import_batches')
op.drop_table('import_batches')
# ### end Alembic commands ###

View File

@ -0,0 +1,38 @@
"""auto
Revision ID: fa3de05c91fb
Revises: cb3ce762cabb
Create Date: 2025-06-09 10:06:16.352992
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'fa3de05c91fb'
down_revision = 'cb3ce762cabb'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('plant', sa.Column('mother_uuid', sa.String(length=36), nullable=True))
op.add_column('plant', sa.Column('notes', sa.Text(), nullable=True))
op.add_column('plant', sa.Column('is_active', sa.Boolean(), nullable=False))
op.add_column('plant', sa.Column('featured_media_id', sa.Integer(), nullable=True))
op.create_foreign_key(None, 'plant', 'media', ['featured_media_id'], ['id'])
op.create_foreign_key(None, 'plant', 'plant', ['mother_uuid'], ['uuid'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'plant', type_='foreignkey')
op.drop_constraint(None, 'plant', type_='foreignkey')
op.drop_column('plant', 'featured_media_id')
op.drop_column('plant', 'is_active')
op.drop_column('plant', 'notes')
op.drop_column('plant', 'mother_uuid')
# ### end Alembic commands ###

View File

@ -1,32 +0,0 @@
"""auto
Revision ID: fad6fe2b5e43
Revises: 6cb1c3054071
Create Date: 2025-06-06 09:24:38.663461
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = 'fad6fe2b5e43'
down_revision = '6cb1c3054071'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('submissions', 'plant_name',
existing_type=mysql.VARCHAR(length=100),
nullable=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('submissions', 'plant_name',
existing_type=mysql.VARCHAR(length=100),
nullable=False)
# ### end Alembic commands ###

View File

@ -1,38 +0,0 @@
"""auto
Revision ID: fb0243eaa7c3
Revises: e34cff15a95e
Create Date: 2025-06-06 07:43:42.387700
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = 'fb0243eaa7c3'
down_revision = 'e34cff15a95e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('submissions', 'plant_name',
existing_type=mysql.VARCHAR(length=100),
nullable=False)
op.drop_column('submissions', 'price')
op.drop_column('submissions', 'source')
op.drop_column('submissions', 'submission_type')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('submissions', sa.Column('submission_type', mysql.VARCHAR(length=50), nullable=False))
op.add_column('submissions', sa.Column('source', mysql.VARCHAR(length=255), nullable=True))
op.add_column('submissions', sa.Column('price', mysql.FLOAT(), nullable=True))
op.alter_column('submissions', 'plant_name',
existing_type=mysql.VARCHAR(length=100),
nullable=True)
# ### end Alembic commands ###

View File

@ -51,12 +51,15 @@
</div>
</nav>
<main class="container">
{% with messages = get_flashed_messages() %}
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="alert alert-warning">
{% for message in messages %}
<div>{{ message }}</div>
{% endfor %}
<div class="container mt-3">
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}

View File

@ -136,7 +136,7 @@ def upload():
mreader = csv.DictReader(mf)
if mreader.fieldnames != MEDIA_HEADERS:
missing = set(MEDIA_HEADERS) - set(mreader.fieldnames or [])
extra = set(reader.fieldnames or []) - set(MEDIA_HEADERS)
extra = set(mreader.fieldnames or []) - set(MEDIA_HEADERS)
os.remove(tmp_zip.name)
flash(f"media.csv header mismatch. Missing: {missing}, Extra: {extra}", "danger")
return redirect(request.url)
@ -213,13 +213,13 @@ def upload():
with open(src, "rb") as sf, open(dst, "wb") as df:
df.write(sf.read())
# 🔧 FIXED: match your Media model exactly
media = Media(
user_id=current_user.id,
plant_id=plant_obj.id,
original_filename=os.path.basename(src),
path=f"uploads/{current_user.id}/{plant_obj.id}/{fname}",
file_url=f"uploads/{current_user.id}/{plant_obj.id}/{fname}",
uploaded_at=datetime.fromisoformat(mrow["Uploaded At"]),
source_type=mrow["Source Type"]
uploader_id=current_user.id,
caption=mrow["Source Type"],
plant_id=plant_obj.id
)
db.session.add(media)
added_media += 1
@ -285,7 +285,7 @@ def upload():
"mother_uuid": mother_uuid
}
review_list.append(item)
session["pending_rows"].append(item)
session["pending_rows"].append(item)
session["review_list"] = review_list
return redirect(url_for("importer.review"))
@ -336,7 +336,7 @@ def review():
)
db.session.add(scientific)
db.session.flush()
all_sci = all_scientific[scientific.name.lower()] = scientific
all_scientific[scientific.name.lower()] = scientific
verified = not suggested or (suggested and accepted)
@ -352,6 +352,7 @@ def review():
)
db.session.add(plant)
db.session.flush()
log = PlantOwnershipLog(
plant_id = plant.id,
user_id = current_user.id,

View File

@ -16,6 +16,7 @@ class Media(db.Model):
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)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
update = db.relationship("PlantUpdate", back_populates="media_items")

View File

@ -1,5 +1,10 @@
# plugins/media/routes.py
import os
from uuid import uuid4
from datetime import datetime
from PIL import Image
from flask import (
Blueprint,
redirect,
@ -10,76 +15,139 @@ from flask import (
current_app,
jsonify
)
from flask_login import current_user, login_required
import os
from flask_login import login_required, current_user
from app import db
from .models import Media, ImageHeart, FeaturedImage
from plugins.plant.models import Plant
bp = Blueprint("media", __name__, template_folder="templates")
bp = Blueprint("media", __name__, url_prefix="/media", template_folder="templates")
# We store only "YYYY/MM/DD/<uuid>.ext" in Media.file_url.
# All files live under "/app/static/uploads/YYYY/MM/DD/<uuid>.ext" in the container.
BASE_UPLOAD_FOLDER = "static/uploads"
ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "gif"}
# -----------------------------------------------------------------------------
# Make generate_image_url available in all templates
# -----------------------------------------------------------------------------
@bp.app_context_processor
def utility_processor():
def generate_image_url(path):
if path:
return url_for("media.media_file", filename=path)
w, h = current_app.config.get("STANDARD_IMG_SIZE", (300, 200))
return f"https://placehold.co/{w}x{h}"
return dict(generate_image_url=generate_image_url)
# -----------------------------------------------------------------------------
# Helpers & config
# -----------------------------------------------------------------------------
def allowed_file(filename):
return (
"." in filename
and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
)
ext = filename.rsplit(".", 1)[-1].lower() if "." in filename else ""
return ext in current_app.config.get("ALLOWED_EXTENSIONS", {"png","jpg","jpeg","gif"})
@bp.route("/media/", methods=["GET"])
def get_upload_path():
base = current_app.config.get("UPLOAD_FOLDER", "static/uploads")
now = datetime.utcnow()
subdir = os.path.join(str(now.year), f"{now.month:02}", f"{now.day:02}")
full = os.path.join(base, subdir)
os.makedirs(full, exist_ok=True)
return full, subdir
# -----------------------------------------------------------------------------
# Routes
# -----------------------------------------------------------------------------
@bp.route("/", methods=["GET"])
def media_index():
"""
/media/ is not used standalone—redirect back to homepage.
"""
return redirect(url_for("core_ui.home"))
@bp.route("/media/files/<path:filename>", methods=["GET"])
@bp.route("/files/<path:filename>", methods=["GET"])
def media_file(filename):
"""
Serve files from "/app/static/uploads/<filename>".
Example: GET /media/files/2025/06/07/abcdef1234abcd.jpg
"""
# Use os.getcwd() to guarantee "/app/static/uploads" (not "/app/app/static/uploads")
full_dir = os.path.join(os.getcwd(), BASE_UPLOAD_FOLDER)
return send_from_directory(full_dir, filename)
# Strip leading "uploads/" if present
if filename.startswith("uploads/"):
filename = filename[len("uploads/"):]
folder = current_app.config.get("UPLOAD_FOLDER", "static/uploads")
return send_from_directory(folder, filename)
@bp.route("/media/heart/<int:media_id>", methods=["POST"])
@bp.route("/heart/<int:media_id>", methods=["POST"])
@login_required
def toggle_heart(media_id):
"""
Add/remove a "heart" from an image.
"""
existing = ImageHeart.query.filter_by(
user_id=current_user.id, media_id=media_id
).first()
existing = ImageHeart.query.filter_by(user_id=current_user.id, media_id=media_id).first()
if existing:
db.session.delete(existing)
db.session.commit()
return jsonify({"status": "unhearted"})
else:
heart = ImageHeart(user_id=current_user.id, media_id=media_id)
db.session.add(heart)
db.session.commit()
return jsonify({"status": "hearted"})
heart = ImageHeart(user_id=current_user.id, media_id=media_id)
db.session.add(heart)
db.session.commit()
return jsonify({"status": "hearted"})
@bp.route("/media/feature/<int:media_id>", methods=["POST"])
@bp.route("/add/<string:plant_uuid>", methods=["POST"])
@login_required
def add_media(plant_uuid):
plant = Plant.query.filter_by(uuid=plant_uuid).first_or_404()
file = request.files.get("file")
if not file or not allowed_file(file.filename):
flash("Invalid or missing file.", "danger")
return redirect(request.referrer or url_for("plant.edit", uuid_val=plant_uuid))
ext = file.filename.rsplit(".", 1)[-1].lower()
filename = f"{uuid4()}.{ext}"
full_path, subdir = get_upload_path()
file.save(os.path.join(full_path, filename))
media = Media(
file_url=os.path.join(subdir, filename).replace("\\", "/"),
uploader_id=current_user.id,
plant_id=plant.id
)
db.session.add(media)
db.session.commit()
flash("Media uploaded successfully.", "success")
return redirect(request.referrer or url_for("plant.edit", uuid_val=plant_uuid))
@bp.route("/feature/<int:media_id>", methods=["POST"])
@login_required
def set_featured_image(media_id):
"""
Toggle featured status on a media item. Only the uploader or an admin may do so.
"""
media = Media.query.get_or_404(media_id)
if (current_user.id != media.uploader_id) and (current_user.role != "admin"):
if current_user.id != media.uploader_id and current_user.role != "admin":
flash("Not authorized to set featured image.", "danger")
return redirect(request.referrer or url_for("core_ui.home"))
# Remove any existing featured entries for this media
FeaturedImage.query.filter_by(media_id=media_id).delete()
featured = FeaturedImage(media_id=media_id, is_featured=True)
db.session.add(featured)
db.session.commit()
flash("Image set as featured.", "success")
return redirect(request.referrer or url_for("core_ui.home"))
return redirect(request.referrer or url_for("plant.edit", uuid_val=media.plant.uuid))
@bp.route("/delete/<int:media_id>", methods=["POST"])
@login_required
def delete_media(media_id):
media = Media.query.get_or_404(media_id)
if current_user.id != media.uploader_id and current_user.role != "admin":
flash("Not authorized to delete this media.", "danger")
return redirect(request.referrer or url_for("core_ui.home"))
full_path = os.path.join(current_app.config.get("UPLOAD_FOLDER", "static/uploads"), media.file_url)
if os.path.exists(full_path):
os.remove(full_path)
db.session.delete(media)
db.session.commit()
flash("Media deleted.", "success")
return redirect(request.referrer or url_for("plant.edit", uuid_val=media.plant.uuid))
@bp.route("/rotate/<int:media_id>", methods=["POST"])
@login_required
def rotate_media(media_id):
media = Media.query.get_or_404(media_id)
if current_user.id != media.uploader_id and current_user.role != "admin":
flash("Not authorized to rotate this media.", "danger")
return redirect(request.referrer or url_for("core_ui.home"))
full_path = os.path.join(current_app.config.get("UPLOAD_FOLDER", "static/uploads"), media.file_url)
try:
with Image.open(full_path) as img:
img.rotate(-90, expand=True).save(full_path)
flash("Image rotated successfully.", "success")
except Exception as e:
flash(f"Failed to rotate image: {e}", "danger")
return redirect(request.referrer or url_for("plant.edit", uuid_val=media.plant.uuid))

View File

@ -1,21 +1,38 @@
# plugins/media/utils.py
import os
import uuid
from datetime import datetime
from PIL import Image
from flask import current_app, url_for
from app import db
from .models import Media
from plugins.plant.models import Plant
def get_upload_path():
"""
Return (full_disk_path, subdir) based on UTC date,
creating directories if needed.
e.g. ('/app/static/uploads/2025/06/09', '2025/06/09')
"""
base = current_app.config.get("UPLOAD_FOLDER", "static/uploads")
now = datetime.utcnow()
subdir = os.path.join(str(now.year), f"{now.month:02}", f"{now.day:02}")
full = os.path.join(base, subdir)
os.makedirs(full, exist_ok=True)
return full, subdir
def generate_random_filename(original_filename):
"""
Returns a random filename preserving the original extension.
e.g. “abcd1234efgh.jpg” for “myphoto.jpg”.
Preserve extension, randomize base name.
"""
ext = os.path.splitext(original_filename)[1].lower() # includes dot, e.g. ".jpg"
random_name = uuid.uuid4().hex # 32char hex string
return f"{random_name}{ext}"
ext = os.path.splitext(original_filename)[1].lower()
return f"{uuid.uuid4().hex}{ext}"
def strip_metadata_and_save(source_file, destination_path):
"""
Opens an image with Pillow, strips EXIF (metadata), and saves it cleanly.
Opens an image with Pillow, strips EXIF metadata, and saves it.
Supports common formats (JPEG, PNG).
"""
with Image.open(source_file) as img:
@ -23,3 +40,72 @@ def strip_metadata_and_save(source_file, destination_path):
clean_image = Image.new(img.mode, img.size)
clean_image.putdata(data)
clean_image.save(destination_path)
def generate_image_url(path):
"""
If path is set, route through /media/files/<path>; otherwise
return a placehold.co URL sized to STANDARD_IMG_SIZE.
"""
if path:
return url_for("media.media_file", filename=path)
w, h = current_app.config.get("STANDARD_IMG_SIZE", (300, 200))
return f"https://placehold.co/{w}x{h}"
def save_media_file(file_storage, uploader_id, related_model=None, related_uuid=None):
"""
- file_storage: Werkzeug FileStorage
- uploader_id: current_user.id
- related_model: e.g. 'plant'
- related_uuid: the Plant.uuid string
Returns the new Media instance.
"""
full_path, subdir = get_upload_path()
filename = generate_random_filename(file_storage.filename)
disk_path = os.path.join(full_path, filename)
file_storage.save(disk_path)
media = Media(
file_url=os.path.join(subdir, filename).replace("\\", "/"),
uploader_id=uploader_id
)
# Associate to plant if requested
if related_model == "plant" and related_uuid:
plant = Plant.query.filter_by(uuid=related_uuid).first()
if plant:
media.plant_id = plant.id
db.session.add(media)
db.session.commit()
return media
def delete_media_file(media):
"""
Remove file from disk and delete DB record.
"""
base = current_app.config.get("UPLOAD_FOLDER", "static/uploads")
path = os.path.join(base, media.file_url)
try:
os.remove(path)
except OSError:
pass
db.session.delete(media)
db.session.commit()
def rotate_media_file(media, angle=-90):
"""
Rotate the file on disk (in place) and leave DB record intact.
"""
base = current_app.config.get("UPLOAD_FOLDER", "static/uploads")
path = os.path.join(base, media.file_url)
try:
with Image.open(path) as img:
rotated = img.rotate(angle, expand=True)
rotated.save(path)
except Exception:
pass
# no DB changes needed

View File

@ -1,10 +1,20 @@
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, BooleanField, SubmitField
from wtforms.validators import DataRequired
from wtforms import StringField, TextAreaField, BooleanField, SubmitField, SelectField
from wtforms.validators import Optional, DataRequired
class PlantForm(FlaskForm):
name = StringField('Name', validators=[DataRequired()])
type = StringField('Type')
notes = TextAreaField('Notes')
common_name = SelectField('Common Name', validators=[Optional()], coerce=int)
scientific_name = SelectField('Scientific Name', validators=[Optional()], coerce=int)
mother_uuid = SelectField('Mother UUID', validators=[Optional()], coerce=str)
plant_type = SelectField('Plant Type', validators=[DataRequired()], choices=[
('cutting', 'Cutting'),
('tissue_culture', 'Tissue Culture'),
('plant', 'Plant'),
('seed', 'Seed'),
('division', 'Division'),
])
custom_slug = StringField('Custom Slug', validators=[Optional()])
notes = TextAreaField('Notes', validators=[Optional()])
data_verified = BooleanField('Data Verified', default=False)
is_active = BooleanField('Active', default=True)
submit = SubmitField('Save')

View File

@ -3,8 +3,7 @@
from datetime import datetime
import uuid as uuid_lib
from app import db
# from plugins.auth.models import User
from plugins.media.models import Media # import Media so we can refer to Media.plant_id
# Association table for Plant ↔ Tag (unchanged)
plant_tags = db.Table(
@ -21,7 +20,7 @@ class Tag(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(128), unique=True, nullable=False)
# … any other columns you had …
class PlantCommonName(db.Model):
__tablename__ = 'plant_common_name'
@ -38,6 +37,7 @@ class PlantCommonName(db.Model):
cascade='all, delete-orphan'
)
class PlantScientificName(db.Model):
__tablename__ = 'plant_scientific_name'
__table_args__ = {'extend_existing': True}
@ -47,22 +47,17 @@ class PlantScientificName(db.Model):
common_id = db.Column(db.Integer, db.ForeignKey('plant_common_name.id'), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# We removed the “plants” relationship from here to avoid backref conflicts.
# If you need it, you can still do Plant.query.filter_by(scientific_id=<this id>).
class PlantOwnershipLog(db.Model):
__tablename__ = 'plant_ownership_log'
__table_args__ = {'extend_existing': True}
id = db.Column(db.Integer, primary_key=True)
plant_id = db.Column(db.Integer, db.ForeignKey('plant.id'), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
date_acquired = db.Column(db.DateTime, default=datetime.utcnow)
transferred = db.Column(db.Boolean, default=False, nullable=False)
is_verified = db.Column(db.Boolean, default=False, nullable=False)
# Optional: if you ever want to store a pointer to the Neo4j node, you can re-add:
# graph_node_id = db.Column(db.String(255), nullable=True)
id = db.Column(db.Integer, primary_key=True)
plant_id = db.Column(db.Integer, db.ForeignKey('plant.id'), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
date_acquired = db.Column(db.DateTime, default=datetime.utcnow)
transferred = db.Column(db.Boolean, default=False, nullable=False)
is_verified = db.Column(db.Boolean, default=False, nullable=False)
user = db.relationship(
'plugins.auth.models.User',
@ -70,56 +65,73 @@ class PlantOwnershipLog(db.Model):
lazy=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)
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)
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)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, onupdate=datetime.utcnow)
mother_uuid = db.Column(db.String(36), db.ForeignKey('plant.uuid'), nullable=True)
# ─── NEW: Flag that indicates whether the common/scientific name pair was human-verified ─────────────────
data_verified = db.Column(db.Boolean, default=False, nullable=False)
plant_type = db.Column(db.String(50), nullable=False)
notes = db.Column(db.Text, nullable=True)
is_active = db.Column(db.Boolean, default=True, nullable=False)
featured_media_id = db.Column(db.Integer, db.ForeignKey('media.id'), nullable=True)
# Relationships
updates = db.relationship(
'plugins.growlog.models.PlantUpdate',
backref='plant',
lazy=True,
cascade='all, delete-orphan'
)
tags = db.relationship(
'plugins.plant.models.Tag',
secondary=plant_tags,
backref='plants',
lazy='dynamic'
)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, onupdate=datetime.utcnow)
data_verified = db.Column(db.Boolean, default=False, nullable=False)
common_name = db.relationship(
'plugins.plant.models.PlantCommonName',
backref=db.backref('plants', lazy='dynamic'),
lazy=True
)
scientific_name = db.relationship(
'plugins.plant.models.PlantScientificName',
backref=db.backref('plants', lazy='dynamic'),
lazy=True
)
# ─── FIXED: explicitly join on Media.plant_id ──────────────────────────────
media = db.relationship(
Media,
backref='plant',
lazy=True,
cascade='all, delete-orphan',
foreign_keys=[Media.plant_id]
)
ownership_logs = db.relationship(
'plugins.plant.models.PlantOwnershipLog',
backref='plant',
lazy=True,
cascade='all, delete-orphan'
)
featured_media = db.relationship(
Media,
foreign_keys=[featured_media_id],
uselist=False
)
updates = db.relationship(
'plugins.growlog.models.PlantUpdate',
backref='plant',
lazy=True,
cascade='all, delete-orphan'
)
tags = db.relationship(
Tag,
secondary=plant_tags,
backref='plants',
lazy='dynamic'
)
common_name = db.relationship(
PlantCommonName,
backref=db.backref('plants', lazy='dynamic'),
lazy=True
)
scientific_name = db.relationship(
PlantScientificName,
backref=db.backref('plants', lazy='dynamic'),
lazy=True
)
ownership_logs = db.relationship(
PlantOwnershipLog,
backref='plant',
lazy=True,
cascade='all, delete-orphan'
)
def __repr__(self):
return f"<Plant {self.uuid} ({self.plant_type})>"

View File

@ -1,43 +1,205 @@
from flask import Blueprint, render_template, redirect, url_for, request, flash
from uuid import uuid4
from flask import (
Blueprint,
render_template,
redirect,
url_for,
request,
flash,
)
from flask_login import login_required, current_user
from app import db
from .models import Plant
from .models import Plant, PlantCommonName, PlantScientificName
from .forms import PlantForm
from plugins.media.models import Media
from plugins.media.utils import save_media_file, delete_media_file, rotate_media_file, generate_image_url
bp = Blueprint('plant', __name__, template_folder='templates')
bp = Blueprint(
'plant',
__name__,
url_prefix='/plants',
template_folder='templates'
)
@bp.route('/plants/')
# -----------------------------------------------------------------------------
# Make generate_image_url available in all templates
# -----------------------------------------------------------------------------
@bp.app_context_processor
def inject_image_helper():
return dict(generate_image_url=generate_image_url)
# ─── LIST ─────────────────────────────────────────────────────────────────────
@bp.route('/', methods=['GET'])
@login_required
def index():
plants = Plant.query.order_by(Plant.created_at.desc()).all()
return render_template('plant/index.html', plants=plants)
plants = (
Plant.query
.filter_by(owner_id=current_user.id)
.order_by(Plant.created_at.desc())
.all()
)
stats = {
'user_plants': Plant.query.filter_by(owner_id=current_user.id).count(),
'user_images': Media.query.filter_by(uploader_id=current_user.id).count(),
'total_plants': Plant.query.count(),
'total_images': Media.query.count(),
}
return render_template('plant/index.html', plants=plants, stats=stats)
@bp.route('/plants/<int:plant_id>')
def detail(plant_id):
plant = Plant.query.get_or_404(plant_id)
return render_template('plant/detail.html', plant=plant)
@bp.route('/plants/new', methods=['GET', 'POST'])
# ─── CREATE ───────────────────────────────────────────────────────────────────
@bp.route('/create', methods=['GET', 'POST'])
@login_required
def create():
form = PlantForm()
if form.validate_on_submit():
plant = Plant(
name=form.name.data,
type=form.type.data,
notes=form.notes.data,
is_active=form.is_active.data
)
db.session.add(plant)
db.session.commit()
flash('Plant created successfully.', 'success')
return redirect(url_for('plant.index'))
return render_template('plant/form.html', form=form)
@bp.route('/plants/<int:plant_id>/edit', methods=['GET', 'POST'])
def edit(plant_id):
plant = Plant.query.get_or_404(plant_id)
form = PlantForm(obj=plant)
# ─── dropdown choices ───────────────────────────────────────────────────────
form.plant_type.choices = [
('plant', 'Plant'),
('cutting', 'Cutting'),
('seed', 'Seed'),
('tissue_culture', 'Tissue Culture'),
('division', 'Division'),
]
form.common_name.choices = [
(c.id, c.name)
for c in PlantCommonName.query.order_by(PlantCommonName.name)
]
form.scientific_name.choices = [
(s.id, s.name)
for s in PlantScientificName.query.order_by(PlantScientificName.name)
]
form.mother_uuid.choices = [('N/A', 'None')] + [
(p.uuid, f"{p.common_name.name if p.common_name else 'Unnamed'} {p.uuid}")
for p in Plant.query.order_by(Plant.created_at.desc()).all()
]
if form.validate_on_submit():
form.populate_obj(plant)
new_plant = Plant(
uuid=str(uuid4()),
owner_id=current_user.id,
plant_type=form.plant_type.data,
common_id=form.common_name.data,
scientific_id=form.scientific_name.data,
mother_uuid=(
form.mother_uuid.data
if form.mother_uuid.data != 'N/A'
else None
),
custom_slug=form.custom_slug.data,
notes=form.notes.data,
data_verified=form.data_verified.data,
is_active=form.is_active.data,
)
db.session.add(new_plant)
db.session.commit()
flash('New plant created successfully.', 'success')
return redirect(url_for('plant.edit', uuid_val=new_plant.uuid))
return render_template('plant/create.html', form=form)
# ─── DETAIL ───────────────────────────────────────────────────────────────────
@bp.route('/<uuid:uuid_val>', methods=['GET'])
@login_required
def detail(uuid_val):
plant = Plant.query.filter_by(uuid=str(uuid_val)).first_or_404()
return render_template('plant/detail.html', plant=plant)
# ─── EDIT ─────────────────────────────────────────────────────────────────────
@bp.route('/<uuid:uuid_val>/edit', methods=['GET', 'POST'])
@login_required
def edit(uuid_val):
plant = Plant.query.filter_by(uuid=str(uuid_val)).first_or_404()
form = PlantForm()
form.plant_type.choices = [
('plant', 'Plant'),
('cutting', 'Cutting'),
('seed', 'Seed'),
('tissue_culture', 'Tissue Culture'),
('division', 'Division'),
]
form.common_name.choices = [
(c.id, c.name)
for c in PlantCommonName.query.order_by(PlantCommonName.name)
]
form.scientific_name.choices = [
(s.id, s.name)
for s in PlantScientificName.query.order_by(PlantScientificName.name)
]
form.mother_uuid.choices = [('N/A', 'None')] + [
(p.uuid, f"{p.common_name.name if p.common_name else 'Unnamed'} {p.uuid}")
for p in Plant.query.filter(Plant.uuid != plant.uuid).all()
]
if request.method == 'GET':
form.plant_type.data = plant.plant_type
form.common_name.data = plant.common_id
form.scientific_name.data = plant.scientific_id
form.mother_uuid.data = plant.mother_uuid or 'N/A'
form.custom_slug.data = plant.custom_slug
form.notes.data = plant.notes
form.data_verified.data = plant.data_verified
form.is_active.data = getattr(plant, 'is_active', True)
if form.validate_on_submit():
plant.plant_type = form.plant_type.data
plant.common_id = form.common_name.data
plant.scientific_id = form.scientific_name.data
plant.mother_uuid = (
form.mother_uuid.data
if form.mother_uuid.data != 'N/A'
else None
)
plant.custom_slug = form.custom_slug.data
plant.notes = form.notes.data
plant.data_verified = form.data_verified.data
plant.is_active = form.is_active.data
db.session.commit()
flash('Plant updated successfully.', 'success')
return redirect(url_for('plant.detail', plant_id=plant.id))
return render_template('plant/form.html', form=form, plant=plant)
return redirect(url_for('plant.detail', uuid_val=plant.uuid))
return render_template('plant/edit.html', form=form, plant=plant)
# ─── IMAGE ROUTES ────────────────────────────────────────────────────────────
@bp.route('/<uuid:uuid_val>/upload', methods=['POST'])
@login_required
def upload_image(uuid_val):
plant = Plant.query.filter_by(uuid=uuid_val).first_or_404()
file = request.files.get('file')
if file and file.filename:
save_media_file(
file,
current_user.id,
related_model='plant',
related_uuid=str(plant.uuid)
)
flash('Image uploaded successfully.', 'success')
return redirect(url_for('plant.edit', uuid_val=plant.uuid))
@bp.route('/<uuid:uuid_val>/feature/<int:media_id>', methods=['POST'])
@login_required
def set_featured_image(uuid_val, media_id):
plant = Plant.query.filter_by(uuid=uuid_val).first_or_404()
media = Media.query.get_or_404(media_id)
plant.featured_media_id = media.id
db.session.commit()
flash('Featured image set.', 'success')
return redirect(url_for('plant.edit', uuid_val=plant.uuid))
@bp.route('/<uuid:uuid_val>/delete/<int:media_id>', methods=['POST'])
@login_required
def delete_image(uuid_val, media_id):
plant = Plant.query.filter_by(uuid=uuid_val).first_or_404()
media = Media.query.get_or_404(media_id)
delete_media_file(media)
flash('Image deleted.', 'success')
return redirect(url_for('plant.edit', uuid_val=plant.uuid))
@bp.route('/<uuid:uuid_val>/rotate/<int:media_id>', methods=['POST'])
@login_required
def rotate_image(uuid_val, media_id):
plant = Plant.query.filter_by(uuid=uuid_val).first_or_404()
media = Media.query.get_or_404(media_id)
rotate_media_file(media)
flash('Image rotated.', 'success')
return redirect(url_for('plant.edit', uuid_val=plant.uuid))

View File

@ -0,0 +1,53 @@
{% extends 'core_ui/base.html' %}
{% block title %}Add New Plant Nature In Pots{% endblock %}
{% block content %}
<div class="container mt-4">
<h2>Create New Plant</h2>
<form method="POST">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.plant_type.label(class="form-label") }}
{{ form.plant_type(class="form-select") }}
</div>
<div class="mb-3">
{{ form.common_name.label(class="form-label") }}
{{ form.common_name(class="form-select") }}
</div>
<div class="mb-3">
{{ form.scientific_name.label(class="form-label") }}
{{ form.scientific_name(class="form-select") }}
</div>
<div class="mb-3">
{{ form.mother_uuid.label(class="form-label") }}
{{ form.mother_uuid(class="form-select") }}
</div>
<div class="mb-3">
{{ form.custom_slug.label(class="form-label") }}
{{ form.custom_slug(class="form-control") }}
</div>
<div class="mb-3">
{{ form.notes.label(class="form-label") }}
{{ form.notes(class="form-control", rows=4) }}
</div>
<div class="form-check mb-3">
{{ form.data_verified(class="form-check-input") }}
{{ form.data_verified.label(class="form-check-label") }}
</div>
<div class="form-check mb-3">
{{ form.is_active(class="form-check-input") }}
{{ form.is_active.label(class="form-check-label") }}
</div>
<button type="submit" class="btn btn-success">Create Plant</button>
</form>
</div>
{% endblock %}

View File

@ -1,37 +1,73 @@
{# plugins/plant/templates/plant/detail.html #}
{% extends 'core_ui/base.html' %}
{% block title %}{{ plant.common_name.name }} Nature In Pots{% endblock %}
{% block title %}
{{ plant.common_name.name if plant.common_name else "Unnamed Plant" }} Nature In Pots
{% endblock %}
{% block content %}
<div class="container my-4">
<div class="row">
<div class="row gx-4">
<div class="col-md-4">
<img src="https://placehold.co/300x300"
class="img-fluid rounded mb-3"
alt="{{ plant.common_name.name }}">
{# determine featured or fallback to first media item #}
{% set featured = plant.featured_media or plant.media|first %}
<img
src="{{ generate_image_url(featured.file_url if featured else None) }}"
alt="Image of {{ plant.common_name.name if plant.common_name else 'Plant' }}"
class="img-fluid rounded shadow-sm"
style="object-fit: cover; width: 100%; height: auto;"
>
</div>
<div class="col-md-8">
<h1>{{ plant.common_name.name }}</h1>
<h2>
{{ plant.common_name.name if plant.common_name else "Unnamed Plant" }}
</h2>
{% if plant.scientific_name %}
<p class="text-muted"><em>{{ plant.scientific_name.name }}</em></p>
<h5 class="text-muted">
{{ plant.scientific_name.name }}
</h5>
{% endif %}
<dl class="row">
<dt class="col-sm-3">Date Added</dt>
<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">
{{ 'Dead' if plant.is_dead else 'Active' }}
</dd>
</dl>
<p class="mt-3">
{{ plant.notes or "No description provided." }}
</p>
<a href="{{ url_for('plant.index') }}" class="btn btn-secondary">
← Back to list
</a>
<a href="{{ url_for('plant.edit', plant_id=plant.id) }}"
class="btn btn-primary">
Edit
</a>
{% if plant.mother_uuid %}
<p class="text-muted">
Parent:
<a href="{{ url_for('plant.detail', uuid_val=plant.mother_uuid) }}">
{{ plant.mother_uuid }}
</a>
</p>
{% endif %}
<div class="mt-4">
<a
href="{{ url_for('plant.edit', uuid_val=plant.uuid) }}"
class="btn btn-primary me-2"
>Edit</a>
<a
href="{{ url_for('plant.index') }}"
class="btn btn-secondary"
>Back to List</a>
</div>
</div>
</div>
{% if plant.media|length > (1 if plant.featured_media else 0) %}
<hr class="my-4">
<h4>Additional Images</h4>
<div class="d-flex flex-wrap gap-3">
{% for img in plant.media if img != featured %}
<img
src="{{ generate_image_url(img.file_url) }}"
alt="Plant image"
class="img-thumbnail"
style="height: 160px; object-fit: cover;"
>
{% endfor %}
</div>
{% endif %}
</div>
{% endblock %}

View File

@ -0,0 +1,127 @@
{% extends 'core_ui/base.html' %}
{% block title %}Edit Plant Nature In Pots{% endblock %}
{% block content %}
<div class="container mt-4">
<h2>Edit Plant</h2>
{# ─── Main plantdata form ──────────────────────────────────────────── #}
<form method="POST" enctype="multipart/form-data">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.plant_type.label(class="form-label") }}
{{ form.plant_type(class="form-select") }}
</div>
<div class="mb-3">
{{ form.common_name.label(class="form-label") }}
{{ form.common_name(class="form-select") }}
</div>
<div class="mb-3">
{{ form.scientific_name.label(class="form-label") }}
{{ form.scientific_name(class="form-select") }}
</div>
<div class="mb-3">
{{ form.mother_uuid.label(class="form-label") }}
{{ form.mother_uuid(class="form-select") }}
</div>
<div class="mb-3">
{{ form.custom_slug.label(class="form-label") }}
{{ form.custom_slug(class="form-control") }}
</div>
<div class="mb-3">
{{ form.notes.label(class="form-label") }}
{{ form.notes(class="form-control", rows=4) }}
</div>
<div class="form-check mb-3">
{{ form.data_verified(class="form-check-input") }}
{{ form.data_verified.label(class="form-check-label") }}
</div>
<div class="form-check mb-3">
{{ form.is_active(class="form-check-input") }}
{{ form.is_active.label(class="form-check-label") }}
</div>
<button type="submit" class="btn btn-primary">Save Changes</button>
</form>
<hr>
{# ─── Upload new image ─────────────────────────────────────────────── #}
<h4>Upload Image</h4>
<form
method="POST"
action="{{ url_for('plant.upload_image', uuid_val=plant.uuid) }}"
enctype="multipart/form-data"
class="mb-4"
>
<input
type="hidden"
name="csrf_token"
value="{{ csrf_token() }}"
>
<div class="input-group">
<input type="file" name="file" class="form-control" required>
<button class="btn btn-secondary" type="submit">Upload</button>
</div>
</form>
{# ─── Existing images ──────────────────────────────────────────────── #}
<h4>Existing Images</h4>
<div class="row">
{% for media in plant.media %}
<div class="col-md-3 mb-4">
<div class="card h-100">
<img
src="{{ generate_image_url(media.file_url) }}"
class="card-img-top img-fluid"
alt="Plant Image"
style="object-fit:cover; height:150px;"
>
<div class="card-body text-center">
{% if plant.featured_media_id == media.id %}
<span class="badge bg-success mb-2">Featured</span>
{% else %}
<form
method="POST"
action="{{ url_for('plant.set_featured_image', uuid_val=plant.uuid, media_id=media.id) }}"
class="mb-2"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-outline-primary btn-sm">Set Featured</button>
</form>
{% endif %}
<div class="d-grid gap-1">
<form
method="POST"
action="{{ url_for('plant.rotate_image', uuid_val=plant.uuid, media_id=media.id) }}"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-outline-secondary btn-sm">Rotate</button>
</form>
<form
method="POST"
action="{{ url_for('plant.delete_image', uuid_val=plant.uuid, media_id=media.id) }}"
onsubmit="return confirm('Delete this image?');"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-outline-danger btn-sm">Delete</button>
</form>
</div>
</div>
</div>
</div>
{% else %}
<p class="text-muted">No images uploaded yet.</p>
{% endfor %}
</div>
</div>
{% endblock %}

View File

@ -1,12 +0,0 @@
{% extends 'core_ui/base.html' %}
{% block content %}
<h1>{% if plant %}Edit{% else %}New{% endif %} Plant</h1>
<form method="POST">
{{ form.hidden_tag() }}
<p>{{ form.name.label }}<br>{{ form.name(size=40) }}</p>
<p>{{ form.type.label }}<br>{{ form.type(size=40) }}</p>
<p>{{ form.notes.label }}<br>{{ form.notes(rows=5, cols=40) }}</p>
<p>{{ form.is_active() }} {{ form.is_active.label }}</p>
<p>{{ form.submit() }}</p>
</form>
{% endblock %}

Some files were not shown because too many files have changed in this diff Show More