2017-01-27 1 views
2

Ich möchte alle Ereignisdauern pro Tag summieren. Das ist mein Modell:Aggregierte gruppierte Annotation

class Event(models.Model): 
    start = models.DateTimeField() 
    end = models.DateTimeField() 

Beispieldaten:

import datetime 
from random import randint 

for i in range(0, 1000): 
    start = datetime.datetime(
     year=2016, 
     month=1, 
     day=randint(1, 10), 
     hour=randint(0, 23), 
     minute=randint(0, 59), 
     second=randint(0, 59) 
    ) 
    end = start + datetime.timedelta(seconds=randint(30, 1000)) 
    Event.objects.create(start=start, end=end) 

ich die Veranstaltung pro Tag zählen wie so bekommen kann: (ich weiß extra schlecht ist, aber ich bin mit 1,9 im Moment . aktualisieren, wenn ich ich werde mit TruncDate) bewegen

Event.objects.extra({'date': 'date(start)'}).order_by('date').values('date').annotate(count=Count('id')) 

[{'count': 131, 'date': datetime.date(2016, 1, 1)}, 
{'count': 95, 'date': datetime.date(2016, 1, 2)}, 
{'count': 99, 'date': datetime.date(2016, 1, 3)}, 
{'count': 85, 'date': datetime.date(2016, 1, 4)}, 
{'count': 87, 'date': datetime.date(2016, 1, 5)}, 
{'count': 94, 'date': datetime.date(2016, 1, 6)}, 
{'count': 97, 'date': datetime.date(2016, 1, 7)}, 
{'count': 111, 'date': datetime.date(2016, 1, 8)}, 
{'count': 97, 'date': datetime.date(2016, 1, 9)}, 
{'count': 104, 'date': datetime.date(2016, 1, 10)}] 

I mit Anmerkungen versehen können die Dauer hinzuzufügen:

In [3]: Event.objects.annotate(duration=F('end') - F('start')).first().duration 
Out[3]: datetime.timedelta(0, 470) 

Aber ich kann nicht herausfinden, wie diese Annotation auf die gleiche Weise wie ich Ereignisse zählen kann. Ich habe folgendes versucht, aber ich bekomme eine KeyError auf "Dauer".

Event.objects.annotate(duration=F('end') - F('start')).extra({'date': 'date(start)'}).order_by('date').values('date').annotate(total_duration=Sum('duration')) 

und wenn ich hinzufügen duration auf die values Klausel dann nicht mehr Gruppen nach Datum.

Ist dies in einer einzigen Abfrage und ohne Hinzufügen eines Dauerfelds zum Modell möglich?

+0

Wenn Sie 'Werte ('Datum') 'auf diesem Abfrage-Set verwenden, eliminieren Sie das Feld' Dauer ', sodass nichts zu summieren ist. Was passiert, wenn Sie nach dem Aufruf 'values ​​()' call und dann 'order_by ('date') '' duration '' hinzufügen? Oder ist es notwendig, 'values ​​()' überhaupt zu verwenden? – ChidG

+1

Hey @ChidG! (Wie ist Ze'ev?) "Werte" wird für die Gruppe verwendet - ich denke, es ist erforderlich. Wenn ich dem Aufruf "values" die Option "duration" hinzufüge, versucht es, Ereignisse nicht nur mit "date", sondern mit "date" und "duration" zu gruppieren. –

Antwort

2

Ich wollte gerade eine Antwort schreiben, dass Django ORM dies nicht unterstützt. Und ja, dann habe ich eine weitere Stunde mit diesem Problem verbracht (zusätzlich zu den 1,5 Stunden, die ich bereits verbracht habe, bevor ich angefangen habe, diese Antwort zu schreiben), aber wie sich herausstellt, unterstützt Django es. Und ohne zu hacken. Gute Nachrichten!

import datetime as dt 

from django.db import models 
from django.db.models import F, Sum, When, Case 
from django.db.models.functions import TruncDate 

from app.models import Event 

a = Event.objects.annotate(date=TruncDate('start')).values('date').annotate(
    day_duration=Sum(Case(
     When(date=TruncDate(F('start')), then=F('end') - F('start')), 
     default=dt.timedelta(), output_field=models.DurationField() 
    )) 
) 

Und einige vorläufige Tests, um (hoffentlich) zu beweisen, dass dieses Zeug tatsächlich das tut, was Sie gefragt haben.

In [71]: a = Event.objects.annotate(date=TruncDate('start')).values('date').annotate(day_duration=Sum(Case(
    ...:   When(date=TruncDate(F('start')), then=F('end') - F('start')), 
    ...:   default=dt.timedelta(), output_field=models.DurationField() 
    ...: )) 
    ...:) 

In [72]: for e in a: 
    ...:  print(e) 
    ...:  
{'day_duration': datetime.timedelta(0, 41681), 'date': datetime.date(2016, 1, 10)} 
{'day_duration': datetime.timedelta(0, 46881), 'date': datetime.date(2016, 1, 3)} 
{'day_duration': datetime.timedelta(0, 48650), 'date': datetime.date(2016, 1, 1)} 
{'day_duration': datetime.timedelta(0, 52689), 'date': datetime.date(2016, 1, 8)} 
{'day_duration': datetime.timedelta(0, 45788), 'date': datetime.date(2016, 1, 5)} 
{'day_duration': datetime.timedelta(0, 49418), 'date': datetime.date(2016, 1, 7)} 
{'day_duration': datetime.timedelta(0, 45984), 'date': datetime.date(2016, 1, 9)} 
{'day_duration': datetime.timedelta(0, 51841), 'date': datetime.date(2016, 1, 2)} 
{'day_duration': datetime.timedelta(0, 63770), 'date': datetime.date(2016, 1, 4)} 
{'day_duration': datetime.timedelta(0, 57205), 'date': datetime.date(2016, 1, 6)} 

In [73]: q = dt.timedelta() 

In [74]: o = Event.objects.filter(start__date=dt.date(2016, 1, 7)) 

In [75]: p = Event.objects.filter(start__date=dt.date(2016, 1, 10)) 

In [76]: for e in o: 
    ...:  q += (e.end - e.start) 

In [77]: q 
Out[77]: datetime.timedelta(0, 49418) # Matches 2016.1.7, yay! 

In [78]: q = dt.timedelta() 

In [79]: for e in p: 
    ...:  q += (e.end - e.start) 

In [80]: q 
Out[80]: datetime.timedelta(0, 41681) # Matches 2016.1.10, yay! 

Achtung! Dies funktioniert ab Version 1.9, ich glaube nicht, dass Sie dies mit den früheren Versionen tun können, weil die Funktion fehlt. Und vor 1.8 hast du natürlich auch nicht die Case und When Dinger.

+0

Das ist sehr cool, gut gemacht! (Auch toll, einen anderen Djangonaut mit Mechatronikhintergrund zu sehen) –