lots of things
This commit is contained in:
@ -5,6 +5,7 @@ services:
|
|||||||
- "5000:5000"
|
- "5000:5000"
|
||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
- .:/app
|
||||||
|
- ./static/uploads:/app/static/uploads
|
||||||
environment:
|
environment:
|
||||||
- FLASK_APP=app
|
- FLASK_APP=app
|
||||||
- FLASK_ENV=development
|
- FLASK_ENV=development
|
||||||
@ -24,7 +25,6 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
start_period: 30s
|
start_period: 30s
|
||||||
|
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: mysql:8
|
image: mysql:8
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@ -43,7 +43,6 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
|
|
||||||
adminer:
|
adminer:
|
||||||
image: adminer
|
image: adminer
|
||||||
restart: always
|
restart: always
|
||||||
|
28
migrations/versions/39f714eda2bf_auto.py
Normal file
28
migrations/versions/39f714eda2bf_auto.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
"""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 ###
|
28
migrations/versions/408c432b5835_auto.py
Normal file
28
migrations/versions/408c432b5835_auto.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
"""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 ###
|
36
migrations/versions/447ff559592b_auto.py
Normal file
36
migrations/versions/447ff559592b_auto.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
"""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 ###
|
28
migrations/versions/6420e024f896_auto.py
Normal file
28
migrations/versions/6420e024f896_auto.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
"""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 ###
|
44
migrations/versions/64c1927562cc_auto.py
Normal file
44
migrations/versions/64c1927562cc_auto.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
"""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 ###
|
28
migrations/versions/6539ef5f5419_auto.py
Normal file
28
migrations/versions/6539ef5f5419_auto.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
"""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 ###
|
28
migrations/versions/6cb1c3054071_auto.py
Normal file
28
migrations/versions/6cb1c3054071_auto.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
"""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 ###
|
38
migrations/versions/77087ff2442e_auto.py
Normal file
38
migrations/versions/77087ff2442e_auto.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
"""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 ###
|
28
migrations/versions/7d232205181b_auto.py
Normal file
28
migrations/versions/7d232205181b_auto.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
"""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 ###
|
70
migrations/versions/e34cff15a95e_auto.py
Normal file
70
migrations/versions/e34cff15a95e_auto.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
"""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 ###
|
28
migrations/versions/f4987441cc85_auto.py
Normal file
28
migrations/versions/f4987441cc85_auto.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
"""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 ###
|
32
migrations/versions/fad6fe2b5e43_auto.py
Normal file
32
migrations/versions/fad6fe2b5e43_auto.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
"""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 ###
|
38
migrations/versions/fb0243eaa7c3_auto.py
Normal file
38
migrations/versions/fb0243eaa7c3_auto.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
"""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 ###
|
@ -1,14 +1,34 @@
|
|||||||
{% macro render_media_list(media_list, thumb_width=150) -%}
|
{% macro render_media_list(media_list, thumb_width=150, current_user=None) -%}
|
||||||
{% if media_list %}
|
{% if media_list %}
|
||||||
<ul class="media-thumbnails">
|
<div class="row">
|
||||||
{% for media in media_list %}
|
{% for media in media_list %}
|
||||||
<li>
|
<div class="col-md-3 mb-4" data-media-id="{{ media.id }}">
|
||||||
<img src="{{ url_for('media.media_file', filename=media.file_url) }}" width="{{ thumb_width }}">
|
<div class="card shadow-sm">
|
||||||
|
<img src="{{ url_for('media.media_file', filename=media.file_url.split('/')[-1]) }}"
|
||||||
|
class="card-img-top" style="width:100%; height:auto;">
|
||||||
{% if media.caption %}
|
{% if media.caption %}
|
||||||
<p>{{ media.caption }}</p>
|
<div class="card-body p-2">
|
||||||
|
<p class="card-text text-center">{{ media.caption }}</p>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</li>
|
<div class="card-footer d-flex justify-content-between align-items-center p-2">
|
||||||
|
<button class="btn btn-sm btn-outline-danger heart-btn" data-id="{{ media.id }}">
|
||||||
|
❤️ <span class="heart-count">{{ media.imageheart_set|length }}</span>
|
||||||
|
</button>
|
||||||
|
{% if current_user and (current_user.id == media.uploader_id or current_user.role == 'admin') %}
|
||||||
|
<form method="POST" action="{{ url_for('media.set_featured_image', media_id=media.id) }}">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
{% if media.featuredimage %}
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" type="submit">★ Featured</button>
|
||||||
|
{% else %}
|
||||||
|
<button class="btn btn-sm btn-outline-primary" type="submit">☆ Set Featured</button>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
@ -25,5 +25,6 @@ class PlantUpdate(db.Model):
|
|||||||
update_type = db.Column(db.String(50), nullable=False)
|
update_type = db.Column(db.String(50), nullable=False)
|
||||||
description = db.Column(db.Text, nullable=True)
|
description = db.Column(db.Text, nullable=True)
|
||||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
media_items = db.relationship('Media', back_populates='update', lazy=True)
|
|
||||||
|
|
||||||
|
# Link to Media.update_id (must match the FK defined there)
|
||||||
|
media_items = db.relationship('Media', back_populates='update', lazy=True)
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
from flask_wtf import FlaskForm
|
|
||||||
from wtforms import StringField, SelectField, SubmitField, IntegerField
|
|
||||||
from flask_wtf.file import FileField, FileAllowed, FileRequired
|
|
||||||
from wtforms.validators import DataRequired
|
|
||||||
|
|
||||||
class MediaUploadForm(FlaskForm):
|
|
||||||
image = FileField('Image', validators=[
|
|
||||||
FileRequired(),
|
|
||||||
FileAllowed(['jpg', 'jpeg', 'png', 'gif'], 'Images only!')
|
|
||||||
])
|
|
||||||
caption = StringField('Caption')
|
|
||||||
plant_id = IntegerField('Plant ID')
|
|
||||||
growlog_id = IntegerField('GrowLog ID')
|
|
||||||
submit = SubmitField('Upload')
|
|
@ -1,40 +1,42 @@
|
|||||||
|
# plugins/media/models.py
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from plugins.plant.models import Plant
|
|
||||||
|
|
||||||
class Media(db.Model):
|
class Media(db.Model):
|
||||||
__tablename__ = 'media'
|
__tablename__ = "media"
|
||||||
__table_args__ = {'extend_existing': True}
|
__table_args__ = {"extend_existing": True}
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
file_url = db.Column(db.String(256), nullable=False)
|
file_url = db.Column(db.String(256), nullable=False)
|
||||||
uploaded_at = db.Column(db.DateTime, default=datetime.utcnow)
|
uploaded_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
uploader_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
|
||||||
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)
|
|
||||||
|
|
||||||
caption = db.Column(db.String(255), nullable=True)
|
caption = db.Column(db.String(255), nullable=True)
|
||||||
|
|
||||||
# Relationship to PlantUpdate
|
plant_id = db.Column(db.Integer, db.ForeignKey("plant.id"), nullable=True)
|
||||||
update = db.relationship('PlantUpdate', back_populates='media_items', lazy=True)
|
growlog_id = db.Column(db.Integer, db.ForeignKey("grow_logs.id"), nullable=True)
|
||||||
|
update_id = db.Column(db.Integer, db.ForeignKey("plant_updates.id"), nullable=True)
|
||||||
|
|
||||||
|
update = db.relationship("PlantUpdate", back_populates="media_items")
|
||||||
|
|
||||||
class ImageHeart(db.Model):
|
class ImageHeart(db.Model):
|
||||||
__tablename__ = 'image_hearts'
|
__tablename__ = "image_hearts"
|
||||||
__table_args__ = {'extend_existing': True}
|
__table_args__ = {"extend_existing": True}
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
|
||||||
submission_image_id = db.Column(db.Integer, db.ForeignKey('submission_images.id'), nullable=False)
|
media_id = db.Column(db.Integer, db.ForeignKey("media.id"), nullable=False)
|
||||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
media = db.relationship("Media", backref="hearts")
|
||||||
|
|
||||||
class FeaturedImage(db.Model):
|
class FeaturedImage(db.Model):
|
||||||
__tablename__ = 'featured_images'
|
__tablename__ = "featured_images"
|
||||||
__table_args__ = {'extend_existing': True}
|
__table_args__ = {"extend_existing": True}
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
submission_image_id = db.Column(db.Integer, db.ForeignKey('submission_images.id'), nullable=False)
|
media_id = db.Column(db.Integer, db.ForeignKey("media.id"), nullable=False)
|
||||||
override_text = db.Column(db.String(255), nullable=True)
|
override_text = db.Column(db.String(255), nullable=True)
|
||||||
is_featured = db.Column(db.Boolean, default=True)
|
is_featured = db.Column(db.Boolean, default=True)
|
||||||
|
|
||||||
|
media = db.relationship("Media", backref="featured_entries")
|
||||||
|
@ -1,77 +1,84 @@
|
|||||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify, send_from_directory, current_app
|
# plugins/media/routes.py
|
||||||
|
|
||||||
|
from flask import (
|
||||||
|
Blueprint,
|
||||||
|
redirect,
|
||||||
|
url_for,
|
||||||
|
request,
|
||||||
|
flash,
|
||||||
|
send_from_directory,
|
||||||
|
current_app,
|
||||||
|
jsonify
|
||||||
|
)
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
from werkzeug.utils import secure_filename
|
|
||||||
import os
|
import os
|
||||||
from app import db
|
from app import db
|
||||||
from .models import Media, ImageHeart, FeaturedImage
|
from .models import Media, ImageHeart, FeaturedImage
|
||||||
from plugins.plant.models import Plant
|
|
||||||
|
|
||||||
bp = Blueprint("media", __name__, template_folder="templates")
|
bp = Blueprint("media", __name__, template_folder="templates")
|
||||||
|
|
||||||
UPLOAD_FOLDER = "static/uploads"
|
# 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"}
|
ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "gif"}
|
||||||
|
|
||||||
def allowed_file(filename):
|
def allowed_file(filename):
|
||||||
return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
|
return (
|
||||||
|
"." in filename
|
||||||
|
and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||||
|
)
|
||||||
|
|
||||||
@bp.route("/media/upload", methods=["GET", "POST"])
|
@bp.route("/media/", methods=["GET"])
|
||||||
@login_required
|
def media_index():
|
||||||
def upload_media():
|
"""
|
||||||
if request.method == "POST":
|
/media/ is not used stand‐alone—redirect back to homepage.
|
||||||
file = request.files.get("image")
|
"""
|
||||||
caption = request.form.get("caption")
|
return redirect(url_for("core_ui.home"))
|
||||||
plant_id = request.form.get("plant_id")
|
|
||||||
|
|
||||||
if file and allowed_file(file.filename):
|
@bp.route("/media/files/<path:filename>", methods=["GET"])
|
||||||
filename = secure_filename(file.filename)
|
|
||||||
save_path = os.path.join(current_app.root_path, UPLOAD_FOLDER)
|
|
||||||
os.makedirs(save_path, exist_ok=True)
|
|
||||||
file.save(os.path.join(save_path, filename))
|
|
||||||
|
|
||||||
media = Media(file_url=f"{UPLOAD_FOLDER}/{filename}", caption=caption, plant_id=plant_id)
|
|
||||||
db.session.add(media)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
flash("Image uploaded successfully.", "success")
|
|
||||||
return redirect(url_for("media.upload_media"))
|
|
||||||
else:
|
|
||||||
flash("Invalid file or no file uploaded.", "danger")
|
|
||||||
|
|
||||||
return render_template("media/upload.html")
|
|
||||||
|
|
||||||
@bp.route("/media/files/<path:filename>")
|
|
||||||
def media_file(filename):
|
def media_file(filename):
|
||||||
return send_from_directory(os.path.join(current_app.root_path, "static/uploads"), 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)
|
||||||
|
|
||||||
@bp.route("/media/heart/<int:image_id>", methods=["POST"])
|
@bp.route("/media/heart/<int:media_id>", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def toggle_heart(image_id):
|
def toggle_heart(media_id):
|
||||||
existing = ImageHeart.query.filter_by(user_id=current_user.id, submission_image_id=image_id).first()
|
"""
|
||||||
|
Add/remove a "heart" from an image.
|
||||||
|
"""
|
||||||
|
existing = ImageHeart.query.filter_by(
|
||||||
|
user_id=current_user.id, media_id=media_id
|
||||||
|
).first()
|
||||||
|
|
||||||
if existing:
|
if existing:
|
||||||
db.session.delete(existing)
|
db.session.delete(existing)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return jsonify({"status": "unhearted"})
|
return jsonify({"status": "unhearted"})
|
||||||
else:
|
else:
|
||||||
heart = ImageHeart(user_id=current_user.id, submission_image_id=image_id)
|
heart = ImageHeart(user_id=current_user.id, media_id=media_id)
|
||||||
db.session.add(heart)
|
db.session.add(heart)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return jsonify({"status": "hearted"})
|
return jsonify({"status": "hearted"})
|
||||||
|
|
||||||
@bp.route("/media/feature/<int:image_id>", methods=["POST"])
|
@bp.route("/media/feature/<int:media_id>", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def set_featured_image(image_id):
|
def set_featured_image(media_id):
|
||||||
image = Media.query.get_or_404(image_id)
|
"""
|
||||||
plant = image.plant
|
Toggle featured status on a media item. Only the uploader or an admin may do so.
|
||||||
if not plant:
|
"""
|
||||||
flash("This image is not linked to a plant.", "danger")
|
media = Media.query.get_or_404(media_id)
|
||||||
return redirect(request.referrer or url_for("core_ui.home"))
|
if (current_user.id != media.uploader_id) and (current_user.role != "admin"):
|
||||||
|
|
||||||
if current_user.id != plant.owner_id and current_user.role != "admin":
|
|
||||||
flash("Not authorized to set featured image.", "danger")
|
flash("Not authorized to set featured image.", "danger")
|
||||||
return redirect(request.referrer or url_for("core_ui.home"))
|
return redirect(request.referrer or url_for("core_ui.home"))
|
||||||
|
|
||||||
FeaturedImage.query.filter_by(submission_image_id=image_id).delete()
|
# Remove any existing featured entries for this media
|
||||||
featured = FeaturedImage(submission_image_id=image_id, is_featured=True)
|
FeaturedImage.query.filter_by(media_id=media_id).delete()
|
||||||
|
featured = FeaturedImage(media_id=media_id, is_featured=True)
|
||||||
db.session.add(featured)
|
db.session.add(featured)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash("Image set as featured.", "success")
|
flash("Image set as featured.", "success")
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
{% extends 'core_ui/base.html' %}
|
|
||||||
{% block content %}
|
|
||||||
<h2>Upload Media</h2>
|
|
||||||
<form method="POST" enctype="multipart/form-data">
|
|
||||||
{{ form.hidden_tag() }}
|
|
||||||
<p>{{ form.image.label }}<br>{{ form.image() }}</p>
|
|
||||||
<p>{{ form.caption.label }}<br>{{ form.caption(size=40) }}</p>
|
|
||||||
<p>{{ form.plant_id.label }}<br>{{ form.plant_id() }}</p>
|
|
||||||
<p>{{ form.growlog_id.label }}<br>{{ form.growlog_id() }}</p>
|
|
||||||
<p>{{ form.submit() }}</p>
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
|
25
plugins/media/utils.py
Normal file
25
plugins/media/utils.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# plugins/media/utils.py
|
||||||
|
|
||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
def generate_random_filename(original_filename):
|
||||||
|
"""
|
||||||
|
Returns a random filename preserving the original extension.
|
||||||
|
e.g. “abcd1234efgh.jpg” for “myphoto.jpg”.
|
||||||
|
"""
|
||||||
|
ext = os.path.splitext(original_filename)[1].lower() # includes dot, e.g. ".jpg"
|
||||||
|
random_name = uuid.uuid4().hex # 32‐char hex string
|
||||||
|
return f"{random_name}{ext}"
|
||||||
|
|
||||||
|
def strip_metadata_and_save(source_file, destination_path):
|
||||||
|
"""
|
||||||
|
Opens an image with Pillow, strips EXIF (metadata), and saves it cleanly.
|
||||||
|
Supports common formats (JPEG, PNG).
|
||||||
|
"""
|
||||||
|
with Image.open(source_file) as img:
|
||||||
|
data = list(img.getdata())
|
||||||
|
clean_image = Image.new(img.mode, img.size)
|
||||||
|
clean_image.putdata(data)
|
||||||
|
clean_image.save(destination_path)
|
50
plugins/submission/forms.py
Normal file
50
plugins/submission/forms.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# plugins/submission/forms.py
|
||||||
|
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import (
|
||||||
|
StringField,
|
||||||
|
FloatField,
|
||||||
|
SelectField,
|
||||||
|
IntegerField,
|
||||||
|
TextAreaField,
|
||||||
|
MultipleFileField,
|
||||||
|
SubmitField
|
||||||
|
)
|
||||||
|
from wtforms.validators import Optional, NumberRange, Length, DataRequired
|
||||||
|
|
||||||
|
class SubmissionForm(FlaskForm):
|
||||||
|
submission_type = SelectField(
|
||||||
|
"Submission Type",
|
||||||
|
choices=[
|
||||||
|
("market_price", "Market Price"),
|
||||||
|
("name_correction", "Name Correction"),
|
||||||
|
("new_plant", "New Plant Suggestion"),
|
||||||
|
("mutation", "Mutation Discovery"),
|
||||||
|
("vendor_rating", "Vendor Rating/Review"),
|
||||||
|
("vendor_alias", "Vendor Alias Submission"),
|
||||||
|
],
|
||||||
|
validators=[DataRequired()]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make plant_name Optional; route logic enforces when necessary
|
||||||
|
plant_name = StringField("Common Name", validators=[Optional(), Length(max=100)])
|
||||||
|
scientific_name = StringField("Scientific Name", validators=[Optional(), Length(max=120)])
|
||||||
|
notes = TextAreaField("Notes", validators=[Optional()])
|
||||||
|
|
||||||
|
# Market Price fields
|
||||||
|
price = FloatField("Price", validators=[Optional()])
|
||||||
|
source = StringField("Source/Vendor (e.g. Etsy, eBay)", validators=[Optional(), Length(max=255)])
|
||||||
|
|
||||||
|
# Vendor Rating / Review fields
|
||||||
|
vendor_name = StringField("Vendor Name", validators=[Optional(), Length(max=255)])
|
||||||
|
rating = IntegerField("Rating (1–5)", validators=[Optional(), NumberRange(min=1, max=5)])
|
||||||
|
|
||||||
|
# Vendor Alias Submission fields
|
||||||
|
old_vendor = StringField("Existing Vendor Name", validators=[Optional(), Length(max=255)])
|
||||||
|
new_vendor = StringField("New Vendor Name (Alias)", validators=[Optional(), Length(max=255)])
|
||||||
|
alias_reason = TextAreaField("Reason for Alias", validators=[Optional()])
|
||||||
|
|
||||||
|
# Images (max 10)
|
||||||
|
images = MultipleFileField("Upload Images (max 10)", validators=[Optional()])
|
||||||
|
|
||||||
|
submit = SubmitField("Submit")
|
@ -1,7 +1,7 @@
|
|||||||
|
# plugins/submission/models.py
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from app import db
|
from app import db
|
||||||
from plugins.plant.models import Plant
|
|
||||||
from plugins.auth.models import User
|
|
||||||
|
|
||||||
class Submission(db.Model):
|
class Submission(db.Model):
|
||||||
__tablename__ = "submissions"
|
__tablename__ = "submissions"
|
||||||
@ -10,21 +10,48 @@ class Submission(db.Model):
|
|||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
|
user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
|
||||||
submitted_at = db.Column(db.DateTime, default=datetime.utcnow)
|
submitted_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
plant_name = db.Column(db.String(100), nullable=False)
|
|
||||||
|
# Core fields
|
||||||
|
plant_name = db.Column(db.String(100), nullable=True) # now nullable
|
||||||
scientific_name = db.Column(db.String(120), nullable=True)
|
scientific_name = db.Column(db.String(120), nullable=True)
|
||||||
notes = db.Column(db.Text, nullable=True)
|
notes = db.Column(db.Text, nullable=True)
|
||||||
|
|
||||||
|
submission_type = db.Column(db.String(50), nullable=False)
|
||||||
|
|
||||||
|
# Market price fields
|
||||||
|
price = db.Column(db.Float, nullable=True)
|
||||||
|
source = db.Column(db.String(255), nullable=True)
|
||||||
|
|
||||||
|
# Vendor fields
|
||||||
|
vendor_name = db.Column(db.String(255), nullable=True)
|
||||||
|
rating = db.Column(db.Integer, nullable=True)
|
||||||
|
|
||||||
|
# Alias fields
|
||||||
|
old_vendor = db.Column(db.String(255), nullable=True)
|
||||||
|
new_vendor = db.Column(db.String(255), nullable=True)
|
||||||
|
alias_reason = db.Column(db.Text, nullable=True)
|
||||||
|
|
||||||
approved = db.Column(db.Boolean, default=None)
|
approved = db.Column(db.Boolean, default=None)
|
||||||
approved_at = db.Column(db.DateTime, nullable=True)
|
approved_at = db.Column(db.DateTime, nullable=True)
|
||||||
reviewed_by = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True)
|
reviewed_by = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True)
|
||||||
|
|
||||||
# Explicit bidirectional relationships
|
# Relationships
|
||||||
submitter = db.relationship("User", foreign_keys=[user_id], back_populates="submitted_submissions")
|
submitter = db.relationship(
|
||||||
reviewer = db.relationship("User", foreign_keys=[reviewed_by], back_populates="reviewed_submissions")
|
"User",
|
||||||
|
foreign_keys=[user_id],
|
||||||
|
back_populates="submitted_submissions",
|
||||||
|
lazy=True
|
||||||
|
)
|
||||||
|
reviewer = db.relationship(
|
||||||
|
"User",
|
||||||
|
foreign_keys=[reviewed_by],
|
||||||
|
back_populates="reviewed_submissions",
|
||||||
|
lazy=True
|
||||||
|
)
|
||||||
|
|
||||||
images = db.relationship("SubmissionImage", backref="submission", lazy=True)
|
images = db.relationship("SubmissionImage", backref="submission", lazy=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class SubmissionImage(db.Model):
|
class SubmissionImage(db.Model):
|
||||||
__tablename__ = "submission_images"
|
__tablename__ = "submission_images"
|
||||||
__table_args__ = {"extend_existing": True}
|
__table_args__ = {"extend_existing": True}
|
||||||
|
@ -1,78 +1,144 @@
|
|||||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify, send_from_directory, current_app
|
# plugins/submission/routes.py
|
||||||
from flask_login import current_user, login_required
|
|
||||||
from werkzeug.utils import secure_filename
|
from flask import (
|
||||||
import os
|
Blueprint,
|
||||||
|
render_template,
|
||||||
|
request,
|
||||||
|
redirect,
|
||||||
|
url_for,
|
||||||
|
flash,
|
||||||
|
jsonify
|
||||||
|
)
|
||||||
|
from flask_login import login_required, current_user
|
||||||
from app import db
|
from app import db
|
||||||
from .models import SubmissionImage, ImageHeart, FeaturedImage
|
from .models import Submission, SubmissionImage
|
||||||
from plugins.plant.models import Plant
|
from .forms import SubmissionForm
|
||||||
|
from datetime import datetime
|
||||||
|
import os
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
from plugins.media.utils import generate_random_filename, strip_metadata_and_save
|
||||||
|
|
||||||
bp = Blueprint("submission", __name__, template_folder="templates")
|
bp = Blueprint("submission", __name__, template_folder="templates", url_prefix="/submission")
|
||||||
|
|
||||||
UPLOAD_FOLDER = "static/uploads"
|
# We store only "YYYY/MM/DD/<uuid>.ext" in SubmissionImage.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"}
|
ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "gif"}
|
||||||
|
|
||||||
def allowed_file(filename):
|
def allowed_file(filename):
|
||||||
return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
|
return (
|
||||||
|
"." in filename
|
||||||
|
and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||||
|
)
|
||||||
|
|
||||||
@bp.route("/submissions/upload", methods=["GET", "POST"])
|
@bp.route("/", methods=["GET"])
|
||||||
@login_required
|
@login_required
|
||||||
def upload_submissions():
|
def submission_index():
|
||||||
if request.method == "POST":
|
return redirect(url_for("submission.new_submission"))
|
||||||
file = request.files.get("image")
|
|
||||||
caption = request.form.get("caption")
|
|
||||||
plant_id = request.form.get("plant_id")
|
|
||||||
|
|
||||||
if file and allowed_file(file.filename):
|
@bp.route("/new", methods=["GET", "POST"])
|
||||||
filename = secure_filename(file.filename)
|
@bp.route("/new/", methods=["GET", "POST"])
|
||||||
save_path = os.path.join(current_app.root_path, UPLOAD_FOLDER)
|
|
||||||
os.makedirs(save_path, exist_ok=True)
|
|
||||||
file.save(os.path.join(save_path, filename))
|
|
||||||
|
|
||||||
submissions = SubmissionImage(file_url=f"{UPLOAD_FOLDER}/{filename}", caption=caption, plant_id=plant_id)
|
|
||||||
db.session.add(submissions)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
flash("Image uploaded successfully.", "success")
|
|
||||||
return redirect(url_for("submissions.upload_submissions"))
|
|
||||||
else:
|
|
||||||
flash("Invalid file or no file uploaded.", "danger")
|
|
||||||
|
|
||||||
return render_template("submissions/upload.html")
|
|
||||||
|
|
||||||
@bp.route("/submissions/files/<path:filename>")
|
|
||||||
def submissions_file(filename):
|
|
||||||
return send_from_directory(os.path.join(current_app.root_path, "static/uploads"), filename)
|
|
||||||
|
|
||||||
@bp.route("/submissions/heart/<int:image_id>", methods=["POST"])
|
|
||||||
@login_required
|
@login_required
|
||||||
def toggle_heart(image_id):
|
def new_submission():
|
||||||
existing = ImageHeart.query.filter_by(user_id=current_user.id, submission_image_id=image_id).first()
|
form = SubmissionForm()
|
||||||
if existing:
|
if form.validate_on_submit():
|
||||||
db.session.delete(existing)
|
plant_types = {"market_price", "name_correction", "new_plant", "mutation"}
|
||||||
db.session.commit()
|
t = form.submission_type.data
|
||||||
return jsonify({"status": "unhearted"})
|
|
||||||
else:
|
|
||||||
heart = ImageHeart(user_id=current_user.id, submission_image_id=image_id)
|
|
||||||
db.session.add(heart)
|
|
||||||
db.session.commit()
|
|
||||||
return jsonify({"status": "hearted"})
|
|
||||||
|
|
||||||
@bp.route("/submissions/feature/<int:image_id>", methods=["POST"])
|
# Only require plant_name if the type is plant‐related
|
||||||
|
if t in plant_types and not form.plant_name.data.strip():
|
||||||
|
flash("Common Name is required for this submission type.", "danger")
|
||||||
|
return render_template("submission/new.html", form=form)
|
||||||
|
|
||||||
|
submission = Submission(
|
||||||
|
user_id=current_user.id,
|
||||||
|
submitted_at=datetime.utcnow(),
|
||||||
|
submission_type=t,
|
||||||
|
plant_name=form.plant_name.data,
|
||||||
|
scientific_name=form.scientific_name.data,
|
||||||
|
notes=form.notes.data,
|
||||||
|
price=form.price.data if form.price.data else None,
|
||||||
|
source=form.source.data,
|
||||||
|
vendor_name=form.vendor_name.data,
|
||||||
|
rating=form.rating.data,
|
||||||
|
old_vendor=form.old_vendor.data,
|
||||||
|
new_vendor=form.new_vendor.data,
|
||||||
|
alias_reason=form.alias_reason.data,
|
||||||
|
approved=None
|
||||||
|
)
|
||||||
|
db.session.add(submission)
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
# date subfolder: "YYYY/MM/DD"
|
||||||
|
today = datetime.utcnow().strftime("%Y/%m/%d")
|
||||||
|
|
||||||
|
# Write into "/app/static/uploads/YYYY/MM/DD", not "/app/app/static/uploads..."
|
||||||
|
save_dir = os.path.join(os.getcwd(), BASE_UPLOAD_FOLDER, today)
|
||||||
|
os.makedirs(save_dir, exist_ok=True)
|
||||||
|
|
||||||
|
files = request.files.getlist("images")
|
||||||
|
for f in files:
|
||||||
|
if f and allowed_file(f.filename):
|
||||||
|
orig_name = secure_filename(f.filename)
|
||||||
|
rand_name = generate_random_filename(orig_name)
|
||||||
|
|
||||||
|
# Temporarily save under "/app/temp_<uuid>.ext"
|
||||||
|
temp_path = os.path.join(os.getcwd(), "temp_" + rand_name)
|
||||||
|
f.save(temp_path)
|
||||||
|
|
||||||
|
final_path = os.path.join(save_dir, rand_name)
|
||||||
|
strip_metadata_and_save(temp_path, final_path)
|
||||||
|
os.remove(temp_path)
|
||||||
|
|
||||||
|
# Store only "YYYY/MM/DD/<uuid>.ext"
|
||||||
|
rel_url = f"{today}/{rand_name}"
|
||||||
|
|
||||||
|
img = SubmissionImage(
|
||||||
|
submission_id=submission.id,
|
||||||
|
file_url=rel_url,
|
||||||
|
uploaded_at=datetime.utcnow()
|
||||||
|
)
|
||||||
|
db.session.add(img)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
flash("Submission received. Thank you!", "success")
|
||||||
|
return redirect(url_for("submission.new_submission"))
|
||||||
|
|
||||||
|
return render_template("submission/new.html", form=form)
|
||||||
|
|
||||||
|
@bp.route("/list", methods=["GET"])
|
||||||
|
@bp.route("/list/", methods=["GET"])
|
||||||
@login_required
|
@login_required
|
||||||
def set_featured_image(image_id):
|
def list_submissions():
|
||||||
image = SubmissionImage.query.get_or_404(image_id)
|
selected_type = request.args.get("type", None)
|
||||||
plant = image.plant
|
query = Submission.query.filter_by(user_id=current_user.id)
|
||||||
if not plant:
|
if selected_type:
|
||||||
flash("This image is not linked to a plant.", "danger")
|
query = query.filter_by(submission_type=selected_type)
|
||||||
return redirect(request.referrer or url_for("core_ui.home"))
|
subs = query.order_by(Submission.submitted_at.desc()).all()
|
||||||
|
|
||||||
if current_user.id != plant.owner_id and current_user.role != "admin":
|
all_types = [
|
||||||
flash("Not authorized to set featured image.", "danger")
|
("", "All"),
|
||||||
return redirect(request.referrer or url_for("core_ui.home"))
|
("market_price", "Market Price"),
|
||||||
|
("name_correction", "Name Correction"),
|
||||||
|
("new_plant", "New Plant Suggestion"),
|
||||||
|
("mutation", "Mutation Discovery"),
|
||||||
|
("vendor_rating", "Vendor Rating/Review"),
|
||||||
|
("vendor_alias", "Vendor Alias Submission"),
|
||||||
|
]
|
||||||
|
return render_template(
|
||||||
|
"submission/list.html",
|
||||||
|
submissions=subs,
|
||||||
|
selected_type=selected_type,
|
||||||
|
all_types=all_types
|
||||||
|
)
|
||||||
|
|
||||||
FeaturedImage.query.filter_by(submission_image_id=image_id).delete()
|
@bp.route("/view/<int:submission_id>", methods=["GET"])
|
||||||
featured = FeaturedImage(submission_image_id=image_id, is_featured=True)
|
@bp.route("/view/<int:submission_id>/", methods=["GET"])
|
||||||
db.session.add(featured)
|
@login_required
|
||||||
db.session.commit()
|
def view_submission(submission_id):
|
||||||
flash("Image set as featured.", "success")
|
sub = Submission.query.get_or_404(submission_id)
|
||||||
return redirect(request.referrer or url_for("core_ui.home"))
|
if sub.user_id != current_user.id and current_user.role != "admin":
|
||||||
|
flash("Not authorized to view this submission.", "danger")
|
||||||
|
return redirect(url_for("submission.list_submissions"))
|
||||||
|
images = SubmissionImage.query.filter_by(submission_id=sub.id).all()
|
||||||
|
return render_template("submission/view.html", submission=sub, images=images)
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
{% extends "core_ui/base.html" %}
|
|
||||||
{% block content %}
|
|
||||||
<h2>Submissions</h2>
|
|
||||||
<ul>
|
|
||||||
{% for s in submissions %}
|
|
||||||
<li>{{ s.common_name }} ({{ s.scientific_name or 'N/A' }}) - ${{ s.price }}</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endblock %}
|
|
55
plugins/submission/templates/submission/list.html
Normal file
55
plugins/submission/templates/submission/list.html
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
{% extends 'core_ui/base.html' %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mt-4">
|
||||||
|
<h2>Your Submissions</h2>
|
||||||
|
|
||||||
|
<!-- Filter dropdown -->
|
||||||
|
<form method="GET" class="row g-2 align-items-center mb-3">
|
||||||
|
<div class="col-auto">
|
||||||
|
<label for="filter_type" class="form-label">Filter by Type:</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<select id="filter_type" name="type" class="form-select" onchange="this.form.submit()">
|
||||||
|
{% for val, label in all_types %}
|
||||||
|
<option value="{{ val }}" {% if selected_type == val %}selected{% endif %}>
|
||||||
|
{{ label }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<noscript><button type="submit" class="btn btn-primary">Apply</button></noscript>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if submissions %}
|
||||||
|
<ul class="list-group">
|
||||||
|
{% for sub in submissions %}
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<strong>{{ sub.submission_type.replace('_', ' ').title() }}</strong>
|
||||||
|
— {{ sub.plant_name or '--' }}
|
||||||
|
{% if sub.submission_type == 'market_price' and sub.price is not none %}
|
||||||
|
· ${{ '%.2f' % sub.price }}
|
||||||
|
{% elif sub.submission_type == 'vendor_rating' and sub.vendor_name %}
|
||||||
|
· Rated “{{ sub.vendor_name }}” ({{ sub.rating }}/5)
|
||||||
|
{% elif sub.submission_type == 'vendor_alias' and sub.old_vendor %}
|
||||||
|
· Alias: “{{ sub.old_vendor }}” → “{{ sub.new_vendor }}”
|
||||||
|
{% endif %}
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">Submitted {{ sub.submitted_at.strftime('%Y-%m-%d %H:%M') }}</small>
|
||||||
|
</div>
|
||||||
|
<a href="{{ url_for('submission.view_submission', submission_id=sub.id) }}"
|
||||||
|
class="btn btn-outline-primary btn-sm">View</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p>You have no submissions that match this filter.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<a href="{{ url_for('submission.new_submission') }}" class="btn btn-primary">New Submission</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
166
plugins/submission/templates/submission/new.html
Normal file
166
plugins/submission/templates/submission/new.html
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
{% extends 'core_ui/base.html' %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mt-4">
|
||||||
|
<h2>New Submission</h2>
|
||||||
|
<form method="POST" enctype="multipart/form-data" id="submission-form">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
|
||||||
|
<!-- Submission Type selector -->
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ form.submission_type.label(class="form-label") }}
|
||||||
|
{{ form.submission_type(class="form-select", id="submission_type") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Plant fields: only for plant‐related types -->
|
||||||
|
<div class="mb-3" id="plant-name-group">
|
||||||
|
{{ form.plant_name.label(class="form-label") }}
|
||||||
|
{{ form.plant_name(class="form-control") }}
|
||||||
|
<div class="form-text">Common name of the plant (required for plant‐related submissions).</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3" id="scientific-name-group">
|
||||||
|
{{ form.scientific_name.label(class="form-label") }}
|
||||||
|
{{ form.scientific_name(class="form-control") }}
|
||||||
|
<div class="form-text">Scientific name (optional for plant‐related submissions).</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Market Price fields -->
|
||||||
|
<div class="mb-3" id="price-group">
|
||||||
|
{{ form.price.label(class="form-label") }}
|
||||||
|
{{ form.price(class="form-control") }}
|
||||||
|
<div class="form-text">Only for “Market Price” submissions.</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3" id="source-group">
|
||||||
|
{{ form.source.label(class="form-label") }}
|
||||||
|
{{ form.source(class="form-control") }}
|
||||||
|
<div class="form-text">Source/vendor (for market price).</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Name Correction: no extra fields beyond notes -->
|
||||||
|
|
||||||
|
<!-- New Plant Suggestion helper text -->
|
||||||
|
<div class="mb-3" id="new-plant-group">
|
||||||
|
<p class="text-muted">
|
||||||
|
For new plant suggestions, simply fill in common/scientific name and optional notes/images.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mutation Discovery helper text -->
|
||||||
|
<div class="mb-3" id="mutation-group">
|
||||||
|
<p class="text-muted">
|
||||||
|
For mutation discovery, fill in plant details and upload mutation images.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Vendor Rating / Review fields -->
|
||||||
|
<div class="mb-3" id="vendor-name-group">
|
||||||
|
{{ form.vendor_name.label(class="form-label") }}
|
||||||
|
{{ form.vendor_name(class="form-control") }}
|
||||||
|
<div class="form-text">Vendor name to rate/review (e.g., “Etsy Seller X”).</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3" id="rating-group">
|
||||||
|
{{ form.rating.label(class="form-label") }}
|
||||||
|
{{ form.rating(class="form-select") }}
|
||||||
|
<div class="form-text">1 = Poor, 5 = Excellent.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Vendor Alias Submission fields -->
|
||||||
|
<div class="mb-3" id="old-vendor-group">
|
||||||
|
{{ form.old_vendor.label(class="form-label") }}
|
||||||
|
{{ form.old_vendor(class="form-control") }}
|
||||||
|
<div class="form-text">Existing vendor name (to alias).</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3" id="new-vendor-group">
|
||||||
|
{{ form.new_vendor.label(class="form-label") }}
|
||||||
|
{{ form.new_vendor(class="form-control") }}
|
||||||
|
<div class="form-text">New alias name for vendor.</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3" id="alias-reason-group">
|
||||||
|
{{ form.alias_reason.label(class="form-label") }}
|
||||||
|
{{ form.alias_reason(class="form-control", rows=3) }}
|
||||||
|
<div class="form-text">Why this alias is needed (e.g., vendor changed name).</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Notes & Images (always present) -->
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ form.notes.label(class="form-label") }}
|
||||||
|
{{ form.notes(class="form-control", rows=3) }}
|
||||||
|
<div class="form-text">Additional notes or context.</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ form.images.label(class="form-label") }}
|
||||||
|
{{ form.images(class="form-control", multiple=True) }}
|
||||||
|
<div class="form-text">Upload up to 10 images relevant to your submission.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-success">{{ form.submit.label.text }}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const typeField = document.getElementById('submission_type');
|
||||||
|
|
||||||
|
// Groups to show/hide
|
||||||
|
const plantNameGroup = document.getElementById('plant-name-group');
|
||||||
|
const scientificNameGroup = document.getElementById('scientific-name-group');
|
||||||
|
const priceGroup = document.getElementById('price-group');
|
||||||
|
const sourceGroup = document.getElementById('source-group');
|
||||||
|
const newPlantGroup = document.getElementById('new-plant-group');
|
||||||
|
const mutationGroup = document.getElementById('mutation-group');
|
||||||
|
const vendorNameGroup = document.getElementById('vendor-name-group');
|
||||||
|
const ratingGroup = document.getElementById('rating-group');
|
||||||
|
const oldVendorGroup = document.getElementById('old-vendor-group');
|
||||||
|
const newVendorGroup = document.getElementById('new-vendor-group');
|
||||||
|
const aliasReasonGroup = document.getElementById('alias-reason-group');
|
||||||
|
|
||||||
|
function toggleFields() {
|
||||||
|
const t = typeField.value;
|
||||||
|
|
||||||
|
// First hide everything
|
||||||
|
[
|
||||||
|
plantNameGroup, scientificNameGroup,
|
||||||
|
priceGroup, sourceGroup,
|
||||||
|
newPlantGroup, mutationGroup,
|
||||||
|
vendorNameGroup, ratingGroup,
|
||||||
|
oldVendorGroup, newVendorGroup, aliasReasonGroup
|
||||||
|
].forEach(div => div.style.display = 'none');
|
||||||
|
|
||||||
|
// Show core plant fields for plant‐related types
|
||||||
|
if (['market_price', 'name_correction', 'new_plant', 'mutation'].includes(t)) {
|
||||||
|
plantNameGroup.style.display = 'block';
|
||||||
|
scientificNameGroup.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show Market Price fields
|
||||||
|
if (t === 'market_price') {
|
||||||
|
priceGroup.style.display = 'block';
|
||||||
|
sourceGroup.style.display = 'block';
|
||||||
|
}
|
||||||
|
// Show New Plant helper
|
||||||
|
else if (t === 'new_plant') {
|
||||||
|
newPlantGroup.style.display = 'block';
|
||||||
|
}
|
||||||
|
// Show Mutation helper
|
||||||
|
else if (t === 'mutation') {
|
||||||
|
mutationGroup.style.display = 'block';
|
||||||
|
}
|
||||||
|
// Show Vendor Rating / Review fields
|
||||||
|
else if (t === 'vendor_rating') {
|
||||||
|
vendorNameGroup.style.display = 'block';
|
||||||
|
ratingGroup.style.display = 'block';
|
||||||
|
}
|
||||||
|
// Show Vendor Alias fields
|
||||||
|
else if (t === 'vendor_alias') {
|
||||||
|
oldVendorGroup.style.display = 'block';
|
||||||
|
newVendorGroup.style.display = 'block';
|
||||||
|
aliasReasonGroup.style.display = 'block';
|
||||||
|
}
|
||||||
|
// For name_correction: only plant fields + notes/images
|
||||||
|
}
|
||||||
|
|
||||||
|
typeField.addEventListener('change', toggleFields);
|
||||||
|
toggleFields(); // initial call when page first loads
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
101
plugins/submission/templates/submission/view.html
Normal file
101
plugins/submission/templates/submission/view.html
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
{% extends 'core_ui/base.html' %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mt-4">
|
||||||
|
<h2>Submission Details</h2>
|
||||||
|
<dl class="row">
|
||||||
|
<dt class="col-sm-3">Type:</dt>
|
||||||
|
<dd class="col-sm-9">
|
||||||
|
{{ submission.submission_type.replace('_', ' ').title() }}
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-3">Common Name:</dt>
|
||||||
|
<dd class="col-sm-9">
|
||||||
|
{% if submission.plant_name %}
|
||||||
|
{{ submission.plant_name }}
|
||||||
|
{% else %}
|
||||||
|
—
|
||||||
|
{% endif %}
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-3">Scientific Name:</dt>
|
||||||
|
<dd class="col-sm-9">
|
||||||
|
{% if submission.scientific_name %}
|
||||||
|
{{ submission.scientific_name }}
|
||||||
|
{% else %}
|
||||||
|
—
|
||||||
|
{% endif %}
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
{% if submission.submission_type == 'market_price' %}
|
||||||
|
<dt class="col-sm-3">Price:</dt>
|
||||||
|
<dd class="col-sm-9">${{ '%.2f' % submission.price }}</dd>
|
||||||
|
<dt class="col-sm-3">Source:</dt>
|
||||||
|
<dd class="col-sm-9">{{ submission.source or '—' }}</dd>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if submission.submission_type == 'vendor_rating' %}
|
||||||
|
<dt class="col-sm-3">Vendor Name:</dt>
|
||||||
|
<dd class="col-sm-9">{{ submission.vendor_name }}</dd>
|
||||||
|
<dt class="col-sm-3">Rating:</dt>
|
||||||
|
<dd class="col-sm-9">{{ submission.rating }} / 5</dd>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if submission.submission_type == 'vendor_alias' %}
|
||||||
|
<dt class="col-sm-3">Old Vendor:</dt>
|
||||||
|
<dd class="col-sm-9">{{ submission.old_vendor }}</dd>
|
||||||
|
<dt class="col-sm-3">New Vendor Alias:</dt>
|
||||||
|
<dd class="col-sm-9">{{ submission.new_vendor }}</dd>
|
||||||
|
<dt class="col-sm-3">Alias Reason:</dt>
|
||||||
|
<dd class="col-sm-9">{{ submission.alias_reason or '—' }}</dd>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<dt class="col-sm-3">Notes:</dt>
|
||||||
|
<dd class="col-sm-9">{{ submission.notes or '—' }}</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-3">Submitted At:</dt>
|
||||||
|
<dd class="col-sm-9">
|
||||||
|
{{ submission.submitted_at.strftime('%Y-%m-%d %H:%M') }}
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-3">Status:</dt>
|
||||||
|
<dd class="col-sm-9">
|
||||||
|
{% if submission.approved is none %}
|
||||||
|
<span class="badge bg-warning">Pending</span>
|
||||||
|
{% elif submission.approved %}
|
||||||
|
<span class="badge bg-success">Approved</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-danger">Rejected</span>
|
||||||
|
{% endif %}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<h4>Images</h4>
|
||||||
|
<div class="row">
|
||||||
|
{% if images %}
|
||||||
|
{% for img in images %}
|
||||||
|
{# img.file_url == "YYYY/MM/DD/<uuid>.ext" #}
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<a href="{{ url_for('media.media_file', filename=img.file_url) }}" target="_blank">
|
||||||
|
<img src="{{ url_for('media.media_file', filename=img.file_url) }}"
|
||||||
|
class="card-img-top"
|
||||||
|
alt="Submission Image">
|
||||||
|
</a>
|
||||||
|
<div class="card-body p-2">
|
||||||
|
<p class="card-text text-center">
|
||||||
|
<small class="text-muted">{{ img.uploaded_at.strftime('%Y-%m-%d') }}</small>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<p>No images uploaded.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<a href="{{ url_for('submission.list_submissions') }}" class="btn btn-secondary">Back to List</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -8,5 +8,5 @@ Werkzeug>=2.3.0
|
|||||||
pymysql
|
pymysql
|
||||||
python-dotenv
|
python-dotenv
|
||||||
cryptography
|
cryptography
|
||||||
|
Pillow
|
||||||
neo4j>=5.18.0
|
neo4j>=5.18.0
|
||||||
|
BIN
static/uploads/2025/06/06/5c86afeae316454b8f8ca73f8f3b9825.jpeg
Normal file
BIN
static/uploads/2025/06/06/5c86afeae316454b8f8ca73f8f3b9825.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 133 KiB |
Reference in New Issue
Block a user