2.8 KiB
Plant Card Photos — Design Spec
Date: 2026-05-28
Overview
Allow users to store photos of the physical plant label/care card that came with a plant. Up to 3 photos per plant (e.g. front and back of the card). Accessed via a small button on the plant detail page, opening a dedicated card page — keeping the detail page clean.
Model
New model PlantCardPhoto:
plant— ForeignKey toPlant,on_delete=CASCADE,related_name='card_photos'image— ImageField,upload_to='plants/cards/'uploaded_at— DateTimeField,auto_now_add=True
No thumbnail concept. Ordering by uploaded_at.
The upload view enforces a maximum of 3 card photos per plant (silently blocks if already at 3).
Views & URLs
Two new views in plants/views/plants.py, both @require_POST:
| View | URL | Action |
|---|---|---|
upload_card_photo(request, pk) |
plants/<pk>/card-photos/upload/ |
Creates a PlantCardPhoto, re-renders the card page partial |
delete_card_photo(request, card_photo_pk) |
card-photos/<pk>/delete/ |
Deletes the file and record, re-renders the card page partial |
A new GET view:
| View | URL |
|---|---|
plant_card(request, pk) |
plants/<pk>/card/ |
The card page renders plants/plant_card.html with all card photos for the plant.
Templates
plants/views/plants.py change
The plant_detail view's prefetch_related call needs 'card_photos' added so the card_photos.exists() check in the template doesn't fire an extra query.
plants/plant_detail.html change
Add a small button near the bottom (above the delete link), rendered only when plant.card_photos.exists():
{% if plant.card_photos.exists %}
<a href="{% url 'plant_card' plant.pk %}" class="btn btn-sm btn-outline-secondary mb-2">🪧 View plant card</a>
{% endif %}
New: plants/plant_card.html
Full page extending base.html:
- Back button →
plant_detail - Title: "Plant card"
- Photo grid (portrait thumbnails, 3-column) with delete button per photo
- Upload form with
capture="environment"for mobile camera - Upload form hidden when 3 photos already uploaded
- Hint text: "Up to 3 photos — front & back of the label"
The upload and delete forms use htmx (hx-post, hx-target="#card-gallery", hx-swap="outerHTML") targeting a <div id="card-gallery"> that wraps the grid + upload form, following the same pattern as photo_gallery.html.
New partial: plants/partials/card_gallery.html
Contains the photo grid and upload form, rendered by both the initial page load and htmx responses.
Migration
Single migration adding PlantCardPhoto.
Out of Scope
- OCR or data extraction from card photos (future work)
- Viewing card photos inline on the plant detail page
- Lightbox / full-screen image viewer