"""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", "ContextGlobalOrTenantModel"] 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 class ContextGlobalOrTenantModel(GlobalOrTenantModel): """A global-or-tenant model that fills an unset tenant from context on save. The global row still exists (``force_global=True`` on :meth:`save`), but a normal write inside a request/task lands in the current tenant instead of silently becoming a platform-wide row. Used for per-user overrides (``Grant``/``Revoke``) where a tenant-scoped write is the safe default. """ class Meta: abstract = True def save(self, *args: Any, force_global: bool = False, **kwargs: Any) -> None: if self.tenant_id is None and not force_global: tenant = get_current_tenant() if tenant is not None: self.tenant = tenant super().save(*args, **kwargs)