More files
This commit is contained in:
2
Makefile
2
Makefile
@ -80,7 +80,7 @@ test:
|
|||||||
|
|
||||||
wait:
|
wait:
|
||||||
@echo "[⏳] Waiting for web container to be healthy..."
|
@echo "[⏳] Waiting for web container to be healthy..."
|
||||||
@timeout 90 bash -c '\
|
@timeout 30 bash -c '\
|
||||||
WEB_CONTAINER=$$($(DOCKER_COMPOSE) ps -q web); \
|
WEB_CONTAINER=$$($(DOCKER_COMPOSE) ps -q web); \
|
||||||
if [ -z "$$WEB_CONTAINER" ]; then \
|
if [ -z "$$WEB_CONTAINER" ]; then \
|
||||||
echo "[❌] Could not detect web container!"; \
|
echo "[❌] Could not detect web container!"; \
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
import importlib.util
|
import importlib.util
|
||||||
|
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
|
||||||
@ -18,7 +19,7 @@ def create_app():
|
|||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
migrate.init_app(app, db)
|
migrate.init_app(app, db)
|
||||||
login_manager.init_app(app)
|
login_manager.init_app(app)
|
||||||
login_manager.login_view = 'auth.login' # Redirect for @login_required
|
login_manager.login_view = 'auth.login'
|
||||||
|
|
||||||
# Register error handlers
|
# Register error handlers
|
||||||
from .errors import bp as errors_bp
|
from .errors import bp as errors_bp
|
||||||
@ -27,18 +28,41 @@ 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):
|
||||||
full_path = os.path.join(plugin_path, plugin, 'routes.py')
|
# Skip folders that end with `.noload`
|
||||||
if os.path.isfile(full_path):
|
if plugin.endswith('.noload'):
|
||||||
spec = importlib.util.spec_from_file_location(f"plugins.{plugin}.routes", full_path)
|
print(f"[⏭] Skipping plugin '{plugin}' (marked as .noload)")
|
||||||
mod = importlib.util.module_from_spec(spec)
|
continue
|
||||||
spec.loader.exec_module(mod)
|
|
||||||
if hasattr(mod, 'bp'):
|
|
||||||
app.register_blueprint(mod.bp)
|
|
||||||
|
|
||||||
# Register CLI commands if the plugin has any
|
plugin_dir = os.path.join(plugin_path, plugin)
|
||||||
if hasattr(mod, 'cli_commands'):
|
if not os.path.isdir(plugin_dir):
|
||||||
for command in mod.cli_commands:
|
continue
|
||||||
app.cli.add_command(command)
|
|
||||||
|
# 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
|
return app
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
from flask_login import UserMixin
|
from flask_login import UserMixin
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from app import db
|
from app import db
|
||||||
@ -7,7 +8,7 @@ class User(db.Model, UserMixin):
|
|||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
email = db.Column(db.String(120), unique=True, nullable=False)
|
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')
|
role = db.Column(db.String(50), default='user')
|
||||||
is_verified = db.Column(db.Boolean, default=False)
|
is_verified = db.Column(db.Boolean, default=False)
|
||||||
excluded_from_analytics = db.Column(db.Boolean, default=False)
|
excluded_from_analytics = db.Column(db.Boolean, default=False)
|
||||||
@ -16,112 +17,8 @@ class User(db.Model, UserMixin):
|
|||||||
# Optional: relationship to submissions
|
# Optional: relationship to submissions
|
||||||
submissions = db.relationship('Submission', backref='user', lazy=True)
|
submissions = db.relationship('Submission', backref='user', lazy=True)
|
||||||
|
|
||||||
|
def set_password(self, password):
|
||||||
|
self.password_hash = generate_password_hash(password)
|
||||||
|
|
||||||
class Submission(db.Model):
|
def check_password(self, password):
|
||||||
__tablename__ = 'submission'
|
return check_password_hash(self.password_hash, password)
|
||||||
|
|
||||||
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)
|
|
||||||
|
@ -4,9 +4,9 @@ from werkzeug.security import check_password_hash
|
|||||||
from app import db
|
from app import db
|
||||||
from .models import User
|
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():
|
def login():
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
email = request.form['email']
|
email = request.form['email']
|
||||||
@ -15,14 +15,33 @@ def login():
|
|||||||
if user and check_password_hash(user.password_hash, password):
|
if user and check_password_hash(user.password_hash, password):
|
||||||
login_user(user)
|
login_user(user)
|
||||||
flash('Logged in successfully.', 'success')
|
flash('Logged in successfully.', 'success')
|
||||||
return redirect(url_for('core.user_dashboard'))
|
return redirect(url_for('core_ui.home'))
|
||||||
else:
|
else:
|
||||||
flash('Invalid credentials.', 'danger')
|
flash('Invalid credentials.', 'danger')
|
||||||
return render_template('login.html')
|
return render_template('auth/login.html')
|
||||||
|
|
||||||
@auth.route('/auth/logout')
|
@bp.route('/logout')
|
||||||
@login_required
|
@login_required
|
||||||
def logout():
|
def logout():
|
||||||
logout_user()
|
logout_user()
|
||||||
flash('Logged out.', 'info')
|
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')
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'core_ui/base.html' %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2>Login</h2>
|
<h2>Login</h2>
|
||||||
<form method="POST" action="/login">
|
<form method="POST" action="/login">
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'core_ui/base.html' %}
|
||||||
|
{% block title %}Register{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2>Register</h2>
|
<h2>Register</h2>
|
||||||
<form method="POST" action="/register">
|
<form method="POST">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="email" class="form-label">Email</label>
|
<label>Email</label>
|
||||||
<input type="email" class="form-control" id="email" name="email" required>
|
<input name="email" class="form-control" type="email" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="password" class="form-label">Password</label>
|
<label>Password</label>
|
||||||
<input type="password" class="form-control" id="password" name="password" required>
|
<input name="password" class="form-control" type="password" required>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-success">Register</button>
|
<button class="btn btn-primary" type="submit">Register</button>
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,2 +1,10 @@
|
|||||||
from .seed import seed_admin
|
from .seed import preload_data, seed_admin
|
||||||
from .preload import preload_data
|
|
||||||
|
cli_commands = [
|
||||||
|
preload_data,
|
||||||
|
seed_admin
|
||||||
|
]
|
||||||
|
|
||||||
|
def register_cli(app):
|
||||||
|
for command in cli_commands:
|
||||||
|
app.cli.add_command(command)
|
||||||
|
@ -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.")
|
|
@ -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]
|
|
@ -1,26 +1,32 @@
|
|||||||
import click
|
import click
|
||||||
from flask.cli import with_appcontext
|
from flask.cli import with_appcontext
|
||||||
from werkzeug.security import generate_password_hash
|
from plugins.plant.models import Plant
|
||||||
from ..core.models import User
|
from plugins.auth.models import User
|
||||||
from .. import db
|
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
|
@with_appcontext
|
||||||
def seed_admin():
|
def seed_admin():
|
||||||
"""Seed a default admin user if none exists."""
|
click.echo("Seeding admin user...")
|
||||||
admin_email = "admin@example.com"
|
if not User.query.filter_by(email='admin@example.com').first():
|
||||||
admin_password = "admin123"
|
user = User(email='admin@example.com', role='admin', is_verified=True)
|
||||||
|
user.set_password('password') # Make sure this method exists in your model
|
||||||
if User.query.filter_by(email=admin_email).first():
|
db.session.add(user)
|
||||||
click.echo("[ℹ] Admin user already exists.")
|
db.session.commit()
|
||||||
return
|
click.echo("✅ Admin user created.")
|
||||||
|
else:
|
||||||
user = User(
|
click.echo("ℹ️ Admin user already exists.")
|
||||||
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}")
|
|
||||||
|
@ -1 +0,0 @@
|
|||||||
# core_ui media patch
|
|
15
plugins/core_ui/routes.py
Normal file
15
plugins/core_ui/routes.py
Normal file
@ -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')
|
11
plugins/core_ui/templates/core_ui/admin_dashboard.html
Normal file
11
plugins/core_ui/templates/core_ui/admin_dashboard.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{% extends 'core_ui/base.html' %}
|
||||||
|
{% block title %}Admin Dashboard | Nature In Pots{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h1 class="mb-4 text-danger">Admin Dashboard</h1>
|
||||||
|
<p class="lead">Manage submissions, users, and plugin controls here.</p>
|
||||||
|
<ul>
|
||||||
|
<li><a href="#">View Unapproved Submissions</a></li>
|
||||||
|
<li><a href="#">Manage Users</a></li>
|
||||||
|
<li><a href="#">Export Data</a></li>
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
69
plugins/core_ui/templates/core_ui/base.html
Normal file
69
plugins/core_ui/templates/core_ui/base.html
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>{% block title %}Nature In Pots Community{% endblock %}</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 1rem 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand" href="{{ url_for('core_ui.home') }}">Nature In Pots</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav me-auto">
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('core_ui.home') }}">Home</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('plant.index') }}">Plants</a></li>
|
||||||
|
{% if current_user.is_authenticated and current_user.role == 'admin' %}
|
||||||
|
<li class="nav-item"><a class="nav-link text-danger" href="{{ url_for('core_ui.admin_dashboard') }}">Admin Dashboard</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% block plugin_links %}{% endblock %}
|
||||||
|
</ul>
|
||||||
|
<ul class="navbar-nav align-items-center">
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
<li class="nav-item ms-3">
|
||||||
|
<a class="nav-link" href="{{ url_for('auth.logout') }}">Logout</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="nav-item ms-3"><a class="nav-link" href="{{ url_for('auth.login') }}">Login</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('auth.register') }}">Register</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<main class="container">
|
||||||
|
{% with messages = get_flashed_messages() %}
|
||||||
|
{% if messages %}
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
{% for message in messages %}
|
||||||
|
<div>{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
© {{ current_year | default(2025) }} Nature In Pots Community. All rights reserved.
|
||||||
|
</footer>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
6
plugins/core_ui/templates/core_ui/home.html
Normal file
6
plugins/core_ui/templates/core_ui/home.html
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{% extends 'core_ui/base.html' %}
|
||||||
|
{% block title %}Home | Nature In Pots{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h1 class="mb-4">Welcome to Nature In Pots 🌿</h1>
|
||||||
|
<p>This is the community hub for plant tracking, propagation history, and price sharing.</p>
|
||||||
|
{% endblock %}
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'core_ui/base.html' %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2>Add Log for Plant #{{ plant.id }}</h2>
|
<h2>Add Log for Plant #{{ plant.id }}</h2>
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{% import 'core_ui/_media_macros.html' as media %}
|
{% import 'core_ui/_media_macros.html' as media %}
|
||||||
{% extends 'base.html' %}
|
{% extends 'core_ui/base.html' %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2>Logs for Plant #{{ plant.id }}</h2>
|
<h2>Logs for Plant #{{ plant.id }}</h2>
|
||||||
<a href="{{ url_for('growlog.add_log', plant_id=plant.id) }}">Add New Log</a>
|
<a href="{{ url_for('growlog.add_log', plant_id=plant.id) }}">Add New Log</a>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'core_ui/base.html' %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2>All Uploaded Media</h2>
|
<h2>All Uploaded Media</h2>
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'core_ui/base.html' %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2>Upload Media</h2>
|
<h2>Upload Media</h2>
|
||||||
<form method="POST" enctype="multipart/form-data">
|
<form method="POST" enctype="multipart/form-data">
|
||||||
|
@ -9,3 +9,113 @@ class Plant(db.Model):
|
|||||||
is_active = db.Column(db.Boolean, default=True)
|
is_active = db.Column(db.Boolean, default=True)
|
||||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'core_ui/base.html' %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2>Search Plants</h2>
|
<h2>Search Plants</h2>
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'core_ui/base.html' %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2>Search Results</h2>
|
<h2>Search Results</h2>
|
||||||
{% if results %}
|
{% if results %}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'core_ui/base.html' %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<p>This page was replaced by AJAX functionality.</p>
|
<p>This page was replaced by AJAX functionality.</p>
|
||||||
{% endblock %}
|
{% endblock %}
|
Reference in New Issue
Block a user