Override query on Django DateField to change date searched for

Viewed 104

I have a model in Django that represents a week. I'd like to have someone enter any date and have the model automatically save the start of the week. That's easy, just override save.

class Week(models.Model):
  start_date = models.DateField()
  
  def save(self):
    self.start_date = self.start_date - datetime.timedelta(days=date.weekday())

But I'd also like someone to be able to query any day and get the week. So for example, I'd want someone to do this:

this_week = Week.objects.filter(start_date=date.today())

where today is a Wednesday, and get the week object where the date is set for the start of the week. It needs to work for any date, not just today.

I know we can override get_queryset in a manager, but is there a way to edit what was actually searched for? Every manager example I can find just changed the queryset in a static way. Or would my best bet be trying to subclass the DateField?

(Note code above is typed in, simplified, and may contain mistakes, but it works in my actual code)

2 Answers

We can subclass the DateField to each time clean a date time to the start of the week. This thus looks like:

from django.db.models.fields import DateField
from datetime import timedelta

class WeekField(DateField):
    def to_python(self, value):
        value = super().to_python(value)
        if value is not None:
            value -= timedelta(days=value.weekday())
        return value

Then we can create, filter, etc. with a WeekField. We can thus for example specify this in a Week model:

class Week(models.Model):
    week = WeekField()

and then for example use .get_or_create(…):

>>> from datetime import date
>>> Week.objects.get_or_create(week=date.today())
(<Week: Week object (1)>, True)
>>> Week.objects.get_or_create(week=date.today())
(<Week: Week object (1)>, False)

Filter with weeks:

>>> Week.objects.filter(week=date(2021, 8, 31))
<QuerySet [<Week: Week object (1)>]>
>>> Week.objects.filter(week=date(2021, 8, 2))
<QuerySet []>

see the start of the week with .values():

>>> Week.objects.values()
<QuerySet [{'id': 1, 'week': datetime.date(2021, 8, 30)}]>

I did not tested this extensively, but I guess most of the functionality is covered. Please ping me if there is still a use case that is not covered.

At the database level, you can make the ordering based on the start_date, as opposed to the default primary_key

class Week(models.Model):
  start_date = models.DateField()

  class Meta:
    ordering = ['start_date']

From here, it's fairly straight forward to get the week you're looking for.

week = Week.objects.filter(start_date__lte=date.today()).first()

The alternative to using Meta is to simply utilize order_by in your query.

week = Week.objects.filter(start_date__lte=date.today()).order_by('start_date').first()
Related