more crap
This commit is contained in:
@ -1,8 +1,7 @@
|
|||||||
# Environment toggles
|
# Environment toggles
|
||||||
USE_REMOTE_MYSQL=0
|
USE_REMOTE_MYSQL=0
|
||||||
ENABLE_DB_PRELOAD=0
|
|
||||||
ENABLE_DB_SEEDING=1
|
ENABLE_DB_SEEDING=1
|
||||||
SEED_EXTRA_DATA=false
|
DOCKER_ENV=development
|
||||||
UPLOAD_FOLDER=app/static/uploads
|
UPLOAD_FOLDER=app/static/uploads
|
||||||
SECRET_KEY=supersecretplantappkey
|
SECRET_KEY=supersecretplantappkey
|
||||||
|
|
||||||
|
13
Dockerfile
13
Dockerfile
@ -4,15 +4,14 @@ WORKDIR /app
|
|||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Required for mysqlclient + netcat wait
|
# Required for mysqlclient + netcat wait
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y gcc default-libmysqlclient-dev pkg-config netcat-openbsd curl && rm -rf /var/lib/apt/lists/*
|
||||||
gcc \
|
|
||||||
default-libmysqlclient-dev \
|
|
||||||
pkg-config \
|
|
||||||
netcat-openbsd \
|
|
||||||
curl \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
RUN pip install --upgrade pip
|
RUN pip install --upgrade pip
|
||||||
RUN pip install -r requirements.txt
|
RUN pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Add entrypoint script
|
||||||
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
CMD ["flask", "run", "--host=0.0.0.0"]
|
CMD ["flask", "run", "--host=0.0.0.0"]
|
||||||
|
2
Makefile
2
Makefile
@ -55,7 +55,7 @@ status:
|
|||||||
@docker ps --filter "name=$(PROJECT_NAME)-" --format "table {{.Names}}\t{{.Status}}"
|
@docker ps --filter "name=$(PROJECT_NAME)-" --format "table {{.Names}}\t{{.Status}}"
|
||||||
|
|
||||||
seed:
|
seed:
|
||||||
@docker exec -it $$(docker ps -qf "name=$(PROJECT_NAME)-web") flask seed-admin
|
@docker exec -it $$(docker ps -qf "name=$(PROJECT_NAME)-web") flask preload-data
|
||||||
|
|
||||||
shell:
|
shell:
|
||||||
@docker exec -it $$(docker ps -qf "name=$(PROJECT_NAME)-web") bash
|
@docker exec -it $$(docker ps -qf "name=$(PROJECT_NAME)-web") bash
|
||||||
|
@ -1,19 +1,27 @@
|
|||||||
import os
|
import os
|
||||||
|
import json
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import importlib
|
import importlib
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from flask_migrate import Migrate
|
from flask_migrate import Migrate
|
||||||
from flask_login import LoginManager
|
from flask_login import LoginManager
|
||||||
|
from flask_wtf.csrf import CSRFProtect
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Load environment variables from .env
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
migrate = Migrate()
|
migrate = Migrate()
|
||||||
login_manager = LoginManager()
|
login_manager = LoginManager()
|
||||||
|
csrf = CSRFProtect()
|
||||||
|
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config.from_object('app.config.Config')
|
app.config.from_object('app.config.Config')
|
||||||
|
csrf.init_app(app)
|
||||||
|
|
||||||
# Initialize core extensions
|
# Initialize core extensions
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
@ -28,7 +36,6 @@ def create_app():
|
|||||||
# Plugin auto-loader
|
# Plugin auto-loader
|
||||||
plugin_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'plugins'))
|
plugin_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'plugins'))
|
||||||
for plugin in os.listdir(plugin_path):
|
for plugin in os.listdir(plugin_path):
|
||||||
# Skip folders that end with `.noload`
|
|
||||||
if plugin.endswith('.noload'):
|
if plugin.endswith('.noload'):
|
||||||
print(f"[⏭] Skipping plugin '{plugin}' (marked as .noload)")
|
print(f"[⏭] Skipping plugin '{plugin}' (marked as .noload)")
|
||||||
continue
|
continue
|
||||||
@ -49,13 +56,23 @@ def create_app():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[⚠️] Failed to load routes from plugin '{plugin}': {e}")
|
print(f"[⚠️] Failed to load routes from plugin '{plugin}': {e}")
|
||||||
|
|
||||||
# Register CLI commands
|
# Register CLI commands and plugin entry points
|
||||||
init_file = os.path.join(plugin_dir, '__init__.py')
|
init_file = os.path.join(plugin_dir, '__init__.py')
|
||||||
if os.path.isfile(init_file):
|
if os.path.isfile(init_file):
|
||||||
try:
|
try:
|
||||||
cli_module = importlib.import_module(f"plugins.{plugin}")
|
cli_module = importlib.import_module(f"plugins.{plugin}")
|
||||||
if hasattr(cli_module, 'register_cli'):
|
if hasattr(cli_module, 'register_cli'):
|
||||||
cli_module.register_cli(app)
|
cli_module.register_cli(app)
|
||||||
|
|
||||||
|
plugin_json = os.path.join(plugin_dir, 'plugin.json')
|
||||||
|
if os.path.isfile(plugin_json):
|
||||||
|
try:
|
||||||
|
meta = json.load(open(plugin_json, 'r'))
|
||||||
|
entry = meta.get('entry_point')
|
||||||
|
if entry and hasattr(cli_module, entry):
|
||||||
|
getattr(cli_module, entry)(app)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[⚠️] Failed to run entry_point '{entry}' for plugin '{plugin}': {e}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[⚠️] Failed to load CLI from plugin '{plugin}': {e}")
|
print(f"[⚠️] Failed to load CLI from plugin '{plugin}': {e}")
|
||||||
|
|
||||||
|
@ -22,6 +22,5 @@ class Config:
|
|||||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
|
|
||||||
# Optional toggles
|
# Optional toggles
|
||||||
ENABLE_DB_PRELOAD = os.environ.get('ENABLE_DB_PRELOAD', '0') == '1'
|
|
||||||
ENABLE_DB_SEEDING = os.environ.get('ENABLE_DB_SEEDING', '0') == '1'
|
ENABLE_DB_SEEDING = os.environ.get('ENABLE_DB_SEEDING', '0') == '1'
|
||||||
SEED_EXTRA_DATA = os.environ.get('SEED_EXTRA_DATA', 'false').lower() == 'true'
|
DOCKER_ENV = os.environ.get('FLASK_ENV', 'production')
|
||||||
|
@ -9,7 +9,6 @@ services:
|
|||||||
- FLASK_APP=app
|
- FLASK_APP=app
|
||||||
- FLASK_ENV=development
|
- FLASK_ENV=development
|
||||||
- USE_REMOTE_MYSQL=${USE_REMOTE_MYSQL}
|
- USE_REMOTE_MYSQL=${USE_REMOTE_MYSQL}
|
||||||
- ENABLE_DB_PRELOAD=${ENABLE_DB_PRELOAD}
|
|
||||||
- ENABLE_DB_SEEDING=${ENABLE_DB_SEEDING}
|
- ENABLE_DB_SEEDING=${ENABLE_DB_SEEDING}
|
||||||
- MYSQL_DATABASE=${MYSQL_DATABASE}
|
- MYSQL_DATABASE=${MYSQL_DATABASE}
|
||||||
- MYSQL_USER=${MYSQL_USER}
|
- MYSQL_USER=${MYSQL_USER}
|
||||||
@ -46,16 +45,10 @@ services:
|
|||||||
|
|
||||||
echo '[✔] Running DB migrations...'
|
echo '[✔] Running DB migrations...'
|
||||||
flask db upgrade
|
flask db upgrade
|
||||||
|
|
||||||
if [ "$$ENABLE_DB_PRELOAD" = "1" ]; then
|
|
||||||
echo '[📥] Preloading data...'; flask preload-data;
|
|
||||||
else
|
|
||||||
echo '[⚠️] Skipping preload...';
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ \"$$ENABLE_DB_SEEDING\" = \"1\" ]; then
|
if [ \"$ENABLE_DB_SEEDING\" = \"1\" ]; then
|
||||||
echo '[🌱] Seeding admin user...'
|
echo '[🌱] Seeding Data...'
|
||||||
flask seed-admin
|
flask preload-data
|
||||||
else
|
else
|
||||||
echo '[⚠️] DB seeding skipped by config.'
|
echo '[⚠️] DB seeding skipped by config.'
|
||||||
fi
|
fi
|
||||||
|
21
entrypoint.sh
Normal file
21
entrypoint.sh
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Initialize migrations folder if needed
|
||||||
|
if [ ! -d migrations ]; then
|
||||||
|
echo "[✔] Initializing migrations directory"
|
||||||
|
flask db init
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[✔] Running migrations"
|
||||||
|
flask db migrate -m "auto"
|
||||||
|
flask db upgrade
|
||||||
|
|
||||||
|
# Seed database if enabled
|
||||||
|
if [ "$ENABLE_DB_SEEDING" = "true" ]; then
|
||||||
|
echo "[🌱] Seeding Data"
|
||||||
|
flask preload-data
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start the main process
|
||||||
|
exec "$@"
|
@ -1,10 +1,6 @@
|
|||||||
from .seed import preload_data, seed_admin
|
import click
|
||||||
|
from flask import Flask
|
||||||
|
|
||||||
cli_commands = [
|
def register_cli(app: Flask):
|
||||||
preload_data,
|
from .seed import preload_data_cli
|
||||||
seed_admin
|
app.cli.add_command(preload_data_cli)
|
||||||
]
|
|
||||||
|
|
||||||
def register_cli(app):
|
|
||||||
for command in cli_commands:
|
|
||||||
app.cli.add_command(command)
|
|
||||||
|
@ -1,32 +1,121 @@
|
|||||||
import click
|
import click
|
||||||
from flask.cli import with_appcontext
|
from flask.cli import with_appcontext
|
||||||
from plugins.plant.models import Plant
|
from datetime import datetime, timedelta
|
||||||
from plugins.auth.models import User
|
|
||||||
from app import db
|
from app import db
|
||||||
|
from plugins.auth.models import User
|
||||||
|
from plugins.plant.models import (
|
||||||
|
Plant, PlantCommonName, PlantScientificName, PlantOwnershipLog,
|
||||||
|
PlantLineage
|
||||||
|
)
|
||||||
|
from plugins.growlog.models import PlantUpdate
|
||||||
|
from plugins.media.models import Media, ImageHeart, FeaturedImage
|
||||||
|
from plugins.submission.models import Submission, SubmissionImage
|
||||||
|
|
||||||
|
|
||||||
@click.command('preload-data')
|
@click.command(name='preload-data') # 🔧 changed from preload_data
|
||||||
@with_appcontext
|
@with_appcontext
|
||||||
def preload_data():
|
def preload_data_cli():
|
||||||
click.echo("Preloading data...")
|
preload_data(auto=False)
|
||||||
if not Plant.query.first():
|
|
||||||
plant = Plant(name="Default Plant")
|
|
||||||
db.session.add(plant)
|
|
||||||
db.session.commit()
|
|
||||||
click.echo("Default plant added.")
|
|
||||||
else:
|
|
||||||
click.echo("Plant data already exists.")
|
|
||||||
|
|
||||||
|
def preload_data(auto=False):
|
||||||
|
if User.query.filter_by(email='admin@example.com').first():
|
||||||
|
if not auto:
|
||||||
|
click.echo("⚠️ Demo data already exists, skipping.")
|
||||||
|
return
|
||||||
|
|
||||||
@click.command('seed-admin')
|
if not auto:
|
||||||
@with_appcontext
|
click.echo("🌱 Seeding demo data...")
|
||||||
def seed_admin():
|
|
||||||
click.echo("Seeding admin user...")
|
# USERS
|
||||||
if not User.query.filter_by(email='admin@example.com').first():
|
admin = User(email='admin@example.com', role='admin', is_verified=True)
|
||||||
user = User(email='admin@example.com', role='admin', is_verified=True)
|
admin.set_password('password123')
|
||||||
user.set_password('password') # Make sure this method exists in your model
|
|
||||||
db.session.add(user)
|
user = User(email='user@example.com', role='user', is_verified=True)
|
||||||
db.session.commit()
|
user.set_password('password123')
|
||||||
click.echo("✅ Admin user created.")
|
|
||||||
else:
|
db.session.add_all([admin, user])
|
||||||
click.echo("ℹ️ Admin user already exists.")
|
db.session.commit()
|
||||||
|
|
||||||
|
# COMMON & SCIENTIFIC NAMES
|
||||||
|
monstera_common = PlantCommonName(name='Monstera')
|
||||||
|
deliciosa_sci = PlantScientificName(name='Monstera deliciosa')
|
||||||
|
aurea_sci = PlantScientificName(name='Monstera aurea')
|
||||||
|
db.session.add_all([monstera_common, deliciosa_sci, aurea_sci])
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# PLANTS
|
||||||
|
parent_plant = Plant(
|
||||||
|
common_name_id=monstera_common.id,
|
||||||
|
scientific_name_id=deliciosa_sci.id,
|
||||||
|
created_by_user_id=admin.id
|
||||||
|
)
|
||||||
|
child_plant = Plant(
|
||||||
|
common_name_id=monstera_common.id,
|
||||||
|
scientific_name_id=aurea_sci.id,
|
||||||
|
created_by_user_id=user.id,
|
||||||
|
parent_id=1
|
||||||
|
)
|
||||||
|
db.session.add_all([parent_plant, child_plant])
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
# LINEAGE & OWNERSHIP
|
||||||
|
db.session.add(PlantLineage(parent_plant_id=parent_plant.id, child_plant_id=child_plant.id))
|
||||||
|
db.session.add(PlantOwnershipLog(
|
||||||
|
plant_id=child_plant.id,
|
||||||
|
user_id=user.id,
|
||||||
|
date_acquired=datetime.utcnow() - timedelta(days=20)
|
||||||
|
))
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# UPDATE & MEDIA
|
||||||
|
update = PlantUpdate(
|
||||||
|
plant_id=child_plant.id,
|
||||||
|
update_type='Repotted',
|
||||||
|
description='Moved to a 6" pot with a new moss pole.',
|
||||||
|
)
|
||||||
|
db.session.add(update)
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
db.session.add(Media(
|
||||||
|
file_url='uploads/demo_plant_update.jpg',
|
||||||
|
update_id=update.id,
|
||||||
|
caption='Freshly repotted.'
|
||||||
|
))
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# SUBMISSION & IMAGE
|
||||||
|
submission = Submission(
|
||||||
|
user_id=user.id,
|
||||||
|
plant_id=child_plant.id,
|
||||||
|
common_name='Monstera',
|
||||||
|
scientific_name='Monstera aurea',
|
||||||
|
price=120.00,
|
||||||
|
source='Etsy',
|
||||||
|
height=45,
|
||||||
|
width=30,
|
||||||
|
leaf_count=5,
|
||||||
|
potting_mix='2:1:1 bark:pumice:coco',
|
||||||
|
container_size='6"',
|
||||||
|
health_status='Healthy',
|
||||||
|
notes='Some minor yellowing on one leaf.'
|
||||||
|
)
|
||||||
|
db.session.add(submission)
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
image = SubmissionImage(
|
||||||
|
submission_id=submission.id,
|
||||||
|
file_path='uploads/demo_submission.jpg',
|
||||||
|
is_visible=True
|
||||||
|
)
|
||||||
|
db.session.add(image)
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
db.session.add_all([
|
||||||
|
ImageHeart(user_id=admin.id, submission_image_id=image.id),
|
||||||
|
FeaturedImage(submission_image_id=image.id, override_text='Gorgeous coloration', is_featured=True)
|
||||||
|
])
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
if not auto:
|
||||||
|
click.echo("🎉 Demo data seeded successfully.")
|
||||||
|
@ -1,15 +1,27 @@
|
|||||||
from app import db
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from app import db
|
||||||
|
|
||||||
|
|
||||||
class GrowLog(db.Model):
|
class GrowLog(db.Model):
|
||||||
__tablename__ = 'grow_logs'
|
__tablename__ = 'grow_logs'
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
plant_id = db.Column(db.Integer, db.ForeignKey('plants.id'), nullable=False)
|
plant_id = db.Column(db.Integer, db.ForeignKey('plants.id'), nullable=False)
|
||||||
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
|
title = db.Column(db.String(255), nullable=False)
|
||||||
event_type = db.Column(db.String(64), nullable=False)
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
note = db.Column(db.Text)
|
|
||||||
|
|
||||||
media = db.relationship("Media", backref="growlog", lazy=True)
|
updates = db.relationship('PlantUpdate', backref='grow_log', lazy=True)
|
||||||
|
|
||||||
|
|
||||||
|
class PlantUpdate(db.Model):
|
||||||
|
__tablename__ = 'plant_updates'
|
||||||
|
__table_args__ = {'extend_existing': True}
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
plant_id = db.Column(db.Integer, db.ForeignKey('plants.id'), nullable=False)
|
||||||
|
growlog_id = db.Column(db.Integer, db.ForeignKey('grow_logs.id'), nullable=True)
|
||||||
|
update_type = db.Column(db.String(50), nullable=False)
|
||||||
|
description = db.Column(db.Text, nullable=True)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
media_items = db.relationship('Media', back_populates='update', lazy=True)
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<GrowLog {self.event_type} @ {self.timestamp}>"
|
|
@ -1,12 +1,40 @@
|
|||||||
from app import db
|
from app import db
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
class Media(db.Model):
|
class Media(db.Model):
|
||||||
__tablename__ = 'media'
|
__tablename__ = 'media'
|
||||||
|
__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)
|
||||||
|
|
||||||
plant_id = db.Column(db.Integer, db.ForeignKey('plants.id'), nullable=True)
|
plant_id = db.Column(db.Integer, db.ForeignKey('plants.id'), nullable=True)
|
||||||
growlog_id = db.Column(db.Integer, db.ForeignKey('grow_logs.id'), nullable=True)
|
growlog_id = db.Column(db.Integer, db.ForeignKey('grow_logs.id'), nullable=True)
|
||||||
caption = db.Column(db.String(255), nullable=True)
|
update_id = db.Column(db.Integer, db.ForeignKey('plant_updates.id'), nullable=True)
|
||||||
|
|
||||||
|
caption = db.Column(db.String(255), nullable=True)
|
||||||
|
|
||||||
|
# Relationship to PlantUpdate
|
||||||
|
update = db.relationship('PlantUpdate', back_populates='media_items', lazy=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ImageHeart(db.Model):
|
||||||
|
__tablename__ = 'image_hearts'
|
||||||
|
__table_args__ = {'extend_existing': True}
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||||
|
submission_image_id = db.Column(db.Integer, db.ForeignKey('submission_images.id'), nullable=False)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
|
||||||
|
class FeaturedImage(db.Model):
|
||||||
|
__tablename__ = 'featured_images'
|
||||||
|
__table_args__ = {'extend_existing': True}
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
submission_image_id = db.Column(db.Integer, db.ForeignKey('submission_images.id'), nullable=False)
|
||||||
|
override_text = db.Column(db.String(255), nullable=True)
|
||||||
|
is_featured = db.Column(db.Boolean, default=True)
|
||||||
|
@ -1,42 +1,5 @@
|
|||||||
import os
|
from flask import Blueprint
|
||||||
import uuid
|
|
||||||
from flask import Blueprint, render_template, redirect, url_for, request, current_app, flash
|
|
||||||
from flask_login import login_required
|
|
||||||
from werkzeug.utils import secure_filename
|
|
||||||
from app import db
|
|
||||||
from .models import Media
|
|
||||||
from .forms import MediaUploadForm
|
|
||||||
|
|
||||||
bp = Blueprint('media', __name__, template_folder='templates')
|
media_bp = Blueprint('media', __name__)
|
||||||
|
|
||||||
@bp.route('/media/upload', methods=['GET', 'POST'])
|
# Add routes here as needed; do NOT define models here.
|
||||||
@login_required
|
|
||||||
def upload_media():
|
|
||||||
form = MediaUploadForm()
|
|
||||||
if form.validate_on_submit():
|
|
||||||
file = form.image.data
|
|
||||||
filename = f"{uuid.uuid4().hex}_{secure_filename(file.filename)}"
|
|
||||||
upload_path = os.path.join(current_app.config['UPLOAD_FOLDER'], filename)
|
|
||||||
file.save(upload_path)
|
|
||||||
media = Media(
|
|
||||||
file_url=filename,
|
|
||||||
caption=form.caption.data,
|
|
||||||
plant_id=form.plant_id.data or None,
|
|
||||||
growlog_id=form.growlog_id.data or None
|
|
||||||
)
|
|
||||||
db.session.add(media)
|
|
||||||
db.session.commit()
|
|
||||||
flash("Image uploaded successfully.", "success")
|
|
||||||
return redirect(url_for('media.upload_media'))
|
|
||||||
return render_template('media/upload.html', form=form)
|
|
||||||
|
|
||||||
@bp.route('/media')
|
|
||||||
@login_required
|
|
||||||
def list_media():
|
|
||||||
images = Media.query.order_by(Media.uploaded_at.desc()).all()
|
|
||||||
return render_template('media/list.html', images=images)
|
|
||||||
|
|
||||||
@bp.route('/media/files/<filename>')
|
|
||||||
def media_file(filename):
|
|
||||||
from flask import send_from_directory
|
|
||||||
return send_from_directory(current_app.config['UPLOAD_FOLDER'], filename)
|
|
||||||
|
@ -1,39 +1,31 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from app import db
|
from app import db
|
||||||
|
from plugins.search.models import Tag, plant_tags
|
||||||
|
from plugins.growlog.models import PlantUpdate
|
||||||
|
|
||||||
|
class PlantCommonName(db.Model):
|
||||||
class Submission(db.Model):
|
__tablename__ = 'plants_common'
|
||||||
__tablename__ = 'submission'
|
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
user_id = db.Column(
|
name = db.Column(db.String(120), unique=True, nullable=False)
|
||||||
db.Integer,
|
|
||||||
db.ForeignKey('users.id', name='fk_submission_user_id'),
|
|
||||||
nullable=False
|
|
||||||
)
|
|
||||||
common_name = db.Column(db.String(120), nullable=False)
|
|
||||||
scientific_name = db.Column(db.String(120))
|
|
||||||
price = db.Column(db.Float, nullable=False)
|
|
||||||
source = db.Column(db.String(120))
|
|
||||||
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
|
|
||||||
height = db.Column(db.Float)
|
|
||||||
width = db.Column(db.Float)
|
|
||||||
leaf_count = db.Column(db.Integer)
|
|
||||||
potting_mix = db.Column(db.String(255))
|
|
||||||
container_size = db.Column(db.String(120))
|
|
||||||
health_status = db.Column(db.String(50))
|
|
||||||
notes = db.Column(db.Text)
|
|
||||||
plant_id = db.Column(db.Integer)
|
|
||||||
images = db.relationship('SubmissionImage', backref='submission', lazy=True)
|
|
||||||
|
|
||||||
|
|
||||||
class SubmissionImage(db.Model):
|
|
||||||
__tablename__ = 'submission_images'
|
|
||||||
|
|
||||||
|
class PlantScientificName(db.Model):
|
||||||
|
__tablename__ = 'plants_scientific'
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
submission_id = db.Column(db.Integer, db.ForeignKey('submission.id'), nullable=False)
|
name = db.Column(db.String(255), unique=True, nullable=False)
|
||||||
file_path = db.Column(db.String(255), nullable=False)
|
|
||||||
|
|
||||||
|
class PlantLineage(db.Model):
|
||||||
|
__tablename__ = 'plant_lineage'
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
parent_plant_id = db.Column(db.Integer, db.ForeignKey('plants.id'), nullable=False)
|
||||||
|
child_plant_id = db.Column(db.Integer, db.ForeignKey('plants.id'), nullable=False)
|
||||||
|
|
||||||
|
class PlantOwnershipLog(db.Model):
|
||||||
|
__tablename__ = 'plant_ownership_log'
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
plant_id = db.Column(db.Integer, db.ForeignKey('plants.id'), nullable=False)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||||
|
date_acquired = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
date_relinquished = db.Column(db.DateTime, nullable=True)
|
||||||
|
|
||||||
class Plant(db.Model):
|
class Plant(db.Model):
|
||||||
__tablename__ = 'plants'
|
__tablename__ = 'plants'
|
||||||
@ -46,68 +38,7 @@ class Plant(db.Model):
|
|||||||
created_by_user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
|
created_by_user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
updates = db.relationship('PlantUpdate', backref='plant', lazy=True)
|
updates = db.relationship('PlantUpdate', backref='growlog', lazy=True)
|
||||||
lineage = db.relationship('PlantLineage', backref='child', lazy=True,
|
lineage = db.relationship('PlantLineage', backref='child', lazy=True, foreign_keys='PlantLineage.child_plant_id')
|
||||||
foreign_keys='PlantLineage.child_plant_id')
|
tags = db.relationship('Tag', secondary=plant_tags, backref='plants')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class PlantCommonName(db.Model):
|
|
||||||
__tablename__ = 'plants_common'
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
|
||||||
name = db.Column(db.String(120), unique=True, nullable=False)
|
|
||||||
|
|
||||||
|
|
||||||
class PlantScientificName(db.Model):
|
|
||||||
__tablename__ = 'plants_scientific'
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
|
||||||
name = db.Column(db.String(255), unique=True, nullable=False)
|
|
||||||
|
|
||||||
|
|
||||||
class PlantLineage(db.Model):
|
|
||||||
__tablename__ = 'plant_lineage'
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
|
||||||
parent_plant_id = db.Column(db.Integer, db.ForeignKey('plants.id'), nullable=False)
|
|
||||||
child_plant_id = db.Column(db.Integer, db.ForeignKey('plants.id'), nullable=False)
|
|
||||||
|
|
||||||
|
|
||||||
class PlantOwnershipLog(db.Model):
|
|
||||||
__tablename__ = 'plant_ownership_log'
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
|
||||||
plant_id = db.Column(db.Integer, db.ForeignKey('plants.id'), nullable=False)
|
|
||||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
|
||||||
date_acquired = db.Column(db.DateTime, default=datetime.utcnow)
|
|
||||||
date_relinquished = db.Column(db.DateTime, nullable=True)
|
|
||||||
|
|
||||||
|
|
||||||
class PlantUpdate(db.Model):
|
|
||||||
__tablename__ = 'plant_updates'
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
|
||||||
plant_id = db.Column(db.Integer, db.ForeignKey('plants.id'), nullable=False)
|
|
||||||
update_type = db.Column(db.String(100))
|
|
||||||
description = db.Column(db.Text)
|
|
||||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
|
||||||
images = db.relationship('UpdateImage', backref='update', lazy=True)
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateImage(db.Model):
|
|
||||||
__tablename__ = 'update_images'
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
|
||||||
update_id = db.Column(db.Integer, db.ForeignKey('plant_updates.id'), nullable=False)
|
|
||||||
file_path = db.Column(db.String(255), nullable=False)
|
|
||||||
|
|
||||||
|
|
||||||
class ImageHeart(db.Model):
|
|
||||||
__tablename__ = 'image_hearts'
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
|
||||||
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)
|
|
||||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
|
||||||
|
|
||||||
|
|
||||||
class FeaturedImage(db.Model):
|
|
||||||
__tablename__ = 'featured_images'
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
|
||||||
submission_image_id = db.Column(db.Integer, db.ForeignKey('submission_images.id'), nullable=False)
|
|
||||||
override_text = db.Column(db.String(255), nullable=True)
|
|
||||||
is_featured = db.Column(db.Boolean, default=True)
|
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
from app import db
|
from app import db
|
||||||
|
|
||||||
class Tag(db.Model):
|
plant_tags = db.Table(
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
'plant_tags',
|
||||||
name = db.Column(db.String(64), unique=True, nullable=False)
|
db.Column('plant_id', db.Integer, db.ForeignKey('plants.id'), primary_key=True),
|
||||||
|
db.Column('tag_id', db.Integer, db.ForeignKey('tags.id'), primary_key=True)
|
||||||
|
)
|
||||||
|
|
||||||
def __repr__(self):
|
class Tag(db.Model):
|
||||||
return f"<Tag {self.name}>"
|
__tablename__ = 'tags'
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
name = db.Column(db.String(100), unique=True, nullable=False)
|
||||||
|
@ -1,2 +1,5 @@
|
|||||||
def register():
|
def register(app):
|
||||||
from . import routes
|
"""Register submission routes & blueprint."""
|
||||||
|
from .routes import bp as submissions_bp
|
||||||
|
app.register_blueprint(submissions_bp)
|
||||||
|
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from app import db
|
from app import db
|
||||||
|
|
||||||
|
|
||||||
class Submission(db.Model):
|
class Submission(db.Model):
|
||||||
__tablename__ = 'submissions'
|
__tablename__ = 'submissions'
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||||
plant_id = db.Column(db.Integer, db.ForeignKey('plants.id'), nullable=True)
|
plant_id = db.Column(db.Integer, db.ForeignKey('plants.id'), nullable=True)
|
||||||
|
|
||||||
common_name = db.Column(db.String(120), nullable=False)
|
common_name = db.Column(db.String(120), nullable=False)
|
||||||
scientific_name = db.Column(db.String(120))
|
scientific_name = db.Column(db.String(120))
|
||||||
price = db.Column(db.Float, nullable=False)
|
price = db.Column(db.Float, nullable=False)
|
||||||
source = db.Column(db.String(120))
|
source = db.Column(db.String(120))
|
||||||
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
|
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
height = db.Column(db.Float)
|
height = db.Column(db.Float)
|
||||||
width = db.Column(db.Float)
|
width = db.Column(db.Float)
|
||||||
leaf_count = db.Column(db.Integer)
|
leaf_count = db.Column(db.Integer)
|
||||||
@ -19,6 +22,8 @@ class Submission(db.Model):
|
|||||||
container_size = db.Column(db.String(120))
|
container_size = db.Column(db.String(120))
|
||||||
health_status = db.Column(db.String(50))
|
health_status = db.Column(db.String(50))
|
||||||
notes = db.Column(db.Text)
|
notes = db.Column(db.Text)
|
||||||
|
|
||||||
|
# Image references via SubmissionImage table
|
||||||
images = db.relationship('SubmissionImage', backref='submission', lazy=True)
|
images = db.relationship('SubmissionImage', backref='submission', lazy=True)
|
||||||
|
|
||||||
|
|
||||||
@ -29,21 +34,3 @@ class SubmissionImage(db.Model):
|
|||||||
submission_id = db.Column(db.Integer, db.ForeignKey('submissions.id'), nullable=False)
|
submission_id = db.Column(db.Integer, db.ForeignKey('submissions.id'), nullable=False)
|
||||||
file_path = db.Column(db.String(255), nullable=False)
|
file_path = db.Column(db.String(255), nullable=False)
|
||||||
is_visible = db.Column(db.Boolean, default=True)
|
is_visible = db.Column(db.Boolean, default=True)
|
||||||
|
|
||||||
|
|
||||||
class ImageHeart(db.Model):
|
|
||||||
__tablename__ = 'image_hearts'
|
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
|
||||||
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)
|
|
||||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
|
||||||
|
|
||||||
|
|
||||||
class FeaturedImage(db.Model):
|
|
||||||
__tablename__ = 'featured_images'
|
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
|
||||||
submission_image_id = db.Column(db.Integer, db.ForeignKey('submission_images.id'), nullable=False)
|
|
||||||
override_text = db.Column(db.String(255), nullable=True)
|
|
||||||
is_featured = db.Column(db.Boolean, default=True)
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "submission",
|
"name": "submission",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Plugin to handle user-submitted plant data and images.",
|
"description": "Plugin to handle user-submitted plant data and images."
|
||||||
"entry_point": "register"
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user