Skip to content
This repository was archived by the owner on Feb 7, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
069395f
merge conflict resolution
jacob-a-brown Jul 1, 2025
911206e
Merge branch 'pre-production' into dev_jab
jacob-a-brown Jul 8, 2025
e672a10
split routers into own files
jacob-a-brown Jul 8, 2025
11f9664
fix: remove tailing endpoint slashes
jacob-a-brown Jul 8, 2025
5d2c12b
fix: update README
jacob-a-brown Jul 8, 2025
723f118
fix: make Redoc default docs
jacob-a-brown Jul 8, 2025
d3d7490
style: update function name
jacob-a-brown Jul 8, 2025
c8465f8
style: update function name
jacob-a-brown Jul 8, 2025
1cc199e
fix: delete api.py
jacob-a-brown Jul 8, 2025
e7b7693
cleanup: remove outdate test files
jacob-a-brown Jul 8, 2025
ecf07a8
style: add __init__.py to tests
jacob-a-brown Jul 8, 2025
6a4def3
feat: first init for test_mvp.py
jacob-a-brown Jul 8, 2025
157f6b1
fix: remove old test.py
jacob-a-brown Jul 8, 2025
74f2436
feat: add sql alchemy poc mvp tests
jacob-a-brown Jul 8, 2025
894f8ac
test: set up mvp tests
jacob-a-brown Jul 8, 2025
8f22d99
Merge branch 'pre-production' into dev_jab
jacob-a-brown Jul 8, 2025
0a208f4
fix: assign test client to each test
jacob-a-brown Jul 8, 2025
633f881
fix: fix test client
jacob-a-brown Jul 8, 2025
a8d0b08
style: remove unecessary urls namespace
jacob-a-brown Jul 8, 2025
8b04c51
fix: utilize Django's self.assert...
jacob-a-brown Jul 8, 2025
06491e6
feat: add more mvp tests
jacob-a-brown Jul 9, 2025
fbdfc77
style: add crud file for all DB interactions
jacob-a-brown Jul 9, 2025
d143149
style: put BaseTestClass in __init__.py for use by all tests
jacob-a-brown Jul 9, 2025
3466f0a
test: create contact tests for mvp
jacob-a-brown Jul 9, 2025
d9dc861
fix: use thing_id instead of id
jacob-a-brown Jul 9, 2025
cc57a8b
test: scaffold geochronology test
jacob-a-brown Jul 9, 2025
7b5b9ff
test: move geospatial tests to test_location.py
jacob-a-brown Jul 9, 2025
18c0485
test: skip geothermal tests
jacob-a-brown Jul 9, 2025
bc6d722
test: scaffold group tests
jacob-a-brown Jul 9, 2025
c203039
test: use absolute path imports from root
jacob-a-brown Jul 9, 2025
3274169
style: use absolute imports for urls
jacob-a-brown Jul 9, 2025
551a7b1
test: scaffold lexicon tests
jacob-a-brown Jul 10, 2025
04079c0
fix: add : to end of class definition
jacob-a-brown Jul 10, 2025
37842b5
test: scaffold location tests
jacob-a-brown Jul 10, 2025
089be35
Create migration 0002
Jul 24, 2025
0e8c1a1
Merge remote-tracking branch 'origin/pre-production' into dev_kas
Jul 24, 2025
19a8ba1
refactor: Add `thing_type` field and choices for `thing_type` field.
Jul 24, 2025
9f7e395
refactor: Add fields specific to a WELL. Add fields specific to a SPR…
Jul 24, 2025
89c5467
refactor: Update __str__ method for Thing to include type and name
Jul 24, 2025
88dd1ae
refactor: Remove WellThing model- it has been merged into Thing model.
Jul 24, 2025
235f28f
refactor: Remove SpringThing model- it has been merged into Thing model.
Jul 24, 2025
12b4c90
refactor: De-register WellThing and SpringThing models
Jul 24, 2025
3afe9d0
fix: api now has its own modules
jacob-a-brown Jul 28, 2025
3c44f2a
feat: setup tests for things and locations
jacob-a-brown Jul 28, 2025
40291db
feat: implement thing feature collection
jacob-a-brown Jul 28, 2025
25d2cc3
fix: remove depricated models
jacob-a-brown Jul 28, 2025
26b17ab
setup thing endpoint and tests
jacob-a-brown Jul 28, 2025
5450006
Merge remote-tracking branch 'origin/dev_kas' into dev_jab
jacob-a-brown Jul 28, 2025
a76e653
fix: fix thing test endpoints
jacob-a-brown Jul 28, 2025
b302f33
fix: reset migrations for new models
jacob-a-brown Jul 28, 2025
08afa06
feat: implement thing endpoints and tests
jacob-a-brown Jul 29, 2025
272997c
feat: implement /location endpoint
jacob-a-brown Jul 29, 2025
ad6b9a7
refactor: Move ThingType choices outside of Thing model
Jul 30, 2025
1eb2d67
refactor: Add sample_matrix field to Sample model. Add sample matrix …
Jul 30, 2025
fb15065
style: Correct sample_matrix field formatting
Jul 30, 2025
266f88b
refactor: remove previous migrations
Jul 30, 2025
22204b9
refactor: Remove import of WellThing and SpringThing models
Jul 30, 2025
12d14bb
Perform new initial migration (2025-07-30)
Jul 30, 2025
44b1d33
Merge branch 'dev_kas' into dev_jab
jacob-a-brown Jul 30, 2025
d7ac245
fix: remove description column
jacob-a-brown Jul 30, 2025
fa9f511
fix: update tests for newly defined thing model
jacob-a-brown Jul 30, 2025
85f1356
refactor: better organize and use schemas
jacob-a-brown Jul 31, 2025
fe090b0
fix: remove uneeded lines
jacob-a-brown Jul 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/)
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
version: '3.9'

services:
db:
image: timescale/timescaledb-ha:pg17-all
Expand Down
7 changes: 5 additions & 2 deletions geodjango/geodjango/api.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from ninja import NinjaAPI
from ninja import NinjaAPI, Redoc

api = NinjaAPI()

api.add_router('/samplelocations/', 'samplelocations.api.router')
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'])
2 changes: 1 addition & 1 deletion geodjango/geodjango/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
18 changes: 9 additions & 9 deletions geodjango/samplelocations/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
Expand All @@ -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)
Expand Down
20 changes: 0 additions & 20 deletions geodjango/samplelocations/api.py

This file was deleted.

Empty file.
22 changes: 22 additions & 0 deletions geodjango/samplelocations/api/contacts.py
Original file line number Diff line number Diff line change
@@ -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 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}
Empty file.
38 changes: 38 additions & 0 deletions geodjango/samplelocations/api/locations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from ninja import Router
from samplelocations.models import Location
from samplelocations.schemas import LocationSchema, NotFoundSchema
from typing import List

router = Router()

@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
93 changes: 93 additions & 0 deletions geodjango/samplelocations/api/thing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
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
from django.http import HttpResponse
from typing import List, Tuple

router = Router()

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.
"""
"""
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:
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 = {
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [location.coordinate.x, location.coordinate.y, location.coordinate.z],
},
"properties": thing_properties.dict(),
}

features.append(feature)

response = FeatureCollection(
type="FeatureCollection",
features=features
)

return response

@router.get('')
def get_all_things(request):
"""
List all things.
"""
things = get_things()
response = construct_feature_collection(things)
return response

@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 == [None]:
return 404, {"detail": f"Thing with id {thing_id} not found"}
response = construct_feature_collection(thing)
return response
55 changes: 55 additions & 0 deletions geodjango/samplelocations/api/wells.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# from ninja import Router
# from ..models import Location, 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(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}

Loading