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 %}
+
+ {% else %}
+ ★
+ {% endif %}
+
+
+
+ {% endfor %}
+
+ {% endif %}
+ {% endwith %}
+
+
+
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 %}
-
+{% if plant.thumbnail_url %}
+
{% elif plant.species and plant.species.api_image_url %}
{% 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
-
- {% for log in plant.pruning_logs.all %}
- -
- {{ log.pruned_on|date:"j M Y" }}
- {% if log.notes %}{{ log.notes }}{% endif %}
-
- {% endfor %}
-
+
+ {% include "plants/partials/photo_gallery.html" %}
-{% 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})