Django-filter: Utilisation peu claire pour "OrderingFilter" et les champs calculés

Créé le 18 juil. 2020  ·  4Commentaires  ·  Source: carltongibson/django-filter

Reconnaissance

Bonjour!

Ce fut un plaisir d'utiliser cette bibliothèque ! Je suis tellement ravie de toute la commodité qu'il offre!

Merci pour ça!

But

Mon objectif en ce moment est que je souhaite avoir un point de terminaison qui prend en charge la capacité de recherche sur les paramètres du modèle, ainsi que la commande.

Modèle de données

J'ai un modèle School qui contient un champ calculé appelé learner_enrolled_count

La réponse JSON ressemble à ceci :

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

Le learner_enrolled_count est un champ calculé.

Le problème

J'ai lu la documentation ici :
https://django-filter.readthedocs.io/en/stable/ref/filters.html?highlight=order#orderingfilter
et ici:
https://django-filter.readthedocs.io/en/stable/ref/filters.html?highlight=order#adding -custom-filter-choices

Donc, sur cette base, j'ai écrit ce filtre défini ici :

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

Ce problème est qu'il ne semble pas commander du tout ! Je ne sais pas pourquoi. C'est si étrange.

Si je dépose une trace de débogage dans la méthode filter de SchoolOrderingFilter je vois que values est None . Je ne sais pas ce que cela devrait être.

La demande que je fais ressemble à ça
{{API_URL}}/api/v1/schools/?offset=5&limit=3&ordering=learner_enrolled_count

Et la vue qui reçoit cette requête ressemble à ceci :

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

Questions

Je pense que ce n'est vraiment pas clair pour moi dans la documentation sur la façon d'utiliser la fonction de filtrage ET comment utiliser l'ordre des champs calculé.

Qu'est-ce que je fais mal? Est-ce que je comprends mal cette fonctionnalité ? J'ai l'impression d'effectuer les étapes correctes pour cela, mais je n'arrive pas à faire fonctionner la fonctionnalité ordering de cette bibliothèque !

Encore merci pour tout !

Commentaire le plus utile

Salut @loganknecht. Je dois dire, merci pour ce merveilleux rapport de problème. Cela commence bien et s'améliore. Tellement clair. Alors merci.

Tout d'abord : il semble que vous ayez défini le SchoolOrderingFilter avec le nom de champ o :

o = SchoolOrderingFilter(...)

Alors quand tu dis :

La demande que je fais ressemble à ça
{{API_URL}}/api/v1/schools/?offset=5&limit=3&ordering=learner_enrolled_count

Je m'attends à ce que le paramètre de chaîne de requête soit également o . Est-ce que ça marche : {{API_URL}}/api/v1/schools/?offset=5&limit=3&o=learner_enrolled_count ?

Ensuite (le peu dont je ne suis pas sûr dans votre rapport) vous dites que c'est un _champ calculé_ -- que voulez-vous dire exactement ? c'est-à-dire est-ce un champ qui apparaît sur le modèle, ou est-ce une propriété Python disons ?

Une autre façon de demander (une partie de) est-ce que order_by() sur le jeu de requêtes fonctionne avec ce champ ? c'est-à-dire est-ce que ça marche:

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

Tous les 4 commentaires

Salut @loganknecht. Je dois dire, merci pour ce merveilleux rapport de problème. Cela commence bien et s'améliore. Tellement clair. Alors merci.

Tout d'abord : il semble que vous ayez défini le SchoolOrderingFilter avec le nom de champ o :

o = SchoolOrderingFilter(...)

Alors quand tu dis :

La demande que je fais ressemble à ça
{{API_URL}}/api/v1/schools/?offset=5&limit=3&ordering=learner_enrolled_count

Je m'attends à ce que le paramètre de chaîne de requête soit également o . Est-ce que ça marche : {{API_URL}}/api/v1/schools/?offset=5&limit=3&o=learner_enrolled_count ?

Ensuite (le peu dont je ne suis pas sûr dans votre rapport) vous dites que c'est un _champ calculé_ -- que voulez-vous dire exactement ? c'est-à-dire est-ce un champ qui apparaît sur le modèle, ou est-ce une propriété Python disons ?

Une autre façon de demander (une partie de) est-ce que order_by() sur le jeu de requêtes fonctionne avec ce champ ? c'est-à-dire est-ce que ça marche:

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

@carltongibson C'est assez intéressant ! Je ne m'attendais pas à ce que o soit le paramètre ordering défini ! Je n'ai pas compris ça dans la documentation

Utilisation du paramètre o

Ce qui est intéressant, c'est si j'utilise la requête
{{API_URL}}/api/v1/schools/?offset=5&limit=3&o=learner_enrolled_count

J'obtiens ces résultats :

{
    "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 partie intéressante à ce sujet est maintenant que l'école que je veux être classée en haut n'est pas dans ce résultat de recherche. Ce qui signifie que cela a probablement été commandé mais que la pagination compense toujours les résultats. Cependant, lorsque j'ai utilisé learner_enrolled_count c'est à la dernière place.
{{API_URL}}/api/v1/schools/?limit=3&o=learner_enrolled_count

Je l'ai donc testé en utilisant le -learner_enrolled_count (en supprimant le décalage et en triant par ordre décroissant)
{{API_URL}}/api/v1/schools/?limit=3&o=-learner_enrolled_count
et j'ai eu cette réponse

{
    "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
        }
    ]
}

On dirait que ça a marché !

Champ calculé

learner_enrolled_count n'existe pas sur le modèle lui-même. J'ai utilisé une annotation pour calculer cela et l'injecter dans les résultats.

Au risque de trop plonger dans le modèle de données, ce point de terminaison est censé être un point d'entrée unique pour filtrer et ordonner les requêtes pour une liste d'écoles.

Je dois optimiser ce point de terminaison en utilisant la récupération de modèle comme ceci :

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

Le bit calculated field est dans la variable query_set_to_return . Il prend le count du total des apprenants inscrits et le annotates dans le champ learner_enrolled_count . Je ne sais pas si c'est la bonne façon d'y parvenir, mais cela semble fonctionner

Suggestion

Une suggestion que je ferais est de clarifier dans la documentation, soit en utilisant des exemples d'URL réels pour les demandes, soit du texte simple et d'expliquer que le o dans les exemples est ce qui est exposé comme paramètre de commande.

Conclusion

Cela semble fonctionner correctement maintenant que vous avez clarifié le paramètre o auquel le filtre de classement est affecté !

Si le querysets que je fournis pour le filter déjà un champ annoté - il semble, d'après mes tests, que je ne suis pas obligé de créer mon propre OrderingFilter .

Donc, à cause de cela, je pense que je peux simplement utiliser un simple OrderingFilter place !

Corrigez-moi si j'ai tort, s'il-vous plait!

Je crois que cela résout ma confusion! Merci!

Salut @loganknecht. Super, on dirait que ça marche. ??

Si vous utilisez une annotation, vous devriez pouvoir filtrer et ordonner celle-ci avec la classe standard yes.

Je vais faire un tweak à la doc.

Merci pour votre contribution.

@carltongibson et tous les autres, merci pour l'incroyable bibliothèque !

Cette page vous a été utile?
0 / 5 - 0 notes

Questions connexes

sassanh picture sassanh  ·  4Commentaires

csarcom picture csarcom  ·  3Commentaires

jnegro picture jnegro  ·  3Commentaires

Alexx-G picture Alexx-G  ·  4Commentaires

edmorley picture edmorley  ·  3Commentaires