From d6466f9500fae754c34b5e07d796f84039f0eebc Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Thu, 18 Sep 2025 13:48:22 -0600 Subject: [PATCH 1/4] update: create new `Permission` model --- db/permission.py | 85 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 db/permission.py diff --git a/db/permission.py b/db/permission.py new file mode 100644 index 000000000..63c7e307d --- /dev/null +++ b/db/permission.py @@ -0,0 +1,85 @@ +""" +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 sqlalchemy import ( + Integer, + ForeignKey, + String, + Boolean, + Date, + Text, + and_, +) +from sqlalchemy.orm import relationship, Mapped, mapped_column, foreign + +from db.base import Base, AutoBaseMixin, ReleaseMixin + +if TYPE_CHECKING: + from db.contact import Contact + from db.thing import Thing + from db.location import Location + + +class Permission(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.contact_id"), nullable=False + ) + + # --- Columns --- + allow_sampling: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) + allow_installation: Mapped[bool] = mapped_column( + Boolean, nullable=False, default=False + ) + start_date: Mapped[Date] = mapped_column(Date, nullable=True) + end_date: Mapped[Date] = mapped_column(Date, nullable=True) + notes: Mapped[str] = mapped_column(Text, nullable=True) + + # --- Polymorphic Columns --- + permissible_id: Mapped[int] = mapped_column(Integer, nullable=False) + permissible_type: 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(permissible_id) == Thing.thing_id, permissible_type == "Thing" + ), + viewonly=True, + ) + _location_target: Mapped["Location"] = relationship( + "Location", + primaryjoin=and_( + foreign(permissible_id) == Location.location_id, + permissible_type == "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.permissible_type.lower()}_target") From 3eb3e41dccd680321233d25df834e9bbfb5dc7c1 Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Thu, 18 Sep 2025 14:54:29 -0600 Subject: [PATCH 2/4] update: add `PermissionMixin` to `db/base.py` to automatically create a polymorphic One-to-Many relationship to the Permission table. --- db/base.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/db/base.py b/db/base.py index 4da639a20..31ec9ecd4 100644 --- a/db/base.py +++ b/db/base.py @@ -29,7 +29,7 @@ - `ReleaseMixin`: Adds a release status column referencing the `lexicon_term` table. - `AuditMixin`: Adds standard audit columns (created_at, created_by, updated_at, updated_by). 5. A simple `User` model for tracking user information in audit columns. -6. Polymorphic helper mixins (`StatusHistoryMixin`, `NotesMixin`, `AttributionMixin`, etc.) +6. Polymorphic helper mixins (`StatusHistoryMixin`, `NotesMixin`, `AttributionMixin`, `PermissionMixin`.) which provide a clean, reusable way to add relationships to the polymorphic metadata tables. Any model that can have a status history (like Thing or Location) can simply inherit from the `StatusHistoryMixin` mixin. @@ -191,6 +191,25 @@ def status_history(self): ) +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__}.{self.__name__.lower()}_id==Permission.permissible_id, " + f"Permission.permissible_type=='{self.__name__}')", + lazy="selectin", + viewonly=True, + ) + + class User(Base): """Represents a user in the system.""" From a35f59153dfa61d90fadf9b46db6e2d578dd726e Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Fri, 19 Sep 2025 10:31:09 -0600 Subject: [PATCH 3/4] reformat: change `start_date` and `end_date` data types from Date to DateTime. --- db/permission.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/db/permission.py b/db/permission.py index 63c7e307d..d88adc083 100644 --- a/db/permission.py +++ b/db/permission.py @@ -14,14 +14,16 @@ ForeignKey, String, Boolean, - Date, Text, and_, + DateTime, ) from sqlalchemy.orm import relationship, Mapped, mapped_column, foreign from db.base import Base, AutoBaseMixin, ReleaseMixin +import datetime + if TYPE_CHECKING: from db.contact import Contact from db.thing import Thing @@ -44,8 +46,12 @@ class Permission(Base, AutoBaseMixin, ReleaseMixin): allow_installation: Mapped[bool] = mapped_column( Boolean, nullable=False, default=False ) - start_date: Mapped[Date] = mapped_column(Date, nullable=True) - end_date: Mapped[Date] = mapped_column(Date, nullable=True) + start_date: Mapped[datetime.datetime] = mapped_column( + DateTime(timezone=True), nullable=True + ) + end_date: Mapped[datetime.datetime] = mapped_column( + DateTime(timezone=True), nullable=True + ) notes: Mapped[str] = mapped_column(Text, nullable=True) # --- Polymorphic Columns --- From 082cd4f16273f6f8a28c13e7e94e38707dc223e5 Mon Sep 17 00:00:00 2001 From: ksmuczynski Date: Fri, 19 Sep 2025 11:19:26 -0600 Subject: [PATCH 4/4] reformat: change `start_date` and `end_date` data types back to Date since time is not important to capture here. --- db/permission.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/db/permission.py b/db/permission.py index d88adc083..93e688e86 100644 --- a/db/permission.py +++ b/db/permission.py @@ -14,15 +14,14 @@ ForeignKey, String, Boolean, + Date, Text, and_, - DateTime, ) from sqlalchemy.orm import relationship, Mapped, mapped_column, foreign from db.base import Base, AutoBaseMixin, ReleaseMixin -import datetime if TYPE_CHECKING: from db.contact import Contact @@ -46,12 +45,8 @@ class Permission(Base, AutoBaseMixin, ReleaseMixin): allow_installation: Mapped[bool] = mapped_column( Boolean, nullable=False, default=False ) - start_date: Mapped[datetime.datetime] = mapped_column( - DateTime(timezone=True), nullable=True - ) - end_date: Mapped[datetime.datetime] = mapped_column( - DateTime(timezone=True), nullable=True - ) + start_date: Mapped[Date] = mapped_column(Date, nullable=True) + end_date: Mapped[Date] = mapped_column(Date, nullable=True) notes: Mapped[str] = mapped_column(Text, nullable=True) # --- Polymorphic Columns ---