Compare commits

..

No commits in common. "798240f968de05da72691cf23474a92da62e931a" and "f308d006e4e1eef239b64889ad4b6ae094bb4afa" have entirely different histories.

7 changed files with 25 additions and 102 deletions

View file

@ -75,29 +75,3 @@ PLANTNET_API_KEY = os.environ.get('PLANTNET_API_KEY', '')
_trusted = os.environ.get('CSRF_TRUSTED_ORIGINS', '')
if _trusted:
CSRF_TRUSTED_ORIGINS = [o.strip() for o in _trusted.split(',')]
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
},
},
'root': {
'handlers': ['console'],
'level': 'WARNING',
},
'loggers': {
'django': {
'handlers': ['console'],
'level': 'WARNING',
'propagate': False,
},
'plants': {
'handlers': ['console'],
'level': 'DEBUG',
'propagate': False,
},
},
}

View file

@ -24,8 +24,6 @@ class Species(models.Model):
pruning_months = models.JSONField(default=list)
bloom_months = models.JSONField(default=list)
api_image_url = models.URLField(blank=True)
frost_hardiness_c = models.SmallIntegerField(null=True, blank=True)
planting_density_m2 = models.PositiveSmallIntegerField(null=True, blank=True)
perenual_id = models.IntegerField(unique=True, null=True, blank=True)
vpc_slug = models.CharField(max_length=200, blank=True, db_index=True)
vpc_raw = models.JSONField(null=True, blank=True)

View file

@ -53,17 +53,14 @@ def identify(images):
if not results:
raise PlantNetError('No match found — try a different photo or organ.')
try:
return [
{
'scientific_name': r['species']['scientificNameWithoutAuthor'],
'common_names': r['species'].get('commonNames', []),
'score': r['score'],
'gbif_id': (r.get('gbif') or {}).get('id'),
'family': r['species'].get('family', {}).get('scientificNameWithoutAuthor', ''),
'genus': r['species'].get('genus', {}).get('scientificNameWithoutAuthor', ''),
}
for r in results[:5]
]
except (KeyError, TypeError) as exc:
raise PlantNetError('Unexpected API response format.') from exc
return [
{
'scientific_name': r['species']['scientificNameWithoutAuthor'],
'common_names': r['species'].get('commonNames', []),
'score': r['score'],
'gbif_id': (r.get('gbif') or {}).get('id'),
'family': r['species'].get('family', {}).get('scientificNameWithoutAuthor', ''),
'genus': r['species'].get('genus', {}).get('scientificNameWithoutAuthor', ''),
}
for r in results[:5]
]

View file

@ -13,7 +13,7 @@
{% if not photo.is_thumbnail %}
<form method="post" action="{% url 'set_thumbnail' photo.pk %}"
hx-post="{% url 'set_thumbnail' photo.pk %}"
hx-target="#photo-gallery" hx-swap="innerHTML">
hx-target="#photo-gallery" hx-swap="outerHTML">
{% csrf_token %}
<button type="submit" class="btn btn-outline-success btn-sm py-0 px-1" style="font-size:.7rem;" title="Set as thumbnail"></button>
</form>
@ -22,7 +22,7 @@
{% endif %}
<form method="post" action="{% url 'delete_photo' photo.pk %}"
hx-post="{% url 'delete_photo' photo.pk %}"
hx-target="#photo-gallery" hx-swap="innerHTML">
hx-target="#photo-gallery" hx-swap="outerHTML">
{% csrf_token %}
<button type="submit" class="btn btn-outline-danger btn-sm py-0 px-1" style="font-size:.7rem;" title="Delete"></button>
</form>
@ -34,26 +34,13 @@
{% endwith %}
<form method="post" action="{% url 'upload_photo' plant.pk %}" enctype="multipart/form-data"
hx-post="{% url 'upload_photo' plant.pk %}" hx-target="#photo-gallery" hx-swap="innerHTML"
hx-encoding="multipart/form-data" hx-indicator="#photo-upload-spinner">
hx-post="{% url 'upload_photo' plant.pk %}" hx-target="#photo-gallery" hx-swap="outerHTML"
hx-encoding="multipart/form-data">
{% csrf_token %}
<div class="d-flex gap-2 align-items-center flex-wrap">
<input type="file" name="image" accept="image/*"
<div class="d-flex gap-2 align-items-center">
<input type="file" name="image" accept="image/*" capture="environment"
class="form-control form-control-sm" style="max-width:220px;">
<button type="submit" class="btn btn-sm btn-outline-secondary">Upload</button>
<span id="photo-upload-spinner" class="htmx-indicator text-muted small">Uploading…</span>
</div>
</form>
</div>
{% if request.htmx %}
<div id="plant-hero-container" hx-swap-oob="true">
{% if plant.species and plant.species.api_image_url %}
<img src="{{ plant.species.api_image_url }}" class="plant-hero rounded mb-3" alt="{{ plant.species.common_name }}">
{% elif plant.thumbnail_url %}
<img src="{{ plant.thumbnail_url }}" class="plant-hero rounded mb-3" alt="{{ plant.name }}">
{% else %}
<div class="plant-hero-placeholder rounded mb-3">🌿</div>
{% endif %}
</div>
{% endif %}

View file

@ -8,7 +8,6 @@
<a href="{% url 'plant_edit' plant.pk %}" class="btn btn-sm btn-outline-secondary">✏️</a>
</div>
<div id="plant-hero-container">
{% if plant.species and plant.species.api_image_url %}
<img src="{{ plant.species.api_image_url }}" class="plant-hero rounded mb-3" alt="{{ plant.species.common_name }}">
{% elif plant.thumbnail_url %}
@ -16,7 +15,6 @@
{% else %}
<div class="plant-hero-placeholder rounded mb-3">🌿</div>
{% endif %}
</div>
<div class="d-flex justify-content-between align-items-start mb-1">
<div>

View file

@ -1,5 +1,3 @@
import logging
import os
import uuid
from django.shortcuts import render, redirect
from django.core.files.base import ContentFile
@ -7,9 +5,7 @@ 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
logger = logging.getLogger(__name__)
from plants.models import Plant, Species
_SESSION_KEYS = [
'identify_matches', 'identify_image_paths', 'identify_selected',
@ -49,23 +45,13 @@ def identify_upload(request):
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})
if default_storage.exists(p['path']):
default_storage.delete(p['path'])
return render(request, 'plants/identify_upload.html', {'error': str(e)})
request.session['identify_matches'] = matches
request.session['identify_image_paths'] = saved_paths
@ -77,8 +63,6 @@ def identify_confirm(request):
return redirect('identify_upload')
matches = request.session['identify_matches']
if not matches:
return redirect('identify_upload')
if request.method == 'POST':
try:
@ -172,23 +156,9 @@ def identify_fields(request):
plant = Plant.objects.create(name=name, species=species)
try:
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))
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
for p in request.session.get('identify_image_paths', []):
if default_storage.exists(p['path']):
default_storage.delete(p['path'])
finally:
_clear_session(request)

View file

@ -211,10 +211,9 @@ def crop_thumbnail(request, pk):
def upload_photo(request, pk):
plant = get_object_or_404(Plant, pk=pk)
plant = get_object_or_404(Plant.objects.prefetch_related('photos'), pk=pk)
if request.FILES.get('image'):
PlantPhoto.objects.create(plant=plant, image=request.FILES['image'])
plant = Plant.objects.prefetch_related('photos').get(pk=pk)
return render(request, 'plants/partials/photo_gallery.html', {'plant': plant})