Django Rest Framework:当默认排序字段可为空时,Paginaion 详细视图中断

Django Rest Framework: Paginaion in detail view breaks when default ordering field is nullable

我有一个用于竞赛的 Django 应用程序,其中一个竞赛可以有多个条目 -

Contest 在 models.py

class Contest(models.Model):
    is_winners_announced = models.BooleanField(default=False)
    ...

ContestEntry 在 models.py

class ContestEntry(models.Model):
    contest = models.ForeignKey(Contest, on_delete=models.CASCADE,
                                related_name='entries')
    submitted_at = models.DateTimeField(auto_now_add=True)
    assigned_rank = models.PositiveSmallIntegerField(null=True, blank=True)
    ...

在 ContestViewSet 中,我有一个 detail 路由,它为比赛的所有条目提供服务 -

def pagination_type_by_field(PaginationClass, field):
    class CustomPaginationClass(PaginationClass):
        ordering = field
    return CustomPaginationClass

...

@decorators.action(
    detail=True,
    methods=['GET'],
)
def entries(self, request, pk=None):
    contest = self.get_object()
    entries = contest.entries.all()

    # Order by rank only if winners are announced
    ordering_array = ['-submitted_at']
    if contest.is_winners_announced:
        ordering_array.insert(0, 'assigned_rank')
    pagination_obj = pagination_type_by_field(
        pagination.CursorPagination, ordering_array)()
    paginated_data = contest_serializers.ContestEntrySerializer(
        instance=pagination_obj.paginate_queryset(entries, request),
        many=True,
        context={'request': request},
    ).data

    return pagination_obj.get_paginated_response(paginated_data)

当未宣布比赛获胜者时,分页工作正常 -

GET http://localhost:8000/contests/<id>/entries/

{
    "next": "http://localhost:8000/contests/<id>/entries/?cursor=cD0yMDIwLTAyLTE3KzIwJTNBNDQlM0EwNy4yMDMyMTUlMkIwMCUzQTAw",
    "previous": null,
    "results": [  // Contains all objects and pagination works
        {...},
        ...
    ]
}

但是当宣布获奖者时,分页中断:

GET http://localhost:8000/contests/<id>/entries/

{
    "next": "https://localhost:8000/contests/4/entries/?cursor=bz03JnA9Mw%3D%3D",
    "previous": null,
    "results": [  // Contains all objects only for the first page; next page is empty even when there are more entries pending to be displayed
        {...},
        ...
    ]
}

我在这里看到的奇怪的事情是第二种情况下的光标看起来与通常看起来不同。

终于找到了解决问题的方法。

由于最重要的排序字段(即 assigned_rank)中的空值,分页失败。

转到下一页时,游标分页会尝试根据上一页的最低值计算数据库中的下一行 -

if self.cursor.reverse != is_reversed:
    kwargs = {order_attr + '__lt': current_position}
else:
    kwargs = {order_attr + '__gt': current_position}

queryset = queryset.filter(**kwargs)

Internal Implementation

因此,所有行都被过滤掉了。


为了防止这种情况,我们可以设置一个假排名,它不会是 None,如果实际排名是 None,也不会影响排序。这个后备排名可以是 max(all_ranks) + 1 -

from django.db.models.functions import Coalesce
...

@decorators.action(
    detail=True,
    methods=['GET'],
)
def entries(self, request, pk=None):
    contest = self.get_object()
    entries = contest.entries.all()

    ordering = ('-submitted_at',)
    if contest.is_winners_announced:
        max_rank = entries.aggregate(
            Max('assigned_rank')
        )['assigned_rank__max']
        next_rank = max_rank + 1
        entries = entries.annotate(
            pseudo_rank=Coalesce('assigned_rank', next_rank))
        ordering = ('pseudo_rank',) + ordering
    ...  # same as before

Read more about Coalesce.

这将导致将所有条目的等级设置为比分配最差等级的条目高 1。例如,如果我们分配了排名 1、2 和 3,则所有其他条目的 pseudo_rank 将为 4,它们将按 -submitted_at.

排序

然后正如他们所说,"It worked like charm ✨"。