This commit is contained in:
2025-05-18 05:21:16 -05:00
parent 132073ca19
commit c19bedc54a
65 changed files with 705 additions and 575 deletions

127
plugins/auth/models.py Normal file
View File

@ -0,0 +1,127 @@
from flask_login import UserMixin
from datetime import datetime
from app import db
class User(db.Model, UserMixin):
__tablename__ = 'users'
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)
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)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# Optional: relationship to submissions
submissions = db.relationship('Submission', backref='user', lazy=True)
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)

28
plugins/auth/routes.py Normal file
View File

@ -0,0 +1,28 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash
from flask_login import login_user, logout_user, login_required
from werkzeug.security import check_password_hash
from app import db
from .models import User
auth = Blueprint('auth', __name__)
@auth.route('/auth/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
email = request.form['email']
password = request.form['password']
user = User.query.filter_by(email=email).first()
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'))
else:
flash('Invalid credentials.', 'danger')
return render_template('login.html')
@auth.route('/auth/logout')
@login_required
def logout():
logout_user()
flash('Logged out.', 'info')
return redirect(url_for('core.index'))

View File

@ -0,0 +1,15 @@
{% extends 'base.html' %}
{% block content %}
<h2>Login</h2>
<form method="POST" action="/login">
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary">Login</button>
</form>
{% endblock %}

View File

@ -0,0 +1,15 @@
{% extends 'base.html' %}
{% block content %}
<h2>Register</h2>
<form method="POST" action="/register">
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-success">Register</button>
</form>
{% endblock %}

2
plugins/cli/__init__.py Normal file
View File

@ -0,0 +1,2 @@
from .seed import seed_admin
from .preload import preload_data

79
plugins/cli/preload.py Normal file
View File

@ -0,0 +1,79 @@
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.")

26
plugins/cli/seed.py Normal file
View File

@ -0,0 +1,26 @@
import click
from flask.cli import with_appcontext
from werkzeug.security import generate_password_hash
from ..core.models import User
from .. import db
@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}")

View File

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

View File

@ -0,0 +1 @@
{ "name": "core_ui", "version": "1.1", "description": "Media rendering macros and styling helpers" }

View File

@ -0,0 +1,14 @@
{% macro render_media_list(media_list, thumb_width=150) -%}
{% if media_list %}
<ul class="media-thumbnails">
{% for media in media_list %}
<li>
<img src="{{ url_for('media.media_file', filename=media.file_url) }}" width="{{ thumb_width }}">
{% if media.caption %}
<p>{{ media.caption }}</p>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
{%- endmacro %}

View File

@ -0,0 +1,18 @@
<style>
.media-thumbnails {
list-style-type: none;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.media-thumbnails li {
display: inline-block;
text-align: center;
}
.media-thumbnails img {
border: 1px solid #ccc;
border-radius: 4px;
padding: 2px;
}
</style>

View File

@ -0,0 +1 @@
# growlog plugin init

15
plugins/growlog/forms.py Normal file
View File

@ -0,0 +1,15 @@
from flask_wtf import FlaskForm
from wtforms import TextAreaField, SelectField, SubmitField
from wtforms.validators import DataRequired, Length
class GrowLogForm(FlaskForm):
event_type = SelectField('Event Type', choices=[
('water', 'Watered'),
('fertilizer', 'Fertilized'),
('repot', 'Repotted'),
('note', 'Note'),
('pest', 'Pest Observed')
], validators=[DataRequired()])
note = TextAreaField('Notes', validators=[Length(max=1000)])
submit = SubmitField('Add Log')

15
plugins/growlog/models.py Normal file
View File

@ -0,0 +1,15 @@
from app import db
from datetime import datetime
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)
media = db.relationship("Media", backref="growlog", lazy=True)
def __repr__(self):
return f"<GrowLog {self.event_type} @ {self.timestamp}>"

View File

@ -0,0 +1 @@
{ "name": "growlog", "version": "1.0", "description": "Tracks time-based plant care logs" }

31
plugins/growlog/routes.py Normal file
View File

@ -0,0 +1,31 @@
from flask import Blueprint, render_template, redirect, url_for, request
from flask_login import login_required
from app import db
from .models import GrowLog
from .forms import GrowLogForm
from plugins.plant.models import Plant
bp = Blueprint('growlog', __name__, template_folder='templates')
@bp.route('/plants/<int:plant_id>/logs')
@login_required
def view_logs(plant_id):
plant = Plant.query.get_or_404(plant_id)
logs = GrowLog.query.filter_by(plant_id=plant.id).order_by(GrowLog.timestamp.desc()).all()
return render_template('growlog/log_list.html', plant=plant, logs=logs)
@bp.route('/plants/<int:plant_id>/logs/add', methods=['GET', 'POST'])
@login_required
def add_log(plant_id):
plant = Plant.query.get_or_404(plant_id)
form = GrowLogForm()
if form.validate_on_submit():
log = GrowLog(
plant_id=plant.id,
event_type=form.event_type.data,
note=form.note.data
)
db.session.add(log)
db.session.commit()
return redirect(url_for('growlog.view_logs', plant_id=plant.id))
return render_template('growlog/log_form.html', form=form, plant=plant)

View File

@ -0,0 +1,10 @@
{% extends 'base.html' %}
{% block content %}
<h2>Add Log for Plant #{{ plant.id }}</h2>
<form method="POST">
{{ form.hidden_tag() }}
<p>{{ form.event_type.label }}<br>{{ form.event_type() }}</p>
<p>{{ form.note.label }}<br>{{ form.note(rows=4) }}</p>
<p>{{ form.submit() }}</p>
</form>
{% endblock %}

View File

@ -0,0 +1,25 @@
{% import 'core_ui/_media_macros.html' as media %}
{% extends 'base.html' %}
{% block content %}
<h2>Logs for Plant #{{ plant.id }}</h2>
<a href="{{ url_for('growlog.add_log', plant_id=plant.id) }}">Add New Log</a>
<ul>
{% for log in logs %}
<li>
<strong>{{ log.timestamp.strftime('%Y-%m-%d') }}:</strong> {{ log.event_type }} - {{ log.note }}
{% if log.media %}
<br><em>Images:</em>
<ul>
{% for image in log.media %}
<li>
<img src="{{ url_for('media.media_file', filename=image.file_url) }}" width="150"><br>
{{ image.caption or "No caption" }}
</li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
</ul>
{{ media.render_media_list(log.media) }}
{% endblock %}

View File

@ -0,0 +1 @@
# media plugin init

14
plugins/media/forms.py Normal file
View File

@ -0,0 +1,14 @@
from flask_wtf import FlaskForm
from wtforms import StringField, SelectField, SubmitField, IntegerField
from flask_wtf.file import FileField, FileAllowed, FileRequired
from wtforms.validators import DataRequired
class MediaUploadForm(FlaskForm):
image = FileField('Image', validators=[
FileRequired(),
FileAllowed(['jpg', 'jpeg', 'png', 'gif'], 'Images only!')
])
caption = StringField('Caption')
plant_id = IntegerField('Plant ID')
growlog_id = IntegerField('GrowLog ID')
submit = SubmitField('Upload')

12
plugins/media/models.py Normal file
View File

@ -0,0 +1,12 @@
from app import db
from datetime import datetime
class Media(db.Model):
__tablename__ = 'media'
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)

View File

@ -0,0 +1 @@
{ "name": "media", "version": "1.0", "description": "Upload and attach media to plants and grow logs" }

42
plugins/media/routes.py Normal file
View File

@ -0,0 +1,42 @@
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
bp = Blueprint('media', __name__, template_folder='templates')
@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)

View File

@ -0,0 +1,14 @@
{% extends 'base.html' %}
{% block content %}
<h2>All Uploaded Media</h2>
<ul>
{% for image in images %}
<li>
<img src="{{ url_for('media.media_file', filename=image.file_url) }}" alt="{{ image.caption }}" width="200"><br>
{{ image.caption or "No caption" }}
{% if image.plant_id %}<br>Plant ID: {{ image.plant_id }}{% endif %}
{% if image.growlog_id %}<br>GrowLog ID: {{ image.growlog_id }}{% endif %}
</li>
{% endfor %}
</ul>
{% endblock %}

View File

@ -0,0 +1,12 @@
{% extends 'base.html' %}
{% block content %}
<h2>Upload Media</h2>
<form method="POST" enctype="multipart/form-data">
{{ form.hidden_tag() }}
<p>{{ form.image.label }}<br>{{ form.image() }}</p>
<p>{{ form.caption.label }}<br>{{ form.caption(size=40) }}</p>
<p>{{ form.plant_id.label }}<br>{{ form.plant_id() }}</p>
<p>{{ form.growlog_id.label }}<br>{{ form.growlog_id() }}</p>
<p>{{ form.submit() }}</p>
</form>
{% endblock %}

View File

@ -0,0 +1 @@
# plant plugin init

10
plugins/plant/forms.py Normal file
View File

@ -0,0 +1,10 @@
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, BooleanField, SubmitField
from wtforms.validators import DataRequired
class PlantForm(FlaskForm):
name = StringField('Name', validators=[DataRequired()])
type = StringField('Type')
notes = TextAreaField('Notes')
is_active = BooleanField('Active', default=True)
submit = SubmitField('Save')

11
plugins/plant/models.py Normal file
View File

@ -0,0 +1,11 @@
from datetime import datetime
from app import db
class Plant(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(128), nullable=False)
type = db.Column(db.String(64))
notes = db.Column(db.Text)
is_active = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

43
plugins/plant/routes.py Normal file
View File

@ -0,0 +1,43 @@
from flask import Blueprint, render_template, redirect, url_for, request, flash
from app import db
from .models import Plant
from .forms import PlantForm
bp = Blueprint('plant', __name__, template_folder='templates')
@bp.route('/plants')
def index():
plants = Plant.query.order_by(Plant.created_at.desc()).all()
return render_template('plant/index.html', plants=plants)
@bp.route('/plants/<int:plant_id>')
def detail(plant_id):
plant = Plant.query.get_or_404(plant_id)
return render_template('plant/detail.html', plant=plant)
@bp.route('/plants/new', methods=['GET', 'POST'])
def create():
form = PlantForm()
if form.validate_on_submit():
plant = Plant(
name=form.name.data,
type=form.type.data,
notes=form.notes.data,
is_active=form.is_active.data
)
db.session.add(plant)
db.session.commit()
flash('Plant created successfully.', 'success')
return redirect(url_for('plant.index'))
return render_template('plant/form.html', form=form)
@bp.route('/plants/<int:plant_id>/edit', methods=['GET', 'POST'])
def edit(plant_id):
plant = Plant.query.get_or_404(plant_id)
form = PlantForm(obj=plant)
if form.validate_on_submit():
form.populate_obj(plant)
db.session.commit()
flash('Plant updated successfully.', 'success')
return redirect(url_for('plant.detail', plant_id=plant.id))
return render_template('plant/form.html', form=form, plant=plant)

View File

@ -0,0 +1,9 @@
{% extends 'core_ui/base.html' %}
{% block content %}
<h1>{{ plant.name }}</h1>
<p>Type: {{ plant.type }}</p>
<p>{{ plant.notes }}</p>
<p>Status: {% if plant.is_active %}Active{% else %}Inactive{% endif %}</p>
<a href="{{ url_for('plant.edit', plant_id=plant.id) }}">Edit</a>
<a href="{{ url_for('plant.index') }}">Back to list</a>
{% endblock %}

View File

@ -0,0 +1,12 @@
{% extends 'core_ui/base.html' %}
{% block content %}
<h1>{% if plant %}Edit{% else %}New{% endif %} Plant</h1>
<form method="POST">
{{ form.hidden_tag() }}
<p>{{ form.name.label }}<br>{{ form.name(size=40) }}</p>
<p>{{ form.type.label }}<br>{{ form.type(size=40) }}</p>
<p>{{ form.notes.label }}<br>{{ form.notes(rows=5, cols=40) }}</p>
<p>{{ form.is_active() }} {{ form.is_active.label }}</p>
<p>{{ form.submit() }}</p>
</form>
{% endblock %}

View File

@ -0,0 +1,10 @@
{% extends 'core_ui/base.html' %}
{% block content %}
<h1>Plant List</h1>
<ul>
{% for plant in plants %}
<li><a href="{{ url_for('plant.detail', plant_id=plant.id) }}">{{ plant.name }}</a></li>
{% endfor %}
</ul>
<a href="{{ url_for('plant.create') }}">Add New Plant</a>
{% endblock %}

View File

@ -0,0 +1 @@
# search plugin initialization

15
plugins/search/forms.py Normal file
View File

@ -0,0 +1,15 @@
from flask_wtf import FlaskForm
from wtforms import StringField, SelectMultipleField, SubmitField
from wtforms.validators import Optional, Length, Regexp
class SearchForm(FlaskForm):
query = StringField(
'Search',
validators=[
Optional(),
Length(min=2, max=100, message="Search term must be between 2 and 100 characters."),
Regexp(r'^[\w\s\-]+$', message="Search can only include letters, numbers, spaces, and dashes.")
]
)
tags = SelectMultipleField('Tags', coerce=int)
submit = SubmitField('Search')

8
plugins/search/models.py Normal file
View File

@ -0,0 +1,8 @@
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)
def __repr__(self):
return f"<Tag {self.name}>"

View File

@ -0,0 +1 @@
{ "name": "search", "version": "1.1", "description": "Updated search plugin with live Plant model integration" }

38
plugins/search/routes.py Normal file
View File

@ -0,0 +1,38 @@
from flask import Blueprint, render_template, request, jsonify
from flask_login import login_required, current_user
from app import db
from .models import Tag
from .forms import SearchForm
from plugins.plant.models import Plant
bp = Blueprint('search', __name__, template_folder='templates')
@bp.route('/search', methods=['GET', 'POST'])
@login_required
def search():
form = SearchForm()
form.tags.choices = [(tag.id, tag.name) for tag in Tag.query.order_by(Tag.name).all()]
results = []
if form.validate_on_submit():
query = db.session.query(Plant).join(PlantScientific).join(PlantCommon)
if form.query.data:
q = f"%{form.query.data}%"
query = query.filter(
db.or_(
PlantScientific.name.ilike(q),
PlantCommon.name.ilike(q),
Plant.current_status.ilike(q)
)
)
if form.tags.data:
query = query.filter(Plant.tags.any(Tag.id.in_(form.tags.data)))
query = query.filter(Plant.owner_id == current_user.id)
results = query.all()
return render_template('search/search.html', form=form, results=results)
@bp.route('/search/tags')
@login_required
def search_tags():
term = request.args.get('term', '')
tags = Tag.query.filter(Tag.name.ilike(f"%{term}%")).limit(10).all()
return jsonify([tag.name for tag in tags])

View File

@ -0,0 +1,26 @@
{% extends 'base.html' %}
{% block content %}
<h2>Search Plants</h2>
<form method="POST">
{{ form.hidden_tag() }}
<p>
{{ form.query.label }}<br>
{{ form.query(size=32) }}
</p>
<p>
{{ form.tags.label }}<br>
{{ form.tags(multiple=True) }}
</p>
<p>{{ form.submit() }}</p>
</form>
{% if results %}
<h3>Search Results</h3>
<ul>
{% for result in results %}
<li>{{ result.name }}</li>
{% endfor %}
</ul>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,13 @@
{% extends 'base.html' %}
{% block content %}
<h2>Search Results</h2>
{% if results %}
<ul>
{% for result in results %}
<li>{{ result.name }}</li>
{% endfor %}
</ul>
{% else %}
<p>No results found.</p>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,4 @@
{% extends 'base.html' %}
{% block content %}
<p>This page was replaced by AJAX functionality.</p>
{% endblock %}