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。 我不得不说,感谢您提供如此精彩的问题报告。 它开始得很好,而且只会变得更好。 就是这么清楚。 所以谢谢。

首先:看起来您使用字段名称o定义了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

然后(我在你的报告中不确定这一点)你说它是一个 _computed field_——你到底是什么意思? 即它是出现在模型上的一个字段,还是一个 Python 属性?

另一种询问(部分)的方式是查询集上的order_by()是否与此字段一起使用? 即做这项工作:

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

所有4条评论

嗨@loganknecht。 我不得不说,感谢您提供如此精彩的问题报告。 它开始得很好,而且只会变得更好。 就是这么清楚。 所以谢谢。

首先:看起来您使用字段名称o定义了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

然后(我在你的报告中不确定这一点)你说它是一个 _computed field_——你到底是什么意思? 即它是出现在模型上的一个字段,还是一个 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
        }
    ]
}

有趣的是,现在我想排在最前面的一所学校不在此搜索结果中。 这意味着这可能是有序的,但分页仍在抵消结果。 但是,当我使用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变量中。 它所做的是将注册的学习者总数的countannotates带到字段learner_enrolled_count 。 我不知道这是否是实现这一目标的正确方法,但它似乎确实有效😂

建议

我提出的一个建议是在文档中澄清,要么使用实际的 url 示例请求或简单的文本,并解释示例中的o是作为排序参数公开的内容。

结论

现在您澄清了o参数是排序过滤器被分配给的内容,它似乎工作正常!

如果querysets我提供的filter有一个注释字段,他们已经-它看起来,从我的测试中,我没有义务使自己的自定义OrderingFilter .

因此,我认为我可以使用简单的OrderingFilter来代替!

如果我错了,请纠正我!

我相信这可以解决我的困惑! 谢谢!

嗨@loganknecht。 超级,看起来你已经成功了。 💃

如果您使用的是注释,那么您应该能够使用标准类 yes 对其进行过滤和排序。

我将对文档进行调整。

感谢您的输入。

@carltongibson和其他所有人,感谢您的精彩图书馆!

此页面是否有帮助?
0 / 5 - 0 等级