# 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 to `Plant`, `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//card-photos/upload/` | Creates a `PlantCardPhoto`, re-renders the card page partial | | `delete_card_photo(request, card_photo_pk)` | `card-photos//delete/` | Deletes the file and record, re-renders the card page partial | A new GET view: | View | URL | |---|---| | `plant_card(request, pk)` | `plants//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()`: ```html {% if plant.card_photos.exists %} 🪧 View plant card {% 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 `