diff --git a/django/contrib/postgres/functions.py b/django/contrib/postgres/functions.py index 3184c492fc..fc0dae8ece 100644 --- a/django/contrib/postgres/functions.py +++ b/django/contrib/postgres/functions.py @@ -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): diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py index df82d2ec58..f79865d153 100644 --- a/django/db/models/expressions.py +++ b/django/db/models/expressions.py @@ -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' diff --git a/django/db/models/functions/__init__.py b/django/db/models/functions/__init__.py new file mode 100644 index 0000000000..01e1c5f6d7 --- /dev/null +++ b/django/db/models/functions/__init__.py @@ -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', +] diff --git a/django/db/models/functions.py b/django/db/models/functions/base.py similarity index 100% rename from django/db/models/functions.py rename to django/db/models/functions/base.py diff --git a/django/db/models/functions/datetime.py b/django/db/models/functions/datetime.py new file mode 100644 index 0000000000..e6815fbb03 --- /dev/null +++ b/django/db/models/functions/datetime.py @@ -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) diff --git a/django/db/models/lookups.py b/django/db/models/lookups.py index 9f398b2b1a..cc02e2f6ff 100644 --- a/django/db/models/lookups.py +++ b/django/db/models/lookups.py @@ -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) diff --git a/django/db/models/query.py b/django/db/models/query.py index dce643869e..8bd981db51 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -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 diff --git a/docs/ref/models/database-functions.txt b/docs/ref/models/database-functions.txt index cd2d1ee368..f6db325540 100644 --- a/docs/ref/models/database-functions.txt +++ b/docs/ref/models/database-functions.txt @@ -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=), + 'hour': datetime.datetime(2014, 6, 16, 0, 0, tzinfo=), + 'minute': 'minute': datetime.datetime(2014, 6, 15, 14, 30, tzinfo=), + 'second': datetime.datetime(2014, 6, 15, 14, 30, 50, tzinfo=) + } diff --git a/docs/releases/1.10.txt b/docs/releases/1.10.txt index a73790b9b8..14b23def72 100644 --- a/docs/releases/1.10.txt +++ b/docs/releases/1.10.txt @@ -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 diff --git a/tests/datetimes/models.py b/tests/datetimes/models.py index 47ce417e4c..2fcb72be09 100644 --- a/tests/datetimes/models.py +++ b/tests/datetimes/models.py @@ -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") diff --git a/tests/datetimes/tests.py b/tests/datetimes/tests.py index 447cbef99a..5bd3bc6d5f 100644 --- a/tests/datetimes/tests.py +++ b/tests/datetimes/tests.py @@ -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')) diff --git a/tests/db_functions/models.py b/tests/db_functions/models.py index 788fe01b9f..7494680c23 100644 --- a/tests/db_functions/models.py +++ b/tests/db_functions/models.py @@ -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) diff --git a/tests/db_functions/test_datetime.py b/tests/db_functions/test_datetime.py new file mode 100644 index 0000000000..011db8bb88 --- /dev/null +++ b/tests/db_functions/test_datetime.py @@ -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) diff --git a/tests/expressions/tests.py b/tests/expressions/tests.py index 478754b6fa..27929c9146 100644 --- a/tests/expressions/tests.py +++ b/tests/expressions/tests.py @@ -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): " 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')), "") self.assertEqual( diff --git a/tests/queries/tests.py b/tests/queries/tests.py index 6af66ba7f9..19735295ce 100644 --- a/tests/queries/tests.py +++ b/tests/queries/tests.py @@ -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):