bloombase/plants/views/species.py
Stephan Kerkman f594105311 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>
2026-05-28 20:20:06 +02:00

135 lines
5 KiB
Python

from concurrent.futures import ThreadPoolExecutor
from django.shortcuts import render, redirect, get_object_or_404
from django.urls import reverse
from django.views.decorators.http import require_POST
from plants.models import Species
from plants.services.perenual import search_species as perenual_search, PerenualError
from plants.services import vpc as vpc_service
from plants.services.vpc import VPCError
_MONTH_MAP = {
'January': 1, 'February': 2, 'March': 3, 'April': 4,
'May': 5, 'June': 6, 'July': 7, 'August': 8,
'September': 9, 'October': 10, 'November': 11, 'December': 12,
}
def species_search(request):
q = request.GET.get('q', '').strip()
perenual_results = []
vpc_results = []
error = None
if len(q) >= 2:
# If the query looks like a VPC URL or slug, skip the search and show
# it directly as a single importable result.
vpc_slug = vpc_service.slug_from_query(q)
if vpc_slug:
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():
try:
return vpc_service.search_species(q)
except VPCError:
return []
with ThreadPoolExecutor(max_workers=2) as ex:
f_p = ex.submit(_run_perenual)
f_v = ex.submit(_run_vpc)
perenual_results, error = f_p.result()
vpc_results = f_v.result()
return render(request, 'plants/partials/species_results.html', {
'perenual_results': perenual_results,
'vpc_results': vpc_results,
'q': q,
'error': error,
})
def species_select(request):
perenual_id = int(request.POST['perenual_id'])
if not Species.objects.filter(perenual_id=perenual_id).exists():
pruning_str = request.POST.get('pruning_months', '')
pruning_months = [
_MONTH_MAP[m.strip()]
for m in pruning_str.split(',')
if m.strip() in _MONTH_MAP
]
Species.objects.create(
perenual_id=perenual_id,
common_name=request.POST.get('common_name', ''),
scientific_name=request.POST.get('scientific_name', ''),
watering=request.POST.get('watering', ''),
sunlight=request.POST.get('sunlight', ''),
pruning_months=pruning_months,
api_image_url=request.POST.get('api_image_url', ''),
)
species = Species.objects.get(perenual_id=perenual_id)
return redirect(f"{reverse('plant_add')}?species_id={species.pk}")
def vpc_select(request):
slug = request.POST.get('vpc_slug', '').strip()
if not slug:
return redirect(reverse('plant_add'))
existing = Species.objects.filter(vpc_slug=slug).first()
if existing:
return redirect(f"{reverse('plant_add')}?species_id={existing.pk}")
try:
data = vpc_service.scrape_plant(slug)
except VPCError:
# Fall back to plant_add without species pre-fill
return redirect(f"{reverse('plant_add')}?species_id=skip")
species = Species.objects.create(
common_name=data['common_name'],
scientific_name=data['scientific_name'],
bloom_months=data['bloom_months'],
max_height_cm=data['max_height_cm'],
sunlight=data['sunlight'],
growth_rate=data['growth_rate'],
description=data['description'],
api_image_url=data['api_image_url'],
vpc_slug=slug,
vpc_raw=data,
)
return redirect(f"{reverse('plant_add')}?species_id={species.pk}")
@require_POST
def vpc_species_reset(request, species_pk):
"""Restore species fields to the original VPC scrape values."""
species = get_object_or_404(Species, pk=species_pk, vpc_slug__gt='')
raw = species.vpc_raw
if not raw:
# vpc_raw missing (imported before this feature) — re-scrape
try:
raw = vpc_service.scrape_plant(species.vpc_slug)
species.vpc_raw = raw
except VPCError:
pass
if raw:
species.common_name = raw.get('common_name', species.common_name)
species.scientific_name = raw.get('scientific_name', species.scientific_name)
species.bloom_months = raw.get('bloom_months', species.bloom_months)
species.max_height_cm = raw.get('max_height_cm', species.max_height_cm)
species.sunlight = raw.get('sunlight', species.sunlight)
species.growth_rate = raw.get('growth_rate', species.growth_rate)
species.description = raw.get('description', species.description)
species.api_image_url = raw.get('api_image_url', species.api_image_url)
species.save()
# Redirect back to the plant that triggered the reset, if provided
next_url = request.POST.get('next') or reverse('plant_list')
return redirect(next_url)