Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
670737e
feat: import mixins into `Thing` model
ksmuczynski Sep 29, 2025
362ca0d
reformat: updated fields to comform to SQLAlchemy 2.0 notation. Reorg…
ksmuczynski Sep 29, 2025
c53bc5d
reformat: rename code sections for clarity
ksmuczynski Sep 29, 2025
c13129a
reformat: move well-related and spring-related columns under the `col…
ksmuczynski Sep 29, 2025
431884b
feat: add versioning to Thing table
ksmuczynski Sep 29, 2025
8c68fa0
refactor: add descriptions to relationships in `Thing` model
ksmuczynski Sep 30, 2025
602ae6a
refactor: update relationships in `Thing` and `Group` tables.
ksmuczynski Sep 30, 2025
46333ae
feat: add `thing` relationship to `WellScreen` model
ksmuczynski Sep 30, 2025
af284d6
feat: add `screens` relationship to `Thing` model
ksmuczynski Sep 30, 2025
f6a5460
refactor: update `screens` relationship to `Thing` model
ksmuczynski Sep 30, 2025
36a8f43
refactor: add 'passive_deletes=True' parameter to necessary relations…
ksmuczynski Sep 30, 2025
b494e4b
refactor: add 'ondelete=CASCADE' to thing_id foreign key
ksmuczynski Sep 30, 2025
c9ad45f
refactor: add 'passive_deletes=True' to `asset_associations` relation…
ksmuczynski Sep 30, 2025
1f01dcc
refactor: move `ondelete` parameter into foreign key constraint in De…
ksmuczynski Sep 30, 2025
2c96c0c
refactor: add TYPE_CHECKING to `Contact` model to mitigate circular r…
ksmuczynski Sep 30, 2025
6106c68
feat: add `first_visit_date` field to the `Thing` model
ksmuczynski Sep 30, 2025
98a0d64
refactor: rename `well_type` field to `well_purpose` as decided with …
ksmuczynski Sep 30, 2025
94bd103
feat: add `first_visit_date` field to applicable tests in `test.thing…
ksmuczynski Sep 30, 2025
c18a341
feat: add `first_visit_date` field to `tests/conftest.py`
ksmuczynski Sep 30, 2025
64b2cba
refactor: correct polymorphic parent relationship on `Permission` tab…
ksmuczynski Oct 1, 2025
0995492
fix(models): Correct primaryjoin condition in `PermissionMixin` and `…
ksmuczynski Oct 1, 2025
680ac5d
feat: add `permission` and `status` history models to `db/__init__.py`
ksmuczynski Oct 1, 2025
275ae12
fix(models): Use string-based join in Permission model
ksmuczynski Oct 1, 2025
c3650f3
fix(models): Correct ForeignKey reference for `contact_id` in `Permis…
ksmuczynski Oct 1, 2025
01127a3
feat: Add `Permission` relationship to `Contact` table
ksmuczynski Oct 1, 2025
e9bedf3
refactor: Add 'cascade="all, delete, delete-orphan"' to phone, emails…
ksmuczynski Oct 1, 2025
dc93b6a
refactor: reorganize `Contact` model
ksmuczynski Oct 1, 2025
dd0c927
fix(models): Explicitly mark foreign keys in polymorphic joins
ksmuczynski Oct 1, 2025
57c5500
feat(models): Update column name in the `search_vector`.
ksmuczynski Oct 1, 2025
f5ea3b4
fix(model): resolve errors to allow tests to run without issue.
ksmuczynski Oct 1, 2025
cfe8829
refactor: update instance of `well_type` to `well_purpose`.
ksmuczynski Oct 1, 2025
d370f3b
refactor: remove `tkinter` import
ksmuczynski Oct 1, 2025
a2e781a
refactor (schema): update data type
ksmuczynski Oct 2, 2025
c560d13
refactor (schema): update `first_visit_date` field to be required (no…
ksmuczynski Oct 2, 2025
b8fff0e
refactor (model): update relationship name
ksmuczynski Oct 2, 2025
8699695
refactor (model): remove # noqa: F821 calls
ksmuczynski Oct 2, 2025
a727f74
refactor: update instances of `FieldEventContactAssociation` to `Fie…
ksmuczynski Oct 2, 2025
d08c565
fix: resolve foreign key reference
ksmuczynski Oct 2, 2025
440d639
refactor: update data type
ksmuczynski Oct 3, 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
4 changes: 2 additions & 2 deletions alembic/versions/66ac1af4ba69_initial_migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ def upgrade() -> None:
# sa.Column('spring_type', sa.String(length=100), nullable=True),
# sa.Column('well_depth', sa.Float(), nullable=True),
# sa.Column('hole_depth', sa.Float(), nullable=True),
# sa.Column('well_type', sa.String(length=100), nullable=True),
# sa.Column('well_purpose', sa.String(length=100), nullable=True),
# sa.Column('well_casing_diameter', sa.Float(), nullable=True),
# sa.Column('well_casing_depth', sa.Float(), nullable=True),
# sa.Column('well_casing_description', sa.String(length=50), nullable=True),
Expand All @@ -216,7 +216,7 @@ def upgrade() -> None:
# sa.ForeignKeyConstraint(['release_status'], ['lexicon_term.term'], ),
# sa.ForeignKeyConstraint(['spring_type'], ['lexicon_term.term'], ),
# sa.ForeignKeyConstraint(['thing_type'], ['lexicon_term.term'], ),
# sa.ForeignKeyConstraint(['well_type'], ['lexicon_term.term'], ),
# sa.ForeignKeyConstraint(['well_purpose'], ['lexicon_term.term'], ),
# sa.PrimaryKeyConstraint('id')
# )
# op.create_index('ix_thing_search_vector', 'thing', ['search_vector'], unique=False, postgresql_using='gin')
Expand Down
2 changes: 1 addition & 1 deletion api/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def make_well_response(thing: Thing) -> dict:
"Wells",
thing,
{
"well_type": thing.well_type,
"well_purpose": thing.well_purpose,
"well_depth": thing.well_depth,
"hole_depth": thing.hole_depth,
},
Expand Down
10 changes: 5 additions & 5 deletions core/lexicon.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
{"name": "thing_type", "description": null},
{"name": "unit", "description": null},
{"name": "vertical_datum", "description": null},
{"name": "well_type", "description": null}],
{"name": "well_purpose", "description": null}],
"terms": [
{"categories": ["qc_type"], "term": "Normal", "definition": "The primary environmental sample collected from the well, spring, or soil boring."},
{"categories": ["qc_type"], "term": "Duplicate", "definition": "A second, independent sample collected at the same location, at the same time, and in the same manner as the normal sample. This sample is sent to the primary laboratory."},
Expand Down Expand Up @@ -431,10 +431,10 @@
{"categories": ["spring_type"], "term": "Perennial", "definition": "perennial spring"},
{"categories": ["spring_type"], "term": "Thermal", "definition": "thermal spring"},
{"categories": ["spring_type"], "term": "Mineral", "definition": "mineral spring"},
{"categories": ["well_type"], "term": "Exploration", "definition": "Exploration well"},
{"categories": ["well_type"], "term": "Monitoring", "definition": "Monitoring"},
{"categories": ["well_type"], "term": "Production", "definition": "Production"},
{"categories": ["well_type"], "term": "Injection", "definition": "Injection"},
{"categories": ["well_purpose"], "term": "Exploration", "definition": "Exploration well"},
{"categories": ["well_purpose"], "term": "Monitoring", "definition": "Monitoring"},
{"categories": ["well_purpose"], "term": "Production", "definition": "Production"},
{"categories": ["well_purpose"], "term": "Injection", "definition": "Injection"},
{"categories": ["casing_material"], "term": "PVC", "definition": "Polyvinyl Chloride"},
{"categories": ["casing_material"], "term": "Steel", "definition": "Steel"},
{"categories": ["casing_material"], "term": "Concrete", "definition": "Concrete"},
Expand Down
2 changes: 2 additions & 0 deletions db/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@
from db.lexicon import *
from db.location import *
from db.observation import *
from db.permission import *
from db.publication import *
from db.sample import *
from db.sensor import *
from db.status_history import *
from db.thing import *

from sqlalchemy import (
Expand Down
4 changes: 2 additions & 2 deletions db/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ def status_history(self):
# One-to-Many polymorphic relationship
return relationship(
"StatusHistory",
primaryjoin=f"and_({self.__name__}.{self.__name__.lower()}_id==StatusHistory.statusable_id, "
primaryjoin=f"and_({self.__name__}.id==foreign(StatusHistory.statusable_id), "
f"StatusHistory.statusable_type=='{self.__name__}')",
cascade="all, delete-orphan",
lazy="selectin",
Expand All @@ -203,7 +203,7 @@ def permissions(self):
# One-to-Many polymorphic relationship
return relationship(
"Permission",
primaryjoin=f"and_({self.__name__}.{self.__name__.lower()}_id==Permission.permissible_id, "
primaryjoin=f"and_({self.__name__}.id==foreign(Permission.permissible_id), "
f"Permission.permissible_type=='{self.__name__}')",
lazy="selectin",
viewonly=True,
Expand Down
82 changes: 51 additions & 31 deletions db/contact.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,17 @@
from sqlalchemy.ext.associationproxy import association_proxy, AssociationProxy
from sqlalchemy.orm import relationship, Mapped, mapped_column
from sqlalchemy_utils import TSVectorType
from typing import List
from typing import List, TYPE_CHECKING

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

if TYPE_CHECKING:
from db.thing import Thing
from db.field import FieldEvent
from db.field import FieldEventParticipants
from db.permission import Permission
from db.publication import AuthorContactAssociation, Author


class ThingContactAssociation(Base, AutoBaseMixin):
thing_id: Mapped[int] = mapped_column(
Expand All @@ -30,8 +37,12 @@ class ThingContactAssociation(Base, AutoBaseMixin):
ForeignKey("contact.id", ondelete="CASCADE"), nullable=False
)

contact: Mapped[List["Contact"]] = relationship("Contact")
thing: Mapped[List["Thing"]] = relationship("Thing") # noqa: F821
contact: Mapped["Contact"] = relationship(
"Contact", back_populates="thing_associations"
)
thing: Mapped["Thing"] = relationship(
"Thing", back_populates="contact_associations"
) # noqa: F821
Comment thread
ksmuczynski marked this conversation as resolved.


class Contact(Base, AutoBaseMixin, ReleaseMixin):
Expand All @@ -41,53 +52,62 @@ class Contact(Base, AutoBaseMixin, ReleaseMixin):
contact_type: Mapped[str] = lexicon_term(nullable=False)
nma_pk_owners: Mapped[str] = mapped_column(String(100), nullable=True)

# --- Relationships ---
# One-To-Many: A Contact can have many phone numbers.
phones: Mapped[List["Phone"]] = relationship(
"Phone", back_populates="contact", passive_deletes=True
"Phone", back_populates="contact", cascade="all, delete, delete-orphan"
)
# One-To-Many: A Contact can have many email addresses.
emails: Mapped[List["Email"]] = relationship(
"Email", back_populates="contact", passive_deletes=True
"Email", back_populates="contact", cascade="all, delete, delete-orphan"
)
# One-To-Many: A Contact can have many addresses.
addresses: Mapped[List["Address"]] = relationship(
"Address", back_populates="contact", passive_deletes=True
)

search_vector: Mapped[TSVectorType] = mapped_column(
TSVectorType("name", "role", "organization", "nma_pk_owners")
"Address", back_populates="contact", cascade="all, delete, delete-orphan"
)

author_associations: Mapped[List["AuthorContactAssociation"]] = ( # noqa: F821
relationship(
"AuthorContactAssociation",
back_populates="contact",
cascade="all, delete-orphan",
)
# One-To-Many: A Contact can grant many Permissions.
permissions: Mapped[List["Permission"]] = relationship(
"Permission", back_populates="contact", cascade="all, delete, delete-orphan"
)
authors: AssociationProxy[list["Author"]] = association_proxy( # noqa: F821
"author_associations", "author"
# One-To-Many: A Contact can be associated with many Authors (in Publications).
author_associations: Mapped[List["AuthorContactAssociation"]] = relationship(
"AuthorContactAssociation",
back_populates="contact",
cascade="all, delete-orphan",
)
# One-To-Many: A Contact can be associated with many Things.
thing_associations: Mapped[List["ThingContactAssociation"]] = relationship(
"ThingContactAssociation",
back_populates="contact",
cascade="all, delete-orphan",
)
# One-To-Many: A Contact can participate in many Field Events.
field_event_participants: Mapped[list["FieldEventParticipants"]] = relationship(
"FieldEventParticipants",
back_populates="contact",
cascade="all, delete-orphan",
passive_deletes=True,
)
things: AssociationProxy[list["Thing"]] = association_proxy( # noqa: F821

# --- Association Proxies ---
# Proxy to directly access the Author objects associated with this Contact
authors: AssociationProxy[list["Author"]] = association_proxy(
"author_associations", "author"
)

# Proxy to directly access the Thing objects associated with this Contact
things: AssociationProxy[list["Thing"]] = association_proxy(
"thing_associations", "thing"
)

# Proxy to directly access the FieldEvent objects in which this Contact participated.
# fmt: off
field_event_contact_associations: Mapped[list["FieldEventContactAssociation"]] = ( # noqa: F821
relationship(
"FieldEventContactAssociation",
back_populates="contact",
cascade="all, delete-orphan",
passive_deletes=True,
)
field_events: AssociationProxy[list["FieldEvent"]] = association_proxy(
"field_event_participants", "field_event"
)
# fmt: on
field_events: AssociationProxy[list["FieldEvent"]] = ( # noqa: F821
association_proxy("field_event_contact_associations", "field_event")

# Full-Text Search Vector
search_vector: Mapped[TSVectorType] = mapped_column(
TSVectorType("name", "role", "organization", "nma_pk_owners")
)


Expand Down
4 changes: 3 additions & 1 deletion db/deployment.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ class Deployment(Base, AutoBaseMixin, ReleaseMixin):

# --- Foreign Keys ---
thing_id: Mapped[int] = mapped_column(
Integer, ForeignKey("thing.id"), nullable=False
Integer,
ForeignKey("thing.id", ondelete="CASCADE"),
nullable=False,
)
sensor_id: Mapped[int] = mapped_column(
Integer, ForeignKey("sensor.id"), nullable=False
Expand Down
20 changes: 9 additions & 11 deletions db/field.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from db.contact import Contact


class FieldEventContactAssociation(Base, AutoBaseMixin, ReleaseMixin):
class FieldEventParticipants(Base, AutoBaseMixin, ReleaseMixin):
"""
This association table is to create a many-to-many relationship between
FieldEvent and Contact. These are participants in the field event.
Expand All @@ -32,10 +32,10 @@ class FieldEventContactAssociation(Base, AutoBaseMixin, ReleaseMixin):

# --- Relationships ---
field_event: Mapped["FieldEvent"] = relationship(
"FieldEvent", back_populates="field_event_contact_associations"
"FieldEvent", back_populates="field_event_participants"
)
contact: Mapped["Contact"] = relationship( # noqa: F821
"Contact", back_populates="field_event_contact_associations"
"Contact", back_populates="field_event_participants"
)

# map associated contacts to samples to restrict the people who could have
Expand Down Expand Up @@ -86,19 +86,17 @@ class FieldEvent(Base, AutoBaseMixin, ReleaseMixin):
field_activities: Mapped[list["FieldActivity"]] = relationship(
"FieldActivity", back_populates="field_event"
)
field_event_contact_associations: Mapped[list["FieldEventContactAssociation"]] = (
relationship(
"FieldEventContactAssociation",
back_populates="field_event",
cascade="all, delete-orphan",
passive_deletes=True,
)
field_event_participants: Mapped[list["FieldEventParticipants"]] = relationship(
"FieldEventParticipants",
back_populates="field_event",
cascade="all, delete-orphan",
passive_deletes=True,
)

# --- Association Proxies ---
# Proxy to directly access the Contact objects participating in this event.
contacts: AssociationProxy[list["Contact"]] = association_proxy( # noqa: F821
"field_event_contact_associations", "contact"
"field_event_participants", "contact"
)


Expand Down
28 changes: 25 additions & 3 deletions db/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,21 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# ===============================================================================
from typing import Optional
from typing import Optional, List, TYPE_CHECKING

from geoalchemy2 import Geometry, WKBElement
from sqlalchemy import String, Integer, ForeignKey
from sqlalchemy.orm import relationship, Mapped
from sqlalchemy.testing.schema import mapped_column
from sqlalchemy.ext.associationproxy import association_proxy, AssociationProxy

from constants import SRID_WGS84
from db.base import Base, AutoBaseMixin, ReleaseMixin

if TYPE_CHECKING:
from db.group import GroupThingAssociation
from db.thing import Thing


class Group(Base, AutoBaseMixin, ReleaseMixin):
# --- Column Definitions ---
Expand All @@ -37,8 +42,18 @@ class Group(Base, AutoBaseMixin, ReleaseMixin):
Integer, ForeignKey("group.id", ondelete="CASCADE"), nullable=True
)

# --- Relationship Definitions ---
things = relationship("Thing", secondary="group_thing_association")
# --- Relationships ---
# One-To-Many (Association Object): A Group has many members (Things).
# The GroupThingAssociation table manages this many-to-many relationship.
thing_associations: Mapped[List["GroupThingAssociation"]] = relationship(
"GroupThingAssociation", back_populates="group", cascade="all, delete-orphan"
)

# --- Association Proxies ---
# Many-To-Many Proxy: Provides direct access to the Thing objects in this group.
things: AssociationProxy[List["Thing"]] = association_proxy(
"thing_associations", "thing"
)


class GroupThingAssociation(Base, AutoBaseMixin):
Expand All @@ -49,5 +64,12 @@ class GroupThingAssociation(Base, AutoBaseMixin):
Integer, ForeignKey("thing.id", ondelete="CASCADE"), nullable=False
)

# --- Relationship Definitions ---
# Many-To-One: This association links to one Thing.
thing: Mapped["Thing"] = relationship("Thing", back_populates="group_associations")

# Many-To-One: This association links to one Group.
group: Mapped["Group"] = relationship("Group", back_populates="thing_associations")


# ============= EOF =============================================
16 changes: 6 additions & 10 deletions db/permission.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,8 @@
Boolean,
Date,
Text,
and_,
)
from sqlalchemy.orm import relationship, Mapped, mapped_column, foreign
from sqlalchemy.orm import relationship, Mapped, mapped_column

from db.base import Base, AutoBaseMixin, ReleaseMixin

Expand All @@ -37,7 +36,7 @@ class Permission(Base, AutoBaseMixin, ReleaseMixin):

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

# --- Columns ---
Expand All @@ -62,17 +61,14 @@ class Permission(Base, AutoBaseMixin, ReleaseMixin):
# They tell SQLAlchemy exactly how to find the specific parent record for a given child.
_thing_target: Mapped["Thing"] = relationship(
"Thing",
primaryjoin=and_(
foreign(permissible_id) == Thing.thing_id, permissible_type == "Thing"
),
primaryjoin="and_(foreign(Permission.permissible_id) == Thing.id, "
"Permission.permissible_type == 'Thing')",
viewonly=True,
)
_location_target: Mapped["Location"] = relationship(
"Location",
primaryjoin=and_(
foreign(permissible_id) == Location.location_id,
permissible_type == "Location",
),
primaryjoin="and_(foreign(Permission.permissible_id) == Location.id, "
"Permission.permissible_type == 'Location')",
viewonly=True,
)

Expand Down
6 changes: 3 additions & 3 deletions db/sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from db.field import FieldEvent, FieldActivity, FieldEventContactAssociation
from db.field import FieldEvent, FieldActivity, FieldEventParticipants
from db.thing import Thing
from db.contact import Contact
from db.observation import Observation
Expand All @@ -51,7 +51,7 @@ class Sample(Base, AutoBaseMixin, ReleaseMixin):
)

field_event_contact_id: Mapped[str] = mapped_column(
ForeignKey("field_event_contact_association.id"), nullable=True
ForeignKey("field_event_participants.id"), nullable=True
)

# --- Columns ---
Expand Down Expand Up @@ -86,7 +86,7 @@ class Sample(Base, AutoBaseMixin, ReleaseMixin):

# --- Relationship Definitions ---
field_activity: Mapped["FieldActivity"] = relationship(back_populates="samples")
field_event_contact: Mapped["FieldEventContactAssociation"] = relationship(
field_event_contact: Mapped["FieldEventParticipants"] = relationship(
back_populates="samples"
)

Expand Down
Loading
Loading