Override query on Django DateField to change date searched for - python

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)

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

SQLAlchemy - Querying with DateTime columns to filter by month/day/year

I'm building a Flask website that involves keeping track of payments, and I've run into an issue where I can't really seem to filter one of my db models by date.
For instance, if this is what my table looks like:
payment_to, amount, due_date (a DateTime object)
company A, 3000, 7-20-2018
comapny B, 3000, 7-21-2018
company C, 3000, 8-20-2018
and I want to filter it so that I get all rows that's after July 20th, or all rows that are in August, etc.
I can think of a crude, brute-force way to filter all payments and THEN iterate through the list to filter by month/year, but I'd rather stay away from those methods.
This is my payment db model:
class Payment(db.Model, UserMixin):
id = db.Column(db.Integer, unique = True, primary_key = True)
payment_to = db.Column(db.String, nullable = False)
amount = db.Column(db.Float, nullable = False)
due_date = db.Column(db.DateTime, nullable = False, default = datetime.strftime(datetime.today(), "%b %d %Y"))
week_of = db.Column(db.String, nullable = False)
And this is me attempting to filter Payment by date:
Payment.query.filter(Payment.due_date.month == today.month, Payment.due_date.year == today.year, Payment.due_date.day >= today.day).all()
where today is simply datetime.today().
I assumed the due_date column would have all DateTime attributes when I call it (e.g. .month), but it seems I was wrong.
What is the best way to filter the columns of Payment by date? Thank you for your help.
SQLAlchemy effectively translates your query expressed in Python into SQL. But it does that at a relatively superficial level, based on the data type that you assign to the Column when defining your model.
This means that it won't necessarily replicate Python's datetime.datetime API on its DateTime construct - after all, those two classes are meant to do very different things! (datetime.datetime provides datetime functionality to Python, while SQLAlchemy's DateTime tells its SQL-translation logic that it's dealing with a SQL DATETIME or TIMESTAMP column).
But don't worry! There are quite a few different ways for you to do achieve what you're trying to do, and some of them are super easy. The three easiest I think are:
Construct your filter using a complete datetime instance, rather than its component pieces (day, month, year).
Using SQLAlchemy's extract construct in your filter.
Define three hybrid properties in your model that return the payment month, day, and year which you can then filter against.
Filtering on a datetime Object
This is the simplest of the three (easy) ways to achieve what you're trying, and it should also perform the fastest. Basically, instead of trying to filter on each component (day, month, year) separately in your query, just use a single datetime value.
Basically, the following should be equivalent to what you're trying to do in your query above:
from datetime import datetime
todays_datetime = datetime(datetime.today().year, datetime.today().month, datetime.today().day)
payments = Payment.query.filter(Payment.due_date >= todays_datetime).all()
Now, payments should be all payments whose due date occurs after the start (time 00:00:00) of your system's current date.
If you want to get more complicated, like filter payments that were made in the last 30 days. You could do that with the following code:
from datetime import datetime, timedelta
filter_after = datetime.today() - timedelta(days = 30)
payments = Payment.query.filter(Payment.due_date >= filter_after).all()
You can combine multiple filter targets using and_ and or_. For example to return payments that were due within the last 30 days AND were due more than 15 ago, you can use:
from datetime import datetime, timedelta
from sqlalchemy import and_
thirty_days_ago = datetime.today() - timedelta(days = 30)
fifteen_days_ago = datetime.today() - timedelta(days = 15)
# Using and_ IMPLICITLY:
payments = Payment.query.filter(Payment.due_date >= thirty_days_ago,
Payment.due_date <= fifteen_days_ago).all()
# Using and_ explicitly:
payments = Payment.query.filter(and_(Payment.due_date >= thirty_days_ago,
Payment.due_date <= fifteen_days_ago)).all()
The trick here - from your perspective - is to construct your filter target datetime instances correctly before executing your query.
Using the extract Construct
SQLAlchemy's extract expression (documented here) is used to execute a SQL EXTRACT statement, which is how in SQL you can extract a month, day, or year from a DATETIME/TIMESTAMP value.
Using this approach, SQLAlchemy tells your SQL database "first, pull the month, day, and year out of my DATETIME column and then filter on that extracted value". Be aware that this approach will be slower than filtering on a datetime value as described above. But here's how this works:
from sqlalchemy import extract
payments = Payment.query.filter(extract('month', Payment.due_date) >= datetime.today().month,
extract('year', Payment.due_date) >= datetime.today().year,
extract('day', Payment.due_date) >= datetime.today().day).all()
Using Hybrid Attributes
SQLAlchemy Hybrid Attributes are wonderful things. They allow you to transparently apply Python functionality without modifying your database. I suspect for this specific use case they might be overkill, but they are a third way to achieve what you want.
Basically, you can think of hybrid attributes as "virtual columns" that don't actually exist in your database, but which SQLAlchemy can calculate on-the-fly from your database columns when it needs to.
In your specific question, we would define three hybrid properties: due_date_day, due_date_month, due_date_year in your Payment model. Here's how that would work:
... your existing import statements
from sqlalchemy import extract
from sqlalchemy.ext.hybrid import hybrid_property
class Payment(db.Model, UserMixin):
id = db.Column(db.Integer, unique = True, primary_key = True)
payment_to = db.Column(db.String, nullable = False)
amount = db.Column(db.Float, nullable = False)
due_date = db.Column(db.DateTime, nullable = False, default = datetime.strftime(datetime.today(), "%b %d %Y"))
week_of = db.Column(db.String, nullable = False)
#hybrid_property
def due_date_year(self):
return self.due_date.year
#due_date_year.expression
def due_date_year(cls):
return extract('year', cls.due_date)
#hybrid_property
def due_date_month(self):
return self.due_date.month
#due_date_month.expression
def due_date_month(cls):
return extract('month', cls.due_date)
#hybrid_property
def due_date_day(self):
return self.due_date.day
#due_date_day.expression
def due_date_day(cls):
return extract('day', cls.due_date)
payments = Payment.query.filter(Payment.due_date_year >= datetime.today().year,
Payment.due_date_month >= datetime.today().month,
Payment.due_date_day >= datetime.today().day).all()
Here's what the above is doing:
You're defining your Payment model as you already do.
But then you're adding some read-only instance attributes called due_date_year, due_date_month, and due_date_day. Using due_date_year as an example, this is an instance attribute which operates on instances of your Payment class. This means that when you execute one_of_my_payments.due_date_year the property will extract the due_date value from the Python instance. Because this is all happening within Python (i.e. not touching your database) it will operate on the already-translated datetime.datetime object that SQLAlchemy has stored in your instance. And it will return back the result of due_date.year.
Then you're adding a class attribute. This is the bit that is decorated with #due_date_year.expression. This decorator tells SQLAlchemy that when it is translating references to due_date_year into SQL expressions, it should do so as defined in in this method. So the example above tells SQLAlchemy "if you need to use due_date_year in a SQL expression, then extract('year', Payment.due_date) is how due_date_year should be expressed.
(note: The example above assumes due_date_year, due_date_month, and due_date_day are all read-only properties. You can of course define custom setters as well using #due_date_year.setter which accepts arguments (self, value) as well)
In Conclusion
Of these three approaches, I think the first approach (filtering on datetime) is both the easiest to understand, the easiest to implement, and will perform the fastest. It's probably the best way to go. But the principles of these three approaches are very important and I think will help you get the most value out of SQLAlchemy. I hope this proves helpful!

Annotate Django queryset with previous Sunday based on row's date field

I have a table with a set of Orders that my customers made (purchased, to say so).
The customers can choose the delivery date. That value is stored in each Order in a field called Order.delivery_date (not too much inventive there)
class Order(BaseModel):
customer = ForeignKey(Customer, on_delete=CASCADE, related_name='orders')
delivery_date = DateField(null=True, blank=True)
I would like to annotate a queryset that fetches Orders with the previous Sunday for that delivery_date (mostly to create a weekly report, "bucketized" per week)
I thought "Oh! I know! I'll get the date index in the week and I'll subtract a datetime.timedelta with the number of days of that week index, and I'll use that to get the Sunday (like Python's .weekday() function)":
from server.models import *
import datetime
from django.db.models import F, DateField, ExpressionWrapper
from django.db.models.functions import ExtractWeekDay
Order.objects.filter(
delivery_date__isnull=False
).annotate(
sunday=ExpressionWrapper(
F('delivery_date') - datetime.timedelta(days=ExtractWeekDay(F('delivery_date')) + 1),
output_field=DateField()
)
).last().sunday
But if I do that, I get a TypeError: unsupported type for timedelta days component: CombinedExpression when trying to "construct": the timedelta expression.
Not using the F function in the Extract doesn't make a difference either: I get the same error regardless of whether I use Extract(F('delivery_date')) or Extract('delivery_date')
This is a Python 3.4, with Django 2.0.3 over MySQL 5.7.21
I know that I can always fetch the Order object and do this in Python (I even have a little helper function that would do this) but it'd be nice to fetch the objects with that annotation from the DB (and also for learning purposes)
Thank you in advance.
Oh, I had forgotten about extra
It looks like this should do (at least for MySQL)
orders_q = Order.objects.filter(
delivery_date__isnull=False
).extra(
select={
'sunday': "DATE_ADD(`delivery_date`, interval(1 - DAYOFWEEK(`delivery_date`)) DAY)"
},
).order_by('-id')
It seems to work:
for record in orders_q.values('sunday', 'delivery_date'):
print("date: {delivery_date}, sunday: {sunday} is_sunday?: {is_sunday}".format(
is_sunday=record['sunday'].weekday() == 6, **record)
)
date: 2018-06-04, sunday: 2018-06-03 is_sunday?: True
date: 2018-05-30, sunday: 2018-05-27 is_sunday?: True
date: 2018-05-21, sunday: 2018-05-20 is_sunday?: True
date: 2018-06-04, sunday: 2018-06-03 is_sunday?: True
EDIT: Apparently, extra is on its way to being deprecated/unsupported. At the very least, is not very... erm... fondly received by Django developers. Maybe it'd be better using RawSQL instead. Actually, I was having issues trying to do further filter in the sunday annotation using extra which I'm not getting with the RawSQL method..
This seems to work better:
orders_q = orders_q.annotate(
sunday=RawSQL("DATE_ADD(`delivery_date`, interval(1 - DAYOFWEEK(`delivery_date`)) DAY)", ())
)
Which allows me to further annotate...
orders_q.annotate(sunday_count=Count('sunday'))
I'm not sure why, but when I was using extra, I'd get Cannot resolve keyword 'sunday' into field

Get all objects created on same day till now

I have a model which contains created_at DateTimeField. I want to filter the model to get all object created on this day, month till now.
For Example : Date = 21/06/2016
Now I want to get all objects created on 21/06 till now irrespective of year.
Edit:
To be precise, I have model which stores Date of Birth of Users. I want to get all the users who were born on this day.
I tried using the __range, __gte, __month & __day. This things did not work.
Thanks for your comments and answers. I have used this answer to solve the problem. I have removed the timedelta(days) from the code.
An example of filtering outside of the queryset.
Get the date u want and remove unwanted results
from datetime import datetime, timedelta
twodaysago = str(datetime.strptime(str(datetime.now().date()-timedelta(days=2)), '%Y-%m-%d')).split()[0]
now do your query and filter it like this, you may also do it in the query filter, but if u need some extra manipulations do it in your code
date_filtered = [x for x in query\
if datetime.strptime(
x.get('created_at ', ''),
'%Y-%m-%d') > twodaysago
]
Not sure if I understand the problem correctly, but try this:
before = datetime.date(2016, 06, 21)
today = datetime.date.today()
MyModel.objects.filter(
created_at__month__range=(before.month, today.month),
created_at__day__range=(before.day, today.day)
)
From what I can understand from your question, you want to get all objects with the same date as today, irrespective of the year.
I would use something like this. See if this helps.
from datetime import datetime
today = datetime.now()
OneModel.objects.filter(created_at__day = today.day.__str__(),
created_at__month = today.month.__str__())
For more see this link: How can I filter a date of a DateTimeField in Django?

Accessing to models methods "def()" from a query Django

im fighting with something here i'm using django and may you can help me.
I got a Account model with a date_of_birth field, and i have a method for find out the age.
class Account(AbstractBaseUser, PermissionsMixin):
date_of_birth = models.DateField()
def age(self):
"""
Returns the age from the date of birth
"""
today = datetime.date.today()
try:
birthday = self.date_of_birth.replace(year=today.year)
except ValueError: # raised when birth date is February 29 and the current year is not a leap year
birthday = self.date_of_birth.replace(year=today.year, day=self.date_of_birth.day-1)
if birthday > today:
return today.year - self.date_of_birth.year - 1
else:
return today.year - self.date_of_birth.year
i was wondering if is possible to obtain the age from a query like this:
list = Account.objects.filter('account__age__gte', today)
i tried already but i got this error:
cannot resolve keyword 'age' into field. Choices are:......
and only shows me the fields. not the methods.\
i appreciate your help.
thanks a lot.
You cannot directly query against model method, since custom methods cannot evaluate to their corresponding SQL queries.
You have a couple of options instead:
In the view, compute the earliest date of birth given the age. Example 24 years:
from dateutil.relativedelta import relativedelta
datetime.date.today() - relativedelta(years=24)
datetime.date(1989, 11, 15)
and now, the query would be on the date_of_birth field.
Note that dateutil is a 3rd party library and may not be available with your python by default. (If you want to use timedelta, you could do that too, since datetime.timedelta is python builtin)
Another option (a little less efficient) is to fetch the object queryset, and use a list comprehension to filter out the unwanted records.
qs = Account.objects.all()
qs = [account for account in qs if account.age() > 24]
24, obviously was just an example. replace that with some "sane" value.
I know you have an answer for this already and that answer is accurate, but I think you might benefit from making your age method into a property (actually, I thought this is what is what model properties were for, but I would be happy to be corrected on this point if I am wrong).
Thus, you could do something like this:
class Account(AbstractBaseUser, PermissionsMixin):
date_of_birth = models.DateField()
def _age(self):
"""
Returns the age from the date of birth
"""
today = datetime.date.today()
... {value is computed and returned} ...
age = property(_age)
This, of course, doesn't solve your filtering problem; it just makes it easier to treat the method as if it's an instance attribute and your SQL query will still need to grab everything, or filter by date_of_birth (which if you're going to do a lot, may be nice to include as a custom manager).

Django Group By Weekday?

I'm using Django 1.5.1, Python 3.3.x, and can't use raw queries for this.
Is there a way to get a QuerySet grouped by weekday, for a QuerySet that uses a date __range filter? I'm trying to group results by weekday, for a query that ranges between any two dates (could be as much as a year apart). I know how to get rows that match a weekday, but that would require pounding the DB with 7 queries just to find out the data for each weekday.
I've been trying to figure this out for a couple hours by trying different tweaks with the __week_day filter, but nothing's working. Even Googling doesn't help, which makes me wonder if this is even possible. Any Django guru's here know how, if it is possible to do?
Since extra is deprecated, here is a new way of grouping on the day of the week using ExtractDayOfWeek.
from django.db.models.functions import ExtractWeekDay
YourObjects.objects
.annotate(weekday=ExtractWeekDay('timestamp'))
.values('weekday')
.annotate(count=Count('id'))
.values('weekday', 'count')
This will return a result like:
[{'weekday': 1, 'count': 534}, {'weekday': 2, 'count': 574},.......}
It is also important to note that 1 = Sunday and Saturday = 7
Well man I did an algorithm this one brings you all the records since the beginning of the week (Monday) until today
for example if you have a model like this in your app:
from django.db import models
class x(models.Model):
date = models.DateField()
from datetime import datetime
from myapp.models import x
start_date = datetime.date(datetime.now())
week = start_date.isocalendar()[1]
day_week =start_date.isoweekday()
days_quited = 0
less_days = day_week
while less_days != 1:
days_quited += 1
less_days -= 1
week_begin = datetime.date(datetime(start_date.year,start_date.month,start_date.day-days_quited))
records = x.objects.filter(date__range=(week_begin, datetime.date(datetime.now())))
And if you add some records in the admin with a range between June 17 (Monday) and June 22 (today) you will see all those records, and if you add more records with the date of tomorrow for example or with the date of the next Monday you will not see those records.
If you want the records of other week unntil now you only have to put this:
start_date = datetime.date(datetime(year, month, day))
records = x.objects.filter(date__range=(week_begin, datetime.date(datetime.now())))
Hope this helps! :D
You need to add an extra weekday field to the selection, then group by that in the sum or average aggregation. Note that this becomes a database specific query, because the 'extra' notation becomes passed through to the DB select statement.
Given the model:
class x(models.Model):
date = models.DateField()
value = models.FloatField()
Then, for mysql, with a mapping of the ODBC weekday to the python datetime weekday:
x.objects.extra(select={'weekday':"MOD(dayofweek(date)+5,7)"}).values('weekday').annotate(weekday_value=Avg('value'), weekday_value_std=StdDev('value'))
Note that if you do not need to convert the MySql ODBC weekday (1 = Sunday, 2 = Monday...) to python weekday (Monday is 0 and Sunday is 6), then you do not need to do the modulo.
For model like this:
class A(models.Model):
date = models.DateField()
value = models.FloatField()
You can use query:
weekday = {"w": """strftime('%%w', date)"""}
qs = A.objects.extra(select=weekday).values('w').annotate(stat = Sum("value")).order_by()

Categories