djangodjango-rest-frameworkdjango-viewsdjango-orm

Count by Month in Django Query


Good afternoon,

I am struggling with a query. Ultimately, I have some records I want to query and get a count by month.

Ultimately, my goal is to come up with a data set that looks like:

Month, opened, closed
Jan, 2, 3
Feb, 1, 5

Etc... for the past six months.

I have been using this as a base: Django: Query Group By Month

This works to get each individually:

six_months_ago = datetime.now() - timedelta(days = 180)
open_six_months = (Rec.objects
   .annotate(open=Month('open_date'))
   .values('open')
   .annotate(rec_opened=Count('status', filter=Q(status__is_open=True)))
   .order_by())
        
closed_six_months = (Rec.objects
   .annotate(closed=Month('close_date'))
   .values('closed')
   .annotate(rec_closed=Count('status', filter=Q(status__is_open=False) and Q(close_date__isnull=False) ))
   .order_by())

Not all records have a closed date yet.

This returns the right data when testing, but its in two different queries.

I'd like to have one single data set if possible.

Is there an easy way to combine this into one query that has a single row with both the count of the open date and the count of the closed date by month?

Thank you for your help.

BCBB


Solution

  • An option might be to just join the two together as:

    from django.db.models import IntegerField, Q, Value
    from django.db.models.functions import ExtractMonth
    
    six_months_ago = datetime.now() - timedelta(days=180)
    all_items = (
        Rec.objects.values(
            open=ExtractMonth('open_date'),
            closed=Value(None, output_field=IntegerField()),
        )
        .annotate(item=Count('status', filter=Q(status__is_open=True)))
        .order_by()
        .union(
            Rec.objects.values(
                open=Value(None, output_field=IntegerField()),
                closed=ExtractMonth('close_date'),
            )
            .annotate(
                item=Count(
                    'status',
                    filter=Q(status__is_open=False, close_date__isnull=False),
                )
            )
            .order_by(),
            all=True,
        )
    )

    then we post-process as:

    result = {}
    for item in all_items:
        month = item['open'] or item['close']
        record = result.setdefault(month, [0, 0])
        record[item['open'] is None] += item['item']

    This will map every month number on a list of two items with the first the number of items opened that month, and the second the number of items closed that month.