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('/', 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('//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('//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('//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('//delete/', 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('//rotate/', 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/') @bp.route('/') 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)