Django Rest Framework - вложенный фильтр один ко многим

Чего я хочу достичь:

Мне нужен список пользователей с их соответствующими миссиями и фильтрация по дате начала миссий.

# Pseudo json
User 1
  - mission 1
  - mission 2
User 2
  - mission 1
  - mission 2
  - mission 3

Моя структура данных:

Модели:

class Mission(models.Model):
  start = models.DateTimeField()
  user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="missions")

Сериализаторы:

# Mission
class MissionSerializer(serializers.ModelSerializer):
  class Meta:
    model  = Mission
    fields = (
      'start',
      'end',
    )

# User
class UserSerializer(serializers.ModelSerializer):
  missions = MissionSerializer(many=True)
  class Meta:
    model  = MyUser
    fields = (
      'username',
      'missions',
    )

Наборы представлений:

# Filter
class UserFilter(django_filters.FilterSet):
  class Meta:
    model  = MyUser
    fields = {
      'missions__start': ['gte','lt']
    }

# Viewset
class UserViewset(viewsets.ModelViewSet):
  filter_backends  = (filters.OrderingFilter, filters.DjangoFilterBackend,)
  filter_class     = UserFilter
  serializer_class = UserSerializer

  @list_route(methods=['get'])
  def listCalendar(self, request):
    prefetched_missions = Prefetch('missions', queryset=Mission.objects.all())
    objects_list = MyUser.objects.prefetch_related( prefetched_missions )
    objects_list = self.filter_queryset(objects_list)
    serializer   = UserSerializer(objects_list, many=True)
    return Response(serializer.data)

Моя проблема:

При вызове этого URL:

/api/users/listCalendar/?start__gte=2015-06-29&start__lt=2015-08-10

Фильтр игнорируется, и я не могу понять, как заставить его работать. У меня есть интуиция, что проблема связана с Mission.objects.all() в ViewSet, который, вероятно, должен быть чем-то вроде: Mission.objects.filter(*But what here?*)

Любая помощь будет очень высоко ценится!


Редактировать 1:

Есть прогресс! Но все еще не работает... Как вы предложили, Марк Галлоуэй, я попытался вызвать следующий URL-адрес:

/api/users/listCalendar/?missions__start__gte=2015-06-29&missions__start__lt=2015-08-10

Но это запрос, который выполняется:

SELECT "app_myuser"."id", "app_myuser"."username"
FROM "app_myuser"
INNER JOIN "app_mission" ON ( "app_myuser"."id" = "app_mission"."user_id" )
INNER JOIN "app_mission" T4 ON ( "app_myuser"."id" = T4."user_id" )
WHERE ("app_mission"."start" >= '2015-07-06T00:00:00+00:00'::timestamptz
AND T4."start" < '2015-07-12T00:00:00+00:00'::timestamptz)
ORDER BY "app_myuser"."username" ASC;

Как видите, INNER JOIN 2 вместо 1. По некоторым причинам он принимает 2 отфильтрованных поля, как если бы они были в отдельных таблицах. В результате мои результаты дублируются.


person gkpo    schedule 03.07.2015    source источник
comment
Как насчет этого? API/пользователи/listCalendar/?missions__start__gte=2015-06-29&missions__start__lt=2015-08-10   -  person Mark Galloway    schedule 04.07.2015


Ответы (4)


Здесь есть три вещи

Во-первых, вам не хватает DjangoFilterBackend в вашем filter_backends list. Это то, что говорит платформе Django REST посмотреть на filter_class и применить соответствующую фильтрацию к запросу, и без этого ваш filter_class будет игнорироваться (как вы видели).

class UserViewset(viewsets.ModelViewSet):
    filter_backends = (filters.OrderingFilter, filters.DjangoFilterBackend, )
    filter_class = UserFilter
    serializer_class = UserSerializer

Во-вторых, вы ожидаете, что сможете использовать параметры запроса start и end, но говорите django-filter, чтобы он смотрел поле missions__start в файле Meta.fields. Вы можете исправить это, вручную определив поля на FilterSet с вашим псевдонимом.

class UserFilter(django_filters.FilterSet):
    start_gte = django_filter.DateTimeFilter(name='missions__start', lookup_type='gte', distinct=True)
    start_lte = django_filter.DateTimeFilter(name='missions__start', lookup_type='lte', distinct=True)

    end_gte = django_filter.DateTimeFilter(name='missions__end', lookup_type='gte', distinct=True)
    end_lte = django_filter.DateTimeFilter(missions__name='end', lookup_type='lte', distinct=True)

    class Meta:
        model  = MyUser
        fields = ('start_gte', 'start_lte', 'end_gte', 'end_lte', )

Или, просто ссылаясь на параметры запроса, будут полные значения (missions__start_gte вместо start_gte).

В-третьих, из-за того, как INNER JOIN запросы работают с несколькими таблицами, вы получите дублирующиеся значения при выполнении фильтра, затрагивающего несколько миссий под одним пользователем. Это можно исправить, используя аргумент distinct в вашем фильтры (как показано выше) или добавление .distinct() в конец ваших вызовов фильтра в filter_queryset.

person Kevin Brown    schedule 04.07.2015
comment
Вверх, я также забыл DjangoFilterBackend и продолжал задаваться вопросом, что я сделал неправильно. Благодарю вас! - person gabn88; 23.10.2015

Учитывая, что вы хотите фильтровать вложенные миссии

Я бы посоветовал вам сделать это наоборот, а затем обработать остальную часть клиента. то есть

Сначала отправьте запрос на отфильтрованные миссии, которые ссылаются на идентификатор своего пользователя.
Затем отправьте запрос на упомянутых пользователей, т. е. "#id__in=1,2, 3"
...или если у вас будет только небольшое количество пользователей: отправьте запрос для всех пользователей

При этом я думаю, что вы также можете поступить по-своему, если хотите, применяя фильтры и к миссиям, расширяя filter_queryset

Вот один из подходов к фильтрации вложенных миссий.

Обратите внимание: если вы не хотите фильтровать вложенные миссии, вы можете просто удалить метод filter_queryset из класса.

class MissionFilter(django_filters.FilterSet):
    class Meta:
        model = Mission
        fields = {
            'start': ['gte', 'lt'],
            'end': ['gte', 'lt'],
        }

class UserFilter(django_filters.FilterSet):
    class Meta:
        model = MyUser
        fields = {
            'start': ['gte', 'lt'],
            'end': ['gte', 'lt'],
        }

class UserViewset(viewsets.ModelViewSet):
    filter_backends  = (filters.OrderingFilter, filters.DjangoFilterBackend,)
    filter_class     = UserFilter
    serializer_class = UserSerializer

    def get_queryset(self):
        # Get the original queryset:
        qs = super(UserViewset, self).get_queryset()

        # * Annotate:
        #     * start = the start date of the first mission
        #     * end = the end date of the last mission
        # * Make sure, we don't get duplicate values by adding .distinct()
        return qs.annotate(start=models.Min('missions__start'),
                           end=models.Max('missions__end')).distinct()

    def filter_queryset(self, queryset):
        # Get the original queryset:
        qs = super(UserViewset, self).filter_queryset(queryset)

        # Apply same filters to missions:
        mqs = MissionFilter(self.request.query_params,
                            queryset=Missions.objects.all()).qs
        # Notice: Since we "start", and "end" in the User queryset,
        #         we can apply the same filters to both querysets

        return qs.prefetch_related(Prefetch('missions', queryset=mqs))

Вот еще одна идея

Таким образом, вы можете использовать те же параметры запроса, которые вы уже используете.

class MissionFilter(django_filters.FilterSet):
    class Meta:
        model = Mission
        fields = {
            'start': ['gte', 'lt'],
            'end': ['gte', 'lt'],
        }

class UserFilter(django_filters.FilterSet):
    class Meta:
        model = MyUser
        fields = {
            'missions__start': ['gte', 'lt'],
            'missions__end': ['gte', 'lt'],
        }

class UserViewset(viewsets.ModelViewSet):
    filter_backends  = (filters.OrderingFilter, filters.DjangoFilterBackend,)
    filter_class     = UserFilter
    serializer_class = UserSerializer
    queryset         = MyUser.objects.all().distinct()

    def filter_queryset(self, queryset):
        # Get the original queryset:
        qs = super(UserViewset, self).filter_queryset(queryset)

        # Create a copy of the query_params:
        query_params = self.request.GET.copy()

        # Check if filtering of nested missions is requested:
        if query_params.pop('filter_missions', None) == None:
            return qs

        # Find and collect missions filters with 'missions__' removed:
        params = {k.split('__', 1)[1]: v
                  for k, v in query_params.items() if k.startswith('missions__')}

        # Create a Mission queryset with filters applied:
        mqs = MissionFilter(params, queryset=Missions.objects).qs.distinct()

        return qs.prefetch_related(Prefetch('missions', queryset=mqs))

Я не проверял ничего из этого, поэтому было бы здорово получить отзывы.

person demux    schedule 04.07.2015
comment
Ах, подождите... вам же нужны только те миссии, которые соответствуют этим датам, верно? В таком случае это не сработает. - person demux; 04.07.2015
comment
Я только что добавил метод filter_queryset, который также позаботится об этом сценарии. - person demux; 04.07.2015
comment
Полностью создавать собственный фильтр, который почти заменяет набор фильтров, здесь излишне. - person Kevin Brown; 04.07.2015
comment
В filter_queryset добавлено всего 12 строк кода. Конечно, это немного хак, но я действительно не думаю, что заслуживаю отрицательного голоса по этому поводу. - person demux; 04.07.2015
comment
Я переписал пример и добавил новую идею, основанную на исходной. - person demux; 06.07.2015

Ваш filter_class игнорируется, потому что вы не объявляете DjangoFilterBackend внутри filter_backends.

class UserViewset(viewsets.ModelViewSet):
  filter_backends = (filters.OrderingFilter, filters.DjangoFilterBackend)
  filter_class = UserFilter

Поскольку у вас есть OrderingFilter, но нет полей ordering_fields, возможно, вы указали не тот сервер?

person Mark Galloway    schedule 04.07.2015
comment
Привет, ты прав, это не сработает. Но я добавил его, и он по-прежнему игнорирует фильтры... - person gkpo; 04.07.2015

Я предполагаю, что вам нужно Mission.objects.filter(id=self.request.user), с этим вы получите все миссии для текущего пользователя

person EDDIE VALVERDE    schedule 04.07.2015
comment
Привет, я хочу отфильтровать дату начала миссий. Я отредактировал вопрос, чтобы он был более очевидным. - person gkpo; 04.07.2015