From 6de9729329a0574dcc38f1b77a7ef2bebbbfadec Mon Sep 17 00:00:00 2001 From: Bryson Date: Sat, 24 May 2025 03:23:15 -0500 Subject: [PATCH] More files --- Makefile | 2 +- app/__init__.py | 50 ++++++-- plugins/auth/models.py | 117 ++---------------- plugins/auth/routes.py | 31 ++++- plugins/auth/templates/auth/login.html | 2 +- plugins/auth/templates/auth/register.html | 23 ++-- plugins/cli/__init__.py | 12 +- plugins/cli/preload.py | 79 ------------ plugins/cli/routes.py | 20 --- plugins/cli/seed.py | 48 +++---- plugins/core_ui/__init__.py | 1 - plugins/core_ui/routes.py | 15 +++ .../templates/core_ui/admin_dashboard.html | 11 ++ plugins/core_ui/templates/core_ui/base.html | 69 +++++++++++ plugins/core_ui/templates/core_ui/home.html | 6 + .../growlog/templates/growlog/log_form.html | 2 +- .../growlog/templates/growlog/log_list.html | 2 +- plugins/media/templates/media/list.html | 2 +- plugins/media/templates/media/upload.html | 2 +- plugins/plant/models.py | 110 ++++++++++++++++ plugins/search/templates/search/search.html | 2 +- .../templates/search/search_results.html | 2 +- .../search/templates/search/search_tags.html | 2 +- 23 files changed, 338 insertions(+), 272 deletions(-) delete mode 100644 plugins/cli/preload.py delete mode 100644 plugins/cli/routes.py create mode 100644 plugins/core_ui/routes.py create mode 100644 plugins/core_ui/templates/core_ui/admin_dashboard.html create mode 100644 plugins/core_ui/templates/core_ui/base.html create mode 100644 plugins/core_ui/templates/core_ui/home.html diff --git a/Makefile b/Makefile index 3522504..e69915f 100644 --- a/Makefile +++ b/Makefile @@ -80,7 +80,7 @@ test: wait: @echo "[⏳] Waiting for web container to be healthy..." - @timeout 90 bash -c '\ + @timeout 30 bash -c '\ WEB_CONTAINER=$$($(DOCKER_COMPOSE) ps -q web); \ if [ -z "$$WEB_CONTAINER" ]; then \ echo "[❌] Could not detect web container!"; \ diff --git a/app/__init__.py b/app/__init__.py index 3585d46..957489b 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,5 +1,6 @@ import os import importlib.util +import importlib from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate @@ -18,7 +19,7 @@ def create_app(): db.init_app(app) migrate.init_app(app, db) login_manager.init_app(app) - login_manager.login_view = 'auth.login' # Redirect for @login_required + login_manager.login_view = 'auth.login' # Register error handlers from .errors import bp as errors_bp @@ -27,18 +28,41 @@ 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): - full_path = os.path.join(plugin_path, plugin, 'routes.py') - if os.path.isfile(full_path): - spec = importlib.util.spec_from_file_location(f"plugins.{plugin}.routes", full_path) - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - if hasattr(mod, 'bp'): - app.register_blueprint(mod.bp) - - # Register CLI commands if the plugin has any - if hasattr(mod, 'cli_commands'): - for command in mod.cli_commands: - app.cli.add_command(command) + # Skip folders that end with `.noload` + if plugin.endswith('.noload'): + print(f"[⏭] Skipping plugin '{plugin}' (marked as .noload)") + continue + + plugin_dir = os.path.join(plugin_path, plugin) + if not os.path.isdir(plugin_dir): + continue + + # Register routes + route_file = os.path.join(plugin_dir, 'routes.py') + if os.path.isfile(route_file): + try: + spec = importlib.util.spec_from_file_location(f"plugins.{plugin}.routes", route_file) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + if hasattr(mod, 'bp'): + app.register_blueprint(mod.bp) + except Exception as e: + print(f"[⚠️] Failed to load routes from plugin '{plugin}': {e}") + + # Register CLI commands + 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) + except Exception as e: + print(f"[⚠️] Failed to load CLI from plugin '{plugin}': {e}") + + @app.context_processor + def inject_current_year(): + from datetime import datetime + return {'current_year': datetime.now().year} return app diff --git a/plugins/auth/models.py b/plugins/auth/models.py index b0eabd6..c8dfc82 100644 --- a/plugins/auth/models.py +++ b/plugins/auth/models.py @@ -1,3 +1,4 @@ +from werkzeug.security import generate_password_hash, check_password_hash from flask_login import UserMixin from datetime import datetime from app import db @@ -7,7 +8,7 @@ class User(db.Model, UserMixin): id = db.Column(db.Integer, primary_key=True) email = db.Column(db.String(120), unique=True, nullable=False) - password_hash = db.Column(db.String(128), nullable=False) + password_hash = db.Column(db.Text, nullable=False) role = db.Column(db.String(50), default='user') is_verified = db.Column(db.Boolean, default=False) excluded_from_analytics = db.Column(db.Boolean, default=False) @@ -15,113 +16,9 @@ class User(db.Model, UserMixin): # Optional: relationship to submissions submissions = db.relationship('Submission', backref='user', lazy=True) + + def set_password(self, password): + self.password_hash = generate_password_hash(password) - -class Submission(db.Model): - __tablename__ = 'submission' - - 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' - - 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) - - -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 Plant(db.Model): - __tablename__ = 'plants' - id = db.Column(db.Integer, primary_key=True) - common_name_id = db.Column(db.Integer, db.ForeignKey('plants_common.id')) - scientific_name_id = db.Column(db.Integer, db.ForeignKey('plants_scientific.id')) - parent_id = db.Column(db.Integer, db.ForeignKey('plants.id'), nullable=True) - is_dead = db.Column(db.Boolean, default=False) - date_added = db.Column(db.DateTime, default=datetime.utcnow) - created_by_user_id = db.Column(db.Integer, db.ForeignKey('users.id')) - - # Relationships - updates = db.relationship('PlantUpdate', backref='plant', lazy=True) - lineage = db.relationship('PlantLineage', backref='child', lazy=True, - foreign_keys='PlantLineage.child_plant_id') - - -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) + def check_password(self, password): + return check_password_hash(self.password_hash, password) diff --git a/plugins/auth/routes.py b/plugins/auth/routes.py index 379aa21..fbed437 100644 --- a/plugins/auth/routes.py +++ b/plugins/auth/routes.py @@ -4,9 +4,9 @@ from werkzeug.security import check_password_hash from app import db from .models import User -auth = Blueprint('auth', __name__) +bp = Blueprint('auth', __name__, template_folder='templates') -@auth.route('/auth/login', methods=['GET', 'POST']) +@bp.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': email = request.form['email'] @@ -15,14 +15,33 @@ def login(): if user and check_password_hash(user.password_hash, password): login_user(user) flash('Logged in successfully.', 'success') - return redirect(url_for('core.user_dashboard')) + return redirect(url_for('core_ui.home')) else: flash('Invalid credentials.', 'danger') - return render_template('login.html') + return render_template('auth/login.html') -@auth.route('/auth/logout') +@bp.route('/logout') @login_required def logout(): logout_user() flash('Logged out.', 'info') - return redirect(url_for('core.index')) + return redirect(url_for('core_ui.home')) + +@bp.route('/register', methods=['GET', 'POST']) +def register(): + if request.method == 'POST': + email = request.form['email'] + password = request.form['password'] + + existing_user = User.query.filter_by(email=email).first() + if existing_user: + flash('Email already registered.', 'warning') + else: + user = User(email=email) + user.set_password(password) + db.session.add(user) + db.session.commit() + flash('Account created. You can now log in.', 'success') + return redirect(url_for('auth.login')) + + return render_template('auth/register.html') diff --git a/plugins/auth/templates/auth/login.html b/plugins/auth/templates/auth/login.html index 25f1a7e..ef842e8 100644 --- a/plugins/auth/templates/auth/login.html +++ b/plugins/auth/templates/auth/login.html @@ -1,4 +1,4 @@ -{% extends 'base.html' %} +{% extends 'core_ui/base.html' %} {% block content %}

Login

diff --git a/plugins/auth/templates/auth/register.html b/plugins/auth/templates/auth/register.html index a85a360..f5e33a1 100644 --- a/plugins/auth/templates/auth/register.html +++ b/plugins/auth/templates/auth/register.html @@ -1,15 +1,16 @@ -{% extends 'base.html' %} +{% extends 'core_ui/base.html' %} +{% block title %}Register{% endblock %} {% block content %}

Register

- -
- - -
-
- - -
- + +
+ + +
+
+ + +
+
{% endblock %} diff --git a/plugins/cli/__init__.py b/plugins/cli/__init__.py index e25577c..909dddd 100644 --- a/plugins/cli/__init__.py +++ b/plugins/cli/__init__.py @@ -1,2 +1,10 @@ -from .seed import seed_admin -from .preload import preload_data +from .seed import preload_data, seed_admin + +cli_commands = [ + preload_data, + seed_admin +] + +def register_cli(app): + for command in cli_commands: + app.cli.add_command(command) diff --git a/plugins/cli/preload.py b/plugins/cli/preload.py deleted file mode 100644 index 458f495..0000000 --- a/plugins/cli/preload.py +++ /dev/null @@ -1,79 +0,0 @@ -import click -from flask.cli import with_appcontext -from datetime import datetime -from app import db -from app.models.user import ( - User, Plant, PlantCommonName, PlantScientificName, Submission, SubmissionImage -) - -@click.command("preload-data") -@with_appcontext -def preload_data(): - click.echo("[📦] Preloading demo data...") - - if not User.query.filter_by(email="demo@example.com").first(): - demo_user = User( - email="demo@example.com", - password_hash="fakehash123", # you can hash a real one if needed - role="user", - is_verified=True - ) - db.session.add(demo_user) - db.session.commit() - - common_entries = { - "Monstera Albo": "Monstera deliciosa 'Albo-Variegata'", - "Philodendron Pink Princess": "Philodendron erubescens", - "Cebu Blue Pothos": "Epipremnum pinnatum" - } - - for common_name, sci_name in common_entries.items(): - common = PlantCommonName.query.filter_by(name=common_name).first() - if not common: - common = PlantCommonName(name=common_name) - db.session.add(common) - - scientific = PlantScientificName.query.filter_by(name=sci_name).first() - if not scientific: - scientific = PlantScientificName(name=sci_name) - db.session.add(scientific) - - db.session.commit() - - common_map = {c.name: c.id for c in PlantCommonName.query.all()} - scientific_map = {s.name: s.id for s in PlantScientificName.query.all()} - demo_user = User.query.filter_by(email="demo@example.com").first() - - plants = [] - for i in range(1, 4): - plant = Plant( - common_name_id=common_map["Monstera Albo"], - scientific_name_id=scientific_map["Monstera deliciosa 'Albo-Variegata'"], - created_by_user_id=demo_user.id - ) - db.session.add(plant) - plants.append(plant) - - db.session.commit() - - for plant in plants: - for i in range(2): - submission = Submission( - user_id=demo_user.id, - plant_id=plant.id, - common_name="Monstera Albo", - scientific_name="Monstera deliciosa 'Albo-Variegata'", - price=85.0 + i * 15, - source="Etsy", - height=12 + i * 2, - width=10 + i, - potting_mix="Pumice:Bark:Coco 2:1:1", - container_size="4 inch", - health_status="Healthy", - notes="Good variegation", - timestamp=datetime.utcnow() - ) - db.session.add(submission) - - db.session.commit() - click.echo("[✅] Demo data loaded.") diff --git a/plugins/cli/routes.py b/plugins/cli/routes.py deleted file mode 100644 index f189df2..0000000 --- a/plugins/cli/routes.py +++ /dev/null @@ -1,20 +0,0 @@ -# plugins/cli/routes.py - -import click -from flask.cli import with_appcontext -from app.extensions import db -from plugins.plant.models import Plant - -@click.command('preload-data') -@with_appcontext -def preload_data(): - """Preloads plant data into the database.""" - if not Plant.query.first(): - db.session.add(Plant(name="Example Plant")) - db.session.commit() - click.echo("✅ Preloaded sample plant.") - else: - click.echo("ℹ️ Plant data already exists.") - -# Export command(s) so __init__.py can register them -cli_commands = [preload_data] diff --git a/plugins/cli/seed.py b/plugins/cli/seed.py index cea40b7..5a2989c 100644 --- a/plugins/cli/seed.py +++ b/plugins/cli/seed.py @@ -1,26 +1,32 @@ import click from flask.cli import with_appcontext -from werkzeug.security import generate_password_hash -from ..core.models import User -from .. import db +from plugins.plant.models import Plant +from plugins.auth.models import User +from app import db -@click.command("seed-admin") + +@click.command('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.") + + +@click.command('seed-admin') @with_appcontext def seed_admin(): - """Seed a default admin user if none exists.""" - admin_email = "admin@example.com" - admin_password = "admin123" - - if User.query.filter_by(email=admin_email).first(): - click.echo("[ℹ] Admin user already exists.") - return - - user = User( - email=admin_email, - password_hash=generate_password_hash(admin_password), - role="admin", - is_verified=True - ) - db.session.add(user) - db.session.commit() - click.echo(f"[✔] Created default admin: {admin_email}") + 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.") diff --git a/plugins/core_ui/__init__.py b/plugins/core_ui/__init__.py index e2c485a..e69de29 100644 --- a/plugins/core_ui/__init__.py +++ b/plugins/core_ui/__init__.py @@ -1 +0,0 @@ -# core_ui media patch \ No newline at end of file diff --git a/plugins/core_ui/routes.py b/plugins/core_ui/routes.py new file mode 100644 index 0000000..19d0c9c --- /dev/null +++ b/plugins/core_ui/routes.py @@ -0,0 +1,15 @@ +from flask import Blueprint, render_template +from flask_login import login_required, current_user + +bp = Blueprint('core_ui', __name__, template_folder='templates') + +@bp.route('/') +def home(): + return render_template('core_ui/home.html') + +@bp.route('/admin') +@login_required +def admin_dashboard(): + if current_user.role != 'admin': + return "Access denied", 403 + return render_template('core_ui/admin_dashboard.html') diff --git a/plugins/core_ui/templates/core_ui/admin_dashboard.html b/plugins/core_ui/templates/core_ui/admin_dashboard.html new file mode 100644 index 0000000..6cd35a0 --- /dev/null +++ b/plugins/core_ui/templates/core_ui/admin_dashboard.html @@ -0,0 +1,11 @@ +{% extends 'core_ui/base.html' %} +{% block title %}Admin Dashboard | Nature In Pots{% endblock %} +{% block content %} +

Admin Dashboard

+

Manage submissions, users, and plugin controls here.

+ +{% endblock %} diff --git a/plugins/core_ui/templates/core_ui/base.html b/plugins/core_ui/templates/core_ui/base.html new file mode 100644 index 0000000..eeb5b67 --- /dev/null +++ b/plugins/core_ui/templates/core_ui/base.html @@ -0,0 +1,69 @@ + + + + + {% block title %}Nature In Pots Community{% endblock %} + + + + + +
+ {% with messages = get_flashed_messages() %} + {% if messages %} +
+ {% for message in messages %} +
{{ message }}
+ {% endfor %} +
+ {% endif %} + {% endwith %} + {% block content %}{% endblock %} +
+ + + + diff --git a/plugins/core_ui/templates/core_ui/home.html b/plugins/core_ui/templates/core_ui/home.html new file mode 100644 index 0000000..b68933c --- /dev/null +++ b/plugins/core_ui/templates/core_ui/home.html @@ -0,0 +1,6 @@ +{% extends 'core_ui/base.html' %} +{% block title %}Home | Nature In Pots{% endblock %} +{% block content %} +

Welcome to Nature In Pots 🌿

+

This is the community hub for plant tracking, propagation history, and price sharing.

+{% endblock %} diff --git a/plugins/growlog/templates/growlog/log_form.html b/plugins/growlog/templates/growlog/log_form.html index 0f0424b..235dfa4 100644 --- a/plugins/growlog/templates/growlog/log_form.html +++ b/plugins/growlog/templates/growlog/log_form.html @@ -1,4 +1,4 @@ -{% extends 'base.html' %} +{% extends 'core_ui/base.html' %} {% block content %}

Add Log for Plant #{{ plant.id }}

diff --git a/plugins/growlog/templates/growlog/log_list.html b/plugins/growlog/templates/growlog/log_list.html index 6f812de..095df6b 100644 --- a/plugins/growlog/templates/growlog/log_list.html +++ b/plugins/growlog/templates/growlog/log_list.html @@ -1,5 +1,5 @@ {% import 'core_ui/_media_macros.html' as media %} -{% extends 'base.html' %} +{% extends 'core_ui/base.html' %} {% block content %}

Logs for Plant #{{ plant.id }}

Add New Log diff --git a/plugins/media/templates/media/list.html b/plugins/media/templates/media/list.html index af7bfb7..cac4ced 100644 --- a/plugins/media/templates/media/list.html +++ b/plugins/media/templates/media/list.html @@ -1,4 +1,4 @@ -{% extends 'base.html' %} +{% extends 'core_ui/base.html' %} {% block content %}

All Uploaded Media