220 lines
7.7 KiB
Python
220 lines
7.7 KiB
Python
import logging
|
|
import os
|
|
import uuid
|
|
import requests
|
|
from django.shortcuts import render, redirect
|
|
from django.core.files.base import ContentFile
|
|
from django.core.files.storage import default_storage
|
|
from plants.services import plantnet
|
|
from plants.services.plantnet import PlantNetError
|
|
from plants.services.vpc import search_species as vpc_search
|
|
from plants.models import Plant, PlantPhoto, Species, Location
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_SESSION_KEYS = [
|
|
'identify_matches', 'identify_image_paths', 'identify_selected',
|
|
'identify_vpc_results', 'identify_vpc_idx',
|
|
]
|
|
|
|
_VALID_ORGANS = {'flower', 'leaf', 'fruit', 'bark', 'habit', 'auto'}
|
|
|
|
|
|
def _clear_session(request):
|
|
for key in _SESSION_KEYS:
|
|
request.session.pop(key, None)
|
|
|
|
|
|
def identify_upload(request):
|
|
if request.method != 'POST':
|
|
return render(request, 'plants/identify_upload.html', {'active_tab': 'home'})
|
|
|
|
files = request.FILES.getlist('images')
|
|
organs = request.POST.getlist('organs')
|
|
|
|
if not files:
|
|
return render(request, 'plants/identify_upload.html',
|
|
{'error': 'Please select at least one photo.', 'active_tab': 'home'})
|
|
|
|
scan_id = uuid.uuid4().hex
|
|
saved_paths = []
|
|
images = []
|
|
|
|
for i, f in enumerate(files[:5]):
|
|
organ = organs[i] if i < len(organs) else 'auto'
|
|
if organ not in _VALID_ORGANS:
|
|
organ = 'auto'
|
|
file_bytes = f.read()
|
|
name = f'plants/temp_identify/{scan_id}_{i}.jpg'
|
|
default_storage.save(name, ContentFile(file_bytes))
|
|
saved_paths.append({'path': name, 'organ': organ})
|
|
images.append((file_bytes, organ))
|
|
|
|
error_msg = None
|
|
try:
|
|
matches = plantnet.identify(images)
|
|
except PlantNetError as e:
|
|
error_msg = str(e)
|
|
except Exception as e:
|
|
logger.exception('identify_upload unexpected error: %s', e)
|
|
error_msg = 'Identification failed — please try again.'
|
|
|
|
if error_msg:
|
|
for p in saved_paths:
|
|
try:
|
|
if default_storage.exists(p['path']):
|
|
default_storage.delete(p['path'])
|
|
except Exception:
|
|
pass
|
|
return render(request, 'plants/identify_upload.html', {'error': error_msg, 'active_tab': 'home'})
|
|
|
|
request.session['identify_matches'] = matches
|
|
request.session['identify_image_paths'] = saved_paths
|
|
return redirect('identify_confirm')
|
|
|
|
|
|
def identify_confirm(request):
|
|
if 'identify_matches' not in request.session:
|
|
return redirect('identify_upload')
|
|
|
|
matches = request.session['identify_matches']
|
|
if not matches:
|
|
return redirect('identify_upload')
|
|
|
|
if request.method == 'POST':
|
|
try:
|
|
idx = int(request.POST.get('match_idx', 0))
|
|
idx = max(0, min(idx, len(matches) - 1))
|
|
except (ValueError, TypeError):
|
|
idx = 0
|
|
|
|
selected = matches[idx]
|
|
request.session['identify_selected'] = selected
|
|
|
|
try:
|
|
vpc_results = vpc_search(selected['scientific_name'])[:3]
|
|
except Exception:
|
|
vpc_results = []
|
|
request.session['identify_vpc_results'] = vpc_results
|
|
request.session['identify_vpc_idx'] = 0
|
|
|
|
return redirect('identify_fields')
|
|
|
|
locations = list(Location.objects.all())
|
|
return render(request, 'plants/identify_confirm.html', {
|
|
'matches': matches,
|
|
'locations': locations,
|
|
})
|
|
|
|
|
|
def _apply_vpc_care_fields(species, vpc_result):
|
|
from plants.services.vpc import scrape_plant, VPCError
|
|
slug = vpc_result.get('slug', '')
|
|
if not slug:
|
|
return
|
|
try:
|
|
data = scrape_plant(slug)
|
|
except VPCError:
|
|
return
|
|
for field in ('bloom_months', 'max_height_cm', 'sunlight', 'growth_rate',
|
|
'description', 'api_image_url', 'frost_hardiness_c',
|
|
'planting_density_m2'):
|
|
val = data.get(field)
|
|
if val not in (None, '', []):
|
|
setattr(species, field, val)
|
|
species.vpc_slug = slug
|
|
species.vpc_raw = data
|
|
species.save()
|
|
|
|
|
|
def identify_fields(request):
|
|
if 'identify_selected' not in request.session:
|
|
return redirect('identify_upload')
|
|
|
|
selected = request.session['identify_selected']
|
|
vpc_results = request.session.get('identify_vpc_results', [])
|
|
vpc_idx = request.session.get('identify_vpc_idx', 0)
|
|
|
|
if request.method == 'GET' and 'vpc_idx' in request.GET:
|
|
try:
|
|
new_idx = int(request.GET['vpc_idx'])
|
|
vpc_idx = max(0, min(new_idx, len(vpc_results) - 1))
|
|
request.session['identify_vpc_idx'] = vpc_idx
|
|
except (ValueError, TypeError):
|
|
pass
|
|
return redirect('identify_fields')
|
|
|
|
vpc_match = vpc_results[vpc_idx] if vpc_results else None
|
|
|
|
if request.method == 'POST':
|
|
common_name_source = request.POST.get('common_name_source', 'plantnet')
|
|
scientific_name_source = request.POST.get('scientific_name_source', 'plantnet')
|
|
name = request.POST.get('name', '').strip() or selected['scientific_name']
|
|
|
|
if common_name_source == 'vpc' and vpc_match:
|
|
common_name = vpc_match['common_name']
|
|
else:
|
|
common_name = (selected['common_names'] or [selected['scientific_name']])[0]
|
|
|
|
if scientific_name_source == 'vpc' and vpc_match:
|
|
scientific_name = vpc_match['scientific_name']
|
|
else:
|
|
scientific_name = selected['scientific_name']
|
|
|
|
species = Species.objects.filter(scientific_name=scientific_name).first()
|
|
if not species:
|
|
species = Species.objects.create(
|
|
scientific_name=scientific_name,
|
|
common_name=common_name,
|
|
)
|
|
else:
|
|
species.common_name = common_name
|
|
species.save(update_fields=['common_name'])
|
|
|
|
if vpc_match:
|
|
_apply_vpc_care_fields(species, vpc_match)
|
|
|
|
plant = Plant.objects.create(name=name, species=species)
|
|
|
|
try:
|
|
# Download Pl@ntNet reference image as thumbnail
|
|
plantnet_img_url = selected.get('image_url')
|
|
if plantnet_img_url:
|
|
try:
|
|
r = requests.get(plantnet_img_url, timeout=10)
|
|
r.raise_for_status()
|
|
thumb_filename = f'plantnet_{uuid.uuid4().hex}.jpg'
|
|
thumb = PlantPhoto(plant=plant, is_thumbnail=True)
|
|
thumb.image.save(thumb_filename, ContentFile(r.content), save=True)
|
|
except Exception:
|
|
logger.exception('identify_fields: failed to download Pl@ntNet image')
|
|
|
|
# Save uploaded scan photos; only first becomes thumbnail if no Pl@ntNet image
|
|
image_paths = request.session.get('identify_image_paths', [])
|
|
for i, p in enumerate(image_paths):
|
|
if not default_storage.exists(p['path']):
|
|
continue
|
|
try:
|
|
with default_storage.open(p['path'], 'rb') as fh:
|
|
data = fh.read()
|
|
filename = os.path.basename(p['path'])
|
|
photo = PlantPhoto(plant=plant, is_thumbnail=(i == 0 and not plantnet_img_url))
|
|
photo.image.save(filename, ContentFile(data), save=True)
|
|
except Exception:
|
|
logger.exception('identify_fields: failed to save photo %s', p['path'])
|
|
finally:
|
|
try:
|
|
default_storage.delete(p['path'])
|
|
except Exception:
|
|
pass
|
|
finally:
|
|
_clear_session(request)
|
|
|
|
return redirect('plant_detail', pk=plant.pk)
|
|
|
|
return render(request, 'plants/identify_fields.html', {
|
|
'selected': selected,
|
|
'vpc_results': vpc_results,
|
|
'vpc_idx': vpc_idx,
|
|
'vpc_match': vpc_match,
|
|
})
|