diff --git a/plants/forms.py b/plants/forms.py index 3fe77a5..e90bc64 100644 --- a/plants/forms.py +++ b/plants/forms.py @@ -1,5 +1,5 @@ from django import forms -from .models import Plant, PruningLog +from .models import Plant, PruningLog, Location MONTH_CHOICES = [ ('1', 'Jan'), ('2', 'Feb'), ('3', 'Mar'), ('4', 'Apr'), @@ -15,14 +15,25 @@ class PlantForm(forms.ModelForm): required=False, label='Bloom months', ) + location = forms.ModelChoiceField( + queryset=Location.objects.all(), + required=False, + empty_label='— select location —', + widget=forms.Select(attrs={'class': 'form-select'}), + ) + new_location = forms.CharField( + max_length=200, + required=False, + label='Or add new location', + widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'e.g. Living room'}), + ) class Meta: model = Plant - fields = ['name', 'location', 'is_indoor', 'bloom_months', 'photo', 'notes'] + fields = ['name', 'location', 'is_indoor', 'bloom_months', 'notes'] widgets = { - 'notes': forms.Textarea(attrs={'rows': 3}), + 'notes': forms.Textarea(attrs={'rows': 3, 'class': 'form-control'}), 'name': forms.TextInput(attrs={'class': 'form-control'}), - 'location': forms.TextInput(attrs={'class': 'form-control'}), } def __init__(self, *args, **kwargs): @@ -35,6 +46,14 @@ class PlantForm(forms.ModelForm): def clean_bloom_months(self): return [int(m) for m in self.cleaned_data.get('bloom_months', [])] + def clean(self): + cleaned = super().clean() + new_loc = cleaned.get('new_location', '').strip() + if new_loc: + loc, _ = Location.objects.get_or_create(name=new_loc) + cleaned['location'] = loc + return cleaned + class PruningLogForm(forms.ModelForm): class Meta: diff --git a/plants/migrations/0004_location_plantphoto.py b/plants/migrations/0004_location_plantphoto.py new file mode 100644 index 0000000..1737150 --- /dev/null +++ b/plants/migrations/0004_location_plantphoto.py @@ -0,0 +1,71 @@ +import django.db.models.deletion +from django.db import migrations, models + + +def migrate_location_strings(apps, schema_editor): + Plant = apps.get_model('plants', 'Plant') + Location = apps.get_model('plants', 'Location') + for plant in Plant.objects.all(): + name = (plant.location_old or '').strip() + if name: + loc, _ = Location.objects.get_or_create(name=name) + plant.location_new = loc + plant.save(update_fields=['location_new']) + + +class Migration(migrations.Migration): + + dependencies = [ + ('plants', '0003_add_bloom_months'), + ] + + operations = [ + migrations.CreateModel( + name='Location', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200, unique=True)), + ], + options={ + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='PlantPhoto', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('image', models.ImageField(upload_to='plants/photos/')), + ('is_thumbnail', models.BooleanField(default=False)), + ('uploaded_at', models.DateTimeField(auto_now_add=True)), + ('plant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='photos', to='plants.plant')), + ], + options={ + 'ordering': ['uploaded_at'], + }, + ), + migrations.RenameField( + model_name='plant', + old_name='location', + new_name='location_old', + ), + migrations.AddField( + model_name='plant', + name='location_new', + field=models.ForeignKey( + blank=True, null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name='plants', + to='plants.location', + ), + ), + migrations.RunPython(migrate_location_strings, migrations.RunPython.noop), + migrations.RemoveField( + model_name='plant', + name='location_old', + ), + migrations.RenameField( + model_name='plant', + old_name='location_new', + new_name='location', + ), + ] diff --git a/plants/models.py b/plants/models.py index 4f2b663..cef40e4 100644 --- a/plants/models.py +++ b/plants/models.py @@ -1,6 +1,16 @@ from django.db import models +class Location(models.Model): + name = models.CharField(max_length=200, unique=True) + + class Meta: + ordering = ['name'] + + def __str__(self): + return self.name + + class Species(models.Model): common_name = models.CharField(max_length=200) scientific_name = models.CharField(max_length=200, blank=True) @@ -30,7 +40,10 @@ class Plant(models.Model): Species, null=True, blank=True, on_delete=models.SET_NULL, related_name='plants', ) - location = models.CharField(max_length=200) + location = models.ForeignKey( + Location, null=True, blank=True, + on_delete=models.SET_NULL, related_name='plants', + ) is_indoor = models.BooleanField(default=False) pruning_months = models.JSONField(default=list) bloom_months = models.JSONField(default=list) @@ -44,6 +57,28 @@ class Plant(models.Model): def __str__(self): return self.name + @property + def thumbnail_url(self): + thumb = self.photos.filter(is_thumbnail=True).first() + if thumb: + return thumb.image.url + first = self.photos.first() + if first: + return first.image.url + if self.photo: + return self.photo.url + return None + + +class PlantPhoto(models.Model): + plant = models.ForeignKey(Plant, on_delete=models.CASCADE, related_name='photos') + image = models.ImageField(upload_to='plants/photos/') + is_thumbnail = models.BooleanField(default=False) + uploaded_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ['uploaded_at'] + class PruningLog(models.Model): plant = models.ForeignKey( diff --git a/plants/templates/plants/partials/photo_gallery.html b/plants/templates/plants/partials/photo_gallery.html new file mode 100644 index 0000000..7bcae6f --- /dev/null +++ b/plants/templates/plants/partials/photo_gallery.html @@ -0,0 +1,46 @@ +
+
Photos
+ + {% with photos=plant.photos.all %} + {% if photos %} +
+ {% for photo in photos %} +
+ +
+ {% if not photo.is_thumbnail %} +
+ {% csrf_token %} + +
+ {% else %} + + {% endif %} +
+ {% csrf_token %} + +
+
+
+ {% endfor %} +
+ {% endif %} + {% endwith %} + +
+ {% csrf_token %} +
+ + +
+
+
diff --git a/plants/templates/plants/plant_detail.html b/plants/templates/plants/plant_detail.html index e09c0b4..ded196c 100644 --- a/plants/templates/plants/plant_detail.html +++ b/plants/templates/plants/plant_detail.html @@ -8,8 +8,8 @@ ✏️ -{% if plant.photo %} -{{ plant.name }} +{% if plant.thumbnail_url %} +{{ plant.name }} {% elif plant.species and plant.species.api_image_url %} {{ plant.species.common_name }} {% else %} @@ -26,7 +26,7 @@ -

📍 {{ plant.location }}

+{% if plant.location %}

📍 {{ plant.location.name }}

{% endif %} {% if plant.species %}
@@ -43,13 +43,6 @@ 🌱 {{ plant.species.growth_rate }} {% endif %}
- -{% if plant.species.api_image_url and plant.photo %} -
- - Species reference: {{ plant.species.common_name }} -
-{% endif %} {% endif %} {% load plant_extras %} @@ -68,20 +61,10 @@ {% endif %} -{% if plant.pruning_logs.all %} -
-
Pruning history
- + -{% endif %} -Delete plant +Delete plant {% endblock %} diff --git a/plants/templates/plants/plant_form.html b/plants/templates/plants/plant_form.html index 3315b1d..82ea60c 100644 --- a/plants/templates/plants/plant_form.html +++ b/plants/templates/plants/plant_form.html @@ -33,6 +33,11 @@ {{ form.location }} {% if form.location.errors %}
{{ form.location.errors }}
{% endif %} +
+ + {{ form.new_location }} +
Leave empty if selecting from the list above.
+
@@ -52,12 +57,6 @@
-
- - {{ form.photo }} -
Take a photo with your camera or upload one.
-
-
{{ form.notes }} diff --git a/plants/tests/test_models.py b/plants/tests/test_models.py index 3b3f298..c58e5e5 100644 --- a/plants/tests/test_models.py +++ b/plants/tests/test_models.py @@ -1,8 +1,13 @@ import pytest -from plants.models import Species, Plant, PruningLog +from plants.models import Species, Plant, PruningLog, Location from datetime import date +def make_location(name): + loc, _ = Location.objects.get_or_create(name=name) + return loc + + @pytest.mark.django_db def test_species_str(): s = Species.objects.create(common_name='Rose') @@ -11,19 +16,19 @@ def test_species_str(): @pytest.mark.django_db def test_plant_str(): - p = Plant.objects.create(name='Kitchen Fern', location='Kitchen') + p = Plant.objects.create(name='Kitchen Fern', location=make_location('Kitchen')) assert str(p) == 'Kitchen Fern' @pytest.mark.django_db def test_plant_species_nullable(): - p = Plant.objects.create(name='Unknown plant', location='Hallway') + p = Plant.objects.create(name='Unknown plant') assert p.species is None @pytest.mark.django_db def test_pruning_log_str(): - p = Plant.objects.create(name='Rose', location='Garden') + p = Plant.objects.create(name='Rose', location=make_location('Garden')) log = PruningLog.objects.create(plant=p, pruned_on=date(2026, 5, 1)) assert 'Rose' in str(log) assert '2026-05-01' in str(log) @@ -31,7 +36,7 @@ def test_pruning_log_str(): @pytest.mark.django_db def test_plant_pruning_months_default(): - p = Plant.objects.create(name='Fern', location='Office') + p = Plant.objects.create(name='Fern', location=make_location('Office')) assert p.pruning_months == [] @@ -44,7 +49,7 @@ def test_species_pruning_months_default(): @pytest.mark.django_db def test_delete_species_nulls_plant_species(): s = Species.objects.create(common_name='Rose') - p = Plant.objects.create(name='Garden rose', location='Garden', species=s) + p = Plant.objects.create(name='Garden rose', location=make_location('Garden'), species=s) s.delete() p.refresh_from_db() assert p.species is None diff --git a/plants/tests/test_pruning.py b/plants/tests/test_pruning.py index 042ebf8..7e54ce8 100644 --- a/plants/tests/test_pruning.py +++ b/plants/tests/test_pruning.py @@ -1,9 +1,14 @@ import pytest from datetime import date -from plants.models import Plant, PruningLog +from plants.models import Plant, PruningLog, Location from plants.utils.pruning import pruning_status, months_before +def make_location(name): + loc, _ = Location.objects.get_or_create(name=name) + return loc + + def test_months_before_same_year(): d = date(2026, 5, 27) assert months_before(d, 1) == date(2026, 4, 1) @@ -24,21 +29,21 @@ def test_months_before_january(): @pytest.mark.django_db def test_no_schedule(): - plant = Plant.objects.create(name='Fern', location='Office', pruning_months=[]) + plant = Plant.objects.create(name='Fern', pruning_months=[]) assert pruning_status(plant) == 'no_schedule' @pytest.mark.django_db def test_due_this_month(): today = date(2026, 5, 15) - plant = Plant.objects.create(name='Rose', location='Garden', pruning_months=[5]) + plant = Plant.objects.create(name='Rose', pruning_months=[5]) assert pruning_status(plant, today) == 'due_this_month' @pytest.mark.django_db def test_due_this_month_already_logged(): today = date(2026, 5, 15) - plant = Plant.objects.create(name='Rose', location='Garden', pruning_months=[5]) + plant = Plant.objects.create(name='Rose', pruning_months=[5]) PruningLog.objects.create(plant=plant, pruned_on=date(2026, 5, 1)) assert pruning_status(plant, today) == 'upcoming' @@ -46,14 +51,14 @@ def test_due_this_month_already_logged(): @pytest.mark.django_db def test_overdue_when_past_month_not_logged(): today = date(2026, 5, 15) - plant = Plant.objects.create(name='Rose', location='Garden', pruning_months=[3]) + plant = Plant.objects.create(name='Rose', pruning_months=[3]) assert pruning_status(plant, today) == 'overdue' @pytest.mark.django_db def test_not_overdue_when_past_month_logged(): today = date(2026, 5, 15) - plant = Plant.objects.create(name='Rose', location='Garden', pruning_months=[3]) + plant = Plant.objects.create(name='Rose', pruning_months=[3]) PruningLog.objects.create(plant=plant, pruned_on=date(2026, 3, 10)) assert pruning_status(plant, today) == 'upcoming' @@ -62,7 +67,7 @@ def test_not_overdue_when_past_month_logged(): def test_not_overdue_when_logged_after_scheduled_month(): # Pruned in May clears a March schedule — the common real-world case today = date(2026, 5, 28) - plant = Plant.objects.create(name='Rose', location='Garden', pruning_months=[3]) + plant = Plant.objects.create(name='Rose', pruning_months=[3]) PruningLog.objects.create(plant=plant, pruned_on=date(2026, 5, 28)) assert pruning_status(plant, today) == 'upcoming' @@ -70,7 +75,7 @@ def test_not_overdue_when_logged_after_scheduled_month(): @pytest.mark.django_db def test_upcoming_future_month(): today = date(2026, 5, 15) - plant = Plant.objects.create(name='Rose', location='Garden', pruning_months=[8]) + plant = Plant.objects.create(name='Rose', pruning_months=[8]) assert pruning_status(plant, today) == 'upcoming' @@ -78,7 +83,7 @@ def test_upcoming_future_month(): def test_overdue_crosses_year_boundary(): # Pruning month December, checked in January today = date(2026, 1, 10) - plant = Plant.objects.create(name='Apple', location='Garden', pruning_months=[12]) + plant = Plant.objects.create(name='Apple', pruning_months=[12]) assert pruning_status(plant, today) == 'overdue' @@ -86,5 +91,5 @@ def test_overdue_crosses_year_boundary(): def test_overdue_several_months_before_year_boundary(): # Plant scheduled in August, checked in January — should be overdue today = date(2026, 1, 10) - plant = Plant.objects.create(name='Oak', location='Garden', pruning_months=[8]) + plant = Plant.objects.create(name='Oak', pruning_months=[8]) assert pruning_status(plant, today) == 'overdue' diff --git a/plants/tests/test_views.py b/plants/tests/test_views.py index 6b5b8ca..38e01a9 100644 --- a/plants/tests/test_views.py +++ b/plants/tests/test_views.py @@ -1,7 +1,12 @@ import pytest from django.urls import reverse from datetime import date -from plants.models import Plant, PruningLog +from plants.models import Plant, PruningLog, Location + + +def make_location(name): + loc, _ = Location.objects.get_or_create(name=name) + return loc @pytest.mark.django_db @@ -11,21 +16,21 @@ class TestDashboard: assert resp.status_code == 200 def test_shows_total_plant_count(self, client): - Plant.objects.create(name='Fern', location='Office') - Plant.objects.create(name='Rose', location='Garden') + Plant.objects.create(name='Fern', location=make_location('Office')) + Plant.objects.create(name='Rose', location=make_location('Garden')) resp = client.get(reverse('dashboard')) assert resp.context['total_plants'] == 2 def test_blooming_now_plant_appears_in_context(self, client): today = date.today() - plant = Plant.objects.create(name='Rose', location='Garden', bloom_months=[today.month]) + plant = Plant.objects.create(name='Rose', bloom_months=[today.month]) resp = client.get(reverse('dashboard')) assert plant in resp.context['blooming_now'] def test_non_blooming_plant_not_in_blooming_now(self, client): today = date.today() other_month = today.month % 12 + 1 - plant = Plant.objects.create(name='Apple', location='Garden', bloom_months=[other_month]) + plant = Plant.objects.create(name='Apple', bloom_months=[other_month]) resp = client.get(reverse('dashboard')) assert plant not in resp.context['blooming_now'] @@ -37,13 +42,13 @@ class TestPlantList: assert resp.status_code == 200 def test_lists_plants(self, client): - Plant.objects.create(name='Monstera', location='Living room') + Plant.objects.create(name='Monstera', location=make_location('Living room')) resp = client.get(reverse('plant_list')) assert 'Monstera' in resp.content.decode() def test_search_filters_by_name(self, client): - Plant.objects.create(name='Monstera', location='Living room') - Plant.objects.create(name='Rose', location='Garden') + Plant.objects.create(name='Monstera', location=make_location('Living room')) + Plant.objects.create(name='Rose', location=make_location('Garden')) resp = client.get(reverse('plant_list') + '?q=Rose') content = resp.content.decode() assert 'Rose' in content @@ -58,8 +63,8 @@ class TestPlantList: assert resp.templates[0].name == 'plants/partials/plant_list_results.html' def test_indoor_filter(self, client): - Plant.objects.create(name='Fern', location='Office', is_indoor=True) - Plant.objects.create(name='Rose', location='Garden', is_indoor=False) + Plant.objects.create(name='Fern', location=make_location('Office'), is_indoor=True) + Plant.objects.create(name='Rose', location=make_location('Garden'), is_indoor=False) resp = client.get(reverse('plant_list') + '?filter=indoor') content = resp.content.decode() assert 'Fern' in content @@ -69,7 +74,7 @@ class TestPlantList: @pytest.mark.django_db class TestPlantDetail: def test_returns_200(self, client): - plant = Plant.objects.create(name='Fern', location='Office') + plant = Plant.objects.create(name='Fern', location=make_location('Office')) resp = client.get(reverse('plant_detail', args=[plant.pk])) assert resp.status_code == 200 @@ -78,7 +83,7 @@ class TestPlantDetail: assert resp.status_code == 404 def test_context_contains_plant_and_status(self, client): - plant = Plant.objects.create(name='Rose', location='Garden', pruning_months=[]) + plant = Plant.objects.create(name='Rose', pruning_months=[]) resp = client.get(reverse('plant_detail', args=[plant.pk])) assert resp.context['plant'] == plant assert resp.context['pruning_status'] == 'no_schedule' @@ -87,7 +92,7 @@ class TestPlantDetail: @pytest.mark.django_db class TestLogPruning: def test_creates_pruning_log(self, client): - plant = Plant.objects.create(name='Rose', location='Garden') + plant = Plant.objects.create(name='Rose', location=make_location('Garden')) resp = client.post( reverse('log_pruning', args=[plant.pk]), {'pruned_on': '2026-05-01', 'notes': ''}, @@ -96,7 +101,7 @@ class TestLogPruning: assert PruningLog.objects.filter(plant=plant, pruned_on='2026-05-01').exists() def test_returns_pruning_strip_partial(self, client): - plant = Plant.objects.create(name='Rose', location='Garden') + plant = Plant.objects.create(name='Rose', location=make_location('Garden')) resp = client.post( reverse('log_pruning', args=[plant.pk]), {'pruned_on': '2026-05-01', 'notes': ''}, @@ -160,7 +165,7 @@ class TestPlantAdd: def test_post_creates_plant_and_redirects(self, client): resp = client.post(reverse('plant_add'), { 'name': 'New Fern', - 'location': 'Office', + 'new_location': 'Office', 'is_indoor': 'on', 'notes': '', 'pruning_months': [], @@ -175,7 +180,7 @@ class TestPlantAdd: s = Species.objects.create(common_name='Rose', pruning_months=[2]) client.post( reverse('plant_add') + f'?species_id={s.pk}', - {'name': 'Garden Rose', 'location': 'Garden', 'notes': '', 'pruning_months': ['2']}, + {'name': 'Garden Rose', 'new_location': 'Garden', 'notes': '', 'pruning_months': ['2']}, ) plant = Plant.objects.get(name='Garden Rose') assert plant.species == s @@ -184,16 +189,16 @@ class TestPlantAdd: @pytest.mark.django_db class TestPlantEdit: def test_edit_form_shows_current_values(self, client): - plant = Plant.objects.create(name='Fern', location='Office', pruning_months=[3]) + plant = Plant.objects.create(name='Fern', location=make_location('Office'), pruning_months=[3]) resp = client.get(reverse('plant_edit', args=[plant.pk])) assert resp.status_code == 200 assert resp.context['plant'] == plant def test_post_updates_plant(self, client): - plant = Plant.objects.create(name='Fern', location='Office') + plant = Plant.objects.create(name='Fern', location=make_location('Office')) client.post(reverse('plant_edit', args=[plant.pk]), { 'name': 'Updated Fern', - 'location': 'Bedroom', + 'new_location': 'Bedroom', 'notes': '', 'pruning_months': [], }) @@ -204,12 +209,12 @@ class TestPlantEdit: @pytest.mark.django_db class TestPlantDelete: def test_delete_confirm_page(self, client): - plant = Plant.objects.create(name='Fern', location='Office') + plant = Plant.objects.create(name='Fern', location=make_location('Office')) resp = client.get(reverse('plant_delete', args=[plant.pk])) assert resp.status_code == 200 def test_post_deletes_plant(self, client): - plant = Plant.objects.create(name='Fern', location='Office') + plant = Plant.objects.create(name='Fern', location=make_location('Office')) client.post(reverse('plant_delete', args=[plant.pk])) assert not Plant.objects.filter(pk=plant.pk).exists() @@ -221,19 +226,19 @@ class TestPruningCalendar: assert resp.status_code == 200 def test_overdue_plant_in_context(self, client): - plant = Plant.objects.create(name='Rose', location='Garden', pruning_months=[3]) + plant = Plant.objects.create(name='Rose', location=make_location('Garden'), pruning_months=[3]) resp = client.get(reverse('pruning_calendar')) assert plant in resp.context['overdue'] def test_due_this_month_in_context(self, client): from datetime import date today = date.today() - plant = Plant.objects.create(name='Apple', location='Garden', pruning_months=[today.month]) + plant = Plant.objects.create(name='Apple', location=make_location('Garden'), pruning_months=[today.month]) resp = client.get(reverse('pruning_calendar')) assert plant in resp.context['due_this_month'] def test_no_schedule_plants_excluded(self, client): - plant = Plant.objects.create(name='Fern', location='Office', pruning_months=[]) + plant = Plant.objects.create(name='Fern', location=make_location('Office'), pruning_months=[]) resp = client.get(reverse('pruning_calendar')) assert plant not in resp.context['overdue'] assert plant not in resp.context['due_this_month'] diff --git a/plants/urls.py b/plants/urls.py index b710f20..7b8dae0 100644 --- a/plants/urls.py +++ b/plants/urls.py @@ -9,6 +9,9 @@ urlpatterns = [ path('plants//edit/', plants.plant_edit, name='plant_edit'), path('plants//delete/', plants.plant_delete, name='plant_delete'), path('plants//log-pruning/', plants.log_pruning, name='log_pruning'), + path('plants//upload-photo/', plants.upload_photo, name='upload_photo'), + path('photos//set-thumbnail/', plants.set_thumbnail, name='set_thumbnail'), + path('photos//delete/', plants.delete_photo, name='delete_photo'), path('pruning/', pruning.pruning_calendar, name='pruning_calendar'), path('species/search/', species.species_search, name='species_search'), path('species/select/', species.species_select, name='species_select'), diff --git a/plants/views/plants.py b/plants/views/plants.py index 3efcb8f..efde4cb 100644 --- a/plants/views/plants.py +++ b/plants/views/plants.py @@ -2,18 +2,18 @@ from datetime import date from django.shortcuts import render, get_object_or_404, redirect from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.http import require_POST -from plants.models import Plant, Species, PruningLog +from plants.models import Plant, Species, PruningLog, PlantPhoto from plants.forms import PlantForm, PruningLogForm from plants.utils.pruning import pruning_status def plant_list(request): - qs = Plant.objects.select_related('species').all() + qs = Plant.objects.select_related('species', 'location').all() q = request.GET.get('q', '') indoor_filter = request.GET.get('filter', '') if q: - qs = qs.filter(name__icontains=q) | qs.filter(location__icontains=q) + qs = qs.filter(name__icontains=q) | qs.filter(location__name__icontains=q) if indoor_filter == 'indoor': qs = qs.filter(is_indoor=True) elif indoor_filter == 'outdoor': @@ -29,7 +29,7 @@ def plant_list(request): def plant_detail(request, pk): plant = get_object_or_404( - Plant.objects.select_related('species').prefetch_related('pruning_logs'), + Plant.objects.select_related('species', 'location').prefetch_related('pruning_logs', 'photos'), pk=pk, ) today = date.today() @@ -111,3 +111,32 @@ def plant_delete(request, pk): plant.delete() return redirect('plant_list') return render(request, 'plants/plant_confirm_delete.html', {'plant': plant}) + + +@require_POST +def upload_photo(request, 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']) + return render(request, 'plants/partials/photo_gallery.html', {'plant': plant}) + + +@require_POST +def set_thumbnail(request, photo_pk): + photo = get_object_or_404(PlantPhoto, pk=photo_pk) + PlantPhoto.objects.filter(plant=photo.plant).update(is_thumbnail=False) + photo.is_thumbnail = True + photo.save(update_fields=['is_thumbnail']) + plant = Plant.objects.prefetch_related('photos').get(pk=photo.plant_id) + return render(request, 'plants/partials/photo_gallery.html', {'plant': plant}) + + +@require_POST +def delete_photo(request, photo_pk): + photo = get_object_or_404(PlantPhoto, pk=photo_pk) + plant = Plant.objects.prefetch_related('photos').get(pk=photo.plant_id) + photo.image.delete(save=False) + photo.delete() + plant.refresh_from_db() + plant = Plant.objects.prefetch_related('photos').get(pk=plant.pk) + return render(request, 'plants/partials/photo_gallery.html', {'plant': plant})