Django-filter: Uso poco claro para "OrderingFilter" y campos calculados

Creado en 18 jul. 2020  ·  4Comentarios  ·  Fuente: carltongibson/django-filter

Gratitud

¡Hola!

¡Ha sido un placer utilizar esta biblioteca! ¡Estoy tan emocionado por toda la comodidad que ofrece!

¡Gracias por eso!

Objetivo

Mi objetivo en este momento es tener un punto final que admita la capacidad de búsqueda en los parámetros del modelo y también en los pedidos.

Modelo de datos

Tengo un modelo School que tiene un campo calculado llamado learner_enrolled_count

La respuesta JSON se parece a esto:

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

learner_enrolled_count es un campo calculado.

El problema

He leído la documentación aquí:
https://django-filter.readthedocs.io/en/stable/ref/filters.html?highlight=order#orderingfilter
y aquí:
https://django-filter.readthedocs.io/en/stable/ref/filters.html?highlight=order#adding -custom-filter-options

Entonces, en base a eso, escribí este conjunto de filtros aquí:

# ------------------------------------------------------------------------------
# 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"
        ]

¡Este problema es que no parece estar ordenando nada! No tengo ni idea de porqué. Es tan extraño.

Si coloco un seguimiento de depuración en el método filter de SchoolOrderingFilter , veo que values es None . No estoy seguro de qué debería ser.

La solicitud que estoy haciendo se parece a esto
{{API_URL}}/api/v1/schools/?offset=5&limit=3&ordering=learner_enrolled_count

Y la vista que recibe esta solicitud se ve así:

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

Las preguntas

Creo que no está muy claro para mí en la documentación sobre cómo usar la función de filtrado Y cómo usar el orden de campo calculado.

¿Qué estoy haciendo mal? ¿Estoy entendiendo mal esta funcionalidad? ¡Siento que estoy siguiendo los pasos correctos para esto, pero parece que no puedo hacer que funcione la funcionalidad ordering de esta biblioteca!

Nuevamente, gracias por todo!

Comentario más útil

Hola @loganknecht. Debo decirle, gracias por tan maravilloso informe temático. Empieza bien y simplemente mejora. Tan claro. Así que gracias.

En primer lugar: parece que definió SchoolOrderingFilter con el nombre de campo o :

o = SchoolOrderingFilter(...)

Entonces, cuando dices:

La solicitud que estoy haciendo se parece a esto
{{API_URL}}/api/v1/schools/?offset=5&limit=3&ordering=learner_enrolled_count

Espero que el parámetro de cadena de consulta sea o también. ¿Funciona esto: {{API_URL}}/api/v1/schools/?offset=5&limit=3&o=learner_enrolled_count ?

Entonces (la parte de la que no estoy seguro en su informe) dice que es un _campo calculado_. ¿Qué quiere decir exactamente? es decir, ¿es un campo que aparece en el modelo, o es una propiedad de Python, por ejemplo?

Otra forma de preguntar (parte de) eso es, ¿funciona order_by() en el conjunto de consultas con este campo? es decir, ¿esto funciona?

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

Todos 4 comentarios

Hola @loganknecht. Debo decirle, gracias por tan maravilloso informe temático. Empieza bien y simplemente mejora. Tan claro. Así que gracias.

En primer lugar: parece que definió SchoolOrderingFilter con el nombre de campo o :

o = SchoolOrderingFilter(...)

Entonces, cuando dices:

La solicitud que estoy haciendo se parece a esto
{{API_URL}}/api/v1/schools/?offset=5&limit=3&ordering=learner_enrolled_count

Espero que el parámetro de cadena de consulta sea o también. ¿Funciona esto: {{API_URL}}/api/v1/schools/?offset=5&limit=3&o=learner_enrolled_count ?

Entonces (la parte de la que no estoy seguro en su informe) dice que es un _campo calculado_. ¿Qué quiere decir exactamente? es decir, ¿es un campo que aparece en el modelo, o es una propiedad de Python, por ejemplo?

Otra forma de preguntar (parte de) eso es, ¿funciona order_by() en el conjunto de consultas con este campo? es decir, ¿esto funciona?

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

@carltongibson ¡ Eso es bastante interesante! ¡No esperaba que o fuera el parámetro ordering definido! No entendí eso de la documentación 😂

Usando el parámetro o

Lo interesante es si uso la consulta
{{API_URL}}/api/v1/schools/?offset=5&limit=3&o=learner_enrolled_count

Obtengo estos 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
        }
    ]
}

La parte interesante de esto es que ahora la única escuela que quiero que se ordene en la parte superior no está en este resultado de búsqueda. Lo que significa que probablemente se ordenó, pero la paginación aún está compensando los resultados. Sin embargo, cuando usé learner_enrolled_count está en el último lugar.
{{API_URL}}/api/v1/schools/?limit=3&o=learner_enrolled_count

Así que lo probé usando -learner_enrolled_count (eliminando el desplazamiento y ordenando descendiendo)
{{API_URL}}/api/v1/schools/?limit=3&o=-learner_enrolled_count
y obtuve esta respuesta

{
    "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 funcionó!

Campo calculado

learner_enrolled_count no existe en el modelo en sí. Usé una anotación para calcular esto e inyectarlo en los resultados.

Con el riesgo de sumergirse demasiado en el modelo de datos, este punto final está destinado a ser un punto de entrada singular para filtrar y ordenar consultas para una lista de escuelas.

Tengo que optimizar este punto final usando la recuperación de modelos como este:

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

El bit calculated field está en la variable query_set_to_return . Lo que hace es llevar los count del total de estudiantes inscritos y annotates al campo learner_enrolled_count . No tengo idea de si esta es la forma correcta de lograr esto, pero parece funcionar 😂

Sugerencia

Una sugerencia que haría es aclarar en la documentación, ya sea usando ejemplos de URL reales para solicitudes o texto simple y explicar que o en los ejemplos es lo que se expone como parámetro para ordenar.

Conclusión

¡Parece estar funcionando correctamente ahora que aclaró que el parámetro o es a lo que se asigna el filtro de orden!

Si los querysets que estoy proporcionando para los filter tienen un campo anotado, parece, según mis pruebas, que no estoy obligado a hacer mi propio OrderingFilter .

¡Por eso creo que puedo usar un simple OrderingFilter lugar!

¡Por favor corrígeme si estoy equivocado!

¡Creo que eso resuelve mi confusión! ¡Gracias!

Hola @loganknecht. Super, parece que lo tienes funcionando. 💃

Si está utilizando una anotación, debería poder filtrar y ordenar en ella con la clase estándar sí.

Haré unos retoques en los documentos.

Gracias por tu contribución.

@carltongibson y todos los demás, ¡gracias por la increíble biblioteca!

¿Fue útil esta página
0 / 5 - 0 calificaciones