bloombase/plants/tests/test_views.py
Stephan Kerkman b88ba03884 feat: location FK, multi-photo gallery, hide pruning history
- 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>
2026-05-28 18:27:45 +02:00

244 lines
9.5 KiB
Python

import pytest
from django.urls import reverse
from datetime import date
from plants.models import Plant, PruningLog, Location
def make_location(name):
loc, _ = Location.objects.get_or_create(name=name)
return loc
@pytest.mark.django_db
class TestDashboard:
def test_returns_200(self, client):
resp = client.get(reverse('dashboard'))
assert resp.status_code == 200
def test_shows_total_plant_count(self, client):
Plant.objects.create(name='Fern', location=make_location('Office'))
Plant.objects.create(name='Rose', location=make_location('Garden'))
resp = client.get(reverse('dashboard'))
assert resp.context['total_plants'] == 2
def test_blooming_now_plant_appears_in_context(self, client):
today = date.today()
plant = Plant.objects.create(name='Rose', bloom_months=[today.month])
resp = client.get(reverse('dashboard'))
assert plant in resp.context['blooming_now']
def test_non_blooming_plant_not_in_blooming_now(self, client):
today = date.today()
other_month = today.month % 12 + 1
plant = Plant.objects.create(name='Apple', bloom_months=[other_month])
resp = client.get(reverse('dashboard'))
assert plant not in resp.context['blooming_now']
@pytest.mark.django_db
class TestPlantList:
def test_returns_200(self, client):
resp = client.get(reverse('plant_list'))
assert resp.status_code == 200
def test_lists_plants(self, client):
Plant.objects.create(name='Monstera', location=make_location('Living room'))
resp = client.get(reverse('plant_list'))
assert 'Monstera' in resp.content.decode()
def test_search_filters_by_name(self, client):
Plant.objects.create(name='Monstera', location=make_location('Living room'))
Plant.objects.create(name='Rose', location=make_location('Garden'))
resp = client.get(reverse('plant_list') + '?q=Rose')
content = resp.content.decode()
assert 'Rose' in content
assert 'Monstera' not in content
def test_htmx_search_uses_partial_template(self, client):
resp = client.get(
reverse('plant_list') + '?q=rose',
HTTP_HX_REQUEST='true',
)
assert resp.status_code == 200
assert resp.templates[0].name == 'plants/partials/plant_list_results.html'
def test_indoor_filter(self, client):
Plant.objects.create(name='Fern', location=make_location('Office'), is_indoor=True)
Plant.objects.create(name='Rose', location=make_location('Garden'), is_indoor=False)
resp = client.get(reverse('plant_list') + '?filter=indoor')
content = resp.content.decode()
assert 'Fern' in content
assert 'Rose' not in content
@pytest.mark.django_db
class TestPlantDetail:
def test_returns_200(self, client):
plant = Plant.objects.create(name='Fern', location=make_location('Office'))
resp = client.get(reverse('plant_detail', args=[plant.pk]))
assert resp.status_code == 200
def test_404_for_missing_plant(self, client):
resp = client.get(reverse('plant_detail', args=[9999]))
assert resp.status_code == 404
def test_context_contains_plant_and_status(self, client):
plant = Plant.objects.create(name='Rose', pruning_months=[])
resp = client.get(reverse('plant_detail', args=[plant.pk]))
assert resp.context['plant'] == plant
assert resp.context['pruning_status'] == 'no_schedule'
@pytest.mark.django_db
class TestLogPruning:
def test_creates_pruning_log(self, client):
plant = Plant.objects.create(name='Rose', location=make_location('Garden'))
resp = client.post(
reverse('log_pruning', args=[plant.pk]),
{'pruned_on': '2026-05-01', 'notes': ''},
)
assert resp.status_code == 200
assert PruningLog.objects.filter(plant=plant, pruned_on='2026-05-01').exists()
def test_returns_pruning_strip_partial(self, client):
plant = Plant.objects.create(name='Rose', location=make_location('Garden'))
resp = client.post(
reverse('log_pruning', args=[plant.pk]),
{'pruned_on': '2026-05-01', 'notes': ''},
)
assert resp.templates[0].name == 'plants/partials/pruning_strip.html'
@pytest.mark.django_db
class TestSpeciesSearch:
def test_search_page_returns_200(self, client):
resp = client.get(reverse('plant_add'))
assert resp.status_code == 200
def test_htmx_search_returns_partial(self, client, settings):
settings.PERENUAL_API_KEY = ''
resp = client.get(
reverse('species_search') + '?q=rose',
HTTP_HX_REQUEST='true',
)
assert resp.status_code == 200
assert resp.templates[0].name == 'plants/partials/species_results.html'
def test_species_select_creates_species_and_redirects(self, client):
resp = client.post(reverse('species_select'), {
'perenual_id': '42',
'common_name': 'Test Rose',
'scientific_name': 'Rosa testii',
'watering': 'Weekly',
'sunlight': 'Full sun',
'pruning_months': 'February, August',
'api_image_url': '',
})
assert resp.status_code == 302
from plants.models import Species
assert Species.objects.filter(perenual_id=42).exists()
def test_species_select_deduplicates(self, client):
from plants.models import Species
Species.objects.create(perenual_id=99, common_name='Existing')
client.post(reverse('species_select'), {
'perenual_id': '99',
'common_name': 'Existing',
'scientific_name': '',
'watering': '',
'sunlight': '',
'pruning_months': '',
'api_image_url': '',
})
assert Species.objects.filter(perenual_id=99).count() == 1
@pytest.mark.django_db
class TestPlantAdd:
def test_add_with_species_shows_prefilled_form(self, client):
from plants.models import Species
s = Species.objects.create(common_name='Rose', pruning_months=[2, 8])
resp = client.get(reverse('plant_add') + f'?species_id={s.pk}')
assert resp.status_code == 200
assert resp.context['species'] == s
def test_post_creates_plant_and_redirects(self, client):
resp = client.post(reverse('plant_add'), {
'name': 'New Fern',
'new_location': 'Office',
'is_indoor': 'on',
'notes': '',
'pruning_months': [],
})
assert Plant.objects.filter(name='New Fern').exists()
plant = Plant.objects.get(name='New Fern')
assert resp.status_code == 302
assert resp['Location'] == f'/plants/{plant.pk}/'
def test_post_with_species_links_species(self, client):
from plants.models import Species
s = Species.objects.create(common_name='Rose', pruning_months=[2])
client.post(
reverse('plant_add') + f'?species_id={s.pk}',
{'name': 'Garden Rose', 'new_location': 'Garden', 'notes': '', 'pruning_months': ['2']},
)
plant = Plant.objects.get(name='Garden Rose')
assert plant.species == s
@pytest.mark.django_db
class TestPlantEdit:
def test_edit_form_shows_current_values(self, client):
plant = Plant.objects.create(name='Fern', location=make_location('Office'), pruning_months=[3])
resp = client.get(reverse('plant_edit', args=[plant.pk]))
assert resp.status_code == 200
assert resp.context['plant'] == plant
def test_post_updates_plant(self, client):
plant = Plant.objects.create(name='Fern', location=make_location('Office'))
client.post(reverse('plant_edit', args=[plant.pk]), {
'name': 'Updated Fern',
'new_location': 'Bedroom',
'notes': '',
'pruning_months': [],
})
plant.refresh_from_db()
assert plant.name == 'Updated Fern'
@pytest.mark.django_db
class TestPlantDelete:
def test_delete_confirm_page(self, client):
plant = Plant.objects.create(name='Fern', location=make_location('Office'))
resp = client.get(reverse('plant_delete', args=[plant.pk]))
assert resp.status_code == 200
def test_post_deletes_plant(self, client):
plant = Plant.objects.create(name='Fern', location=make_location('Office'))
client.post(reverse('plant_delete', args=[plant.pk]))
assert not Plant.objects.filter(pk=plant.pk).exists()
@pytest.mark.django_db
class TestPruningCalendar:
def test_returns_200(self, client):
resp = client.get(reverse('pruning_calendar'))
assert resp.status_code == 200
def test_overdue_plant_in_context(self, client):
plant = Plant.objects.create(name='Rose', location=make_location('Garden'), pruning_months=[3])
resp = client.get(reverse('pruning_calendar'))
assert plant in resp.context['overdue']
def test_due_this_month_in_context(self, client):
from datetime import date
today = date.today()
plant = Plant.objects.create(name='Apple', location=make_location('Garden'), pruning_months=[today.month])
resp = client.get(reverse('pruning_calendar'))
assert plant in resp.context['due_this_month']
def test_no_schedule_plants_excluded(self, client):
plant = Plant.objects.create(name='Fern', location=make_location('Office'), pruning_months=[])
resp = client.get(reverse('pruning_calendar'))
assert plant not in resp.context['overdue']
assert plant not in resp.context['due_this_month']