Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 20 additions & 1 deletion db/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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."""

Expand Down
86 changes: 86 additions & 0 deletions db/permission.py
Original file line number Diff line number Diff line change
@@ -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)
Comment thread
ksmuczynski marked this conversation as resolved.
notes: Mapped[str] = mapped_column(Text, nullable=True)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this relate back to the polymorphic notes table? rather than have a notes field

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of a polymorphic notes table, but thought it might be easier to implement single note fields in individual tables to start. Later we could go back and implement the polymorphic notes table, but I'm open to discussion. If we have the time I think it could be worth implementing now. We could still retain NMAquifer notes in nma_notes fields where appropriate.


# --- 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,
)
Comment on lines +63 to +77
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this to work do the thing and location tables need to inherit from the permission mixin?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait! That's been noted in your notes. Should it be present in this PR? Or wait to implement that so that it can be implemented elsewhere, too, like services and tests?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. The idea with this PR is to just create the table. Then a separate PR for Thing and Location updates, where they would inherit these new tables. It seemed simpler to break down the PRs into their individual component tasks and focus on one thing at a time. Personal opinion of course, open to discussion re: best practices for PRs.


@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")
Loading