feat: pruning calendar grouped by urgency

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Stephan Kerkman 2026-05-28 10:06:55 +02:00
parent eb4dd82b88
commit 370a02e00e
3 changed files with 125 additions and 2 deletions

View file

@ -1,2 +1,68 @@
{% extends "plants/base.html" %} {% extends "plants/base.html" %}
{% block content %}<p>Pruning calendar coming soon.</p>{% endblock %} {% block title %}Pruning Calendar — PlantDB{% endblock %}
{% block content %}
<h5 class="mb-3">✂️ Pruning Calendar</h5>
{% if overdue %}
<h6 class="text-danger">⚠ Overdue</h6>
<div class="list-group mb-3">
{% for plant in overdue %}
<a href="{% url 'plant_detail' plant.pk %}" class="list-group-item list-group-item-action list-group-item-danger d-flex justify-content-between align-items-center">
<div>
<div class="fw-semibold">{{ plant.name }}</div>
<small>{{ plant.location }}</small>
</div>
<span class="badge bg-danger">Overdue</span>
</a>
{% endfor %}
</div>
{% endif %}
{% if due_this_month %}
<h6 style="color:#7d4a00;">This month — {{ month_name }}</h6>
<div class="list-group mb-3">
{% for plant in due_this_month %}
<a href="{% url 'plant_detail' plant.pk %}" class="list-group-item list-group-item-action list-group-item-warning d-flex justify-content-between align-items-center">
<div>
<div class="fw-semibold">{{ plant.name }}</div>
<small>{{ plant.location }}</small>
</div>
<span class="badge bg-warning text-dark">This month</span>
</a>
{% endfor %}
</div>
{% endif %}
{% if next_month_plants %}
<h6 class="text-muted">Next — {{ next_month_name }}</h6>
<div class="list-group mb-3">
{% for plant in next_month_plants %}
<a href="{% url 'plant_detail' plant.pk %}" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
<div>
<div class="fw-semibold">{{ plant.name }}</div>
<small>{{ plant.location }}</small>
</div>
<span class="badge bg-secondary">{{ next_month_name }}</span>
</a>
{% endfor %}
</div>
{% endif %}
{% if later %}
<details>
<summary class="text-muted mb-2">Later this year ({{ later|length }})</summary>
<div class="list-group mb-3">
{% for plant in later %}
<a href="{% url 'plant_detail' plant.pk %}" class="list-group-item list-group-item-action">
<div class="fw-semibold">{{ plant.name }}</div>
<small class="text-muted">{{ plant.location }} · Months: {{ plant.pruning_months|join:", " }}</small>
</a>
{% endfor %}
</div>
</details>
{% endif %}
{% if not overdue and not due_this_month and not next_month_plants and not later %}
<p class="text-muted text-center mt-4">No pruning schedules set. Add pruning months to your plants.</p>
{% endif %}
{% endblock %}

View file

@ -215,3 +215,28 @@ class TestPlantDelete:
plant = Plant.objects.create(name='Fern', location='Office') plant = Plant.objects.create(name='Fern', location='Office')
client.post(reverse('plant_delete', args=[plant.pk])) client.post(reverse('plant_delete', args=[plant.pk]))
assert not Plant.objects.filter(pk=plant.pk).exists() 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='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='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='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']

View file

@ -5,4 +5,36 @@ from plants.utils.pruning import pruning_status
def pruning_calendar(request): def pruning_calendar(request):
return render(request, 'plants/pruning_calendar.html', {}) today = date.today()
next_month_num = today.month % 12 + 1
next_month_year = today.year + (1 if today.month == 12 else 0)
next_month_date = date(next_month_year, next_month_num, 1)
all_plants = list(
Plant.objects.select_related('species').prefetch_related('pruning_logs')
)
overdue, due_this_month, next_month_plants, later = [], [], [], []
for plant in all_plants:
if not plant.pruning_months:
continue
status = pruning_status(plant, today)
if status == 'overdue':
overdue.append(plant)
elif status == 'due_this_month':
due_this_month.append(plant)
elif next_month_num in plant.pruning_months:
next_month_plants.append(plant)
else:
later.append(plant)
return render(request, 'plants/pruning_calendar.html', {
'overdue': overdue,
'due_this_month': due_this_month,
'next_month_plants': next_month_plants,
'later': later,
'today': today,
'month_name': today.strftime('%B'),
'next_month_name': next_month_date.strftime('%B'),
})