raphaelm / django-scopes

Safely separate multiple tenants in a Django database
Apache License 2.0
233 stars 15 forks source link

How to scope a many-to-many relationship #21

Open andrewtmendoza opened 3 years ago

andrewtmendoza commented 3 years ago

Hi @raphaelm - I'm working on a multi-tenancy app in which there is a many-to-many relationship between users and tenants.

Is this the correct way to represent it in django-scopes?

models.py

class MyUserManager(UserManager):
    use_in_migrations = False

class User(AbstractUser):
    USER_TYPE_CHOICES = [
        ("C", "Customer"),
        ("E", "Employee"),
    ]

    tenants = models.ManyToManyField(Tenant)
    phone_number = PhoneNumberField(blank=True)
    line_of_business = models.ManyToManyField(LineOfBusiness)
    sponsors = models.ManyToManyField("web.Sponsor")
    user_type = models.CharField(choices=USER_TYPE_CHOICES, max_length=5)

    # https://github.com/raphaelm/django-scopes#scoping-the-user-model
    # https://stackoverflow.com/a/4508083/614770 - how to scope many to many field
    objects = ScopedManager(tenant="tenants__id__contains", _manager_class=MyUserManager)

middleware.py

class ScopingMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        with scope(tenant=request.tenant.pk):
            return self.get_response(request)

It seems to work most of the time, although I occasionally see a strange error with the User ModelForm where the Sponsor model data is retrieved from the incorrect tenant. image

raphaelm commented 3 years ago

Actually, support for many to many relationships isn't intended (so far), so whatever works is more of a coincidence than a feature :(

robinsandstrom commented 3 years ago

I'm late to the show but you can just do this by:

objects = ScopedManager(tenant="tenants", _manager_class=MyUserManager)

And add a middleware:

class SetTenantMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        if request.user.is_authenticated:
            if request.path.startswith("/admin_panel/"):
                # no scoping in admin
                with scopes_disabled():
                    response = self.get_response(request)
            else:
                with scope(tenant=request.user.tenant_id):
                    response = self.get_response(request)

        else:
            response = self.get_response(request)

        return response

In case anyone else would encounter the same problem.