citusdata / django-multitenant

Python/Django support for distributed multi-tenant databases like Postgres+Citus
MIT License
730 stars 118 forks source link

Tenant not filtered in model forms #87

Open christokritz opened 4 years ago

christokritz commented 4 years ago

Suppose I have the following 2 models and a model form

`

class CostCenter(ClientModelMixin, models.Model):
    tenant_id = 'client_id'
    client = models.ForeignKey('accounts.Client', on_delete=models.CASCADE)
    name = models.CharField(max_length=50)
    objects = TenantManager()

class Account(ClientModelMixin, models.Model):
    tenant_id = 'client_id'
    client = models.ForeignKey('accounts.Client', on_delete=models.CASCADE)
    name = models.CharField(max_length=50)
    cost_center = TenantForeignKey(CostCenter, on_delete=models.PROTECT)
    objects = TenantManager()

class CreateForm(forms.ModelForm):
    class Meta:
        model = Account
        fields = ['cost_center']

`

When the model form is rendered it shows all cost centers in the dropdown. I think it happens because the model form is created before the tenant is set and then no tenant filtering is applied.

I can override the init method of every form and set the queryset correctly but this isn't ideal.

How can this be solved?

rob101 commented 4 years ago

I think the best option is to subclass ModelForm:

class TenantModelForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        kwargs.setdefault('label_suffix', '')
        super().__init__(*args, **kwargs)

        tenant = get_current_tenant()
        if tenant:
            for field in self.fields.values():
                if isinstance(
                        field,
                        (forms.ModelChoiceField,
                         forms.ModelMultipleChoiceField),
                ) and hasattr(field.queryset.model, 'tenant_id'):
                    # Add filter restricting queryset to values to this
                    # tenant only.
                    kwargs = {field.queryset.model.tenant_id: tenant}
                    field.queryset = field.queryset.filter(**kwargs)

        else:
            raise ImproperlyConfigured('Current tenant is not set. You must '
                                       'have a tenant set when calling a '
                                       'subclass of TenantModelForm')
denys-chura commented 1 year ago

If you use django-simple-multitenant v3.1.0 or above and define tenant_id using TenantMeta, code from comment above will no longer work, you need to replace:

if isinstance(
...
) and hasattr(field.queryset.model, 'tenant_id'):
...
    kwargs = {field.queryset.model.tenant_id: tenant}

with something like:

from django_multitenant.utils import get_tenant_filters
from django_multitenant.models import TenantModel
...

if isinstance(
...
) and issubclass(field.queryset.model, TenantModel):
...
    kwargs = get_tenant_filters(field.queryset.model)