Files
2025-06-27 17:43:50 -05:00

354 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from uuid import uuid4
import os
import string
from sqlalchemy.orm import joinedload
from flask import (
Blueprint,
render_template,
redirect,
url_for,
request,
flash,
current_app,
)
from flask_login import login_required, current_user
from app import db
from .models import Plant, PlantScientificName, PlantCommonName
from .forms import PlantForm
from plugins.media.models import Media
from plugins.media.routes import (
save_media_file,
delete_media_file,
rotate_media_file,
generate_image_url,
)
bp = Blueprint(
'plant',
__name__,
url_prefix='/plants',
template_folder='templates',
)
@bp.app_context_processor
def inject_image_helper():
return dict(generate_image_url=generate_image_url)
@bp.route('/', methods=['GET'])
@login_required
def index():
# ── 1) Read query-params ───────────────────────────────────────────
page = request.args.get('page', 1, type=int)
per_page = request.args.get(
'per_page',
current_app.config.get('PLANTS_PER_PAGE', 12),
type=int
)
view_mode = request.args.get('view', 'grid', type=str) # 'grid' or 'list'
q = request.args.get('q', '', type=str).strip()
type_filter= request.args.get('type', '', type=str).strip().lower()
# ── 2) Build base SQLAlchemy query ────────────────────────────────
qry = (
Plant.query
.options(joinedload(Plant.media_items))
.filter_by(owner_id=current_user.id)
)
# ── 3) Optional name search ───────────────────────────────────────
if q:
qry = qry.join(PlantCommonName).filter(
PlantCommonName.name.ilike(f'%{q}%')
)
# ── 4) Optional type filter ───────────────────────────────────────
if type_filter:
qry = qry.filter(Plant.plant_type.ilike(type_filter))
# ── 5) Apply ordering + paginate ─────────────────────────────────
pagination = (
qry.order_by(Plant.id.desc())
.paginate(page=page, per_page=per_page, error_out=False)
)
plants = pagination.items
# ── 6) Gather stats and distinct types as before ─────────────────
stats = {
'user_plants': Plant.query.filter_by(owner_id=current_user.id).count(),
'user_images': Media.query.filter_by(uploader_id=current_user.id).count(),
'total_plants': Plant.query.count(),
'total_images': Media.query.count(),
}
plant_types = [
row[0]
for row in (
db.session
.query(Plant.plant_type)
.filter_by(owner_id=current_user.id)
.distinct()
.all()
)
]
# ── 7) Render, passing both pagination AND per-page items ─────────
return render_template(
'plant/index.html',
plants = plants,
pagination = pagination,
view_mode = view_mode,
q = q,
type_filter = type_filter,
per_page = per_page,
plant_types = plant_types,
stats = stats
)
@bp.route('/create', methods=['GET', 'POST'])
@login_required
def create():
form = PlantForm()
form.plant_type.choices = [
('plant', 'Plant'),
('cutting', 'Cutting'),
('seed', 'Seed'),
('tissue_culture', 'Tissue Culture'),
('division', 'Division'),
]
form.common_name.choices = [
(c.id, c.name)
for c in PlantCommonName.query.order_by(PlantCommonName.name).all()
]
form.scientific_name.choices = [
(s.id, s.name)
for s in PlantScientificName.query.order_by(PlantScientificName.name).all()
]
if form.validate_on_submit():
new_plant = Plant(
uuid=str(uuid4()),
owner_id=current_user.id,
plant_type=form.plant_type.data,
common_id=form.common_name.data,
scientific_id=form.scientific_name.data,
mother_uuid=(form.mother_uuid.data if form.mother_uuid.data != 'N/A' else None),
custom_slug=(form.custom_slug.data.strip() or None),
vendor_name=(form.vendor_name.data.strip() or None),
price=(form.price.data or None),
notes=form.notes.data,
data_verified=form.data_verified.data,
is_active=form.is_active.data,
)
db.session.add(new_plant)
db.session.commit()
flash('New plant created successfully.', 'success')
return redirect(url_for('plant.edit', uuid_val=new_plant.uuid))
return render_template('plant/create.html', form=form)
@bp.route('/<uuid:uuid_val>', methods=['GET'])
@login_required
def detail(uuid_val):
# 1) load this plant (and its media) or 404
plant = (
Plant.query
.options(joinedload(Plant.media_items))
.filter_by(uuid=str(uuid_val), owner_id=current_user.id)
.first_or_404()
)
# 2) load any child plants (same owner, mother_uuid pointing here)
children = (
Plant.query
.options(joinedload(Plant.media_items))
.filter_by(owner_id=current_user.id, mother_uuid=plant.uuid)
.order_by(Plant.id)
.all()
)
# 3) build linear nav of this user's plants (by insertion order)
all_plants = (
Plant.query
.filter_by(owner_id=current_user.id)
.order_by(Plant.id)
.all()
)
uuids = [p.uuid for p in all_plants]
try:
idx = uuids.index(str(uuid_val))
except ValueError:
idx = None
prev_uuid = uuids[idx - 1] if idx not in (None, 0) else None
next_uuid = uuids[idx + 1] if idx is not None and idx < len(uuids) - 1 else None
return render_template(
'plant/detail.html',
plant=plant,
children=children,
prev_uuid=prev_uuid,
next_uuid=next_uuid
)
@bp.route('/<uuid:uuid_val>/generate_children', methods=['POST'])
@login_required
def generate_children(uuid_val):
parent = (
Plant.query
.filter_by(uuid=str(uuid_val), owner_id=current_user.id)
.first_or_404()
)
try:
count = int(request.form.get('count', 1))
except ValueError:
count = 1
created = 0
for _ in range(count):
child = Plant(
uuid=str(uuid4()),
owner_id=current_user.id,
plant_type='cutting',
common_id=parent.common_id,
scientific_id=parent.scientific_id,
mother_uuid=parent.uuid,
custom_slug=None,
vendor_name=None,
price=None,
notes=None,
data_verified=False,
is_active=True
)
db.session.add(child)
created += 1
db.session.commit()
flash(f"Generated {created} cuttings for {parent.common_name.name}.", 'success')
return redirect(url_for('plant.detail', uuid_val=parent.uuid))
@bp.route('/<uuid:uuid_val>/edit', methods=['GET', 'POST'])
@login_required
def edit(uuid_val):
plant = Plant.query.filter_by(
uuid=str(uuid_val),
owner_id=current_user.id
).first_or_404()
form = PlantForm()
form.plant_type.choices = [
('plant', 'Plant'),
('cutting', 'Cutting'),
('seed', 'Seed'),
('tissue_culture', 'Tissue Culture'),
('division', 'Division'),
]
form.common_name.choices = [
(c.id, c.name)
for c in PlantCommonName.query.order_by(PlantCommonName.name).all()
]
form.scientific_name.choices = [
(s.id, s.name)
for s in PlantScientificName.query.order_by(PlantScientificName.name).all()
]
form.mother_uuid.choices = [('N/A', 'None')] + [
(p.uuid, f"{p.common_name.name if p.common_name else 'Unnamed'} {p.uuid}")
for p in Plant.query.filter(Plant.uuid != plant.uuid).all()
]
if request.method == 'GET':
form.plant_type.data = plant.plant_type
form.common_name.data = plant.common_id
form.scientific_name.data = plant.scientific_id
form.mother_uuid.data = plant.mother_uuid or 'N/A'
form.custom_slug.data = plant.custom_slug or ''
form.vendor_name.data = plant.vendor_name or ''
form.price.data = plant.price or None
form.notes.data = plant.notes
form.data_verified.data = plant.data_verified
form.is_active.data = getattr(plant, 'is_active', True)
if form.validate_on_submit():
plant.plant_type = form.plant_type.data
plant.common_id = form.common_name.data
plant.scientific_id = form.scientific_name.data
plant.mother_uuid = (form.mother_uuid.data if form.mother_uuid.data != 'N/A' else None)
plant.custom_slug = (form.custom_slug.data.strip() or None)
plant.vendor_name = (form.vendor_name.data.strip() or None)
plant.price = form.price.data or None
plant.notes = form.notes.data
plant.data_verified = form.data_verified.data
plant.is_active = form.is_active.data
# (No more inline "featured_media_id" patch here—handled in media plugin)
db.session.commit()
flash('Plant updated successfully.', 'success')
return redirect(url_for('plant.detail', uuid_val=plant.uuid))
return render_template('plant/edit.html', form=form, plant=plant)
@bp.route('/<uuid:uuid_val>/upload', methods=['POST'])
@login_required
def upload_image(uuid_val):
plant = Plant.query.filter_by(
uuid=str(uuid_val),
owner_id=current_user.id
).first_or_404()
file = request.files.get('image')
if not file or file.filename == '':
flash('No file selected.', 'danger')
return redirect(url_for('plant.edit', uuid_val=plant.uuid))
try:
media = save_media_file(file, 'plants', plant.id)
media.uploader_id = current_user.id
db.session.add(media)
db.session.commit()
flash('Image uploaded.', 'success')
except Exception as e:
current_app.logger.error(f"Upload failed: {e}")
flash('Failed to upload image.', 'danger')
return redirect(url_for('plant.edit', uuid_val=plant.uuid))
@bp.route('/<uuid:uuid_val>/delete/<int:media_id>', methods=['POST'])
@login_required
def delete_image(uuid_val, media_id):
plant = Plant.query.filter_by(uuid=str(uuid_val)).first_or_404()
media = Media.query.get_or_404(media_id)
delete_media_file(media)
flash('Image deleted.', 'success')
return redirect(url_for('plant.edit', uuid_val=plant.uuid))
@bp.route('/<uuid:uuid_val>/rotate/<int:media_id>', methods=['POST'])
@login_required
def rotate_image(uuid_val, media_id):
plant = Plant.query.filter_by(uuid=str(uuid_val)).first_or_404()
media = Media.query.get_or_404(media_id)
rotate_media_file(media)
flash('Image rotated.', 'success')
return redirect(url_for('plant.edit', uuid_val=plant.uuid))
@bp.route('/f/<string:short_id>')
@bp.route('/<string:short_id>')
def view_card(short_id):
p = Plant.query.filter_by(short_id=short_id).first_or_404()
# 2) owner → straight to the normal detail page
if current_user.is_authenticated and p.owner_id == current_user.id:
return redirect(url_for('plant.detail', uuid_val=p.uuid))
# 3) everyone else → public card
featured = next((m for m in p.media_items if getattr(m, 'featured', False)), None)
return render_template('plant/card.html', plant=p, featured=featured)