Fixed #22394 -- Refactored built-in datetime lookups to transforms.
This commit is contained in:
parent
039d7881b4
commit
b5e0eede40
1
AUTHORS
1
AUTHORS
|
@ -347,6 +347,7 @@ answer newbie questions, and generally made Django that much better:
|
||||||
John Paulett <john@paulett.org>
|
John Paulett <john@paulett.org>
|
||||||
John Shaffer <jshaffer2112@gmail.com>
|
John Shaffer <jshaffer2112@gmail.com>
|
||||||
Jökull Sólberg Auðunsson <jokullsolberg@gmail.com>
|
Jökull Sólberg Auðunsson <jokullsolberg@gmail.com>
|
||||||
|
Jon Dufresne <jon.dufresne@gmail.com>
|
||||||
Jonathan Buchanan <jonathan.buchanan@gmail.com>
|
Jonathan Buchanan <jonathan.buchanan@gmail.com>
|
||||||
Jonathan Daugherty (cygnus) <http://www.cprogrammer.org/>
|
Jonathan Daugherty (cygnus) <http://www.cprogrammer.org/>
|
||||||
Jonathan Feignberg <jdf@pobox.com>
|
Jonathan Feignberg <jdf@pobox.com>
|
||||||
|
|
|
@ -12,7 +12,7 @@ from base64 import b64decode, b64encode
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.db import connection
|
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.db.models.query_utils import QueryWrapper
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django import forms
|
from django import forms
|
||||||
|
@ -724,7 +724,6 @@ class Field(RegisterLookupMixin):
|
||||||
if lookup_type in {
|
if lookup_type in {
|
||||||
'iexact', 'contains', 'icontains',
|
'iexact', 'contains', 'icontains',
|
||||||
'startswith', 'istartswith', 'endswith', 'iendswith',
|
'startswith', 'istartswith', 'endswith', 'iendswith',
|
||||||
'month', 'day', 'week_day', 'hour', 'minute', 'second',
|
|
||||||
'isnull', 'search', 'regex', 'iregex',
|
'isnull', 'search', 'regex', 'iregex',
|
||||||
}:
|
}:
|
||||||
return value
|
return value
|
||||||
|
@ -732,12 +731,6 @@ class Field(RegisterLookupMixin):
|
||||||
return self.get_prep_value(value)
|
return self.get_prep_value(value)
|
||||||
elif lookup_type in ('range', 'in'):
|
elif lookup_type in ('range', 'in'):
|
||||||
return [self.get_prep_value(v) for v in value]
|
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)
|
return self.get_prep_value(value)
|
||||||
|
|
||||||
def get_db_prep_lookup(self, lookup_type, value, connection,
|
def get_db_prep_lookup(self, lookup_type, value, connection,
|
||||||
|
@ -761,8 +754,7 @@ class Field(RegisterLookupMixin):
|
||||||
sql, params = value._as_sql(connection=connection)
|
sql, params = value._as_sql(connection=connection)
|
||||||
return QueryWrapper(('(%s)' % sql), params)
|
return QueryWrapper(('(%s)' % sql), params)
|
||||||
|
|
||||||
if lookup_type in ('month', 'day', 'week_day', 'hour', 'minute',
|
if lookup_type in ('search', 'regex', 'iregex', 'contains',
|
||||||
'second', 'search', 'regex', 'iregex', 'contains',
|
|
||||||
'icontains', 'iexact', 'startswith', 'endswith',
|
'icontains', 'iexact', 'startswith', 'endswith',
|
||||||
'istartswith', 'iendswith'):
|
'istartswith', 'iendswith'):
|
||||||
return [value]
|
return [value]
|
||||||
|
@ -774,13 +766,6 @@ class Field(RegisterLookupMixin):
|
||||||
prepared=prepared) for v in value]
|
prepared=prepared) for v in value]
|
||||||
elif lookup_type == 'isnull':
|
elif lookup_type == 'isnull':
|
||||||
return []
|
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:
|
else:
|
||||||
return [value]
|
return [value]
|
||||||
|
|
||||||
|
@ -1302,13 +1287,6 @@ class DateField(DateTimeCheckMixin, Field):
|
||||||
curry(cls._get_next_or_previous_by_FIELD, field=self,
|
curry(cls._get_next_or_previous_by_FIELD, field=self,
|
||||||
is_next=False))
|
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):
|
def get_prep_value(self, value):
|
||||||
value = super(DateField, self).get_prep_value(value)
|
value = super(DateField, self).get_prep_value(value)
|
||||||
return self.to_python(value)
|
return self.to_python(value)
|
||||||
|
@ -2408,3 +2386,143 @@ class UUIDField(Field):
|
||||||
}
|
}
|
||||||
defaults.update(kwargs)
|
defaults.update(kwargs)
|
||||||
return super(UUIDField, self).formfield(**defaults)
|
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)
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import inspect
|
import inspect
|
||||||
from copy import copy
|
from copy import copy
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.utils import timezone
|
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.six.moves import range
|
from django.utils.six.moves import range
|
||||||
|
|
||||||
|
@ -408,11 +406,6 @@ class Between(BuiltinLookup):
|
||||||
return "BETWEEN %s AND %s" % (rhs, rhs)
|
return "BETWEEN %s AND %s" % (rhs, rhs)
|
||||||
|
|
||||||
|
|
||||||
class Year(Between):
|
|
||||||
lookup_name = 'year'
|
|
||||||
default_lookups['year'] = Year
|
|
||||||
|
|
||||||
|
|
||||||
class Range(BuiltinLookup):
|
class Range(BuiltinLookup):
|
||||||
lookup_name = 'range'
|
lookup_name = 'range'
|
||||||
|
|
||||||
|
@ -430,57 +423,6 @@ class Range(BuiltinLookup):
|
||||||
default_lookups['range'] = Range
|
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):
|
class IsNull(BuiltinLookup):
|
||||||
lookup_name = 'isnull'
|
lookup_name = 'isnull'
|
||||||
|
|
||||||
|
|
|
@ -2431,36 +2431,45 @@ numbers and even characters.
|
||||||
year
|
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::
|
Example::
|
||||||
|
|
||||||
Entry.objects.filter(pub_date__year=2005)
|
Entry.objects.filter(pub_date__year=2005)
|
||||||
|
Entry.objects.filter(pub_date__year__gte=2005)
|
||||||
|
|
||||||
SQL equivalent::
|
SQL equivalent::
|
||||||
|
|
||||||
SELECT ... WHERE pub_date BETWEEN '2005-01-01' AND '2005-12-31';
|
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.)
|
(The exact SQL syntax varies for each database engine.)
|
||||||
|
|
||||||
When :setting:`USE_TZ` is ``True``, datetime fields are converted to the
|
When :setting:`USE_TZ` is ``True``, datetime fields are converted to the
|
||||||
current time zone before filtering.
|
current time zone before filtering.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.9
|
||||||
|
|
||||||
|
Allowed chaining additional field lookups.
|
||||||
|
|
||||||
.. fieldlookup:: month
|
.. fieldlookup:: month
|
||||||
|
|
||||||
month
|
month
|
||||||
~~~~~
|
~~~~~
|
||||||
|
|
||||||
For date and datetime fields, an exact month match. Takes an integer 1
|
For date and datetime fields, an exact month match. Allows chaining additional
|
||||||
(January) through 12 (December).
|
field lookups. Takes an integer 1 (January) through 12 (December).
|
||||||
|
|
||||||
Example::
|
Example::
|
||||||
|
|
||||||
Entry.objects.filter(pub_date__month=12)
|
Entry.objects.filter(pub_date__month=12)
|
||||||
|
Entry.objects.filter(pub_date__month__gte=6)
|
||||||
|
|
||||||
SQL equivalent::
|
SQL equivalent::
|
||||||
|
|
||||||
SELECT ... WHERE EXTRACT('month' FROM pub_date) = '12';
|
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.)
|
(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
|
current time zone before filtering. This requires :ref:`time zone definitions
|
||||||
in the database <database-time-zone-definitions>`.
|
in the database <database-time-zone-definitions>`.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.9
|
||||||
|
|
||||||
|
Allowed chaining additional field lookups.
|
||||||
|
|
||||||
.. fieldlookup:: day
|
.. fieldlookup:: day
|
||||||
|
|
||||||
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::
|
Example::
|
||||||
|
|
||||||
Entry.objects.filter(pub_date__day=3)
|
Entry.objects.filter(pub_date__day=3)
|
||||||
|
Entry.objects.filter(pub_date__day__gte=3)
|
||||||
|
|
||||||
SQL equivalent::
|
SQL equivalent::
|
||||||
|
|
||||||
SELECT ... WHERE EXTRACT('day' FROM pub_date) = '3';
|
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.)
|
(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
|
current time zone before filtering. This requires :ref:`time zone definitions
|
||||||
in the database <database-time-zone-definitions>`.
|
in the database <database-time-zone-definitions>`.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.9
|
||||||
|
|
||||||
|
Allowed chaining additional field lookups.
|
||||||
|
|
||||||
.. fieldlookup:: week_day
|
.. fieldlookup:: week_day
|
||||||
|
|
||||||
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
|
Takes an integer value representing the day of week from 1 (Sunday) to 7
|
||||||
(Saturday).
|
(Saturday).
|
||||||
|
@ -2505,6 +2526,7 @@ Takes an integer value representing the day of week from 1 (Sunday) to 7
|
||||||
Example::
|
Example::
|
||||||
|
|
||||||
Entry.objects.filter(pub_date__week_day=2)
|
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
|
(No equivalent SQL code fragment is included for this lookup because
|
||||||
implementation of the relevant query varies among different database engines.)
|
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
|
current time zone before filtering. This requires :ref:`time zone definitions
|
||||||
in the database <database-time-zone-definitions>`.
|
in the database <database-time-zone-definitions>`.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.9
|
||||||
|
|
||||||
|
Allowed chaining additional field lookups.
|
||||||
|
|
||||||
.. fieldlookup:: hour
|
.. fieldlookup:: hour
|
||||||
|
|
||||||
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::
|
Example::
|
||||||
|
|
||||||
Event.objects.filter(timestamp__hour=23)
|
Event.objects.filter(timestamp__hour=23)
|
||||||
|
Event.objects.filter(timestamp__hour__gte=12)
|
||||||
|
|
||||||
SQL equivalent::
|
SQL equivalent::
|
||||||
|
|
||||||
SELECT ... WHERE EXTRACT('hour' FROM timestamp) = '23';
|
SELECT ... WHERE EXTRACT('hour' FROM timestamp) = '23';
|
||||||
|
SELECT ... WHERE EXTRACT('hour' FROM timestamp) >= '12';
|
||||||
|
|
||||||
(The exact SQL syntax varies for each database engine.)
|
(The exact SQL syntax varies for each database engine.)
|
||||||
|
|
||||||
When :setting:`USE_TZ` is ``True``, values are converted to the current time
|
When :setting:`USE_TZ` is ``True``, values are converted to the current time
|
||||||
zone before filtering.
|
zone before filtering.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.9
|
||||||
|
|
||||||
|
Allowed chaining additional field lookups.
|
||||||
|
|
||||||
.. fieldlookup:: minute
|
.. fieldlookup:: minute
|
||||||
|
|
||||||
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::
|
Example::
|
||||||
|
|
||||||
Event.objects.filter(timestamp__minute=29)
|
Event.objects.filter(timestamp__minute=29)
|
||||||
|
Event.objects.filter(timestamp__minute__gte=29)
|
||||||
|
|
||||||
SQL equivalent::
|
SQL equivalent::
|
||||||
|
|
||||||
SELECT ... WHERE EXTRACT('minute' FROM timestamp) = '29';
|
SELECT ... WHERE EXTRACT('minute' FROM timestamp) = '29';
|
||||||
|
SELECT ... WHERE EXTRACT('minute' FROM timestamp) >= '29';
|
||||||
|
|
||||||
(The exact SQL syntax varies for each database engine.)
|
(The exact SQL syntax varies for each database engine.)
|
||||||
|
|
||||||
When :setting:`USE_TZ` is ``True``, values are converted to the current time
|
When :setting:`USE_TZ` is ``True``, values are converted to the current time
|
||||||
zone before filtering.
|
zone before filtering.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.9
|
||||||
|
|
||||||
|
Allowed chaining additional field lookups.
|
||||||
|
|
||||||
.. fieldlookup:: second
|
.. fieldlookup:: second
|
||||||
|
|
||||||
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::
|
Example::
|
||||||
|
|
||||||
Event.objects.filter(timestamp__second=31)
|
Event.objects.filter(timestamp__second=31)
|
||||||
|
Event.objects.filter(timestamp__second__gte=31)
|
||||||
|
|
||||||
SQL equivalent::
|
SQL equivalent::
|
||||||
|
|
||||||
SELECT ... WHERE EXTRACT('second' FROM timestamp) = '31';
|
SELECT ... WHERE EXTRACT('second' FROM timestamp) = '31';
|
||||||
|
SELECT ... WHERE EXTRACT('second' FROM timestamp) >= '31';
|
||||||
|
|
||||||
(The exact SQL syntax varies for each database engine.)
|
(The exact SQL syntax varies for each database engine.)
|
||||||
|
|
||||||
When :setting:`USE_TZ` is ``True``, values are converted to the current time
|
When :setting:`USE_TZ` is ``True``, values are converted to the current time
|
||||||
zone before filtering.
|
zone before filtering.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.9
|
||||||
|
|
||||||
|
Allowed chaining additional field lookups.
|
||||||
|
|
||||||
.. fieldlookup:: isnull
|
.. fieldlookup:: isnull
|
||||||
|
|
||||||
isnull
|
isnull
|
||||||
|
|
|
@ -184,6 +184,10 @@ Models
|
||||||
* Added a system check to prevent defining both ``Meta.ordering`` and
|
* Added a system check to prevent defining both ``Meta.ordering`` and
|
||||||
``order_with_respect_to`` on the same model.
|
``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
|
CSRF
|
||||||
^^^^
|
^^^^
|
||||||
|
|
||||||
|
|
|
@ -63,7 +63,8 @@ class UpperBilateralTransform(models.Transform):
|
||||||
|
|
||||||
|
|
||||||
class YearTransform(models.Transform):
|
class YearTransform(models.Transform):
|
||||||
lookup_name = 'year'
|
# Use a name that avoids collision with the built-in year lookup.
|
||||||
|
lookup_name = 'testyear'
|
||||||
|
|
||||||
def as_sql(self, compiler, connection):
|
def as_sql(self, compiler, connection):
|
||||||
lhs_sql, params = compiler.compile(self.lhs)
|
lhs_sql, params = compiler.compile(self.lhs)
|
||||||
|
@ -400,19 +401,19 @@ class YearLteTests(TestCase):
|
||||||
def test_year_lte(self):
|
def test_year_lte(self):
|
||||||
baseqs = Author.objects.order_by('name')
|
baseqs = Author.objects.order_by('name')
|
||||||
self.assertQuerysetEqual(
|
self.assertQuerysetEqual(
|
||||||
baseqs.filter(birthdate__year__lte=2012),
|
baseqs.filter(birthdate__testyear__lte=2012),
|
||||||
[self.a1, self.a2, self.a3, self.a4], lambda x: x)
|
[self.a1, self.a2, self.a3, self.a4], lambda x: x)
|
||||||
self.assertQuerysetEqual(
|
self.assertQuerysetEqual(
|
||||||
baseqs.filter(birthdate__year=2012),
|
baseqs.filter(birthdate__testyear=2012),
|
||||||
[self.a2, self.a3, self.a4], lambda x: x)
|
[self.a2, self.a3, self.a4], lambda x: x)
|
||||||
|
|
||||||
self.assertNotIn('BETWEEN', str(baseqs.filter(birthdate__year=2012).query))
|
self.assertNotIn('BETWEEN', str(baseqs.filter(birthdate__testyear=2012).query))
|
||||||
self.assertQuerysetEqual(
|
self.assertQuerysetEqual(
|
||||||
baseqs.filter(birthdate__year__lte=2011),
|
baseqs.filter(birthdate__testyear__lte=2011),
|
||||||
[self.a1], lambda x: x)
|
[self.a1], lambda x: x)
|
||||||
# The non-optimized version works, too.
|
# The non-optimized version works, too.
|
||||||
self.assertQuerysetEqual(
|
self.assertQuerysetEqual(
|
||||||
baseqs.filter(birthdate__year__lt=2012),
|
baseqs.filter(birthdate__testyear__lt=2012),
|
||||||
[self.a1], lambda x: x)
|
[self.a1], lambda x: x)
|
||||||
|
|
||||||
@unittest.skipUnless(connection.vendor == 'postgresql', "PostgreSQL specific SQL used")
|
@unittest.skipUnless(connection.vendor == 'postgresql', "PostgreSQL specific SQL used")
|
||||||
|
@ -425,10 +426,10 @@ class YearLteTests(TestCase):
|
||||||
self.a4.save()
|
self.a4.save()
|
||||||
baseqs = Author.objects.order_by('name')
|
baseqs = Author.objects.order_by('name')
|
||||||
self.assertQuerysetEqual(
|
self.assertQuerysetEqual(
|
||||||
baseqs.filter(birthdate__year__lte=models.F('age')),
|
baseqs.filter(birthdate__testyear__lte=models.F('age')),
|
||||||
[self.a3, self.a4], lambda x: x)
|
[self.a3, self.a4], lambda x: x)
|
||||||
self.assertQuerysetEqual(
|
self.assertQuerysetEqual(
|
||||||
baseqs.filter(birthdate__year__lt=models.F('age')),
|
baseqs.filter(birthdate__testyear__lt=models.F('age')),
|
||||||
[self.a4], lambda x: x)
|
[self.a4], lambda x: x)
|
||||||
|
|
||||||
def test_year_lte_sql(self):
|
def test_year_lte_sql(self):
|
||||||
|
@ -437,16 +438,16 @@ class YearLteTests(TestCase):
|
||||||
# error - not running YearLte SQL at all.
|
# error - not running YearLte SQL at all.
|
||||||
baseqs = Author.objects.order_by('name')
|
baseqs = Author.objects.order_by('name')
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
'<= (2011 || ', str(baseqs.filter(birthdate__year__lte=2011).query))
|
'<= (2011 || ', str(baseqs.filter(birthdate__testyear__lte=2011).query))
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
'-12-31', str(baseqs.filter(birthdate__year__lte=2011).query))
|
'-12-31', str(baseqs.filter(birthdate__testyear__lte=2011).query))
|
||||||
|
|
||||||
def test_postgres_year_exact(self):
|
def test_postgres_year_exact(self):
|
||||||
baseqs = Author.objects.order_by('name')
|
baseqs = Author.objects.order_by('name')
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
'= (2011 || ', str(baseqs.filter(birthdate__year=2011).query))
|
'= (2011 || ', str(baseqs.filter(birthdate__testyear=2011).query))
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
'-12-31', str(baseqs.filter(birthdate__year=2011).query))
|
'-12-31', str(baseqs.filter(birthdate__testyear=2011).query))
|
||||||
|
|
||||||
def test_custom_implementation_year_exact(self):
|
def test_custom_implementation_year_exact(self):
|
||||||
try:
|
try:
|
||||||
|
@ -462,7 +463,7 @@ class YearLteTests(TestCase):
|
||||||
setattr(YearExact, 'as_' + connection.vendor, as_custom_sql)
|
setattr(YearExact, 'as_' + connection.vendor, as_custom_sql)
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
'concat(',
|
'concat(',
|
||||||
str(Author.objects.filter(birthdate__year=2012).query))
|
str(Author.objects.filter(birthdate__testyear=2012).query))
|
||||||
finally:
|
finally:
|
||||||
delattr(YearExact, 'as_' + connection.vendor)
|
delattr(YearExact, 'as_' + connection.vendor)
|
||||||
try:
|
try:
|
||||||
|
@ -483,14 +484,15 @@ class YearLteTests(TestCase):
|
||||||
YearTransform.register_lookup(CustomYearExact)
|
YearTransform.register_lookup(CustomYearExact)
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
'CONCAT(',
|
'CONCAT(',
|
||||||
str(Author.objects.filter(birthdate__year=2012).query))
|
str(Author.objects.filter(birthdate__testyear=2012).query))
|
||||||
finally:
|
finally:
|
||||||
YearTransform._unregister_lookup(CustomYearExact)
|
YearTransform._unregister_lookup(CustomYearExact)
|
||||||
YearTransform.register_lookup(YearExact)
|
YearTransform.register_lookup(YearExact)
|
||||||
|
|
||||||
|
|
||||||
class TrackCallsYearTransform(YearTransform):
|
class TrackCallsYearTransform(YearTransform):
|
||||||
lookup_name = 'year'
|
# Use a name that avoids collision with the built-in year lookup.
|
||||||
|
lookup_name = 'testyear'
|
||||||
call_order = []
|
call_order = []
|
||||||
|
|
||||||
def as_sql(self, compiler, connection):
|
def as_sql(self, compiler, connection):
|
||||||
|
@ -516,23 +518,23 @@ class LookupTransformCallOrderTests(TestCase):
|
||||||
try:
|
try:
|
||||||
# junk lookup - tries lookup, then transform, then fails
|
# junk lookup - tries lookup, then transform, then fails
|
||||||
with self.assertRaises(FieldError):
|
with self.assertRaises(FieldError):
|
||||||
Author.objects.filter(birthdate__year__junk=2012)
|
Author.objects.filter(birthdate__testyear__junk=2012)
|
||||||
self.assertEqual(TrackCallsYearTransform.call_order,
|
self.assertEqual(TrackCallsYearTransform.call_order,
|
||||||
['lookup', 'transform'])
|
['lookup', 'transform'])
|
||||||
TrackCallsYearTransform.call_order = []
|
TrackCallsYearTransform.call_order = []
|
||||||
# junk transform - tries transform only, then fails
|
# junk transform - tries transform only, then fails
|
||||||
with self.assertRaises(FieldError):
|
with self.assertRaises(FieldError):
|
||||||
Author.objects.filter(birthdate__year__junk__more_junk=2012)
|
Author.objects.filter(birthdate__testyear__junk__more_junk=2012)
|
||||||
self.assertEqual(TrackCallsYearTransform.call_order,
|
self.assertEqual(TrackCallsYearTransform.call_order,
|
||||||
['transform'])
|
['transform'])
|
||||||
TrackCallsYearTransform.call_order = []
|
TrackCallsYearTransform.call_order = []
|
||||||
# Just getting the year (implied __exact) - lookup only
|
# Just getting the year (implied __exact) - lookup only
|
||||||
Author.objects.filter(birthdate__year=2012)
|
Author.objects.filter(birthdate__testyear=2012)
|
||||||
self.assertEqual(TrackCallsYearTransform.call_order,
|
self.assertEqual(TrackCallsYearTransform.call_order,
|
||||||
['lookup'])
|
['lookup'])
|
||||||
TrackCallsYearTransform.call_order = []
|
TrackCallsYearTransform.call_order = []
|
||||||
# Just getting the year (explicit __exact) - lookup only
|
# Just getting the year (explicit __exact) - lookup only
|
||||||
Author.objects.filter(birthdate__year__exact=2012)
|
Author.objects.filter(birthdate__testyear__exact=2012)
|
||||||
self.assertEqual(TrackCallsYearTransform.call_order,
|
self.assertEqual(TrackCallsYearTransform.call_order,
|
||||||
['lookup'])
|
['lookup'])
|
||||||
|
|
||||||
|
|
|
@ -713,6 +713,34 @@ class LookupTests(TestCase):
|
||||||
self.assertEqual(Player.objects.filter(games__season__year__gt=2010).distinct().count(), 2)
|
self.assertEqual(Player.objects.filter(games__season__year__gt=2010).distinct().count(), 2)
|
||||||
self.assertEqual(Player.objects.filter(games__season__gt__gt=222).distinct().count(), 2)
|
self.assertEqual(Player.objects.filter(games__season__gt__gt=222).distinct().count(), 2)
|
||||||
|
|
||||||
|
def test_chain_date_time_lookups(self):
|
||||||
|
self.assertQuerysetEqual(
|
||||||
|
Article.objects.filter(pub_date__month__gt=7),
|
||||||
|
['<Article: Article 5>', '<Article: Article 6>'],
|
||||||
|
ordered=False
|
||||||
|
)
|
||||||
|
self.assertQuerysetEqual(
|
||||||
|
Article.objects.filter(pub_date__day__gte=27),
|
||||||
|
['<Article: Article 2>', '<Article: Article 3>',
|
||||||
|
'<Article: Article 4>', '<Article: Article 7>'],
|
||||||
|
ordered=False
|
||||||
|
)
|
||||||
|
self.assertQuerysetEqual(
|
||||||
|
Article.objects.filter(pub_date__hour__lt=8),
|
||||||
|
['<Article: Article 1>', '<Article: Article 2>',
|
||||||
|
'<Article: Article 3>', '<Article: Article 4>',
|
||||||
|
'<Article: Article 7>'],
|
||||||
|
ordered=False
|
||||||
|
)
|
||||||
|
self.assertQuerysetEqual(
|
||||||
|
Article.objects.filter(pub_date__minute__lte=0),
|
||||||
|
['<Article: Article 1>', '<Article: Article 2>',
|
||||||
|
'<Article: Article 3>', '<Article: Article 4>',
|
||||||
|
'<Article: Article 5>', '<Article: Article 6>',
|
||||||
|
'<Article: Article 7>'],
|
||||||
|
ordered=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class LookupTransactionTests(TransactionTestCase):
|
class LookupTransactionTests(TransactionTestCase):
|
||||||
available_apps = ['lookup']
|
available_apps = ['lookup']
|
||||||
|
|
Loading…
Reference in New Issue