Cómo anotar el recuento con una condición en un queryset de Django

Usando Django ORM, se puede hacer algo como queryset.objects.annotate(Count('queryset_objects', gte=VALUE)) . ¿Me entiendes?


Aquí hay un ejemplo rápido para ilustrar una posible respuesta:

En un sitio web de Django, los creadores de contenido envían artículos, y los usuarios regulares ven (es decir, leen) dichos artículos. Los artículos pueden publicarse (es decir, estar disponibles para que todos los lean) o en modo borrador. Los modelos que representan estos requisitos son:

 class Article(models.Model): author = models.ForeignKey(User) published = models.BooleanField(default=False) class Readership(models.Model): reader = models.ForeignKey(User) which_article = models.ForeignKey(Article) what_time = models.DateTimeField(auto_now_add=True) 

Mi pregunta es: ¿Cómo puedo obtener todos los artículos publicados, ordenados por lectores únicos de los últimos 30 minutos? Es decir, quiero contar cuántas vistas distintas (únicas) obtuvo cada artículo publicado en la última media hora y luego generar una lista de artículos ordenados por estas vistas distintas.


Lo intenté:

 date = datetime.now()-timedelta(minutes=30) articles = Article.objects.filter(published=True).extra(select = { "views" : """ SELECT COUNT(*) FROM myapp_readership JOIN myapp_article on myapp_readership.which_article_id = myapp_article.id WHERE myapp_readership.reader_id = myapp_user.id AND myapp_readership.what_time > %s """ % date, }).order_by("-views") 

Esto provocó el error: error de syntax en o cerca de “01” (donde “01” era el objeto datetime dentro de extra). No es mucho para seguir.

Para django> = 1.8

Usar agregación condicional :

 from django.db.models import Count, Case, When, IntegerField Article.objects.annotate( numviews=Count(Case( When(readership__what_time__lt=treshold, then=1), output_field=IntegerField(), )) ) 

Explicación: la consulta normal a través de sus artículos se numviews con el campo numviews . Ese campo se construirá como una expresión CASE / WHEN, envuelta por Count, que devolverá 1 para los criterios coincidentes de lectura y NULL para los lectores que no coincidan con los criterios. Count ignorará los valores nulos y solo contará los valores.

Obtendrá ceros en los artículos que no se han visto recientemente y puede usar ese campo numviews para ordenar y filtrar.

La consulta detrás de esto para PostgreSQL será:

 SELECT "app_article"."id", "app_article"."author", "app_article"."published", COUNT( CASE WHEN "app_readership"."what_time" < 2015-11-18 11:04:00.000000+01:00 THEN 1 ELSE NULL END ) as "numviews" FROM "app_article" LEFT OUTER JOIN "app_readership" ON ("app_article"."id" = "app_readership"."which_article_id") GROUP BY "app_article"."id", "app_article"."author", "app_article"."published" 

Si deseamos rastrear solo consultas únicas, podemos agregar distinción en Count y hacer que nuestra cláusula When devuelva valor, queremos diferenciarlo.

 from django.db.models import Count, Case, When, CharField, F Article.objects.annotate( numviews=Count(Case( When(readership__what_time__lt=treshold, then=F('readership__reader')), # it can be also `readership__reader_id`, it doesn't matter output_field=CharField(), ), distinct=True) ) 

Eso producirá:

 SELECT "app_article"."id", "app_article"."author", "app_article"."published", COUNT( DISTINCT CASE WHEN "app_readership"."what_time" < 2015-11-18 11:04:00.000000+01:00 THEN "app_readership"."reader_id" ELSE NULL END ) as "numviews" FROM "app_article" LEFT OUTER JOIN "app_readership" ON ("app_article"."id" = "app_readership"."which_article_id") GROUP BY "app_article"."id", "app_article"."author", "app_article"."published" 

Para django <1.8 y PostgreSQL

Solo puede usar raw para ejecutar la sentencia SQL creada por versiones más nuevas de django. Aparentemente, no existe un método simple y optimizado para consultar esos datos sin usar raw datos en raw (incluso con extra hay algunos problemas al inyectar la cláusula JOIN requerida).

 Articles.objects.raw('SELECT' ' "app_article"."id",' ' "app_article"."author",' ' "app_article"."published",' ' COUNT(' ' DISTINCT CASE WHEN "app_readership"."what_time" < 2015-11-18 11:04:00.000000+01:00 THEN "app_readership"."reader_id"' ' ELSE NULL END' ' ) as "numviews"' 'FROM "app_article" LEFT OUTER JOIN "app_readership"' ' ON ("app_article"."id" = "app_readership"."which_article_id")' 'GROUP BY "app_article"."id", "app_article"."author", "app_article"."published"') 

Para django> = 2.0 puede usar la agregación condicional con un argumento de filter en las funciones agregadas:

 from datetime import timedelta from django.utils import timezone from django.db.models import Count Article.objects.annotate( numviews=Count( 'readership__reader__id', filter=Q(readership__what_time__gt=timezone.now() - timedelta(minutes=30)), distinct=True ) )