Как получить статистику в админ-панели django пользователя с фильтром даты в соответствующем поле? - PullRequest
1 голос
/ 14 апреля 2020

Родственная модель

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

Проблема

Мне нужно показывать статистику User в день в админ-панели. Допустим, мне просто нужно количество выданных заданий. И мне нужно иметь возможность фильтровать его по дате выпуска (сколько было выпущено вчера, позавчера, и т. Д. c).

Как я пытаюсь это сделать

I используйте User модели прокси для регистрации ModelAdmin для различных страниц статистики.

Я использую слегка измененные (измененные диапазоны дат) DateFieldListFilter в поле task__issued_at:

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"

Проблема в том, что фильтр добавляет второе объединение к таблице «задача», когда мне нужен только один.

Форсирование первого внутреннего соединения путем добавления .filter(task__isnull=False) не помогает. Он просто выполняет два одинаковых внутренних соединения.

Такое же поведение в django 2 и 3.

Можно ли это сделать в Django? Желательно максимально просто: без необработанных sql, без особых волхвов c и с продолжением использования DateFieldListFilter. Но любое решение поможет.

1 Ответ

1 голос
/ 14 апреля 2020

Альтернативный QuerySet ниже дает тот же результат без каких-либо дополнительных объединений :

(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')

и указать новый класс:

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

Вот и все.

...