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() For relationship filtering: class Parent(db.Model, SoftDeleteMixin): children = db.relationship( "Child", primaryjoin="and_(Parent.id == Child.parent_id, " "Child.status == 'active')", ... ) """ 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) @property def is_active(self): """Check if this record is active (not soft-deleted).""" return self.status == self.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