Files
shopdb-flask/shopdb/core/models/base.py

99 lines
3.1 KiB
Python

"""Base model class with common fields."""
from datetime import datetime, timezone
from shopdb.extensions import db
def _utcnow():
# naive UTC for DB columns (stored without tzinfo)
return datetime.now(timezone.utc).replace(tzinfo=None)
class BaseModel(db.Model):
"""
Abstract base model with common fields.
All models should inherit from this.
"""
__abstract__ = True
createddate = db.Column(
db.DateTime,
default=_utcnow,
nullable=False
)
modifieddate = db.Column(
db.DateTime,
default=_utcnow,
onupdate=_utcnow,
nullable=False
)
isactive = db.Column(db.Boolean, default=True, nullable=False)
# Columns the default to_dict NEVER emits, whatever model carries them.
#
# The default serialiser walks __table__.columns and publishes every one, so
# a model that does not override it publishes whatever it has - including
# columns added to it later, by someone who was not thinking about this
# method. User is the live example: it does not override to_dict, so
# User.to_dict() returns passwordhash. Nothing calls it in a response path
# today, which is luck rather than design.
#
# A blanket allowlist would be the stronger rule, but it would also silently
# empty the output of all 34 models that rely on this default. This is the
# part that can be fixed without breaking anything: name the columns that
# must never be published, and let a model add its own.
#
# A model with something sensitive should still override to_dict and say
# exactly what it emits. This is a floor, not permission to skip that.
HIDDEN_COLUMNS = frozenset({
'passwordhash',
'tokenhash',
'apikey',
'clientsecret',
'collectorkey',
})
def to_dict(self):
"""Convert model to dictionary, minus anything secret-bearing."""
hidden = self.HIDDEN_COLUMNS
result = {}
for c in self.__table__.columns:
if c.name in hidden:
continue
value = getattr(self, c.name)
if isinstance(value, datetime):
value = value.isoformat() + 'Z'
result[c.name] = value
return result
def update(self, **kwargs):
"""Update model attributes."""
for key, value in kwargs.items():
if hasattr(self, key):
setattr(self, key, value)
@classmethod
def get_active(cls):
"""Return query for active records only."""
return cls.query.filter_by(isactive=True)
class SoftDeleteMixin:
"""Mixin for soft delete functionality."""
deleteddate = db.Column(db.DateTime, nullable=True)
deletedby = db.Column(db.String(100), nullable=True)
def soft_delete(self, deleted_by: str = None):
"""Mark record as deleted."""
self.isactive = False
self.deleteddate = datetime.now(timezone.utc).replace(tzinfo=None)
self.deletedby = deleted_by
class AuditMixin:
"""Mixin for audit fields."""
createdby = db.Column(db.String(100), nullable=True)
modifiedby = db.Column(db.String(100), nullable=True)