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', '') _trusted = os.environ.get('CSRF_TRUSTED_ORIGINS', '')
if _trusted: if _trusted:
CSRF_TRUSTED_ORIGINS = [o.strip() for o in _trusted.split(',')] 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) pruning_months = models.JSONField(default=list)
bloom_months = models.JSONField(default=list) bloom_months = models.JSONField(default=list)
api_image_url = models.URLField(blank=True) 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) perenual_id = models.IntegerField(unique=True, null=True, blank=True)
vpc_slug = models.CharField(max_length=200, blank=True, db_index=True) vpc_slug = models.CharField(max_length=200, blank=True, db_index=True)
vpc_raw = models.JSONField(null=True, blank=True) vpc_raw = models.JSONField(null=True, blank=True)

View file

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

View file

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

View file

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

View file

@ -211,10 +211,9 @@ def crop_thumbnail(request, pk):
def upload_photo(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'): if request.FILES.get('image'):
PlantPhoto.objects.create(plant=plant, image=request.FILES['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}) return render(request, 'plants/partials/photo_gallery.html', {'plant': plant})