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 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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
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'
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
^^^^
|
||||
|
||||
|
|
|
@ -63,7 +63,8 @@ class UpperBilateralTransform(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):
|
||||
lhs_sql, params = compiler.compile(self.lhs)
|
||||
|
@ -400,19 +401,19 @@ class YearLteTests(TestCase):
|
|||
def test_year_lte(self):
|
||||
baseqs = Author.objects.order_by('name')
|
||||
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.assertQuerysetEqual(
|
||||
baseqs.filter(birthdate__year=2012),
|
||||
baseqs.filter(birthdate__testyear=2012),
|
||||
[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(
|
||||
baseqs.filter(birthdate__year__lte=2011),
|
||||
baseqs.filter(birthdate__testyear__lte=2011),
|
||||
[self.a1], lambda x: x)
|
||||
# The non-optimized version works, too.
|
||||
self.assertQuerysetEqual(
|
||||
baseqs.filter(birthdate__year__lt=2012),
|
||||
baseqs.filter(birthdate__testyear__lt=2012),
|
||||
[self.a1], lambda x: x)
|
||||
|
||||
@unittest.skipUnless(connection.vendor == 'postgresql', "PostgreSQL specific SQL used")
|
||||
|
@ -425,10 +426,10 @@ class YearLteTests(TestCase):
|
|||
self.a4.save()
|
||||
baseqs = Author.objects.order_by('name')
|
||||
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.assertQuerysetEqual(
|
||||
baseqs.filter(birthdate__year__lt=models.F('age')),
|
||||
baseqs.filter(birthdate__testyear__lt=models.F('age')),
|
||||
[self.a4], lambda x: x)
|
||||
|
||||
def test_year_lte_sql(self):
|
||||
|
@ -437,16 +438,16 @@ class YearLteTests(TestCase):
|
|||
# error - not running YearLte SQL at all.
|
||||
baseqs = Author.objects.order_by('name')
|
||||
self.assertIn(
|
||||
'<= (2011 || ', str(baseqs.filter(birthdate__year__lte=2011).query))
|
||||
'<= (2011 || ', str(baseqs.filter(birthdate__testyear__lte=2011).query))
|
||||
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):
|
||||
baseqs = Author.objects.order_by('name')
|
||||
self.assertIn(
|
||||
'= (2011 || ', str(baseqs.filter(birthdate__year=2011).query))
|
||||
'= (2011 || ', str(baseqs.filter(birthdate__testyear=2011).query))
|
||||
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):
|
||||
try:
|
||||
|
@ -462,7 +463,7 @@ class YearLteTests(TestCase):
|
|||
setattr(YearExact, 'as_' + connection.vendor, as_custom_sql)
|
||||
self.assertIn(
|
||||
'concat(',
|
||||
str(Author.objects.filter(birthdate__year=2012).query))
|
||||
str(Author.objects.filter(birthdate__testyear=2012).query))
|
||||
finally:
|
||||
delattr(YearExact, 'as_' + connection.vendor)
|
||||
try:
|
||||
|
@ -483,14 +484,15 @@ class YearLteTests(TestCase):
|
|||
YearTransform.register_lookup(CustomYearExact)
|
||||
self.assertIn(
|
||||
'CONCAT(',
|
||||
str(Author.objects.filter(birthdate__year=2012).query))
|
||||
str(Author.objects.filter(birthdate__testyear=2012).query))
|
||||
finally:
|
||||
YearTransform._unregister_lookup(CustomYearExact)
|
||||
YearTransform.register_lookup(YearExact)
|
||||
|
||||
|
||||
class TrackCallsYearTransform(YearTransform):
|
||||
lookup_name = 'year'
|
||||
# Use a name that avoids collision with the built-in year lookup.
|
||||
lookup_name = 'testyear'
|
||||
call_order = []
|
||||
|
||||
def as_sql(self, compiler, connection):
|
||||
|
@ -516,23 +518,23 @@ class LookupTransformCallOrderTests(TestCase):
|
|||
try:
|
||||
# junk lookup - tries lookup, then transform, then fails
|
||||
with self.assertRaises(FieldError):
|
||||
Author.objects.filter(birthdate__year__junk=2012)
|
||||
Author.objects.filter(birthdate__testyear__junk=2012)
|
||||
self.assertEqual(TrackCallsYearTransform.call_order,
|
||||
['lookup', 'transform'])
|
||||
TrackCallsYearTransform.call_order = []
|
||||
# junk transform - tries transform only, then fails
|
||||
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,
|
||||
['transform'])
|
||||
TrackCallsYearTransform.call_order = []
|
||||
# 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,
|
||||
['lookup'])
|
||||
TrackCallsYearTransform.call_order = []
|
||||
# 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,
|
||||
['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__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):
|
||||
available_apps = ['lookup']
|
||||
|
|
Loading…
Reference in New Issue