more crap

This commit is contained in:
2025-05-26 22:25:39 -05:00
parent eb111dde14
commit 7bff7f0bd3
18 changed files with 265 additions and 225 deletions

View File

@ -1,8 +1,7 @@
# Environment toggles
USE_REMOTE_MYSQL=0
ENABLE_DB_PRELOAD=0
ENABLE_DB_SEEDING=1
SEED_EXTRA_DATA=false
DOCKER_ENV=development
UPLOAD_FOLDER=app/static/uploads
SECRET_KEY=supersecretplantappkey

View File

@ -4,15 +4,14 @@ WORKDIR /app
COPY . .
# Required for mysqlclient + netcat wait
RUN apt-get update && apt-get install -y \
gcc \
default-libmysqlclient-dev \
pkg-config \
netcat-openbsd \
curl \
&& rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get install -y gcc default-libmysqlclient-dev pkg-config netcat-openbsd curl && rm -rf /var/lib/apt/lists/*
RUN pip install --upgrade pip
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"]

View File

@ -55,7 +55,7 @@ status:
@docker ps --filter "name=$(PROJECT_NAME)-" --format "table {{.Names}}\t{{.Status}}"
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:
@docker exec -it $$(docker ps -qf "name=$(PROJECT_NAME)-web") bash

View File

@ -1,19 +1,27 @@
import os
import json
import importlib.util
import importlib
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
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()
migrate = Migrate()
login_manager = LoginManager()
csrf = CSRFProtect()
def create_app():
app = Flask(__name__)
app.config.from_object('app.config.Config')
csrf.init_app(app)
# Initialize core extensions
db.init_app(app)
@ -28,7 +36,6 @@ def create_app():
# Plugin auto-loader
plugin_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'plugins'))
for plugin in os.listdir(plugin_path):
# Skip folders that end with `.noload`
if plugin.endswith('.noload'):
print(f"[⏭] Skipping plugin '{plugin}' (marked as .noload)")
continue
@ -49,13 +56,23 @@ def create_app():
except Exception as 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')
if os.path.isfile(init_file):
try:
cli_module = importlib.import_module(f"plugins.{plugin}")
if hasattr(cli_module, 'register_cli'):
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:
print(f"[⚠️] Failed to load CLI from plugin '{plugin}': {e}")

View File

@ -22,6 +22,5 @@ class Config:
SQLALCHEMY_TRACK_MODIFICATIONS = False
# Optional toggles
ENABLE_DB_PRELOAD = os.environ.get('ENABLE_DB_PRELOAD', '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')

View File

@ -9,7 +9,6 @@ services:
- FLASK_APP=app
- FLASK_ENV=development
- USE_REMOTE_MYSQL=${USE_REMOTE_MYSQL}
- ENABLE_DB_PRELOAD=${ENABLE_DB_PRELOAD}
- ENABLE_DB_SEEDING=${ENABLE_DB_SEEDING}
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_USER=${MYSQL_USER}
@ -46,16 +45,10 @@ services:
echo '[✔] Running DB migrations...'
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
echo '[🌱] Seeding admin user...'
flask seed-admin
if [ \"$ENABLE_DB_SEEDING\" = \"1\" ]; then
echo '[🌱] Seeding Data...'
flask preload-data
else
echo '[⚠️] DB seeding skipped by config.'
fi

21
entrypoint.sh Normal file
View 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 "$@"

BIN
files.zip

Binary file not shown.

View File

@ -1,10 +1,6 @@
from .seed import preload_data, seed_admin
import click
from flask import Flask
cli_commands = [
preload_data,
seed_admin
]
def register_cli(app):
for command in cli_commands:
app.cli.add_command(command)
def register_cli(app: Flask):
from .seed import preload_data_cli
app.cli.add_command(preload_data_cli)

View File

@ -1,32 +1,121 @@
import click
from flask.cli import with_appcontext
from plugins.plant.models import Plant
from plugins.auth.models import User
from datetime import datetime, timedelta
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
def preload_data():
click.echo("Preloading data...")
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_cli():
preload_data(auto=False)
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')
@with_appcontext
def seed_admin():
click.echo("Seeding admin user...")
if not User.query.filter_by(email='admin@example.com').first():
user = User(email='admin@example.com', role='admin', is_verified=True)
user.set_password('password') # Make sure this method exists in your model
db.session.add(user)
db.session.commit()
click.echo("✅ Admin user created.")
else:
click.echo(" Admin user already exists.")
if not auto:
click.echo("🌱 Seeding demo data...")
# USERS
admin = User(email='admin@example.com', role='admin', is_verified=True)
admin.set_password('password123')
user = User(email='user@example.com', role='user', is_verified=True)
user.set_password('password123')
db.session.add_all([admin, user])
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.")

View File

@ -1,15 +1,27 @@
from app import db
from datetime import datetime
from app import db
class GrowLog(db.Model):
__tablename__ = 'grow_logs'
id = db.Column(db.Integer, primary_key=True)
plant_id = db.Column(db.Integer, db.ForeignKey('plants.id'), nullable=False)
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
event_type = db.Column(db.String(64), nullable=False)
note = db.Column(db.Text)
title = db.Column(db.String(255), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
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}>"

View File

@ -1,12 +1,40 @@
from app import db
from datetime import datetime
class Media(db.Model):
__tablename__ = 'media'
__table_args__ = {'extend_existing': True}
id = db.Column(db.Integer, primary_key=True)
file_url = db.Column(db.String(256), nullable=False)
uploaded_at = db.Column(db.DateTime, default=datetime.utcnow)
plant_id = db.Column(db.Integer, db.ForeignKey('plants.id'), nullable=True)
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)

View File

@ -1,42 +1,5 @@
import os
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
from flask import Blueprint
bp = Blueprint('media', __name__, template_folder='templates')
media_bp = Blueprint('media', __name__)
@bp.route('/media/upload', methods=['GET', 'POST'])
@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)
# Add routes here as needed; do NOT define models here.

View File

@ -1,39 +1,31 @@
from datetime import datetime
from app import db
from plugins.search.models import Tag, plant_tags
from plugins.growlog.models import PlantUpdate
class Submission(db.Model):
__tablename__ = 'submission'
class PlantCommonName(db.Model):
__tablename__ = 'plants_common'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(
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'
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)
submission_id = db.Column(db.Integer, db.ForeignKey('submission.id'), nullable=False)
file_path = db.Column(db.String(255), nullable=False)
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 Plant(db.Model):
__tablename__ = 'plants'
@ -46,68 +38,7 @@ class Plant(db.Model):
created_by_user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
# Relationships
updates = db.relationship('PlantUpdate', backref='plant', lazy=True)
lineage = db.relationship('PlantLineage', backref='child', lazy=True,
foreign_keys='PlantLineage.child_plant_id')
updates = db.relationship('PlantUpdate', backref='growlog', lazy=True)
lineage = db.relationship('PlantLineage', backref='child', lazy=True, foreign_keys='PlantLineage.child_plant_id')
tags = db.relationship('Tag', secondary=plant_tags, backref='plants')
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)

View File

@ -1,8 +1,12 @@
from app import db
class Tag(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True, nullable=False)
plant_tags = db.Table(
'plant_tags',
db.Column('plant_id', db.Integer, db.ForeignKey('plants.id'), primary_key=True),
db.Column('tag_id', db.Integer, db.ForeignKey('tags.id'), primary_key=True)
)
def __repr__(self):
return f"<Tag {self.name}>"
class Tag(db.Model):
__tablename__ = 'tags'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)

View File

@ -1,2 +1,5 @@
def register():
from . import routes
def register(app):
"""Register submission routes & blueprint."""
from .routes import bp as submissions_bp
app.register_blueprint(submissions_bp)

View File

@ -1,17 +1,20 @@
from datetime import datetime
from app import db
class Submission(db.Model):
__tablename__ = 'submissions'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
plant_id = db.Column(db.Integer, db.ForeignKey('plants.id'), nullable=True)
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)
@ -19,6 +22,8 @@ class Submission(db.Model):
container_size = db.Column(db.String(120))
health_status = db.Column(db.String(50))
notes = db.Column(db.Text)
# Image references via SubmissionImage table
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)
file_path = db.Column(db.String(255), nullable=False)
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)

View File

@ -1,6 +1,5 @@
{
"name": "submission",
"version": "1.0.0",
"description": "Plugin to handle user-submitted plant data and images.",
"entry_point": "register"
"description": "Plugin to handle user-submitted plant data and images."
}