feat: pruning calendar grouped by urgency
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
eb4dd82b88
commit
370a02e00e
3 changed files with 125 additions and 2 deletions
|
|
@ -1,2 +1,68 @@
|
|||
{% 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 %}
|
||||
|
|
|
|||
|
|
@ -215,3 +215,28 @@ class TestPlantDelete:
|
|||
plant = Plant.objects.create(name='Fern', 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='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']
|
||||
|
|
|
|||
|
|
@ -5,4 +5,36 @@ from plants.utils.pruning import pruning_status
|
|||
|
||||
|
||||
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'),
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue