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 %}
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 @@ + + + + +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 %}