feat: import VPC plant by pasting URL directly in search

slug_from_query() detects VPC URLs and bare slugs (must contain a hyphen
to avoid treating single search words as slugs). When matched, the search
bypasses the keyword search and shows the plant directly as a VPC result.
Updated search hint text to make this discoverable.

Fixes: VPC search by English name (e.g. "crowfoot") is a VPC limitation —
their search only indexes Latin titles, not custom fields like Engelse naam.
The URL paste workflow is the reliable workaround.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Stephan Kerkman 2026-05-28 20:20:06 +02:00
parent a48ee9e73d
commit f594105311
3 changed files with 37 additions and 16 deletions

View file

@ -76,6 +76,20 @@ def _slug_from_url(url):
return m.group(1) if m else '' return m.group(1) if m else ''
def slug_from_query(query):
"""Return a VPC slug if query is a VPC URL or bare slug, else empty string."""
query = query.strip()
# Full URL containing /soorten/
slug = _slug_from_url(query)
if slug:
return slug
# Bare slug: lowercase, hyphens, no spaces, must contain at least one hyphen
# (single words are normal search terms, not slugs)
if re.fullmatch(r'[a-z0-9][a-z0-9\-]+', query) and '-' in query:
return query
return ''
def search_species(query): def search_species(query):
""" """
Search VPC and return up to 8 result dicts: Search VPC and return up to 8 result dicts:

View file

@ -2,7 +2,7 @@
{% block title %}Add Plant — PlantDB{% endblock %} {% block title %}Add Plant — PlantDB{% endblock %}
{% block content %} {% block content %}
<h5 class="mb-3">Add a plant</h5> <h5 class="mb-3">Add a plant</h5>
<p class="text-muted small">Search for the species to auto-fill care info, or skip to enter details manually.</p> <p class="text-muted small">Search by name, or paste a VPC URL to import a specific plant directly.</p>
<div class="input-group mb-2"> <div class="input-group mb-2">
<input <input

View file

@ -21,23 +21,30 @@ def species_search(request):
error = None error = None
if len(q) >= 2: if len(q) >= 2:
def _run_perenual(): # If the query looks like a VPC URL or slug, skip the search and show
try: # it directly as a single importable result.
return perenual_search(q), None vpc_slug = vpc_service.slug_from_query(q)
except PerenualError as e: if vpc_slug:
return [], str(e) vpc_results = [{'slug': vpc_slug, 'common_name': vpc_slug.replace('-', ' ').title(),
'scientific_name': '', 'thumbnail_url': '', 'source': 'vpc'}]
else:
def _run_perenual():
try:
return perenual_search(q), None
except PerenualError as e:
return [], str(e)
def _run_vpc(): def _run_vpc():
try: try:
return vpc_service.search_species(q) return vpc_service.search_species(q)
except VPCError: except VPCError:
return [] return []
with ThreadPoolExecutor(max_workers=2) as ex: with ThreadPoolExecutor(max_workers=2) as ex:
f_p = ex.submit(_run_perenual) f_p = ex.submit(_run_perenual)
f_v = ex.submit(_run_vpc) f_v = ex.submit(_run_vpc)
perenual_results, error = f_p.result() perenual_results, error = f_p.result()
vpc_results = f_v.result() vpc_results = f_v.result()
return render(request, 'plants/partials/species_results.html', { return render(request, 'plants/partials/species_results.html', {
'perenual_results': perenual_results, 'perenual_results': perenual_results,