Skip to content

Commit 343af2b

Browse files
authored
Merge pull request #138 from DataIntegrationGroup/statushistory_model
statushistory_model
2 parents 98ac4ac + 87bd7ca commit 343af2b

2 files changed

Lines changed: 105 additions & 3 deletions

File tree

db/base.py

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,45 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515
# ===============================================================================
16+
"""
17+
db/base.py
18+
19+
This file defines the foundational components for the SQLAlchemy models.
20+
It includes:
21+
1. The declarative base class (`Base`) that all models will inherit from.
22+
2. A helper function (`lexicon_term`) to create standardized foreign key columns
23+
referencing the `lexicon_term` table.
24+
3. A helper function (`pascal_to_snake`) to convert class names from PascalCase to snake_case
25+
for automatic table naming.
26+
4. Mixins for common functionality:
27+
- `AutoBaseMixin`: Adds automatic table naming and an auto-incrementing primary key.
28+
- `PropertiesMixin`: Adds a JSONB properties column for storing additional attributes.
29+
- `ReleaseMixin`: Adds a release status column referencing the `lexicon_term` table.
30+
- `AuditMixin`: Adds standard audit columns (created_at, created_by, updated_at, updated_by).
31+
5. A simple `User` model for tracking user information in audit columns.
32+
6. Polymorphic helper mixins (`StatusHistoryMixin`, `NotesMixin`, `AttributionMixin`, etc.)
33+
which provide a clean, reusable way to add relationships to the polymorphic
34+
metadata tables. Any model that can have a status history (like Thing or Location)
35+
can simply inherit from the `StatusHistoryMixin` mixin.
36+
7. An `AuditMixin` to add standard audit columns to tables.
37+
"""
38+
1639
from sqlalchemy import (
1740
Column,
1841
DateTime,
1942
func,
2043
Integer,
2144
JSON,
2245
String,
23-
Boolean,
24-
Text,
2546
ForeignKey,
2647
)
27-
from sqlalchemy.orm import DeclarativeBase, declared_attr, Mapped, mapped_column
48+
from sqlalchemy.orm import (
49+
DeclarativeBase,
50+
declared_attr,
51+
Mapped,
52+
mapped_column,
53+
relationship,
54+
)
2855
from sqlalchemy_searchable import make_searchable
2956
from sqlalchemy_continuum import make_versioned
3057
import re
@@ -41,6 +68,12 @@ class Base(DeclarativeBase):
4168

4269

4370
def lexicon_term(foreignkeykw=None, **kw):
71+
"""Create a SQLAlchemy mapped column for a self-referencing lexicon term.
72+
73+
This helper function simplifies the creation of a string column that also
74+
acts as a foreign key to the 'term' column of the 'lexicon_term' table.
75+
It standardizes the column type to String(100) and sets the onupdate
76+
behavior to "CASCADE"."""
4477

4578
fkw = foreignkeykw if foreignkeykw else {}
4679

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

5790

91+
# ============= Common Mixins =============================================
5892
class ReleaseMixin:
93+
"""Mixin to add release status to a model."""
94+
5995
@declared_attr
6096
def release_status(self):
6197
return lexicon_term(default="draft")
6298

6399

64100
class AuditMixin:
101+
"""Mixin to add standard audit columns to a model."""
102+
65103
@declared_attr
66104
def created_at(self):
67105
return Column(
@@ -109,6 +147,8 @@ def updated_by_id(self):
109147

110148

111149
class AutoBaseMixin(AuditMixin):
150+
"""Mixin to add automatic table naming and an auto-incrementing primary key."""
151+
112152
@declared_attr
113153
def __tablename__(self):
114154
return pascal_to_snake(self.__name__)
@@ -119,6 +159,8 @@ def id(self):
119159

120160

121161
class PropertiesMixin:
162+
"""Mixin to add a JSONB properties column for storing additional attributes."""
163+
122164
@declared_attr
123165
def properties(self):
124166
return Column(
@@ -129,7 +171,29 @@ def properties(self):
129171
)
130172

131173

174+
# ============= Polymorphic Helper Mixins =============================================
175+
class StatusHistoryMixin:
176+
"""
177+
Mixin for models that can have a status history (e.g., Thing, Location).
178+
It automatically creates a polymorphic One-to-Many relationship to the
179+
StatusHistory table.
180+
"""
181+
182+
@declared_attr
183+
def status_history(self):
184+
# One-to-Many polymorphic relationship
185+
return relationship(
186+
"StatusHistory",
187+
primaryjoin=f"and_({self.__name__}.{self.__name__.lower()}_id==StatusHistory.statusable_id, "
188+
f"StatusHistory.statusable_type=='{self.__name__}')",
189+
cascade="all, delete-orphan",
190+
lazy="selectin",
191+
)
192+
193+
132194
class User(Base):
195+
"""Represents a user in the system."""
196+
133197
__tablename__ = "user"
134198

135199
id: Mapped[int] = mapped_column(Integer, primary_key=True, nullable=False)

db/status_history.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""
2+
models/status_history.py
3+
4+
This model defines the `StatusHistory` table, a central, polymorphic log for
5+
all time-variant operational statuses (e.g., Use Status, Access Status).
6+
7+
**NOTE**: This is a polymorphic table. It does not define outgoing relationships
8+
itself. Instead, other tables (like Thing and Location) use the `StatusHistoryMixin`
9+
mixin to establish a One-to-Many relationship TO this table.
10+
"""
11+
12+
import datetime
13+
14+
from sqlalchemy import (
15+
Integer,
16+
String,
17+
DateTime,
18+
Text,
19+
)
20+
from sqlalchemy.orm import Mapped, mapped_column
21+
22+
from db.base import Base, AutoBaseMixin, ReleaseMixin
23+
24+
25+
class StatusHistory(Base, AutoBaseMixin, ReleaseMixin):
26+
status_type: Mapped[str] = mapped_column(String(50), nullable=False)
27+
status_value: Mapped[str] = mapped_column(String(50), nullable=False)
28+
start_date: Mapped[datetime.datetime] = mapped_column(
29+
DateTime(timezone=True), nullable=True
30+
)
31+
end_date: Mapped[datetime.datetime] = mapped_column(
32+
DateTime(timezone=True), nullable=True
33+
)
34+
reason: Mapped[str] = mapped_column(Text, nullable=True)
35+
36+
# Polymorphic relationship columns
37+
statusable_id: Mapped[int] = mapped_column(Integer, nullable=False)
38+
statusable_type: Mapped[str] = mapped_column(String(50), nullable=False)

0 commit comments

Comments
 (0)