How to annotate a difference of datetime in days - django

I have a Booking model that has start and end datetime fields. I want to know how many days a booking covers. I can do this in Python but I need this value for further annotations.
Here's what I've tried:
In [1]: Booking.objects.annotate(days=F('end')-F('start'))[0].days
Out[1]: datetime.timedelta(16, 50400)
There are a few problems here:
I want an integer (or other number type I can use in calculations) of days as the output, not a timedelta. Setting output_field doesn't do anything meaningful here.
My sums are based on datetimes. Subtractions like this, without removing the time could lead to the whole number of days being off.
In Python I would do (end.date() - start.date()).days + 1. How can I do that in-database, preferably through the ORM (eg database functions), but a RawSQL would suffice to get this out the door?

I've written a couple of database functions to cast and truncate the dates to solve both problems under PostgreSQL. The DATE_PART and DATE_TRUNC internal function I'm using are DB-specific ☹
from django.db.models import Func
class DiffDays(Func):
function = 'DATE_PART'
template = "%(function)s('day', %(expressions)s)"
class CastDate(Func):
function = 'date_trunc'
template = "%(function)s('day', %(expressions)s)"
Then I can:
In [25]: Booking.objects.annotate(days=DiffDays(CastDate(F('end'))-CastDate(F('start'))) + 1)[0].days
Out[25]: 18.0

There is another, easy solution of this problem. You can use:
from django.db.models import F
from django.db.models.functions import ExtractDay
and then:
Booking.objects.annotate(days=(ExtractDay(F('end')-F('start'))+1))[0].days

If you are using MYSQL database, You could do it using Custom DB Function as,
from django.db.models.functions import Func
class TimeStampDiff(Func):
class PrettyStringFormatting(dict):
def __missing__(self, key):
return '%(' + key + ')s'
def __init__(self, *expressions, **extra):
unit = extra.pop('unit', 'day')
self.template = self.template % self.PrettyStringFormatting({"unit": unit})
super().__init__(*expressions, **extra)
function = 'TIMESTAMPDIFF'
template = "%(function)s(%(unit)s, %(expressions)s)"
Usage
from django.db.models import F, IntegerField
booking_queryset = Booking.objects.annotate(
days=TimeStampDiff(F('start'), F('end'), output_field=IntegerField()))
if booking_queryset.exist():
print(booking_queryset[0].__dict__)

Related

Django - Add Nearest Monday to Queryset

I have an Order model like so:
class Order(models.Model):
created_at = models.DateTimeField(...)
An order can be created at any time, but all orders get shipped out on the following Monday.
How can I add an extra field to my orders queryset called assembly_date that reflects the next Monday (date the order should be shipped)?
I tried creating a custom OrderManager like so, but am not sure how to correctly set the assembly_date:
from django.db.models import F, ExpressionWrapper, DateField
from django.db.models.functions import ExtractWeekDay
class OrderManager(models.Manager):
def get_queryset():
# need help with logic here:
return self.super().get_queryset().annotate(
assembly_date = ExpressionWrapper(
F("created_at") - ExtractWeekDay("created_at"),
output_field = DateField(),
)
)
But this operation results in the following error:
django.db.utils.ProgrammingError: operator does not exist: timestamp with time zone - double precision
LINE 1: ...E NULL END) * 2628000.0) * INTERVAL '1 seconds')) - EXTRACT(...
^
HINT: No operator matches the given name and argument types. You might need to add explicit type casts.
Keep in mind, I want to be able to filter all orders based on their assembly_date.
Basically you need to dynamically generate timedelta inside of annotate. But as far as I know, there is no way you can apply isoweekday() to a datetimefield inside of annotate.
You can have another field as assembly_date in your model, and use it directly to query.
from datetime import timedelta, date
class Order(models.Model):
created_at = models.DateTimeField(...)
assembly_date = models.DateTimeField(...)
def save(self, *args, **kwargs):
weekday = self.created_at.isoweekday() # 1 is Monday, 2 is Tuesday.
daysdiff = 8 - weekday
self.assembly_date = self.created_at + timedelta(days= daysdiff)
super(Order, self).save(*args, **kwargs)

Django DateTimeRangeField: default=[timezone.now()]-[timezone.now()]+[10YEARS]

I want an "active_in" attribute as a timeframe. I assume that the DBMS is optimized for the postgresql tsrange field, and as such it is preferable to utilize the DateTimeRangeField rather than 2 separate fields for start_date and end_date.
Doing this I desire a default value for the field.
active_in = models.DateTimeRangeField(default=timezone.now+'-'+timezone.now+10YEARS)
Is my assumption about the DateTimeRangeField performance true?
Is there a smart solution be it creating a new; function,class or
simply manipulating the 2nd last digit?
My possible solutions:
Code using string manipulation:
active_in = models.DateTimeRangeField(default=timezone.now+'-'+timezone.now[:-2]+'30')
Code using custom function object: (adjusted from here: https://stackoverflow.com/a/27491426/7458018)
def today_years_ahead():
return timezone.now + '-' timezone.now() + timezone.timedelta(years=10)
class MyModel(models.Model):
...
active_in = models.DateTimeRangeField(default=today_years_ahead)
There's no need for string manipulation, as the documented Python type for this field is DateTimeTZRange.
I can't say I've ever used this field before, but something like this should work:
from psycopg2.extras import DateTimeTZRange
from django.utils import timezone
from datetime import timedelta
def next_ten_years():
now = timezone.now()
# use a more accurate version of "10 years" if you need it
return DateTimeTZRange(now, now + timedelta(days=3652))
class MyModel(models.Model):
...
active_in = models.DateTimeRangeField(default=next_ten_years)

DjangoORM: Resolve F Expression in Custom DB Function

I am trying to write a custom PostgreSQL function in Django that will coerce datetimes to a specified timezone inside of a queryset. My first pass at the db function looks like this:
from django.db.models.expressions import Func
class DateTimeInTimezone(Func):
template="%(expressions)s AT TIME ZONE %(tz_info)s"
This function works in the simple case where I pass a timezone string into the function directly like so:
MyModel.objects.filter(timefield__gte=DateTimeInTimezone(Now(), tz_info='EST'))
However it doesn't work in the more complex case, where the timezone is defined on some field on the model. Consider the following contrived example:
class User(models.Model):
time_zone = models.CharField()
class Meeting(models.Model):
users = models.ManyToManyField(User, related_name='meetings')
start_time = models.DateTimeField() # in UTC
end_time = models.DateTimeField() # in UTC
To answer the question "What users will be in a meeting at 12pm local time today?", I'd need some variation of this queryset:
noon_utc = ...
User.objects.filter(
meetings__start_time__lte=DateTimeInTimezone(noon_utc, tz_info=F('time_zone')),
meetings__end_time__gt=DateTimeInTimezone(noon_utc, tz_info=F('time_zone'))
)
As currently written, however, DateTimeInTimezone will simply inject the string F('time_zone') into the sql rather than resolving the field.
Is it possible to add support for F Expressions to this function? Is there some other approach that I should consider?
A simple solution is possible with parameter arg_joiner:
class DateTimeInTimezone(Func):
function = ''
arg_joiner = ' AT TIME ZONE '
def __init__(self, timestamp, tz_info):
super(DateTimeInTimezone, self).__init__(timestamp, tz_info)
The method __init__ is used only for the purpose of readability with clear names of parameters. Then arity is not important if parameters are declared by __init__.
A oneliner function is useful for fast development if readability is not important:
...filter(
meetings__start_time__lte=Func(noon_utc, tz_info=F('time_zone'), arg_joiner=' AT TIME ZONE ', function=''),
)
Verified:
>>> qs = User.objects.filter(...)
>>> print(str(qs.query))
SELECT ... WHERE ("app_meeting"."start_time" <= ((2017-10-03 08:18:12.663640 AT TIME ZONE "app_user"."time_zone")) AND ...)
Found an acceptable solution. I overrode the as_sql method for function like so, allowing the django internals to resolve the F expression and then separating it back out into a kwarg I could use in a different part of the template.
class DateTimeInTimezone(Func):
'''
Coerce a datetime into a specified timezone
'''
template="%(expressions)s AT TIME ZONE %(tz_info)s"
arity = 2
def as_sql(self, compiler, connection, function=None, template=None, arg_joiner=None, **extra_context):
connection.ops.check_expression_support(self)
sql_parts = []
params = []
for arg in self.source_expressions:
arg_sql, arg_params = compiler.compile(arg)
sql_parts.append(arg_sql)
params.extend(arg_params)
data = self.extra.copy()
data.update(**extra_context)
# Use the first supplied value in this order: the parameter to this
# method, a value supplied in __init__()'s **extra (the value in
# `data`), or the value defined on the class.
if function is not None:
data['function'] = function
else:
data.setdefault('function', self.function)
template = template or data.get('template', self.template)
arg_joiner = arg_joiner or data.get('arg_joiner', self.arg_joiner)
data['expressions'] = data['field'] = arg_joiner.join(sql_parts)
parts = data['expressions'].split(', ')
data['expressions'] = parts[0]
data['tz_info'] = parts[1]
return template % data, params
I added the three lines between the assignment of data['expressions'] and the final return template % data, params. This isn't a great long term solution as the django internals for this method could change in the next version, but it suits my needs for the time being.

Django: Combine a date and time field and filter

I have a django model that has a date field and a separate time field. I am trying to use a filter to find a value on the latest record by date/time that is less than the current record's date time.
How do I use annotate/aggregate to combine the date and time fields into one and then do a filter on it?
models.py
class Note(models.model):
note_date = models.DateField(null=True)
note_time = models.TimeField(null=True)
note_value = models.PositiveIntegerField(null=True)
def get_last(n):
"""
n: Note
return: Return the note_value of the most recent Note prior to given Note.
"""
latest = Note.objects.filter(
note_date__lte=n.note_date
).order_by(
'-note_date', '-note_time'
).first()
return latest.note_value if latest else return 0
This will return any notes from a previous date, but if I have a two notes on the same date, one at 3pm and one at 1pm, and I send the 3pm note to the function, I want to get the value of the 1pm note. Is there a way to annotate the two fields into one for comparison, or do I have to perform a raw SQL query? Is there a way to convert the date and time component into one, similar to how you could use Concat for strings?
Note.objects.annotate(
my_dt=Concat('note_date', 'note_time')
).filter(
my_dt__lt=Concat(models.F('note_date'), models.F('note_time')
).first()
I am too late but here is what I did
from django.db.models import DateTimeField, ExpressionWrapper, F
notes = Note.objects.annotate(my_dt=ExpressionWrapper(F('note_date') + F('note_time'), output_field=DateTimeField()))
Now we have added a new field my_dt of datetime type and can add a filter further to do operations
Found an answer using models.Q here: filter combined date and time in django
Note.objects.filter(
models.Q(note_date__lt=n.note_date) | models.Q(
note_date=n.note_date,
note_time__lt=n.note_time
)
).first()
I guess I just wasn't searching by the right criteria.
Here is another Approach which is more authentic
from django.db.models import Value, DateTimeField
from django.db.models.functions import Cast, Concat
notes = Note.objects.annotate(my_dt=Cast(
Concat('note_date', Value(" "), 'note_time', output_field=DateTimeField()),
output_field=DateTimeField()
).filter(my_dt__lte=datetime.now())
Here is another solution following others.
def get_queryset(self):
from django.db import models
datetime_wrapper = models.ExpressionWrapper(models.F('note_date') + models.F('note_time'), output_field=models.DateTimeField())
return Note.objects.annotate(
note_datetime=datetime_wrapper
).filter(note_datetime__gt=timezone.now()).order_by('note_datetime')

How to do SELECT MAX in Django?

I have a list of objects how can I run a query to give the max value of a field:
I'm using this code:
def get_best_argument(self):
try:
arg = self.argument_set.order_by('-rating')[0].details
except IndexError:
return 'no posts'
return arg
rating is an integer
See this. Your code would be something like the following:
from django.db.models import Max
# Generates a "SELECT MAX..." query
Argument.objects.aggregate(Max('rating')) # {'rating__max': 5}
You can also use this on existing querysets:
from django.db.models import Max
args = Argument.objects.filter(name='foo') # or whatever arbitrary queryset
args.aggregate(Max('rating')) # {'rating__max': 5}
If you need the model instance that contains this max value, then the code you posted is probably the best way to do it:
arg = args.order_by('-rating')[0]
Note that this will error if the queryset is empty, i.e. if no arguments match the query (because the [0] part will raise an IndexError). If you want to avoid that behavior and instead simply return None in that case, use .first():
arg = args.order_by('-rating').first() # may return None
Django also has the 'latest(field_name = None)' function that finds the latest (max. value) entry. It not only works with date fields but also with strings and integers.
You can give the field name when calling that function:
max_rated_entry = YourModel.objects.latest('rating')
return max_rated_entry.details
Or you can already give that field name in your models meta data:
from django.db import models
class YourModel(models.Model):
#your class definition
class Meta:
get_latest_by = 'rating'
Now you can call 'latest()' without any parameters:
max_rated_entry = YourModel.objects.latest()
return max_rated_entry.details
I've tested this for my project, it finds the max/min in O(n) time:
from django.db.models import Max
# Find the maximum value of the rating and then get the record with that rating.
# Notice the double underscores in rating__max
max_rating = App.objects.aggregate(Max('rating'))['rating__max']
return App.objects.get(rating=max_rating)
This is guaranteed to get you one of the maximum elements efficiently, rather than sorting the whole table and getting the top (around O(n*logn)).
sol 01:
from .models import MyMODEL
max_rating = MyMODEL.objects.order_by('-rating').first()
sol 02:
from django.db.models import Max
from .models import MyMODEL
max_rating = MyMODEL.objects.aggregate(Max('rating'))
If you also want to get a value other than None in case the table is empty (e.g. 0), combine Max with Coalesce:
from django.db.models import Max, Value
from django.db.models.functions import Coalesce
max_rating = SomeModel.objects.aggregate(
max_rating=Coalesce(Max('rating'), Value(0))
)['max_rating']
To maybe improve on #afahim answer with regards to #Raydel Miranda comment, if you want a random comment. If you want all, then use just the filter
from django.db.models import Max
# Find the maximum value of the rating and then get the record with that rating.
# Notice the double underscores in rating__max
max_rating = App.objects.aggregate(Max('rating'))['rating__max']
return App.objects.filter(rating=max_rating).first()
maybe it will help someone's trouble, in CBV
def get_queryset(self):
sorgu = Sunum.objects.values('id', 'firma', 'projeadi', 'sunumdurum__durum', 'sunumdurum__aciklama'
).annotate(max_rank=Max('sunumdurum__kayittarihi'))
szlk={}
for sor in sorgu :
ana = sor['id'], sor['firma'], sor['projeadi']
dana = sor['sunumdurum__durum'], sor['sunumdurum__aciklama'], sor['max_rank']
szlk.setdefault(ana, dana)
return szlk