Django-filter: Unklare Verwendung für `OrderingFilter` und berechnete Felder

Erstellt am 18. Juli 2020  ·  4Kommentare  ·  Quelle: carltongibson/django-filter

Dankbarkeit

Hallo!

Es war so ein Vergnügen, diese Bibliothek zu benutzen! Ich bin so begeistert von all dem Komfort, den es bietet!

Danke für das!

Ziel

Mein Ziel ist es im Moment, einen Endpunkt zu haben, der die Suchfähigkeit von Modellparametern und auch die Sortierung unterstützt.

Datenmodell

Ich habe ein School Modell mit einem berechneten Feld namens learner_enrolled_count

Die JSON-Antwort sieht in etwa so aus:

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

Das learner_enrolled_count ist ein berechnetes Feld.

Das Problem

Ich habe die Dokumentation hier gelesen:
https://django-filter.readthedocs.io/en/stable/ref/filters.html?highlight=order#orderingfilter
und hier:
https://django-filter.readthedocs.io/en/stable/ref/filters.html?highlight=order#adding -custom-filter-choices

Darauf aufbauend habe ich diesen Filtersatz hier geschrieben:

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

Dieses Problem ist, dass es anscheinend überhaupt nicht bestellt wird! Ich habe keine Idee warum. Es ist so komisch.

Wenn ich einen Debug-Trace in der Methode filter von SchoolOrderingFilter ablege, sehe ich, dass values None . Ich bin mir nicht sicher was das sein soll.

Meine Anfrage sieht so aus
{{API_URL}}/api/v1/schools/?offset=5&limit=3&ordering=learner_enrolled_count

Und die Ansicht, die diese Anfrage erhält, sieht so aus:

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

Die Fragen

Ich denke, es ist für mich in der Dokumentation wirklich unklar, wie man die Filterfunktion UND die berechnete Feldreihenfolge verwendet.

Was mache ich falsch? Verstehe ich diese Funktion falsch? Ich habe das Gefühl, dass ich die richtigen Schritte dafür ausführe, aber ich kann die ordering Funktionalität dieser Bibliothek einfach nicht zum Laufen bringen!

Nochmals vielen Dank für alles!

Hilfreichster Kommentar

Hallo @loganknecht. Ich muss sagen, vielen Dank für diesen wunderbaren Themenbericht. Es fängt gut an und wird immer besser. Einfach so klar. Also vielen Dank.

Zunächst einmal: Es sieht so aus, als ob Sie SchoolOrderingFilter mit dem Feldnamen o :

o = SchoolOrderingFilter(...)

Also wenn du sagst:

Meine Anfrage sieht so aus
{{API_URL}}/api/v1/schools/?offset=5&limit=3&ordering=learner_enrolled_count

Ich erwarte, dass der Abfragezeichenfolgenparameter auch o ist. Funktioniert das: {{API_URL}}/api/v1/schools/?offset=5&limit=3&o=learner_enrolled_count ?

Dann (wobei ich mir in Ihrem Bericht unsicher bin) sagen Sie, es sei ein _berechnetes Feld_ -- was genau meinst du? Dh ist es ein Feld, das im Modell angezeigt wird, oder ist es eine Python-Eigenschaft, sagen wir?

Eine andere Möglichkeit, (einen Teil davon) zu fragen, ist, ob order_by() im Abfragesatz mit diesem Feld funktioniert? dh funktioniert das:

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

Alle 4 Kommentare

Hallo @loganknecht. Ich muss sagen, vielen Dank für diesen wunderbaren Themenbericht. Es fängt gut an und wird immer besser. Einfach so klar. Also vielen Dank.

Zunächst einmal: Es sieht so aus, als ob Sie SchoolOrderingFilter mit dem Feldnamen o :

o = SchoolOrderingFilter(...)

Also wenn du sagst:

Meine Anfrage sieht so aus
{{API_URL}}/api/v1/schools/?offset=5&limit=3&ordering=learner_enrolled_count

Ich erwarte, dass der Abfragezeichenfolgenparameter auch o ist. Funktioniert das: {{API_URL}}/api/v1/schools/?offset=5&limit=3&o=learner_enrolled_count ?

Dann (wobei ich mir in Ihrem Bericht unsicher bin) sagen Sie, es sei ein _berechnetes Feld_ -- was genau meinst du? Dh ist es ein Feld, das im Modell angezeigt wird, oder ist es eine Python-Eigenschaft, sagen wir?

Eine andere Möglichkeit, (einen Teil davon) zu fragen, ist, ob order_by() im Abfragesatz mit diesem Feld funktioniert? dh funktioniert das:

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

@carltongibson Das ist ziemlich interessant! Ich habe nicht erwartet, dass o der definierte Parameter ordering ist! Das habe ich aus der Dokumentation nicht verstanden 😂

Verwenden des Parameters o

Interessant ist, wenn ich die Abfrage verwende
{{API_URL}}/api/v1/schools/?offset=5&limit=3&o=learner_enrolled_count

Ich bekomme diese Ergebnisse:

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

Das Interessante daran ist, dass jetzt die eine Schule, die ich ganz oben ordnen möchte, nicht in diesem Suchergebnis enthalten ist. Das bedeutet, dass dies wahrscheinlich bestellt wurde, aber die Paginierung gleicht die Ergebnisse noch aus. Wenn ich jedoch learner_enrolled_count es an der letzten Stelle.
{{API_URL}}/api/v1/schools/?limit=3&o=learner_enrolled_count

Also habe ich es mit dem -learner_enrolled_count getestet (Entfernen des Offsets und Sortieren nach absteigend)
{{API_URL}}/api/v1/schools/?limit=3&o=-learner_enrolled_count
und habe diese antwort bekommen

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

Es sieht so aus, als hätte es funktioniert!

Berechnetes Feld

learner_enrolled_count existiert nicht auf dem Modell selbst. Ich habe eine Annotation verwendet, um dies zu berechnen und in die Ergebnisse einzufügen.

Da die Gefahr besteht, dass Sie zu sehr in das Datenmodell eintauchen, ist dieser Endpunkt als einzelner Einstiegspunkt zum Filtern und Ordnen von Abfragen für eine Liste von Schulen gedacht.

Ich muss diesen Endpunkt mithilfe des Modellabrufs wie folgt optimieren:

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

Das Bit calculated field befindet sich in der Variablen query_set_to_return . Es nimmt die count der gesamten eingeschriebenen Lernenden und annotates in das Feld learner_enrolled_count . Ich habe keine Ahnung, ob das der richtige Weg ist, aber es scheint zu funktionieren 😂

Anregung

Ein Vorschlag, den ich machen würde, besteht darin, dies in der Dokumentation klarzustellen, indem Sie entweder tatsächliche URL-Beispiele für Anforderungen oder einfachen Text verwenden und erklären, dass o in den Beispielen als Parameter für die Bestellung bereitgestellt wird.

Abschluss

Es scheint jetzt richtig zu funktionieren, da Sie den Parameter o geklärt haben, dem der Sortierfilter zugewiesen wird!

Wenn die querysets ich für die filter bereitstelle, bereits ein kommentiertes Feld enthalten - aus meinen Tests sieht es so aus, dass ich nicht verpflichtet bin, meine eigenen benutzerdefinierten OrderingFilter .

Deshalb denke ich, dass ich stattdessen einfach ein einfaches OrderingFilter verwenden kann!

Bitte korrigiere mich wenn ich falsch liege!

Ich glaube, das löst meine Verwirrung! Dankeschön!

Hallo @loganknecht. Super, sieht so aus, als ob es bei dir funktioniert. 💃

Wenn Sie eine Annotation verwenden, sollten Sie diese mit der Standardklasse yes filtern und sortieren können.

Ich werde die Dokumente anpassen.

Danke für deinen Beitrag.

@carltongibson und alle anderen, danke für die tolle Bibliothek!

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen