Django-filter: Uso pouco claro para `OrderingFilter` e campos calculados

Criado em 18 jul. 2020  ·  4Comentários  ·  Fonte: carltongibson/django-filter

Gratidão

Olá!

Foi um prazer usar esta biblioteca! Estou tão entusiasmado com toda a conveniência que ele oferece!

Obrigado por isso!

Meta

Meu objetivo agora é ter um ponto de extremidade que suporte a capacidade de pesquisa em parâmetros de modelo e ordenação também.

Modelo de dados

Eu tenho um modelo School que tem um campo calculado chamado learner_enrolled_count

A resposta JSON é semelhante a esta:

{
    "schools": [
        {
            "id": 6,
            "name": "Piano Gym Six",
            "courses": [
                // ...
            ],
            "learner_enrolled_count": 0
        },
        {
            "id": 7,
            "name": "Piano Gym Seven",
            "courses": [
                // ...
            ],
            "learner_enrolled_count": 5
        }
    ]
}

O learner_enrolled_count é um campo calculado.

O problema

Eu li a documentação aqui:
https://django-filter.readthedocs.io/en/stable/ref/filters.html?highlight=order#orderingfilter
e aqui:
https://django-filter.readthedocs.io/en/stable/ref/filters.html?highlight=order#adding -custom-filter-choices

Com base nisso, escrevi este conjunto de filtros aqui:

# ------------------------------------------------------------------------------
# Python Standard Libraries
# ------------------------------------------------------------------------------
# N/A
# ------------------------------------------------------------------------------
# Third-party Libraries
# ------------------------------------------------------------------------------
from django_filters import CharFilter
from django_filters import OrderingFilter
from django_filters.rest_framework import FilterSet
# ------------------------------------------------------------------------------
# Custom Libraries
# ------------------------------------------------------------------------------
from piano_gym_api.versions.v1.models.school_model import SchoolModel

# See:
# https://django-filter.readthedocs.io/en/stable/ref/filters.html#adding-custom-filter-choices
class SchoolOrderingFilter(OrderingFilter):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.extra["choices"] += [
            ("learner_enrolled_count", "Learner Enrolled Count"),
            ("-learner_enrolled_count", "Learner Enrolled Count (descending)"),
        ]

    def filter(self, query_set, values):
        if(values is None):
            return super().filter(query_set, values)

        for value in values:
            if value in ['learner_enrolled_count', '-learner_enrolled_count']:
                return query_set.order_by(value)

        return super().filter(query_set, values)


class SchoolFilter(FilterSet):
    school_name = CharFilter(field_name="name",
                             lookup_expr="icontains")
    # ---
    course_name = CharFilter(field_name="school_course__name",
                             lookup_expr="icontains")
    course_description = CharFilter(field_name="school_course__description",
                                    lookup_expr="icontains")
    # ---
    lesson_name = CharFilter(field_name="school_lesson__name",
                             lookup_expr="icontains")
    lesson_description = CharFilter(field_name="school_lesson__description",
                                    lookup_expr="icontains")
    # ---
    flash_card_set_name = CharFilter(field_name="school_lesson__flash_card_set__name",
                                     lookup_expr="icontains")
    flash_card_set_description = CharFilter(field_name="school_lesson__flash_card_set__description",
                                            lookup_expr="icontains")
    # ---
    headmaster_username = CharFilter(field_name="school_board__school_board_headmaster__learner__user__username",
                                     lookup_expr="icontains")
    board_member_username = CharFilter(field_name="school_board__school_board_member__learner__user__username",
                                       lookup_expr="icontains")

    # See:
    # https://django-filter.readthedocs.io/en/stable/ref/filters.html#orderingfilter
    o = SchoolOrderingFilter(
        # tuple-mapping retains order
        fields=(
            ("learner_enrolled_count", "learner_enrolled_count"),
        ),

        # labels do not need to retain order
        field_labels={
            "learner_enrolled_count": "Total learners enrolled in school",
        }
    )

    class Meta:
        model = SchoolModel
        fields = [
            "school_name",
            # ---
            "course_name",
            "course_description",
            # ---
            "lesson_name",
            "lesson_description",
            # ---
            "headmaster_username",
            "board_member_username"
        ]

Esse problema é que ele não parece estar pedindo nada! Eu não tenho ideia do porquê. É tão estranho.

Se eu soltar um rastreamento de depuração no método filter de SchoolOrderingFilter , vejo que values é None . Não tenho certeza do que deveria ser.

O pedido que estou fazendo é parecido com este
{{API_URL}}/api/v1/schools/?offset=5&limit=3&ordering=learner_enrolled_count

E a vista que recebe essa solicitação tem a seguinte aparência:

class SchoolViewSet(ViewSet):
    # ...

    def list(self, request):
        all_school_models = SchoolModel.objects.getBrowseSchoolsData()

        school_filter = SchoolFilter(request.GET, queryset=all_school_models)

        paginator = HeaderLinkPagination()
        current_page_results = paginator.paginate_queryset(school_filter.qs,
                                                           request)

        all_schools_serializer = SchoolSerializer(current_page_results,
                                                  many=True)
        response_data = {
            "schools": all_schools_serializer.data
        }

        response_to_return = paginator.get_paginated_response(response_data)
        return response_to_return

As questões

Acho que não está muito claro para mim na documentação sobre como usar o recurso de filtragem E como usar a ordenação de campos calculada.

O que estou fazendo de errado? Estou entendendo mal essa funcionalidade? Sinto que estou executando as etapas corretas para isso, mas simplesmente não consigo fazer com que a funcionalidade ordering desta biblioteca funcione!

Mais uma vez, obrigado por tudo!

Comentários muito úteis

Olá, @loganknecht. Eu tenho que dizer, obrigado por um relatório de problema tão maravilhoso. Começa bem e fica ainda melhor. Tão claro. Então, obrigada.

Primeiro: parece que você definiu SchoolOrderingFilter com o nome do campo o :

o = SchoolOrderingFilter(...)

Então, quando você diz:

O pedido que estou fazendo é parecido com este
{{API_URL}}/api/v1/schools/?offset=5&limit=3&ordering=learner_enrolled_count

Estou esperando que o parâmetro da string de consulta seja o também. Isso funciona: {{API_URL}}/api/v1/schools/?offset=5&limit=3&o=learner_enrolled_count ?

Então (a parte que não tenho certeza em seu relatório) você diz que é um _campo computado_ - o que exatamente você quer dizer? ou seja, é um campo que aparece no modelo ou é uma propriedade Python, digamos?

Outra maneira de perguntar (parte de) é que order_by() no queryset funciona com este campo? ou seja, isso funciona:

SchoolModel.objects.filter(...).order_by(learner_enrolled_count)

Todos 4 comentários

Olá, @loganknecht. Eu tenho que dizer, obrigado por um relatório de problema tão maravilhoso. Começa bem e fica ainda melhor. Tão claro. Então, obrigada.

Primeiro: parece que você definiu SchoolOrderingFilter com o nome do campo o :

o = SchoolOrderingFilter(...)

Então, quando você diz:

O pedido que estou fazendo é parecido com este
{{API_URL}}/api/v1/schools/?offset=5&limit=3&ordering=learner_enrolled_count

Estou esperando que o parâmetro da string de consulta seja o também. Isso funciona: {{API_URL}}/api/v1/schools/?offset=5&limit=3&o=learner_enrolled_count ?

Então (a parte que não tenho certeza em seu relatório) você diz que é um _campo computado_ - o que exatamente você quer dizer? ou seja, é um campo que aparece no modelo ou é uma propriedade Python, digamos?

Outra maneira de perguntar (parte de) é que order_by() no queryset funciona com este campo? ou seja, isso funciona:

SchoolModel.objects.filter(...).order_by(learner_enrolled_count)

@carltongibson Isso é bem interessante! Não esperava que o fosse o parâmetro ordering definido! Eu não entendi isso da documentação 😂

Usando o parâmetro o

O que é interessante é se eu usar a consulta
{{API_URL}}/api/v1/schools/?offset=5&limit=3&o=learner_enrolled_count

Eu obtenho estes resultados:

{
    "schools": [
        {
            "id": 6,
            "name": "Piano Gym Six",
            "courses": [
                # ...
            ],
            "learner_enrolled_count": 0
        },
        {
            "id": 8,
            "name": "Piano Gym Eight",
            "courses": [
                # ...
            ],
            "learner_enrolled_count": 0
        },
        {
            "id": 9,
            "name": "Piano Gym Nine",
            "courses": [
                # ...
            ],
            "learner_enrolled_count": 0
        }
    ]
}

A parte interessante sobre isso, agora é que a escola que eu quero que seja ordenada no topo não está neste resultado de pesquisa. O que significa que provavelmente foi pedido, mas a paginação ainda está compensando os resultados. No entanto, quando usei learner_enrolled_count ele está no último lugar.
{{API_URL}}/api/v1/schools/?limit=3&o=learner_enrolled_count

Então eu testei usando -learner_enrolled_count (removendo o deslocamento e classificando em ordem decrescente)
{{API_URL}}/api/v1/schools/?limit=3&o=-learner_enrolled_count
e recebi esta resposta

{
    "schools": [
        {
            "id": 7,
            "name": "Piano Gym Seven",
            "courses": [
                # ...
            ],
            "learner_enrolled_count": 1
        },
        {
            "id": 1,
            "name": "Piano Gym",
            "courses": [
                # ...
            ],
            "learner_enrolled_count": 0
        },
        {
            "id": 2,
            "name": "Piano Gym Two",
            "courses": [
                # ...
            ],
            "learner_enrolled_count": 0
        }
    ]
}

Parece que funcionou!

Campo Computado

learner_enrolled_count não existe no próprio modelo. Usei uma anotação para calcular isso e injetá-la nos resultados.

Correndo o risco de mergulhar muito no modelo de dados, este ponto de extremidade deve ser um ponto de entrada único para filtrar e ordenar consultas para uma lista de escolas.

Tenho que otimizar este endpoint usando a recuperação de modelo assim:

class SchoolModelManager(Manager):
    def getBrowseSchoolsData(self, *args, **kwargs):
        """Return all Schools that contain lessons with flash card sets.

        Does not exclude empty sets, just requires that the school has something
        to enroll in
        """
        # WARNING: This MUST be imported here otherwise the compilation fails
        #          because of circular dependencies
        from piano_gym_api.versions.v1.models.flash_card_model import FlashCardModel
        # from piano_gym_api.versions.v1.models.flash_card_model import PlaySheetMusicFlashCardModel
        # from piano_gym_api.versions.v1.models.flash_card_model import TrueOrFalseFlashCardModel
        # from piano_gym_api.versions.v1.models.flash_card_set_model import FlashCardSetModel
        from piano_gym_api.versions.v1.models.sheet_music_model import SheetMusicModel
        # import pdb
        # pdb.set_trace()

        # --------------------
        sheet_music_query_set = (SheetMusicModel.objects.all()
                                 .select_related("school"))
        play_sheet_music_flash_card_sheet_music_prefetch = Prefetch("playsheetmusicflashcardmodel__sheet_music",
                                                                    sheet_music_query_set)
        # --------------------
        flash_card_query_set = (FlashCardModel.objects.all()
                                .select_related("flash_card_set",
                                                "playsheetmusicflashcardmodel",
                                                "school",
                                                "trueorfalseflashcardmodel")
                                .prefetch_related(play_sheet_music_flash_card_sheet_music_prefetch))
        flash_card_prefetch = Prefetch("flash_card_set__flash_card", flash_card_query_set)
        # --------------------
        school_lesson_query_set = (SchoolLessonModel.objects.all()
                                   .select_related("course",
                                                   "flash_card_set",
                                                   "school")
                                   .prefetch_related(flash_card_prefetch))
        school_lesson_prefetch = Prefetch("school_lesson", school_lesson_query_set)
        # --------------------
        school_course_query_set = (SchoolCourseModel.objects.all()
                                   .select_related("school")
                                   .prefetch_related(school_lesson_prefetch))
        school_course_prefetch = Prefetch("school_course", school_course_query_set)
        # --------------------
        query_set_to_return = (SchoolModel.objects.filter(school_lesson__flash_card_set__isnull=False)
                               .distinct()
                               # .annotate(learner_enrolled_count=Count("learner_enrolled_school", distinct=True))
                               .annotate(learner_enrolled_count=Case(
                                   When(learner_enrolled_school__learner_enrolled_course__learner_enrolled_lesson__is_enrolled=True,
                                        then=1),
                                   default=0,
                                   output_field=IntegerField())
        ).prefetch_related(school_course_prefetch))

        return query_set_to_return

O bit calculated field está na variável query_set_to_return . O que ele faz é levar count do total de alunos matriculados e annotates para o campo learner_enrolled_count . Não tenho ideia se essa é a maneira correta de fazer isso, mas parece funcionar 😂

Sugestão

Uma sugestão que eu faria é esclarecer na documentação, usando exemplos reais de url para solicitações ou texto simples e explicar que o nos exemplos é o que está exposto como um parâmetro para pedido.

Conclusão

Parece estar funcionando corretamente agora que você esclareceu que o parâmetro o é o que o filtro de ordenação é atribuído!

Se querysets que estou fornecendo para filter já tiverem um campo anotado - parece, a partir de meus testes, que não sou obrigado a fazer meu próprio OrderingFilter .

Então, por causa disso, acho que posso usar um simples OrderingFilter vez disso!

Por favor me corrija se eu estiver errado!

Acredito que isso resolve minha confusão! Obrigado!

Olá, @loganknecht. Ótimo, parece que está funcionando. 💃

Se você estiver usando uma anotação, deverá ser capaz de filtrar e ordenar com a classe padrão sim.

Vou fazer um ajuste nos documentos.

Obrigado pela sua contribuição.

@carltongibson e todos os outros, obrigado pela biblioteca incrível!

Esta página foi útil?
0 / 5 - 0 avaliações