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.""" diff --git a/db/permission.py b/db/permission.py new file mode 100644 index 000000000..93e688e86 --- /dev/null +++ b/db/permission.py @@ -0,0 +1,86 @@ +""" +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")