Django-filter: `OrderingFilter`と計算フィールドの不明確な使用法

作成日 2020年07月18日  ·  4コメント  ·  ソース: carltongibson/django-filter

感謝

こんにちは!

このライブラリを使用することはとても嬉しいことです! 私はそれが提供するすべての便利さにとても興奮しています!

有難うございます!

ゴール

今の私の目標は、モデルパラメータの検索機能と順序付けもサポートするエンドポイントが必要です。

データ・モデル

私がしているSchoolその上で計算されたフィールドを持つモデルが呼ばれるlearner_enrolled_count

JSON応答は次のようになります。

{
    "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は計算フィールドです。

問題

私はここのドキュメントを読みました:
https://django-filter.readthedocs.io/en/stable/ref/filters.html?highlight=order#orderingfilter
そしてここ:
https://django-filter.readthedocs.io/en/stable/ref/filters.html?highlight=order#adding -custom-filter-choices

それで、それに基づいて、私はこのフィルターセットをここに書きました:

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

この問題は、まったく注文していないように見えることです。 理由がわかりません。 それはとても奇妙です。

SchoolOrderingFilterfilterメソッドにデバッグトレースをドロップすると、 valuesNoneことがわかります。 それがどうあるべきかわかりません。

私が行っているリクエストは次のようになります
{{API_URL}}/api/v1/schools/?offset=5&limit=3&ordering=learner_enrolled_count

そして、このリクエストを受け取るビューは次のようになります。

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

質問

フィルタリング機能の使用方法と計算されたフィールドの順序の使用方法に関するドキュメントでは、私には本当に不明確だと思います。

私は何が間違っているのですか? この機能を誤解していますか? これに対して正しい手順を実行しているように感じますが、このライブラリのordering機能を機能させることができないようです。

繰り返しになりますが、すべてに感謝します!

最も参考になるコメント

こんにちは@loganknecht。 素晴らしい問題報告をありがとうございました。 それはうまく始まり、そしてただ良くなります。 とても明確です。 だからありがとう。

まず最初に: SchoolOrderingFilterをフィールド名o定義したようです:

o = SchoolOrderingFilter(...)

だからあなたが言うとき:

私が行っているリクエストは次のようになります
{{API_URL}}/api/v1/schools/?offset=5&limit=3&ordering=learner_enrolled_count

クエリ文字列パラメータもoになると思います。 これは機能しますか: {{API_URL}}/api/v1/schools/?offset=5&limit=3&o=learner_enrolled_count

それから(あなたのレポートで私が確信していないビット)あなたはそれが_計算されたフィールド_であると言います-あなたは正確に何を意味しますか? つまり、モデルに表示されるフィールドですか、それともPythonプロパティが言いますか?

クエリセットのorder_by()がこのフィールドで機能するかどうか(の一部)を尋ねる別の方法はありますか? つまり、これは機能しますか?

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

全てのコメント4件

こんにちは@loganknecht。 素晴らしい問題報告をありがとうございました。 それはうまく始まり、そしてただ良くなります。 とても明確です。 だからありがとう。

まず最初に: SchoolOrderingFilterをフィールド名o定義したようです:

o = SchoolOrderingFilter(...)

だからあなたが言うとき:

私が行っているリクエストは次のようになります
{{API_URL}}/api/v1/schools/?offset=5&limit=3&ordering=learner_enrolled_count

クエリ文字列パラメータもoになると思います。 これは機能しますか: {{API_URL}}/api/v1/schools/?offset=5&limit=3&o=learner_enrolled_count

それから(あなたのレポートで私が確信していないビット)あなたはそれが_計算されたフィールド_であると言います-あなたは正確に何を意味しますか? つまり、モデルに表示されるフィールドですか、それともPythonプロパティが言いますか?

クエリセットのorder_by()がこのフィールドで機能するかどうか(の一部)を尋ねる別の方法はありますか? つまり、これは機能しますか?

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

@carltongibsonそれは非常に興味深いです! oが定義されたorderingパラメーターになるとは思っていません

oパラメーターの使用

興味深いのは、クエリを使用するかどうかです
{{API_URL}}/api/v1/schools/?offset=5&limit=3&o=learner_enrolled_count

私はこれらの結果を得る:

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

これについての興味深い部分は、今、私が一番上に注文したい1つの学校がこの検索結果にないことです。 これは、これが注文された可能性が高いことを意味しますが、ページ付けはまだ結果を相殺しています。 しかし、私がlearner_enrolled_countを使用したとき、それは最後の場所にあります。
{{API_URL}}/api/v1/schools/?limit=3&o=learner_enrolled_count

そこで、 -learner_enrolled_countを使用してテストしました(オフセットを削除し、降順で並べ替えます)
{{API_URL}}/api/v1/schools/?limit=3&o=-learner_enrolled_count
そしてこの応答を得ました

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

うまくいったようです!

計算フィールド

learner_enrolled_countはモデル自体には存在しません。 注釈を使用してこれを計算し、結果に挿入しました。

データモデルに飛び込みすぎるリスクがあるため、このエンドポイントは、学校のリストに対するクエリをフィルタリングおよび順序付けするための単一のエントリポイントとなることを目的としています。

次のようなモデル検索を使用して、このエンドポイントを最適化する必要があります。

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

calculated fieldビットはquery_set_to_return変数にあります。 これは、登録されている学習者の総数のcountを取得し、 annotatesをフィールドlearner_enrolled_countます。 これがこれを達成するための正しい方法であるかどうかはわかりませんが、うまくいくようです😂

提案

私が行う提案の1つは、リクエストの実際のURLの例または単純なテキストを使用してドキュメントで明確にし、例のoが注文のパラメーターとして公開されていることを説明することです。

結論

oパラメーターが順序付けフィルターに割り当てられるものであることを明確にしたので、正しく機能しているように見えます。

もしquerysets私が提供していますfilterすでにそれらに注釈付きフィールドを持っている-それは私のテストから、見えます、私は私自身のカスタムすることを義務付けておりませんことをOrderingFilter

そのため、代わりに単純なOrderingFilter使用できると思います。

私が間違っている場合は私を訂正してください!

私はそれが私の混乱を解決すると信じています! ありがとうございました!

こんにちは@loganknecht。 スーパー、あなたはそれが機能しているように見えます。 💃

アノテーションを使用している場合は、標準クラスyesを使用してアノテーションをフィルタリングおよび順序付けできるはずです。

ドキュメントを微調整します。

ご意見ありがとうございます。

@carltongibsonと他のみんな、素晴らしいライブラリに感謝します!

このページは役に立ちましたか?
0 / 5 - 0 評価