Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
13f2a45
refactor: rename Permission -> PermissionHistory | move mixin to same…
jacob-a-brown Nov 13, 2025
6e036e5
feat: add permission types to lexicon
jacob-a-brown Nov 13, 2025
091637c
refactor: import PermissionHistoryMixin from correct dir
jacob-a-brown Nov 13, 2025
71d8534
fix: fix imports for newly renamed PermissionHistory
jacob-a-brown Nov 13, 2025
825ec4a
feat: add permission_type to lexicon
jacob-a-brown Nov 13, 2025
8ed0ce4
feat: update util to correspond with bdms 221
jacob-a-brown Nov 13, 2025
3b36c49
fix: sync bdms 221/227 for util functions
jacob-a-brown Nov 13, 2025
1657bb9
feat: implement permissions
jacob-a-brown Nov 13, 2025
307f47c
feat: add is_suitable_for_datalogger to thing
jacob-a-brown Nov 13, 2025
38e86b2
Merge branch 'bdms-227' into bdms-227-jab-updates-to-pass-tests
jacob-a-brown Nov 19, 2025
b4c8beb
fix: fix artifacts from merge conflicts
jacob-a-brown Nov 19, 2025
2c33a79
feat: implement data source tests for well additional information
jacob-a-brown Nov 19, 2025
8ef2592
fix: remove outdated notes
jacob-a-brown Nov 19, 2025
722425f
feat: implement origin_type
jacob-a-brown Nov 20, 2025
827652a
Merge branch 'bdms-227' into bdms-227-jab-updates-to-pass-tests
jacob-a-brown Nov 24, 2025
1342b8d
fix: fix __table_args__ for aquifer and geology
jacob-a-brown Nov 24, 2025
88419b7
fix: add geologic_formation relation to WellScreen
jacob-a-brown Nov 24, 2025
1a23dff
fix: add missing comma
jacob-a-brown Nov 24, 2025
d82b163
fix: use BaseResponseModel for GeoJSONProperties in location schema
jacob-a-brown Nov 24, 2025
20af430
fix: include baseresponseinfo in location geojson properties
jacob-a-brown Nov 24, 2025
5778b74
feat: implement aquifer_systems in thing response
jacob-a-brown Nov 24, 2025
c4f9de0
feat: fix aquifer systems test
jacob-a-brown Nov 24, 2025
d3abd83
fix: return aquifer system and types in dict
jacob-a-brown Nov 24, 2025
b4cd84e
feat: implement geologic_formations from feature files
jacob-a-brown Nov 24, 2025
0994bec
refactor: clean up permission history testing data
jacob-a-brown Nov 25, 2025
3041102
fix: use origin_type to retrieve well depth source
jacob-a-brown Nov 25, 2025
5e8fdad
refactor: use __tabename__ for polymorphic mixins
jacob-a-brown Nov 25, 2025
95ff8c0
fix: use __tablename__ for NotesMixin add_note
jacob-a-brown Nov 25, 2025
bdff12e
refactor: use sets in feature tests for comparison
jacob-a-brown Nov 26, 2025
19d736a
refactor: return permission history records for a well
jacob-a-brown Nov 26, 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
3 changes: 2 additions & 1 deletion core/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,12 @@
Vertical_datum: type[Enum] = build_enum_from_lexicon_category("vertical_datum")
ScreenType: type[Enum] = build_enum_from_lexicon_category("screen_type")
SensorType: type[Enum] = build_enum_from_lexicon_category("sensor_type")
WellPumpType: type[Enum] = build_enum_from_lexicon_category("well_pump_type")
PermissionType: type[Enum] = build_enum_from_lexicon_category("permission_type")
GroupType: type[Enum] = build_enum_from_lexicon_category("group_type")
MonitoringFrequency: type[Enum] = build_enum_from_lexicon_category(
"monitoring_frequency"
)
WellPumpType: type[Enum] = build_enum_from_lexicon_category("well_pump_type")
AquiferType: type[Enum] = build_enum_from_lexicon_category("aquifer_type")
GeographicScale: type[Enum] = build_enum_from_lexicon_category("geographic_scale")
Lithology: type[Enum] = build_enum_from_lexicon_category("lithology")
Expand Down
7 changes: 5 additions & 2 deletions core/lexicon.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@
{"name": "status_type", "description": null},
{"name": "status_value", "description": null},
{"name": "origin_source", "description": null},
{"name": "status_value", "description": null},
{"name": "well_pump_type", "description": null},
{"name": "permission_type", "description": null},
{"name": "formation_code", "description": null},
{"name": "lithology", "description": null}
],
Expand Down Expand Up @@ -1104,6 +1104,9 @@
{"categories": ["note_type"], "term": "Historical", "definition": "Historical information or context about the well or location."},
{"categories": ["note_type"], "term": "Other", "definition": "Other types of notes that do not fit into the predefined categories."},
{"categories": ["note_type"], "term": "Water", "definition": "Water bearing zone information and other info from ose reports"},
{"categories": ["note_type"], "term": "Measuring", "definition": "Notes about measuring/visiting the well, on Access form"}
{"categories": ["note_type"], "term": "Measuring", "definition": "Notes about measuring/visiting the well, on Access form"},
{"categories": ["permission_type"], "term": "Water Level Sample", "definition": "Permissions for taking water level samples"},
{"categories": ["permission_type"], "term": "Water Chemistry Sample", "definition": "Permissions for water taking chemistry samples"},
{"categories": ["permission_type"], "term": "Datalogger Installation", "definition": "Permissions for installing dataloggers"}
]
}
2 changes: 1 addition & 1 deletion db/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from db.notes import *
from db.observation import *
from db.parameter import *
from db.permission import *
from db.permission_history import *
from db.publication import *
from db.regulatory_limit import *
from db.sample import *
Expand Down
2 changes: 1 addition & 1 deletion db/aquifer_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,4 @@ class AquiferSystem(Base, AutoBaseMixin, ReleaseMixin):
)

# --- Table Arguments ---
__table_args__ = Index("ix_aquifersystem_name", "name")
__table_args__ = (Index("ix_aquifersystem_name", "name"),)
20 changes: 0 additions & 20 deletions db/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@
declared_attr,
Mapped,
mapped_column,
relationship,
)
from sqlalchemy_continuum import make_versioned
from sqlalchemy_searchable import make_searchable
Expand Down Expand Up @@ -179,25 +178,6 @@ def properties(self):
# ============= Polymorphic Helper Mixins =============================================


class PermissionMixin:
"""
Mixin for models that can have permissions (e.g., Thing, Location).
It automatically creates a polymorphic One-to-Many relationship to the
Permission table.
"""

@declared_attr
def permissions(self):
# One-to-Many polymorphic relationship
return relationship(
"Permission",
primaryjoin=f"and_({self.__name__}.id==foreign(Permission.permissible_id), "
f"Permission.permissible_type=='{self.__name__}')",
lazy="selectin",
viewonly=True,
)


class User(Base):
"""Represents a user in the system."""

Expand Down
8 changes: 5 additions & 3 deletions db/contact.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from db.field import FieldEventParticipant, FieldEvent
from db.thing import Thing
from db.publication import Author, AuthorContactAssociation
from db.permission import Permission
from db.permission_history import PermissionHistory


class ThingContactAssociation(Base, AutoBaseMixin):
Expand Down Expand Up @@ -74,8 +74,10 @@ class Contact(Base, AutoBaseMixin, ReleaseMixin):
)

# One-To-Many: A Contact can grant many Permissions.
permissions: Mapped[List["Permission"]] = relationship(
"Permission", back_populates="contact", cascade="all, delete, delete-orphan"
permissions: Mapped[List["PermissionHistory"]] = relationship(
"PermissionHistory",
back_populates="contact",
cascade="all, delete, delete-orphan",
)
# One-To-Many: A Contact can be associated with many Authors (in Publications).
author_associations: Mapped[List["AuthorContactAssociation"]] = relationship(
Expand Down
12 changes: 8 additions & 4 deletions db/data_provenance.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from sqlalchemy import Integer, Index, and_
from sqlalchemy.orm import relationship, Mapped, mapped_column, declared_attr, foreign

from db.base import Base, AutoBaseMixin, ReleaseMixin, pascal_to_snake
from db.base import Base, AutoBaseMixin, ReleaseMixin

from db import lexicon_term

Expand Down Expand Up @@ -53,9 +53,13 @@ class DataProvenance(AutoBaseMixin, ReleaseMixin, Base):
)
# Values from the following NMAquifer tables are included as `origin_source` terms in the lexicon:
# 'LU_DataSource', 'LU_Depth_CompletionSource'.
origin_source: Mapped[str] = lexicon_term(
origin_type: Mapped[str] = lexicon_term(
nullable=True,
comment="Indicates the origin source of the data (e.g'Driller's Log', 'Well Report'.",
comment="Indicates the type of origin the data (e.g'Driller's Log', 'Well Report'.",
)
origin_source: Mapped[str] = mapped_column(
nullable=True,
comment="The specific source of the data (e.g., 'J. Brown Thesis, \"I like APIs\", Pomona College, 1994').",
)
# Values from the following NMAquifer tables are included as `collection_method` terms in the lexicon:
# 'LU_AltitudeMethod','LU_CoordinateMethod'.
Expand Down Expand Up @@ -116,7 +120,7 @@ def data_provenance(cls):
"DataProvenance",
primaryjoin=and_(
cls.id == foreign(DataProvenance.target_id),
DataProvenance.target_table == pascal_to_snake(cls.__name__),
DataProvenance.target_table == cls.__tablename__,
),
lazy="selectin",
viewonly=True,
Expand Down
7 changes: 2 additions & 5 deletions db/geologic_formation.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class GeologicFormation(Base, AutoBaseMixin, ReleaseMixin):
)
)
# One-To-Many: A GeologicFormation can have many physical WellScreens installed in it.
screens: Mapped[List["WellScreen"]] = relationship(
well_screens: Mapped[List["WellScreen"]] = relationship(
"WellScreen", back_populates="geologic_formation", passive_deletes=True
)

Expand All @@ -79,7 +79,4 @@ class GeologicFormation(Base, AutoBaseMixin, ReleaseMixin):
things: AssociationProxy["Thing"] = association_proxy("thing_associations", "thing")

# --- Table Arguments ---
__table_args__ = (
Index("ix_geologicformation_name", "name"),
Index("ix_geologicformation_code", "code"),
)
__table_args__ = (Index("ix_geologicformation_formation_code", "formation_code"),)
4 changes: 2 additions & 2 deletions db/notes.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def notes(cls):
"Notes",
primaryjoin=and_(
cls.id == foreign(Notes.target_id),
Notes.target_table == cls.__name__,
Notes.target_table == cls.__tablename__,
),
cascade="all, delete-orphan",
lazy="selectin",
Expand All @@ -120,7 +120,7 @@ def add_note(
content=content,
note_type=note_type,
target_id=self.id,
target_table=self.__class__.__name__,
target_table=self.__class__.__tablename__,
release_status=release_status,
)

Expand Down
82 changes: 0 additions & 82 deletions db/permission.py

This file was deleted.

96 changes: 96 additions & 0 deletions db/permission_history.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""
models/permission.py

This model defines the `Permission` table, a polymorphic table that tracks
all legal and administrative agreements related to site access and activity.
Its purpose is to track who granted permission, what activities they authorized,
which entity the permission applies to, and for what period of time.
"""

from typing import TYPE_CHECKING
from datetime import date
from sqlalchemy import Integer, ForeignKey, String, and_
from sqlalchemy.orm import relationship, Mapped, mapped_column, declared_attr, foreign

from db.base import Base, AutoBaseMixin, ReleaseMixin, lexicon_term


if TYPE_CHECKING:
from db.contact import Contact
from db.thing import Thing
from db.location import Location


class PermissionHistory(Base, AutoBaseMixin, ReleaseMixin):
"""
Represents a specific grant of permission from a Contact for a
specific entity (e.g., a Thing or Location).
"""

# --- Foreign Keys ---
contact_id: Mapped[int] = mapped_column(
Integer, ForeignKey("contact.id", ondelete="CASCADE"), nullable=False
)

# --- Columns ---
permission_type: Mapped[str] = lexicon_term(nullable=False)
permission_allowed: Mapped[bool] = mapped_column(nullable=False, default=False)
start_date: Mapped[date] = mapped_column(nullable=False)
end_date: Mapped[date] = mapped_column(nullable=True)
notes: Mapped[str] = mapped_column(nullable=True)

# --- Polymorphic Columns ---
target_id: Mapped[int] = mapped_column(nullable=False)
target_table: Mapped[str] = mapped_column(String(50), nullable=False)

# --- Relationships ---
# Many-To-One: A Permission is granted by one Contact.
contact: Mapped["Contact"] = relationship("Contact", back_populates="permissions")

# --- Polymorphic Parent Relationships (Internal) ---
# These are view-only relationships used by the 'target' property below.
# They tell SQLAlchemy exactly how to find the specific parent record for a given child.
_thing_target: Mapped["Thing"] = relationship(
"Thing",
primaryjoin="and_(foreign(PermissionHistory.target_id) == Thing.id, "
"PermissionHistory.target_table == 'thing')",
viewonly=True,
)
_location_target: Mapped["Location"] = relationship(
"Location",
primaryjoin="and_(foreign(PermissionHistory.target_id) == Location.id, "
"PermissionHistory.target_table == 'location')",
viewonly=True,
)

@property
def target(self):
"""
A generic property to get the parent object (Thing, Location, etc.).
This is useful for simplifying application code by providing a single,
consistent way to access the parent of a polymorphic record.
"""
return getattr(self, f"_{self.target_table}_target")


class PermissionHistoryMixin:
"""
Mixin for models that can have permissions (e.g., Thing, Location).
It automatically creates a polymorphic One-to-Many relationship to the
Permission table.
"""

@declared_attr
def permission_history(cls):
# One-to-Many polymorphic relationship
return relationship(
"PermissionHistory",
primaryjoin=(
and_(
cls.id == foreign(PermissionHistory.target_id),
PermissionHistory.target_table == cls.__tablename__,
)
),
lazy="selectin",
viewonly=True,
)
4 changes: 2 additions & 2 deletions db/status_history.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
)
from sqlalchemy.orm import Mapped, mapped_column, declared_attr, relationship, foreign

from db.base import Base, AutoBaseMixin, ReleaseMixin, lexicon_term, pascal_to_snake
from db.base import Base, AutoBaseMixin, ReleaseMixin, lexicon_term


class StatusHistory(Base, AutoBaseMixin, ReleaseMixin):
Expand Down Expand Up @@ -47,7 +47,7 @@ def status_history(cls):
"StatusHistory",
primaryjoin=and_(
cls.id == foreign(StatusHistory.target_id),
StatusHistory.target_table == pascal_to_snake(cls.__name__),
StatusHistory.target_table == cls.__tablename__,
),
cascade="all, delete-orphan",
lazy="selectin",
Expand Down
Loading
Loading