- Location model: plants now FK to Location instead of free-text CharField - Data migration 0004 converts existing location strings to Location objects - PlantForm uses ModelChoiceField dropdown + inline "add new location" text field - PlantPhoto model: multiple photos per plant with is_thumbnail flag - Plant.thumbnail_url property: picks marked thumbnail, falls back to first photo then legacy photo field - Photo gallery partial: upload, set thumbnail (★), delete (✕) with HTMX swaps on #photo-gallery - plant_detail: uses thumbnail_url for hero image, pruning history section removed - plant_list search updated: location__name__icontains instead of location__icontains - Three new URL routes: upload_photo, set_thumbnail, delete_photo - All 53 tests updated and passing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
95 lines
3 KiB
Python
95 lines
3 KiB
Python
import pytest
|
|
from datetime import date
|
|
from plants.models import Plant, PruningLog, Location
|
|
from plants.utils.pruning import pruning_status, months_before
|
|
|
|
|
|
def make_location(name):
|
|
loc, _ = Location.objects.get_or_create(name=name)
|
|
return loc
|
|
|
|
|
|
def test_months_before_same_year():
|
|
d = date(2026, 5, 27)
|
|
assert months_before(d, 1) == date(2026, 4, 1)
|
|
assert months_before(d, 4) == date(2026, 1, 1)
|
|
|
|
|
|
def test_months_before_crosses_year():
|
|
d = date(2026, 5, 27)
|
|
assert months_before(d, 5) == date(2025, 12, 1)
|
|
assert months_before(d, 12) == date(2025, 5, 1)
|
|
|
|
|
|
def test_months_before_january():
|
|
d = date(2026, 1, 15)
|
|
assert months_before(d, 1) == date(2025, 12, 1)
|
|
assert months_before(d, 2) == date(2025, 11, 1)
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_no_schedule():
|
|
plant = Plant.objects.create(name='Fern', pruning_months=[])
|
|
assert pruning_status(plant) == 'no_schedule'
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_due_this_month():
|
|
today = date(2026, 5, 15)
|
|
plant = Plant.objects.create(name='Rose', pruning_months=[5])
|
|
assert pruning_status(plant, today) == 'due_this_month'
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_due_this_month_already_logged():
|
|
today = date(2026, 5, 15)
|
|
plant = Plant.objects.create(name='Rose', pruning_months=[5])
|
|
PruningLog.objects.create(plant=plant, pruned_on=date(2026, 5, 1))
|
|
assert pruning_status(plant, today) == 'upcoming'
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_overdue_when_past_month_not_logged():
|
|
today = date(2026, 5, 15)
|
|
plant = Plant.objects.create(name='Rose', pruning_months=[3])
|
|
assert pruning_status(plant, today) == 'overdue'
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_not_overdue_when_past_month_logged():
|
|
today = date(2026, 5, 15)
|
|
plant = Plant.objects.create(name='Rose', pruning_months=[3])
|
|
PruningLog.objects.create(plant=plant, pruned_on=date(2026, 3, 10))
|
|
assert pruning_status(plant, today) == 'upcoming'
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_not_overdue_when_logged_after_scheduled_month():
|
|
# Pruned in May clears a March schedule — the common real-world case
|
|
today = date(2026, 5, 28)
|
|
plant = Plant.objects.create(name='Rose', pruning_months=[3])
|
|
PruningLog.objects.create(plant=plant, pruned_on=date(2026, 5, 28))
|
|
assert pruning_status(plant, today) == 'upcoming'
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_upcoming_future_month():
|
|
today = date(2026, 5, 15)
|
|
plant = Plant.objects.create(name='Rose', pruning_months=[8])
|
|
assert pruning_status(plant, today) == 'upcoming'
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_overdue_crosses_year_boundary():
|
|
# Pruning month December, checked in January
|
|
today = date(2026, 1, 10)
|
|
plant = Plant.objects.create(name='Apple', pruning_months=[12])
|
|
assert pruning_status(plant, today) == 'overdue'
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_overdue_several_months_before_year_boundary():
|
|
# Plant scheduled in August, checked in January — should be overdue
|
|
today = date(2026, 1, 10)
|
|
plant = Plant.objects.create(name='Oak', pruning_months=[8])
|
|
assert pruning_status(plant, today) == 'overdue'
|