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
70 changes: 67 additions & 3 deletions db/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,45 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# ===============================================================================
"""
db/base.py

This file defines the foundational components for the SQLAlchemy models.
It includes:
1. The declarative base class (`Base`) that all models will inherit from.
2. A helper function (`lexicon_term`) to create standardized foreign key columns
referencing the `lexicon_term` table.
3. A helper function (`pascal_to_snake`) to convert class names from PascalCase to snake_case
for automatic table naming.
4. Mixins for common functionality:
- `AutoBaseMixin`: Adds automatic table naming and an auto-incrementing primary key.
- `PropertiesMixin`: Adds a JSONB properties column for storing additional attributes.
- `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.)
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.
7. An `AuditMixin` to add standard audit columns to tables.
"""

from sqlalchemy import (
Column,
DateTime,
func,
Integer,
JSON,
String,
Boolean,
Text,
ForeignKey,
)
from sqlalchemy.orm import DeclarativeBase, declared_attr, Mapped, mapped_column
from sqlalchemy.orm import (
DeclarativeBase,
declared_attr,
Mapped,
mapped_column,
relationship,
)
from sqlalchemy_searchable import make_searchable
from sqlalchemy_continuum import make_versioned
import re
Expand All @@ -41,6 +68,12 @@ class Base(DeclarativeBase):


def lexicon_term(foreignkeykw=None, **kw):
"""Create a SQLAlchemy mapped column for a self-referencing lexicon term.

This helper function simplifies the creation of a string column that also
acts as a foreign key to the 'term' column of the 'lexicon_term' table.
It standardizes the column type to String(100) and sets the onupdate
behavior to "CASCADE"."""

fkw = foreignkeykw if foreignkeykw else {}

Expand All @@ -55,13 +88,18 @@ def pascal_to_snake(name):
return re.sub(r"(?<!^)(?=[A-Z])", "_", name).lower()


# ============= Common Mixins =============================================
class ReleaseMixin:
"""Mixin to add release status to a model."""

@declared_attr
def release_status(self):
return lexicon_term(default="draft")


class AuditMixin:
"""Mixin to add standard audit columns to a model."""

@declared_attr
def created_at(self):
return Column(
Expand Down Expand Up @@ -109,6 +147,8 @@ def updated_by_id(self):


class AutoBaseMixin(AuditMixin):
"""Mixin to add automatic table naming and an auto-incrementing primary key."""

@declared_attr
def __tablename__(self):
return pascal_to_snake(self.__name__)
Expand All @@ -119,6 +159,8 @@ def id(self):


class PropertiesMixin:
"""Mixin to add a JSONB properties column for storing additional attributes."""

@declared_attr
def properties(self):
return Column(
Expand All @@ -129,7 +171,29 @@ def properties(self):
)


# ============= Polymorphic Helper Mixins =============================================
class StatusHistoryMixin:
"""
Mixin for models that can have a status history (e.g., Thing, Location).
It automatically creates a polymorphic One-to-Many relationship to the
StatusHistory table.
"""

@declared_attr
def status_history(self):
# One-to-Many polymorphic relationship
return relationship(
"StatusHistory",
primaryjoin=f"and_({self.__name__}.{self.__name__.lower()}_id==StatusHistory.statusable_id, "
f"StatusHistory.statusable_type=='{self.__name__}')",
cascade="all, delete-orphan",
lazy="selectin",
Comment thread
ksmuczynski marked this conversation as resolved.
)


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

__tablename__ = "user"

id: Mapped[int] = mapped_column(Integer, primary_key=True, nullable=False)
Expand Down
38 changes: 38 additions & 0 deletions db/status_history.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""
models/status_history.py

This model defines the `StatusHistory` table, a central, polymorphic log for
all time-variant operational statuses (e.g., Use Status, Access Status).

**NOTE**: This is a polymorphic table. It does not define outgoing relationships
itself. Instead, other tables (like Thing and Location) use the `StatusHistoryMixin`
mixin to establish a One-to-Many relationship TO this table.
"""

import datetime

from sqlalchemy import (
Integer,
String,
DateTime,
Text,
)
from sqlalchemy.orm import Mapped, mapped_column

from db.base import Base, AutoBaseMixin, ReleaseMixin


class StatusHistory(Base, AutoBaseMixin, ReleaseMixin):
status_type: Mapped[str] = mapped_column(String(50), nullable=False)
status_value: Mapped[str] = mapped_column(String(50), nullable=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
)
reason: Mapped[str] = mapped_column(Text, nullable=True)

# Polymorphic relationship columns
statusable_id: Mapped[int] = mapped_column(Integer, nullable=False)
Comment thread
ksmuczynski marked this conversation as resolved.
statusable_type: Mapped[str] = mapped_column(String(50), nullable=False)
Loading