354 lines
12 KiB
Python
354 lines
12 KiB
Python
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)
|