[Django]-Queryset of people with a birthday in the next X days

13đź‘Ť

âś…

Assuming a model like this–

class Person(models.Model):
    name = models.CharField(max_length=40)
    birthday = models.DateTimeField() # their next birthday

The next step would be to create a query filtering out any records with birthdays having a month and day in between (now.month, now.day) and (then.month, then.day). You can actually access the month and day attributes of the datetime object using the queryset API by passing Person.objects.filter a keyword argument like this: “birthday__month.” I tried this with an actual queryset API method like “birthday__month__gte” and it failed though. So I would suggest simply generating a literal list of month/day tuples representing each (month, day) in the date range you want records for, then compose them all into a query with django.db.models.Q, like so:

from datetime import datetime, timedelta
import operator

from django.db.models import Q

def birthdays_within(days):

    now = datetime.now()
    then = now + timedelta(days)

    # Build the list of month/day tuples.
    monthdays = [(now.month, now.day)]
    while now <= then:
        monthdays.append((now.month, now.day))
        now += timedelta(days=1)

    # Tranform each into queryset keyword args.
    monthdays = (dict(zip(("birthday__month", "birthday__day"), t)) 
                 for t in monthdays)


    # Compose the djano.db.models.Q objects together for a single query.
    query = reduce(operator.or_, (Q(**d) for d in monthdays))

    # Run the query.
    return Person.objects.filter(query)

After debugging, this should return a queryset with each person who has a birthday with month and day equal to any of the months or days in the specified list of tuples.

👤twneale

1đź‘Ť

Assuming it’s datetime field do something like this (using future_date from dimosaur answer):

Profile.objects.get(
    Q(birthday__lte=future_date),
    Q(birthday__gte=datetime.date.today())
)
👤starenka

1đź‘Ť

I can think of 2 ways without using custom queries, both with “problems”

1) Not efficient as it does 1 query per day

start = datetime.date.today()
max_days = 14
days = [ start + datetime.timedelta(days=i) for i in xrange(0, max_days) ]

birthdays = []
for d in days:
    for p in Profile.objects.filter(birthday__month=d.month, birthday__day=d.day):
        birthdays.append(p)

print birthdays

2) Single query, but requires a model change. You would need to add bday_month and bday_day integer fields. These can obviously be populated automatically from the real date.

The limitation of this example is that you can only check against 2 months, start month and the end month. Setting 29 days you could jump over february, showing only Jan 31 and Mar 1.

from django.db.models import Q    
start = datetime.date.today()
end = start + datetime.timedelta(days=14)

print Profile.objects.filter(
    Q(bday_month=start.month) & Q(bday_day__gte=start.day) | 
    Q(bday_month=end.month) & Q(bday_day__lte=end.day)
)
👤PiGGeH

0đź‘Ť

If X is a constant that you know:

import datetime
future_date = datetime.date.today() + datetime.timedelta(days=X)
Profile.objects.filter(
    birth_date__month=future_date.month,
    birth_date__day=future_date.day
)

Something like that.

👤dmrz

0đź‘Ť

I have tried to do it in a really silly way, but seems it works:

import datetime
from django.db.models import Q

x = 5
q_args = ''

for d in range(x):
    future_date = datetime.date.today() + datetime.timedelta(days=d)
    q_args += 'Q(birth_date__month=%d, birth_date__day=%d)%s' % (
        future_date.month,
        future_date.day,
        ' | ' if d < x - 1 else ''
    )

people = People.objects.filter(eval(q_args))
👤dmrz

0đź‘Ť

I was unsatisfied with all replies here. They are all a variant on “check one date/year by one in a range…”, making a long, ugly queries. Here is a simple solution, if one is willing to denormalize a bit:

Change your model so instead of just datetime birthdate(yyyy, mm, dd) holding the real date you add a datetime birthday(DUMMY_YEAR, mm, dd) column. So every person in your DB will have saved its real birth date, and then a another birth date with a fixed year, shared with everyone else. Don’t show this second field to users, though, and don’t allow them to edit it.

Once you edited your model, make sure the birthdate and birthday are always connected by extending models.Model save method in your class:

def save(self, *args, **kwargs):
    self.birthday = datetime.date(BIRTHDAY_YEAR, 
         self.birthdate.month, self.birthdate.day)
    super(YOUR_CLASS, self).save(*args, **kwargs)

And once you ensured that whenever a date is saved as birthdate, the birthday is updated too, you can filter it with just birthday__gte/birthday__lte. See an excerpt from my admin filter, where I take care of a year boundary:

def queryset(self, request, queryset):
    if self.value() == 'today':
        # if we are looking for just today, it is simple
        return queryset.filter(birthday = datetime.date(
                BIRTHDAY_YEAR, now().month, now().day
            ))

    if self.value() == 'week':
        # However, if we are looking for next few days,
        # we have to bear in mind what happens on the eve
        # of a new year. So if the interval we are looking at
        # is going over the new year, break the search into
        # two with an OR.

        future_date = (now() + datetime.timedelta(days=7)).date()
        if (now().year == future_date.year):
            return queryset.filter(
                    Q(birthday__gte = datetime.date(
                        BIRTHDAY_YEAR, now().month, now().day
                    )) &
                    Q(birthday__lte = datetime.date(
                        BIRTHDAY_YEAR,
                        future_date.month,
                        future_date.day)
                    )
                )
        else:
            return queryset.filter(
                    # end of the old year
                    Q(birthday__gte = datetime.date(
                        BIRTHDAY_YEAR, now().month, now().day
                    )) &
                    Q(birthday__lte = datetime.date(BIRTHDAY_YEAR,12, 31)) |
                    # beginning of the new year
                    Q(birthday__gte = datetime.date(BIRTHDAY_YEAR, 1, 1)) &
                    Q(birthday__lte = datetime.date(BIRTHDAY_YEAR,
                        future_date.month,
                        future_date.day)
                    )
                )

In case you wonder what the Q() is, look on Complex lookups with Q objects

👤Zopper

Leave a comment