feat: implement step 3 field picker view and template
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
62fdbf7029
commit
adea702a77
2 changed files with 221 additions and 2 deletions
|
|
@ -1,5 +1,133 @@
|
|||
{% extends "plants/base.html" %}
|
||||
{% block title %}Review fields — PlantDB{% endblock %}
|
||||
{% block title %}Review & save — PlantDB{% endblock %}
|
||||
{% block content %}
|
||||
<p>Fields step (coming soon)</p>
|
||||
|
||||
<div class="d-flex align-items-center gap-2 mb-3">
|
||||
<a href="{% url 'identify_confirm' %}" class="btn btn-sm btn-outline-secondary">←</a>
|
||||
<h5 class="mb-0">Review & save</h5>
|
||||
</div>
|
||||
|
||||
<p class="text-muted small mb-3">Tap a tile to switch source. Green border = selected.</p>
|
||||
|
||||
{% if vpc_match %}
|
||||
<div class="d-flex align-items-center gap-2 p-2 rounded mb-3" style="background:#d8f3dc;">
|
||||
<span style="font-size:18px;">🏪</span>
|
||||
<div class="flex-grow-1 small">
|
||||
<div class="fw-semibold">{{ vpc_match.common_name }}</div>
|
||||
<div class="text-muted" style="font-size:11px;">vasteplantencatalogus.nl</div>
|
||||
</div>
|
||||
{% if vpc_results|length > 1 %}
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||
change
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
{% for r in vpc_results %}
|
||||
<li>
|
||||
<a class="dropdown-item {% if forloop.counter0 == vpc_idx %}active{% endif %}"
|
||||
href="{% url 'identify_fields' %}?vpc_idx={{ forloop.counter0 }}">
|
||||
{{ r.common_name }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning py-2 small mb-3">
|
||||
No VPC data found — using Pl@ntNet data only.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Common name -->
|
||||
<div class="mb-3">
|
||||
<div class="text-muted small text-uppercase fw-semibold mb-2" style="letter-spacing:.05em; font-size:10px;">Common name</div>
|
||||
<div class="d-flex gap-2">
|
||||
<div class="flex-fill source-tile p-2 border rounded"
|
||||
style="cursor:pointer; {% if not vpc_match %}border-color:#52b788 !important; background:#f0faf4;{% endif %}"
|
||||
data-field="common_name" data-value="plantnet" onclick="selectSource(this)">
|
||||
<span class="badge mb-1" style="background:#52b788; font-size:9px;">Pl@ntNet</span>
|
||||
<div class="small fw-semibold">{{ selected.common_names.0|default:selected.scientific_name }}</div>
|
||||
{% if selected.common_names|length > 1 %}
|
||||
<div class="text-muted" style="font-size:10px;">{{ selected.common_names.1 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if vpc_match %}
|
||||
<div class="flex-fill source-tile p-2 border rounded"
|
||||
style="cursor:pointer; border-color:#52b788 !important; background:#f0faf4;"
|
||||
data-field="common_name" data-value="vpc" onclick="selectSource(this)">
|
||||
<span class="badge mb-1" style="background:#6c757d; font-size:9px;">VPC</span>
|
||||
<div class="small fw-semibold">{{ vpc_match.common_name }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<input type="hidden" name="common_name_source" id="common_name_source"
|
||||
value="{% if vpc_match %}vpc{% else %}plantnet{% endif %}">
|
||||
</div>
|
||||
|
||||
<!-- Scientific name -->
|
||||
<div class="mb-3">
|
||||
<div class="text-muted small text-uppercase fw-semibold mb-2" style="letter-spacing:.05em; font-size:10px;">Scientific name</div>
|
||||
<div class="d-flex gap-2">
|
||||
<div class="flex-fill source-tile p-2 border rounded"
|
||||
style="cursor:pointer; border-color:#52b788 !important; background:#f0faf4;"
|
||||
data-field="scientific_name" data-value="plantnet" onclick="selectSource(this)">
|
||||
<span class="badge mb-1" style="background:#52b788; font-size:9px;">Pl@ntNet</span>
|
||||
<div class="small fw-semibold fst-italic">{{ selected.scientific_name }}</div>
|
||||
</div>
|
||||
{% if vpc_match %}
|
||||
<div class="flex-fill source-tile p-2 border rounded"
|
||||
style="cursor:pointer;"
|
||||
data-field="scientific_name" data-value="vpc" onclick="selectSource(this)">
|
||||
<span class="badge mb-1" style="background:#6c757d; font-size:9px;">VPC</span>
|
||||
<div class="small fw-semibold fst-italic">{{ vpc_match.scientific_name }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<input type="hidden" name="scientific_name_source" id="scientific_name_source" value="plantnet">
|
||||
</div>
|
||||
|
||||
{% if vpc_match %}
|
||||
<hr class="my-3">
|
||||
<div class="text-muted small text-uppercase fw-semibold mb-2" style="letter-spacing:.05em; font-size:10px;">
|
||||
VPC care data <span class="badge ms-1" style="background:#e9ecef; color:#6c757d; font-size:9px;">from VPC</span>
|
||||
</div>
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-12">
|
||||
<div class="p-2 rounded" style="background:#f8f9fa; font-size:12px;">
|
||||
<div class="text-muted" style="font-size:10px; text-transform:uppercase; margin-bottom:2px;">Loaded after save</div>
|
||||
<div class="small text-muted">bloom months · height · sunlight · frost hardiness · planting density</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<hr class="my-3">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold small">My plant name <span class="text-danger">*</span></label>
|
||||
<input type="text" name="name" class="form-control"
|
||||
value="{{ selected.scientific_name }}" required autofocus>
|
||||
<div class="text-muted" style="font-size:11px; margin-top:3px;">How you refer to this plant in your collection</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-success w-100">Save plant ✓</button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
function selectSource(el) {
|
||||
const field = el.dataset.field;
|
||||
document.querySelectorAll(`.source-tile[data-field="${field}"]`).forEach(t => {
|
||||
t.style.borderColor = '';
|
||||
t.style.background = '';
|
||||
});
|
||||
el.style.borderColor = '#52b788';
|
||||
el.style.background = '#f0faf4';
|
||||
document.getElementById(field + '_source').value = el.dataset.value;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -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'"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue