From 2c61b03519e048703549eb6e10908eedeb65ca3e Mon Sep 17 00:00:00 2001 From: Stephan Kerkman Date: Wed, 27 May 2026 19:51:11 +0200 Subject: [PATCH] feat: add Species, Plant, PruningLog models Co-Authored-By: Claude Sonnet 4.6 --- plants/admin.py | 20 ++++++++- plants/migrations/0001_initial.py | 70 +++++++++++++++++++++++++++++++ plants/models.py | 56 ++++++++++++++++++++++++- plants/tests/__init__.py | 0 plants/tests/test_models.py | 35 ++++++++++++++++ 5 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 plants/migrations/0001_initial.py create mode 100644 plants/tests/__init__.py create mode 100644 plants/tests/test_models.py diff --git a/plants/admin.py b/plants/admin.py index 8c38f3f..6e3601a 100644 --- a/plants/admin.py +++ b/plants/admin.py @@ -1,3 +1,21 @@ from django.contrib import admin +from .models import Species, Plant, PruningLog -# Register your models here. + +@admin.register(Species) +class SpeciesAdmin(admin.ModelAdmin): + list_display = ['common_name', 'scientific_name', 'perenual_id'] + search_fields = ['common_name', 'scientific_name'] + + +@admin.register(Plant) +class PlantAdmin(admin.ModelAdmin): + list_display = ['name', 'species', 'location', 'is_indoor', 'date_added'] + list_filter = ['is_indoor'] + search_fields = ['name', 'location'] + + +@admin.register(PruningLog) +class PruningLogAdmin(admin.ModelAdmin): + list_display = ['plant', 'pruned_on'] + list_filter = ['pruned_on'] diff --git a/plants/migrations/0001_initial.py b/plants/migrations/0001_initial.py new file mode 100644 index 0000000..0820e5e --- /dev/null +++ b/plants/migrations/0001_initial.py @@ -0,0 +1,70 @@ +# Generated by Django 5.1.15 on 2026-05-27 17:50 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Plant', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200)), + ('location', models.CharField(max_length=200)), + ('is_indoor', models.BooleanField(default=False)), + ('pruning_months', models.JSONField(default=list)), + ('photo', models.ImageField(blank=True, upload_to='plants/')), + ('notes', models.TextField(blank=True)), + ('date_added', models.DateField(auto_now_add=True)), + ], + options={ + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='Species', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('common_name', models.CharField(max_length=200)), + ('scientific_name', models.CharField(blank=True, max_length=200)), + ('description', models.TextField(blank=True)), + ('watering', models.CharField(blank=True, max_length=100)), + ('sunlight', models.CharField(blank=True, max_length=200)), + ('shadow_tolerance', models.CharField(blank=True, max_length=100)), + ('max_height_cm', models.IntegerField(blank=True, null=True)), + ('growth_rate', models.CharField(blank=True, max_length=100)), + ('growth_season', models.CharField(blank=True, max_length=100)), + ('pruning_months', models.JSONField(default=list)), + ('api_image_url', models.URLField(blank=True)), + ('perenual_id', models.IntegerField(blank=True, null=True, unique=True)), + ], + options={ + 'verbose_name_plural': 'species', + 'ordering': ['common_name'], + }, + ), + migrations.CreateModel( + name='PruningLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('pruned_on', models.DateField()), + ('notes', models.TextField(blank=True)), + ('plant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pruninglogs', to='plants.plant')), + ], + options={ + 'ordering': ['-pruned_on'], + }, + ), + migrations.AddField( + model_name='plant', + name='species', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='plants', to='plants.species'), + ), + ] diff --git a/plants/models.py b/plants/models.py index 71a8362..1ee059d 100644 --- a/plants/models.py +++ b/plants/models.py @@ -1,3 +1,57 @@ from django.db import models -# Create your models here. + +class Species(models.Model): + common_name = models.CharField(max_length=200) + scientific_name = models.CharField(max_length=200, blank=True) + description = models.TextField(blank=True) + watering = models.CharField(max_length=100, blank=True) + sunlight = models.CharField(max_length=200, blank=True) + shadow_tolerance = models.CharField(max_length=100, blank=True) + max_height_cm = models.IntegerField(null=True, blank=True) + growth_rate = models.CharField(max_length=100, blank=True) + growth_season = models.CharField(max_length=100, blank=True) + pruning_months = models.JSONField(default=list) + api_image_url = models.URLField(blank=True) + perenual_id = models.IntegerField(unique=True, null=True, blank=True) + + class Meta: + ordering = ['common_name'] + verbose_name_plural = 'species' + + def __str__(self): + return self.common_name + + +class Plant(models.Model): + name = models.CharField(max_length=200) + species = models.ForeignKey( + Species, null=True, blank=True, + on_delete=models.SET_NULL, related_name='plants', + ) + location = models.CharField(max_length=200) + is_indoor = models.BooleanField(default=False) + pruning_months = models.JSONField(default=list) + photo = models.ImageField(upload_to='plants/', blank=True) + notes = models.TextField(blank=True) + date_added = models.DateField(auto_now_add=True) + + class Meta: + ordering = ['name'] + + def __str__(self): + return self.name + + +class PruningLog(models.Model): + plant = models.ForeignKey( + Plant, on_delete=models.CASCADE, related_name='pruninglogs', + ) + pruned_on = models.DateField() + notes = models.TextField(blank=True) + + class Meta: + ordering = ['-pruned_on'] + + def __str__(self): + return f'{self.plant.name} pruned on {self.pruned_on}' diff --git a/plants/tests/__init__.py b/plants/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plants/tests/test_models.py b/plants/tests/test_models.py new file mode 100644 index 0000000..5c14eee --- /dev/null +++ b/plants/tests/test_models.py @@ -0,0 +1,35 @@ +import pytest +from plants.models import Species, Plant, PruningLog +from datetime import date + + +@pytest.mark.django_db +def test_species_str(): + s = Species.objects.create(common_name='Rose') + assert str(s) == 'Rose' + + +@pytest.mark.django_db +def test_plant_str(): + p = Plant.objects.create(name='Kitchen Fern', 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') + assert p.species is None + + +@pytest.mark.django_db +def test_pruning_log_str(): + p = Plant.objects.create(name='Rose', 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) + + +@pytest.mark.django_db +def test_plant_pruning_months_default(): + p = Plant.objects.create(name='Fern', location='Office') + assert p.pruning_months == []