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