kanban-app/backend/app/models/base.py

124 lines
3.6 KiB
Python
Raw Normal View History

2026-05-01 15:03:00 +00:00
from datetime import UTC, datetime
from app import db
class SoftDeleteMixin:
"""Mixin that provides soft-delete functionality.
Instead of removing rows from the database, records are marked with
status='deleted' and a deleted_at timestamp. This preserves data
integrity and supports audit/recovery use-cases.
Usage:
class MyModel(db.Model, SoftDeleteMixin):
...
# Queries
MyModel.active().filter_by(name="foo").all()
# Soft-delete
instance = db.session.get(MyModel, 1)
instance.soft_delete()
db.session.commit()
# Restore
instance.restore()
db.session.commit()
"""
STATUS_ACTIVE = "active"
STATUS_DELETED = "deleted"
status = db.Column(
db.String(20),
default=STATUS_ACTIVE,
nullable=False,
index=True,
)
deleted_at = db.Column(db.DateTime, nullable=True)
@classmethod
def active(cls):
"""Return a query scoped to non-deleted records only.
Usage:
Board.active().filter_by(user_id=1).all()
Card.active().order_by(Card.pos).all()
"""
return cls.query.filter(cls.status == cls.STATUS_ACTIVE)
def soft_delete(self):
"""Mark this record as deleted and cascade to child relationships.
Every relationship defined with cascade="all, delete-orphan" or
cascade="all" is considered a *soft-deletable child* and will also
be soft-deleted recursively.
"""
self._do_soft_delete()
def _do_soft_delete(self):
"""Internal recursive soft-delete implementation."""
now = datetime.now(UTC)
# Soft-delete children from configured cascade relationships
for prop in self.__mapper__.relationships:
cascade = prop.cascade
if cascade is None:
continue
# Only cascade to relationships that had delete/delete-orphan
if not (cascade.delete or cascade.delete_orphan):
continue
# Get the related objects
children = getattr(self, prop.key, None)
if children is None:
continue
# Handle dynamic/lazy relationships (query-like)
if hasattr(children, "all"):
children = children.all()
# Handle single (many-to-one / scalar) relationships
if not isinstance(children, list):
children = [children] if children else []
for child in children:
if isinstance(child, SoftDeleteMixin):
child._do_soft_delete()
self.status = self.STATUS_DELETED
self.deleted_at = now
def restore(self):
"""Restore a soft-deleted record and cascade to children."""
self._do_restore()
def _do_restore(self):
"""Internal recursive restore implementation."""
for prop in self.__mapper__.relationships:
cascade = prop.cascade
if cascade is None:
continue
if not (cascade.delete or cascade.delete_orphan):
continue
children = getattr(self, prop.key, None)
if children is None:
continue
if hasattr(children, "all"):
children = children.all()
if not isinstance(children, list):
children = [children] if children else []
for child in children:
if isinstance(child, SoftDeleteMixin):
child._do_restore()
self.status = self.STATUS_ACTIVE
self.deleted_at = None