Django-filter: Penggunaan Tidak Jelas Untuk `OrderingFilter` dan Kolom Terhitung

Dibuat pada 18 Jul 2020  ·  4Komentar  ·  Sumber: carltongibson/django-filter

Rasa syukur

Halo!

Sangat menyenangkan menggunakan perpustakaan ini! Saya sangat senang dengan semua kemudahan yang ditawarkannya!

Terima kasih untuk itu!

Sasaran

Tujuan saya saat ini adalah saya ingin memiliki titik akhir yang mendukung kemampuan pencarian pada parameter model, dan juga pemesanan.

Model data

Saya memiliki model School yang memiliki bidang terhitung yang disebut learner_enrolled_count

Respons JSON terlihat seperti ini:

{
    "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 adalah bidang terhitung.

Masalah

Saya telah membaca dokumentasi di sini:
https://django-filter.readthedocs.io/en/stable/ref/filters.html?highlight=order#orderingfilter
dan di sini:
https://django-filter.readthedocs.io/en/stable/ref/filters.html?highlight=order#adding -custom-filter-choices

Jadi berdasarkan itu saya menulis set filter ini di sini:

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

Masalah ini tampaknya tidak memesan sama sekali! Saya tidak tahu mengapa. Ini sangat aneh.

Jika saya memasukkan jejak debug ke dalam metode filter dari SchoolOrderingFilter Saya melihat bahwa values adalah None . Saya tidak yakin apa yang seharusnya.

Permintaan yang saya buat terlihat seperti ini
{{API_URL}}/api/v1/schools/?offset=5&limit=3&ordering=learner_enrolled_count

Dan tampilan yang menerima permintaan ini terlihat seperti ini:

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

Pertanyaan-pertanyaan

Saya pikir itu benar-benar tidak jelas bagi saya dalam dokumentasi tentang cara menggunakan fitur pemfilteran DAN cara menggunakan pengurutan bidang terhitung.

Apa yang saya lakukan salah? Apakah saya salah memahami fungsi ini? Saya merasa seperti saya melakukan langkah-langkah yang benar untuk ini, tetapi sepertinya tidak bisa membuat fungsi ordering dari perpustakaan ini berfungsi!

Sekali lagi, terima kasih untuk semuanya!

Komentar yang paling membantu

Hai @loganknecht. Saya harus mengatakan, terima kasih atas laporan masalah yang luar biasa. Ini dimulai dengan baik, dan menjadi lebih baik. Hanya begitu jelas. Jadi terima kasih.

Pertama: sepertinya Anda mendefinisikan SchoolOrderingFilter dengan nama bidang o :

o = SchoolOrderingFilter(...)

Jadi ketika Anda mengatakan:

Permintaan yang saya buat terlihat seperti ini
{{API_URL}}/api/v1/schools/?offset=5&limit=3&ordering=learner_enrolled_count

Saya mengharapkan parameter string kueri menjadi o juga. Apakah ini berfungsi: {{API_URL}}/api/v1/schools/?offset=5&limit=3&o=learner_enrolled_count ?

Kemudian (sedikit yang saya tidak yakin dalam laporan Anda) Anda mengatakan itu _bidang yang dihitung_ -- apa sebenarnya yang Anda maksud? yaitu apakah itu bidang yang muncul pada model, atau apakah itu properti Python?

Cara bertanya lain (bagian dari) yaitu apakah order_by() pada queryset berfungsi dengan bidang ini? yaitu apakah ini bekerja:

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

Semua 4 komentar

Hai @loganknecht. Saya harus mengatakan, terima kasih atas laporan masalah yang luar biasa. Ini dimulai dengan baik, dan menjadi lebih baik. Hanya begitu jelas. Jadi terima kasih.

Pertama: sepertinya Anda mendefinisikan SchoolOrderingFilter dengan nama bidang o :

o = SchoolOrderingFilter(...)

Jadi ketika Anda mengatakan:

Permintaan yang saya buat terlihat seperti ini
{{API_URL}}/api/v1/schools/?offset=5&limit=3&ordering=learner_enrolled_count

Saya mengharapkan parameter string kueri menjadi o juga. Apakah ini berfungsi: {{API_URL}}/api/v1/schools/?offset=5&limit=3&o=learner_enrolled_count ?

Kemudian (sedikit yang saya tidak yakin dalam laporan Anda) Anda mengatakan itu _bidang yang dihitung_ -- apa sebenarnya yang Anda maksud? yaitu apakah itu bidang yang muncul pada model, atau apakah itu properti Python?

Cara bertanya lain (bagian dari) yaitu apakah order_by() pada queryset berfungsi dengan bidang ini? yaitu apakah ini bekerja:

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

@carltongibson Itu cukup menarik! Saya tidak berharap o menjadi parameter ordering ditentukan! Saya tidak mengerti itu dari dokumentasi 😂

Menggunakan parameter o

Yang menarik adalah jika saya menggunakan kueri
{{API_URL}}/api/v1/schools/?offset=5&limit=3&o=learner_enrolled_count

Saya mendapatkan hasil ini:

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

Yang menarik dari ini, sekarang sekolah yang ingin saya urutkan paling atas tidak ada di hasil pencarian ini. Yang berarti bahwa ini mungkin dipesan tetapi pagination masih mengimbangi hasilnya. Namun ketika saya menggunakan learner_enrolled_count itu di tempat terakhir.
{{API_URL}}/api/v1/schools/?limit=3&o=learner_enrolled_count

Jadi saya menggunakan mengujinya menggunakan -learner_enrolled_count (menghapus offset dan mengurutkan dengan turun)
{{API_URL}}/api/v1/schools/?limit=3&o=-learner_enrolled_count
dan mendapat tanggapan ini

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

Sepertinya itu berhasil!

Bidang Komputasi

learner_enrolled_count tidak ada pada model itu sendiri. Saya menggunakan anotasi untuk menghitung ini dan memasukkannya ke dalam hasil.

Dengan risiko menyelami model data terlalu banyak, titik akhir ini dimaksudkan sebagai titik masuk tunggal untuk memfilter dan memesan kueri untuk daftar sekolah.

Saya harus mengoptimalkan titik akhir ini menggunakan pengambilan model seperti ini:

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

The calculated field bit adalah di query_set_to_return variabel. Apa yang dilakukannya adalah mengambil count dari total pelajar yang terdaftar dan annotates ke bidang learner_enrolled_count . Saya tidak tahu apakah ini cara yang benar untuk mencapai ini, tetapi tampaknya berhasil 😂

Saran

Satu saran yang akan saya buat adalah mengklarifikasi dalam dokumentasi, baik menggunakan contoh url aktual untuk permintaan atau teks sederhana dan menjelaskan bahwa o dalam contoh adalah apa yang diekspos sebagai parameter untuk pemesanan.

Kesimpulan

Tampaknya berfungsi dengan benar sekarang setelah Anda mengklarifikasi parameter o adalah apa yang ditugaskan oleh filter pemesanan!

Jika querysets saya sediakan untuk filter sudah memiliki bidang beranotasi di dalamnya - tampaknya, dari pengujian saya, saya tidak berkewajiban membuat OrderingFilter kustom saya sendiri

Jadi karena itu saya pikir saya bisa menggunakan OrderingFilter saja!

Tolong koreksi saya jika saya salah!

Saya percaya itu memecahkan kebingungan saya! Terima kasih!

Hai @loganknecht. Super, sepertinya Anda berhasil. 💃

Jika Anda menggunakan anotasi maka Anda harus dapat memfilter dan memesannya dengan kelas standar ya.

Saya akan membuat tweak ke dokumen.

Terima kasih atas masukan Anda.

@carltongibson dan semua orang, terima kasih untuk perpustakaan yang luar biasa!

Apakah halaman ini membantu?
0 / 5 - 0 peringkat