ICVOSS DJANGO PACKAGE REGISTRY

The package index django-boundary Customise the terminology (merchant, organisation, club)

Customise the terminology (merchant, organisation, club)

Documentation

Goal

Rename "tenant" everywhere it surfaces in your project, so your code and your users see your domain's word instead. By the end you will have a model with a field like Product.merchant, views that read request.merchant, error messages that say "No merchant is active", and a 404 body that reads "Merchant not found".

Boundary exposes three settings and one factory for this:

For the exhaustive option table, see the settings reference in the README.

Prerequisites

Steps

1. Set the master switch

Most projects only need one setting. BOUNDARY_TENANT_FK_FIELD flows through to both the label and the request attribute, so setting it alone renames everything consistently.

# settings.py
BOUNDARY_TENANT_MODEL = "merchants.Merchant"
BOUNDARY_TENANT_FK_FIELD = "merchant"

With this in place, and nothing else changed:

The label is title-cased automatically for the HTTP response bodies, so you do not need a separately capitalised setting.

2. Give your models the renamed FK with make_tenant_mixin()

The built-in TenantMixin always uses the field name tenant. To get Product.merchant, build a mixin with the factory and subclass it instead.

# products/models.py
from django.db import models
from boundary.models import make_tenant_mixin

MerchantMixin = make_tenant_mixin("merchant")


class Product(MerchantMixin):
    sku = models.CharField(max_length=64)
    name = models.CharField(max_length=200)
    # Product.merchant is the FK to BOUNDARY_TENANT_MODEL.
    # Product.objects auto-filters by the active tenant via the "merchant" field.
    # Product.unscoped bypasses filtering.

make_tenant_mixin() returns an abstract model that wires up the FK, objects = TenantManager() (auto-filtering and auto-populate), and unscoped = UnscopedManager() (the escape hatch). Auto-populate, bulk_create, and bulk_update all work through the custom field name.

If you call it with no argument, it falls back to BOUNDARY_TENANT_FK_FIELD, so these two lines are equivalent once step 1 is done:

MerchantMixin = make_tenant_mixin("merchant")
MerchantMixin = make_tenant_mixin()  # reads BOUNDARY_TENANT_FK_FIELD

The factory also accepts keyword arguments to tune the FK, all keyword-only:

MerchantMixin = make_tenant_mixin(
    "merchant",
    on_delete=models.PROTECT,   # default models.CASCADE
    related_name="products",     # default "%(app_label)s_%(class)s_set"
    db_index=True,               # default True
    null=False,                  # default False
)

The FK's verbose_name is set from BOUNDARY_TENANT_LABEL, so the field reads as "merchant" in the admin and forms without extra work.

3. Read the tenant off the request as request.merchant

TenantMiddleware always sets request.tenant for backwards compatibility. When BOUNDARY_REQUEST_ATTR resolves to something other than "tenant", the middleware also sets that attribute to the same object. With step 1 done, BOUNDARY_REQUEST_ATTR is already "merchant", so:

def dashboard(request):
    merchant = request.merchant   # same object as request.tenant
    ...

If you only want to rename the request attribute (and nothing else), set it explicitly:

# settings.py
BOUNDARY_REQUEST_ATTR = "merchant"

4. Override the label or request attribute independently (optional)

The three settings are independent, even though two default to the FK field name. Override them when your column name, your user-facing word, and your request attribute should differ. For example, an FK column named org but a label of "organisation" shown to users:

# settings.py
BOUNDARY_TENANT_FK_FIELD = "org"            # Product.org, column org_id
BOUNDARY_TENANT_LABEL = "organisation"      # "No organisation is active..."
BOUNDARY_REQUEST_ATTR = "organisation"      # request.organisation

For a club-style app where the field, label, and request attribute should all read "club", step 1 alone is enough:

# settings.py
BOUNDARY_TENANT_FK_FIELD = "club"

Verify it worked

Check the field name and column

>>> from products.models import Product
>>> [f.name for f in Product._meta.get_fields()]
['id', 'merchant', 'sku', 'name']      # "merchant", not "tenant"
>>> Product._meta.get_field("merchant").column
'merchant_id'

Check auto-populate uses the renamed field

from boundary.testing import set_tenant
from products.models import Product

with set_tenant(some_merchant):
    product = Product.objects.create(sku="A1", name="Widget")
    assert product.merchant == some_merchant
    assert product.merchant_id == some_merchant.pk

Check the error message and request alias

>>> from boundary.context import TenantContext
>>> TenantContext.require()      # with no tenant set
Traceback (most recent call last):
    ...
boundary.exceptions.TenantNotSetError: No merchant is active in context. ...

After a request flows through TenantMiddleware, both attributes point at the same object:

assert request.merchant is request.tenant

You can also confirm the model is registered and report its FK field name:

>>> from boundary.models import is_tenant_model, get_tenant_fk_field
>>> is_tenant_model(Product)
True
>>> get_tenant_fk_field(Product)
'merchant'

Common pitfalls