More files

This commit is contained in:
2025-05-24 03:23:15 -05:00
parent 558dcfe81e
commit 6de9729329
23 changed files with 338 additions and 272 deletions

View File

@ -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!"; \

View File

@ -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

View File

@ -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)

View File

@ -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')

View File

@ -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">

View File

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

View File

@ -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)

View File

@ -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.")

View File

@ -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]

View File

@ -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}")

View File

@ -1 +0,0 @@
# core_ui media patch

15
plugins/core_ui/routes.py Normal file
View 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')

View 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 %}

View 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>
&copy; {{ 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>

View 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 %}

View File

@ -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">

View File

@ -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>

View File

@ -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>

View File

@ -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">

View File

@ -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)

View File

@ -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">

View File

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

View File

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