ICVOSS DJANGO PACKAGE REGISTRY

The package index django-boundary Scope a model to a tenant through a relation

Scope a model to a tenant through a relation

Documentation

Goal

Make a model tenant-scoped when it does not carry its own tenant foreign key but reaches the tenant through a relation, for example an ExportLog that belongs to a Destination which belongs to a merchant. The model should auto-filter by the active tenant just like a direct-FK model, without dropping to a hand-written manager.

Prerequisites

When to use this

Use make_tenant_path_mixin when the tenant is reached by a lookup path rather than a local column:

Shape Mixin
Model has its own tenant/merchant FK TenantMixin / make_tenant_mixin("merchant")
Model reaches the tenant through a relation make_tenant_path_mixin("destination__merchant")

If you find yourself writing a bespoke Manager with a for_tenant/for_merchant method that does filter(rel__rel__merchant=...), that is exactly the case this replaces.

Steps

  1. Build a mixin from the lookup path to the tenant and apply it:

```python from django.db import models from boundary.models import make_tenant_path_mixin

ExportScopedMixin = make_tenant_path_mixin("destination__merchant")

class ExportLog(ExportScopedMixin, TimeStampedModel): destination = models.ForeignKey(Destination, on_delete=models.CASCADE) # ExportLog.objects auto-filters on destination__merchant ```

The path is just a Django ORM lookup string, so multi-hop paths work the same way:

python make_tenant_path_mixin("export_log__destination__merchant")

  1. Use the model exactly like any tenant-scoped model:

python with TenantContext.using(merchant): ExportLog.objects.all() # only this merchant's export logs ExportLog.objects.create(destination=dest) # no tenant kwarg needed

The mixin adds no foreign key: the model already has the relation the path traverses. There is no tenant/merchant column on the table, and nothing to populate on save.

Verify it worked

with TenantContext.using(merchant_a):
    ExportLog.objects.create(destination=dest_a)
with TenantContext.using(merchant_b):
    ExportLog.objects.create(destination=dest_b)

with TenantContext.using(merchant_a):
    assert ExportLog.objects.count() == 1      # auto-filtered

assert ExportLog.unscoped.count() == 2         # bypass still sees all

How it differs from a direct-FK model

Path-scoped models have no local tenant column, which changes a few behaviours. boundary handles all of this for you:

Introspection

from boundary.models import (
    is_tenant_model, has_tenant_column, get_tenant_lookup, get_tenant_fk_field,
)

is_tenant_model(ExportLog)      # True
has_tenant_column(ExportLog)    # False: no local column
get_tenant_lookup(ExportLog)    # "destination__merchant"
get_tenant_fk_field(ExportLog)  # None: there is no column