diff --git a/plants/templates/plants/partials/plant_list_results.html b/plants/templates/plants/partials/plant_list_results.html new file mode 100644 index 0000000..4caa2a9 --- /dev/null +++ b/plants/templates/plants/partials/plant_list_results.html @@ -0,0 +1,24 @@ +{% for plant in plants %} + +
+
+ {% if plant.photo %} + + {% elif plant.species and plant.species.api_image_url %} + + {% else %} +
๐ŸŒฟ
+ {% endif %} +
+
{{ plant.name }}
+ + {{ plant.location }} + ยท {% if plant.is_indoor %}Indoor{% else %}Outdoor{% endif %} + +
+
+
+
+{% empty %} +

No plants found.

+{% endfor %} diff --git a/plants/templates/plants/plant_list.html b/plants/templates/plants/plant_list.html new file mode 100644 index 0000000..7172eae --- /dev/null +++ b/plants/templates/plants/plant_list.html @@ -0,0 +1,27 @@ +{% extends "plants/base.html" %} +{% block title %}Plants โ€” PlantDB{% endblock %} +{% block content %} +
+ +
+ +
+ All + Indoor + Outdoor +
+ +
+ {% include "plants/partials/plant_list_results.html" %} +
+{% endblock %} diff --git a/plants/templates/plants/pruning_calendar.html b/plants/templates/plants/pruning_calendar.html new file mode 100644 index 0000000..ab5446a --- /dev/null +++ b/plants/templates/plants/pruning_calendar.html @@ -0,0 +1,2 @@ +{% extends "plants/base.html" %} +{% block content %}

Pruning calendar coming soon.

{% endblock %} diff --git a/plants/tests/test_views.py b/plants/tests/test_views.py index 5aa9252..dee8134 100644 --- a/plants/tests/test_views.py +++ b/plants/tests/test_views.py @@ -31,3 +31,39 @@ class TestDashboard: ) resp = client.get(reverse('dashboard')) assert plant in resp.context['due_this_month'] + + +@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='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='Living room') + Plant.objects.create(name='Rose', 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='Office', is_indoor=True) + Plant.objects.create(name='Rose', 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 diff --git a/plants/urls.py b/plants/urls.py index 3a3dc2f..4a0611f 100644 --- a/plants/urls.py +++ b/plants/urls.py @@ -1,18 +1,13 @@ from django.urls import path -from plants.views import dashboard - -# Stub views for URLs referenced in templates โ€” replaced in later tasks -from django.http import HttpResponse - - -def _stub(request, *args, **kwargs): - return HttpResponse('stub') - +from plants.views import dashboard, plants, pruning urlpatterns = [ path('', dashboard.dashboard, name='dashboard'), - path('plants/', _stub, name='plant_list'), - path('plants/add/', _stub, name='plant_add'), - path('plants//', _stub, name='plant_detail'), - path('pruning/', _stub, name='pruning_calendar'), + path('plants/', plants.plant_list, name='plant_list'), + path('plants/add/', plants.plant_add, name='plant_add'), + path('plants//', plants.plant_detail, name='plant_detail'), + path('plants//edit/', plants.plant_edit, name='plant_edit'), + path('plants//delete/', plants.plant_delete, name='plant_delete'), + path('plants//log-pruning/', plants.log_pruning, name='log_pruning'), + path('pruning/', pruning.pruning_calendar, name='pruning_calendar'), ] diff --git a/plants/views/plants.py b/plants/views/plants.py new file mode 100644 index 0000000..6162f57 --- /dev/null +++ b/plants/views/plants.py @@ -0,0 +1,47 @@ +from datetime import date +from django.shortcuts import render, get_object_or_404, redirect +from django.views.decorators.http import require_POST +from plants.models import Plant, Species, PruningLog +from plants.forms import PlantForm, PruningLogForm +from plants.utils.pruning import pruning_status + + +def plant_list(request): + qs = Plant.objects.select_related('species').all() + q = request.GET.get('q', '') + indoor_filter = request.GET.get('filter', '') + + if q: + qs = qs.filter(name__icontains=q) | qs.filter(location__icontains=q) + if indoor_filter == 'indoor': + qs = qs.filter(is_indoor=True) + elif indoor_filter == 'outdoor': + qs = qs.filter(is_indoor=False) + + template = ( + 'plants/partials/plant_list_results.html' + if request.htmx + else 'plants/plant_list.html' + ) + return render(request, template, {'plants': qs, 'q': q, 'filter': indoor_filter}) + + +def plant_detail(request, pk): + pass + + +def plant_add(request): + pass + + +def plant_edit(request, pk): + pass + + +def plant_delete(request, pk): + pass + + +@require_POST +def log_pruning(request, pk): + pass diff --git a/plants/views/pruning.py b/plants/views/pruning.py new file mode 100644 index 0000000..28aac4a --- /dev/null +++ b/plants/views/pruning.py @@ -0,0 +1,8 @@ +from datetime import date +from django.shortcuts import render +from plants.models import Plant +from plants.utils.pruning import pruning_status + + +def pruning_calendar(request): + return render(request, 'plants/pruning_calendar.html', {})