GROUP By в Django ORM для страницы в Django Admin

Я не очень долго в Django, извините за возможно глупый вопрос. Но после многих часов попыток решения и огромного количества поисков в интернете, я не нашел решения.

Мои модели:

class Offer(models.Model):
    seller = models.ForeignKey()<..>
    # other fields
class OfferViewCount(models.Model):
    offer = models.ForeignKey(Offer, verbose_name=_('Offer'), on_delete=models.CASCADE)
    user_agent = models.CharField(verbose_name=_('User Agent'), max_length=200)
    ip_address = models.CharField(verbose_name=_('IP Address'), max_length=32)
    created_date = models.DateTimeField(auto_now_add=True)

В базе данных модели OfferViewCount имеются следующие данные:

id;user_agent;ip_address;created_date;offer_id
24;insomnia/2022.6.0f;127.0.0.1;2022-11-18 14:14:52.501008+00;192
25;insomnia/2022.6.0z;127.0.0.1;2022-11-18 15:03:31.471366+00;192
23;insomnia/2022.6.0;127.0.0.1;2022-11-18 14:14:49.840141+00;193
28;insomnia/2022.6.0;127.0.0.1;2022-11-18 15:04:18.867051+00;195
29;insomnia/2022.6.0;127.0.0.1;2022-11-21 11:33:15.719524+00;195
30;test;127.0.1.1;2022-11-22 19:34:39+00;195

Если я использую стандартный вывод в Django Admin следующим образом:

class OfferViewCountAdmin(admin.ModelAdmin):
    list_display = ('offer',)

Я получаю следующее:

Offer
offer #192
offer #192
offer #193
offer #195
offer #195
offer #195

Я хочу получить результат, подобный этому:

Offer;Views
offer #192;2
offer #193;1
offer #195;3

Проще говоря, я хочу отображать по одному экземпляру каждого дублирующегося поста в админке, а общее их количество выводить в пользовательском поле. В SQL это будет выглядеть примерно так:

SELECT offer_id, COUNT(*) AS count FROM offer_offerviewcount GROUP BY offer_id ORDER BY COUNT DESC;

Я перепробовал множество вариантов, включая перезапись get_queryset. В общем, мне удалось добиться желаемого результата следующим образом:

class OfferViewCountAdmin(admin.ModelAdmin):
    list_display = ('offer', 'get_views')
    list_filter = ['created_date', 'offer']
    list_per_page = 20

    def get_views(self, obj):
        return OfferViewCount.objects.filter(offer=obj.offer).count()

    def get_queryset(self, request):
        qs = OfferViewCount.objects.filter(
            ~Exists(OfferViewCount.objects.filter(
                Q(offer__lt=OuterRef('offer')) | Q(offer=OuterRef('offer'), pk__lt=OuterRef('pk')),
                offer=OuterRef('offer')
            ))
        )
        
        return qs

    get_views.short_description = _('Views')

Но в этом случае сортировка по Views не работает. Если я добавляю ее явно через admin_order_field для get_views, то получаю ошибку, так как такого поля в базе данных нет. Чтобы избежать такой ошибки, необходимо закрепить перезаписанный аннотационный кверисет, примерно так:

qs = OfferViewCount.objects.filter(
    ~Exists(OfferViewCount.objects.filter(
        Q(offer__lt=OuterRef('offer')) | Q(offer=OuterRef('offer'), pk__lt=OuterRef('pk')),
        offer=OuterRef('offer')
    ))
).annotate(_views_count=Count('offer'))

И измените get_views на:

def get_views(self, obj):
    return obj._views_count

Но в этом случае Count('offer') всегда возвращает 1, возможно потому, что там анализируется не вся база.

Подскажите, пожалуйста, как добавить работающую сортировку? Если есть какой-то гораздо более простой способ (без ~Exists и конструкций с Q()|Q()).

Вы можете использовать следующий набор запросов для группировки по запросу

from django.db.models import Count

def get_queryset(self, request):
    qs = OfferViewCount.objects.values(
            'offer'
        ).annotate(
            offer_count=Count('id')
        ).order_by("-offer_count")
    return qs

В mysql необработанный запрос типа,

SELECT 
`offer_offerviewcount`.`offer_id`, 
COUNT(`offer_offerviewcount`.`id`) AS `offer_count` 
FROM `offer_offerviewcount` 
GROUP BY `offer_offerviewcount`.`offer_id` 
ORDER BY `offer_count` DESC

Но если вы используете admin default change_list template, то этот запрос выдает error. Потому что django admin при выводе значений в шаблоне ожидает список объектов в queryset, а запрос group_by возвращает в queryset как список dict.

Если вы хотите использовать приведенный выше запрос, то переопределите шаблон change_list и отобразите сами данные.

вы должны использовать Django group by, как показано ниже

def get_queryset(self, request):
    qs = OfferViewCount.objects.values("offer")
    .annotate(count=Count("offer")).distinct().order_by("count")
    return qs
Вернуться на верх