citusdata / django-multitenant

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

Tenant filter does not work in django-filter ModelChoiceFilter #59

Open AlexanderNelzin opened 5 years ago

AlexanderNelzin commented 5 years ago

Problem: sql-clause "table"."tenant_id" = current_tenant_value dissapers from query for django-filter's ModelChoiceFilter when order_by is added to queryset.

Code to reproduce.

models.py

from django_multitenant.fields import TenantForeignKey
from django_multitenant.mixins import TenantManagerMixin, TenantQuerySet
from django_multitenant.models import TenantModel
from django_multitenant.utils import get_current_tenant, set_current_tenant

class CompanyObjectModel(TenantModel):
    """Абстрактный класс для разделяемых по компаниям моделей"""
    company = models.ForeignKey(
        Company,
        default=get_current_tenant,
        on_delete=models.PROTECT
    )

    tenant_id = 'company_id'
    class Meta:
        abstract = True
        unique_together = ["id", "company"]

class EventClass(CompanyObjectModel):
    """
    Описание шаблона мероприятия (Класс вид).
    Например, тренировки в зале бокса у Иванова по средам и пятницам
    """
    name = models.CharField("Наименование", max_length=100)
    location = TenantForeignKey(
        Location,
        on_delete=models.PROTECT,
        verbose_name="Место проведения")
    coach = TenantForeignKey(
        Coach,
        on_delete=models.PROTECT,
        verbose_name="Тренер")
    date_from = models.DateField("Начало тренировок", null=True, blank=True)
    date_to = models.DateField("Окончание тренировок", null=True, blank=True)
    planned_attendance = models.PositiveSmallIntegerField(
        "Плановая посещаемость",
        null=True,
        blank=True,
        validators=[MinValueValidator(0)]
    )

    objects = EventClassManager()

class EventClassManager(TenantManagerMixin, models.Manager):
    def active(self):
        return self.get_queryset().filter(
            Q(date_to__isnull=True) | Q(date_to__gte=date.today())
        )

    def in_range(self, day_start, day_end):
        return self.get_queryset().filter(
            (
                Q(date_from__isnull=False) & Q(date_to__isnull=False) &
                Q(date_from__lte=day_end) & Q(date_to__gte=day_start)
            ) | (
                Q(date_from__isnull=False) & Q(date_to__isnull=True) &
                Q(date_from__lte=day_end)
            ) | (
                Q(date_from__isnull=True) & Q(date_to__isnull=False) &
                Q(date_to__gte=day_start)
            ) | (
                Q(date_from__isnull=True) & Q(date_to__isnull=True)
            )
        )

filters.py

import django_filters
from crm import models

class VisitReportFilterNew(django_filters.FilterSet):

    event_class = django_filters.ModelChoiceFilter(
        label='Группа:',
        field_name='event_class',
        queryset=models.EventClass.objects,
        empty_label=None,
        widget=forms.Select(
            attrs={
                'class': 'selectpicker form-control',
                'title': 'Группа',
            }
        ),
        required=False,
    )

    class Meta:
        model = models.Event
        fields = ('date',)

report.html

        <div class="col-6 col-md-3">
          <div class="form-group select">
            <label for="id_event_class">{{ filter.form.event_class.label }}</label>
            {{ filter.form.event_class }}
          </div>
        </div>

in ModelChoiseFilter queryset=models.EventClass.objects generates valid SQL-query:

DECLARE "_django_curs_140336784169760_1" NO SCROLL
CURSOR WITH HOLD
   FOR SELECT "crm_eventclass"."id",
       "crm_eventclass"."company_id",
       "crm_eventclass"."name",
       "crm_eventclass"."location_id",
       "crm_eventclass"."coach_id",
       "crm_eventclass"."date_from",
       "crm_eventclass"."date_to",
       "crm_eventclass"."planned_attendance"
  FROM "crm_eventclass"
 WHERE "crm_eventclass"."company_id" = 71

But when I try to sort filter values changing queryset in ModelChoiseFilter to queryset=models.EventClass.objects.order_by("name"). The SQL-query is ordered, but tenant filter is missing, it returns all rows from table:

DECLARE "_django_curs_140075094870816_1" NO SCROLL
CURSOR WITH HOLD
   FOR SELECT "crm_eventclass"."id",
       "crm_eventclass"."company_id",
       "crm_eventclass"."name",
       "crm_eventclass"."location_id",
       "crm_eventclass"."coach_id",
       "crm_eventclass"."date_from",
       "crm_eventclass"."date_to",
       "crm_eventclass"."planned_attendance"
  FROM "crm_eventclass"
 ORDER BY "crm_eventclass"."name" ASC

Versions:

Django==2.1.7
django-multitenant==2.0.0
django-filter==2.1.0
onlined commented 5 years ago

Does the filter disappear again if you change the keyword argument from queryset=models.EventClass.objects to queryset=models.EventClass.objects.all()?

AlexanderNelzin commented 4 years ago

Yes, with queryset=models.EventClass.objects.all() filter disappears.

onlined commented 4 years ago

django-multitenant needs to get the tenant from request before filtering. In your example, filtering happens at import time, which is before request. With django-filter, instead of a queryset, you can use a callable which takes the ongoing request and returns a queryset. For example, you can change queryset=models.EventClass.objects.order_by("name") to queryset=lambda _: models.EventClass.objects.order_by("name") (_ is the ignored request parameter here). With that change, the filtering is done after the request sent and the current tenant can be detected.