BuTaMuH
BuTaMuH

Reputation: 1

GROUP By in Django ORM for page in Django Admin

I'm not very long in Django, sorry for the probably stupid question. But after many hours of trying to solve and a huge number of searches on the Internet, I did not find a solution.

My Models:

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)

The database of the OfferViewCount model has the following data:

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

If I use the default output in Django Admin like this:

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

I get this:

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

I want to get a result like this:

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

Simply put, I want to display one instance of each duplicate post in the admin, and display the total number of them in a custom field. In SQL it would look something like this:

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

I've tried many options, including overwriting get_queryset. In general, I managed to achieve the desired result like this:

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

But in this case, sorting by Views does not work. If I add it explicitly via admin_order_field for get_views, I get an error because there is no such field in the database. To avoid such an error, it is necessary to fasten the overwritten annotate queriset, something like this:

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

And change get_views to:

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

But in this case, Count('offer') always returns 1, probably because not the entire base is analyzed there.

Actually, tell me, please, how to add a working sorting? If there is some much simpler way (without ~Exists and constructions with Q()|Q()).

Upvotes: 0

Views: 917

Answers (2)

alvand_ashhadi
alvand_ashhadi

Reputation: 31

you should use the Django group by like below

def get_queryset(self, request):
    qs = OfferViewCount.objects.values("offer")
    .annotate(count=Count("offer")).distinct().order_by("count")
    return qs

Upvotes: 1

Md Shahbaz Ahmad
Md Shahbaz Ahmad

Reputation: 1166

You can use below queryset for group by query

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

In mysql raw query like,

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

But if you use admin default change_list template then this query gives you an error. Because django admin when render values in template expect list of objects in queryset and group_by query return in queryset as list of dict.

If you want to use above query, then you override change_list template and rendered data itself.

Upvotes: 0

Related Questions