From 069395fbfa57640ab5d3d576f4d91cf7a2d401c6 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 1 Jul 2025 16:16:42 -0600 Subject: [PATCH 01/57] merge conflict resolution --- docker-compose.yml | 2 + .../samplelocations/tests/test_contact.py | 20 +++++++++ .../tests/test_contact_owner.py | 26 +++++++++++ .../samplelocations/tests/test_equipment.py | 35 +++++++++++++++ .../samplelocations/tests/test_lexicon.py | 19 ++++++++ geodjango/samplelocations/tests/test_owner.py | 22 ++++++++++ .../tests/test_samplelocation.py | 36 +++++++++++++++ .../samplelocations/tests/test_spring.py | 18 ++++++++ geodjango/samplelocations/tests/test_well.py | 44 +++++++++++++++++++ .../samplelocations/tests/test_wellscreen.py | 30 +++++++++++++ uv.lock | 6 +++ 11 files changed, 258 insertions(+) create mode 100644 geodjango/samplelocations/tests/test_contact.py create mode 100644 geodjango/samplelocations/tests/test_contact_owner.py create mode 100644 geodjango/samplelocations/tests/test_equipment.py create mode 100644 geodjango/samplelocations/tests/test_lexicon.py create mode 100644 geodjango/samplelocations/tests/test_owner.py create mode 100644 geodjango/samplelocations/tests/test_samplelocation.py create mode 100644 geodjango/samplelocations/tests/test_spring.py create mode 100644 geodjango/samplelocations/tests/test_well.py create mode 100644 geodjango/samplelocations/tests/test_wellscreen.py diff --git a/docker-compose.yml b/docker-compose.yml index b377236..49a3d08 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,3 +1,5 @@ +version: '3.9' + services: db: image: postgis/postgis:16-3.4 diff --git a/geodjango/samplelocations/tests/test_contact.py b/geodjango/samplelocations/tests/test_contact.py new file mode 100644 index 0000000..6e6ff65 --- /dev/null +++ b/geodjango/samplelocations/tests/test_contact.py @@ -0,0 +1,20 @@ +from django.test import TestCase +from ..models import Contact + +class ContactModelTest(TestCase): + def test_create_contact(self): + contact = Contact.objects.create(name="John Doe", email="john@example.com", phone="1234567890") + self.assertEqual(contact.name, "John Doe") + self.assertEqual(contact.email, "john@example.com") + self.assertEqual(contact.phone, "1234567890") + self.assertIsNotNone(contact.date_created) + + def test_str_method(self): + contact = Contact.objects.create(name="Jane Doe", email="jane@example.com") + self.assertEqual(str(contact), "Jane Doe") + + def test_ordering(self): + Contact.objects.create(name="B", email="b@example.com") + Contact.objects.create(name="A", email="a@example.com") + names = list(Contact.objects.values_list('name', flat=True)) + self.assertEqual(names, sorted(names)) diff --git a/geodjango/samplelocations/tests/test_contact_owner.py b/geodjango/samplelocations/tests/test_contact_owner.py new file mode 100644 index 0000000..10dfbbc --- /dev/null +++ b/geodjango/samplelocations/tests/test_contact_owner.py @@ -0,0 +1,26 @@ +# hello +# This file is now split into test_contact.py and test_owner.py. You can remove this file if you wish. + +from django.test import TestCase +from ..models import Contact, Owner + + +class OwnerModelTest(TestCase): + def setUp(self): + self.contact = Contact.objects.create(name="Owner Contact", email="owner@example.com") + + def test_create_owner(self): + owner = Owner.objects.create(name="Owner1", contact=self.contact) + self.assertEqual(owner.name, "Owner1") + self.assertEqual(owner.contact, self.contact) + self.assertIsNotNone(owner.date_created) + + def test_str_method(self): + owner = Owner.objects.create(name="Owner2", contact=self.contact) + self.assertEqual(str(owner), "Owner2") + + def test_ordering(self): + Owner.objects.create(name="B", contact=self.contact) + Owner.objects.create(name="A", contact=self.contact) + names = list(Owner.objects.values_list('name', flat=True)) + self.assertEqual(names, sorted(names)) diff --git a/geodjango/samplelocations/tests/test_equipment.py b/geodjango/samplelocations/tests/test_equipment.py new file mode 100644 index 0000000..02a24c1 --- /dev/null +++ b/geodjango/samplelocations/tests/test_equipment.py @@ -0,0 +1,35 @@ +from django.test import TestCase +from ..models import Equipment, SampleLocation, Owner, Contact +from django.contrib.gis.geos import Point +from datetime import datetime + +class EquipmentModelTest(TestCase): + def setUp(self): + self.contact = Contact.objects.create(name="Contact", email="contact@example.com") + self.owner = Owner.objects.create(name="Owner", contact=self.contact) + self.location = SampleLocation.objects.create(name="Loc", point=Point(-105, 40), owner=self.owner) + + def test_create_equipment(self): + eq = Equipment.objects.create( + equipment_type="Pump", + model="ModelX", + serial_no="SN123", + date_installed=datetime(2020, 1, 1), + date_removed=datetime(2021, 1, 1), + recording_interval=60, + equipment_notes="Test notes", + location=self.location + ) + self.assertEqual(eq.equipment_type, "Pump") + self.assertEqual(eq.model, "ModelX") + self.assertEqual(eq.serial_no, "SN123") + self.assertEqual(eq.date_installed.year, 2020) + self.assertEqual(eq.date_removed.year, 2021) + self.assertEqual(eq.recording_interval, 60) + self.assertEqual(eq.equipment_notes, "Test notes") + self.assertEqual(eq.location, self.location) + + def test_str_method(self): + eq = Equipment.objects.create(equipment_type="Sensor", model="M1", serial_no="S1", location=self.location) + self.assertIn("Sensor", str(eq)) + self.assertIn(self.location.name, str(eq)) diff --git a/geodjango/samplelocations/tests/test_lexicon.py b/geodjango/samplelocations/tests/test_lexicon.py new file mode 100644 index 0000000..b099953 --- /dev/null +++ b/geodjango/samplelocations/tests/test_lexicon.py @@ -0,0 +1,19 @@ +from django.test import TestCase +from ..models import Lexicon + +class LexiconModelTest(TestCase): + def test_create_lexicon(self): + lex = Lexicon.objects.create(name="Test Lexicon", description="A test lexicon.") + self.assertEqual(lex.name, "Test Lexicon") + self.assertEqual(lex.description, "A test lexicon.") + self.assertIsNotNone(lex.date_created) + + def test_str_method(self): + lex = Lexicon.objects.create(name="Lexicon1") + self.assertEqual(str(lex), "Lexicon1") + + def test_ordering(self): + Lexicon.objects.create(name="B") + Lexicon.objects.create(name="A") + names = list(Lexicon.objects.values_list('name', flat=True)) + self.assertEqual(names, sorted(names)) diff --git a/geodjango/samplelocations/tests/test_owner.py b/geodjango/samplelocations/tests/test_owner.py new file mode 100644 index 0000000..13ce4a8 --- /dev/null +++ b/geodjango/samplelocations/tests/test_owner.py @@ -0,0 +1,22 @@ +from django.test import TestCase +from ..models import Owner, Contact + +class OwnerModelTest(TestCase): + def setUp(self): + self.contact = Contact.objects.create(name="Owner Contact", email="owner@example.com") + + def test_create_owner(self): + owner = Owner.objects.create(name="Owner1", contact=self.contact) + self.assertEqual(owner.name, "Owner1") + self.assertEqual(owner.contact, self.contact) + self.assertIsNotNone(owner.date_created) + + def test_str_method(self): + owner = Owner.objects.create(name="Owner2", contact=self.contact) + self.assertEqual(str(owner), "Owner2") + + def test_ordering(self): + Owner.objects.create(name="B", contact=self.contact) + Owner.objects.create(name="A", contact=self.contact) + names = list(Owner.objects.values_list('name', flat=True)) + self.assertEqual(names, sorted(names)) diff --git a/geodjango/samplelocations/tests/test_samplelocation.py b/geodjango/samplelocations/tests/test_samplelocation.py new file mode 100644 index 0000000..a5e2d0e --- /dev/null +++ b/geodjango/samplelocations/tests/test_samplelocation.py @@ -0,0 +1,36 @@ +from django.test import TestCase +from django.contrib.gis.geos import Point +from ..models import SampleLocation, Owner, Contact + +class SampleLocationModelTest(TestCase): + def setUp(self): + self.contact = Contact.objects.create(name="Contact", email="contact@example.com") + self.owner = Owner.objects.create(name="Owner", contact=self.contact) + + def test_create_sample_location(self): + point = Point(-105.0, 40.0) + loc = SampleLocation.objects.create( + name="Loc1", + description="Test location", + visible=True, + point=point, + owner=self.owner + ) + self.assertEqual(loc.name, "Loc1") + self.assertEqual(loc.description, "Test location") + self.assertTrue(loc.visible) + self.assertEqual(loc.point, point) + self.assertEqual(loc.owner, self.owner) + self.assertIsNotNone(loc.date_created) + + def test_str_method(self): + point = Point(-105.0, 40.0) + loc = SampleLocation.objects.create(name="Loc2", point=point, owner=self.owner) + self.assertEqual(str(loc), "Loc2") + + def test_ordering(self): + point = Point(-105.0, 40.0) + SampleLocation.objects.create(name="B", point=point, owner=self.owner) + SampleLocation.objects.create(name="A", point=point, owner=self.owner) + names = list(SampleLocation.objects.values_list('name', flat=True)) + self.assertEqual(names, sorted(names)) diff --git a/geodjango/samplelocations/tests/test_spring.py b/geodjango/samplelocations/tests/test_spring.py new file mode 100644 index 0000000..a390efd --- /dev/null +++ b/geodjango/samplelocations/tests/test_spring.py @@ -0,0 +1,18 @@ +from django.test import TestCase +from ..models import Spring, SampleLocation, Owner, Contact +from django.contrib.gis.geos import Point + +class SpringModelTest(TestCase): + def setUp(self): + self.contact = Contact.objects.create(name="Contact", email="contact@example.com") + self.owner = Owner.objects.create(name="Owner", contact=self.contact) + self.location = SampleLocation.objects.create(name="Loc", point=Point(-105, 40), owner=self.owner) + + def test_create_spring(self): + spring = Spring.objects.create(description="A spring", location=self.location) + self.assertEqual(spring.description, "A spring") + self.assertEqual(spring.location, self.location) + + def test_str_method(self): + spring = Spring.objects.create(location=self.location) + self.assertIn(self.location.name, str(spring)) diff --git a/geodjango/samplelocations/tests/test_well.py b/geodjango/samplelocations/tests/test_well.py new file mode 100644 index 0000000..282ef7e --- /dev/null +++ b/geodjango/samplelocations/tests/test_well.py @@ -0,0 +1,44 @@ +from django.test import TestCase +from ..models import Well, SampleLocation, Owner, Contact, Lexicon +from django.contrib.gis.geos import Point + +class WellModelTest(TestCase): + def setUp(self): + self.contact = Contact.objects.create(name="Contact", email="contact@example.com") + self.owner = Owner.objects.create(name="Owner", contact=self.contact) + self.location = SampleLocation.objects.create(name="Loc", point=Point(-105, 40), owner=self.owner) + self.lexicon = Lexicon.objects.create(name="Type1") + self.formation = Lexicon.objects.create(name="Formation1") + + def test_create_well(self): + well = Well.objects.create( + location=self.location, + ose_pod_id="POD123", + api_id="API123", + usgs_id="USGS123", + well_depth=100.0, + hole_depth=110.0, + well_type=self.lexicon, + casing_diameter=6.0, + casing_depth=80.0, + casing_description="Steel", + construction_notes="Notes", + formation_zone=self.formation + ) + self.assertEqual(well.location, self.location) + self.assertEqual(well.ose_pod_id, "POD123") + self.assertEqual(well.api_id, "API123") + self.assertEqual(well.usgs_id, "USGS123") + self.assertEqual(well.well_depth, 100.0) + self.assertEqual(well.hole_depth, 110.0) + self.assertEqual(well.well_type, self.lexicon) + self.assertEqual(well.casing_diameter, 6.0) + self.assertEqual(well.casing_depth, 80.0) + self.assertEqual(well.casing_description, "Steel") + self.assertEqual(well.construction_notes, "Notes") + self.assertEqual(well.formation_zone, self.formation) + + def test_str_method(self): + well = Well.objects.create(location=self.location) + self.assertIn(self.location.name, str(well)) + diff --git a/geodjango/samplelocations/tests/test_wellscreen.py b/geodjango/samplelocations/tests/test_wellscreen.py new file mode 100644 index 0000000..3f85ada --- /dev/null +++ b/geodjango/samplelocations/tests/test_wellscreen.py @@ -0,0 +1,30 @@ +from django.test import TestCase +from ..models import Well, WellScreen, SampleLocation, Owner, Contact, Lexicon +from django.contrib.gis.geos import Point + +class WellScreenModelTest(TestCase): + def setUp(self): + self.contact = Contact.objects.create(name="Contact", email="contact@example.com") + self.owner = Owner.objects.create(name="Owner", contact=self.contact) + self.location = SampleLocation.objects.create(name="Loc", point=Point(-105, 40), owner=self.owner) + self.well = Well.objects.create(location=self.location) + self.screen_type = Lexicon.objects.create(name="ScreenType1") + + def test_create_well_screen(self): + screen = WellScreen.objects.create( + well=self.well, + screen_depth_top=10.0, + screen_depth_bottom=20.0, + screen_type=self.screen_type + ) + self.assertEqual(screen.well, self.well) + self.assertEqual(screen.screen_depth_top, 10.0) + self.assertEqual(screen.screen_depth_bottom, 20.0) + self.assertEqual(screen.screen_type, self.screen_type) + + def test_str_method(self): + screen = WellScreen.objects.create(well=self.well, screen_depth_top=5.0, screen_depth_bottom=15.0) + self.assertIn("5.0-15.0", str(screen)) + self.assertIn(str(self.well), str(screen)) + +# test comment diff --git a/uv.lock b/uv.lock index eaa0248..d66f825 100644 --- a/uv.lock +++ b/uv.lock @@ -47,6 +47,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/08/ec/0cfa9b817f048cdec354354ae0569d7c0fd63907e5b1f927a7ee04a18635/django_ninja-1.4.3-py3-none-any.whl", hash = "sha256:f3204137a059437b95677049474220611f1cf9efedba9213556474b75168fa01", size = 2426185, upload-time = "2025-06-04T15:11:11.314Z" }, ] +[[package]] +name = "gdal" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/86/1c4b3eb5b3d31584ebc694cd54b04e8cdba0d71018bbe65d4faefa489a9f/gdal-3.10.tar.gz", hash = "sha256:98fed53baca2b6ec35daa56773facf425a64cd9d1293eb8be28ef9458ea24820", size = 848086, upload-time = "2024-11-06T15:20:32.923Z" } + [[package]] name = "geodjangopoc" version = "0.1.0" From e672a108bc91239d1d5919bf144b5d6033b4b23b Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 8 Jul 2025 10:12:33 -0600 Subject: [PATCH 02/57] split routers into own files --- geodjango/geodjango/api.py | 4 +- geodjango/samplelocations/api.py | 23 --------- geodjango/samplelocations/api/__init__.py | 0 geodjango/samplelocations/api/contacts.py | 22 +++++++++ geodjango/samplelocations/api/locations.py | 38 +++++++++++++++ geodjango/samplelocations/api/wells.py | 55 ++++++++++++++++++++++ 6 files changed, 118 insertions(+), 24 deletions(-) delete mode 100644 geodjango/samplelocations/api.py create mode 100644 geodjango/samplelocations/api/__init__.py create mode 100644 geodjango/samplelocations/api/contacts.py create mode 100644 geodjango/samplelocations/api/locations.py create mode 100644 geodjango/samplelocations/api/wells.py diff --git a/geodjango/geodjango/api.py b/geodjango/geodjango/api.py index 31371dd..46da17e 100644 --- a/geodjango/geodjango/api.py +++ b/geodjango/geodjango/api.py @@ -2,4 +2,6 @@ api = NinjaAPI() -api.add_router('/samplelocations/', 'samplelocations.api.router') \ No newline at end of file +api.add_router('/locations/', 'samplelocations.api.locations.router') +api.add_router('/wells/', 'samplelocations.api.wells.router') +api.add_router('/contacts/', 'samplelocations.api.contacts.router') \ No newline at end of file diff --git a/geodjango/samplelocations/api.py b/geodjango/samplelocations/api.py deleted file mode 100644 index 373f8d1..0000000 --- a/geodjango/samplelocations/api.py +++ /dev/null @@ -1,23 +0,0 @@ -from ninja import Router -from .models import SampleLocation - -router = Router() - -@router.get('') -def list_samplelocations(request): - """ - List all sample locations. - """ - locations = SampleLocation.objects.all() - return [ - { - 'id': loc.id, - 'name': loc.name, - 'lat': loc.point.y, - 'lon': loc.point.x, - 'description': loc.description, - 'visible': loc.visible, - 'date_created': loc.date_created.isoformat(), - } - for loc in locations - ] diff --git a/geodjango/samplelocations/api/__init__.py b/geodjango/samplelocations/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geodjango/samplelocations/api/contacts.py b/geodjango/samplelocations/api/contacts.py new file mode 100644 index 0000000..0287199 --- /dev/null +++ b/geodjango/samplelocations/api/contacts.py @@ -0,0 +1,22 @@ +from ninja import Router +from ..models import Well, Contact +from django.shortcuts import get_object_or_404 + +router = Router() + +@router.post("") +def add_contact_for_well( + request, + well_id: int, + name: str, + email: str, + phone: str = None +): + well = get_object_or_404(Well, id=well_id) + contact = Contact.objects.create(name=name, email=email, phone=phone) + # Create or update owner for the location if needed + location = well.location + owner = location.owner + owner.contact = contact + owner.save() + return {"contact_id": contact.id, "owner_id": owner.id} \ No newline at end of file diff --git a/geodjango/samplelocations/api/locations.py b/geodjango/samplelocations/api/locations.py new file mode 100644 index 0000000..aced665 --- /dev/null +++ b/geodjango/samplelocations/api/locations.py @@ -0,0 +1,38 @@ +from ninja import Router +from ..models import SampleLocation, Well, WellScreen, Contact, Owner, Lexicon +from django.contrib.gis.geos import Point +from django.shortcuts import get_object_or_404 + +router = Router() + +@router.get('') +def get_locations(request): + """ + List all sample locations. + """ + locations = SampleLocation.objects.all() + return [ + { + 'id': loc.id, + 'name': loc.name, + 'lat': loc.point.y, + 'lon': loc.point.x, + 'description': loc.description, + 'visible': loc.visible, + 'date_created': loc.date_created.isoformat(), + } + for loc in locations + ] + +@router.post("") +def post_location(request, name: str, lat: float, lon: float, owner_id: int, description: str = None, visible: bool = False): + owner = get_object_or_404(Owner, id=owner_id) + point = Point(lon, lat) + location = SampleLocation.objects.create( + name=name, + description=description, + visible=visible, + point=point, + owner=owner + ) + return {"id": location.id, "name": location.name} \ No newline at end of file diff --git a/geodjango/samplelocations/api/wells.py b/geodjango/samplelocations/api/wells.py new file mode 100644 index 0000000..10964bb --- /dev/null +++ b/geodjango/samplelocations/api/wells.py @@ -0,0 +1,55 @@ +from ninja import Router +from ..models import SampleLocation, Well, WellScreen, Lexicon +from django.shortcuts import get_object_or_404 + +router = Router() + + +@router.post("") +def post_well( + request, + location_id: int, + ose_pod_id: str = None, + api_id: str = "", + usgs_id: str = None, + well_depth: float = None, + hole_depth: float = None, + well_type_id: int = None, + casing_diameter: float = None, + casing_depth: float = None, + casing_description: str = None, + construction_notes: str = None, + formation_zone_id: int = None +): + location = get_object_or_404(SampleLocation, id=location_id) + well_type = Lexicon.objects.filter(id=well_type_id).first() if well_type_id else None + formation_zone = Lexicon.objects.filter(id=formation_zone_id).first() if formation_zone_id else None + well = Well.objects.create( + location=location, + ose_pod_id=ose_pod_id, + api_id=api_id, + usgs_id=usgs_id, + well_depth=well_depth, + hole_depth=hole_depth, + well_type=well_type, + casing_diameter=casing_diameter, + casing_depth=casing_depth, + casing_description=casing_description, + construction_notes=construction_notes, + formation_zone=formation_zone + ) + return {"id": well.id, "location": well.location.id} + + +@router.post("well-screens/") +def add_well_screen(request, well_id: int, screen_depth_top: float, screen_depth_bottom: float, screen_type_id: int = None): + well = get_object_or_404(Well, id=well_id) + screen_type = Lexicon.objects.filter(id=screen_type_id).first() if screen_type_id else None + screen = WellScreen.objects.create( + well=well, + screen_depth_top=screen_depth_top, + screen_depth_bottom=screen_depth_bottom, + screen_type=screen_type + ) + return {"id": screen.id, "well": screen.well.id} + From 11f9664e6efb474ee2f1be77ab948e8c76c4deaa Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 8 Jul 2025 10:34:15 -0600 Subject: [PATCH 03/57] fix: remove tailing endpoint slashes --- geodjango/geodjango/api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/geodjango/geodjango/api.py b/geodjango/geodjango/api.py index 46da17e..012eb00 100644 --- a/geodjango/geodjango/api.py +++ b/geodjango/geodjango/api.py @@ -2,6 +2,6 @@ api = NinjaAPI() -api.add_router('/locations/', 'samplelocations.api.locations.router') -api.add_router('/wells/', 'samplelocations.api.wells.router') -api.add_router('/contacts/', 'samplelocations.api.contacts.router') \ No newline at end of file +api.add_router('/locations', 'samplelocations.api.locations.router') +api.add_router('/wells', 'samplelocations.api.wells.router') +api.add_router('/contacts', 'samplelocations.api.contacts.router') \ No newline at end of file From 5d2c12be9e88b7c9b74ab89b6b454cfe31f3ece7 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 8 Jul 2025 10:34:39 -0600 Subject: [PATCH 04/57] fix: update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ad061e8..4a4919a 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ You can replace `makemigrations`, `migrate`, etc. with any Django command as nee [http://localhost:8000/](http://localhost:8000/) - **API Docs:** - [http://localhost:8000/api/docs/](http://localhost:8000/api/docs/) + [http://localhost:8000/api/docs](http://localhost:8000/api/docs) - **Sample Locations API Endpoint Example:** [http://localhost:8000/api/samplelocations/](http://localhost:8000/api/samplelocations/) From 723f11802f791f161a408bc3379cfd42108ffae6 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 8 Jul 2025 10:36:22 -0600 Subject: [PATCH 05/57] fix: make Redoc default docs --- geodjango/geodjango/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/geodjango/geodjango/api.py b/geodjango/geodjango/api.py index 012eb00..23567fe 100644 --- a/geodjango/geodjango/api.py +++ b/geodjango/geodjango/api.py @@ -1,6 +1,6 @@ -from ninja import NinjaAPI +from ninja import NinjaAPI, Redoc -api = NinjaAPI() +api = NinjaAPI(docs=Redoc()) api.add_router('/locations', 'samplelocations.api.locations.router') api.add_router('/wells', 'samplelocations.api.wells.router') From d3d749029e9854ccbbd27179ae3ac8d82ef09eda Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 8 Jul 2025 11:08:19 -0600 Subject: [PATCH 06/57] style: update function name --- geodjango/samplelocations/api/wells.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geodjango/samplelocations/api/wells.py b/geodjango/samplelocations/api/wells.py index 10964bb..09457fc 100644 --- a/geodjango/samplelocations/api/wells.py +++ b/geodjango/samplelocations/api/wells.py @@ -42,7 +42,7 @@ def post_well( @router.post("well-screens/") -def add_well_screen(request, well_id: int, screen_depth_top: float, screen_depth_bottom: float, screen_type_id: int = None): +def post_well_screen(request, well_id: int, screen_depth_top: float, screen_depth_bottom: float, screen_type_id: int = None): well = get_object_or_404(Well, id=well_id) screen_type = Lexicon.objects.filter(id=screen_type_id).first() if screen_type_id else None screen = WellScreen.objects.create( From c8465f8073569114bac6a1e116781f8f7f23ac98 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 8 Jul 2025 11:08:32 -0600 Subject: [PATCH 07/57] style: update function name --- geodjango/samplelocations/api/contacts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geodjango/samplelocations/api/contacts.py b/geodjango/samplelocations/api/contacts.py index 0287199..40ca3cf 100644 --- a/geodjango/samplelocations/api/contacts.py +++ b/geodjango/samplelocations/api/contacts.py @@ -5,7 +5,7 @@ router = Router() @router.post("") -def add_contact_for_well( +def post_contact( request, well_id: int, name: str, From 1cc199e962f4c7f136c4a2a10e31e124b903e4b4 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 8 Jul 2025 11:09:39 -0600 Subject: [PATCH 08/57] fix: delete api.py --- geodjango/geodjango/api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/geodjango/geodjango/api.py b/geodjango/geodjango/api.py index 23567fe..52d032b 100644 --- a/geodjango/geodjango/api.py +++ b/geodjango/geodjango/api.py @@ -2,6 +2,6 @@ api = NinjaAPI(docs=Redoc()) -api.add_router('/locations', 'samplelocations.api.locations.router') -api.add_router('/wells', 'samplelocations.api.wells.router') -api.add_router('/contacts', 'samplelocations.api.contacts.router') \ No newline at end of file +api.add_router('/locations', 'samplelocations.api.locations.router', tags=['locations']) +api.add_router('/wells', 'samplelocations.api.wells.router', tags=['wells']) +api.add_router('/contacts', 'samplelocations.api.contacts.router', tags=['contacts']) \ No newline at end of file From e7b769341cfab89eccff55189514ceea651be51a Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 8 Jul 2025 11:20:40 -0600 Subject: [PATCH 09/57] cleanup: remove outdate test files --- .../samplelocations/tests/test_contact.py | 20 --------- .../tests/test_contact_owner.py | 26 ----------- .../samplelocations/tests/test_equipment.py | 35 --------------- .../samplelocations/tests/test_lexicon.py | 19 -------- geodjango/samplelocations/tests/test_owner.py | 22 ---------- .../tests/test_samplelocation.py | 36 --------------- .../samplelocations/tests/test_spring.py | 18 -------- geodjango/samplelocations/tests/test_well.py | 44 ------------------- .../samplelocations/tests/test_wellscreen.py | 30 ------------- 9 files changed, 250 deletions(-) delete mode 100644 geodjango/samplelocations/tests/test_contact.py delete mode 100644 geodjango/samplelocations/tests/test_contact_owner.py delete mode 100644 geodjango/samplelocations/tests/test_equipment.py delete mode 100644 geodjango/samplelocations/tests/test_lexicon.py delete mode 100644 geodjango/samplelocations/tests/test_owner.py delete mode 100644 geodjango/samplelocations/tests/test_samplelocation.py delete mode 100644 geodjango/samplelocations/tests/test_spring.py delete mode 100644 geodjango/samplelocations/tests/test_well.py delete mode 100644 geodjango/samplelocations/tests/test_wellscreen.py diff --git a/geodjango/samplelocations/tests/test_contact.py b/geodjango/samplelocations/tests/test_contact.py deleted file mode 100644 index 6e6ff65..0000000 --- a/geodjango/samplelocations/tests/test_contact.py +++ /dev/null @@ -1,20 +0,0 @@ -from django.test import TestCase -from ..models import Contact - -class ContactModelTest(TestCase): - def test_create_contact(self): - contact = Contact.objects.create(name="John Doe", email="john@example.com", phone="1234567890") - self.assertEqual(contact.name, "John Doe") - self.assertEqual(contact.email, "john@example.com") - self.assertEqual(contact.phone, "1234567890") - self.assertIsNotNone(contact.date_created) - - def test_str_method(self): - contact = Contact.objects.create(name="Jane Doe", email="jane@example.com") - self.assertEqual(str(contact), "Jane Doe") - - def test_ordering(self): - Contact.objects.create(name="B", email="b@example.com") - Contact.objects.create(name="A", email="a@example.com") - names = list(Contact.objects.values_list('name', flat=True)) - self.assertEqual(names, sorted(names)) diff --git a/geodjango/samplelocations/tests/test_contact_owner.py b/geodjango/samplelocations/tests/test_contact_owner.py deleted file mode 100644 index 10dfbbc..0000000 --- a/geodjango/samplelocations/tests/test_contact_owner.py +++ /dev/null @@ -1,26 +0,0 @@ -# hello -# This file is now split into test_contact.py and test_owner.py. You can remove this file if you wish. - -from django.test import TestCase -from ..models import Contact, Owner - - -class OwnerModelTest(TestCase): - def setUp(self): - self.contact = Contact.objects.create(name="Owner Contact", email="owner@example.com") - - def test_create_owner(self): - owner = Owner.objects.create(name="Owner1", contact=self.contact) - self.assertEqual(owner.name, "Owner1") - self.assertEqual(owner.contact, self.contact) - self.assertIsNotNone(owner.date_created) - - def test_str_method(self): - owner = Owner.objects.create(name="Owner2", contact=self.contact) - self.assertEqual(str(owner), "Owner2") - - def test_ordering(self): - Owner.objects.create(name="B", contact=self.contact) - Owner.objects.create(name="A", contact=self.contact) - names = list(Owner.objects.values_list('name', flat=True)) - self.assertEqual(names, sorted(names)) diff --git a/geodjango/samplelocations/tests/test_equipment.py b/geodjango/samplelocations/tests/test_equipment.py deleted file mode 100644 index 02a24c1..0000000 --- a/geodjango/samplelocations/tests/test_equipment.py +++ /dev/null @@ -1,35 +0,0 @@ -from django.test import TestCase -from ..models import Equipment, SampleLocation, Owner, Contact -from django.contrib.gis.geos import Point -from datetime import datetime - -class EquipmentModelTest(TestCase): - def setUp(self): - self.contact = Contact.objects.create(name="Contact", email="contact@example.com") - self.owner = Owner.objects.create(name="Owner", contact=self.contact) - self.location = SampleLocation.objects.create(name="Loc", point=Point(-105, 40), owner=self.owner) - - def test_create_equipment(self): - eq = Equipment.objects.create( - equipment_type="Pump", - model="ModelX", - serial_no="SN123", - date_installed=datetime(2020, 1, 1), - date_removed=datetime(2021, 1, 1), - recording_interval=60, - equipment_notes="Test notes", - location=self.location - ) - self.assertEqual(eq.equipment_type, "Pump") - self.assertEqual(eq.model, "ModelX") - self.assertEqual(eq.serial_no, "SN123") - self.assertEqual(eq.date_installed.year, 2020) - self.assertEqual(eq.date_removed.year, 2021) - self.assertEqual(eq.recording_interval, 60) - self.assertEqual(eq.equipment_notes, "Test notes") - self.assertEqual(eq.location, self.location) - - def test_str_method(self): - eq = Equipment.objects.create(equipment_type="Sensor", model="M1", serial_no="S1", location=self.location) - self.assertIn("Sensor", str(eq)) - self.assertIn(self.location.name, str(eq)) diff --git a/geodjango/samplelocations/tests/test_lexicon.py b/geodjango/samplelocations/tests/test_lexicon.py deleted file mode 100644 index b099953..0000000 --- a/geodjango/samplelocations/tests/test_lexicon.py +++ /dev/null @@ -1,19 +0,0 @@ -from django.test import TestCase -from ..models import Lexicon - -class LexiconModelTest(TestCase): - def test_create_lexicon(self): - lex = Lexicon.objects.create(name="Test Lexicon", description="A test lexicon.") - self.assertEqual(lex.name, "Test Lexicon") - self.assertEqual(lex.description, "A test lexicon.") - self.assertIsNotNone(lex.date_created) - - def test_str_method(self): - lex = Lexicon.objects.create(name="Lexicon1") - self.assertEqual(str(lex), "Lexicon1") - - def test_ordering(self): - Lexicon.objects.create(name="B") - Lexicon.objects.create(name="A") - names = list(Lexicon.objects.values_list('name', flat=True)) - self.assertEqual(names, sorted(names)) diff --git a/geodjango/samplelocations/tests/test_owner.py b/geodjango/samplelocations/tests/test_owner.py deleted file mode 100644 index 13ce4a8..0000000 --- a/geodjango/samplelocations/tests/test_owner.py +++ /dev/null @@ -1,22 +0,0 @@ -from django.test import TestCase -from ..models import Owner, Contact - -class OwnerModelTest(TestCase): - def setUp(self): - self.contact = Contact.objects.create(name="Owner Contact", email="owner@example.com") - - def test_create_owner(self): - owner = Owner.objects.create(name="Owner1", contact=self.contact) - self.assertEqual(owner.name, "Owner1") - self.assertEqual(owner.contact, self.contact) - self.assertIsNotNone(owner.date_created) - - def test_str_method(self): - owner = Owner.objects.create(name="Owner2", contact=self.contact) - self.assertEqual(str(owner), "Owner2") - - def test_ordering(self): - Owner.objects.create(name="B", contact=self.contact) - Owner.objects.create(name="A", contact=self.contact) - names = list(Owner.objects.values_list('name', flat=True)) - self.assertEqual(names, sorted(names)) diff --git a/geodjango/samplelocations/tests/test_samplelocation.py b/geodjango/samplelocations/tests/test_samplelocation.py deleted file mode 100644 index a5e2d0e..0000000 --- a/geodjango/samplelocations/tests/test_samplelocation.py +++ /dev/null @@ -1,36 +0,0 @@ -from django.test import TestCase -from django.contrib.gis.geos import Point -from ..models import SampleLocation, Owner, Contact - -class SampleLocationModelTest(TestCase): - def setUp(self): - self.contact = Contact.objects.create(name="Contact", email="contact@example.com") - self.owner = Owner.objects.create(name="Owner", contact=self.contact) - - def test_create_sample_location(self): - point = Point(-105.0, 40.0) - loc = SampleLocation.objects.create( - name="Loc1", - description="Test location", - visible=True, - point=point, - owner=self.owner - ) - self.assertEqual(loc.name, "Loc1") - self.assertEqual(loc.description, "Test location") - self.assertTrue(loc.visible) - self.assertEqual(loc.point, point) - self.assertEqual(loc.owner, self.owner) - self.assertIsNotNone(loc.date_created) - - def test_str_method(self): - point = Point(-105.0, 40.0) - loc = SampleLocation.objects.create(name="Loc2", point=point, owner=self.owner) - self.assertEqual(str(loc), "Loc2") - - def test_ordering(self): - point = Point(-105.0, 40.0) - SampleLocation.objects.create(name="B", point=point, owner=self.owner) - SampleLocation.objects.create(name="A", point=point, owner=self.owner) - names = list(SampleLocation.objects.values_list('name', flat=True)) - self.assertEqual(names, sorted(names)) diff --git a/geodjango/samplelocations/tests/test_spring.py b/geodjango/samplelocations/tests/test_spring.py deleted file mode 100644 index a390efd..0000000 --- a/geodjango/samplelocations/tests/test_spring.py +++ /dev/null @@ -1,18 +0,0 @@ -from django.test import TestCase -from ..models import Spring, SampleLocation, Owner, Contact -from django.contrib.gis.geos import Point - -class SpringModelTest(TestCase): - def setUp(self): - self.contact = Contact.objects.create(name="Contact", email="contact@example.com") - self.owner = Owner.objects.create(name="Owner", contact=self.contact) - self.location = SampleLocation.objects.create(name="Loc", point=Point(-105, 40), owner=self.owner) - - def test_create_spring(self): - spring = Spring.objects.create(description="A spring", location=self.location) - self.assertEqual(spring.description, "A spring") - self.assertEqual(spring.location, self.location) - - def test_str_method(self): - spring = Spring.objects.create(location=self.location) - self.assertIn(self.location.name, str(spring)) diff --git a/geodjango/samplelocations/tests/test_well.py b/geodjango/samplelocations/tests/test_well.py deleted file mode 100644 index 282ef7e..0000000 --- a/geodjango/samplelocations/tests/test_well.py +++ /dev/null @@ -1,44 +0,0 @@ -from django.test import TestCase -from ..models import Well, SampleLocation, Owner, Contact, Lexicon -from django.contrib.gis.geos import Point - -class WellModelTest(TestCase): - def setUp(self): - self.contact = Contact.objects.create(name="Contact", email="contact@example.com") - self.owner = Owner.objects.create(name="Owner", contact=self.contact) - self.location = SampleLocation.objects.create(name="Loc", point=Point(-105, 40), owner=self.owner) - self.lexicon = Lexicon.objects.create(name="Type1") - self.formation = Lexicon.objects.create(name="Formation1") - - def test_create_well(self): - well = Well.objects.create( - location=self.location, - ose_pod_id="POD123", - api_id="API123", - usgs_id="USGS123", - well_depth=100.0, - hole_depth=110.0, - well_type=self.lexicon, - casing_diameter=6.0, - casing_depth=80.0, - casing_description="Steel", - construction_notes="Notes", - formation_zone=self.formation - ) - self.assertEqual(well.location, self.location) - self.assertEqual(well.ose_pod_id, "POD123") - self.assertEqual(well.api_id, "API123") - self.assertEqual(well.usgs_id, "USGS123") - self.assertEqual(well.well_depth, 100.0) - self.assertEqual(well.hole_depth, 110.0) - self.assertEqual(well.well_type, self.lexicon) - self.assertEqual(well.casing_diameter, 6.0) - self.assertEqual(well.casing_depth, 80.0) - self.assertEqual(well.casing_description, "Steel") - self.assertEqual(well.construction_notes, "Notes") - self.assertEqual(well.formation_zone, self.formation) - - def test_str_method(self): - well = Well.objects.create(location=self.location) - self.assertIn(self.location.name, str(well)) - diff --git a/geodjango/samplelocations/tests/test_wellscreen.py b/geodjango/samplelocations/tests/test_wellscreen.py deleted file mode 100644 index 3f85ada..0000000 --- a/geodjango/samplelocations/tests/test_wellscreen.py +++ /dev/null @@ -1,30 +0,0 @@ -from django.test import TestCase -from ..models import Well, WellScreen, SampleLocation, Owner, Contact, Lexicon -from django.contrib.gis.geos import Point - -class WellScreenModelTest(TestCase): - def setUp(self): - self.contact = Contact.objects.create(name="Contact", email="contact@example.com") - self.owner = Owner.objects.create(name="Owner", contact=self.contact) - self.location = SampleLocation.objects.create(name="Loc", point=Point(-105, 40), owner=self.owner) - self.well = Well.objects.create(location=self.location) - self.screen_type = Lexicon.objects.create(name="ScreenType1") - - def test_create_well_screen(self): - screen = WellScreen.objects.create( - well=self.well, - screen_depth_top=10.0, - screen_depth_bottom=20.0, - screen_type=self.screen_type - ) - self.assertEqual(screen.well, self.well) - self.assertEqual(screen.screen_depth_top, 10.0) - self.assertEqual(screen.screen_depth_bottom, 20.0) - self.assertEqual(screen.screen_type, self.screen_type) - - def test_str_method(self): - screen = WellScreen.objects.create(well=self.well, screen_depth_top=5.0, screen_depth_bottom=15.0) - self.assertIn("5.0-15.0", str(screen)) - self.assertIn(str(self.well), str(screen)) - -# test comment From ecf07a80a84786147aff0b09a5f6f2ef0f9f8cfb Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 8 Jul 2025 11:21:19 -0600 Subject: [PATCH 10/57] style: add __init__.py to tests --- geodjango/samplelocations/tests/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 geodjango/samplelocations/tests/__init__.py diff --git a/geodjango/samplelocations/tests/__init__.py b/geodjango/samplelocations/tests/__init__.py new file mode 100644 index 0000000..e69de29 From 6a4def3a85fee37eadf54db7caaff65ddb8f7668 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 8 Jul 2025 11:21:52 -0600 Subject: [PATCH 11/57] feat: first init for test_mvp.py --- geodjango/samplelocations/tests/test_mvp.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 geodjango/samplelocations/tests/test_mvp.py diff --git a/geodjango/samplelocations/tests/test_mvp.py b/geodjango/samplelocations/tests/test_mvp.py new file mode 100644 index 0000000..e69de29 From 157f6b1c20653fe73bcf8a2267673be25f37fb1f Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 8 Jul 2025 11:22:19 -0600 Subject: [PATCH 12/57] fix: remove old test.py --- geodjango/samplelocations/tests.py | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 geodjango/samplelocations/tests.py diff --git a/geodjango/samplelocations/tests.py b/geodjango/samplelocations/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/geodjango/samplelocations/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. From 74f24369d171d4e49acdbdcd9932e2b79f085456 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 8 Jul 2025 11:23:28 -0600 Subject: [PATCH 13/57] feat: add sql alchemy poc mvp tests --- geodjango/samplelocations/tests/test_mvp.py | 134 ++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/geodjango/samplelocations/tests/test_mvp.py b/geodjango/samplelocations/tests/test_mvp.py index e69de29..e431126 100644 --- a/geodjango/samplelocations/tests/test_mvp.py +++ b/geodjango/samplelocations/tests/test_mvp.py @@ -0,0 +1,134 @@ +from django.test import TestCase +from ninja.testing import TestClient + +def test_add_location_minimum(): + location = { + "name": "Test Location 1", + "point": "POINT(10.1 10.1)", + "visible": True, + } + + +def test_add_location_all(): + location = { + "name": "Test Location 1", + "point": "POINT(10.1 10.1)", + "description": "this is a test location", + "visible": True, + } + + +def test_add_well_minimum(): + well = { + "location_id": 1, + "well_type": "Monitoring", + } + + +def test_add_well_all(): + well = { + "location_id": 1, + "api_id": "1001-0001", + "ose_pod_id": "RA-0001", + "well_type": "Monitoring", + "well_depth": 100.0, + "hole_depth": 100.0, + "casing_diameter": 10.0, + "casing_depth": 20.0, + "casing_description": "foo bar", + "formation_zone": "San Andres", + "construction_notes": "this is a test of notes", + } + + +def test_add_well_screen_minimum(): + well_screen = { + "well_id": 1, + "screen_depth_top": 100.0, + "screen_depth_bottom": 120.0, + } + + +def test_add_well_screen_all(): + well_screen = { + "well_id": 1, + "screen_depth_top": 100.0, + "screen_depth_bottom": 120.0, + "screen_type": "PVC", + } + + +def test_add_owner_with_contacts(): + owner = { + "name": "The Doe's", + "contact": [ + {"name": "John Doe", "phone": "123-456-7890", "email": "foo@gmail.com"}, + { + "name": "Jane Doe", + "phone": "913-356-7890", + "email": "jane@gmail.com", + }, + ], + } + + +def test_add_owner_without_contacts(): + owner = {"name": "Alice Bob"} + + +def test_add_asset(): + asset = { + "filename": "foo.png", + "storage_service": "gcs", + "storage_path": "gs://...", + "mime_type": "image/png", + "size": 100, + } + + +# ============== optional ? ============= +def test_add_lexicon(): + formation = { + "term": "San Andres", + "definition": "Some sandstone unit", + "category": "Formations", + } + + unit = { + "term": "TDS", + "definition": "Total Dissolved Solids", + "category": "water_chemistry", + } + + +def test_add_lexicon_triple(): + subject = { + "term": "MG-030", + "definition": "magdalena well", + "category": "location_identifier", + } + predicate = "same_as" + object_ = { + "term": "USGS1234", + "definition": "magdalena well", + "category": "location_identifier", + } + + +def test_add_lexicon_triple_existing_subject(): + subject = "TDS" + predicate = "same_as" + object_ = { + "term": "Total Dissolved Solids", + "definition": "all the solids dissolved in sample", + "category": "water_chemistry", + } + + +def test_add_lexicon_triple_existing(): + subject = "TDS" + predicate = "same_as" + object_ = "Total Dissolved Solids" + + +# ============= EOF ============================================= \ No newline at end of file From 894f8ac00736d606697bd9130bb0f4b0c18c77a2 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 8 Jul 2025 11:29:42 -0600 Subject: [PATCH 14/57] test: set up mvp tests --- geodjango/samplelocations/tests/test_mvp.py | 238 +++++++++----------- 1 file changed, 110 insertions(+), 128 deletions(-) diff --git a/geodjango/samplelocations/tests/test_mvp.py b/geodjango/samplelocations/tests/test_mvp.py index e431126..45216e8 100644 --- a/geodjango/samplelocations/tests/test_mvp.py +++ b/geodjango/samplelocations/tests/test_mvp.py @@ -1,134 +1,116 @@ from django.test import TestCase from ninja.testing import TestClient -def test_add_location_minimum(): - location = { - "name": "Test Location 1", - "point": "POINT(10.1 10.1)", - "visible": True, - } - - -def test_add_location_all(): - location = { - "name": "Test Location 1", - "point": "POINT(10.1 10.1)", - "description": "this is a test location", - "visible": True, - } - - -def test_add_well_minimum(): - well = { - "location_id": 1, - "well_type": "Monitoring", - } - - -def test_add_well_all(): - well = { - "location_id": 1, - "api_id": "1001-0001", - "ose_pod_id": "RA-0001", - "well_type": "Monitoring", - "well_depth": 100.0, - "hole_depth": 100.0, - "casing_diameter": 10.0, - "casing_depth": 20.0, - "casing_description": "foo bar", - "formation_zone": "San Andres", - "construction_notes": "this is a test of notes", - } - - -def test_add_well_screen_minimum(): - well_screen = { - "well_id": 1, - "screen_depth_top": 100.0, - "screen_depth_bottom": 120.0, - } - - -def test_add_well_screen_all(): - well_screen = { - "well_id": 1, - "screen_depth_top": 100.0, - "screen_depth_bottom": 120.0, - "screen_type": "PVC", - } - - -def test_add_owner_with_contacts(): - owner = { - "name": "The Doe's", - "contact": [ - {"name": "John Doe", "phone": "123-456-7890", "email": "foo@gmail.com"}, - { - "name": "Jane Doe", - "phone": "913-356-7890", - "email": "jane@gmail.com", - }, - ], - } - - -def test_add_owner_without_contacts(): - owner = {"name": "Alice Bob"} - - -def test_add_asset(): - asset = { - "filename": "foo.png", - "storage_service": "gcs", - "storage_path": "gs://...", - "mime_type": "image/png", - "size": 100, - } - - -# ============== optional ? ============= -def test_add_lexicon(): - formation = { - "term": "San Andres", - "definition": "Some sandstone unit", - "category": "Formations", - } - - unit = { - "term": "TDS", - "definition": "Total Dissolved Solids", - "category": "water_chemistry", - } - - -def test_add_lexicon_triple(): - subject = { - "term": "MG-030", - "definition": "magdalena well", - "category": "location_identifier", - } - predicate = "same_as" - object_ = { - "term": "USGS1234", - "definition": "magdalena well", - "category": "location_identifier", - } - - -def test_add_lexicon_triple_existing_subject(): - subject = "TDS" - predicate = "same_as" - object_ = { - "term": "Total Dissolved Solids", - "definition": "all the solids dissolved in sample", - "category": "water_chemistry", - } - - -def test_add_lexicon_triple_existing(): - subject = "TDS" - predicate = "same_as" - object_ = "Total Dissolved Solids" +client = TestClient() + +class TestLocations(TestCase): + + def test_get_locations(self): + response = client.get("/locations") + assert response.status_code == 200 + + def test_post_location(self): + location = { + "name": "Test Location", + "point": "POINT(10.1 10.1)", + "visible": True, + } + response = client.post("/locations", json=location) + assert response.status_code == 201 + assert response.json()["id"] is not None + + +class TestWells(TestCase): + + def test_get_wells(self): + response = client.get("/wells") + assert response.status_code == 200 + + def test_post_well(self): + well = { + "location_id": 1, + "api_id": "1001-0001", + "ose_pod_id": "RA-0001", + "well_type": "Monitoring", + "well_depth": 100.0, + "hole_depth": 100.0, + "casing_diameter": 10.0, + "casing_depth": 20.0, + "casing_description": "foo bar", + "formation_zone": "San Andres", + "construction_notes": "this is a test of notes", + } + response = client.post("/wells", json=well) + assert response.status_code == 201 + assert response.json()["id"] is not None + + + def test_post_well_screen(self): + well_screen = { + "well_id": 1, + "screen_depth_top": 100.0, + "screen_depth_bottom": 120.0, + "screen_type": "PVC", + } + response = client.post("/wells/well-screens/", json=well_screen) + assert response.status_code == 201 + assert response.json()["id"] is not None + + +class TestContacts(TestCase): + def test_post_contact(self): + contact = { + "well_id": 1, + "name": "John Doe", + "email": "foo@gmail.com", + } + response = client.post("/wells/contacts/", json=contact) + assert response.status_code == 201 + assert response.json()["id"] is not None + +# # ============== optional ? ============= +# def test_add_lexicon(): +# formation = { +# "term": "San Andres", +# "definition": "Some sandstone unit", +# "category": "Formations", +# } + +# unit = { +# "term": "TDS", +# "definition": "Total Dissolved Solids", +# "category": "water_chemistry", +# } + + +# def test_add_lexicon_triple(): +# subject = { +# "term": "MG-030", +# "definition": "magdalena well", +# "category": "location_identifier", +# } +# predicate = "same_as" +# object_ = { +# "term": "USGS1234", +# "definition": "magdalena well", +# "category": "location_identifier", +# } + + +# def test_add_lexicon_triple_existing_subject(): +# subject = "TDS" +# predicate = "same_as" +# object_ = { +# "term": "Total Dissolved Solids", +# "definition": "all the solids dissolved in sample", +# "category": "water_chemistry", +# } + + +# def test_add_lexicon_triple_existing(): +# subject = "TDS" +# predicate = "same_as" +# object_ = "Total Dissolved Solids" # ============= EOF ============================================= \ No newline at end of file From 0a208f47cd838fdbbadf21265525bcc547250bd3 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 8 Jul 2025 14:37:59 -0600 Subject: [PATCH 15/57] fix: assign test client to each test --- geodjango/geodjango/api.py | 2 +- geodjango/samplelocations/api/locations.py | 2 +- geodjango/samplelocations/tests/test_mvp.py | 38 ++++++++++++++------- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/geodjango/geodjango/api.py b/geodjango/geodjango/api.py index 52d032b..bee23e0 100644 --- a/geodjango/geodjango/api.py +++ b/geodjango/geodjango/api.py @@ -1,6 +1,6 @@ from ninja import NinjaAPI, Redoc -api = NinjaAPI(docs=Redoc()) +api = NinjaAPI(urls_namespace="main-api") api.add_router('/locations', 'samplelocations.api.locations.router', tags=['locations']) api.add_router('/wells', 'samplelocations.api.wells.router', tags=['wells']) diff --git a/geodjango/samplelocations/api/locations.py b/geodjango/samplelocations/api/locations.py index aced665..bba834c 100644 --- a/geodjango/samplelocations/api/locations.py +++ b/geodjango/samplelocations/api/locations.py @@ -1,5 +1,5 @@ from ninja import Router -from ..models import SampleLocation, Well, WellScreen, Contact, Owner, Lexicon +from ..models import SampleLocation, Owner from django.contrib.gis.geos import Point from django.shortcuts import get_object_or_404 diff --git a/geodjango/samplelocations/tests/test_mvp.py b/geodjango/samplelocations/tests/test_mvp.py index 45216e8..aa7517e 100644 --- a/geodjango/samplelocations/tests/test_mvp.py +++ b/geodjango/samplelocations/tests/test_mvp.py @@ -1,12 +1,23 @@ from django.test import TestCase from ninja.testing import TestClient +from geodjango.api import api -client = TestClient() -class TestLocations(TestCase): +class BaseTestClass(TestCase): + """ + Base class for all test cases. + This class can be used to set up common fixtures or configurations + that are shared across multiple test cases. + It can also be used to define common methods that can be reused + in all test cases. + """ + + client = TestClient(api) + +class TestLocations(BaseTestClass): def test_get_locations(self): - response = client.get("/locations") + response = self.client.get("/api/locations") assert response.status_code == 200 def test_post_location(self): @@ -15,15 +26,15 @@ def test_post_location(self): "point": "POINT(10.1 10.1)", "visible": True, } - response = client.post("/locations", json=location) - assert response.status_code == 201 + response = self.client.post("/api/locations", json=location) + assert response.status_code == 200 assert response.json()["id"] is not None -class TestWells(TestCase): +class TestWells(BaseTestClass): def test_get_wells(self): - response = client.get("/wells") + response = self.client.get("/api/wells") assert response.status_code == 200 def test_post_well(self): @@ -40,7 +51,7 @@ def test_post_well(self): "formation_zone": "San Andres", "construction_notes": "this is a test of notes", } - response = client.post("/wells", json=well) + response = self.client.post("/wells", json=well) assert response.status_code == 201 assert response.json()["id"] is not None @@ -52,20 +63,21 @@ def test_post_well_screen(self): "screen_depth_bottom": 120.0, "screen_type": "PVC", } - response = client.post("/wells/well-screens/", json=well_screen) - assert response.status_code == 201 + response = self.client.post("/api/wells/well-screens/", json=well_screen) + assert response.status_code == 200 assert response.json()["id"] is not None -class TestContacts(TestCase): +class TestContacts(BaseTestClass): + def test_post_contact(self): contact = { "well_id": 1, "name": "John Doe", "email": "foo@gmail.com", } - response = client.post("/wells/contacts/", json=contact) - assert response.status_code == 201 + response = self.client.post("/api/wells/contacts/", json=contact) + assert response.status_code == 200 assert response.json()["id"] is not None # # ============== optional ? ============= From 633f88129a5d699bf7d581a46ff90b47972c3466 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 8 Jul 2025 15:23:55 -0600 Subject: [PATCH 16/57] fix: fix test client --- geodjango/samplelocations/tests/test_mvp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/geodjango/samplelocations/tests/test_mvp.py b/geodjango/samplelocations/tests/test_mvp.py index aa7517e..b4ec055 100644 --- a/geodjango/samplelocations/tests/test_mvp.py +++ b/geodjango/samplelocations/tests/test_mvp.py @@ -51,8 +51,8 @@ def test_post_well(self): "formation_zone": "San Andres", "construction_notes": "this is a test of notes", } - response = self.client.post("/wells", json=well) - assert response.status_code == 201 + response = self.client.post("/api/wells", json=well) + assert response.status_code == 200 assert response.json()["id"] is not None From a8d0b08fa893feb8da391f23efc09469b0680cda Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 8 Jul 2025 16:24:29 -0600 Subject: [PATCH 17/57] style: remove unecessary urls namespace --- geodjango/geodjango/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geodjango/geodjango/api.py b/geodjango/geodjango/api.py index bee23e0..ec1498e 100644 --- a/geodjango/geodjango/api.py +++ b/geodjango/geodjango/api.py @@ -1,6 +1,6 @@ from ninja import NinjaAPI, Redoc -api = NinjaAPI(urls_namespace="main-api") +api = NinjaAPI() api.add_router('/locations', 'samplelocations.api.locations.router', tags=['locations']) api.add_router('/wells', 'samplelocations.api.wells.router', tags=['wells']) From 8b04c51d5f4ca3131e7a445c33e8490988a06f3b Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 8 Jul 2025 16:38:16 -0600 Subject: [PATCH 18/57] fix: utilize Django's self.assert... --- geodjango/samplelocations/tests/test_mvp.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/geodjango/samplelocations/tests/test_mvp.py b/geodjango/samplelocations/tests/test_mvp.py index b4ec055..46b73d4 100644 --- a/geodjango/samplelocations/tests/test_mvp.py +++ b/geodjango/samplelocations/tests/test_mvp.py @@ -18,7 +18,7 @@ class TestLocations(BaseTestClass): def test_get_locations(self): response = self.client.get("/api/locations") - assert response.status_code == 200 + self.assertEqual(response.status_code, 200) def test_post_location(self): location = { @@ -27,15 +27,15 @@ def test_post_location(self): "visible": True, } response = self.client.post("/api/locations", json=location) - assert response.status_code == 200 - assert response.json()["id"] is not None + self.assertEqual(response.status_code, 200) + self.assertIsNotNone(response.json()["id"]) class TestWells(BaseTestClass): def test_get_wells(self): response = self.client.get("/api/wells") - assert response.status_code == 200 + self.assertEqual(response.status_code, 200) def test_post_well(self): well = { @@ -52,8 +52,8 @@ def test_post_well(self): "construction_notes": "this is a test of notes", } response = self.client.post("/api/wells", json=well) - assert response.status_code == 200 - assert response.json()["id"] is not None + self.assertEqual(response.status_code, 200) + self.assertIsNotNone(response.json()["id"]) def test_post_well_screen(self): @@ -64,8 +64,8 @@ def test_post_well_screen(self): "screen_type": "PVC", } response = self.client.post("/api/wells/well-screens/", json=well_screen) - assert response.status_code == 200 - assert response.json()["id"] is not None + self.assertEqual(response.status_code, 200) + self.assertIsNotNone(response.json()["id"]) class TestContacts(BaseTestClass): @@ -77,8 +77,8 @@ def test_post_contact(self): "email": "foo@gmail.com", } response = self.client.post("/api/wells/contacts/", json=contact) - assert response.status_code == 200 - assert response.json()["id"] is not None + self.assertEqual(response.status_code, 200) + self.assertIsNotNone(response.json()["id"]) # # ============== optional ? ============= # def test_add_lexicon(): From 06491e6ada2d3a1e33f99a1aae7fde46ddf6874e Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Wed, 9 Jul 2025 12:04:51 -0600 Subject: [PATCH 19/57] feat: add more mvp tests --- geodjango/samplelocations/tests/not_tested/__init__.py | 0 geodjango/samplelocations/tests/not_tested/test_asset.py | 3 +++ geodjango/samplelocations/tests/not_tested/test_chemistry.py | 3 +++ geodjango/samplelocations/tests/not_tested/test_collabnet.py | 3 +++ geodjango/samplelocations/tests/not_tested/test_form.py | 3 +++ geodjango/samplelocations/tests/test_contact.py | 0 geodjango/samplelocations/tests/test_geochronology.py | 0 geodjango/samplelocations/tests/test_geospatial.py | 0 geodjango/samplelocations/tests/test_geothermal.py | 0 geodjango/samplelocations/tests/test_group.py | 0 geodjango/samplelocations/tests/test_lexicon.py | 0 geodjango/samplelocations/tests/test_location.py | 0 geodjango/samplelocations/tests/test_observation.py | 0 geodjango/samplelocations/tests/test_publication.py | 0 geodjango/samplelocations/tests/test_query.py | 0 geodjango/samplelocations/tests/test_regex.py | 0 geodjango/samplelocations/tests/test_sample.py | 0 geodjango/samplelocations/tests/test_search.py | 0 geodjango/samplelocations/tests/test_sensor.py | 0 geodjango/samplelocations/tests/test_series.py | 0 geodjango/samplelocations/tests/test_thing.py | 0 21 files changed, 12 insertions(+) create mode 100644 geodjango/samplelocations/tests/not_tested/__init__.py create mode 100644 geodjango/samplelocations/tests/not_tested/test_asset.py create mode 100644 geodjango/samplelocations/tests/not_tested/test_chemistry.py create mode 100644 geodjango/samplelocations/tests/not_tested/test_collabnet.py create mode 100644 geodjango/samplelocations/tests/not_tested/test_form.py create mode 100644 geodjango/samplelocations/tests/test_contact.py create mode 100644 geodjango/samplelocations/tests/test_geochronology.py create mode 100644 geodjango/samplelocations/tests/test_geospatial.py create mode 100644 geodjango/samplelocations/tests/test_geothermal.py create mode 100644 geodjango/samplelocations/tests/test_group.py create mode 100644 geodjango/samplelocations/tests/test_lexicon.py create mode 100644 geodjango/samplelocations/tests/test_location.py create mode 100644 geodjango/samplelocations/tests/test_observation.py create mode 100644 geodjango/samplelocations/tests/test_publication.py create mode 100644 geodjango/samplelocations/tests/test_query.py create mode 100644 geodjango/samplelocations/tests/test_regex.py create mode 100644 geodjango/samplelocations/tests/test_sample.py create mode 100644 geodjango/samplelocations/tests/test_search.py create mode 100644 geodjango/samplelocations/tests/test_sensor.py create mode 100644 geodjango/samplelocations/tests/test_series.py create mode 100644 geodjango/samplelocations/tests/test_thing.py diff --git a/geodjango/samplelocations/tests/not_tested/__init__.py b/geodjango/samplelocations/tests/not_tested/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geodjango/samplelocations/tests/not_tested/test_asset.py b/geodjango/samplelocations/tests/not_tested/test_asset.py new file mode 100644 index 0000000..65b9ba7 --- /dev/null +++ b/geodjango/samplelocations/tests/not_tested/test_asset.py @@ -0,0 +1,3 @@ +""" +Not in MVP Dreama (as of 2025-07-09) +""" \ No newline at end of file diff --git a/geodjango/samplelocations/tests/not_tested/test_chemistry.py b/geodjango/samplelocations/tests/not_tested/test_chemistry.py new file mode 100644 index 0000000..65b9ba7 --- /dev/null +++ b/geodjango/samplelocations/tests/not_tested/test_chemistry.py @@ -0,0 +1,3 @@ +""" +Not in MVP Dreama (as of 2025-07-09) +""" \ No newline at end of file diff --git a/geodjango/samplelocations/tests/not_tested/test_collabnet.py b/geodjango/samplelocations/tests/not_tested/test_collabnet.py new file mode 100644 index 0000000..65b9ba7 --- /dev/null +++ b/geodjango/samplelocations/tests/not_tested/test_collabnet.py @@ -0,0 +1,3 @@ +""" +Not in MVP Dreama (as of 2025-07-09) +""" \ No newline at end of file diff --git a/geodjango/samplelocations/tests/not_tested/test_form.py b/geodjango/samplelocations/tests/not_tested/test_form.py new file mode 100644 index 0000000..65b9ba7 --- /dev/null +++ b/geodjango/samplelocations/tests/not_tested/test_form.py @@ -0,0 +1,3 @@ +""" +Not in MVP Dreama (as of 2025-07-09) +""" \ No newline at end of file diff --git a/geodjango/samplelocations/tests/test_contact.py b/geodjango/samplelocations/tests/test_contact.py new file mode 100644 index 0000000..e69de29 diff --git a/geodjango/samplelocations/tests/test_geochronology.py b/geodjango/samplelocations/tests/test_geochronology.py new file mode 100644 index 0000000..e69de29 diff --git a/geodjango/samplelocations/tests/test_geospatial.py b/geodjango/samplelocations/tests/test_geospatial.py new file mode 100644 index 0000000..e69de29 diff --git a/geodjango/samplelocations/tests/test_geothermal.py b/geodjango/samplelocations/tests/test_geothermal.py new file mode 100644 index 0000000..e69de29 diff --git a/geodjango/samplelocations/tests/test_group.py b/geodjango/samplelocations/tests/test_group.py new file mode 100644 index 0000000..e69de29 diff --git a/geodjango/samplelocations/tests/test_lexicon.py b/geodjango/samplelocations/tests/test_lexicon.py new file mode 100644 index 0000000..e69de29 diff --git a/geodjango/samplelocations/tests/test_location.py b/geodjango/samplelocations/tests/test_location.py new file mode 100644 index 0000000..e69de29 diff --git a/geodjango/samplelocations/tests/test_observation.py b/geodjango/samplelocations/tests/test_observation.py new file mode 100644 index 0000000..e69de29 diff --git a/geodjango/samplelocations/tests/test_publication.py b/geodjango/samplelocations/tests/test_publication.py new file mode 100644 index 0000000..e69de29 diff --git a/geodjango/samplelocations/tests/test_query.py b/geodjango/samplelocations/tests/test_query.py new file mode 100644 index 0000000..e69de29 diff --git a/geodjango/samplelocations/tests/test_regex.py b/geodjango/samplelocations/tests/test_regex.py new file mode 100644 index 0000000..e69de29 diff --git a/geodjango/samplelocations/tests/test_sample.py b/geodjango/samplelocations/tests/test_sample.py new file mode 100644 index 0000000..e69de29 diff --git a/geodjango/samplelocations/tests/test_search.py b/geodjango/samplelocations/tests/test_search.py new file mode 100644 index 0000000..e69de29 diff --git a/geodjango/samplelocations/tests/test_sensor.py b/geodjango/samplelocations/tests/test_sensor.py new file mode 100644 index 0000000..e69de29 diff --git a/geodjango/samplelocations/tests/test_series.py b/geodjango/samplelocations/tests/test_series.py new file mode 100644 index 0000000..e69de29 diff --git a/geodjango/samplelocations/tests/test_thing.py b/geodjango/samplelocations/tests/test_thing.py new file mode 100644 index 0000000..e69de29 From fbdfc77f0455c2d59990ccee866e5581caf59da9 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Wed, 9 Jul 2025 12:05:42 -0600 Subject: [PATCH 20/57] style: add crud file for all DB interactions --- geodjango/samplelocations/api/crud.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 geodjango/samplelocations/api/crud.py diff --git a/geodjango/samplelocations/api/crud.py b/geodjango/samplelocations/api/crud.py new file mode 100644 index 0000000..e69de29 From d1431495fb3f03b34a651d74d28f405fe810e0d2 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Wed, 9 Jul 2025 12:07:39 -0600 Subject: [PATCH 21/57] style: put BaseTestClass in __init__.py for use by all tests --- geodjango/samplelocations/tests/__init__.py | 15 +++++++++++++++ geodjango/samplelocations/tests/test_mvp.py | 16 +--------------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/geodjango/samplelocations/tests/__init__.py b/geodjango/samplelocations/tests/__init__.py index e69de29..d1b97ff 100644 --- a/geodjango/samplelocations/tests/__init__.py +++ b/geodjango/samplelocations/tests/__init__.py @@ -0,0 +1,15 @@ +from django.test import TestCase +from ninja.testing import TestClient +from geodjango.api import api + + +class BaseTestClass(TestCase): + """ + Base class for all test cases. + This class can be used to set up common fixtures or configurations + that are shared across multiple test cases. + It can also be used to define common methods that can be reused + in all test cases. + """ + + client = TestClient(api) \ No newline at end of file diff --git a/geodjango/samplelocations/tests/test_mvp.py b/geodjango/samplelocations/tests/test_mvp.py index 46b73d4..e836408 100644 --- a/geodjango/samplelocations/tests/test_mvp.py +++ b/geodjango/samplelocations/tests/test_mvp.py @@ -1,18 +1,4 @@ -from django.test import TestCase -from ninja.testing import TestClient -from geodjango.api import api - - -class BaseTestClass(TestCase): - """ - Base class for all test cases. - This class can be used to set up common fixtures or configurations - that are shared across multiple test cases. - It can also be used to define common methods that can be reused - in all test cases. - """ - - client = TestClient(api) +from . import BaseTestClass class TestLocations(BaseTestClass): From 3466f0a454efac4d4f7db146116ad3c990f843ae Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Wed, 9 Jul 2025 13:33:53 -0600 Subject: [PATCH 22/57] test: create contact tests for mvp --- .../samplelocations/tests/test_contact.py | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) diff --git a/geodjango/samplelocations/tests/test_contact.py b/geodjango/samplelocations/tests/test_contact.py index e69de29..47727b9 100644 --- a/geodjango/samplelocations/tests/test_contact.py +++ b/geodjango/samplelocations/tests/test_contact.py @@ -0,0 +1,158 @@ +from . import BaseTestClass + +# ADD tests ====================================================== + + +from geodjango.samplelocations.models import Thing + +class TestAddContact(BaseTestClass): + """ + Test cases for adding contacts. + """ + + def setUp(self): + super().setUp() + # Create a Thing instance for use in each test + self.thing = Thing.objects.create( + name="Test Thing", + description="A thing for testing", + ) + + def tearDown(self): + return super().tearDown() + + def test_add_contact(self): + response = self.client.post( + "/contact", + json={ + "name": "Test Contact", + "role": "Owner", + "thing_id": self.thing.id, + "emails": [{"email": "test@example.com", "email_type": "Primary"}], + "phones": [{"phone_number": "+12345678901", "phone_type": "Primary"}], + "addresses": [ + { + "address_line_1": "123 Main St", + "city": "Test City", + "state": "NM", + "postal_code": "87501", + "country": "US", + "address_type": "Primary", + } + ], + }, + ) + data = response.json() + self.assertEqual(response.status_code, 200) + self.assertIn("id", data) + self.assertEqual(data["name"], "Test Contact") + self.assertEqual(data["role"], "Owner") + + self.assertEqual(len(data["emails"]), 1) + self.assertEqual(data["emails"][0]["email"], "test@example.com") + self.assertIn("id", data) + self.assertEqual(data["name"], "Test Contact") + self.assertEqual(data["role"], "Owner") + + self.assertEqual(len(data["emails"]), 1) + self.assertEqual(data["emails"][0]["email"], "test@example.com") + + self.assertEqual(len(data["phones"]), 1) + self.assertEqual(data["phones"][0]["phone_number"], "+12345678901") + self.assertEqual(len(data["addresses"]), 1) + self.assertEqual(data["addresses"][0]["address_line_1"], "123 Main St") + + + def test_phone_validation_fail(self): + for phone in [ + "definitely not a phone", + # "1234567890", + # "123-456-7890", + # "123-456-78901", + # "123-4567-890", + "123-456-789a", + "123-456-7890x1234", + "123.456.7890", + "(123) 456-7890", + ]: + + response = self.client.post( + "/contact", + json={ + "name": "Test Contact 2", + "thing_id": self.thing.id, + "role": "Primary", + "emails": [{"email": "fasdfasdf@gmail.com", "email_type": "Primary"}], + "phones": [{"phone_number": phone, "phone_type": "Primary"}], + "addresses": [ + { + "address_line_1": "123 Main St", + "city": "Test City", + "state": "NM", + "postal_code": "87501", + "country": "US", + "address_type": "Primary", + } + ], + }, + ) + data = response.json() + self.assertEqual(response.status_code, 422) + self.assertIn("detail", data, "Expected 'detail' in response") + self.assertEqual(len(data["detail"]), 1, "Expected 1 error in response") + detail = data["detail"][0] + self.assertEqual(detail["msg"], f"Value error, Invalid phone number. {phone}") + + + def test_email_validation_fail(self): + + for email in [ + "", + "invalid-email", + "invalid@domain", + "invalid@domain.", + "@domain.com", + ]: + response = self.client.post( + "/contact", + json={ + "name": "Test ContactX", + "thing_id": self.thing.id, + "role": "Primary", + "emails": [{"email": email, "email_type": "Primary"}], + "phones": [{"phone_number": "+12345678901", "phone_type": "Primary"}], + "addresses": [ + { + "address_line_1": "123 Main St", + "city": "Test City", + "state": "NM", + "postal_code": "87501", + "country": "US", + "address_type": "Primary", + } + ], + }, + ) + data = response.json() + self.assertEqual(response.status_code, 422) + self.assertIn("detail", data, "Expected 'detail' in response") + self.assertEqual(len(data["detail"]), 1, "Expected 1 error in response") + detail = data["detail"][0] + self.assertEqual(detail["msg"], f"Value error, Invalid email format. {email}") + + +# GET tests ====================================================== + +class TestGetContact(BaseTestClass): + + def test_get_contacts(self): + response = self.client.get("/contact") + self.assertEqual(response.status_code, 200) + self.assertGreater(len(response.json()), 0) + + def test_item_get_contact(self): + response = self.client.get("/contact/1") + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertEqual(data["id"], 1) + self.assertEqual(data["name"], "Test Contact") \ No newline at end of file From d9dc8618fd5825777e4ecfeacd65bd83050d8b8f Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Wed, 9 Jul 2025 14:41:56 -0600 Subject: [PATCH 23/57] fix: use thing_id instead of id --- geodjango/samplelocations/tests/test_contact.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/geodjango/samplelocations/tests/test_contact.py b/geodjango/samplelocations/tests/test_contact.py index 47727b9..9d3c139 100644 --- a/geodjango/samplelocations/tests/test_contact.py +++ b/geodjango/samplelocations/tests/test_contact.py @@ -27,7 +27,7 @@ def test_add_contact(self): json={ "name": "Test Contact", "role": "Owner", - "thing_id": self.thing.id, + "thing_id": self.thing.thing_id, "emails": [{"email": "test@example.com", "email_type": "Primary"}], "phones": [{"phone_number": "+12345678901", "phone_type": "Primary"}], "addresses": [ @@ -80,7 +80,7 @@ def test_phone_validation_fail(self): "/contact", json={ "name": "Test Contact 2", - "thing_id": self.thing.id, + "thing_id": self.thing.thing_id, "role": "Primary", "emails": [{"email": "fasdfasdf@gmail.com", "email_type": "Primary"}], "phones": [{"phone_number": phone, "phone_type": "Primary"}], @@ -117,7 +117,7 @@ def test_email_validation_fail(self): "/contact", json={ "name": "Test ContactX", - "thing_id": self.thing.id, + "thing_id": self.thing.thing_id, "role": "Primary", "emails": [{"email": email, "email_type": "Primary"}], "phones": [{"phone_number": "+12345678901", "phone_type": "Primary"}], From cc57a8b114d9e785e66e025dd5257fa17e4fae07 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Wed, 9 Jul 2025 14:46:48 -0600 Subject: [PATCH 24/57] test: scaffold geochronology test --- .../tests/test_geochronology.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/geodjango/samplelocations/tests/test_geochronology.py b/geodjango/samplelocations/tests/test_geochronology.py index e69de29..cc416d9 100644 --- a/geodjango/samplelocations/tests/test_geochronology.py +++ b/geodjango/samplelocations/tests/test_geochronology.py @@ -0,0 +1,31 @@ +from . import BaseTestClass +from geodjango.samplelocations.models import Thing, Location, Geochronology + +class TestAddGeochronology(BaseTestClass): + + def setUp(self): + super().setUp() + # Create a Thing instance for use in each test + self.thing = self.Thing.objects.create(name="Test Thing", description="A thing for testing") + # Create a Location instance for use in each test + self.location = Location.objects.create(name="Test Location", description="A location for testing") + + def tearDown(self): + return super().tearDown() + + def test_add_age(self): + + response = self.client.post( + "/geochronology", + json={ + "location_id": self.location.location_id, + "age": 100, + "age_unit": "Ma", + "thing_id": self.thing.thing_id, + }, + ) + data = response.json() + self.assertEqual(response.status_code, 200) + self.assertIn("id", data) + self.assertEqual(data["age"], 100) + self.assertEqual(data["age_unit"], "Ma") \ No newline at end of file From 7b5b9ffe97e58c4c90e6dfc096ffc985d6bf61fd Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Wed, 9 Jul 2025 15:24:18 -0600 Subject: [PATCH 25/57] test: move geospatial tests to test_location.py --- geodjango/samplelocations/tests/not_tested/test_geospatial.py | 3 +++ geodjango/samplelocations/tests/test_geospatial.py | 0 2 files changed, 3 insertions(+) create mode 100644 geodjango/samplelocations/tests/not_tested/test_geospatial.py delete mode 100644 geodjango/samplelocations/tests/test_geospatial.py diff --git a/geodjango/samplelocations/tests/not_tested/test_geospatial.py b/geodjango/samplelocations/tests/not_tested/test_geospatial.py new file mode 100644 index 0000000..badccb3 --- /dev/null +++ b/geodjango/samplelocations/tests/not_tested/test_geospatial.py @@ -0,0 +1,3 @@ +""" +These tests belong in test_location.py +""" \ No newline at end of file diff --git a/geodjango/samplelocations/tests/test_geospatial.py b/geodjango/samplelocations/tests/test_geospatial.py deleted file mode 100644 index e69de29..0000000 From 18c048548f1e721239924305a114013da27c9463 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Wed, 9 Jul 2025 15:28:26 -0600 Subject: [PATCH 26/57] test: skip geothermal tests --- geodjango/samplelocations/tests/not_tested/test_geothermal.py | 3 +++ geodjango/samplelocations/tests/test_geothermal.py | 0 2 files changed, 3 insertions(+) create mode 100644 geodjango/samplelocations/tests/not_tested/test_geothermal.py delete mode 100644 geodjango/samplelocations/tests/test_geothermal.py diff --git a/geodjango/samplelocations/tests/not_tested/test_geothermal.py b/geodjango/samplelocations/tests/not_tested/test_geothermal.py new file mode 100644 index 0000000..63ccaa3 --- /dev/null +++ b/geodjango/samplelocations/tests/not_tested/test_geothermal.py @@ -0,0 +1,3 @@ +""" +All geothermal tests are skipped in SQL Alchemy POC +""" \ No newline at end of file diff --git a/geodjango/samplelocations/tests/test_geothermal.py b/geodjango/samplelocations/tests/test_geothermal.py deleted file mode 100644 index e69de29..0000000 From bc6d722f47da1d2043349e8e7ef20d035c97c8bd Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Wed, 9 Jul 2025 15:39:10 -0600 Subject: [PATCH 27/57] test: scaffold group tests --- geodjango/samplelocations/tests/test_group.py | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/geodjango/samplelocations/tests/test_group.py b/geodjango/samplelocations/tests/test_group.py index e69de29..b9c349a 100644 --- a/geodjango/samplelocations/tests/test_group.py +++ b/geodjango/samplelocations/tests/test_group.py @@ -0,0 +1,69 @@ +from . import BaseTestClass + + +class TestAddGroup(BaseTestClass): + + def setUp(self): + super().setUp() + # Create a Thing instance for use in each test + self.thing = self.Thing.objects.create( + name="Test Thing", + description="A thing for testing", + ) + + def tearDown(self): + return super().tearDown() + + def test_add_group(self): + response = self.client.post( + "/group", + json={"name": "Test Group"}, + ) + data = response.json() + self.assertEqual(response.status_code, 201) + self.assertIn("id", data) + self.assertEqual(data["name"], "Test Group") + + + def test_add_group_thing(self): + response = self.client.post( + "/group/association", + json={"group_id": 1, "thing_id": self.thing.id}, + ) + data = response.json() + self.assertEqual(response.status_code, 201) + self.assertIn("id", data) + self.assertEqual(data["group_id"], 1) + self.assertEqual(data["thing_id"], self.thing.id) + + +# GET tests ====================================================== + +class TestGetGroup(BaseTestClass): + + def setUp(self): + super().setUp() + # Create a Group instance for use in each test + self.group = self.Group.objects.create(name="Test Group") + + def tearDown(self): + return super().tearDown() + + def test_get_groups(self): + response = self.client.get("/group") + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertGreater(len(data), 0) # Assuming there are groups in the database + + def test_get_group_by_id(self): + response = self.client.get(f"/group/{self.group.id}") + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertEqual(data["id"], self.group.id) + self.assertEqual(data["name"], self.group.name) + + def test_get_group_association(self): + response = self.client.get(f"/group/association/{self.group.id}") + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertGreater(len(data), 0) \ No newline at end of file From c2030395aa3c7d72c70a2d7c179c83276d3cd570 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Wed, 9 Jul 2025 16:21:11 -0600 Subject: [PATCH 28/57] test: use absolute path imports from root --- geodjango/samplelocations/tests/test_contact.py | 5 ++--- geodjango/samplelocations/tests/test_geochronology.py | 4 ++-- geodjango/samplelocations/tests/test_group.py | 2 +- geodjango/samplelocations/tests/test_mvp.py | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/geodjango/samplelocations/tests/test_contact.py b/geodjango/samplelocations/tests/test_contact.py index 9d3c139..e08ab13 100644 --- a/geodjango/samplelocations/tests/test_contact.py +++ b/geodjango/samplelocations/tests/test_contact.py @@ -1,10 +1,9 @@ -from . import BaseTestClass +from samplelocations.tests import BaseTestClass +from samplelocations.models import Thing # ADD tests ====================================================== -from geodjango.samplelocations.models import Thing - class TestAddContact(BaseTestClass): """ Test cases for adding contacts. diff --git a/geodjango/samplelocations/tests/test_geochronology.py b/geodjango/samplelocations/tests/test_geochronology.py index cc416d9..6263d33 100644 --- a/geodjango/samplelocations/tests/test_geochronology.py +++ b/geodjango/samplelocations/tests/test_geochronology.py @@ -1,5 +1,5 @@ -from . import BaseTestClass -from geodjango.samplelocations.models import Thing, Location, Geochronology +from samplelocations.tests import BaseTestClass +from samplelocations.models import Thing, Location, Geochronology class TestAddGeochronology(BaseTestClass): diff --git a/geodjango/samplelocations/tests/test_group.py b/geodjango/samplelocations/tests/test_group.py index b9c349a..01ca91a 100644 --- a/geodjango/samplelocations/tests/test_group.py +++ b/geodjango/samplelocations/tests/test_group.py @@ -1,4 +1,4 @@ -from . import BaseTestClass +from samplelocations.tests import BaseTestClass class TestAddGroup(BaseTestClass): diff --git a/geodjango/samplelocations/tests/test_mvp.py b/geodjango/samplelocations/tests/test_mvp.py index e836408..8965c96 100644 --- a/geodjango/samplelocations/tests/test_mvp.py +++ b/geodjango/samplelocations/tests/test_mvp.py @@ -1,4 +1,4 @@ -from . import BaseTestClass +from samplelocations.tests import BaseTestClass class TestLocations(BaseTestClass): From 32741690b2f127706e5af1491560c62d35125faf Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Wed, 9 Jul 2025 16:36:49 -0600 Subject: [PATCH 29/57] style: use absolute imports for urls --- geodjango/geodjango/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geodjango/geodjango/urls.py b/geodjango/geodjango/urls.py index 7333cf3..e4b5da3 100644 --- a/geodjango/geodjango/urls.py +++ b/geodjango/geodjango/urls.py @@ -16,7 +16,7 @@ """ from django.contrib import admin from django.urls import path -from .api import api +from geodjango.api import api from samplelocations import views urlpatterns = [ From 551a7b1ec560d32ce45ab039a39b2793002cf66d Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Thu, 10 Jul 2025 09:42:35 -0600 Subject: [PATCH 30/57] test: scaffold lexicon tests --- .../samplelocations/tests/test_lexicon.py | 88 +++++++++++++++++++ .../samplelocations/tests/test_location.py | 21 +++++ 2 files changed, 109 insertions(+) diff --git a/geodjango/samplelocations/tests/test_lexicon.py b/geodjango/samplelocations/tests/test_lexicon.py index e69de29..4811a6f 100644 --- a/geodjango/samplelocations/tests/test_lexicon.py +++ b/geodjango/samplelocations/tests/test_lexicon.py @@ -0,0 +1,88 @@ +from samplelocations.tests import BaseTestClass +from samplelocations.models import Lexicon + +class TestAddLexicon(BaseTestClass): + """ + Test cases for adding lexicon categories and terms. + """ + + def setUp(self): + super().setUp() + + def tearDown(self): + return super().tearDown() + + def test_add_lexicon_category(self): + name = "Test Category" + description = "This is a test category." + + response = self.client.post( + "/lexicon/category/add", + json={"name": name, "description": description}, + ) + + data = response.json() + self.assertEqual(response.status_code, 201) + self.assertEqual(data["name"], name) + self.assertEqual(data["description"], description) + + + def test_add_lexicon_term(self): + term = "test_term" + definition = "This is a test definition." + category = "Test Category" + + response = self.client.post( + "/lexicon/add", + json={"term": term, "definition": definition, "category": category}, + ) + + self.assertEqual(response.status_code, 201) + data = response.json() + self.assertEqual(data["term"], term) + self.assertEqual(data["definition"], definition) + + def test_add_triple(self): + subject = { + "term": "MG-030", + "definition": "magdalena well", + "category": "location_identifier", + } + predicate = "same_as" + object_ = { + "term": "USGS1234", + "definition": "magdalena well", + "category": "location_identifier", + } + + response = self.client.post( + "/lexicon/triple/add", + json={ + "subject": subject, + "predicate": predicate, + "object_": object_, + }, + ) + + self.assertEqual(response.status_code, 201) + data = response.json() + self.assertEqual(data["subject"], subject["term"]) + self.assertEqual(data["predicate"], predicate) + self.assertEqual(data["object_"], object_["term"]) + +class TestAddLexicon(BaseTestClass): + + def setUp(self): + super().setUp() + # Create a test category + self.category = Lexicon.objects.create( + name="Test Category", + description="A category for testing", + ) + + def tearDown(self): + return super().tearDown() + + def test_get_category(self): + response = self.client.get(f"/lexicon/category/{self.category.name}") + self.assertEqual(response.status_code, 200) \ No newline at end of file diff --git a/geodjango/samplelocations/tests/test_location.py b/geodjango/samplelocations/tests/test_location.py index e69de29..c435786 100644 --- a/geodjango/samplelocations/tests/test_location.py +++ b/geodjango/samplelocations/tests/test_location.py @@ -0,0 +1,21 @@ +""" +def test_get_geojson(): + response = client.get("/location/feature_collection") + assert response.status_code == 200 + data = response.json() + assert "type" in data + assert data["type"] == "FeatureCollection" + assert "features" in data + assert len(data["features"]) > 0 # Assuming there are features in the collection + + +def test_get_shapefile(): + response = client.get("/location/shapefile") + assert response.status_code == 200 + assert response.headers["Content-Type"] == "application/zip" + assert "Content-Disposition" in response.headers + assert ( + 'attachment; filename="locations.zip"' + == response.headers["Content-Disposition"] + ) +""" \ No newline at end of file From 04079c0bc22c1549cd72fde2a49825574e6adab7 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Thu, 10 Jul 2025 10:16:03 -0600 Subject: [PATCH 31/57] fix: add : to end of class definition --- geodjango/samplelocations/tests/test_lexicon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geodjango/samplelocations/tests/test_lexicon.py b/geodjango/samplelocations/tests/test_lexicon.py index 4811a6f..5af0075 100644 --- a/geodjango/samplelocations/tests/test_lexicon.py +++ b/geodjango/samplelocations/tests/test_lexicon.py @@ -70,7 +70,7 @@ def test_add_triple(self): self.assertEqual(data["predicate"], predicate) self.assertEqual(data["object_"], object_["term"]) -class TestAddLexicon(BaseTestClass): +class TestGetLexicon(BaseTestClass): def setUp(self): super().setUp() From 37842b585d534c65fcd519f031f804b608079c7d Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Thu, 10 Jul 2025 15:55:50 -0600 Subject: [PATCH 32/57] test: scaffold location tests --- .../migrations/0001_initial.py | 2 +- .../samplelocations/tests/test_location.py | 75 ++++++++++++++++++- 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/geodjango/samplelocations/migrations/0001_initial.py b/geodjango/samplelocations/migrations/0001_initial.py index d6c175a..4e9cd5f 100644 --- a/geodjango/samplelocations/migrations/0001_initial.py +++ b/geodjango/samplelocations/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.3 on 2025-06-26 15:14 +# Generated by Django 5.2.3 on 2025-07-10 21:55 import django.contrib.gis.db.models.fields import django.db.models.deletion diff --git a/geodjango/samplelocations/tests/test_location.py b/geodjango/samplelocations/tests/test_location.py index c435786..cf95525 100644 --- a/geodjango/samplelocations/tests/test_location.py +++ b/geodjango/samplelocations/tests/test_location.py @@ -18,4 +18,77 @@ def test_get_shapefile(): 'attachment; filename="locations.zip"' == response.headers["Content-Disposition"] ) -""" \ No newline at end of file +""" + +from samplelocations.tests import BaseTestClass +from samplelocations.models import Location + +class TestAddLocation(BaseTestClass): + + def test_add_location_visible_is_true(self): + response = self.client.post( + "/location", + json={ + "name": "Test Location 1", + "point": "POINT(10.1 10.1)", + "visible": True, + }, + ) + self.assertEqual(response.status_code, 201) + data = response.json() + self.assertEqual(data["id"], 2) + + def test_add_location_visible_is_false(self): + response = self.client.post( + "/location", + json={ + "name": "Test Location 2", + "point": "POINT(50.0 50.0)", + "visible": False, + }, + ) + self.assertEqual(response.status_code, 201) + data = response.json() + self.assertEqual(data["id"], 3) + + +class TestGetLocation(BaseTestClass): + + def setUp(self): + self.location = Location.objects.create( + name="Test Location", + point="POINT(10.0 10.0)", + visible=True, + ) + + def tearDown(self): + self.location.delete() + return super().tearDown() + + def get_specific_location(self): + response = self.client.get(f"/location/{self.location.id}") + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertEqual(data["id"], self.location.id) + self.assertEqual(data["name"], self.location.name) + self.assertEqual(data["point"], "POINT(10.0 10.0)") + + def test_get_location_as_geojson(self): + response = self.client.get("/location?format=feature-collection") + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertIn("type", data) + self.assertEqual(data["type"], "FeatureCollection") + self.assertIn("features", data) + self.assertGreater(len(data["features"]), 0) # Assuming there are features in the collection + + + def test_get_location_as_shapefile(self): + response = self.client.get("/location?format=shapefile") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.headers["Content-Type"], "application/zip") + self.assertIn("Content-Disposition", response.headers) + self.assertEqual( + 'attachment; filename="locations.zip"', + response.headers["Content-Disposition"] + ) \ No newline at end of file From 089be35c70b469c8b42fe40bb625fdb49cc8cab3 Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Thu, 24 Jul 2025 10:58:47 -0600 Subject: [PATCH 33/57] Create migration 0002 --- ...n_location_sample_sensor_thing_and_more.py | 213 ++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 geodjango/samplelocations/migrations/0002_datastream_observation_location_sample_sensor_thing_and_more.py diff --git a/geodjango/samplelocations/migrations/0002_datastream_observation_location_sample_sensor_thing_and_more.py b/geodjango/samplelocations/migrations/0002_datastream_observation_location_sample_sensor_thing_and_more.py new file mode 100644 index 0000000..adbb230 --- /dev/null +++ b/geodjango/samplelocations/migrations/0002_datastream_observation_location_sample_sensor_thing_and_more.py @@ -0,0 +1,213 @@ +# Generated by Django 5.2.3 on 2025-07-11 02:53 + +import django.contrib.gis.db.models.fields +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('samplelocations', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Datastream', + fields=[ + ('datastream_id', models.BigAutoField(primary_key=True, serialize=False)), + ('observed_property', models.CharField(max_length=100)), + ('release_status', models.BooleanField(default=False)), + ], + ), + migrations.CreateModel( + name='Observation', + fields=[ + ('observation_id', models.BigAutoField(primary_key=True, serialize=False)), + ('observed_value', models.FloatField(help_text='The value of the observation')), + ('release_status', models.BooleanField(default=False)), + ('datastream', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='observations', to='samplelocations.datastream', verbose_name='related datastream')), + ], + ), + migrations.CreateModel( + name='Location', + fields=[ + ('location_id', models.BigAutoField(primary_key=True, serialize=False)), + ('coordinate', django.contrib.gis.db.models.fields.PointField(dim=3, srid=4326)), + ('date_created', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'verbose_name': 'Location', + 'verbose_name_plural': 'Locations', + 'db_table_comment': "This table stores point locations on the earth's surface", + }, + ), + migrations.CreateModel( + name='Sample', + fields=[ + ('sample_id', models.BigAutoField(primary_key=True, serialize=False)), + ('sample_date', models.DateTimeField()), + ('sample_notes', models.TextField(blank=True, null=True)), + ], + ), + migrations.CreateModel( + name='Sensor', + fields=[ + ('sensor_id', models.BigAutoField(primary_key=True, serialize=False)), + ('serial_number', models.CharField(blank=True, max_length=50, null=True)), + ('install_date', models.DateTimeField(blank=True, null=True)), + ('model', models.CharField(blank=True, max_length=50, null=True)), + ('notes', models.TextField(blank=True, null=True)), + ], + ), + migrations.CreateModel( + name='Thing', + fields=[ + ('thing_id', models.BigAutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=100, unique=True)), + ('release_status', models.BooleanField(default=False)), + ('date_created', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'verbose_name': 'Thing', + 'verbose_name_plural': 'Things', + }, + ), + migrations.RemoveField( + model_name='owner', + name='contact', + ), + migrations.RemoveField( + model_name='equipment', + name='location', + ), + migrations.RemoveField( + model_name='well', + name='well_type', + ), + migrations.RemoveField( + model_name='wellscreen', + name='screen_type', + ), + migrations.RemoveField( + model_name='well', + name='formation_zone', + ), + migrations.RemoveField( + model_name='samplelocation', + name='owner', + ), + migrations.RemoveField( + model_name='spring', + name='location', + ), + migrations.RemoveField( + model_name='well', + name='location', + ), + migrations.RemoveField( + model_name='wellscreen', + name='well', + ), + migrations.CreateModel( + name='GroundwaterLevelObservation', + fields=[ + ('groundwater_level_observation_id', models.BigAutoField(primary_key=True, serialize=False)), + ('observation_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, related_name='groundwater_level_observations', to='samplelocations.observation', verbose_name='related observation')), + ], + bases=('samplelocations.observation',), + ), + migrations.CreateModel( + name='Location_Thing_Junction', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('effective_start', models.DateTimeField()), + ('effective_end', models.DateTimeField()), + ('location', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='locations_things', to='samplelocations.location', verbose_name='related location')), + ], + options={ + 'db_table_comment': 'Junction table linking Location and Thing models', + }, + ), + migrations.AddField( + model_name='observation', + name='sample', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='observations', to='samplelocations.sample', verbose_name='related sample'), + ), + migrations.AddField( + model_name='datastream', + name='sensor_id', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='datastreams', to='samplelocations.sensor', verbose_name='related sensor'), + ), + migrations.CreateModel( + name='SpringThing', + fields=[ + ('springthing_id', models.BigAutoField(primary_key=True, serialize=False)), + ('thing_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, related_name='springthings', to='samplelocations.thing', verbose_name='related thing')), + ('description', models.CharField(blank=True, max_length=255, null=True)), + ], + bases=('samplelocations.thing',), + ), + migrations.CreateModel( + name='WellThing', + fields=[ + ('wellthing_id', models.BigAutoField(primary_key=True, serialize=False)), + ('thing_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, related_name='wellthings', to='samplelocations.thing', verbose_name='related thing')), + ('well_depth', models.FloatField(blank=True, help_text='feet below ground surface', null=True)), + ('hole_depth', models.FloatField(blank=True, help_text='feet below ground surface', null=True)), + ('casing_diameter', models.FloatField(blank=True, help_text='inches', null=True)), + ('casing_depth', models.FloatField(blank=True, help_text='feet below ground surface', null=True)), + ('casing_description', models.CharField(blank=True, max_length=50, null=True)), + ('construction_notes', models.TextField(blank=True, null=True)), + ], + bases=('samplelocations.thing',), + ), + migrations.AddField( + model_name='thing', + name='location', + field=models.ManyToManyField(related_name='things', through='samplelocations.Location_Thing_Junction', to='samplelocations.location', verbose_name='related location'), + ), + migrations.AddField( + model_name='sample', + name='thing', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='samples', to='samplelocations.thing', verbose_name='related thing'), + ), + migrations.AddField( + model_name='location_thing_junction', + name='thing', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='locations_things', to='samplelocations.thing', verbose_name='related thing'), + ), + migrations.AddField( + model_name='datastream', + name='thing', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='datastreams', to='samplelocations.thing', verbose_name='related thing'), + ), + migrations.DeleteModel( + name='Contact', + ), + migrations.DeleteModel( + name='Equipment', + ), + migrations.DeleteModel( + name='Lexicon', + ), + migrations.DeleteModel( + name='Owner', + ), + migrations.DeleteModel( + name='Spring', + ), + migrations.DeleteModel( + name='SampleLocation', + ), + migrations.DeleteModel( + name='Well', + ), + migrations.DeleteModel( + name='WellScreen', + ), + migrations.AddConstraint( + model_name='location_thing_junction', + constraint=models.UniqueConstraint(fields=('location', 'thing'), name='unique_location_thing'), + ), + ] From 19a8ba133932b447da45531e02a2eddab763597c Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Thu, 24 Jul 2025 16:06:12 -0600 Subject: [PATCH 34/57] refactor: Add `thing_type` field and choices for `thing_type` field. --- geodjango/samplelocations/models.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/geodjango/samplelocations/models.py b/geodjango/samplelocations/models.py index 13be40f..6860383 100644 --- a/geodjango/samplelocations/models.py +++ b/geodjango/samplelocations/models.py @@ -37,8 +37,22 @@ class Meta: class Thing(models.Model): """A base model representing a generic monitoring station (Thing)""" + + # Define class-based choices for the 'thing_type' field. + # This allows for a more structured way to define and use choices in Django models. + # The format is CHOICE = " database value", "human-readable or display name" + class ThingType(models.TextChoices): + WELL = "W", "Well" + SPRING = "S", "Spring" + thing_id = models.BigAutoField(primary_key=True) name = models.CharField(max_length=100, unique=True) + thing_type = models.CharField( + max_length=2, + choices=ThingType.choices, # Use the choices defined in the ThingType class. + default=ThingType.WELL, # Set a default value for the field. + verbose_name="type of thing" # Human-readable label for user interfaces like forms and admin panel. + ) release_status = models.BooleanField(default=False) date_created = models.DateTimeField(auto_now_add=True) # The 'location' field sets up the M:M relationship and specifies From 9f7e395373ef7834c277470859dd8e5ae6247704 Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Thu, 24 Jul 2025 16:25:51 -0600 Subject: [PATCH 35/57] refactor: Add fields specific to a WELL. Add fields specific to a SPRING. --- geodjango/samplelocations/models.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/geodjango/samplelocations/models.py b/geodjango/samplelocations/models.py index 6860383..6617328 100644 --- a/geodjango/samplelocations/models.py +++ b/geodjango/samplelocations/models.py @@ -64,6 +64,17 @@ class ThingType(models.TextChoices): verbose_name= "related location" # Human-readable label for user interfaces like forms and the admin panel. ) + #Fields specific to a WELL + well_depth_ft = models.FloatField(blank=True, null=True, help_text="well depth feet below ground surface") + hole_depth_ft = models.FloatField(blank=True, null=True, help_text="hole depth feet below ground surface") + casing_diameter_ft = models.FloatField(blank=True, null=True, help_text="casing diameter in ft") + casing_depth_ft = models.FloatField(blank=True, null=True, help_text="casing depth feet below ground surface") + casing_description = models.CharField(max_length=50, blank=True, null=True) + construction_notes = models.TextField(blank=True, null=True) # Use TextField over CharField for long-form text of variable length without a predefined limit. + + #Fields specific to a SPRING + spring_type = models.CharField(max_length=255, blank=True, null=True) # e.g. "artesian", "subartesian", "thermal", etc. + def __str__(self): return f"Thing object with id {self.thing_id} and name {self.name}" From 89c54677b6e0cb2dea4a75e7fe91ac6dc24e30c9 Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Thu, 24 Jul 2025 16:31:21 -0600 Subject: [PATCH 36/57] refactor: Update __str__ method for Thing to include type and name --- geodjango/samplelocations/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geodjango/samplelocations/models.py b/geodjango/samplelocations/models.py index 6617328..598dbfc 100644 --- a/geodjango/samplelocations/models.py +++ b/geodjango/samplelocations/models.py @@ -76,7 +76,7 @@ class ThingType(models.TextChoices): spring_type = models.CharField(max_length=255, blank=True, null=True) # e.g. "artesian", "subartesian", "thermal", etc. def __str__(self): - return f"Thing object with id {self.thing_id} and name {self.name}" + return f"Thing object is a {self.thing_type} with name {self.name}" class Meta: verbose_name = "Thing" From 88dd1ae595d21c5a15c910b7db694f53f653bc3a Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Thu, 24 Jul 2025 16:40:37 -0600 Subject: [PATCH 37/57] refactor: Remove WellThing model- it has been merged into Thing model. --- geodjango/samplelocations/models.py | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/geodjango/samplelocations/models.py b/geodjango/samplelocations/models.py index 598dbfc..8f7c9c9 100644 --- a/geodjango/samplelocations/models.py +++ b/geodjango/samplelocations/models.py @@ -74,6 +74,7 @@ class ThingType(models.TextChoices): #Fields specific to a SPRING spring_type = models.CharField(max_length=255, blank=True, null=True) # e.g. "artesian", "subartesian", "thermal", etc. + description_spring = models.CharField(max_length=255, blank=True, null=True) def __str__(self): return f"Thing object is a {self.thing_type} with name {self.name}" @@ -118,31 +119,6 @@ class Meta: db_table_comment = "Junction table linking Location and Thing models" -#--------WellThing model. Inherits all fields from Thing model ----------- - -class WellThing(Thing): - """ A specific type of monitoring station (Thing) representing a well.""" - wellthing_id = models.BigAutoField(primary_key=True) - # This field creates the inheritance link from WellThing back to Thing. - # The name 'thing_ptr' is a conventional naming choice in Django for the parent link field, - thing_ptr= models.OneToOneField( - Thing, - on_delete=models.CASCADE, - parent_link = True, - related_name='wellthings', - verbose_name="related thing" - ) - well_depth_ft = models.FloatField(blank=True, null=True, help_text="well depth feet below ground surface") - hole_depth_ft = models.FloatField(blank=True, null=True, help_text="hole depth feet below ground surface") - casing_diameter_ft = models.FloatField(blank=True, null=True, help_text="casing diameter in ft") - casing_depth_ft = models.FloatField(blank=True, null=True, help_text="casing depth feet below ground surface") - casing_description = models.CharField(max_length=50, blank=True, null=True) - construction_notes = models.TextField(blank=True, null=True) # Use TextField over CharField for long-form text of variable length without a predefined limit. - - def __str__(self): - return f"{self.name} (Well)" - - #--------SpringThing model. Inherits all fields from Thing model ----------- class SpringThing(Thing): From 235f28f7e14c8714c5b21a16836e3b24650971b6 Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Thu, 24 Jul 2025 16:45:05 -0600 Subject: [PATCH 38/57] refactor: Remove SpringThing model- it has been merged into Thing model. --- geodjango/samplelocations/models.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/geodjango/samplelocations/models.py b/geodjango/samplelocations/models.py index 8f7c9c9..530588d 100644 --- a/geodjango/samplelocations/models.py +++ b/geodjango/samplelocations/models.py @@ -119,26 +119,6 @@ class Meta: db_table_comment = "Junction table linking Location and Thing models" -#--------SpringThing model. Inherits all fields from Thing model ----------- - -class SpringThing(Thing): - """ A specific type of monitoring station (Thing) representing a spring.""" - springthing_id = models.BigAutoField(primary_key=True) - # This field creates the inheritance link from SpringThing back to Thing. - # The name 'thing_ptr' is a conventional naming choice in Django for the parent link field, - thing_ptr = models.OneToOneField( - Thing, - on_delete=models.CASCADE, - parent_link=True, - related_name='springthings', - verbose_name="related thing" - ) - description = models.CharField(max_length=255, blank=True, null=True) - - def __str__(self): - return f"{self.name} (Spring)" - - #--------Sensor model----------- #TODO: add a 'name' field to this model. class Sensor(models.Model): From 12b4c90a6a8337e4964864aec4fbd31e0bdbd080 Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Thu, 24 Jul 2025 16:49:05 -0600 Subject: [PATCH 39/57] refactor: De-register WellThing and SpringThing models --- geodjango/samplelocations/admin.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/geodjango/samplelocations/admin.py b/geodjango/samplelocations/admin.py index 4458283..7ad3bef 100644 --- a/geodjango/samplelocations/admin.py +++ b/geodjango/samplelocations/admin.py @@ -29,14 +29,6 @@ class LocationAdmin(GISModelAdmin): class ThingAdmin(ModelAdmin): pass -@admin.register(WellThing) -class WellThingAdmin(ModelAdmin): - pass - -@admin.register(SpringThing) -class SpringThingAdmin(ModelAdmin): - pass - @admin.register(Location_Thing_Junction) class LocationThingJunctionAdmin(ModelAdmin): pass @@ -61,6 +53,14 @@ class GroundwaterLevelObservationAdmin(ModelAdmin): class SampleAdmin(ModelAdmin): pass +# @admin.register(WellThing) +# class WellThingAdmin(ModelAdmin): +# pass + +# @admin.register(SpringThing) +# class SpringThingAdmin(ModelAdmin): +# pass + #admin.site.register(Lexicon) #admin.site.register(WellScreen) #admin.site.register(Equipment) From 3c44f2ab3dd4c73611d2838c5da59a040977d665 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Mon, 28 Jul 2025 14:25:17 -0600 Subject: [PATCH 40/57] feat: setup tests for things and locations --- geodjango/samplelocations/api/thing.py | 32 +++++++++++++++++++ .../samplelocations/tests/test_location.py | 5 +++ geodjango/samplelocations/tests/test_thing.py | 30 +++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 geodjango/samplelocations/api/thing.py diff --git a/geodjango/samplelocations/api/thing.py b/geodjango/samplelocations/api/thing.py new file mode 100644 index 0000000..f1ef494 --- /dev/null +++ b/geodjango/samplelocations/api/thing.py @@ -0,0 +1,32 @@ +from ninja import Router +from samplelocations.models import SampleLocation, Owner +from django.contrib.gis.geos import Point +from django.shortcuts import get_object_or_404 + +router = Router() + +@router.get('') +def get_things(request): + """ + List all things. + """ + things = Thing.objects.all() + return [ + { + 'id': thing.id, + 'name': thing.name, + 'release_status': thing.release_status, + 'date_created': thing.date_created.isoformat(), + } + for thing in things + ] + +@router.post("") +def post_thing(request, name: str, release_status: bool, owner_id: int): + owner = get_object_or_404(Owner, id=owner_id) + thing = Thing.objects.create( + name=name, + release_status=release_status, + owner=owner + ) + return {"id": thing.id, "name": thing.name} \ No newline at end of file diff --git a/geodjango/samplelocations/tests/test_location.py b/geodjango/samplelocations/tests/test_location.py index cf95525..9b42891 100644 --- a/geodjango/samplelocations/tests/test_location.py +++ b/geodjango/samplelocations/tests/test_location.py @@ -65,6 +65,11 @@ def tearDown(self): self.location.delete() return super().tearDown() + def test_list_locations(self): + """ + list al locations in the database as a feature collection + """ + def get_specific_location(self): response = self.client.get(f"/location/{self.location.id}") self.assertEqual(response.status_code, 200) diff --git a/geodjango/samplelocations/tests/test_thing.py b/geodjango/samplelocations/tests/test_thing.py index e69de29..0a88204 100644 --- a/geodjango/samplelocations/tests/test_thing.py +++ b/geodjango/samplelocations/tests/test_thing.py @@ -0,0 +1,30 @@ +from samplelocations.tests import BaseTestClass +from samplelocations.models import Thing + +class TestThing(BaseTestClass): + """ + Test cases for the Thing model. + """ + + def setUp(self): + super().setUp() + # Create a Thing instance for use in each test + self.thing = Thing.objects.create( + name="Test Thing", + description="A thing for testing", + ) + + def tearDown(self): + self.thing.delete() + return super().tearDown() + + def test_get_all_things(self): + """ + List all things in the database as a feature collection + """ + pass + + def test_get_thing_by_id(self): + """ + Retrieve a specific thing by its ID as a feature collection + """ \ No newline at end of file From 40291db867d6a661acd832145686da130231c3ae Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Mon, 28 Jul 2025 16:18:56 -0600 Subject: [PATCH 41/57] feat: implement thing feature collection --- geodjango/samplelocations/api/thing.py | 147 +++++++++++++++--- geodjango/samplelocations/tests/test_thing.py | 147 +++++++++++++++++- 2 files changed, 264 insertions(+), 30 deletions(-) diff --git a/geodjango/samplelocations/api/thing.py b/geodjango/samplelocations/api/thing.py index f1ef494..033aecf 100644 --- a/geodjango/samplelocations/api/thing.py +++ b/geodjango/samplelocations/api/thing.py @@ -1,32 +1,133 @@ -from ninja import Router -from samplelocations.models import SampleLocation, Owner +from geodjango.samplelocations.api import locations +from ninja import Router, Schema +from samplelocations.models import Thing, Location, Location_Thing_Junction from django.contrib.gis.geos import Point from django.shortcuts import get_object_or_404 +from typing import List router = Router() -@router.get('') -def get_things(request): + +class GeoJSONGeometry(BaseModel): """ - List all things. + Geometry schema for GeoJSON response. + """ + + type: str # e.g., "Point", "LineString", "Polygon" + coordinates: ( + List[float] | List[List[float]] | List[List[List[float]]] + ) # Supports Point, LineString, Polygon, etc. + +class Feature(Schema): + type: str = "Feature" + geometry: GeoJSONGeometry + +class BaseProperties(Schema): + thing_id: int + name: str + thing_type: str + release_status: bool + date_created: str + description: str | None = None + + +class WellProperties(BaseProperties): + well_depth_ft: float | None = None + hole_depth_ft: float | None = None + casing_diameter_ft: float | None = None + casing_depth_ft: float | None = None + casing_description: str | None = None + construction_notes: str | None = None + +class SpringProperties(BaseProperties): + spring_type: str | None = None + +class WellFeature(Feature): + properties: WellProperties + +class SpringFeature(Feature): + properties: SpringProperties + +class FeatureCollection(Schema): + type: str = "FeatureCollection" + features: List[WellFeature | SpringFeature] = [] + + +def get_things(thing_id: int | None = None) -> List[Thing]: + """ + Retrieve all things or a specific thing by ID. + """ + if thing_id is None: + return Thing.objects.all() + else: + # If a specific thing ID is provided, return that thing: + return [Thing.objects.filter(thing_id__in=[thing_id]).first()] + +def construct_feature_collection(things: List[Thing]) -> FeatureCollection: + """ + Construct a GeoJSON FeatureCollection from a list of Thing objects. """ - things = Thing.objects.all() - return [ - { - 'id': thing.id, - 'name': thing.name, - 'release_status': thing.release_status, - 'date_created': thing.date_created.isoformat(), + location_thing_junctions = Location_Thing_Junction.objects.filter(thing_id__in=things.values_list('thing_id', flat=True)) + locations = Location.objects.filter(location_id__in=location_thing_junctions.values_list('location_id', flat=True)) + + features = [] + for thing in things: + if thing.thing_type == "well": + thing_properties = WellProperties(**thing) + elif thing.thing_type == "spring": + thing_properties = SpringProperties(**thing) + + thing_id = thing.thing_id + location_ids = [junction.location_id for junction in location_thing_junctions if junction.thing_id == thing_id] + locations = [loc for loc in locations if loc.location_id in location_ids] + + # assuming, for now, that each thing has a single location + location = locations[0] + + feature = { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [location.coordinate.x, location.coordinate.y, location.coordinate.z], + }, + "properties": thing_properties.dict(), } - for thing in things - ] - -@router.post("") -def post_thing(request, name: str, release_status: bool, owner_id: int): - owner = get_object_or_404(Owner, id=owner_id) - thing = Thing.objects.create( - name=name, - release_status=release_status, - owner=owner + + features.append(feature) + + response = FeatureCollection( + type="FeatureCollection", + features=features ) - return {"id": thing.id, "name": thing.name} \ No newline at end of file + + return response + +@router.get('') +def get_all_things(request): + """ + List all things. + """ + """ + Jacob's notes during development: 2025-07-28 + A disadvantage of Django ORM is that you can't make more complicated queries. + From what I understand, you can use Django ORM to get all the things and their related locations, + but you can't easily filter or join them in a single query. The "joining" has to be done in + Python code after fetching the data, which can be less efficient because it's slower than + SQL and you have to make more SQL queries to get the related data. + + Also, the filtering is kind of confusing... + """ + things = get_things() + response = construct_feature_collection(things) + return response + +@router.get('/{thing_id}') +def get_thing_by_id(request, thing_id: int): + """ + Retrieve a specific thing by its ID. + """ + thing = get_things(thing_id=thing_id) + if thing == []: + return {"detail": f"Thing with id {thing_id} not found"}, 404 + response = construct_feature_collection(thing) + return response \ No newline at end of file diff --git a/geodjango/samplelocations/tests/test_thing.py b/geodjango/samplelocations/tests/test_thing.py index 0a88204..a553eae 100644 --- a/geodjango/samplelocations/tests/test_thing.py +++ b/geodjango/samplelocations/tests/test_thing.py @@ -1,5 +1,7 @@ from samplelocations.tests import BaseTestClass from samplelocations.models import Thing +from samplelocations.models import Location, Location_Thing_Junction + class TestThing(BaseTestClass): """ @@ -8,23 +10,154 @@ class TestThing(BaseTestClass): def setUp(self): super().setUp() - # Create a Thing instance for use in each test - self.thing = Thing.objects.create( - name="Test Thing", - description="A thing for testing", + + # Create Location records + self.location1 = Location.objects.create( + name="Test Location 1", + coordinate="POINT(10.0 10.0 100.0)", + date_created="2023-10-01T00:00:00Z", + ) + self.location2 = Location.objects.create( + name="Test Location 2", + coordinate="POINT(20.0 20.0 200.0)", + date_created="2023-10-01T00:00:00Z", + ) + + # Create Thing records + self.well_thing = Thing.objects.create( + name="Test Well", + description="A well for testing", + thing_type="well", + release_status=True, + date_created="2023-10-01T00:00:00Z", + location_id=self.location1.location_id, + well_depth_ft=100.0, + hole_depth_ft=120.0, + casing_diameter_ft=10.0, + casing_depth_ft=80.0, + casing_description="PVC", + construction_notes="Test well construction notes", + ) + self.spring_thing = Thing.objects.create( + name="Test Spring", + description="A spring for testing", + thing_type="spring", + release_status=True, + date_created="2023-10-01T00:00:00Z", + location_id=self.location2.location_id, + spring_type="thermal", + ) + + # Create Location_Thing_Junction records + self.junction1 = Location_Thing_Junction.objects.create( + location_id=self.location1.location_id, + thing_id=self.well_thing.thing_id + ) + self.junction2 = Location_Thing_Junction.objects.create( + location_id=self.location2.location_id, + thing_id=self.spring_thing.thing_id ) def tearDown(self): - self.thing.delete() + self.junction1.delete() + self.junction2.delete() + self.well_thing.delete() + self.spring_thing.delete() + self.location1.delete() + self.location2.delete() return super().tearDown() def test_get_all_things(self): """ List all things in the database as a feature collection """ - pass + response = self.client.get("/thing") + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.data["features"]), 2) + data = response.json() + assert data == { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [10.0, 10.0, 100.0] + }, + "properties": { + "thing_id": self.well_thing.thing_id, + "name": self.well_thing.name, + "description": self.well_thing.description, + "thing_type": self.well_thing.thing_type, + "release_status": self.well_thing.release_status, + "date_created": self.well_thing.date_created, + "well_depth_ft": self.well_thing.well_depth_ft, + "hole_depth_ft": self.well_thing.hole_depth_ft, + "casing_diameter_ft": self.well_thing.casing_diameter_ft, + "casing_depth_ft": self.well_thing.casing_depth_ft, + "casing_description": self.well_thing.casing_description, + "construction_notes": self.well_thing.construction_notes, + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [20.0, 20.0, 200.0] + }, + "properties": { + "thing_id": self.spring_thing.thing_id, + "name": self.spring_thing.name, + "description": self.spring_thing.description, + "thing_type": self.spring_thing.thing_type, + "release_status": self.spring_thing.release_status, + "date_created": self.spring_thing.date_created, + "spring_type": self.spring_thing.spring_type, + } + } + ] + } def test_get_thing_by_id(self): """ Retrieve a specific thing by its ID as a feature collection - """ \ No newline at end of file + """ + response = self.client.get("/thing/1") + self.assertEqual(response.status_code, 200) + data = response.json() + assert data == { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [10.0, 10.0, 100.0] + }, + "properties": { + "thing_id": self.well_thing.thing_id, + "name": self.well_thing.name, + "description": self.well_thing.description, + "thing_type": self.well_thing.thing_type, + "release_status": self.well_thing.release_status, + "date_created": self.well_thing.date_created, + "well_depth_ft": self.well_thing.well_depth_ft, + "hole_depth_ft": self.well_thing.hole_depth_ft, + "casing_diameter_ft": self.well_thing.casing_diameter_ft, + "casing_depth_ft": self.well_thing.casing_depth_ft, + "casing_description": self.well_thing.casing_description, + "construction_notes": self.well_thing.construction_notes, + } + }, + ] + } + + def test_404_not_found(self): + """ + Test that a 404 is returned for a non-existent thing ID + """ + response = self.client.get("/thing/9999") + self.assertEqual(response.status_code, 404) + data = response.json() + self.assertEqual(data["detail"], "Thing with id 9999 not found") + self.assertIn("error", data) \ No newline at end of file From 25d2cc36ccf2c861d1963d0066f1485576132cf5 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Mon, 28 Jul 2025 17:00:09 -0600 Subject: [PATCH 42/57] fix: remove depricated models --- geodjango/geodjango/api.py | 7 +- geodjango/samplelocations/admin.py | 14 ++-- geodjango/samplelocations/api/contacts.py | 40 ++++----- geodjango/samplelocations/api/locations.py | 70 ++++++++-------- geodjango/samplelocations/api/wells.py | 98 +++++++++++----------- 5 files changed, 115 insertions(+), 114 deletions(-) diff --git a/geodjango/geodjango/api.py b/geodjango/geodjango/api.py index ec1498e..e6efa48 100644 --- a/geodjango/geodjango/api.py +++ b/geodjango/geodjango/api.py @@ -2,6 +2,7 @@ api = NinjaAPI() -api.add_router('/locations', 'samplelocations.api.locations.router', tags=['locations']) -api.add_router('/wells', 'samplelocations.api.wells.router', tags=['wells']) -api.add_router('/contacts', 'samplelocations.api.contacts.router', tags=['contacts']) \ No newline at end of file +# api.add_router('/locations', 'samplelocations.api.locations.router', tags=['locations']) +# api.add_router('/wells', 'samplelocations.api.wells.router', tags=['wells']) +# api.add_router('/contacts', 'samplelocations.api.contacts.router', tags=['contacts']) +api.add_router('/things', 'samplelocations.api.thing.router', tags=['things']) \ No newline at end of file diff --git a/geodjango/samplelocations/admin.py b/geodjango/samplelocations/admin.py index 4458283..9545b53 100644 --- a/geodjango/samplelocations/admin.py +++ b/geodjango/samplelocations/admin.py @@ -3,7 +3,7 @@ from django import forms from django.contrib.gis.admin import GISModelAdmin from django.contrib.gis.geos import Point -from samplelocations.models import Location, Thing, WellThing, SpringThing, Location_Thing_Junction, Sensor, Datastream, Observation, \ +from samplelocations.models import Location, Thing, Location_Thing_Junction, Sensor, Datastream, Observation, \ GroundwaterLevelObservation, Sample class LocationForm(forms.ModelForm): @@ -29,13 +29,13 @@ class LocationAdmin(GISModelAdmin): class ThingAdmin(ModelAdmin): pass -@admin.register(WellThing) -class WellThingAdmin(ModelAdmin): - pass +# @admin.register(WellThing) +# class WellThingAdmin(ModelAdmin): +# pass -@admin.register(SpringThing) -class SpringThingAdmin(ModelAdmin): - pass +# @admin.register(SpringThing) +# class SpringThingAdmin(ModelAdmin): +# pass @admin.register(Location_Thing_Junction) class LocationThingJunctionAdmin(ModelAdmin): diff --git a/geodjango/samplelocations/api/contacts.py b/geodjango/samplelocations/api/contacts.py index 40ca3cf..d1a435f 100644 --- a/geodjango/samplelocations/api/contacts.py +++ b/geodjango/samplelocations/api/contacts.py @@ -1,22 +1,22 @@ -from ninja import Router -from ..models import Well, Contact -from django.shortcuts import get_object_or_404 +# from ninja import Router +# from ..models import Well, Contact +# from django.shortcuts import get_object_or_404 -router = Router() +# router = Router() -@router.post("") -def post_contact( - request, - well_id: int, - name: str, - email: str, - phone: str = None -): - well = get_object_or_404(Well, id=well_id) - contact = Contact.objects.create(name=name, email=email, phone=phone) - # Create or update owner for the location if needed - location = well.location - owner = location.owner - owner.contact = contact - owner.save() - return {"contact_id": contact.id, "owner_id": owner.id} \ No newline at end of file +# @router.post("") +# def post_contact( +# request, +# well_id: int, +# name: str, +# email: str, +# phone: str = None +# ): +# well = get_object_or_404(Well, id=well_id) +# contact = Contact.objects.create(name=name, email=email, phone=phone) +# # Create or update owner for the location if needed +# location = well.location +# owner = location.owner +# owner.contact = contact +# owner.save() +# return {"contact_id": contact.id, "owner_id": owner.id} \ No newline at end of file diff --git a/geodjango/samplelocations/api/locations.py b/geodjango/samplelocations/api/locations.py index bba834c..766528e 100644 --- a/geodjango/samplelocations/api/locations.py +++ b/geodjango/samplelocations/api/locations.py @@ -1,38 +1,38 @@ -from ninja import Router -from ..models import SampleLocation, Owner -from django.contrib.gis.geos import Point -from django.shortcuts import get_object_or_404 +# from ninja import Router +# from samplelocations.models import Location, Owner +# from django.contrib.gis.geos import Point +# from django.shortcuts import get_object_or_404 -router = Router() +# router = Router() -@router.get('') -def get_locations(request): - """ - List all sample locations. - """ - locations = SampleLocation.objects.all() - return [ - { - 'id': loc.id, - 'name': loc.name, - 'lat': loc.point.y, - 'lon': loc.point.x, - 'description': loc.description, - 'visible': loc.visible, - 'date_created': loc.date_created.isoformat(), - } - for loc in locations - ] +# @router.get('') +# def get_locations(request): +# """ +# List all sample locations. +# """ +# locations = SampleLocation.objects.all() +# return [ +# { +# 'id': loc.id, +# 'name': loc.name, +# 'lat': loc.point.y, +# 'lon': loc.point.x, +# 'description': loc.description, +# 'visible': loc.visible, +# 'date_created': loc.date_created.isoformat(), +# } +# for loc in locations +# ] -@router.post("") -def post_location(request, name: str, lat: float, lon: float, owner_id: int, description: str = None, visible: bool = False): - owner = get_object_or_404(Owner, id=owner_id) - point = Point(lon, lat) - location = SampleLocation.objects.create( - name=name, - description=description, - visible=visible, - point=point, - owner=owner - ) - return {"id": location.id, "name": location.name} \ No newline at end of file +# @router.post("") +# def post_location(request, name: str, lat: float, lon: float, owner_id: int, description: str = None, visible: bool = False): +# owner = get_object_or_404(Owner, id=owner_id) +# point = Point(lon, lat) +# location = SampleLocation.objects.create( +# name=name, +# description=description, +# visible=visible, +# point=point, +# owner=owner +# ) +# return {"id": location.id, "name": location.name} \ No newline at end of file diff --git a/geodjango/samplelocations/api/wells.py b/geodjango/samplelocations/api/wells.py index 09457fc..a4dd44a 100644 --- a/geodjango/samplelocations/api/wells.py +++ b/geodjango/samplelocations/api/wells.py @@ -1,55 +1,55 @@ -from ninja import Router -from ..models import SampleLocation, Well, WellScreen, Lexicon -from django.shortcuts import get_object_or_404 +# from ninja import Router +# from ..models import Location, Well, WellScreen, Lexicon +# from django.shortcuts import get_object_or_404 -router = Router() +# router = Router() -@router.post("") -def post_well( - request, - location_id: int, - ose_pod_id: str = None, - api_id: str = "", - usgs_id: str = None, - well_depth: float = None, - hole_depth: float = None, - well_type_id: int = None, - casing_diameter: float = None, - casing_depth: float = None, - casing_description: str = None, - construction_notes: str = None, - formation_zone_id: int = None -): - location = get_object_or_404(SampleLocation, id=location_id) - well_type = Lexicon.objects.filter(id=well_type_id).first() if well_type_id else None - formation_zone = Lexicon.objects.filter(id=formation_zone_id).first() if formation_zone_id else None - well = Well.objects.create( - location=location, - ose_pod_id=ose_pod_id, - api_id=api_id, - usgs_id=usgs_id, - well_depth=well_depth, - hole_depth=hole_depth, - well_type=well_type, - casing_diameter=casing_diameter, - casing_depth=casing_depth, - casing_description=casing_description, - construction_notes=construction_notes, - formation_zone=formation_zone - ) - return {"id": well.id, "location": well.location.id} +# @router.post("") +# def post_well( +# request, +# location_id: int, +# ose_pod_id: str = None, +# api_id: str = "", +# usgs_id: str = None, +# well_depth: float = None, +# hole_depth: float = None, +# well_type_id: int = None, +# casing_diameter: float = None, +# casing_depth: float = None, +# casing_description: str = None, +# construction_notes: str = None, +# formation_zone_id: int = None +# ): +# location = get_object_or_404(Location, id=location_id) +# well_type = Lexicon.objects.filter(id=well_type_id).first() if well_type_id else None +# formation_zone = Lexicon.objects.filter(id=formation_zone_id).first() if formation_zone_id else None +# well = Well.objects.create( +# location=location, +# ose_pod_id=ose_pod_id, +# api_id=api_id, +# usgs_id=usgs_id, +# well_depth=well_depth, +# hole_depth=hole_depth, +# well_type=well_type, +# casing_diameter=casing_diameter, +# casing_depth=casing_depth, +# casing_description=casing_description, +# construction_notes=construction_notes, +# formation_zone=formation_zone +# ) +# return {"id": well.id, "location": well.location.id} -@router.post("well-screens/") -def post_well_screen(request, well_id: int, screen_depth_top: float, screen_depth_bottom: float, screen_type_id: int = None): - well = get_object_or_404(Well, id=well_id) - screen_type = Lexicon.objects.filter(id=screen_type_id).first() if screen_type_id else None - screen = WellScreen.objects.create( - well=well, - screen_depth_top=screen_depth_top, - screen_depth_bottom=screen_depth_bottom, - screen_type=screen_type - ) - return {"id": screen.id, "well": screen.well.id} +# @router.post("well-screens/") +# def post_well_screen(request, well_id: int, screen_depth_top: float, screen_depth_bottom: float, screen_type_id: int = None): +# well = get_object_or_404(Well, id=well_id) +# screen_type = Lexicon.objects.filter(id=screen_type_id).first() if screen_type_id else None +# screen = WellScreen.objects.create( +# well=well, +# screen_depth_top=screen_depth_top, +# screen_depth_bottom=screen_depth_bottom, +# screen_type=screen_type +# ) +# return {"id": screen.id, "well": screen.well.id} From 26b17abadb032d77d426c2058f3f8e62578846cf Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Mon, 28 Jul 2025 17:00:19 -0600 Subject: [PATCH 43/57] setup thing endpoint and tests --- geodjango/samplelocations/api/thing.py | 3 +-- geodjango/samplelocations/tests/test_thing.py | 9 +++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/geodjango/samplelocations/api/thing.py b/geodjango/samplelocations/api/thing.py index 033aecf..1cefa75 100644 --- a/geodjango/samplelocations/api/thing.py +++ b/geodjango/samplelocations/api/thing.py @@ -1,4 +1,3 @@ -from geodjango.samplelocations.api import locations from ninja import Router, Schema from samplelocations.models import Thing, Location, Location_Thing_Junction from django.contrib.gis.geos import Point @@ -8,7 +7,7 @@ router = Router() -class GeoJSONGeometry(BaseModel): +class GeoJSONGeometry(Schema): """ Geometry schema for GeoJSON response. """ diff --git a/geodjango/samplelocations/tests/test_thing.py b/geodjango/samplelocations/tests/test_thing.py index a553eae..e74d4c1 100644 --- a/geodjango/samplelocations/tests/test_thing.py +++ b/geodjango/samplelocations/tests/test_thing.py @@ -13,12 +13,10 @@ def setUp(self): # Create Location records self.location1 = Location.objects.create( - name="Test Location 1", coordinate="POINT(10.0 10.0 100.0)", date_created="2023-10-01T00:00:00Z", ) self.location2 = Location.objects.create( - name="Test Location 2", coordinate="POINT(20.0 20.0 200.0)", date_created="2023-10-01T00:00:00Z", ) @@ -30,7 +28,7 @@ def setUp(self): thing_type="well", release_status=True, date_created="2023-10-01T00:00:00Z", - location_id=self.location1.location_id, + # location_id=self.location1.location_id, well_depth_ft=100.0, hole_depth_ft=120.0, casing_diameter_ft=10.0, @@ -44,10 +42,13 @@ def setUp(self): thing_type="spring", release_status=True, date_created="2023-10-01T00:00:00Z", - location_id=self.location2.location_id, spring_type="thermal", ) + # Assign locations using the ManyToManyField + self.well_thing.location_id.set([self.location1]) + self.spring_thing.location_id.set([self.location2]) + # Create Location_Thing_Junction records self.junction1 = Location_Thing_Junction.objects.create( location_id=self.location1.location_id, From a76e653f72acb85695732e03b61c3a3cfbd0c22d Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Mon, 28 Jul 2025 17:26:52 -0600 Subject: [PATCH 44/57] fix: fix thing test endpoints --- geodjango/geodjango/api.py | 2 +- geodjango/samplelocations/models.py | 2 +- geodjango/samplelocations/tests/test_thing.py | 31 +++++++++++-------- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/geodjango/geodjango/api.py b/geodjango/geodjango/api.py index e6efa48..e427088 100644 --- a/geodjango/geodjango/api.py +++ b/geodjango/geodjango/api.py @@ -5,4 +5,4 @@ # api.add_router('/locations', 'samplelocations.api.locations.router', tags=['locations']) # api.add_router('/wells', 'samplelocations.api.wells.router', tags=['wells']) # api.add_router('/contacts', 'samplelocations.api.contacts.router', tags=['contacts']) -api.add_router('/things', 'samplelocations.api.thing.router', tags=['things']) \ No newline at end of file +api.add_router('/thing', 'samplelocations.api.thing.router', tags=['things']) \ No newline at end of file diff --git a/geodjango/samplelocations/models.py b/geodjango/samplelocations/models.py index 530588d..be7c715 100644 --- a/geodjango/samplelocations/models.py +++ b/geodjango/samplelocations/models.py @@ -55,6 +55,7 @@ class ThingType(models.TextChoices): ) release_status = models.BooleanField(default=False) date_created = models.DateTimeField(auto_now_add=True) + description = models.TextField(blank=True, null=True, help_text="A description of the thing") # The 'location' field sets up the M:M relationship and specifies # the 'Locoation_Thing_Juncation' as the intermediate table. location_id = models.ManyToManyField( @@ -74,7 +75,6 @@ class ThingType(models.TextChoices): #Fields specific to a SPRING spring_type = models.CharField(max_length=255, blank=True, null=True) # e.g. "artesian", "subartesian", "thermal", etc. - description_spring = models.CharField(max_length=255, blank=True, null=True) def __str__(self): return f"Thing object is a {self.thing_type} with name {self.name}" diff --git a/geodjango/samplelocations/tests/test_thing.py b/geodjango/samplelocations/tests/test_thing.py index e74d4c1..d12cf2f 100644 --- a/geodjango/samplelocations/tests/test_thing.py +++ b/geodjango/samplelocations/tests/test_thing.py @@ -15,6 +15,7 @@ def setUp(self): self.location1 = Location.objects.create( coordinate="POINT(10.0 10.0 100.0)", date_created="2023-10-01T00:00:00Z", + ) self.location2 = Location.objects.create( coordinate="POINT(20.0 20.0 200.0)", @@ -25,7 +26,7 @@ def setUp(self): self.well_thing = Thing.objects.create( name="Test Well", description="A well for testing", - thing_type="well", + thing_type="W", release_status=True, date_created="2023-10-01T00:00:00Z", # location_id=self.location1.location_id, @@ -39,26 +40,30 @@ def setUp(self): self.spring_thing = Thing.objects.create( name="Test Spring", description="A spring for testing", - thing_type="spring", + thing_type="S", release_status=True, date_created="2023-10-01T00:00:00Z", spring_type="thermal", ) - # Assign locations using the ManyToManyField - self.well_thing.location_id.set([self.location1]) - self.spring_thing.location_id.set([self.location2]) - # Create Location_Thing_Junction records self.junction1 = Location_Thing_Junction.objects.create( - location_id=self.location1.location_id, - thing_id=self.well_thing.thing_id + location_id=self.location1, + thing_id=self.well_thing, + effective_start="2023-10-01T00:00:00Z", + effective_end="2040-01-01T00:00:00Z", # Assuming a future end date for the test ) self.junction2 = Location_Thing_Junction.objects.create( - location_id=self.location2.location_id, - thing_id=self.spring_thing.thing_id + location_id=self.location2, + thing_id=self.spring_thing, + effective_start="2023-10-01T00:00:00Z", + effective_end="2040-01-01T00:00:00Z", # Assuming a future end date for the test ) + # Assign locations using the ManyToManyField + self.well_thing.location_id.set([self.location1]) + self.spring_thing.location_id.set([self.location2]) + def tearDown(self): self.junction1.delete() self.junction2.delete() @@ -72,7 +77,7 @@ def test_get_all_things(self): """ List all things in the database as a feature collection """ - response = self.client.get("/thing") + response = self.client.get("/api/thing") self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data["features"]), 2) data = response.json() @@ -123,7 +128,7 @@ def test_get_thing_by_id(self): """ Retrieve a specific thing by its ID as a feature collection """ - response = self.client.get("/thing/1") + response = self.client.get("/api/thing/1") self.assertEqual(response.status_code, 200) data = response.json() assert data == { @@ -157,7 +162,7 @@ def test_404_not_found(self): """ Test that a 404 is returned for a non-existent thing ID """ - response = self.client.get("/thing/9999") + response = self.client.get("/api/thing/9999") self.assertEqual(response.status_code, 404) data = response.json() self.assertEqual(data["detail"], "Thing with id 9999 not found") From b302f33a01aa92dc8b5468c2584f631a246c97a2 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Mon, 28 Jul 2025 17:27:17 -0600 Subject: [PATCH 45/57] fix: reset migrations for new models --- .../migrations/0001_initial.py | 162 ++++++------- ...n_location_sample_sensor_thing_and_more.py | 213 ------------------ ...junction_unique_location_thing_and_more.py | 87 ------- .../migrations/0004_sensor_name.py | 18 -- 4 files changed, 86 insertions(+), 394 deletions(-) delete mode 100644 geodjango/samplelocations/migrations/0002_datastream_observation_location_sample_sensor_thing_and_more.py delete mode 100644 geodjango/samplelocations/migrations/0003_remove_location_thing_junction_unique_location_thing_and_more.py delete mode 100644 geodjango/samplelocations/migrations/0004_sensor_name.py diff --git a/geodjango/samplelocations/migrations/0001_initial.py b/geodjango/samplelocations/migrations/0001_initial.py index 4e9cd5f..ea9c635 100644 --- a/geodjango/samplelocations/migrations/0001_initial.py +++ b/geodjango/samplelocations/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.3 on 2025-07-10 21:55 +# Generated by Django 5.2.3 on 2025-07-28 23:09 import django.contrib.gis.db.models.fields import django.db.models.deletion @@ -14,114 +14,124 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Contact', + name='Datastream', fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100)), - ('email', models.EmailField(max_length=254)), - ('phone', models.CharField(blank=True, max_length=20, null=True)), - ('date_created', models.DateTimeField(auto_now_add=True)), + ('datastream_id', models.BigAutoField(primary_key=True, serialize=False)), + ('observed_property', models.CharField(max_length=100)), + ('release_status', models.BooleanField(default=False)), ], - options={ - 'verbose_name': 'Contact', - 'verbose_name_plural': 'Contacts', - 'ordering': ['name'], - }, ), migrations.CreateModel( - name='Lexicon', + name='Observation', fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100, unique=True)), - ('description', models.CharField(blank=True, max_length=255, null=True)), - ('date_created', models.DateTimeField(auto_now_add=True)), + ('observation_id', models.BigAutoField(primary_key=True, serialize=False)), + ('observed_value', models.FloatField(help_text='The value of the observation')), + ('release_status', models.BooleanField(default=False)), + ('datastream_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='observations', to='samplelocations.datastream', verbose_name='related datastream')), ], - options={ - 'verbose_name': 'Lexicon', - 'verbose_name_plural': 'Lexicons', - 'ordering': ['name'], - }, ), migrations.CreateModel( - name='Owner', + name='Location', fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100)), - ('description', models.CharField(blank=True, max_length=255, null=True)), + ('location_id', models.BigAutoField(primary_key=True, serialize=False)), + ('coordinate', django.contrib.gis.db.models.fields.PointField(dim=3, srid=4326)), ('date_created', models.DateTimeField(auto_now_add=True)), - ('contact', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='owners', to='samplelocations.contact')), ], options={ - 'verbose_name': 'Owner', - 'verbose_name_plural': 'Owners', - 'ordering': ['name'], + 'verbose_name': 'Location', + 'verbose_name_plural': 'Locations', + 'db_table_comment': "This table stores point locations on the earth's surface", }, ), migrations.CreateModel( - name='SampleLocation', + name='Sample', fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100)), - ('description', models.CharField(blank=True, max_length=255, null=True)), - ('visible', models.BooleanField(default=False)), - ('point', django.contrib.gis.db.models.fields.PointField(srid=4326)), - ('date_created', models.DateTimeField(auto_now_add=True)), - ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='samplelocations', to='samplelocations.owner')), + ('sample_id', models.BigAutoField(primary_key=True, serialize=False)), + ('sample_date', models.DateTimeField()), + ('sample_notes', models.TextField(blank=True, null=True)), ], - options={ - 'verbose_name': 'Sample Location', - 'verbose_name_plural': 'Sample Locations', - 'ordering': ['name'], - }, ), migrations.CreateModel( - name='Equipment', + name='Sensor', fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('equipment_type', models.CharField(max_length=50)), - ('model', models.CharField(max_length=50)), - ('serial_no', models.CharField(max_length=50)), - ('date_installed', models.DateTimeField(blank=True, null=True)), - ('date_removed', models.DateTimeField(blank=True, null=True)), - ('recording_interval', models.IntegerField(blank=True, null=True)), - ('equipment_notes', models.CharField(blank=True, max_length=50, null=True)), - ('location', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='equipment', to='samplelocations.samplelocation')), + ('sensor_id', models.BigAutoField(primary_key=True, serialize=False)), + ('name', models.CharField(blank=True, max_length=100, null=True)), + ('serial_number', models.CharField(blank=True, max_length=50, null=True)), + ('install_date', models.DateTimeField(blank=True, null=True)), + ('model', models.CharField(blank=True, max_length=50, null=True)), + ('notes', models.TextField(blank=True, null=True)), ], ), migrations.CreateModel( - name='Spring', + name='GroundwaterLevelObservation', fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('description', models.CharField(blank=True, max_length=255, null=True)), - ('location', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='springs', to='samplelocations.samplelocation')), + ('groundwater_level_observation_id', models.BigAutoField(primary_key=True, serialize=False)), + ('observation_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, related_name='groundwater_level_observations', to='samplelocations.observation', verbose_name='related observation')), ], + bases=('samplelocations.observation',), ), migrations.CreateModel( - name='Well', + name='Location_Thing_Junction', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('ose_pod_id', models.CharField(blank=True, max_length=50, null=True)), - ('api_id', models.CharField(blank=True, default='', max_length=50)), - ('usgs_id', models.CharField(blank=True, max_length=50, null=True)), - ('well_depth', models.FloatField(blank=True, help_text='feet below ground surface', null=True)), - ('hole_depth', models.FloatField(blank=True, help_text='feet below ground surface', null=True)), - ('casing_diameter', models.FloatField(blank=True, help_text='inches', null=True)), - ('casing_depth', models.FloatField(blank=True, help_text='feet below ground surface', null=True)), - ('casing_description', models.CharField(blank=True, max_length=50, null=True)), - ('construction_notes', models.CharField(blank=True, max_length=250, null=True)), - ('formation_zone', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='wells_by_formation', to='samplelocations.lexicon')), - ('location', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='wells', to='samplelocations.samplelocation')), - ('well_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='wells_by_type', to='samplelocations.lexicon')), + ('effective_start', models.DateTimeField()), + ('effective_end', models.DateTimeField()), + ('location_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='locations_things', to='samplelocations.location', verbose_name='related location')), ], + options={ + 'db_table_comment': 'Junction table linking Location and Thing models', + }, + ), + migrations.AddField( + model_name='observation', + name='sample', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='observations', to='samplelocations.sample', verbose_name='related sample'), + ), + migrations.AddField( + model_name='datastream', + name='sensor_id', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='datastreams', to='samplelocations.sensor', verbose_name='related sensor'), ), migrations.CreateModel( - name='WellScreen', + name='Thing', fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('screen_depth_top', models.FloatField(help_text='feet below ground surface')), - ('screen_depth_bottom', models.FloatField(help_text='feet below ground surface')), - ('screen_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='well_screens_by_type', to='samplelocations.lexicon')), - ('well', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='screens', to='samplelocations.well')), + ('thing_id', models.BigAutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=100, unique=True)), + ('thing_type', models.CharField(choices=[('W', 'Well'), ('S', 'Spring')], default='W', max_length=2, verbose_name='type of thing')), + ('release_status', models.BooleanField(default=False)), + ('date_created', models.DateTimeField(auto_now_add=True)), + ('description', models.TextField(blank=True, help_text='A description of the thing', null=True)), + ('well_depth_ft', models.FloatField(blank=True, help_text='well depth feet below ground surface', null=True)), + ('hole_depth_ft', models.FloatField(blank=True, help_text='hole depth feet below ground surface', null=True)), + ('casing_diameter_ft', models.FloatField(blank=True, help_text='casing diameter in ft', null=True)), + ('casing_depth_ft', models.FloatField(blank=True, help_text='casing depth feet below ground surface', null=True)), + ('casing_description', models.CharField(blank=True, max_length=50, null=True)), + ('construction_notes', models.TextField(blank=True, null=True)), + ('spring_type', models.CharField(blank=True, max_length=255, null=True)), + ('location_id', models.ManyToManyField(related_name='things', through='samplelocations.Location_Thing_Junction', to='samplelocations.location', verbose_name='related location')), ], + options={ + 'verbose_name': 'Thing', + 'verbose_name_plural': 'Things', + }, + ), + migrations.AddField( + model_name='sample', + name='thing_id', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='samples', to='samplelocations.thing', verbose_name='related thing'), + ), + migrations.AddField( + model_name='location_thing_junction', + name='thing_id', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='locations_things', to='samplelocations.thing', verbose_name='related thing'), + ), + migrations.AddField( + model_name='datastream', + name='thing_id', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='datastreams', to='samplelocations.thing', verbose_name='related thing'), + ), + migrations.AddConstraint( + model_name='location_thing_junction', + constraint=models.UniqueConstraint(fields=('location_id', 'thing_id'), name='unique_location_thing'), ), ] diff --git a/geodjango/samplelocations/migrations/0002_datastream_observation_location_sample_sensor_thing_and_more.py b/geodjango/samplelocations/migrations/0002_datastream_observation_location_sample_sensor_thing_and_more.py deleted file mode 100644 index 1c34efa..0000000 --- a/geodjango/samplelocations/migrations/0002_datastream_observation_location_sample_sensor_thing_and_more.py +++ /dev/null @@ -1,213 +0,0 @@ -# Generated by Django 5.2.3 on 2025-07-11 16:53 - -import django.contrib.gis.db.models.fields -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('samplelocations', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='Datastream', - fields=[ - ('datastream_id', models.BigAutoField(primary_key=True, serialize=False)), - ('observed_property', models.CharField(max_length=100)), - ('release_status', models.BooleanField(default=False)), - ], - ), - migrations.CreateModel( - name='Observation', - fields=[ - ('observation_id', models.BigAutoField(primary_key=True, serialize=False)), - ('observed_value', models.FloatField(help_text='The value of the observation')), - ('release_status', models.BooleanField(default=False)), - ('datastream', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='observations', to='samplelocations.datastream', verbose_name='related datastream')), - ], - ), - migrations.CreateModel( - name='Location', - fields=[ - ('location_id', models.BigAutoField(primary_key=True, serialize=False)), - ('coordinate', django.contrib.gis.db.models.fields.PointField(dim=3, srid=4326)), - ('date_created', models.DateTimeField(auto_now_add=True)), - ], - options={ - 'verbose_name': 'Location', - 'verbose_name_plural': 'Locations', - 'db_table_comment': "This table stores point locations on the earth's surface", - }, - ), - migrations.CreateModel( - name='Sample', - fields=[ - ('sample_id', models.BigAutoField(primary_key=True, serialize=False)), - ('sample_date', models.DateTimeField()), - ('sample_notes', models.TextField(blank=True, null=True)), - ], - ), - migrations.CreateModel( - name='Sensor', - fields=[ - ('sensor_id', models.BigAutoField(primary_key=True, serialize=False)), - ('serial_number', models.CharField(blank=True, max_length=50, null=True)), - ('install_date', models.DateTimeField(blank=True, null=True)), - ('model', models.CharField(blank=True, max_length=50, null=True)), - ('notes', models.TextField(blank=True, null=True)), - ], - ), - migrations.CreateModel( - name='Thing', - fields=[ - ('thing_id', models.BigAutoField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=100, unique=True)), - ('release_status', models.BooleanField(default=False)), - ('date_created', models.DateTimeField(auto_now_add=True)), - ], - options={ - 'verbose_name': 'Thing', - 'verbose_name_plural': 'Things', - }, - ), - migrations.RemoveField( - model_name='owner', - name='contact', - ), - migrations.RemoveField( - model_name='equipment', - name='location', - ), - migrations.RemoveField( - model_name='well', - name='well_type', - ), - migrations.RemoveField( - model_name='wellscreen', - name='screen_type', - ), - migrations.RemoveField( - model_name='well', - name='formation_zone', - ), - migrations.RemoveField( - model_name='samplelocation', - name='owner', - ), - migrations.RemoveField( - model_name='well', - name='location', - ), - migrations.RemoveField( - model_name='spring', - name='location', - ), - migrations.RemoveField( - model_name='wellscreen', - name='well', - ), - migrations.CreateModel( - name='GroundwaterLevelObservation', - fields=[ - ('groundwater_level_observation_id', models.BigAutoField(primary_key=True, serialize=False)), - ('observation_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, related_name='groundwater_level_observations', to='samplelocations.observation', verbose_name='related observation')), - ], - bases=('samplelocations.observation',), - ), - migrations.CreateModel( - name='Location_Thing_Junction', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('effective_start', models.DateTimeField()), - ('effective_end', models.DateTimeField()), - ('location', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='locations_things', to='samplelocations.location', verbose_name='related location')), - ], - options={ - 'db_table_comment': 'Junction table linking Location and Thing models', - }, - ), - migrations.AddField( - model_name='observation', - name='sample', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='observations', to='samplelocations.sample', verbose_name='related sample'), - ), - migrations.AddField( - model_name='datastream', - name='sensor_id', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='datastreams', to='samplelocations.sensor', verbose_name='related sensor'), - ), - migrations.CreateModel( - name='SpringThing', - fields=[ - ('springthing_id', models.BigAutoField(primary_key=True, serialize=False)), - ('thing_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, related_name='springthings', to='samplelocations.thing', verbose_name='related thing')), - ('description', models.CharField(blank=True, max_length=255, null=True)), - ], - bases=('samplelocations.thing',), - ), - migrations.CreateModel( - name='WellThing', - fields=[ - ('wellthing_id', models.BigAutoField(primary_key=True, serialize=False)), - ('thing_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, related_name='wellthings', to='samplelocations.thing', verbose_name='related thing')), - ('well_depth', models.FloatField(blank=True, help_text='feet below ground surface', null=True)), - ('hole_depth', models.FloatField(blank=True, help_text='feet below ground surface', null=True)), - ('casing_diameter', models.FloatField(blank=True, help_text='inches', null=True)), - ('casing_depth', models.FloatField(blank=True, help_text='feet below ground surface', null=True)), - ('casing_description', models.CharField(blank=True, max_length=50, null=True)), - ('construction_notes', models.TextField(blank=True, null=True)), - ], - bases=('samplelocations.thing',), - ), - migrations.AddField( - model_name='thing', - name='location', - field=models.ManyToManyField(related_name='things', through='samplelocations.Location_Thing_Junction', to='samplelocations.location', verbose_name='related location'), - ), - migrations.AddField( - model_name='sample', - name='thing', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='samples', to='samplelocations.thing', verbose_name='related thing'), - ), - migrations.AddField( - model_name='location_thing_junction', - name='thing', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='locations_things', to='samplelocations.thing', verbose_name='related thing'), - ), - migrations.AddField( - model_name='datastream', - name='thing', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='datastreams', to='samplelocations.thing', verbose_name='related thing'), - ), - migrations.DeleteModel( - name='Contact', - ), - migrations.DeleteModel( - name='Equipment', - ), - migrations.DeleteModel( - name='Lexicon', - ), - migrations.DeleteModel( - name='Owner', - ), - migrations.DeleteModel( - name='SampleLocation', - ), - migrations.DeleteModel( - name='Spring', - ), - migrations.DeleteModel( - name='Well', - ), - migrations.DeleteModel( - name='WellScreen', - ), - migrations.AddConstraint( - model_name='location_thing_junction', - constraint=models.UniqueConstraint(fields=('location', 'thing'), name='unique_location_thing'), - ), - ] diff --git a/geodjango/samplelocations/migrations/0003_remove_location_thing_junction_unique_location_thing_and_more.py b/geodjango/samplelocations/migrations/0003_remove_location_thing_junction_unique_location_thing_and_more.py deleted file mode 100644 index a9337d4..0000000 --- a/geodjango/samplelocations/migrations/0003_remove_location_thing_junction_unique_location_thing_and_more.py +++ /dev/null @@ -1,87 +0,0 @@ -# Generated by Django 5.2.3 on 2025-07-11 17:33 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('samplelocations', '0002_datastream_observation_location_sample_sensor_thing_and_more'), - ] - - operations = [ - migrations.RemoveConstraint( - model_name='location_thing_junction', - name='unique_location_thing', - ), - migrations.RenameField( - model_name='datastream', - old_name='thing', - new_name='thing_id', - ), - migrations.RenameField( - model_name='location_thing_junction', - old_name='location', - new_name='location_id', - ), - migrations.RenameField( - model_name='location_thing_junction', - old_name='thing', - new_name='thing_id', - ), - migrations.RenameField( - model_name='observation', - old_name='datastream', - new_name='datastream_id', - ), - migrations.RenameField( - model_name='sample', - old_name='thing', - new_name='thing_id', - ), - migrations.RenameField( - model_name='thing', - old_name='location', - new_name='location_id', - ), - migrations.RemoveField( - model_name='wellthing', - name='casing_depth', - ), - migrations.RemoveField( - model_name='wellthing', - name='casing_diameter', - ), - migrations.RemoveField( - model_name='wellthing', - name='hole_depth', - ), - migrations.RemoveField( - model_name='wellthing', - name='well_depth', - ), - migrations.AddField( - model_name='wellthing', - name='casing_depth_ft', - field=models.FloatField(blank=True, help_text='casing depth feet below ground surface', null=True), - ), - migrations.AddField( - model_name='wellthing', - name='casing_diameter_ft', - field=models.FloatField(blank=True, help_text='casing diameter in ft', null=True), - ), - migrations.AddField( - model_name='wellthing', - name='hole_depth_ft', - field=models.FloatField(blank=True, help_text='hole depth feet below ground surface', null=True), - ), - migrations.AddField( - model_name='wellthing', - name='well_depth_ft', - field=models.FloatField(blank=True, help_text='well depth feet below ground surface', null=True), - ), - migrations.AddConstraint( - model_name='location_thing_junction', - constraint=models.UniqueConstraint(fields=('location_id', 'thing_id'), name='unique_location_thing'), - ), - ] diff --git a/geodjango/samplelocations/migrations/0004_sensor_name.py b/geodjango/samplelocations/migrations/0004_sensor_name.py deleted file mode 100644 index 901f494..0000000 --- a/geodjango/samplelocations/migrations/0004_sensor_name.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.3 on 2025-07-11 18:50 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('samplelocations', '0003_remove_location_thing_junction_unique_location_thing_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='sensor', - name='name', - field=models.CharField(blank=True, max_length=100, null=True), - ), - ] From 08afa0669b9d34d79858f673192f6bb61fa29527 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 29 Jul 2025 11:36:52 -0600 Subject: [PATCH 46/57] feat: implement thing endpoints and tests --- geodjango/samplelocations/api/thing.py | 62 ++++--- geodjango/samplelocations/tests/test_thing.py | 154 +++++++++--------- 2 files changed, 116 insertions(+), 100 deletions(-) diff --git a/geodjango/samplelocations/api/thing.py b/geodjango/samplelocations/api/thing.py index 1cefa75..74d881a 100644 --- a/geodjango/samplelocations/api/thing.py +++ b/geodjango/samplelocations/api/thing.py @@ -2,10 +2,14 @@ from samplelocations.models import Thing, Location, Location_Thing_Junction from django.contrib.gis.geos import Point from django.shortcuts import get_object_or_404 -from typing import List +from django.forms.models import model_to_dict +from django.http import HttpResponse +from typing import List, Tuple router = Router() +class NotFoundSchema(Schema): + detail: str class GeoJSONGeometry(Schema): """ @@ -49,7 +53,7 @@ class SpringFeature(Feature): class FeatureCollection(Schema): type: str = "FeatureCollection" - features: List[WellFeature | SpringFeature] = [] + features: List = [] # can be WellFeature or SpringFeature. Specifying a union of both types makes the schema include unrelated fields, which is not desired. def get_things(thing_id: int | None = None) -> List[Thing]: @@ -66,21 +70,35 @@ def construct_feature_collection(things: List[Thing]) -> FeatureCollection: """ Construct a GeoJSON FeatureCollection from a list of Thing objects. """ - location_thing_junctions = Location_Thing_Junction.objects.filter(thing_id__in=things.values_list('thing_id', flat=True)) - locations = Location.objects.filter(location_id__in=location_thing_junctions.values_list('location_id', flat=True)) + """ + Jacob's notes during development: 2025-07-28 + A disadvantage of Django ORM is that you can't make more complicated queries. + From what I understand, you can use Django ORM to get all the things and their related locations, + but you can't easily filter or join them in a single query. The "joining" has to be done in + Python code after fetching the data, which can be less efficient because it's slower than + SQL and you have to make more SQL queries to get the related data. + + Also, the filtering is kind of confusing... + """ + thing_ids = [thing.thing_id for thing in things] + location_thing_junctions = Location_Thing_Junction.objects.filter(thing_id__in=thing_ids) + location_ids = [junction.location_id.location_id for junction in location_thing_junctions] + locations = Location.objects.filter(location_id__in=location_ids) features = [] for thing in things: - if thing.thing_type == "well": - thing_properties = WellProperties(**thing) - elif thing.thing_type == "spring": - thing_properties = SpringProperties(**thing) - - thing_id = thing.thing_id - location_ids = [junction.location_id for junction in location_thing_junctions if junction.thing_id == thing_id] - locations = [loc for loc in locations if loc.location_id in location_ids] - - # assuming, for now, that each thing has a single location + thing_dict = model_to_dict(thing) + thing_dict["location_id"] = thing_dict["location_id"][0].location_id + thing_dict["date_created"] = thing.date_created.isoformat() + thing_dict["thing_type"] = thing.get_thing_type_display() # Get human-readable type + if thing.thing_type == "W": + thing_properties = WellProperties(**thing_dict) + elif thing.thing_type == "S": + thing_properties = SpringProperties(**thing_dict) + + locations = thing.location_id.all() + + # TODO: assuming, for now, that each thing has a single location. this will have to change if we allow multiple locations per thing. location = locations[0] feature = { @@ -106,27 +124,17 @@ def get_all_things(request): """ List all things. """ - """ - Jacob's notes during development: 2025-07-28 - A disadvantage of Django ORM is that you can't make more complicated queries. - From what I understand, you can use Django ORM to get all the things and their related locations, - but you can't easily filter or join them in a single query. The "joining" has to be done in - Python code after fetching the data, which can be less efficient because it's slower than - SQL and you have to make more SQL queries to get the related data. - - Also, the filtering is kind of confusing... - """ things = get_things() response = construct_feature_collection(things) return response -@router.get('/{thing_id}') +@router.get('/{thing_id}', response={200: FeatureCollection, 404: NotFoundSchema}) def get_thing_by_id(request, thing_id: int): """ Retrieve a specific thing by its ID. """ thing = get_things(thing_id=thing_id) - if thing == []: - return {"detail": f"Thing with id {thing_id} not found"}, 404 + if thing == [None]: + return 404, {"detail": f"Thing with id {thing_id} not found"} response = construct_feature_collection(thing) return response \ No newline at end of file diff --git a/geodjango/samplelocations/tests/test_thing.py b/geodjango/samplelocations/tests/test_thing.py index d12cf2f..84513a5 100644 --- a/geodjango/samplelocations/tests/test_thing.py +++ b/geodjango/samplelocations/tests/test_thing.py @@ -1,25 +1,27 @@ from samplelocations.tests import BaseTestClass from samplelocations.models import Thing from samplelocations.models import Location, Location_Thing_Junction - +from pprint import pprint class TestThing(BaseTestClass): """ Test cases for the Thing model. """ + maxDiff = None + def setUp(self): super().setUp() # Create Location records self.location1 = Location.objects.create( coordinate="POINT(10.0 10.0 100.0)", - date_created="2023-10-01T00:00:00Z", + # date_created="2023-10-01T00:00:00Z", ) self.location2 = Location.objects.create( coordinate="POINT(20.0 20.0 200.0)", - date_created="2023-10-01T00:00:00Z", + # date_created="2023-10-01T00:00:00Z", ) # Create Thing records @@ -28,7 +30,7 @@ def setUp(self): description="A well for testing", thing_type="W", release_status=True, - date_created="2023-10-01T00:00:00Z", + # date_created="2023-10-01T00:00:00Z", # location_id=self.location1.location_id, well_depth_ft=100.0, hole_depth_ft=120.0, @@ -42,7 +44,7 @@ def setUp(self): description="A spring for testing", thing_type="S", release_status=True, - date_created="2023-10-01T00:00:00Z", + # date_created="2023-10-01T00:00:00Z", spring_type="thermal", ) @@ -79,84 +81,91 @@ def test_get_all_things(self): """ response = self.client.get("/api/thing") self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.data["features"]), 2) data = response.json() - assert data == { - "type": "FeatureCollection", - "features": [ - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [10.0, 10.0, 100.0] - }, - "properties": { - "thing_id": self.well_thing.thing_id, - "name": self.well_thing.name, - "description": self.well_thing.description, - "thing_type": self.well_thing.thing_type, - "release_status": self.well_thing.release_status, - "date_created": self.well_thing.date_created, - "well_depth_ft": self.well_thing.well_depth_ft, - "hole_depth_ft": self.well_thing.hole_depth_ft, - "casing_diameter_ft": self.well_thing.casing_diameter_ft, - "casing_depth_ft": self.well_thing.casing_depth_ft, - "casing_description": self.well_thing.casing_description, - "construction_notes": self.well_thing.construction_notes, - } - }, - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [20.0, 20.0, 200.0] + self.assertEqual(len(data["features"]), 2) + self.assertEqual( + data, + { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [10.0, 10.0, 100.0] + }, + "properties": { + "thing_id": self.well_thing.thing_id, + "name": self.well_thing.name, + "description": self.well_thing.description, + "thing_type": "Well", + "release_status": self.well_thing.release_status, + "date_created": self.well_thing.date_created.isoformat(), + "well_depth_ft": self.well_thing.well_depth_ft, + "hole_depth_ft": self.well_thing.hole_depth_ft, + "casing_diameter_ft": self.well_thing.casing_diameter_ft, + "casing_depth_ft": self.well_thing.casing_depth_ft, + "casing_description": self.well_thing.casing_description, + "construction_notes": self.well_thing.construction_notes, + } }, - "properties": { - "thing_id": self.spring_thing.thing_id, - "name": self.spring_thing.name, - "description": self.spring_thing.description, - "thing_type": self.spring_thing.thing_type, - "release_status": self.spring_thing.release_status, - "date_created": self.spring_thing.date_created, - "spring_type": self.spring_thing.spring_type, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [20.0, 20.0, 200.0] + }, + "properties": { + "thing_id": self.spring_thing.thing_id, + "name": self.spring_thing.name, + "description": self.spring_thing.description, + "thing_type": "Spring", + "release_status": self.spring_thing.release_status, + "date_created": self.spring_thing.date_created.isoformat(), + "spring_type": self.spring_thing.spring_type, + } } - } - ] - } + ] + } + ) def test_get_thing_by_id(self): """ Retrieve a specific thing by its ID as a feature collection """ - response = self.client.get("/api/thing/1") + thing_id = self.well_thing.thing_id + response = self.client.get(f"/api/thing/{thing_id}") self.assertEqual(response.status_code, 200) data = response.json() - assert data == { - "type": "FeatureCollection", - "features": [ - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [10.0, 10.0, 100.0] + self.assertEqual( + data, + { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [10.0, 10.0, 100.0] + }, + "properties": { + "thing_id": self.well_thing.thing_id, + "name": self.well_thing.name, + "description": self.well_thing.description, + "thing_type": "Well", + "release_status": self.well_thing.release_status, + "date_created": self.well_thing.date_created.isoformat(), + "well_depth_ft": self.well_thing.well_depth_ft, + "hole_depth_ft": self.well_thing.hole_depth_ft, + "casing_diameter_ft": self.well_thing.casing_diameter_ft, + "casing_depth_ft": self.well_thing.casing_depth_ft, + "casing_description": self.well_thing.casing_description, + "construction_notes": self.well_thing.construction_notes, + } }, - "properties": { - "thing_id": self.well_thing.thing_id, - "name": self.well_thing.name, - "description": self.well_thing.description, - "thing_type": self.well_thing.thing_type, - "release_status": self.well_thing.release_status, - "date_created": self.well_thing.date_created, - "well_depth_ft": self.well_thing.well_depth_ft, - "hole_depth_ft": self.well_thing.hole_depth_ft, - "casing_diameter_ft": self.well_thing.casing_diameter_ft, - "casing_depth_ft": self.well_thing.casing_depth_ft, - "casing_description": self.well_thing.casing_description, - "construction_notes": self.well_thing.construction_notes, - } - }, - ] - } + ] + } + ) def test_404_not_found(self): """ @@ -166,4 +175,3 @@ def test_404_not_found(self): self.assertEqual(response.status_code, 404) data = response.json() self.assertEqual(data["detail"], "Thing with id 9999 not found") - self.assertIn("error", data) \ No newline at end of file From 272997c8414d98b7714aec7f043aeb92d6a6810e Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Tue, 29 Jul 2025 14:47:18 -0600 Subject: [PATCH 47/57] feat: implement /location endpoint --- geodjango/geodjango/api.py | 2 +- geodjango/samplelocations/api/locations.py | 83 +++++++------ .../samplelocations/tests/test_location.py | 116 ++++++++++-------- 3 files changed, 108 insertions(+), 93 deletions(-) diff --git a/geodjango/geodjango/api.py b/geodjango/geodjango/api.py index e427088..b974c8e 100644 --- a/geodjango/geodjango/api.py +++ b/geodjango/geodjango/api.py @@ -2,7 +2,7 @@ api = NinjaAPI() -# api.add_router('/locations', 'samplelocations.api.locations.router', tags=['locations']) +api.add_router('/location', 'samplelocations.api.locations.router', tags=['locations']) # api.add_router('/wells', 'samplelocations.api.wells.router', tags=['wells']) # api.add_router('/contacts', 'samplelocations.api.contacts.router', tags=['contacts']) api.add_router('/thing', 'samplelocations.api.thing.router', tags=['things']) \ No newline at end of file diff --git a/geodjango/samplelocations/api/locations.py b/geodjango/samplelocations/api/locations.py index 766528e..abbce44 100644 --- a/geodjango/samplelocations/api/locations.py +++ b/geodjango/samplelocations/api/locations.py @@ -1,38 +1,45 @@ -# from ninja import Router -# from samplelocations.models import Location, Owner -# from django.contrib.gis.geos import Point -# from django.shortcuts import get_object_or_404 - -# router = Router() - -# @router.get('') -# def get_locations(request): -# """ -# List all sample locations. -# """ -# locations = SampleLocation.objects.all() -# return [ -# { -# 'id': loc.id, -# 'name': loc.name, -# 'lat': loc.point.y, -# 'lon': loc.point.x, -# 'description': loc.description, -# 'visible': loc.visible, -# 'date_created': loc.date_created.isoformat(), -# } -# for loc in locations -# ] - -# @router.post("") -# def post_location(request, name: str, lat: float, lon: float, owner_id: int, description: str = None, visible: bool = False): -# owner = get_object_or_404(Owner, id=owner_id) -# point = Point(lon, lat) -# location = SampleLocation.objects.create( -# name=name, -# description=description, -# visible=visible, -# point=point, -# owner=owner -# ) -# return {"id": location.id, "name": location.name} \ No newline at end of file +from ninja import Router, Schema +from samplelocations.models import Location +from typing import List + +router = Router() + +class NotFoundSchema(Schema): + detail: str + +class LocationSchema(Schema): + location_id: int + coordinates: str + date_created: str + +@router.get("") +def get_locations(request) -> List[LocationSchema]: + """ + List all sample locations. + """ + locations = Location.objects.all() + + response = [ + { + "location_id": location.location_id, + "coordinates": f"POINT({location.coordinate.x} {location.coordinate.y} {location.coordinate.z})", + "date_created": location.date_created.isoformat(), + } + for location in locations + ] + + return response + +@router.get("/{location_id}", response={200: LocationSchema, 404: NotFoundSchema}) +def get_location_by_id(request, location_id: int): + locations = Location.objects.filter(location_id=location_id) + if not locations.exists(): + return 404, {"detail": f"Location with location_id {location_id} not found"} + + location = locations.first() + response = { + "location_id": location.location_id, + "coordinates": f"POINT({location.coordinate.x} {location.coordinate.y} {location.coordinate.z})", + "date_created": location.date_created.isoformat(), + } + return response \ No newline at end of file diff --git a/geodjango/samplelocations/tests/test_location.py b/geodjango/samplelocations/tests/test_location.py index 9b42891..2435e7c 100644 --- a/geodjango/samplelocations/tests/test_location.py +++ b/geodjango/samplelocations/tests/test_location.py @@ -23,77 +23,85 @@ def test_get_shapefile(): from samplelocations.tests import BaseTestClass from samplelocations.models import Location -class TestAddLocation(BaseTestClass): +# class TestAddLocation(BaseTestClass): - def test_add_location_visible_is_true(self): - response = self.client.post( - "/location", - json={ - "name": "Test Location 1", - "point": "POINT(10.1 10.1)", - "visible": True, - }, - ) - self.assertEqual(response.status_code, 201) - data = response.json() - self.assertEqual(data["id"], 2) +# def test_add_location_visible_is_true(self): +# response = self.client.post( +# "/location", +# json={ +# "name": "Test Location 1", +# "point": "POINT(10.1 10.1)", +# "visible": True, +# }, +# ) +# self.assertEqual(response.status_code, 201) +# data = response.json() +# self.assertEqual(data["id"], 2) - def test_add_location_visible_is_false(self): - response = self.client.post( - "/location", - json={ - "name": "Test Location 2", - "point": "POINT(50.0 50.0)", - "visible": False, - }, - ) - self.assertEqual(response.status_code, 201) - data = response.json() - self.assertEqual(data["id"], 3) +# def test_add_location_visible_is_false(self): +# response = self.client.post( +# "/location", +# json={ +# "name": "Test Location 2", +# "point": "POINT(50.0 50.0)", +# "visible": False, +# }, +# ) +# self.assertEqual(response.status_code, 201) +# data = response.json() +# self.assertEqual(data["id"], 3) class TestGetLocation(BaseTestClass): def setUp(self): - self.location = Location.objects.create( - name="Test Location", - point="POINT(10.0 10.0)", - visible=True, + self.location_1 = Location.objects.create( + coordinate = "POINT(10 10 100)" + ) + + self.location_2 = Location.objects.create( + coordinate = "POINT(20 20 200)", ) def tearDown(self): - self.location.delete() + self.location_1.delete() + self.location_2.delete() return super().tearDown() - def test_list_locations(self): + def test_get_all_locations(self): """ - list al locations in the database as a feature collection + Tests that all locations can be listed """ - - def get_specific_location(self): - response = self.client.get(f"/location/{self.location.id}") + response = self.client.get("/api/location") self.assertEqual(response.status_code, 200) data = response.json() - self.assertEqual(data["id"], self.location.id) - self.assertEqual(data["name"], self.location.name) - self.assertEqual(data["point"], "POINT(10.0 10.0)") + self.assertEqual(len(data), 2) + self.assertEqual(data[0]["coordinates"], f"POINT({self.location_1.coordinate.x} {self.location_1.coordinate.y} {self.location_1.coordinate.z})") + self.assertEqual(data[1]["coordinates"], f"POINT({self.location_2.coordinate.x} {self.location_2.coordinate.y} {self.location_2.coordinate.z})") - def test_get_location_as_geojson(self): - response = self.client.get("/location?format=feature-collection") + def test_get_location_by_id(self): + """ + Tests that a specific location can be retrieved by its ID + """ + self.maxDiff = None + response = self.client.get(f"/api/location/{self.location_1.location_id}") self.assertEqual(response.status_code, 200) data = response.json() - self.assertIn("type", data) - self.assertEqual(data["type"], "FeatureCollection") - self.assertIn("features", data) - self.assertGreater(len(data["features"]), 0) # Assuming there are features in the collection - - - def test_get_location_as_shapefile(self): - response = self.client.get("/location?format=shapefile") - self.assertEqual(response.status_code, 200) - self.assertEqual(response.headers["Content-Type"], "application/zip") - self.assertIn("Content-Disposition", response.headers) self.assertEqual( - 'attachment; filename="locations.zip"', - response.headers["Content-Disposition"] - ) \ No newline at end of file + data, + { + "location_id": self.location_1.location_id, + "coordinates": f"POINT({self.location_1.coordinate.x} {self.location_1.coordinate.y} {self.location_1.coordinate.z})", + "date_created": self.location_1.date_created.isoformat(), + } + ) + + def test_404_location_not_found(self): + """ + Tests that a 404 is returned when trying to access a non-existent location + """ + response = self.client.get("/api/location/9999") + print(response) + self.assertEqual(response.status_code, 404) + data = response.json() + self.assertEqual(data["detail"], "Location with location_id 9999 not found") \ No newline at end of file From ad6b9a7464d9b8eb477c5b6a8189e3ba2ea755dd Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Wed, 30 Jul 2025 09:45:57 -0600 Subject: [PATCH 48/57] refactor: Move ThingType choices outside of Thing model --- geodjango/samplelocations/models.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/geodjango/samplelocations/models.py b/geodjango/samplelocations/models.py index 530588d..0e18388 100644 --- a/geodjango/samplelocations/models.py +++ b/geodjango/samplelocations/models.py @@ -34,17 +34,16 @@ class Meta: #--------Thing model ----------- +# Define class-based choices for the 'thing_type' field. +# This allows for a more structured way to define and use choices in Django models. +# The format is CHOICE = " database value", "human-readable or display name" +class ThingType(models.TextChoices): + WELL = "W", "Well" + SPRING = "S", "Spring" class Thing(models.Model): """A base model representing a generic monitoring station (Thing)""" - # Define class-based choices for the 'thing_type' field. - # This allows for a more structured way to define and use choices in Django models. - # The format is CHOICE = " database value", "human-readable or display name" - class ThingType(models.TextChoices): - WELL = "W", "Well" - SPRING = "S", "Spring" - thing_id = models.BigAutoField(primary_key=True) name = models.CharField(max_length=100, unique=True) thing_type = models.CharField( @@ -74,7 +73,7 @@ class ThingType(models.TextChoices): #Fields specific to a SPRING spring_type = models.CharField(max_length=255, blank=True, null=True) # e.g. "artesian", "subartesian", "thermal", etc. - description_spring = models.CharField(max_length=255, blank=True, null=True) + def __str__(self): return f"Thing object is a {self.thing_type} with name {self.name}" From 1eb2d67eadc33ee4ef14f5e08ff16220b0efabeb Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Wed, 30 Jul 2025 09:51:54 -0600 Subject: [PATCH 49/57] refactor: Add sample_matrix field to Sample model. Add sample matrix choices --- geodjango/samplelocations/models.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/geodjango/samplelocations/models.py b/geodjango/samplelocations/models.py index 0e18388..652d99d 100644 --- a/geodjango/samplelocations/models.py +++ b/geodjango/samplelocations/models.py @@ -148,11 +148,22 @@ def __str__(self): #--------Sample model----------- +# Define choices for thhe 'sample_matrix' field. +# The format is CHOICE = " database value", "human-readable or display name" +class SampleMatrix(models.TextChoices): + GROUNDWATER = "GW", "Groundwater" + SOIL = "S", "Soil" class Sample(models.Model): """Represents a sample collected from a Thing""" sample_id = models.BigAutoField(primary_key=True) thing_id = models.ForeignKey(Thing, on_delete=models.CASCADE, related_name="samples", verbose_name="related thing") + sample_matrix - models.CharField( + max_length=2, + choices=SampleMatrix.choices, # Use the choices defined in the SampleMatrix class + default=SampleMatrix.GROUNDWATER, # Set a default value for the field. + verbose_name="type of sample" # Human-readable label for user interfaces like forms and the admin panel. + ) sample_date = models.DateTimeField() sample_notes = models.TextField(blank=True, null=True) From fb15065d0d683ea8e28326f893e8f62cdf65bebe Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Wed, 30 Jul 2025 11:59:11 -0600 Subject: [PATCH 50/57] style: Correct sample_matrix field formatting --- geodjango/samplelocations/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geodjango/samplelocations/models.py b/geodjango/samplelocations/models.py index 652d99d..7b989ea 100644 --- a/geodjango/samplelocations/models.py +++ b/geodjango/samplelocations/models.py @@ -158,7 +158,7 @@ class Sample(models.Model): """Represents a sample collected from a Thing""" sample_id = models.BigAutoField(primary_key=True) thing_id = models.ForeignKey(Thing, on_delete=models.CASCADE, related_name="samples", verbose_name="related thing") - sample_matrix - models.CharField( + sample_matrix = models.CharField( max_length=2, choices=SampleMatrix.choices, # Use the choices defined in the SampleMatrix class default=SampleMatrix.GROUNDWATER, # Set a default value for the field. From 266f88bb554029698b44d355cc20628e60f7b7fb Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Wed, 30 Jul 2025 11:59:44 -0600 Subject: [PATCH 51/57] refactor: remove previous migrations --- .../migrations/0001_initial.py | 127 ----------- ...n_location_sample_sensor_thing_and_more.py | 213 ------------------ ...junction_unique_location_thing_and_more.py | 87 ------- .../migrations/0004_sensor_name.py | 18 -- 4 files changed, 445 deletions(-) delete mode 100644 geodjango/samplelocations/migrations/0001_initial.py delete mode 100644 geodjango/samplelocations/migrations/0002_datastream_observation_location_sample_sensor_thing_and_more.py delete mode 100644 geodjango/samplelocations/migrations/0003_remove_location_thing_junction_unique_location_thing_and_more.py delete mode 100644 geodjango/samplelocations/migrations/0004_sensor_name.py diff --git a/geodjango/samplelocations/migrations/0001_initial.py b/geodjango/samplelocations/migrations/0001_initial.py deleted file mode 100644 index d6c175a..0000000 --- a/geodjango/samplelocations/migrations/0001_initial.py +++ /dev/null @@ -1,127 +0,0 @@ -# Generated by Django 5.2.3 on 2025-06-26 15:14 - -import django.contrib.gis.db.models.fields -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='Contact', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100)), - ('email', models.EmailField(max_length=254)), - ('phone', models.CharField(blank=True, max_length=20, null=True)), - ('date_created', models.DateTimeField(auto_now_add=True)), - ], - options={ - 'verbose_name': 'Contact', - 'verbose_name_plural': 'Contacts', - 'ordering': ['name'], - }, - ), - migrations.CreateModel( - name='Lexicon', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100, unique=True)), - ('description', models.CharField(blank=True, max_length=255, null=True)), - ('date_created', models.DateTimeField(auto_now_add=True)), - ], - options={ - 'verbose_name': 'Lexicon', - 'verbose_name_plural': 'Lexicons', - 'ordering': ['name'], - }, - ), - migrations.CreateModel( - name='Owner', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100)), - ('description', models.CharField(blank=True, max_length=255, null=True)), - ('date_created', models.DateTimeField(auto_now_add=True)), - ('contact', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='owners', to='samplelocations.contact')), - ], - options={ - 'verbose_name': 'Owner', - 'verbose_name_plural': 'Owners', - 'ordering': ['name'], - }, - ), - migrations.CreateModel( - name='SampleLocation', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100)), - ('description', models.CharField(blank=True, max_length=255, null=True)), - ('visible', models.BooleanField(default=False)), - ('point', django.contrib.gis.db.models.fields.PointField(srid=4326)), - ('date_created', models.DateTimeField(auto_now_add=True)), - ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='samplelocations', to='samplelocations.owner')), - ], - options={ - 'verbose_name': 'Sample Location', - 'verbose_name_plural': 'Sample Locations', - 'ordering': ['name'], - }, - ), - migrations.CreateModel( - name='Equipment', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('equipment_type', models.CharField(max_length=50)), - ('model', models.CharField(max_length=50)), - ('serial_no', models.CharField(max_length=50)), - ('date_installed', models.DateTimeField(blank=True, null=True)), - ('date_removed', models.DateTimeField(blank=True, null=True)), - ('recording_interval', models.IntegerField(blank=True, null=True)), - ('equipment_notes', models.CharField(blank=True, max_length=50, null=True)), - ('location', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='equipment', to='samplelocations.samplelocation')), - ], - ), - migrations.CreateModel( - name='Spring', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('description', models.CharField(blank=True, max_length=255, null=True)), - ('location', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='springs', to='samplelocations.samplelocation')), - ], - ), - migrations.CreateModel( - name='Well', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('ose_pod_id', models.CharField(blank=True, max_length=50, null=True)), - ('api_id', models.CharField(blank=True, default='', max_length=50)), - ('usgs_id', models.CharField(blank=True, max_length=50, null=True)), - ('well_depth', models.FloatField(blank=True, help_text='feet below ground surface', null=True)), - ('hole_depth', models.FloatField(blank=True, help_text='feet below ground surface', null=True)), - ('casing_diameter', models.FloatField(blank=True, help_text='inches', null=True)), - ('casing_depth', models.FloatField(blank=True, help_text='feet below ground surface', null=True)), - ('casing_description', models.CharField(blank=True, max_length=50, null=True)), - ('construction_notes', models.CharField(blank=True, max_length=250, null=True)), - ('formation_zone', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='wells_by_formation', to='samplelocations.lexicon')), - ('location', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='wells', to='samplelocations.samplelocation')), - ('well_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='wells_by_type', to='samplelocations.lexicon')), - ], - ), - migrations.CreateModel( - name='WellScreen', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('screen_depth_top', models.FloatField(help_text='feet below ground surface')), - ('screen_depth_bottom', models.FloatField(help_text='feet below ground surface')), - ('screen_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='well_screens_by_type', to='samplelocations.lexicon')), - ('well', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='screens', to='samplelocations.well')), - ], - ), - ] diff --git a/geodjango/samplelocations/migrations/0002_datastream_observation_location_sample_sensor_thing_and_more.py b/geodjango/samplelocations/migrations/0002_datastream_observation_location_sample_sensor_thing_and_more.py deleted file mode 100644 index 1c34efa..0000000 --- a/geodjango/samplelocations/migrations/0002_datastream_observation_location_sample_sensor_thing_and_more.py +++ /dev/null @@ -1,213 +0,0 @@ -# Generated by Django 5.2.3 on 2025-07-11 16:53 - -import django.contrib.gis.db.models.fields -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('samplelocations', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='Datastream', - fields=[ - ('datastream_id', models.BigAutoField(primary_key=True, serialize=False)), - ('observed_property', models.CharField(max_length=100)), - ('release_status', models.BooleanField(default=False)), - ], - ), - migrations.CreateModel( - name='Observation', - fields=[ - ('observation_id', models.BigAutoField(primary_key=True, serialize=False)), - ('observed_value', models.FloatField(help_text='The value of the observation')), - ('release_status', models.BooleanField(default=False)), - ('datastream', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='observations', to='samplelocations.datastream', verbose_name='related datastream')), - ], - ), - migrations.CreateModel( - name='Location', - fields=[ - ('location_id', models.BigAutoField(primary_key=True, serialize=False)), - ('coordinate', django.contrib.gis.db.models.fields.PointField(dim=3, srid=4326)), - ('date_created', models.DateTimeField(auto_now_add=True)), - ], - options={ - 'verbose_name': 'Location', - 'verbose_name_plural': 'Locations', - 'db_table_comment': "This table stores point locations on the earth's surface", - }, - ), - migrations.CreateModel( - name='Sample', - fields=[ - ('sample_id', models.BigAutoField(primary_key=True, serialize=False)), - ('sample_date', models.DateTimeField()), - ('sample_notes', models.TextField(blank=True, null=True)), - ], - ), - migrations.CreateModel( - name='Sensor', - fields=[ - ('sensor_id', models.BigAutoField(primary_key=True, serialize=False)), - ('serial_number', models.CharField(blank=True, max_length=50, null=True)), - ('install_date', models.DateTimeField(blank=True, null=True)), - ('model', models.CharField(blank=True, max_length=50, null=True)), - ('notes', models.TextField(blank=True, null=True)), - ], - ), - migrations.CreateModel( - name='Thing', - fields=[ - ('thing_id', models.BigAutoField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=100, unique=True)), - ('release_status', models.BooleanField(default=False)), - ('date_created', models.DateTimeField(auto_now_add=True)), - ], - options={ - 'verbose_name': 'Thing', - 'verbose_name_plural': 'Things', - }, - ), - migrations.RemoveField( - model_name='owner', - name='contact', - ), - migrations.RemoveField( - model_name='equipment', - name='location', - ), - migrations.RemoveField( - model_name='well', - name='well_type', - ), - migrations.RemoveField( - model_name='wellscreen', - name='screen_type', - ), - migrations.RemoveField( - model_name='well', - name='formation_zone', - ), - migrations.RemoveField( - model_name='samplelocation', - name='owner', - ), - migrations.RemoveField( - model_name='well', - name='location', - ), - migrations.RemoveField( - model_name='spring', - name='location', - ), - migrations.RemoveField( - model_name='wellscreen', - name='well', - ), - migrations.CreateModel( - name='GroundwaterLevelObservation', - fields=[ - ('groundwater_level_observation_id', models.BigAutoField(primary_key=True, serialize=False)), - ('observation_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, related_name='groundwater_level_observations', to='samplelocations.observation', verbose_name='related observation')), - ], - bases=('samplelocations.observation',), - ), - migrations.CreateModel( - name='Location_Thing_Junction', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('effective_start', models.DateTimeField()), - ('effective_end', models.DateTimeField()), - ('location', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='locations_things', to='samplelocations.location', verbose_name='related location')), - ], - options={ - 'db_table_comment': 'Junction table linking Location and Thing models', - }, - ), - migrations.AddField( - model_name='observation', - name='sample', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='observations', to='samplelocations.sample', verbose_name='related sample'), - ), - migrations.AddField( - model_name='datastream', - name='sensor_id', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='datastreams', to='samplelocations.sensor', verbose_name='related sensor'), - ), - migrations.CreateModel( - name='SpringThing', - fields=[ - ('springthing_id', models.BigAutoField(primary_key=True, serialize=False)), - ('thing_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, related_name='springthings', to='samplelocations.thing', verbose_name='related thing')), - ('description', models.CharField(blank=True, max_length=255, null=True)), - ], - bases=('samplelocations.thing',), - ), - migrations.CreateModel( - name='WellThing', - fields=[ - ('wellthing_id', models.BigAutoField(primary_key=True, serialize=False)), - ('thing_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, related_name='wellthings', to='samplelocations.thing', verbose_name='related thing')), - ('well_depth', models.FloatField(blank=True, help_text='feet below ground surface', null=True)), - ('hole_depth', models.FloatField(blank=True, help_text='feet below ground surface', null=True)), - ('casing_diameter', models.FloatField(blank=True, help_text='inches', null=True)), - ('casing_depth', models.FloatField(blank=True, help_text='feet below ground surface', null=True)), - ('casing_description', models.CharField(blank=True, max_length=50, null=True)), - ('construction_notes', models.TextField(blank=True, null=True)), - ], - bases=('samplelocations.thing',), - ), - migrations.AddField( - model_name='thing', - name='location', - field=models.ManyToManyField(related_name='things', through='samplelocations.Location_Thing_Junction', to='samplelocations.location', verbose_name='related location'), - ), - migrations.AddField( - model_name='sample', - name='thing', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='samples', to='samplelocations.thing', verbose_name='related thing'), - ), - migrations.AddField( - model_name='location_thing_junction', - name='thing', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='locations_things', to='samplelocations.thing', verbose_name='related thing'), - ), - migrations.AddField( - model_name='datastream', - name='thing', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='datastreams', to='samplelocations.thing', verbose_name='related thing'), - ), - migrations.DeleteModel( - name='Contact', - ), - migrations.DeleteModel( - name='Equipment', - ), - migrations.DeleteModel( - name='Lexicon', - ), - migrations.DeleteModel( - name='Owner', - ), - migrations.DeleteModel( - name='SampleLocation', - ), - migrations.DeleteModel( - name='Spring', - ), - migrations.DeleteModel( - name='Well', - ), - migrations.DeleteModel( - name='WellScreen', - ), - migrations.AddConstraint( - model_name='location_thing_junction', - constraint=models.UniqueConstraint(fields=('location', 'thing'), name='unique_location_thing'), - ), - ] diff --git a/geodjango/samplelocations/migrations/0003_remove_location_thing_junction_unique_location_thing_and_more.py b/geodjango/samplelocations/migrations/0003_remove_location_thing_junction_unique_location_thing_and_more.py deleted file mode 100644 index a9337d4..0000000 --- a/geodjango/samplelocations/migrations/0003_remove_location_thing_junction_unique_location_thing_and_more.py +++ /dev/null @@ -1,87 +0,0 @@ -# Generated by Django 5.2.3 on 2025-07-11 17:33 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('samplelocations', '0002_datastream_observation_location_sample_sensor_thing_and_more'), - ] - - operations = [ - migrations.RemoveConstraint( - model_name='location_thing_junction', - name='unique_location_thing', - ), - migrations.RenameField( - model_name='datastream', - old_name='thing', - new_name='thing_id', - ), - migrations.RenameField( - model_name='location_thing_junction', - old_name='location', - new_name='location_id', - ), - migrations.RenameField( - model_name='location_thing_junction', - old_name='thing', - new_name='thing_id', - ), - migrations.RenameField( - model_name='observation', - old_name='datastream', - new_name='datastream_id', - ), - migrations.RenameField( - model_name='sample', - old_name='thing', - new_name='thing_id', - ), - migrations.RenameField( - model_name='thing', - old_name='location', - new_name='location_id', - ), - migrations.RemoveField( - model_name='wellthing', - name='casing_depth', - ), - migrations.RemoveField( - model_name='wellthing', - name='casing_diameter', - ), - migrations.RemoveField( - model_name='wellthing', - name='hole_depth', - ), - migrations.RemoveField( - model_name='wellthing', - name='well_depth', - ), - migrations.AddField( - model_name='wellthing', - name='casing_depth_ft', - field=models.FloatField(blank=True, help_text='casing depth feet below ground surface', null=True), - ), - migrations.AddField( - model_name='wellthing', - name='casing_diameter_ft', - field=models.FloatField(blank=True, help_text='casing diameter in ft', null=True), - ), - migrations.AddField( - model_name='wellthing', - name='hole_depth_ft', - field=models.FloatField(blank=True, help_text='hole depth feet below ground surface', null=True), - ), - migrations.AddField( - model_name='wellthing', - name='well_depth_ft', - field=models.FloatField(blank=True, help_text='well depth feet below ground surface', null=True), - ), - migrations.AddConstraint( - model_name='location_thing_junction', - constraint=models.UniqueConstraint(fields=('location_id', 'thing_id'), name='unique_location_thing'), - ), - ] diff --git a/geodjango/samplelocations/migrations/0004_sensor_name.py b/geodjango/samplelocations/migrations/0004_sensor_name.py deleted file mode 100644 index 901f494..0000000 --- a/geodjango/samplelocations/migrations/0004_sensor_name.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.3 on 2025-07-11 18:50 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('samplelocations', '0003_remove_location_thing_junction_unique_location_thing_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='sensor', - name='name', - field=models.CharField(blank=True, max_length=100, null=True), - ), - ] From 22204b95fe3b7be30d254a75f95a47d8240e9367 Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Wed, 30 Jul 2025 12:02:40 -0600 Subject: [PATCH 52/57] refactor: Remove import of WellThing and SpringThing models --- geodjango/samplelocations/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geodjango/samplelocations/admin.py b/geodjango/samplelocations/admin.py index 7ad3bef..9f126f5 100644 --- a/geodjango/samplelocations/admin.py +++ b/geodjango/samplelocations/admin.py @@ -3,7 +3,7 @@ from django import forms from django.contrib.gis.admin import GISModelAdmin from django.contrib.gis.geos import Point -from samplelocations.models import Location, Thing, WellThing, SpringThing, Location_Thing_Junction, Sensor, Datastream, Observation, \ +from samplelocations.models import Location, Thing, Location_Thing_Junction, Sensor, Datastream, Observation, \ GroundwaterLevelObservation, Sample class LocationForm(forms.ModelForm): From 12d14bbee2c3ef67f17bdfbe59ab3122184b32bf Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Wed, 30 Jul 2025 12:12:25 -0600 Subject: [PATCH 53/57] Perform new initial migration (2025-07-30) --- .../migrations/0001_initial.py | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 geodjango/samplelocations/migrations/0001_initial.py diff --git a/geodjango/samplelocations/migrations/0001_initial.py b/geodjango/samplelocations/migrations/0001_initial.py new file mode 100644 index 0000000..fafbb0d --- /dev/null +++ b/geodjango/samplelocations/migrations/0001_initial.py @@ -0,0 +1,137 @@ +# Generated by Django 5.2.3 on 2025-07-30 18:04 + +import django.contrib.gis.db.models.fields +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Datastream', + fields=[ + ('datastream_id', models.BigAutoField(primary_key=True, serialize=False)), + ('observed_property', models.CharField(max_length=100)), + ('release_status', models.BooleanField(default=False)), + ], + ), + migrations.CreateModel( + name='Observation', + fields=[ + ('observation_id', models.BigAutoField(primary_key=True, serialize=False)), + ('observed_value', models.FloatField(help_text='The value of the observation')), + ('release_status', models.BooleanField(default=False)), + ('datastream_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='observations', to='samplelocations.datastream', verbose_name='related datastream')), + ], + ), + migrations.CreateModel( + name='Location', + fields=[ + ('location_id', models.BigAutoField(primary_key=True, serialize=False)), + ('coordinate', django.contrib.gis.db.models.fields.PointField(dim=3, srid=4326)), + ('date_created', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'verbose_name': 'Location', + 'verbose_name_plural': 'Locations', + 'db_table_comment': "This table stores point locations on the earth's surface", + }, + ), + migrations.CreateModel( + name='Sample', + fields=[ + ('sample_id', models.BigAutoField(primary_key=True, serialize=False)), + ('sample_matrix', models.CharField(choices=[('GW', 'Groundwater'), ('S', 'Soil')], default='GW', max_length=2, verbose_name='type of sample')), + ('sample_date', models.DateTimeField()), + ('sample_notes', models.TextField(blank=True, null=True)), + ], + ), + migrations.CreateModel( + name='Sensor', + fields=[ + ('sensor_id', models.BigAutoField(primary_key=True, serialize=False)), + ('name', models.CharField(blank=True, max_length=100, null=True)), + ('serial_number', models.CharField(blank=True, max_length=50, null=True)), + ('install_date', models.DateTimeField(blank=True, null=True)), + ('model', models.CharField(blank=True, max_length=50, null=True)), + ('notes', models.TextField(blank=True, null=True)), + ], + ), + migrations.CreateModel( + name='GroundwaterLevelObservation', + fields=[ + ('groundwater_level_observation_id', models.BigAutoField(primary_key=True, serialize=False)), + ('observation_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, related_name='groundwater_level_observations', to='samplelocations.observation', verbose_name='related observation')), + ], + bases=('samplelocations.observation',), + ), + migrations.CreateModel( + name='Location_Thing_Junction', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('effective_start', models.DateTimeField()), + ('effective_end', models.DateTimeField()), + ('location_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='locations_things', to='samplelocations.location', verbose_name='related location')), + ], + options={ + 'db_table_comment': 'Junction table linking Location and Thing models', + }, + ), + migrations.AddField( + model_name='observation', + name='sample', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='observations', to='samplelocations.sample', verbose_name='related sample'), + ), + migrations.AddField( + model_name='datastream', + name='sensor_id', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='datastreams', to='samplelocations.sensor', verbose_name='related sensor'), + ), + migrations.CreateModel( + name='Thing', + fields=[ + ('thing_id', models.BigAutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=100, unique=True)), + ('thing_type', models.CharField(choices=[('W', 'Well'), ('S', 'Spring')], default='W', max_length=2, verbose_name='type of thing')), + ('release_status', models.BooleanField(default=False)), + ('date_created', models.DateTimeField(auto_now_add=True)), + ('well_depth_ft', models.FloatField(blank=True, help_text='well depth feet below ground surface', null=True)), + ('hole_depth_ft', models.FloatField(blank=True, help_text='hole depth feet below ground surface', null=True)), + ('casing_diameter_ft', models.FloatField(blank=True, help_text='casing diameter in ft', null=True)), + ('casing_depth_ft', models.FloatField(blank=True, help_text='casing depth feet below ground surface', null=True)), + ('casing_description', models.CharField(blank=True, max_length=50, null=True)), + ('construction_notes', models.TextField(blank=True, null=True)), + ('spring_type', models.CharField(blank=True, max_length=255, null=True)), + ('location_id', models.ManyToManyField(related_name='things', through='samplelocations.Location_Thing_Junction', to='samplelocations.location', verbose_name='related location')), + ], + options={ + 'verbose_name': 'Thing', + 'verbose_name_plural': 'Things', + }, + ), + migrations.AddField( + model_name='sample', + name='thing_id', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='samples', to='samplelocations.thing', verbose_name='related thing'), + ), + migrations.AddField( + model_name='location_thing_junction', + name='thing_id', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='locations_things', to='samplelocations.thing', verbose_name='related thing'), + ), + migrations.AddField( + model_name='datastream', + name='thing_id', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='datastreams', to='samplelocations.thing', verbose_name='related thing'), + ), + migrations.AddConstraint( + model_name='location_thing_junction', + constraint=models.UniqueConstraint(fields=('location_id', 'thing_id'), name='unique_location_thing'), + ), + ] From d7ac245e9bdf0e4a6b3f9ae4a0ea0814987e4153 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Wed, 30 Jul 2025 15:35:02 -0600 Subject: [PATCH 54/57] fix: remove description column --- geodjango/samplelocations/api/thing.py | 1 - geodjango/samplelocations/models.py | 1 - 2 files changed, 2 deletions(-) diff --git a/geodjango/samplelocations/api/thing.py b/geodjango/samplelocations/api/thing.py index 74d881a..7e94cfa 100644 --- a/geodjango/samplelocations/api/thing.py +++ b/geodjango/samplelocations/api/thing.py @@ -31,7 +31,6 @@ class BaseProperties(Schema): thing_type: str release_status: bool date_created: str - description: str | None = None class WellProperties(BaseProperties): diff --git a/geodjango/samplelocations/models.py b/geodjango/samplelocations/models.py index 8afcb05..7559afa 100644 --- a/geodjango/samplelocations/models.py +++ b/geodjango/samplelocations/models.py @@ -54,7 +54,6 @@ class Thing(models.Model): ) release_status = models.BooleanField(default=False) date_created = models.DateTimeField(auto_now_add=True) - description = models.TextField(blank=True, null=True, help_text="A description of the thing") # The 'location' field sets up the M:M relationship and specifies # the 'Locoation_Thing_Juncation' as the intermediate table. location_id = models.ManyToManyField( From fa9f5113927be6546121b51b67fc68fcb11672c4 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Wed, 30 Jul 2025 15:35:19 -0600 Subject: [PATCH 55/57] fix: update tests for newly defined thing model --- geodjango/samplelocations/tests/test_thing.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/geodjango/samplelocations/tests/test_thing.py b/geodjango/samplelocations/tests/test_thing.py index 84513a5..0c1556d 100644 --- a/geodjango/samplelocations/tests/test_thing.py +++ b/geodjango/samplelocations/tests/test_thing.py @@ -16,22 +16,17 @@ def setUp(self): # Create Location records self.location1 = Location.objects.create( coordinate="POINT(10.0 10.0 100.0)", - # date_created="2023-10-01T00:00:00Z", ) self.location2 = Location.objects.create( coordinate="POINT(20.0 20.0 200.0)", - # date_created="2023-10-01T00:00:00Z", ) # Create Thing records self.well_thing = Thing.objects.create( name="Test Well", - description="A well for testing", thing_type="W", release_status=True, - # date_created="2023-10-01T00:00:00Z", - # location_id=self.location1.location_id, well_depth_ft=100.0, hole_depth_ft=120.0, casing_diameter_ft=10.0, @@ -41,10 +36,8 @@ def setUp(self): ) self.spring_thing = Thing.objects.create( name="Test Spring", - description="A spring for testing", thing_type="S", release_status=True, - # date_created="2023-10-01T00:00:00Z", spring_type="thermal", ) @@ -97,7 +90,6 @@ def test_get_all_things(self): "properties": { "thing_id": self.well_thing.thing_id, "name": self.well_thing.name, - "description": self.well_thing.description, "thing_type": "Well", "release_status": self.well_thing.release_status, "date_created": self.well_thing.date_created.isoformat(), @@ -118,7 +110,6 @@ def test_get_all_things(self): "properties": { "thing_id": self.spring_thing.thing_id, "name": self.spring_thing.name, - "description": self.spring_thing.description, "thing_type": "Spring", "release_status": self.spring_thing.release_status, "date_created": self.spring_thing.date_created.isoformat(), @@ -151,7 +142,6 @@ def test_get_thing_by_id(self): "properties": { "thing_id": self.well_thing.thing_id, "name": self.well_thing.name, - "description": self.well_thing.description, "thing_type": "Well", "release_status": self.well_thing.release_status, "date_created": self.well_thing.date_created.isoformat(), From 85f13565e9d8ae237e68b6d61cc8b1dd7d015ee8 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Thu, 31 Jul 2025 09:08:04 -0600 Subject: [PATCH 56/57] refactor: better organize and use schemas --- geodjango/samplelocations/api/locations.py | 11 +--- geodjango/samplelocations/api/thing.py | 50 +----------------- geodjango/samplelocations/schemas.py | 60 ++++++++++++++++++++++ 3 files changed, 64 insertions(+), 57 deletions(-) create mode 100644 geodjango/samplelocations/schemas.py diff --git a/geodjango/samplelocations/api/locations.py b/geodjango/samplelocations/api/locations.py index abbce44..cb2da4f 100644 --- a/geodjango/samplelocations/api/locations.py +++ b/geodjango/samplelocations/api/locations.py @@ -1,17 +1,10 @@ -from ninja import Router, Schema +from ninja import Router from samplelocations.models import Location +from samplelocations.schemas import LocationSchema, NotFoundSchema from typing import List router = Router() -class NotFoundSchema(Schema): - detail: str - -class LocationSchema(Schema): - location_id: int - coordinates: str - date_created: str - @router.get("") def get_locations(request) -> List[LocationSchema]: """ diff --git a/geodjango/samplelocations/api/thing.py b/geodjango/samplelocations/api/thing.py index 7e94cfa..d4c7c0b 100644 --- a/geodjango/samplelocations/api/thing.py +++ b/geodjango/samplelocations/api/thing.py @@ -1,5 +1,6 @@ -from ninja import Router, Schema +from ninja import Router from samplelocations.models import Thing, Location, Location_Thing_Junction +from samplelocations.schemas import FeatureCollection, NotFoundSchema, WellProperties, SpringProperties from django.contrib.gis.geos import Point from django.shortcuts import get_object_or_404 from django.forms.models import model_to_dict @@ -8,53 +9,6 @@ router = Router() -class NotFoundSchema(Schema): - detail: str - -class GeoJSONGeometry(Schema): - """ - Geometry schema for GeoJSON response. - """ - - type: str # e.g., "Point", "LineString", "Polygon" - coordinates: ( - List[float] | List[List[float]] | List[List[List[float]]] - ) # Supports Point, LineString, Polygon, etc. - -class Feature(Schema): - type: str = "Feature" - geometry: GeoJSONGeometry - -class BaseProperties(Schema): - thing_id: int - name: str - thing_type: str - release_status: bool - date_created: str - - -class WellProperties(BaseProperties): - well_depth_ft: float | None = None - hole_depth_ft: float | None = None - casing_diameter_ft: float | None = None - casing_depth_ft: float | None = None - casing_description: str | None = None - construction_notes: str | None = None - -class SpringProperties(BaseProperties): - spring_type: str | None = None - -class WellFeature(Feature): - properties: WellProperties - -class SpringFeature(Feature): - properties: SpringProperties - -class FeatureCollection(Schema): - type: str = "FeatureCollection" - features: List = [] # can be WellFeature or SpringFeature. Specifying a union of both types makes the schema include unrelated fields, which is not desired. - - def get_things(thing_id: int | None = None) -> List[Thing]: """ Retrieve all things or a specific thing by ID. diff --git a/geodjango/samplelocations/schemas.py b/geodjango/samplelocations/schemas.py new file mode 100644 index 0000000..2616439 --- /dev/null +++ b/geodjango/samplelocations/schemas.py @@ -0,0 +1,60 @@ +from ninja import Schema +from typing import List + +# ========== General Schemas ========== + +class NotFoundSchema(Schema): + detail: str + +class GeoJSONGeometry(Schema): + """ + Geometry schema for GeoJSON response. + """ + + type: str # e.g., "Point", "LineString", "Polygon" + coordinates: ( + List[float] | List[List[float]] | List[List[List[float]]] + ) # Supports Point, LineString, Polygon, etc. + + +# ========== Thing Schemas ========== + +class Feature(Schema): + type: str = "Feature" + geometry: GeoJSONGeometry + +class BaseProperties(Schema): + thing_id: int + name: str + thing_type: str + release_status: bool + date_created: str + + +class WellProperties(BaseProperties): + well_depth_ft: float | None = None + hole_depth_ft: float | None = None + casing_diameter_ft: float | None = None + casing_depth_ft: float | None = None + casing_description: str | None = None + construction_notes: str | None = None + +class SpringProperties(BaseProperties): + spring_type: str | None = None + +class WellFeature(Feature): + properties: WellProperties + +class SpringFeature(Feature): + properties: SpringProperties + +class FeatureCollection(Schema): + type: str = "FeatureCollection" + features: List = [] # can be WellFeature or SpringFeature. Specifying a union of both types makes the schema include unrelated fields, which is not desired. + +# ========== Location Schemas ========== + +class LocationSchema(Schema): + location_id: int + coordinates: str + date_created: str \ No newline at end of file From fe090b0ee669afb1dcb5d8b6e5a627f64dc0abf5 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Thu, 31 Jul 2025 09:08:47 -0600 Subject: [PATCH 57/57] fix: remove uneeded lines --- geodjango/samplelocations/api/locations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geodjango/samplelocations/api/locations.py b/geodjango/samplelocations/api/locations.py index cb2da4f..ed56c7c 100644 --- a/geodjango/samplelocations/api/locations.py +++ b/geodjango/samplelocations/api/locations.py @@ -3,7 +3,7 @@ from samplelocations.schemas import LocationSchema, NotFoundSchema from typing import List -router = Router() +router = Router() @router.get("") def get_locations(request) -> List[LocationSchema]: