mirror of https://github.com/django/django.git
Fixed #25774 -- Refactor datetime expressions into public API
This commit is contained in:
parent
77b73e79a4
commit
2a4af0ea43
|
@ -1,5 +1,4 @@
|
|||
from django.db.models import DateTimeField
|
||||
from django.db.models.functions import Func
|
||||
from django.db.models import DateTimeField, Func
|
||||
|
||||
|
||||
class TransactionNow(Func):
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import copy
|
||||
import datetime
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import FieldError
|
||||
from django.db.backends import utils as backend_utils
|
||||
from django.db.models import fields
|
||||
from django.db.models.query_utils import Q
|
||||
from django.utils import six, timezone
|
||||
from django.utils import six
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
|
||||
|
@ -860,111 +859,6 @@ class Case(Expression):
|
|||
return sql, sql_params
|
||||
|
||||
|
||||
class Date(Expression):
|
||||
"""
|
||||
Add a date selection column.
|
||||
"""
|
||||
def __init__(self, lookup, lookup_type):
|
||||
super(Date, self).__init__(output_field=fields.DateField())
|
||||
self.lookup = lookup
|
||||
self.col = None
|
||||
self.lookup_type = lookup_type
|
||||
|
||||
def __repr__(self):
|
||||
return "{}({}, {})".format(self.__class__.__name__, self.lookup, self.lookup_type)
|
||||
|
||||
def get_source_expressions(self):
|
||||
return [self.col]
|
||||
|
||||
def set_source_expressions(self, exprs):
|
||||
self.col, = exprs
|
||||
|
||||
def resolve_expression(self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False):
|
||||
copy = self.copy()
|
||||
copy.col = query.resolve_ref(self.lookup, allow_joins, reuse, summarize)
|
||||
field = copy.col.output_field
|
||||
assert isinstance(field, fields.DateField), "%r isn't a DateField." % field.name
|
||||
if settings.USE_TZ:
|
||||
assert not isinstance(field, fields.DateTimeField), (
|
||||
"%r is a DateTimeField, not a DateField." % field.name
|
||||
)
|
||||
return copy
|
||||
|
||||
def as_sql(self, compiler, connection):
|
||||
sql, params = self.col.as_sql(compiler, connection)
|
||||
assert not(params)
|
||||
return connection.ops.date_trunc_sql(self.lookup_type, sql), []
|
||||
|
||||
def copy(self):
|
||||
copy = super(Date, self).copy()
|
||||
copy.lookup = self.lookup
|
||||
copy.lookup_type = self.lookup_type
|
||||
return copy
|
||||
|
||||
def convert_value(self, value, expression, connection, context):
|
||||
if isinstance(value, datetime.datetime):
|
||||
value = value.date()
|
||||
return value
|
||||
|
||||
|
||||
class DateTime(Expression):
|
||||
"""
|
||||
Add a datetime selection column.
|
||||
"""
|
||||
def __init__(self, lookup, lookup_type, tzinfo):
|
||||
super(DateTime, self).__init__(output_field=fields.DateTimeField())
|
||||
self.lookup = lookup
|
||||
self.col = None
|
||||
self.lookup_type = lookup_type
|
||||
if tzinfo is None:
|
||||
self.tzname = None
|
||||
else:
|
||||
self.tzname = timezone._get_timezone_name(tzinfo)
|
||||
self.tzinfo = tzinfo
|
||||
|
||||
def __repr__(self):
|
||||
return "{}({}, {}, {})".format(
|
||||
self.__class__.__name__, self.lookup, self.lookup_type, self.tzinfo)
|
||||
|
||||
def get_source_expressions(self):
|
||||
return [self.col]
|
||||
|
||||
def set_source_expressions(self, exprs):
|
||||
self.col, = exprs
|
||||
|
||||
def resolve_expression(self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False):
|
||||
copy = self.copy()
|
||||
copy.col = query.resolve_ref(self.lookup, allow_joins, reuse, summarize)
|
||||
field = copy.col.output_field
|
||||
assert isinstance(field, fields.DateTimeField), (
|
||||
"%r isn't a DateTimeField." % field.name
|
||||
)
|
||||
return copy
|
||||
|
||||
def as_sql(self, compiler, connection):
|
||||
sql, params = self.col.as_sql(compiler, connection)
|
||||
assert not(params)
|
||||
return connection.ops.datetime_trunc_sql(self.lookup_type, sql, self.tzname)
|
||||
|
||||
def copy(self):
|
||||
copy = super(DateTime, self).copy()
|
||||
copy.lookup = self.lookup
|
||||
copy.lookup_type = self.lookup_type
|
||||
copy.tzname = self.tzname
|
||||
return copy
|
||||
|
||||
def convert_value(self, value, expression, connection, context):
|
||||
if settings.USE_TZ:
|
||||
if value is None:
|
||||
raise ValueError(
|
||||
"Database returned an invalid value in QuerySet.datetimes(). "
|
||||
"Are time zone definitions for your database and pytz installed?"
|
||||
)
|
||||
value = value.replace(tzinfo=None)
|
||||
value = timezone.make_aware(value, self.tzinfo)
|
||||
return value
|
||||
|
||||
|
||||
class OrderBy(BaseExpression):
|
||||
template = '%(expression)s %(ordering)s'
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
from .base import (
|
||||
Cast, Coalesce, Concat, ConcatPair, Greatest, Least, Length, Lower, Now,
|
||||
Substr, Upper,
|
||||
)
|
||||
|
||||
from .datetime import (
|
||||
Extract, ExtractDay, ExtractHour, ExtractMinute, ExtractMonth,
|
||||
ExtractSecond, ExtractWeekDay, ExtractYear, Trunc, TruncDate, TruncDay,
|
||||
TruncHour, TruncMinute, TruncMonth, TruncSecond, TruncYear,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# base
|
||||
'Cast', 'Coalesce', 'Concat', 'ConcatPair', 'Greatest', 'Least', 'Length',
|
||||
'Lower', 'Now', 'Substr', 'Upper',
|
||||
# datetime
|
||||
'Extract', 'ExtractDay', 'ExtractHour', 'ExtractMinute', 'ExtractMonth',
|
||||
'ExtractSecond', 'ExtractWeekDay', 'ExtractYear',
|
||||
'Trunc', 'TruncDate', 'TruncDay', 'TruncHour', 'TruncMinute', 'TruncMonth',
|
||||
'TruncSecond', 'TruncYear',
|
||||
]
|
|
@ -0,0 +1,250 @@
|
|||
from __future__ import absolute_import
|
||||
from datetime import datetime
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import (
|
||||
DateField, DateTimeField, IntegerField, TimeField, Transform,
|
||||
)
|
||||
from django.db.models.lookups import (
|
||||
YearExact, YearGt, YearGte, YearLt, YearLte,
|
||||
)
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
|
||||
class TimezoneMixin(object):
|
||||
tzinfo = None
|
||||
|
||||
def get_tzname(self):
|
||||
# Timezone conversions must happen to the input datetime *before*
|
||||
# applying a function. 2015-12-31 23:00:00 -02:00 is stored in the
|
||||
# database as 2016-01-01 01:00:00 +00:00. Any results should be
|
||||
# based on the input datetime not the stored datetime.
|
||||
tzname = None
|
||||
if settings.USE_TZ:
|
||||
if self.tzinfo is None:
|
||||
tzname = timezone.get_current_timezone_name()
|
||||
else:
|
||||
tzname = timezone._get_timezone_name(self.tzinfo)
|
||||
return tzname
|
||||
|
||||
|
||||
class Extract(TimezoneMixin, Transform):
|
||||
lookup_name = None
|
||||
|
||||
def __init__(self, expression, lookup_name=None, tzinfo=None, **extra):
|
||||
if self.lookup_name is None:
|
||||
self.lookup_name = lookup_name
|
||||
if self.lookup_name is None:
|
||||
raise ValueError('lookup_name must be provided')
|
||||
self.tzinfo = tzinfo
|
||||
super(Extract, self).__init__(expression, **extra)
|
||||
|
||||
def as_sql(self, compiler, connection):
|
||||
sql, params = compiler.compile(self.lhs)
|
||||
lhs_output_field = self.lhs.output_field
|
||||
if isinstance(lhs_output_field, DateTimeField):
|
||||
tzname = self.get_tzname()
|
||||
sql, tz_params = connection.ops.datetime_extract_sql(self.lookup_name, sql, tzname)
|
||||
params.extend(tz_params)
|
||||
elif isinstance(lhs_output_field, DateField):
|
||||
sql = connection.ops.date_extract_sql(self.lookup_name, sql)
|
||||
elif isinstance(lhs_output_field, TimeField):
|
||||
sql = connection.ops.time_extract_sql(self.lookup_name, sql)
|
||||
else:
|
||||
# resolve_expression has already validated the output_field so this
|
||||
# assert should never be hit.
|
||||
assert False, "Tried to Extract from an invalid type."
|
||||
return sql, params
|
||||
|
||||
def resolve_expression(self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False):
|
||||
copy = super(Extract, self).resolve_expression(query, allow_joins, reuse, summarize, for_save)
|
||||
field = copy.lhs.output_field
|
||||
if not isinstance(field, (DateField, DateTimeField, TimeField)):
|
||||
raise ValueError('Extract input expression must be DateField, DateTimeField, or TimeField.')
|
||||
# Passing dates to functions expecting datetimes is most likely a mistake.
|
||||
if type(field) == DateField and copy.lookup_name in ('hour', 'minute', 'second'):
|
||||
raise ValueError(
|
||||
"Cannot extract time component '%s' from DateField '%s'. " % (copy.lookup_name, field.name)
|
||||
)
|
||||
return copy
|
||||
|
||||
@cached_property
|
||||
def output_field(self):
|
||||
return IntegerField()
|
||||
|
||||
|
||||
class ExtractYear(Extract):
|
||||
lookup_name = 'year'
|
||||
|
||||
|
||||
class ExtractMonth(Extract):
|
||||
lookup_name = 'month'
|
||||
|
||||
|
||||
class ExtractDay(Extract):
|
||||
lookup_name = 'day'
|
||||
|
||||
|
||||
class ExtractWeekDay(Extract):
|
||||
"""
|
||||
Return Sunday=1 through Saturday=7.
|
||||
|
||||
To replicate this in Python: (mydatetime.isoweekday() % 7) + 1
|
||||
"""
|
||||
lookup_name = 'week_day'
|
||||
|
||||
|
||||
class ExtractHour(Extract):
|
||||
lookup_name = 'hour'
|
||||
|
||||
|
||||
class ExtractMinute(Extract):
|
||||
lookup_name = 'minute'
|
||||
|
||||
|
||||
class ExtractSecond(Extract):
|
||||
lookup_name = 'second'
|
||||
|
||||
|
||||
DateField.register_lookup(ExtractYear)
|
||||
DateField.register_lookup(ExtractMonth)
|
||||
DateField.register_lookup(ExtractDay)
|
||||
DateField.register_lookup(ExtractWeekDay)
|
||||
|
||||
TimeField.register_lookup(ExtractHour)
|
||||
TimeField.register_lookup(ExtractMinute)
|
||||
TimeField.register_lookup(ExtractSecond)
|
||||
|
||||
DateTimeField.register_lookup(ExtractYear)
|
||||
DateTimeField.register_lookup(ExtractMonth)
|
||||
DateTimeField.register_lookup(ExtractDay)
|
||||
DateTimeField.register_lookup(ExtractWeekDay)
|
||||
DateTimeField.register_lookup(ExtractHour)
|
||||
DateTimeField.register_lookup(ExtractMinute)
|
||||
DateTimeField.register_lookup(ExtractSecond)
|
||||
|
||||
ExtractYear.register_lookup(YearExact)
|
||||
ExtractYear.register_lookup(YearGt)
|
||||
ExtractYear.register_lookup(YearGte)
|
||||
ExtractYear.register_lookup(YearLt)
|
||||
ExtractYear.register_lookup(YearLte)
|
||||
|
||||
|
||||
class TruncBase(TimezoneMixin, Transform):
|
||||
arity = 1
|
||||
kind = None
|
||||
tzinfo = None
|
||||
|
||||
def __init__(self, expression, output_field=None, tzinfo=None, **extra):
|
||||
self.tzinfo = tzinfo
|
||||
super(TruncBase, self).__init__(expression, output_field=output_field, **extra)
|
||||
|
||||
def as_sql(self, compiler, connection):
|
||||
inner_sql, inner_params = compiler.compile(self.lhs)
|
||||
# Escape any params because trunc_sql will format the string.
|
||||
inner_sql = inner_sql.replace('%s', '%%s')
|
||||
if isinstance(self.output_field, DateTimeField):
|
||||
tzname = self.get_tzname()
|
||||
sql, params = connection.ops.datetime_trunc_sql(self.kind, inner_sql, tzname)
|
||||
elif isinstance(self.output_field, DateField):
|
||||
sql = connection.ops.date_trunc_sql(self.kind, inner_sql)
|
||||
params = []
|
||||
else:
|
||||
raise ValueError('Trunc only valid on DateField or DateTimeField.')
|
||||
return sql, inner_params + params
|
||||
|
||||
def resolve_expression(self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False):
|
||||
copy = super(TruncBase, self).resolve_expression(query, allow_joins, reuse, summarize, for_save)
|
||||
field = copy.lhs.output_field
|
||||
# DateTimeField is a subclass of DateField so this works for both.
|
||||
assert isinstance(field, DateField), (
|
||||
"%r isn't a DateField or DateTimeField." % field.name
|
||||
)
|
||||
# If self.output_field was None, then accessing the field will trigger
|
||||
# the resolver to assign it to self.lhs.output_field.
|
||||
if not isinstance(copy.output_field, (DateField, DateTimeField)):
|
||||
raise ValueError('output_field must be either DateField or DateTimeField')
|
||||
# Passing dates to functions expecting datetimes is most likely a
|
||||
# mistake.
|
||||
if type(field) == DateField and (
|
||||
isinstance(copy.output_field, DateTimeField) or copy.kind in ('hour', 'minute', 'second')):
|
||||
raise ValueError("Cannot truncate DateField '%s' to DateTimeField. " % field.name)
|
||||
return copy
|
||||
|
||||
def convert_value(self, value, expression, connection, context):
|
||||
if isinstance(self.output_field, DateTimeField):
|
||||
if settings.USE_TZ:
|
||||
if value is None:
|
||||
raise ValueError(
|
||||
"Database returned an invalid datetime value. "
|
||||
"Are time zone definitions for your database and pytz installed?"
|
||||
)
|
||||
value = value.replace(tzinfo=None)
|
||||
value = timezone.make_aware(value, self.tzinfo)
|
||||
elif isinstance(value, datetime):
|
||||
# self.output_field is definitely a DateField here.
|
||||
value = value.date()
|
||||
return value
|
||||
|
||||
|
||||
class Trunc(TruncBase):
|
||||
|
||||
def __init__(self, expression, kind, output_field=None, tzinfo=None, **extra):
|
||||
self.kind = kind
|
||||
super(Trunc, self).__init__(expression, output_field=output_field, tzinfo=tzinfo, **extra)
|
||||
|
||||
|
||||
class TruncYear(TruncBase):
|
||||
kind = 'year'
|
||||
|
||||
|
||||
class TruncMonth(TruncBase):
|
||||
kind = 'month'
|
||||
|
||||
|
||||
class TruncDay(TruncBase):
|
||||
kind = 'day'
|
||||
|
||||
|
||||
class TruncDate(TruncBase):
|
||||
lookup_name = 'date'
|
||||
|
||||
@cached_property
|
||||
def output_field(self):
|
||||
return DateField()
|
||||
|
||||
def as_sql(self, compiler, connection):
|
||||
# Cast to date rather than truncate to date.
|
||||
lhs, lhs_params = compiler.compile(self.lhs)
|
||||
tzname = timezone.get_current_timezone_name() if settings.USE_TZ else None
|
||||
sql, tz_params = connection.ops.datetime_cast_date_sql(lhs, tzname)
|
||||
lhs_params.extend(tz_params)
|
||||
return sql, lhs_params
|
||||
|
||||
|
||||
class TruncHour(TruncBase):
|
||||
kind = 'hour'
|
||||
|
||||
@cached_property
|
||||
def output_field(self):
|
||||
return DateTimeField()
|
||||
|
||||
|
||||
class TruncMinute(TruncBase):
|
||||
kind = 'minute'
|
||||
|
||||
@cached_property
|
||||
def output_field(self):
|
||||
return DateTimeField()
|
||||
|
||||
|
||||
class TruncSecond(TruncBase):
|
||||
kind = 'second'
|
||||
|
||||
@cached_property
|
||||
def output_field(self):
|
||||
return DateTimeField()
|
||||
|
||||
|
||||
DateTimeField.register_lookup(TruncDate)
|
|
@ -2,13 +2,9 @@ import math
|
|||
import warnings
|
||||
from copy import copy
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models.expressions import Func, Value
|
||||
from django.db.models.fields import (
|
||||
DateField, DateTimeField, Field, IntegerField, TimeField,
|
||||
)
|
||||
from django.db.models.fields import DateTimeField, Field, IntegerField
|
||||
from django.db.models.query_utils import RegisterLookupMixin
|
||||
from django.utils import timezone
|
||||
from django.utils.deprecation import RemovedInDjango20Warning
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.six.moves import range
|
||||
|
@ -480,46 +476,6 @@ class IRegex(Regex):
|
|||
Field.register_lookup(IRegex)
|
||||
|
||||
|
||||
class DateTimeDateTransform(Transform):
|
||||
lookup_name = 'date'
|
||||
|
||||
@cached_property
|
||||
def output_field(self):
|
||||
return DateField()
|
||||
|
||||
def as_sql(self, compiler, connection):
|
||||
lhs, lhs_params = compiler.compile(self.lhs)
|
||||
tzname = timezone.get_current_timezone_name() if settings.USE_TZ else None
|
||||
sql, tz_params = connection.ops.datetime_cast_date_sql(lhs, tzname)
|
||||
lhs_params.extend(tz_params)
|
||||
return sql, lhs_params
|
||||
|
||||
|
||||
class DateTransform(Transform):
|
||||
def as_sql(self, compiler, connection):
|
||||
sql, params = compiler.compile(self.lhs)
|
||||
lhs_output_field = self.lhs.output_field
|
||||
if isinstance(lhs_output_field, DateTimeField):
|
||||
tzname = timezone.get_current_timezone_name() if settings.USE_TZ else None
|
||||
sql, tz_params = connection.ops.datetime_extract_sql(self.lookup_name, sql, tzname)
|
||||
params.extend(tz_params)
|
||||
elif isinstance(lhs_output_field, DateField):
|
||||
sql = connection.ops.date_extract_sql(self.lookup_name, sql)
|
||||
elif isinstance(lhs_output_field, TimeField):
|
||||
sql = connection.ops.time_extract_sql(self.lookup_name, sql)
|
||||
else:
|
||||
raise ValueError('DateTransform only valid on Date/Time/DateTimeFields')
|
||||
return sql, params
|
||||
|
||||
@cached_property
|
||||
def output_field(self):
|
||||
return IntegerField()
|
||||
|
||||
|
||||
class YearTransform(DateTransform):
|
||||
lookup_name = 'year'
|
||||
|
||||
|
||||
class YearLookup(Lookup):
|
||||
def year_lookup_bounds(self, connection, year):
|
||||
output_field = self.lhs.lhs.output_field
|
||||
|
@ -530,20 +486,6 @@ class YearLookup(Lookup):
|
|||
return bounds
|
||||
|
||||
|
||||
@YearTransform.register_lookup
|
||||
class YearExact(YearLookup):
|
||||
lookup_name = 'exact'
|
||||
|
||||
def as_sql(self, compiler, connection):
|
||||
# We will need to skip the extract part and instead go
|
||||
# directly with the originating field, that is self.lhs.lhs.
|
||||
lhs_sql, params = self.process_lhs(compiler, connection, self.lhs.lhs)
|
||||
rhs_sql, rhs_params = self.process_rhs(compiler, connection)
|
||||
bounds = self.year_lookup_bounds(connection, rhs_params[0])
|
||||
params.extend(bounds)
|
||||
return '%s BETWEEN %%s AND %%s' % lhs_sql, params
|
||||
|
||||
|
||||
class YearComparisonLookup(YearLookup):
|
||||
def as_sql(self, compiler, connection):
|
||||
# We will need to skip the extract part and instead go
|
||||
|
@ -564,7 +506,27 @@ class YearComparisonLookup(YearLookup):
|
|||
)
|
||||
|
||||
|
||||
@YearTransform.register_lookup
|
||||
class YearExact(YearLookup, Exact):
|
||||
lookup_name = 'exact'
|
||||
|
||||
def as_sql(self, compiler, connection):
|
||||
# We will need to skip the extract part and instead go
|
||||
# directly with the originating field, that is self.lhs.lhs.
|
||||
lhs_sql, params = self.process_lhs(compiler, connection, self.lhs.lhs)
|
||||
rhs_sql, rhs_params = self.process_rhs(compiler, connection)
|
||||
try:
|
||||
# Check that rhs_params[0] exists (IndexError),
|
||||
# it isn't None (TypeError), and is a number (ValueError)
|
||||
int(rhs_params[0])
|
||||
except (IndexError, TypeError, ValueError):
|
||||
# Can't determine the bounds before executing the query, so skip
|
||||
# optimizations by falling back to a standard exact comparison.
|
||||
return super(Exact, self).as_sql(compiler, connection)
|
||||
bounds = self.year_lookup_bounds(connection, rhs_params[0])
|
||||
params.extend(bounds)
|
||||
return '%s BETWEEN %%s AND %%s' % lhs_sql, params
|
||||
|
||||
|
||||
class YearGt(YearComparisonLookup):
|
||||
lookup_name = 'gt'
|
||||
|
||||
|
@ -572,7 +534,6 @@ class YearGt(YearComparisonLookup):
|
|||
return finish
|
||||
|
||||
|
||||
@YearTransform.register_lookup
|
||||
class YearGte(YearComparisonLookup):
|
||||
lookup_name = 'gte'
|
||||
|
||||
|
@ -580,7 +541,6 @@ class YearGte(YearComparisonLookup):
|
|||
return start
|
||||
|
||||
|
||||
@YearTransform.register_lookup
|
||||
class YearLt(YearComparisonLookup):
|
||||
lookup_name = 'lt'
|
||||
|
||||
|
@ -588,52 +548,8 @@ class YearLt(YearComparisonLookup):
|
|||
return start
|
||||
|
||||
|
||||
@YearTransform.register_lookup
|
||||
class YearLte(YearComparisonLookup):
|
||||
lookup_name = 'lte'
|
||||
|
||||
def get_bound(self, start, finish):
|
||||
return finish
|
||||
|
||||
|
||||
class MonthTransform(DateTransform):
|
||||
lookup_name = 'month'
|
||||
|
||||
|
||||
class DayTransform(DateTransform):
|
||||
lookup_name = 'day'
|
||||
|
||||
|
||||
class WeekDayTransform(DateTransform):
|
||||
lookup_name = 'week_day'
|
||||
|
||||
|
||||
class HourTransform(DateTransform):
|
||||
lookup_name = 'hour'
|
||||
|
||||
|
||||
class MinuteTransform(DateTransform):
|
||||
lookup_name = 'minute'
|
||||
|
||||
|
||||
class SecondTransform(DateTransform):
|
||||
lookup_name = 'second'
|
||||
|
||||
|
||||
DateField.register_lookup(YearTransform)
|
||||
DateField.register_lookup(MonthTransform)
|
||||
DateField.register_lookup(DayTransform)
|
||||
DateField.register_lookup(WeekDayTransform)
|
||||
|
||||
TimeField.register_lookup(HourTransform)
|
||||
TimeField.register_lookup(MinuteTransform)
|
||||
TimeField.register_lookup(SecondTransform)
|
||||
|
||||
DateTimeField.register_lookup(DateTimeDateTransform)
|
||||
DateTimeField.register_lookup(YearTransform)
|
||||
DateTimeField.register_lookup(MonthTransform)
|
||||
DateTimeField.register_lookup(DayTransform)
|
||||
DateTimeField.register_lookup(WeekDayTransform)
|
||||
DateTimeField.register_lookup(HourTransform)
|
||||
DateTimeField.register_lookup(MinuteTransform)
|
||||
DateTimeField.register_lookup(SecondTransform)
|
||||
|
|
|
@ -13,11 +13,12 @@ from django.db import (
|
|||
DJANGO_VERSION_PICKLE_KEY, IntegrityError, connections, router,
|
||||
transaction,
|
||||
)
|
||||
from django.db.models import sql
|
||||
from django.db.models import DateField, DateTimeField, sql
|
||||
from django.db.models.constants import LOOKUP_SEP
|
||||
from django.db.models.deletion import Collector
|
||||
from django.db.models.expressions import Date, DateTime, F
|
||||
from django.db.models.expressions import F
|
||||
from django.db.models.fields import AutoField
|
||||
from django.db.models.functions import Trunc
|
||||
from django.db.models.query_utils import (
|
||||
InvalidQuery, Q, check_rel_lookup_compatibility,
|
||||
)
|
||||
|
@ -739,7 +740,7 @@ class QuerySet(object):
|
|||
assert order in ('ASC', 'DESC'), \
|
||||
"'order' must be either 'ASC' or 'DESC'."
|
||||
return self.annotate(
|
||||
datefield=Date(field_name, kind),
|
||||
datefield=Trunc(field_name, kind, output_field=DateField()),
|
||||
plain_field=F(field_name)
|
||||
).values_list(
|
||||
'datefield', flat=True
|
||||
|
@ -760,7 +761,7 @@ class QuerySet(object):
|
|||
else:
|
||||
tzinfo = None
|
||||
return self.annotate(
|
||||
datetimefield=DateTime(field_name, kind, tzinfo),
|
||||
datetimefield=Trunc(field_name, kind, output_field=DateTimeField(), tzinfo=tzinfo),
|
||||
plain_field=F(field_name)
|
||||
).values_list(
|
||||
'datetimefield', flat=True
|
||||
|
|
|
@ -293,3 +293,402 @@ Usage example::
|
|||
.. versionchanged:: 1.9
|
||||
|
||||
The ability to register the function as a transform was added.
|
||||
|
||||
Date Functions
|
||||
==============
|
||||
|
||||
.. module:: django.db.models.functions.datetime
|
||||
|
||||
.. versionadded:: 1.10
|
||||
|
||||
We'll be using the following model in examples of each function::
|
||||
|
||||
class Experiment(models.Model):
|
||||
start_time = models.DateTimeField()
|
||||
start_date = models.DateField(null=True, blank=True)
|
||||
end_time = models.DateTimeField(null=True, blank=True)
|
||||
end_date = models.DateField(null=True, blank=True)
|
||||
|
||||
``Extract``
|
||||
-----------
|
||||
|
||||
.. class:: Extract(expression, lookup_name=None, tzinfo=None, **extra)
|
||||
|
||||
Extracts a component of a date as a number.
|
||||
|
||||
Takes an ``expression`` representing a ``DateField`` or ``DateTimeField`` and a
|
||||
``lookup_name``, and returns the part of the date referenced by ``lookup_name``
|
||||
as an ``IntegerField``. Django usually uses the databases' extract function, so
|
||||
you may use any ``lookup_name`` that your database supports. A ``tzinfo``
|
||||
subclass, usually provided by ``pytz``, can be passed to extract a value in a
|
||||
specific timezone.
|
||||
|
||||
Given the datetime ``2015-06-15 23:30:01.000321+00:00``, the built-in
|
||||
``lookup_name``\s return:
|
||||
|
||||
* "year": 2015
|
||||
* "month": 6
|
||||
* "day": 15
|
||||
* "week_day": 2
|
||||
* "hour": 23
|
||||
* "minute": 30
|
||||
* "second": 1
|
||||
|
||||
If a different timezone like ``Australia/Melbourne`` is active in Django, then
|
||||
the datetime is converted to the timezone before the value is extracted. The
|
||||
timezone offset for Melbourne in the example date above is +10:00. The values
|
||||
returned when this timezone is active will be the same as above except for:
|
||||
|
||||
* "day": 16
|
||||
* "week_day": 3
|
||||
* "hour": 9
|
||||
|
||||
.. admonition:: ``week_day`` values
|
||||
|
||||
The ``week_day`` ``lookup_type`` is calculated differently from most
|
||||
databases and from Python's standard functions. This function will return
|
||||
``1`` for Sunday, ``2`` for Monday, through ``7`` for Saturday.
|
||||
|
||||
The equivalent calculation in Python is::
|
||||
|
||||
>>> from datetime import datetime
|
||||
>>> dt = datetime(2015, 6, 15)
|
||||
>>> (dt.isoweekday() % 7) + 1
|
||||
2
|
||||
|
||||
Each ``lookup_name`` above has a corresponding ``Extract`` subclass (listed
|
||||
below) that should typically be used instead of the more verbose equivalent,
|
||||
e.g. use ``ExtractYear(...)`` rather than ``Extract(..., lookup_name='year')``.
|
||||
|
||||
Usage example::
|
||||
|
||||
>>> from datetime import datetime
|
||||
>>> from django.db.models.functions import Extract
|
||||
>>> start = datetime(2015, 6, 15)
|
||||
>>> end = datetime(2015, 7, 2)
|
||||
>>> Experiment.objects.create(
|
||||
... start_time=start, start_date=start.date(),
|
||||
... end_time=end, end_date=end.date())
|
||||
>>> # Add the experiment start year as a field in the QuerySet.
|
||||
>>> experiment = Experiment.objects.annotate(
|
||||
... start_year=Extract('start_time', 'year')).get()
|
||||
>>> experiment.start_year
|
||||
2015
|
||||
>>> # How many experiments completed in the same year in which they started?
|
||||
>>> Experiment.objects.filter(
|
||||
... start_time__year=Extract('end_time', 'year')).count()
|
||||
1
|
||||
|
||||
``DateField`` extracts
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. class:: ExtractYear(expression, tzinfo=None, **extra)
|
||||
|
||||
.. attribute:: lookup_name = 'year'
|
||||
|
||||
.. class:: ExtractMonth(expression, tzinfo=None, **extra)
|
||||
|
||||
.. attribute:: lookup_name = 'month'
|
||||
|
||||
.. class:: ExtractDay(expression, tzinfo=None, **extra)
|
||||
|
||||
.. attribute:: lookup_name = 'day'
|
||||
|
||||
.. class:: ExtractWeekDay(expression, tzinfo=None, **extra)
|
||||
|
||||
.. attribute:: lookup_name = 'week_day'
|
||||
|
||||
These are logically equivalent to ``Extract('date_field', lookup_name)``. Each
|
||||
class is also a ``Transform`` registered on ``DateField`` and ``DateTimeField``
|
||||
as ``__(lookup_name)``, e.g. ``__year``.
|
||||
|
||||
Since ``DateField``\s don't have a time component, only ``Extract`` subclasses
|
||||
that deal with date-parts can be used with ``DateField``::
|
||||
|
||||
>>> from datetime import datetime
|
||||
>>> from django.utils import timezone
|
||||
>>> from django.db.models.functions import (
|
||||
... ExtractYear, ExtractMonth, ExtractDay, ExtractWeekDay
|
||||
... )
|
||||
>>> start_2015 = datetime(2015, 6, 15, 23, 30, 1, tzinfo=timezone.utc)
|
||||
>>> end_2015 = datetime(2015, 6, 16, 13, 11, 27, tzinfo=timezone.utc)
|
||||
>>> Experiment.objects.create(
|
||||
... start_time=start_2015, start_date=start_2015.date(),
|
||||
... end_time=end_2015, end_date=end_2015.date())
|
||||
>>> Experiment.objects.annotate(
|
||||
... year=ExtractYear('start_date'),
|
||||
... month=ExtractMonth('start_date'),
|
||||
... day=ExtractDay('start_date'),
|
||||
... weekday=ExtractWeekDay('start_date'),
|
||||
... ).values('year', 'month', 'day', 'weekday').get(
|
||||
... end_date__year=ExtractYear('start_date'),
|
||||
... )
|
||||
{'year': 2015, 'month': 6, 'day': 15, 'weekday': 2}
|
||||
|
||||
``DateTimeField`` extracts
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
In addition to the following, all extracts for ``DateField`` listed above may
|
||||
also be used on ``DateTimeField``\s .
|
||||
|
||||
.. class:: ExtractHour(expression, tzinfo=None, **extra)
|
||||
|
||||
.. attribute:: lookup_name = 'hour'
|
||||
|
||||
.. class:: ExtractMinute(expression, tzinfo=None, **extra)
|
||||
|
||||
.. attribute:: lookup_name = 'minute'
|
||||
|
||||
.. class:: ExtractSecond(expression, tzinfo=None, **extra)
|
||||
|
||||
.. attribute:: lookup_name = 'second'
|
||||
|
||||
These are logically equivalent to ``Extract('datetime_field', lookup_name)``.
|
||||
Each class is also a ``Transform`` registered on ``DateTimeField`` as
|
||||
``__(lookup_name)``, e.g. ``__minute``.
|
||||
|
||||
``DateTimeField`` examples::
|
||||
|
||||
>>> from datetime import datetime
|
||||
>>> from django.utils import timezone
|
||||
>>> from django.db.models.functions import (
|
||||
... ExtractYear, ExtractMonth, ExtractDay, ExtractWeekDay,
|
||||
... ExtractHour, ExtractMinute, ExtractSecond,
|
||||
... )
|
||||
>>> start_2015 = datetime(2015, 6, 15, 23, 30, 1, tzinfo=timezone.utc)
|
||||
>>> end_2015 = datetime(2015, 6, 16, 13, 11, 27, tzinfo=timezone.utc)
|
||||
>>> Experiment.objects.create(
|
||||
... start_time=start_2015, start_date=start_2015.date(),
|
||||
... end_time=end_2015, end_date=end_2015.date())
|
||||
>>> Experiment.objects.annotate(
|
||||
... year=ExtractYear('start_time'),
|
||||
... month=ExtractMonth('start_time'),
|
||||
... day=ExtractDay('start_time'),
|
||||
... weekday=ExtractWeekDay('start_time'),
|
||||
... hour=ExtractHour('start_time'),
|
||||
... minute=ExtractMinute('start_time'),
|
||||
... second=ExtractSecond('start_time'),
|
||||
... ).values(
|
||||
... 'year', 'month', 'day', 'weekday', 'hour', 'minute', 'second',
|
||||
... ).get(end_time__year=ExtractYear('start_time'))
|
||||
{'year': 2015, 'month': 6, 'day': 15, 'weekday': 2, 'hour': 23, 'minute': 30, 'second': 1}
|
||||
|
||||
When :setting:`USE_TZ` is ``True`` then datetimes are stored in the database
|
||||
in UTC. If a different timezone is active in Django, the datetime is converted
|
||||
to that timezone before the value is extracted. The example below converts to
|
||||
the Melbourne timezone (UTC +10:00), which changes the day, weekday, and hour
|
||||
values that are returned::
|
||||
|
||||
>>> import pytz
|
||||
>>> tzinfo = pytz.timezone('Australia/Melbourne') # UTC+10:00
|
||||
>>> with timezone.override(tzinfo):
|
||||
... Experiment.objects.annotate(
|
||||
... day=ExtractDay('start_time'),
|
||||
... weekday=ExtractWeekDay('start_time'),
|
||||
... hour=ExtractHour('start_time'),
|
||||
... ).values('day', 'weekday', 'hour').get(
|
||||
... end_time__year=ExtractYear('start_time'),
|
||||
... )
|
||||
{'day': 16, 'weekday': 3, 'hour': 9}
|
||||
|
||||
Explicitly passing the timezone to the ``Extract`` function behaves in the same
|
||||
way, and takes priority over an active timezone::
|
||||
|
||||
>>> import pytz
|
||||
>>> tzinfo = pytz.timezone('Australia/Melbourne')
|
||||
>>> Experiment.objects.annotate(
|
||||
... day=ExtractDay('start_time', tzinfo=melb),
|
||||
... weekday=ExtractWeekDay('start_time', tzinfo=melb),
|
||||
... hour=ExtractHour('start_time', tzinfo=melb),
|
||||
... ).values('day', 'weekday', 'hour').get(
|
||||
... end_time__year=ExtractYear('start_time'),
|
||||
... )
|
||||
{'day': 16, 'weekday': 3, 'hour': 9}
|
||||
|
||||
|
||||
``Trunc``
|
||||
---------
|
||||
|
||||
.. class:: Trunc(expression, kind, output_field=None, tzinfo=None, **extra)
|
||||
|
||||
Truncates a date up to a significant component.
|
||||
|
||||
When you only care if something happened in a particular year, hour, or day,
|
||||
but not the exact second, then ``Trunc`` (and its subclasses) can be useful to
|
||||
filter or aggregate your data. For example, you can use ``Trunc`` to calculate
|
||||
the number of sales per day.
|
||||
|
||||
``Trunc`` takes a single ``expression``, representing a ``DateField`` or
|
||||
``DateTimeField``, a ``kind`` representing a date part, and an ``output_field``
|
||||
that's either ``DateTimeField()`` or ``DateField()``. It returns a datetime or
|
||||
date, depending on ``output_field``, with fields up to ``kind`` set to their
|
||||
minimum value. If ``output_field`` is omitted, it will default to the
|
||||
``output_field`` of ``expression``. A ``tzinfo`` subclass, usually provided by
|
||||
``pytz``, can be passed to truncate a value in a specific timezone.
|
||||
|
||||
Given the datetime ``2015-06-15 14:30:50.000321+00:00``, the built-in ``kind``\s
|
||||
return:
|
||||
|
||||
* "year": 2015-01-01 00:00:00+00:00
|
||||
* "month": 2015-06-01 00:00:00+00:00
|
||||
* "day": 2015-06-15 00:00:00+00:00
|
||||
* "hour": 2015-06-15 14:00:00+00:00
|
||||
* "minute": 2015-06-15 14:30:00+00:00
|
||||
* "second": 2015-06-15 14:30:50+00:00
|
||||
|
||||
If a different timezone like ``Australia/Melbourne`` is active in Django, then
|
||||
the datetime is converted to the new timezone before the value is truncated.
|
||||
The timezone offset for Melbourne in the example date above is +10:00. The
|
||||
values returned when this timezone is active will be:
|
||||
|
||||
* "year": 2015-01-01 00:00:00+11:00
|
||||
* "month": 2015-06-01 00:00:00+10:00
|
||||
* "day": 2015-06-16 00:00:00+10:00
|
||||
* "hour": 2015-06-16 00:00:00+10:00
|
||||
* "minute": 2015-06-16 00:30:00+10:00
|
||||
* "second": 2015-06-16 00:30:50+10:00
|
||||
|
||||
The year has an offset of +11:00 because the result transitioned into daylight
|
||||
saving time.
|
||||
|
||||
Each ``kind`` above has a corresponding ``Trunc`` subclass (listed below) that
|
||||
should typically be used instead of the more verbose equivalent,
|
||||
e.g. use ``TruncYear(...)`` rather than ``Trunc(..., kind='year')``.
|
||||
|
||||
The subclasses are all defined as transforms, but they aren't registered with
|
||||
any fields, because the obvious lookup names are already reserved by the
|
||||
``Extract`` subclasses.
|
||||
|
||||
Usage example::
|
||||
|
||||
>>> from datetime import datetime
|
||||
>>> from django.db.models import Count, DateTimeField
|
||||
>>> from django.db.models.functions import Trunc
|
||||
>>> Experiment.objects.create(start_time=datetime(2015, 6, 15, 14, 30, 50, 321))
|
||||
>>> Experiment.objects.create(start_time=datetime(2015, 6, 15, 14, 40, 2, 123))
|
||||
>>> Experiment.objects.create(start_time=datetime(2015, 12, 25, 10, 5, 27, 999))
|
||||
>>> experiments_per_day = Experiment.objects.annotate(
|
||||
... start_day=Trunc('start_time', 'day', output_field=DateTimeField())
|
||||
... ).values('start_day').annotate(experiments=Count('id'))
|
||||
>>> for exp in experiments_per_day:
|
||||
... print(exp['start_day'], exp['experiments'])
|
||||
...
|
||||
2015-06-15 00:00:00 2
|
||||
2015-12-25 00:00:00 1
|
||||
>>> experiments = Experiment.objects.annotate(
|
||||
... start_day=Trunc('start_time', 'day', output_field=DateTimeField())
|
||||
... ).filter(start_day=datetime(2015, 6, 15))
|
||||
>>> for exp in experiments:
|
||||
... print(exp.start_time)
|
||||
...
|
||||
2015-06-15 14:30:50.000321
|
||||
2015-06-15 14:40:02.000123
|
||||
|
||||
``DateField`` truncation
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. class:: TruncYear(expression, output_field=None, tzinfo=None, **extra)
|
||||
|
||||
.. attribute:: kind = 'year'
|
||||
|
||||
.. class:: TruncMonth(expression, output_field=None, tzinfo=None, **extra)
|
||||
|
||||
.. attribute:: kind = 'month'
|
||||
|
||||
These are logically equivalent to ``Trunc('date_field', kind)``. They truncate
|
||||
all parts of the date up to ``kind`` which allows grouping or filtering dates
|
||||
with less precision. ``expression`` can have an ``output_field`` of either
|
||||
``DateField`` or ``DateTimeField``.
|
||||
|
||||
Since ``DateField``\s don't have a time component, only ``Trunc`` subclasses
|
||||
that deal with date-parts can be used with ``DateField``::
|
||||
|
||||
>>> from datetime import datetime
|
||||
>>> from django.db.models import Count
|
||||
>>> from django.db.models.functions import TruncMonth, TruncYear
|
||||
>>> from django.utils import timezone
|
||||
>>> start1 = datetime(2014, 6, 15, 14, 30, 50, 321, tzinfo=timezone.utc)
|
||||
>>> start2 = datetime(2015, 6, 15, 14, 40, 2, 123, tzinfo=timezone.utc)
|
||||
>>> start3 = datetime(2015, 12, 31, 17, 5, 27, 999, tzinfo=timezone.utc)
|
||||
>>> Experiment.objects.create(start_time=start1, start_date=start1.date())
|
||||
>>> Experiment.objects.create(start_time=start2, start_date=start2.date())
|
||||
>>> Experiment.objects.create(start_time=start3, start_date=start3.date())
|
||||
>>> experiments_per_year = Experiment.objects.annotate(
|
||||
... year=TruncYear('start_date')).values('year').annotate(
|
||||
... experiments=Count('id'))
|
||||
>>> for exp in experiments_per_year:
|
||||
... print(exp['year'], exp['experiments'])
|
||||
...
|
||||
2014-01-01 1
|
||||
2015-01-01 2
|
||||
|
||||
>>> import pytz
|
||||
>>> melb = pytz.timezone('Australia/Melbourne')
|
||||
>>> experiments_per_month = Experiment.objects.annotate(
|
||||
... month=TruncMonth('start_time', tzinfo=melb)).values('month').annotate(
|
||||
... experiments=Count('id'))
|
||||
>>> for exp in experiments_per_month:
|
||||
... print(exp['month'], exp['experiments'])
|
||||
...
|
||||
2015-06-01 00:00:00+10:00 1
|
||||
2016-01-01 00:00:00+11:00 1
|
||||
2014-06-01 00:00:00+10:00 1
|
||||
|
||||
``DateTimeField`` truncation
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. class:: TruncDate(expression, **extra)
|
||||
|
||||
.. attribute:: lookup_name = 'date'
|
||||
.. attribute:: output_field = DateField()
|
||||
|
||||
``TruncDate`` casts ``expression`` to a date rather than using the built-in SQL
|
||||
truncate function. It's also registered as a transform on ``DateTimeField`` as
|
||||
``__date``.
|
||||
|
||||
.. class:: TruncDay(expression, output_field=None, tzinfo=None, **extra)
|
||||
|
||||
.. attribute:: kind = 'day'
|
||||
|
||||
.. class:: TruncHour(expression, output_field=None, tzinfo=None, **extra)
|
||||
|
||||
.. attribute:: kind = 'hour'
|
||||
|
||||
.. class:: TruncMinute(expression, output_field=None, tzinfo=None, **extra)
|
||||
|
||||
.. attribute:: kind = 'minute'
|
||||
|
||||
.. class:: TruncSecond(expression, output_field=None, tzinfo=None, **extra)
|
||||
|
||||
.. attribute:: kind = 'second'
|
||||
|
||||
These are logically equivalent to ``Trunc('datetime_field', kind)``. They
|
||||
truncate all parts of the date up to ``kind`` and allow grouping or filtering
|
||||
datetimes with less precision. ``expression`` must have an ``output_field`` of
|
||||
``DateTimeField``.
|
||||
|
||||
Usage example::
|
||||
|
||||
>>> from datetime import date, datetime
|
||||
>>> from django.db.models import Count
|
||||
>>> from django.db.models.functions import (
|
||||
... TruncDate, TruncDay, TruncHour, TruncMinute, TruncSecond,
|
||||
... )
|
||||
>>> from django.utils import timezone
|
||||
>>> import pytz
|
||||
>>> start1 = datetime(2014, 6, 15, 14, 30, 50, 321, tzinfo=timezone.utc)
|
||||
>>> Experiment.objects.create(start_time=start1, start_date=start1.date())
|
||||
>>> melb = pytz.timezone('Australia/Melbourne')
|
||||
>>> Experiment.objects.annotate(
|
||||
... date=TruncDate('start_time'),
|
||||
... day=TruncDay('start_time', tzinfo=melb),
|
||||
... hour=TruncHour('start_time', tzinfo=melb),
|
||||
... minute=TruncMinute('start_time'),
|
||||
... second=TruncSecond('start_time'),
|
||||
... ).values('date', 'day', 'hour', 'minute', 'second').get()
|
||||
{'date': datetime.date(2014, 6, 15),
|
||||
'day': datetime.datetime(2014, 6, 16, 0, 0, tzinfo=<DstTzInfo 'Australia/Melbourne' AEST+10:00:00 STD>),
|
||||
'hour': datetime.datetime(2014, 6, 16, 0, 0, tzinfo=<DstTzInfo 'Australia/Melbourne' AEST+10:00:00 STD>),
|
||||
'minute': 'minute': datetime.datetime(2014, 6, 15, 14, 30, tzinfo=<UTC>),
|
||||
'second': datetime.datetime(2014, 6, 15, 14, 30, 50, tzinfo=<UTC>)
|
||||
}
|
||||
|
|
|
@ -443,6 +443,13 @@ Models
|
|||
* A proxy model may now inherit multiple proxy models that share a common
|
||||
non-abstract parent class.
|
||||
|
||||
* Added :class:`~django.db.models.functions.datetime.Extract` functions
|
||||
to extract datetime components as integers, such as year and hour.
|
||||
|
||||
* Added :class:`~django.db.models.functions.datetime.Trunc` functions to
|
||||
truncate a date or datetime to a significant component. They enable queries
|
||||
like sales-per-day or sales-per-hour.
|
||||
|
||||
* ``Model.__init__()`` now sets values of virtual fields from its keyword
|
||||
arguments.
|
||||
|
||||
|
@ -900,6 +907,10 @@ Miscellaneous
|
|||
989 characters. If you were counting on a limited length, truncate the subject
|
||||
yourself.
|
||||
|
||||
* Private expressions ``django.db.models.expressions.Date`` and ``DateTime``
|
||||
are removed. The new :class:`~django.db.models.functions.datetime.Trunc`
|
||||
expressions provide the same functionality.
|
||||
|
||||
.. _deprecated-features-1.10:
|
||||
|
||||
Features deprecated in 1.10
|
||||
|
|
|
@ -8,6 +8,7 @@ from django.utils.encoding import python_2_unicode_compatible
|
|||
class Article(models.Model):
|
||||
title = models.CharField(max_length=100)
|
||||
pub_date = models.DateTimeField()
|
||||
published_on = models.DateField(null=True)
|
||||
|
||||
categories = models.ManyToManyField("Category", related_name="articles")
|
||||
|
||||
|
|
|
@ -153,3 +153,9 @@ class DateTimesTests(TestCase):
|
|||
datetime.datetime(2005, 7, 30, 0, 0),
|
||||
datetime.datetime(2005, 7, 29, 0, 0),
|
||||
datetime.datetime(2005, 7, 28, 0, 0)])
|
||||
|
||||
def test_datetimes_disallows_date_fields(self):
|
||||
dt = datetime.datetime(2005, 7, 28, 12, 15)
|
||||
Article.objects.create(pub_date=dt, published_on=dt.date(), title="Don't put dates into datetime functions!")
|
||||
with self.assertRaisesMessage(ValueError, "Cannot truncate DateField 'published_on' to DateTimeField"):
|
||||
list(Article.objects.datetimes('published_on', 'second'))
|
||||
|
|
|
@ -41,3 +41,18 @@ class Fan(models.Model):
|
|||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class DTModel(models.Model):
|
||||
name = models.CharField(max_length=32)
|
||||
start_datetime = models.DateTimeField(null=True, blank=True)
|
||||
end_datetime = models.DateTimeField(null=True, blank=True)
|
||||
start_date = models.DateField(null=True, blank=True)
|
||||
end_date = models.DateField(null=True, blank=True)
|
||||
start_time = models.TimeField(null=True, blank=True)
|
||||
end_time = models.TimeField(null=True, blank=True)
|
||||
duration = models.DurationField(null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return 'DTModel({0})'.format(self.name)
|
||||
|
|
|
@ -0,0 +1,694 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from datetime import datetime
|
||||
from unittest import skipIf
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import connection
|
||||
from django.db.models import DateField, DateTimeField, IntegerField
|
||||
from django.db.models.functions import (
|
||||
Extract, ExtractDay, ExtractHour, ExtractMinute, ExtractMonth,
|
||||
ExtractSecond, ExtractWeekDay, ExtractYear, Trunc, TruncDate, TruncDay,
|
||||
TruncHour, TruncMinute, TruncMonth, TruncSecond, TruncYear,
|
||||
)
|
||||
from django.test import TestCase, override_settings
|
||||
from django.utils import timezone
|
||||
|
||||
from .models import DTModel
|
||||
|
||||
try:
|
||||
import pytz
|
||||
except ImportError:
|
||||
pytz = None
|
||||
|
||||
|
||||
def microsecond_support(value):
|
||||
return value if connection.features.supports_microsecond_precision else value.replace(microsecond=0)
|
||||
|
||||
|
||||
def truncate_to(value, kind, tzinfo=None):
|
||||
# Convert to target timezone before truncation
|
||||
if tzinfo is not None:
|
||||
value = value.astimezone(tzinfo)
|
||||
|
||||
def truncate(value, kind):
|
||||
if kind == 'second':
|
||||
return value.replace(microsecond=0)
|
||||
if kind == 'minute':
|
||||
return value.replace(second=0, microsecond=0)
|
||||
if kind == 'hour':
|
||||
return value.replace(minute=0, second=0, microsecond=0)
|
||||
if kind == 'day':
|
||||
if isinstance(value, datetime):
|
||||
return value.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
return value
|
||||
if kind == 'month':
|
||||
if isinstance(value, datetime):
|
||||
return value.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
return value.replace(day=1)
|
||||
# otherwise, truncate to year
|
||||
if isinstance(value, datetime):
|
||||
return value.replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
return value.replace(month=1, day=1)
|
||||
|
||||
value = truncate(value, kind)
|
||||
if tzinfo is not None:
|
||||
# If there was a daylight saving transition, then reset the timezone.
|
||||
value = timezone.make_aware(value.replace(tzinfo=None), tzinfo)
|
||||
return value
|
||||
|
||||
|
||||
@override_settings(USE_TZ=False)
|
||||
class DateFunctionTests(TestCase):
|
||||
|
||||
def create_model(self, start_datetime, end_datetime):
|
||||
return DTModel.objects.create(
|
||||
name=start_datetime.isoformat(),
|
||||
start_datetime=start_datetime, end_datetime=end_datetime,
|
||||
start_date=start_datetime.date(), end_date=end_datetime.date(),
|
||||
start_time=start_datetime.time(), end_time=end_datetime.time(),
|
||||
duration=(end_datetime - start_datetime),
|
||||
)
|
||||
|
||||
def test_extract_year_exact_lookup(self):
|
||||
"""
|
||||
Extract year uses a BETWEEN filter to compare the year to allow indexes
|
||||
to be used.
|
||||
"""
|
||||
start_datetime = datetime(2015, 6, 15, 14, 10)
|
||||
end_datetime = datetime(2016, 6, 15, 14, 10)
|
||||
if settings.USE_TZ:
|
||||
start_datetime = timezone.make_aware(start_datetime, is_dst=False)
|
||||
end_datetime = timezone.make_aware(end_datetime, is_dst=False)
|
||||
self.create_model(start_datetime, end_datetime)
|
||||
self.create_model(end_datetime, start_datetime)
|
||||
|
||||
qs = DTModel.objects.filter(start_datetime__year__exact=2015)
|
||||
self.assertEqual(qs.count(), 1)
|
||||
query_string = str(qs.query).lower()
|
||||
self.assertEqual(query_string.count(' between '), 1)
|
||||
self.assertEqual(query_string.count('extract'), 0)
|
||||
|
||||
# exact is implied and should be the same
|
||||
qs = DTModel.objects.filter(start_datetime__year=2015)
|
||||
self.assertEqual(qs.count(), 1)
|
||||
query_string = str(qs.query).lower()
|
||||
self.assertEqual(query_string.count(' between '), 1)
|
||||
self.assertEqual(query_string.count('extract'), 0)
|
||||
|
||||
# date and datetime fields should behave the same
|
||||
qs = DTModel.objects.filter(start_date__year=2015)
|
||||
self.assertEqual(qs.count(), 1)
|
||||
query_string = str(qs.query).lower()
|
||||
self.assertEqual(query_string.count(' between '), 1)
|
||||
self.assertEqual(query_string.count('extract'), 0)
|
||||
|
||||
def test_extract_year_greaterthan_lookup(self):
|
||||
start_datetime = datetime(2015, 6, 15, 14, 10)
|
||||
end_datetime = datetime(2016, 6, 15, 14, 10)
|
||||
if settings.USE_TZ:
|
||||
start_datetime = timezone.make_aware(start_datetime, is_dst=False)
|
||||
end_datetime = timezone.make_aware(end_datetime, is_dst=False)
|
||||
self.create_model(start_datetime, end_datetime)
|
||||
self.create_model(end_datetime, start_datetime)
|
||||
|
||||
qs = DTModel.objects.filter(start_datetime__year__gt=2015)
|
||||
self.assertEqual(qs.count(), 1)
|
||||
self.assertEqual(str(qs.query).lower().count('extract'), 0)
|
||||
qs = DTModel.objects.filter(start_datetime__year__gte=2015)
|
||||
self.assertEqual(qs.count(), 2)
|
||||
self.assertEqual(str(qs.query).lower().count('extract'), 0)
|
||||
|
||||
def test_extract_year_lessthan_lookup(self):
|
||||
start_datetime = datetime(2015, 6, 15, 14, 10)
|
||||
end_datetime = datetime(2016, 6, 15, 14, 10)
|
||||
if settings.USE_TZ:
|
||||
start_datetime = timezone.make_aware(start_datetime, is_dst=False)
|
||||
end_datetime = timezone.make_aware(end_datetime, is_dst=False)
|
||||
self.create_model(start_datetime, end_datetime)
|
||||
self.create_model(end_datetime, start_datetime)
|
||||
|
||||
qs = DTModel.objects.filter(start_datetime__year__lt=2016)
|
||||
self.assertEqual(qs.count(), 1)
|
||||
self.assertEqual(str(qs.query).count('extract'), 0)
|
||||
qs = DTModel.objects.filter(start_datetime__year__lte=2016)
|
||||
self.assertEqual(qs.count(), 2)
|
||||
self.assertEqual(str(qs.query).count('extract'), 0)
|
||||
|
||||
def test_extract_func(self):
|
||||
start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
|
||||
end_datetime = microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123))
|
||||
if settings.USE_TZ:
|
||||
start_datetime = timezone.make_aware(start_datetime, is_dst=False)
|
||||
end_datetime = timezone.make_aware(end_datetime, is_dst=False)
|
||||
self.create_model(start_datetime, end_datetime)
|
||||
self.create_model(end_datetime, start_datetime)
|
||||
|
||||
with self.assertRaisesMessage(ValueError, 'lookup_name must be provided'):
|
||||
Extract('start_datetime')
|
||||
|
||||
msg = 'Extract input expression must be DateField, DateTimeField, or TimeField.'
|
||||
with self.assertRaisesMessage(ValueError, msg):
|
||||
list(DTModel.objects.annotate(extracted=Extract('name', 'hour')))
|
||||
|
||||
with self.assertRaisesMessage(
|
||||
ValueError, "Cannot extract time component 'second' from DateField 'start_date'."):
|
||||
list(DTModel.objects.annotate(extracted=Extract('start_date', 'second')))
|
||||
|
||||
self.assertQuerysetEqual(
|
||||
DTModel.objects.annotate(extracted=Extract('start_datetime', 'year')).order_by('start_datetime'),
|
||||
[(start_datetime, start_datetime.year), (end_datetime, end_datetime.year)],
|
||||
lambda m: (m.start_datetime, m.extracted)
|
||||
)
|
||||
self.assertQuerysetEqual(
|
||||
DTModel.objects.annotate(extracted=Extract('start_datetime', 'month')).order_by('start_datetime'),
|
||||
[(start_datetime, start_datetime.month), (end_datetime, end_datetime.month)],
|
||||
lambda m: (m.start_datetime, m.extracted)
|
||||
)
|
||||
self.assertQuerysetEqual(
|
||||
DTModel.objects.annotate(extracted=Extract('start_datetime', 'day')).order_by('start_datetime'),
|
||||
[(start_datetime, start_datetime.day), (end_datetime, end_datetime.day)],
|
||||
lambda m: (m.start_datetime, m.extracted)
|
||||
)
|
||||
self.assertQuerysetEqual(
|
||||
DTModel.objects.annotate(extracted=Extract('start_datetime', 'week_day')).order_by('start_datetime'),
|
||||
[
|
||||
(start_datetime, (start_datetime.isoweekday() % 7) + 1),
|
||||
(end_datetime, (end_datetime.isoweekday() % 7) + 1)
|
||||
],
|
||||
lambda m: (m.start_datetime, m.extracted)
|
||||
)
|
||||
self.assertQuerysetEqual(
|
||||
DTModel.objects.annotate(extracted=Extract('start_datetime', 'hour')).order_by('start_datetime'),
|
||||
[(start_datetime, start_datetime.hour), (end_datetime, end_datetime.hour)],
|
||||
lambda m: (m.start_datetime, m.extracted)
|
||||
)
|
||||
self.assertQuerysetEqual(
|
||||
DTModel.objects.annotate(extracted=Extract('start_datetime', 'minute')).order_by('start_datetime'),
|
||||
[(start_datetime, start_datetime.minute), (end_datetime, end_datetime.minute)],
|
||||
lambda m: (m.start_datetime, m.extracted)
|
||||
)
|
||||
self.assertQuerysetEqual(
|
||||
DTModel.objects.annotate(extracted=Extract('start_datetime', 'second')).order_by('start_datetime'),
|
||||
[(start_datetime, start_datetime.second), (end_datetime, end_datetime.second)],
|
||||
lambda m: (m.start_datetime, m.extracted)
|
||||
)
|
||||
self.assertEqual(DTModel.objects.filter(start_datetime__year=Extract('start_datetime', 'year')).count(), 2)
|
||||
self.assertEqual(DTModel.objects.filter(start_datetime__hour=Extract('start_datetime', 'hour')).count(), 2)
|
||||
self.assertEqual(DTModel.objects.filter(start_date__month=Extract('start_date', 'month')).count(), 2)
|
||||
self.assertEqual(DTModel.objects.filter(start_time__hour=Extract('start_time', 'hour')).count(), 2)
|
||||
|
||||
def test_extract_year_func(self):
|
||||
start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
|
||||
end_datetime = microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123))
|
||||
if settings.USE_TZ:
|
||||
start_datetime = timezone.make_aware(start_datetime, is_dst=False)
|
||||
end_datetime = timezone.make_aware(end_datetime, is_dst=False)
|
||||
self.create_model(start_datetime, end_datetime)
|
||||
self.create_model(end_datetime, start_datetime)
|
||||
self.assertQuerysetEqual(
|
||||
DTModel.objects.annotate(extracted=ExtractYear('start_datetime')).order_by('start_datetime'),
|
||||
[(start_datetime, start_datetime.year), (end_datetime, end_datetime.year)],
|
||||
lambda m: (m.start_datetime, m.extracted)
|
||||
)
|
||||
self.assertQuerysetEqual(
|
||||
DTModel.objects.annotate(extracted=ExtractYear('start_date')).order_by('start_datetime'),
|
||||
[(start_datetime, start_datetime.year), (end_datetime, end_datetime.year)],
|
||||
lambda m: (m.start_datetime, m.extracted)
|
||||
)
|
||||
self.assertEqual(DTModel.objects.filter(start_datetime__year=ExtractYear('start_datetime')).count(), 2)
|
||||
|
||||
def test_extract_month_func(self):
|
||||
start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
|
||||
end_datetime = microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123))
|
||||
if settings.USE_TZ:
|
||||
start_datetime = timezone.make_aware(start_datetime, is_dst=False)
|
||||
end_datetime = timezone.make_aware(end_datetime, is_dst=False)
|
||||
self.create_model(start_datetime, end_datetime)
|
||||
self.create_model(end_datetime, start_datetime)
|
||||
self.assertQuerysetEqual(
|
||||
DTModel.objects.annotate(extracted=ExtractMonth('start_datetime')).order_by('start_datetime'),
|
||||
[(start_datetime, start_datetime.month), (end_datetime, end_datetime.month)],
|
||||
lambda m: (m.start_datetime, m.extracted)
|
||||
)
|
||||
self.assertQuerysetEqual(
|
||||
DTModel.objects.annotate(extracted=ExtractMonth('start_date')).order_by('start_datetime'),
|
||||
[(start_datetime, start_datetime.month), (end_datetime, end_datetime.month)],
|
||||
lambda m: (m.start_datetime, m.extracted)
|
||||
)
|
||||
self.assertEqual(DTModel.objects.filter(start_datetime__month=ExtractMonth('start_datetime')).count(), 2)
|
||||
|
||||
def test_extract_day_func(self):
|
||||
start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
|
||||
end_datetime = microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123))
|
||||
if settings.USE_TZ:
|
||||
start_datetime = timezone.make_aware(start_datetime, is_dst=False)
|
||||
end_datetime = timezone.make_aware(end_datetime, is_dst=False)
|
||||
self.create_model(start_datetime, end_datetime)
|
||||
self.create_model(end_datetime, start_datetime)
|
||||
self.assertQuerysetEqual(
|
||||
DTModel.objects.annotate(extracted=ExtractDay('start_datetime')).order_by('start_datetime'),
|
||||
[(start_datetime, start_datetime.day), (end_datetime, end_datetime.day)],
|
||||
lambda m: (m.start_datetime, m.extracted)
|
||||
)
|
||||
self.assertQuerysetEqual(
|
||||
DTModel.objects.annotate(extracted=ExtractDay('start_date')).order_by('start_datetime'),
|
||||
[(start_datetime, start_datetime.day), (end_datetime, end_datetime.day)],
|
||||
lambda m: (m.start_datetime, m.extracted)
|
||||
)
|
||||
self.assertEqual(DTModel.objects.filter(start_datetime__day=ExtractDay('start_datetime')).count(), 2)
|
||||
|
||||
def test_extract_weekday_func(self):
|
||||
start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
|
||||
end_datetime = microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123))
|
||||
if settings.USE_TZ:
|
||||
start_datetime = timezone.make_aware(start_datetime, is_dst=False)
|
||||
end_datetime = timezone.make_aware(end_datetime, is_dst=False)
|
||||
self.create_model(start_datetime, end_datetime)
|
||||
self.create_model(end_datetime, start_datetime)
|
||||
self.assertQuerysetEqual(
|
||||
DTModel.objects.annotate(extracted=ExtractWeekDay('start_datetime')).order_by('start_datetime'),
|
||||
[
|
||||
(start_datetime, (start_datetime.isoweekday() % 7) + 1),
|
||||
(end_datetime, (end_datetime.isoweekday() % 7) + 1),
|
||||
],
|
||||
lambda m: (m.start_datetime, m.extracted)
|
||||
)
|
||||
self.assertQuerysetEqual(
|
||||
DTModel.objects.annotate(extracted=ExtractWeekDay('start_date')).order_by('start_datetime'),
|
||||
[
|
||||
(start_datetime, (start_datetime.isoweekday() % 7) + 1),
|
||||
(end_datetime, (end_datetime.isoweekday() % 7) + 1),
|
||||
],
|
||||
lambda m: (m.start_datetime, m.extracted)
|
||||
)
|
||||
self.assertEqual(DTModel.objects.filter(start_datetime__week_day=ExtractWeekDay('start_datetime')).count(), 2)
|
||||
|
||||
def test_extract_hour_func(self):
|
||||
start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
|
||||
end_datetime = microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123))
|
||||
if settings.USE_TZ:
|
||||
start_datetime = timezone.make_aware(start_datetime, is_dst=False)
|
||||
end_datetime = timezone.make_aware(end_datetime, is_dst=False)
|
||||
self.create_model(start_datetime, end_datetime)
|
||||
self.create_model(end_datetime, start_datetime)
|
||||
self.assertQuerysetEqual(
|
||||
DTModel.objects.annotate(extracted=ExtractHour('start_datetime')).order_by('start_datetime'),
|
||||
[(start_datetime, start_datetime.hour), (end_datetime, end_datetime.hour)],
|
||||
lambda m: (m.start_datetime, m.extracted)
|
||||
)
|
||||
self.assertQuerysetEqual(
|
||||
DTModel.objects.annotate(extracted=ExtractHour('start_time')).order_by('start_datetime'),
|
||||
[(start_datetime, start_datetime.hour), (end_datetime, end_datetime.hour)],
|
||||
lambda m: (m.start_datetime, m.extracted)
|
||||
)
|
||||
self.assertEqual(DTModel.objects.filter(start_datetime__hour=ExtractHour('start_datetime')).count(), 2)
|
||||
|
||||
def test_extract_minute_func(self):
|
||||
start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
|
||||
end_datetime = microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123))
|
||||
if settings.USE_TZ:
|
||||
start_datetime = timezone.make_aware(start_datetime, is_dst=False)
|
||||
end_datetime = timezone.make_aware(end_datetime, is_dst=False)
|
||||
self.create_model(start_datetime, end_datetime)
|
||||
self.create_model(end_datetime, start_datetime)
|
||||
self.assertQuerysetEqual(
|
||||
DTModel.objects.annotate(extracted=ExtractMinute('start_datetime')).order_by('start_datetime'),
|
||||
[(start_datetime, start_datetime.minute), (end_datetime, end_datetime.minute)],
|
||||
lambda m: (m.start_datetime, m.extracted)
|
||||
)
|
||||
self.assertQuerysetEqual(
|
||||
DTModel.objects.annotate(extracted=ExtractMinute('start_time')).order_by('start_datetime'),
|
||||
[(start_datetime, start_datetime.minute), (end_datetime, end_datetime.minute)],
|
||||
lambda m: (m.start_datetime, m.extracted)
|
||||
)
|
||||
self.assertEqual(DTModel.objects.filter(start_datetime__minute=ExtractMinute('start_datetime')).count(), 2)
|
||||
|
||||
def test_extract_second_func(self):
|
||||
start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
|
||||
end_datetime = microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123))
|
||||
if settings.USE_TZ:
|
||||
start_datetime = timezone.make_aware(start_datetime, is_dst=False)
|
||||
end_datetime = timezone.make_aware(end_datetime, is_dst=False)
|
||||
self.create_model(start_datetime, end_datetime)
|
||||
self.create_model(end_datetime, start_datetime)
|
||||
self.assertQuerysetEqual(
|
||||
DTModel.objects.annotate(extracted=ExtractSecond('start_datetime')).order_by('start_datetime'),
|
||||
[(start_datetime, start_datetime.second), (end_datetime, end_datetime.second)],
|
||||
lambda m: (m.start_datetime, m.extracted)
|
||||
)
|
||||
self.assertQuerysetEqual(
|
||||
DTModel.objects.annotate(extracted=ExtractSecond('start_time')).order_by('start_datetime'),
|
||||
[(start_datetime, start_datetime.second), (end_datetime, end_datetime.second)],
|
||||
lambda m: (m.start_datetime, m.extracted)
|
||||
)
|
||||
self.assertEqual(DTModel.objects.filter(start_datetime__second=ExtractSecond('start_datetime')).count(), 2)
|
||||
|
||||
def test_trunc_func(self):
|
||||
start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
|
||||
end_datetime = microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123))
|
||||
if settings.USE_TZ:
|
||||
start_datetime = timezone.make_aware(start_datetime, is_dst=False)
|
||||
end_datetime = timezone.make_aware(end_datetime, is_dst=False)
|
||||
self.create_model(start_datetime, end_datetime)
|
||||
self.create_model(end_datetime, start_datetime)
|
||||
|
||||
with self.assertRaisesMessage(ValueError, 'output_field must be either DateField or DateTimeField'):
|
||||
list(DTModel.objects.annotate(truncated=Trunc('start_datetime', 'year', output_field=IntegerField())))
|
||||
|
||||
with self.assertRaisesMessage(AssertionError, "'name' isn't a DateField or DateTimeField."):
|
||||
list(DTModel.objects.annotate(truncated=Trunc('name', 'year', output_field=DateTimeField())))
|
||||
|
||||
with self.assertRaisesMessage(ValueError, "Cannot truncate DateField 'start_date' to DateTimeField"):
|
||||
list(DTModel.objects.annotate(truncated=Trunc('start_date', 'second')))
|
||||
|
||||
with self.assertRaisesMessage(ValueError, "Cannot truncate DateField 'start_date' to DateTimeField"):
|
||||
list(DTModel.objects.annotate(truncated=Trunc('start_date', 'month', output_field=DateTimeField())))
|
||||
|
||||
def test_datetime_kind(kind):
|
||||
self.assertQuerysetEqual(
|
||||
DTModel.objects.annotate(
|
||||
truncated=Trunc('start_datetime', kind, output_field=DateTimeField())
|
||||
).order_by('start_datetime'),
|
||||
[
|
||||
(start_datetime, truncate_to(start_datetime, kind)),
|
||||
(end_datetime, truncate_to(end_datetime, kind))
|
||||
],
|
||||
lambda m: (m.start_datetime, m.truncated)
|
||||
)
|
||||
|
||||
def test_date_kind(kind):
|
||||
self.assertQuerysetEqual(
|
||||
DTModel.objects.annotate(
|
||||
truncated=Trunc('start_date', kind, output_field=DateField())
|
||||
).order_by('start_datetime'),
|
||||
[
|
||||
(start_datetime, truncate_to(start_datetime.date(), kind)),
|
||||
(end_datetime, truncate_to(end_datetime.date(), kind))
|
||||
],
|
||||
lambda m: (m.start_datetime, m.truncated)
|
||||
)
|
||||
|
||||
test_date_kind('year')
|
||||
test_date_kind('month')
|
||||
test_date_kind('day')
|
||||
test_datetime_kind('year')
|
||||
test_datetime_kind('month')
|
||||
test_datetime_kind('day')
|
||||
test_datetime_kind('hour')
|
||||
test_datetime_kind('minute')
|
||||
test_datetime_kind('second')
|
||||
|
||||
qs = DTModel.objects.filter(start_datetime__date=Trunc('start_datetime', 'day', output_field=DateField()))
|
||||
self.assertEqual(qs.count(), 2)
|
||||
|
||||
def test_trunc_year_func(self):
|
||||
start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
|
||||
end_datetime = truncate_to(microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123)), 'year')
|
||||
if settings.USE_TZ:
|
||||
start_datetime = timezone.make_aware(start_datetime, is_dst=False)
|
||||
end_datetime = timezone.make_aware(end_datetime, is_dst=False)
|
||||
self.create_model(start_datetime, end_datetime)
|
||||
self.create_model(end_datetime, start_datetime)
|
||||
self.assertQuerysetEqual(
|
||||
DTModel.objects.annotate(extracted=TruncYear('start_datetime')).order_by('start_datetime'),
|
||||
[
|
||||
(start_datetime, truncate_to(start_datetime, 'year')),
|
||||
(end_datetime, truncate_to(end_datetime, 'year')),
|
||||
],
|
||||
lambda m: (m.start_datetime, m.extracted)
|
||||
)
|
||||
self.assertQuerysetEqual(
|
||||
DTModel.objects.annotate(extracted=TruncYear('start_date')).order_by('start_datetime'),
|
||||
[
|
||||
(start_datetime, truncate_to(start_datetime.date(), 'year')),
|
||||
(end_datetime, truncate_to(end_datetime.date(), 'year')),
|
||||
],
|
||||
lambda m: (m.start_datetime, m.extracted)
|
||||
)
|
||||
self.assertEqual(DTModel.objects.filter(start_datetime=TruncYear('start_datetime')).count(), 1)
|
||||
|
||||
def test_trunc_month_func(self):
|
||||
start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
|
||||
end_datetime = truncate_to(microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123)), 'month')
|
||||
if settings.USE_TZ:
|
||||
start_datetime = timezone.make_aware(start_datetime, is_dst=False)
|
||||
end_datetime = timezone.make_aware(end_datetime, is_dst=False)
|
||||
self.create_model(start_datetime, end_datetime)
|
||||
self.create_model(end_datetime, start_datetime)
|
||||
self.assertQuerysetEqual(
|
||||
DTModel.objects.annotate(extracted=TruncMonth('start_datetime')).order_by('start_datetime'),
|
||||
[
|
||||
(start_datetime, truncate_to(start_datetime, 'month')),
|
||||
(end_datetime, truncate_to(end_datetime, 'month')),
|
||||
],
|
||||
lambda m: (m.start_datetime, m.extracted)
|
||||
)
|
||||
self.assertQuerysetEqual(
|
||||
DTModel.objects.annotate(extracted=TruncMonth('start_date')).order_by('start_datetime'),
|
||||
[
|
||||
(start_datetime, truncate_to(start_datetime.date(), 'month')),
|
||||
(end_datetime, truncate_to(end_datetime.date(), 'month')),
|
||||
],
|
||||
lambda m: (m.start_datetime, m.extracted)
|
||||
)
|
||||
self.assertEqual(DTModel.objects.filter(start_datetime=TruncMonth('start_datetime')).count(), 1)
|
||||
|
||||
def test_trunc_date_func(self):
|
||||
start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
|
||||
end_datetime = microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123))
|
||||
if settings.USE_TZ:
|
||||
start_datetime = timezone.make_aware(start_datetime, is_dst=False)
|
||||
end_datetime = timezone.make_aware(end_datetime, is_dst=False)
|
||||
self.create_model(start_datetime, end_datetime)
|
||||
self.create_model(end_datetime, start_datetime)
|
||||
self.assertQuerysetEqual(
|
||||
DTModel.objects.annotate(extracted=TruncDate('start_datetime')).order_by('start_datetime'),
|
||||
[
|
||||
(start_datetime, start_datetime.date()),
|
||||
(end_datetime, end_datetime.date()),
|
||||
],
|
||||
lambda m: (m.start_datetime, m.extracted)
|
||||
)
|
||||
self.assertEqual(DTModel.objects.filter(start_datetime__date=TruncDate('start_datetime')).count(), 2)
|
||||
|
||||
def test_trunc_day_func(self):
|
||||
start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
|
||||
end_datetime = truncate_to(microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123)), 'day')
|
||||
if settings.USE_TZ:
|
||||
start_datetime = timezone.make_aware(start_datetime, is_dst=False)
|
||||
end_datetime = timezone.make_aware(end_datetime, is_dst=False)
|
||||
self.create_model(start_datetime, end_datetime)
|
||||
self.create_model(end_datetime, start_datetime)
|
||||
self.assertQuerysetEqual(
|
||||
DTModel.objects.annotate(extracted=TruncDay('start_datetime')).order_by('start_datetime'),
|
||||
[
|
||||
(start_datetime, truncate_to(start_datetime, 'day')),
|
||||
(end_datetime, truncate_to(end_datetime, 'day')),
|
||||
],
|
||||
lambda m: (m.start_datetime, m.extracted)
|
||||
)
|
||||
self.assertEqual(DTModel.objects.filter(start_datetime=TruncDay('start_datetime')).count(), 1)
|
||||
|
||||
def test_trunc_hour_func(self):
|
||||
start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
|
||||
end_datetime = truncate_to(microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123)), 'hour')
|
||||
if settings.USE_TZ:
|
||||
start_datetime = timezone.make_aware(start_datetime, is_dst=False)
|
||||
end_datetime = timezone.make_aware(end_datetime, is_dst=False)
|
||||
self.create_model(start_datetime, end_datetime)
|
||||
self.create_model(end_datetime, start_datetime)
|
||||
self.assertQuerysetEqual(
|
||||
DTModel.objects.annotate(extracted=TruncHour('start_datetime')).order_by('start_datetime'),
|
||||
[
|
||||
(start_datetime, truncate_to(start_datetime, 'hour')),
|
||||
(end_datetime, truncate_to(end_datetime, 'hour')),
|
||||
],
|
||||
lambda m: (m.start_datetime, m.extracted)
|
||||
)
|
||||
self.assertEqual(DTModel.objects.filter(start_datetime=TruncHour('start_datetime')).count(), 1)
|
||||
|
||||
with self.assertRaisesMessage(ValueError, "Cannot truncate DateField 'start_date' to DateTimeField"):
|
||||
list(DTModel.objects.annotate(truncated=TruncHour('start_date')))
|
||||
|
||||
with self.assertRaisesMessage(ValueError, "Cannot truncate DateField 'start_date' to DateTimeField"):
|
||||
list(DTModel.objects.annotate(truncated=TruncHour('start_date', output_field=DateField())))
|
||||
|
||||
def test_trunc_minute_func(self):
|
||||
start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
|
||||
end_datetime = truncate_to(microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123)), 'minute')
|
||||
if settings.USE_TZ:
|
||||
start_datetime = timezone.make_aware(start_datetime, is_dst=False)
|
||||
end_datetime = timezone.make_aware(end_datetime, is_dst=False)
|
||||
self.create_model(start_datetime, end_datetime)
|
||||
self.create_model(end_datetime, start_datetime)
|
||||
self.assertQuerysetEqual(
|
||||
DTModel.objects.annotate(extracted=TruncMinute('start_datetime')).order_by('start_datetime'),
|
||||
[
|
||||
(start_datetime, truncate_to(start_datetime, 'minute')),
|
||||
(end_datetime, truncate_to(end_datetime, 'minute')),
|
||||
],
|
||||
lambda m: (m.start_datetime, m.extracted)
|
||||
)
|
||||
self.assertEqual(DTModel.objects.filter(start_datetime=TruncMinute('start_datetime')).count(), 1)
|
||||
|
||||
with self.assertRaisesMessage(ValueError, "Cannot truncate DateField 'start_date' to DateTimeField"):
|
||||
list(DTModel.objects.annotate(truncated=TruncMinute('start_date')))
|
||||
|
||||
with self.assertRaisesMessage(ValueError, "Cannot truncate DateField 'start_date' to DateTimeField"):
|
||||
list(DTModel.objects.annotate(truncated=TruncMinute('start_date', output_field=DateField())))
|
||||
|
||||
def test_trunc_second_func(self):
|
||||
start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
|
||||
end_datetime = truncate_to(microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123)), 'second')
|
||||
if settings.USE_TZ:
|
||||
start_datetime = timezone.make_aware(start_datetime, is_dst=False)
|
||||
end_datetime = timezone.make_aware(end_datetime, is_dst=False)
|
||||
self.create_model(start_datetime, end_datetime)
|
||||
self.create_model(end_datetime, start_datetime)
|
||||
self.assertQuerysetEqual(
|
||||
DTModel.objects.annotate(extracted=TruncSecond('start_datetime')).order_by('start_datetime'),
|
||||
[
|
||||
(start_datetime, truncate_to(start_datetime, 'second')),
|
||||
(end_datetime, truncate_to(end_datetime, 'second'))
|
||||
],
|
||||
lambda m: (m.start_datetime, m.extracted)
|
||||
)
|
||||
|
||||
result = 1 if connection.features.supports_microsecond_precision else 2
|
||||
self.assertEqual(DTModel.objects.filter(start_datetime=TruncSecond('start_datetime')).count(), result)
|
||||
|
||||
with self.assertRaisesMessage(ValueError, "Cannot truncate DateField 'start_date' to DateTimeField"):
|
||||
list(DTModel.objects.annotate(truncated=TruncSecond('start_date')))
|
||||
|
||||
with self.assertRaisesMessage(ValueError, "Cannot truncate DateField 'start_date' to DateTimeField"):
|
||||
list(DTModel.objects.annotate(truncated=TruncSecond('start_date', output_field=DateField())))
|
||||
|
||||
|
||||
@skipIf(pytz is None, "this test requires pytz")
|
||||
@override_settings(USE_TZ=True, TIME_ZONE='UTC')
|
||||
class DateFunctionWithTimeZoneTests(DateFunctionTests):
|
||||
|
||||
def test_extract_func_with_timezone(self):
|
||||
start_datetime = microsecond_support(datetime(2015, 6, 15, 23, 30, 1, 321))
|
||||
end_datetime = microsecond_support(datetime(2015, 6, 16, 13, 11, 27, 123))
|
||||
start_datetime = timezone.make_aware(start_datetime, is_dst=False)
|
||||
end_datetime = timezone.make_aware(end_datetime, is_dst=False)
|
||||
self.create_model(start_datetime, end_datetime)
|
||||
melb = pytz.timezone('Australia/Melbourne')
|
||||
|
||||
qs = DTModel.objects.annotate(
|
||||
day=Extract('start_datetime', 'day'),
|
||||
day_melb=Extract('start_datetime', 'day', tzinfo=melb),
|
||||
weekday=ExtractWeekDay('start_datetime'),
|
||||
weekday_melb=ExtractWeekDay('start_datetime', tzinfo=melb),
|
||||
hour=ExtractHour('start_datetime'),
|
||||
hour_melb=ExtractHour('start_datetime', tzinfo=melb),
|
||||
).order_by('start_datetime')
|
||||
|
||||
utc_model = qs.get()
|
||||
self.assertEqual(utc_model.day, 15)
|
||||
self.assertEqual(utc_model.day_melb, 16)
|
||||
self.assertEqual(utc_model.weekday, 2)
|
||||
self.assertEqual(utc_model.weekday_melb, 3)
|
||||
self.assertEqual(utc_model.hour, 23)
|
||||
self.assertEqual(utc_model.hour_melb, 9)
|
||||
|
||||
with timezone.override(melb):
|
||||
melb_model = qs.get()
|
||||
|
||||
self.assertEqual(melb_model.day, 16)
|
||||
self.assertEqual(melb_model.day_melb, 16)
|
||||
self.assertEqual(melb_model.weekday, 3)
|
||||
self.assertEqual(melb_model.weekday_melb, 3)
|
||||
self.assertEqual(melb_model.hour, 9)
|
||||
self.assertEqual(melb_model.hour_melb, 9)
|
||||
|
||||
def test_extract_func_explicit_timezone_priority(self):
|
||||
start_datetime = microsecond_support(datetime(2015, 6, 15, 23, 30, 1, 321))
|
||||
end_datetime = microsecond_support(datetime(2015, 6, 16, 13, 11, 27, 123))
|
||||
start_datetime = timezone.make_aware(start_datetime, is_dst=False)
|
||||
end_datetime = timezone.make_aware(end_datetime, is_dst=False)
|
||||
self.create_model(start_datetime, end_datetime)
|
||||
melb = pytz.timezone('Australia/Melbourne')
|
||||
|
||||
with timezone.override(melb):
|
||||
model = DTModel.objects.annotate(
|
||||
day_melb=Extract('start_datetime', 'day'),
|
||||
day_utc=Extract('start_datetime', 'day', tzinfo=timezone.utc),
|
||||
).order_by('start_datetime').get()
|
||||
self.assertEqual(model.day_melb, 16)
|
||||
self.assertEqual(model.day_utc, 15)
|
||||
|
||||
def test_trunc_timezone_applied_before_truncation(self):
|
||||
start_datetime = microsecond_support(datetime(2016, 1, 1, 1, 30, 50, 321))
|
||||
end_datetime = microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123))
|
||||
start_datetime = timezone.make_aware(start_datetime, is_dst=False)
|
||||
end_datetime = timezone.make_aware(end_datetime, is_dst=False)
|
||||
self.create_model(start_datetime, end_datetime)
|
||||
|
||||
melb = pytz.timezone('Australia/Melbourne')
|
||||
pacific = pytz.timezone('US/Pacific')
|
||||
|
||||
model = DTModel.objects.annotate(
|
||||
melb_year=TruncYear('start_datetime', tzinfo=melb),
|
||||
pacific_year=TruncYear('start_datetime', tzinfo=pacific),
|
||||
).order_by('start_datetime').get()
|
||||
|
||||
self.assertEqual(model.start_datetime, start_datetime)
|
||||
self.assertEqual(model.melb_year, truncate_to(start_datetime, 'year', melb))
|
||||
self.assertEqual(model.pacific_year, truncate_to(start_datetime, 'year', pacific))
|
||||
self.assertEqual(model.start_datetime.year, 2016)
|
||||
self.assertEqual(model.melb_year.year, 2016)
|
||||
self.assertEqual(model.pacific_year.year, 2015)
|
||||
|
||||
def test_trunc_func_with_timezone(self):
|
||||
"""
|
||||
If the truncated datetime transitions to a different offset (daylight
|
||||
saving) then the returned value will have that new timezone/offset.
|
||||
"""
|
||||
start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
|
||||
end_datetime = microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123))
|
||||
start_datetime = timezone.make_aware(start_datetime, is_dst=False)
|
||||
end_datetime = timezone.make_aware(end_datetime, is_dst=False)
|
||||
self.create_model(start_datetime, end_datetime)
|
||||
self.create_model(end_datetime, start_datetime)
|
||||
|
||||
melb = pytz.timezone('Australia/Melbourne')
|
||||
|
||||
def test_datetime_kind(kind, tzinfo=melb):
|
||||
self.assertQuerysetEqual(
|
||||
DTModel.objects.annotate(
|
||||
truncated=Trunc('start_datetime', kind, output_field=DateTimeField(), tzinfo=melb)
|
||||
).order_by('start_datetime'),
|
||||
[
|
||||
(start_datetime, truncate_to(start_datetime.astimezone(melb), kind, melb)),
|
||||
(end_datetime, truncate_to(end_datetime.astimezone(melb), kind, melb))
|
||||
],
|
||||
lambda m: (m.start_datetime, m.truncated)
|
||||
)
|
||||
|
||||
def test_date_kind(kind, tzinfo=melb):
|
||||
self.assertQuerysetEqual(
|
||||
DTModel.objects.annotate(
|
||||
truncated=Trunc('start_date', kind, output_field=DateField(), tzinfo=melb)
|
||||
).order_by('start_datetime'),
|
||||
[
|
||||
(start_datetime, truncate_to(start_datetime.date(), kind)),
|
||||
(end_datetime, truncate_to(end_datetime.date(), kind))
|
||||
],
|
||||
lambda m: (m.start_datetime, m.truncated)
|
||||
)
|
||||
|
||||
test_date_kind('year')
|
||||
test_date_kind('month')
|
||||
test_date_kind('day')
|
||||
test_datetime_kind('year')
|
||||
test_datetime_kind('month')
|
||||
test_datetime_kind('day')
|
||||
test_datetime_kind('hour')
|
||||
test_datetime_kind('minute')
|
||||
test_datetime_kind('second')
|
||||
|
||||
qs = DTModel.objects.filter(start_datetime__date=Trunc('start_datetime', 'day', output_field=DateField()))
|
||||
self.assertEqual(qs.count(), 2)
|
|
@ -11,8 +11,8 @@ from django.db.models.aggregates import (
|
|||
Avg, Count, Max, Min, StdDev, Sum, Variance,
|
||||
)
|
||||
from django.db.models.expressions import (
|
||||
Case, Col, Date, DateTime, ExpressionWrapper, F, Func, OrderBy, Random,
|
||||
RawSQL, Ref, Value, When,
|
||||
Case, Col, ExpressionWrapper, F, Func, OrderBy, Random, RawSQL, Ref, Value,
|
||||
When,
|
||||
)
|
||||
from django.db.models.functions import (
|
||||
Coalesce, Concat, Length, Lower, Substr, Upper,
|
||||
|
@ -20,7 +20,6 @@ from django.db.models.functions import (
|
|||
from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature
|
||||
from django.test.utils import Approximate
|
||||
from django.utils import six
|
||||
from django.utils.timezone import utc
|
||||
|
||||
from .models import UUID, Company, Employee, Experiment, Number, Time
|
||||
|
||||
|
@ -930,8 +929,6 @@ class ReprTests(TestCase):
|
|||
"<Case: CASE WHEN <Q: (AND: ('a', 1))> THEN Value(None), ELSE Value(None)>"
|
||||
)
|
||||
self.assertEqual(repr(Col('alias', 'field')), "Col(alias, field)")
|
||||
self.assertEqual(repr(Date('published', 'exact')), "Date(published, exact)")
|
||||
self.assertEqual(repr(DateTime('published', 'exact', utc)), "DateTime(published, exact, %s)" % utc)
|
||||
self.assertEqual(repr(F('published')), "F(published)")
|
||||
self.assertEqual(repr(F('cost') + F('tax')), "<CombinedExpression: F(cost) + F(tax)>")
|
||||
self.assertEqual(
|
||||
|
|
|
@ -1312,7 +1312,7 @@ class Queries3Tests(BaseQuerysetTest):
|
|||
def test_ticket8683(self):
|
||||
# An error should be raised when QuerySet.datetimes() is passed the
|
||||
# wrong type of field.
|
||||
with self.assertRaisesMessage(AssertionError, "'name' isn't a DateTimeField."):
|
||||
with self.assertRaisesMessage(AssertionError, "'name' isn't a DateField or DateTimeField."):
|
||||
Item.objects.datetimes('name', 'month')
|
||||
|
||||
def test_ticket22023(self):
|
||||
|
|
Loading…
Reference in New Issue