Commit b5e0eede authored by Jon Dufresne's avatar Jon Dufresne Committed by Tim Graham
Browse files

Fixed #22394 -- Refactored built-in datetime lookups to transforms.

parent 039d7881
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -347,6 +347,7 @@ answer newbie questions, and generally made Django that much better:
    John Paulett <john@paulett.org>
    John Shaffer <jshaffer2112@gmail.com>
    Jökull Sólberg Auðunsson <jokullsolberg@gmail.com>
    Jon Dufresne <jon.dufresne@gmail.com>
    Jonathan Buchanan <jonathan.buchanan@gmail.com>
    Jonathan Daugherty (cygnus) <http://www.cprogrammer.org/>
    Jonathan Feignberg <jdf@pobox.com>
+142 −24
Original line number Diff line number Diff line
@@ -12,7 +12,7 @@ from base64 import b64decode, b64encode

from django.apps import apps
from django.db import connection
from django.db.models.lookups import default_lookups, RegisterLookupMixin
from django.db.models.lookups import default_lookups, RegisterLookupMixin, Transform, Lookup
from django.db.models.query_utils import QueryWrapper
from django.conf import settings
from django import forms
@@ -724,7 +724,6 @@ class Field(RegisterLookupMixin):
        if lookup_type in {
            'iexact', 'contains', 'icontains',
            'startswith', 'istartswith', 'endswith', 'iendswith',
            'month', 'day', 'week_day', 'hour', 'minute', 'second',
            'isnull', 'search', 'regex', 'iregex',
        }:
            return value
@@ -732,12 +731,6 @@ class Field(RegisterLookupMixin):
            return self.get_prep_value(value)
        elif lookup_type in ('range', 'in'):
            return [self.get_prep_value(v) for v in value]
        elif lookup_type == 'year':
            try:
                return int(value)
            except ValueError:
                raise ValueError("The __year lookup type requires an integer "
                                 "argument")
        return self.get_prep_value(value)

    def get_db_prep_lookup(self, lookup_type, value, connection,
@@ -761,8 +754,7 @@ class Field(RegisterLookupMixin):
                sql, params = value._as_sql(connection=connection)
            return QueryWrapper(('(%s)' % sql), params)

        if lookup_type in ('month', 'day', 'week_day', 'hour', 'minute',
                           'second', 'search', 'regex', 'iregex', 'contains',
        if lookup_type in ('search', 'regex', 'iregex', 'contains',
                           'icontains', 'iexact', 'startswith', 'endswith',
                           'istartswith', 'iendswith'):
            return [value]
@@ -774,13 +766,6 @@ class Field(RegisterLookupMixin):
                                           prepared=prepared) for v in value]
        elif lookup_type == 'isnull':
            return []
        elif lookup_type == 'year':
            if isinstance(self, DateTimeField):
                return connection.ops.year_lookup_bounds_for_datetime_field(value)
            elif isinstance(self, DateField):
                return connection.ops.year_lookup_bounds_for_date_field(value)
            else:
                return [value]          # this isn't supposed to happen
        else:
            return [value]

@@ -1302,13 +1287,6 @@ class DateField(DateTimeCheckMixin, Field):
                curry(cls._get_next_or_previous_by_FIELD, field=self,
                      is_next=False))

    def get_prep_lookup(self, lookup_type, value):
        # For dates lookups, convert the value to an int
        # so the database backend always sees a consistent type.
        if lookup_type in ('month', 'day', 'week_day', 'hour', 'minute', 'second'):
            return int(value)
        return super(DateField, self).get_prep_lookup(lookup_type, value)

    def get_prep_value(self, value):
        value = super(DateField, self).get_prep_value(value)
        return self.to_python(value)
@@ -2408,3 +2386,143 @@ class UUIDField(Field):
        }
        defaults.update(kwargs)
        return super(UUIDField, self).formfield(**defaults)


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)
        else:
            # DateField and TimeField.
            sql = connection.ops.date_extract_sql(self.lookup_name, sql)
        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
        if isinstance(output_field, DateTimeField):
            bounds = connection.ops.year_lookup_bounds_for_datetime_field(year)
        else:
            bounds = connection.ops.year_lookup_bounds_for_date_field(year)
        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
        # 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)
        rhs_sql = self.get_rhs_op(connection, rhs_sql)
        start, finish = self.year_lookup_bounds(connection, rhs_params[0])
        params.append(self.get_bound(start, finish))
        return '%s %s' % (lhs_sql, rhs_sql), params

    def get_rhs_op(self, connection, rhs):
        return connection.operators[self.lookup_name] % rhs

    def get_bound(self):
        raise NotImplementedError(
            'subclasses of YearComparisonLookup must provide a get_bound() method'
        )


@YearTransform.register_lookup
class YearGt(YearComparisonLookup):
    lookup_name = 'gt'

    def get_bound(self, start, finish):
        return finish


@YearTransform.register_lookup
class YearGte(YearComparisonLookup):
    lookup_name = 'gte'

    def get_bound(self, start, finish):
        return start


@YearTransform.register_lookup
class YearLt(YearComparisonLookup):
    lookup_name = 'lt'

    def get_bound(self, start, finish):
        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(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)
+0 −58
Original line number Diff line number Diff line
import inspect
from copy import copy

from django.conf import settings
from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.six.moves import range

@@ -408,11 +406,6 @@ class Between(BuiltinLookup):
        return "BETWEEN %s AND %s" % (rhs, rhs)


class Year(Between):
    lookup_name = 'year'
default_lookups['year'] = Year


class Range(BuiltinLookup):
    lookup_name = 'range'

@@ -430,57 +423,6 @@ class Range(BuiltinLookup):
default_lookups['range'] = Range


class DateLookup(BuiltinLookup):
    def process_lhs(self, compiler, connection, lhs=None):
        from django.db.models import DateTimeField
        lhs, params = super(DateLookup, self).process_lhs(compiler, connection, lhs)
        if isinstance(self.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.extract_type, lhs, tzname)
            return connection.ops.lookup_cast(self.lookup_name) % sql, tz_params
        else:
            return connection.ops.date_extract_sql(self.lookup_name, lhs), []

    def get_rhs_op(self, connection, rhs):
        return '= %s' % rhs


class Month(DateLookup):
    lookup_name = 'month'
    extract_type = 'month'
default_lookups['month'] = Month


class Day(DateLookup):
    lookup_name = 'day'
    extract_type = 'day'
default_lookups['day'] = Day


class WeekDay(DateLookup):
    lookup_name = 'week_day'
    extract_type = 'week_day'
default_lookups['week_day'] = WeekDay


class Hour(DateLookup):
    lookup_name = 'hour'
    extract_type = 'hour'
default_lookups['hour'] = Hour


class Minute(DateLookup):
    lookup_name = 'minute'
    extract_type = 'minute'
default_lookups['minute'] = Minute


class Second(DateLookup):
    lookup_name = 'second'
    extract_type = 'second'
default_lookups['second'] = Second


class IsNull(BuiltinLookup):
    lookup_name = 'isnull'

+55 −8
Original line number Diff line number Diff line
@@ -2431,36 +2431,45 @@ numbers and even characters.
year
~~~~

For date and datetime fields, an exact year match. Takes an integer year.
For date and datetime fields, an exact year match. Allows chaining additional
field lookups. Takes an integer year.

Example::

    Entry.objects.filter(pub_date__year=2005)
    Entry.objects.filter(pub_date__year__gte=2005)

SQL equivalent::

    SELECT ... WHERE pub_date BETWEEN '2005-01-01' AND '2005-12-31';
    SELECT ... WHERE pub_date >= '2005-01-01';

(The exact SQL syntax varies for each database engine.)

When :setting:`USE_TZ` is ``True``, datetime fields are converted to the
current time zone before filtering.

.. versionchanged:: 1.9

    Allowed chaining additional field lookups.

.. fieldlookup:: month

month
~~~~~

For date and datetime fields, an exact month match. Takes an integer 1
(January) through 12 (December).
For date and datetime fields, an exact month match. Allows chaining additional
field lookups. Takes an integer 1 (January) through 12 (December).

Example::

    Entry.objects.filter(pub_date__month=12)
    Entry.objects.filter(pub_date__month__gte=6)

SQL equivalent::

    SELECT ... WHERE EXTRACT('month' FROM pub_date) = '12';
    SELECT ... WHERE EXTRACT('month' FROM pub_date) >= '6';

(The exact SQL syntax varies for each database engine.)

@@ -2468,20 +2477,27 @@ When :setting:`USE_TZ` is ``True``, datetime fields are converted to the
current time zone before filtering. This requires :ref:`time zone definitions
in the database <database-time-zone-definitions>`.

.. versionchanged:: 1.9

    Allowed chaining additional field lookups.

.. fieldlookup:: day

day
~~~

For date and datetime fields, an exact day match. Takes an integer day.
For date and datetime fields, an exact day match. Allows chaining additional
field lookups. Takes an integer day.

Example::

    Entry.objects.filter(pub_date__day=3)
    Entry.objects.filter(pub_date__day__gte=3)

SQL equivalent::

    SELECT ... WHERE EXTRACT('day' FROM pub_date) = '3';
    SELECT ... WHERE EXTRACT('day' FROM pub_date) >= '3';

(The exact SQL syntax varies for each database engine.)

@@ -2492,12 +2508,17 @@ When :setting:`USE_TZ` is ``True``, datetime fields are converted to the
current time zone before filtering. This requires :ref:`time zone definitions
in the database <database-time-zone-definitions>`.

.. versionchanged:: 1.9

    Allowed chaining additional field lookups.

.. fieldlookup:: week_day

week_day
~~~~~~~~

For date and datetime fields, a 'day of the week' match.
For date and datetime fields, a 'day of the week' match. Allows chaining
additional field lookups.

Takes an integer value representing the day of week from 1 (Sunday) to 7
(Saturday).
@@ -2505,6 +2526,7 @@ Takes an integer value representing the day of week from 1 (Sunday) to 7
Example::

    Entry.objects.filter(pub_date__week_day=2)
    Entry.objects.filter(pub_date__week_day__gte=2)

(No equivalent SQL code fragment is included for this lookup because
implementation of the relevant query varies among different database engines.)
@@ -2517,66 +2539,91 @@ When :setting:`USE_TZ` is ``True``, datetime fields are converted to the
current time zone before filtering. This requires :ref:`time zone definitions
in the database <database-time-zone-definitions>`.

.. versionchanged:: 1.9

    Allowed chaining additional field lookups.

.. fieldlookup:: hour

hour
~~~~

For datetime fields, an exact hour match. Takes an integer between 0 and 23.
For datetime fields, an exact hour match. Allows chaining additional field
lookups. Takes an integer between 0 and 23.

Example::

    Event.objects.filter(timestamp__hour=23)
    Event.objects.filter(timestamp__hour__gte=12)

SQL equivalent::

    SELECT ... WHERE EXTRACT('hour' FROM timestamp) = '23';
    SELECT ... WHERE EXTRACT('hour' FROM timestamp) >= '12';

(The exact SQL syntax varies for each database engine.)

When :setting:`USE_TZ` is ``True``, values are converted to the current time
zone before filtering.

.. versionchanged:: 1.9

    Allowed chaining additional field lookups.

.. fieldlookup:: minute

minute
~~~~~~

For datetime fields, an exact minute match. Takes an integer between 0 and 59.
For datetime fields, an exact minute match. Allows chaining additional field
lookups. Takes an integer between 0 and 59.

Example::

    Event.objects.filter(timestamp__minute=29)
    Event.objects.filter(timestamp__minute__gte=29)

SQL equivalent::

    SELECT ... WHERE EXTRACT('minute' FROM timestamp) = '29';
    SELECT ... WHERE EXTRACT('minute' FROM timestamp) >= '29';

(The exact SQL syntax varies for each database engine.)

When :setting:`USE_TZ` is ``True``, values are converted to the current time
zone before filtering.

.. versionchanged:: 1.9

    Allowed chaining additional field lookups.

.. fieldlookup:: second

second
~~~~~~

For datetime fields, an exact second match. Takes an integer between 0 and 59.
For datetime fields, an exact second match. Allows chaining additional field
lookups. Takes an integer between 0 and 59.

Example::

    Event.objects.filter(timestamp__second=31)
    Event.objects.filter(timestamp__second__gte=31)

SQL equivalent::

    SELECT ... WHERE EXTRACT('second' FROM timestamp) = '31';
    SELECT ... WHERE EXTRACT('second' FROM timestamp) >= '31';

(The exact SQL syntax varies for each database engine.)

When :setting:`USE_TZ` is ``True``, values are converted to the current time
zone before filtering.

.. versionchanged:: 1.9

    Allowed chaining additional field lookups.

.. fieldlookup:: isnull

isnull
+4 −0
Original line number Diff line number Diff line
@@ -184,6 +184,10 @@ Models
* Added a system check to prevent defining both ``Meta.ordering`` and
  ``order_with_respect_to`` on the same model.

* :lookup:`Date and time <year>` lookups can be chained with other lookups
  (such as :lookup:`exact`, :lookup:`gt`, :lookup:`lt`, etc.). For example:
  ``Entry.objects.filter(pub_date__month__gt=6)``.

CSRF
^^^^

Loading