This question is more about creating a new way on how to do tenants in Django, potentially for a new extension package, since I did not find any one that fulfills the requirement. (i already have a working site with a single tenant).
Requirement
A shared schema tenant / site Django implementation that makes sure that always a tenant is filtered for a model. The implementation should be compliant with the Django design philosophy (separating Models and requests).
The inspriration comes from the package django-scopes: GitHub - raphaelm/django-scopes: Safely separate multiple tenants in a Django database
It is a neat package that uses Context variables to filter a model via the modified ModelManager:
def ScopedManager(_manager_class=models.Manager, **scopes):
required_scopes = set(scopes.keys())
class Manager(_manager_class):
def __init__(self):
super().__init__()
def get_queryset(self):
current_scope = get_scope()
if not current_scope.get('_enabled', True):
return super().get_queryset()
missing_scopes = required_scopes - set(current_scope.keys())
if missing_scopes:
return DisabledQuerySet(self.model, using=self._db, missing_scopes=missing_scopes)
else:
filter_kwargs = {}
for dimension in required_scopes:
current_value = current_scope[dimension]
if isinstance(current_value, (list, tuple)):
filter_kwargs[scopes[dimension] + '__in'] = current_value
elif current_value is not None:
filter_kwargs[scopes[dimension]] = current_value
return super().get_queryset().filter(**filter_kwargs)
def all(self):
a = super().all()
if isinstance(a, DisabledQuerySet):
a = a.all()
return a
return Manager()
I think their tenant / site integration is elegant:
from django_scopes import ScopedManager
class Post(models.Model):
site = models.ForeignKey(Site, …)
title = models.CharField(…)
objects = ScopedManager(site='site')
class Comment(models.Model):
post = models.ForeignKey(Post, …)
text = models.CharField(…)
objects = ScopedManager(site='post__site')
By putting a WITH in the middleware, a tenant can be selected:
Sounds cumbersome to put those with
statements everywhere? Maybe not at all: You probably already have a middleware that determines the site (or tenant, in general) for every request based on URL or logged in user, and you can easily use it there to just automatically wrap it around all your tenant-specific views.
I want to do something similar, but with native Django functionality. To enforce all views, that use objects, to filter the objects like:
Post.objects.with_tenant(site=site).
If the with_tenant filter is forgotten, an error should be raised.
To be able to filter all tenants or objects without a tenant, I would create two specific tenants like “public”, “no_tenant”, which the model manager would recognize and filter correctly as well.
Any ideas how to do this effectively?