From adea702a779912cb1cd286807ffc030dc2538e23 Mon Sep 17 00:00:00 2001 From: Stephan Kerkman Date: Sat, 30 May 2026 17:48:53 +0200 Subject: [PATCH] feat: implement step 3 field picker view and template Co-Authored-By: Claude Sonnet 4.6 --- plants/templates/plants/identify_fields.html | 132 ++++++++++++++++++- plants/tests/test_identify_views.py | 91 +++++++++++++ 2 files changed, 221 insertions(+), 2 deletions(-) diff --git a/plants/templates/plants/identify_fields.html b/plants/templates/plants/identify_fields.html index 8b083e8..33e5a8b 100644 --- a/plants/templates/plants/identify_fields.html +++ b/plants/templates/plants/identify_fields.html @@ -1,5 +1,133 @@ {% extends "plants/base.html" %} -{% block title %}Review fields — PlantDB{% endblock %} +{% block title %}Review & save — PlantDB{% endblock %} {% block content %} -

Fields step (coming soon)

+ +
+ +
Review & save
+
+ +

Tap a tile to switch source. Green border = selected.

+ +{% if vpc_match %} +
+ 🏪 +
+
{{ vpc_match.common_name }}
+
vasteplantencatalogus.nl
+
+ {% if vpc_results|length > 1 %} + + {% endif %} +
+{% else %} +
+ No VPC data found — using Pl@ntNet data only. +
+{% endif %} + +
+ {% csrf_token %} + + +
+
Common name
+
+
+ Pl@ntNet +
{{ selected.common_names.0|default:selected.scientific_name }}
+ {% if selected.common_names|length > 1 %} +
{{ selected.common_names.1 }}
+ {% endif %} +
+ {% if vpc_match %} +
+ VPC +
{{ vpc_match.common_name }}
+
+ {% endif %} +
+ +
+ + +
+
Scientific name
+
+
+ Pl@ntNet +
{{ selected.scientific_name }}
+
+ {% if vpc_match %} +
+ VPC +
{{ vpc_match.scientific_name }}
+
+ {% endif %} +
+ +
+ + {% if vpc_match %} +
+
+ VPC care data from VPC +
+
+
+
+
Loaded after save
+
bloom months · height · sunlight · frost hardiness · planting density
+
+
+
+ {% endif %} + +
+ +
+ + +
How you refer to this plant in your collection
+
+ + +
+ + {% endblock %} diff --git a/plants/tests/test_identify_views.py b/plants/tests/test_identify_views.py index 1792da2..1de8960 100644 --- a/plants/tests/test_identify_views.py +++ b/plants/tests/test_identify_views.py @@ -116,3 +116,94 @@ def test_confirm_post_handles_vpc_failure(client): resp = client.post(reverse('identify_confirm'), {'match_idx': '0'}) assert resp.status_code == 302 assert client.session['identify_vpc_results'] == [] + + +from plants.models import Plant, Species + + +def _set_fields_session(client, vpc_results=None): + session = client.session + session['identify_selected'] = { + 'scientific_name': 'Liatris spicata', + 'common_names': ['Blazing star', 'Pronkster'], + 'score': 0.94, 'gbif_id': '123', 'family': 'Asteraceae', 'genus': 'Liatris', + } + session['identify_vpc_results'] = vpc_results if vpc_results is not None else [ + {'slug': 'liatris-spicata', 'common_name': 'Liatris spicata', 'scientific_name': 'Liatris spicata', 'thumbnail_url': '', 'source': 'vpc'}, + ] + session['identify_vpc_idx'] = 0 + session['identify_matches'] = [] + session['identify_image_paths'] = [] + session.save() + + +@pytest.mark.django_db +def test_fields_get_with_session(client): + _set_fields_session(client) + resp = client.get(reverse('identify_fields')) + assert resp.status_code == 200 + assert b'Liatris spicata' in resp.content + + +@pytest.mark.django_db +def test_fields_post_creates_species_and_plant(client): + _set_fields_session(client) + with patch('plants.views.identify.default_storage') as mock_storage: + mock_storage.exists.return_value = False + with patch('plants.views.identify._apply_vpc_care_fields'): + resp = client.post(reverse('identify_fields'), { + 'common_name_source': 'plantnet', + 'scientific_name_source': 'plantnet', + 'name': 'Liatris', + }) + assert resp.status_code == 302 + assert Plant.objects.filter(name='Liatris').exists() + species = Species.objects.get(scientific_name='Liatris spicata') + assert species.common_name == 'Blazing star' + + +@pytest.mark.django_db +def test_fields_post_deduplicates_species(client): + Species.objects.create(scientific_name='Liatris spicata', common_name='Old name') + _set_fields_session(client) + with patch('plants.views.identify.default_storage') as mock_storage: + mock_storage.exists.return_value = False + with patch('plants.views.identify._apply_vpc_care_fields'): + client.post(reverse('identify_fields'), { + 'common_name_source': 'plantnet', + 'scientific_name_source': 'plantnet', + 'name': 'Liatris', + }) + assert Species.objects.filter(scientific_name='Liatris spicata').count() == 1 + + +@pytest.mark.django_db +def test_fields_post_clears_session(client): + _set_fields_session(client) + with patch('plants.views.identify.default_storage') as mock_storage: + mock_storage.exists.return_value = False + with patch('plants.views.identify._apply_vpc_care_fields'): + client.post(reverse('identify_fields'), { + 'common_name_source': 'plantnet', + 'scientific_name_source': 'plantnet', + 'name': 'Liatris', + }) + assert 'identify_selected' not in client.session + + +@pytest.mark.django_db +def test_fields_post_uses_vpc_common_name(client): + _set_fields_session(client, vpc_results=[ + {'slug': 'liatris-spicata-kobold', 'common_name': "Liatris 'Kobold'", + 'scientific_name': 'Liatris spicata', 'thumbnail_url': '', 'source': 'vpc'}, + ]) + with patch('plants.views.identify.default_storage') as mock_storage: + mock_storage.exists.return_value = False + with patch('plants.views.identify._apply_vpc_care_fields'): + client.post(reverse('identify_fields'), { + 'common_name_source': 'vpc', + 'scientific_name_source': 'plantnet', + 'name': 'Liatris Kobold', + }) + species = Species.objects.get(scientific_name='Liatris spicata') + assert species.common_name == "Liatris 'Kobold'"