"""Abstract model mixins for tenant-owned and global+override resources. Using a mixin keeps the ``tenant`` field and managers consistent across every app without repeating them. Concrete models still declare their own ``db_table`` and constraints. The mixin is the sanctioned way for another app to depend on ``infrasynth.tenancy`` (see the dependency graph in ``PLAN.md`` §3). """ from __future__ import annotations from typing import Any from django.db import models from .context import get_current_tenant from .managers import AllObjectsManager, GlobalOrTenantManager, TenantManager __all__ = ["TenantOwnedModel", "GlobalOrTenantModel"] class TenantOwnedModel(models.Model): """Base for every tenant-owned model (``TENANCY.md`` §4). Provides a non-null ``tenant`` FK, the fail-closed default ``TenantManager``, and the unscoped ``all_objects`` escape hatch. On save, an unset tenant is filled from the bound context so writes inside a request/task land in the right tenant; a write with neither is rejected by the database. """ tenant = models.ForeignKey( "tenancy.Tenant", on_delete=models.CASCADE, related_name="+", editable=False, ) objects = TenantManager() all_objects = AllObjectsManager() class Meta: abstract = True def save(self, *args: Any, **kwargs: Any) -> None: if self.tenant_id is None: tenant = get_current_tenant() if tenant is not None: self.tenant = tenant super().save(*args, **kwargs) class GlobalOrTenantModel(models.Model): """Base for resources that exist globally and can be overridden per tenant. ``tenant IS NULL`` is the platform default; a non-null tenant is that tenant's override. The default manager only ever exposes the global rows plus the current tenant's rows, never another tenant's. """ tenant = models.ForeignKey( "tenancy.Tenant", on_delete=models.CASCADE, null=True, blank=True, related_name="+", ) objects = GlobalOrTenantManager() all_objects = AllObjectsManager() class Meta: abstract = True