如何在相关字段上使用日期过滤器在用户的 django 管理面板中获得统计信息?

How can I have statistics in django admin panel on User with date filter on related field?

相关型号

class AbstractTask(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    issued_at = models.DateTimeField(auto_now_add=True)

问题

我需要在管理面板中每天显示一些 User 统计信息。假设我只需要发布的任务数。而且我需要能够按发行日期过滤它(昨天、前天发行了多少,等等)。

我是怎么做到的

我使用 User 代理模型为不同的统计页面注册 ModelAdmin

我在 task__issued_at 字段上使用略微修改(更改的日期范围)DateFieldListFilter

list_filter = [
    ('task__issued_at', DateFieldListFilter),
    'username',
]

日期字段的过滤器不起作用

过滤器不起作用,因为它们最终会生成类似于以下内容的查询:

queryset = (User.objects
    .annotate(
        # Different statistics.
        num_tasks=Count('task'),
    )
    .filter(
        # DateFieldListFilter.
        task__issued_at__gte='2020-01-01',
        task__issued_at__lt='2020-01-02',
    )
    .values('id', 'num_tasks')
)

SQL:

SELECT "auth_user"."id",
       COUNT("task"."id") AS "num_tasks"
FROM "auth_user"
LEFT OUTER JOIN "task" ON ("auth_user"."id" = "task"."user_id")
INNER JOIN "task" T3 ON ("auth_user"."id" = T3."user_id")
WHERE (T3."issued_at" >= 2020-01-01 00:00:00+03:00
       AND T3."issued_at" < 2020-01-02 00:00:00+03:00)
GROUP BY "auth_user"."id"

问题是当我只需要一个时,过滤器会在 table“任务”上添加第二个连接。

通过添加 .filter(task__isnull=False) 强制第一个内部联接没有帮助。它只是继续执行两个相同的内部连接。

django 2 和 3 中的行为相同。

可以在Django中完成吗? 最好尽可能简单:没有原始 sql,没有太多魔法,继续使用 DateFieldListFilter。 但任何解决方案都会有所帮助。

下面的替代 QuerySet 给出了相同的结果,没有任何额外的 joins:

(queryset = User.objects
    .annotate(
        # Different statistics.
        num_tasks=Count(
            'task', 
            filter=models.Q(
                Q(task__issued_at__gte='2020-01-01') & 
                Q(task__issued_at__lt='2020-01-02')
            )
        ),
    )
    .values('id', 'num_tasks')
)

SQL:

SELECT "auth_user"."id", COUNT("task"."id") 
FILTER (WHERE ("task"."issued_at" >= 2020-01-01 00:00:00+03:00 AND "task"."issed_at" < 2020-01-02 00:00:00+03:00)) AS "num_tasks" 
FROM "auth_user"
LEFT OUTER JOIN "task" ON ("auth_user"."id" = "task"."user_id")
GROUP BY "auth_user"."id"

但不确定与您的性能相比。

无论如何,要使其与 DateFieldListFilter 一起工作,您只需重写 queryset 方法:

class CustomDateFieldListFilter(DateFieldListFilter):
    def queryset(self, request, queryset):
        # Compare the requested value to decide how to filter the queryset.
        q_objects = models.Q()
        for key, value in self.used_parameters.items():
            q_objects &= models.Q(**{key: value})
        return queryset.annotate(num_tasks=Count('task', filter=models.Q(q_objects))).values('id', 'num_tasks')

并指定新的 class:

list_filter = [
    ('task__issued_at', CustomDateFieldListFilter),
    ...
]

就是这样。