test_extract_trunc.py 63 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228
  1. from datetime import datetime, timedelta, timezone as datetime_timezone
  2. import pytz
  3. from django.conf import settings
  4. from django.db.models import (
  5. DateField, DateTimeField, F, IntegerField, Max, OuterRef, Subquery,
  6. TimeField,
  7. )
  8. from django.db.models.functions import (
  9. Extract, ExtractDay, ExtractHour, ExtractIsoWeekDay, ExtractIsoYear,
  10. ExtractMinute, ExtractMonth, ExtractQuarter, ExtractSecond, ExtractWeek,
  11. ExtractWeekDay, ExtractYear, Trunc, TruncDate, TruncDay, TruncHour,
  12. TruncMinute, TruncMonth, TruncQuarter, TruncSecond, TruncTime, TruncWeek,
  13. TruncYear,
  14. )
  15. from django.test import (
  16. TestCase, override_settings, skipIfDBFeature, skipUnlessDBFeature,
  17. )
  18. from django.utils import timezone
  19. from ..models import Author, DTModel, Fan
  20. def truncate_to(value, kind, tzinfo=None):
  21. # Convert to target timezone before truncation
  22. if tzinfo is not None:
  23. value = value.astimezone(tzinfo)
  24. def truncate(value, kind):
  25. if kind == 'second':
  26. return value.replace(microsecond=0)
  27. if kind == 'minute':
  28. return value.replace(second=0, microsecond=0)
  29. if kind == 'hour':
  30. return value.replace(minute=0, second=0, microsecond=0)
  31. if kind == 'day':
  32. if isinstance(value, datetime):
  33. return value.replace(hour=0, minute=0, second=0, microsecond=0)
  34. return value
  35. if kind == 'week':
  36. if isinstance(value, datetime):
  37. return (value - timedelta(days=value.weekday())).replace(hour=0, minute=0, second=0, microsecond=0)
  38. return value - timedelta(days=value.weekday())
  39. if kind == 'month':
  40. if isinstance(value, datetime):
  41. return value.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
  42. return value.replace(day=1)
  43. if kind == 'quarter':
  44. month_in_quarter = value.month - (value.month - 1) % 3
  45. if isinstance(value, datetime):
  46. return value.replace(month=month_in_quarter, day=1, hour=0, minute=0, second=0, microsecond=0)
  47. return value.replace(month=month_in_quarter, day=1)
  48. # otherwise, truncate to year
  49. if isinstance(value, datetime):
  50. return value.replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0)
  51. return value.replace(month=1, day=1)
  52. value = truncate(value, kind)
  53. if tzinfo is not None:
  54. # If there was a daylight saving transition, then reset the timezone.
  55. value = timezone.make_aware(value.replace(tzinfo=None), tzinfo)
  56. return value
  57. @override_settings(USE_TZ=False)
  58. class DateFunctionTests(TestCase):
  59. def create_model(self, start_datetime, end_datetime):
  60. return DTModel.objects.create(
  61. name=start_datetime.isoformat() if start_datetime else 'None',
  62. start_datetime=start_datetime,
  63. end_datetime=end_datetime,
  64. start_date=start_datetime.date() if start_datetime else None,
  65. end_date=end_datetime.date() if end_datetime else None,
  66. start_time=start_datetime.time() if start_datetime else None,
  67. end_time=end_datetime.time() if end_datetime else None,
  68. duration=(end_datetime - start_datetime) if start_datetime and end_datetime else None,
  69. )
  70. def test_extract_year_exact_lookup(self):
  71. """
  72. Extract year uses a BETWEEN filter to compare the year to allow indexes
  73. to be used.
  74. """
  75. start_datetime = datetime(2015, 6, 15, 14, 10)
  76. end_datetime = datetime(2016, 6, 15, 14, 10)
  77. if settings.USE_TZ:
  78. start_datetime = timezone.make_aware(start_datetime, is_dst=False)
  79. end_datetime = timezone.make_aware(end_datetime, is_dst=False)
  80. self.create_model(start_datetime, end_datetime)
  81. self.create_model(end_datetime, start_datetime)
  82. for lookup in ('year', 'iso_year'):
  83. with self.subTest(lookup):
  84. qs = DTModel.objects.filter(**{'start_datetime__%s__exact' % lookup: 2015})
  85. self.assertEqual(qs.count(), 1)
  86. query_string = str(qs.query).lower()
  87. self.assertEqual(query_string.count(' between '), 1)
  88. self.assertEqual(query_string.count('extract'), 0)
  89. # exact is implied and should be the same
  90. qs = DTModel.objects.filter(**{'start_datetime__%s' % lookup: 2015})
  91. self.assertEqual(qs.count(), 1)
  92. query_string = str(qs.query).lower()
  93. self.assertEqual(query_string.count(' between '), 1)
  94. self.assertEqual(query_string.count('extract'), 0)
  95. # date and datetime fields should behave the same
  96. qs = DTModel.objects.filter(**{'start_date__%s' % lookup: 2015})
  97. self.assertEqual(qs.count(), 1)
  98. query_string = str(qs.query).lower()
  99. self.assertEqual(query_string.count(' between '), 1)
  100. self.assertEqual(query_string.count('extract'), 0)
  101. # an expression rhs cannot use the between optimization.
  102. qs = DTModel.objects.annotate(
  103. start_year=ExtractYear('start_datetime'),
  104. ).filter(end_datetime__year=F('start_year') + 1)
  105. self.assertEqual(qs.count(), 1)
  106. query_string = str(qs.query).lower()
  107. self.assertEqual(query_string.count(' between '), 0)
  108. self.assertEqual(query_string.count('extract'), 3)
  109. def test_extract_year_greaterthan_lookup(self):
  110. start_datetime = datetime(2015, 6, 15, 14, 10)
  111. end_datetime = datetime(2016, 6, 15, 14, 10)
  112. if settings.USE_TZ:
  113. start_datetime = timezone.make_aware(start_datetime, is_dst=False)
  114. end_datetime = timezone.make_aware(end_datetime, is_dst=False)
  115. self.create_model(start_datetime, end_datetime)
  116. self.create_model(end_datetime, start_datetime)
  117. for lookup in ('year', 'iso_year'):
  118. with self.subTest(lookup):
  119. qs = DTModel.objects.filter(**{'start_datetime__%s__gt' % lookup: 2015})
  120. self.assertEqual(qs.count(), 1)
  121. self.assertEqual(str(qs.query).lower().count('extract'), 0)
  122. qs = DTModel.objects.filter(**{'start_datetime__%s__gte' % lookup: 2015})
  123. self.assertEqual(qs.count(), 2)
  124. self.assertEqual(str(qs.query).lower().count('extract'), 0)
  125. qs = DTModel.objects.annotate(
  126. start_year=ExtractYear('start_datetime'),
  127. ).filter(**{'end_datetime__%s__gte' % lookup: F('start_year')})
  128. self.assertEqual(qs.count(), 1)
  129. self.assertGreaterEqual(str(qs.query).lower().count('extract'), 2)
  130. def test_extract_year_lessthan_lookup(self):
  131. start_datetime = datetime(2015, 6, 15, 14, 10)
  132. end_datetime = datetime(2016, 6, 15, 14, 10)
  133. if settings.USE_TZ:
  134. start_datetime = timezone.make_aware(start_datetime, is_dst=False)
  135. end_datetime = timezone.make_aware(end_datetime, is_dst=False)
  136. self.create_model(start_datetime, end_datetime)
  137. self.create_model(end_datetime, start_datetime)
  138. for lookup in ('year', 'iso_year'):
  139. with self.subTest(lookup):
  140. qs = DTModel.objects.filter(**{'start_datetime__%s__lt' % lookup: 2016})
  141. self.assertEqual(qs.count(), 1)
  142. self.assertEqual(str(qs.query).count('extract'), 0)
  143. qs = DTModel.objects.filter(**{'start_datetime__%s__lte' % lookup: 2016})
  144. self.assertEqual(qs.count(), 2)
  145. self.assertEqual(str(qs.query).count('extract'), 0)
  146. qs = DTModel.objects.annotate(
  147. end_year=ExtractYear('end_datetime'),
  148. ).filter(**{'start_datetime__%s__lte' % lookup: F('end_year')})
  149. self.assertEqual(qs.count(), 1)
  150. self.assertGreaterEqual(str(qs.query).lower().count('extract'), 2)
  151. def test_extract_func(self):
  152. start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
  153. end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
  154. if settings.USE_TZ:
  155. start_datetime = timezone.make_aware(start_datetime, is_dst=False)
  156. end_datetime = timezone.make_aware(end_datetime, is_dst=False)
  157. self.create_model(start_datetime, end_datetime)
  158. self.create_model(end_datetime, start_datetime)
  159. with self.assertRaisesMessage(ValueError, 'lookup_name must be provided'):
  160. Extract('start_datetime')
  161. msg = 'Extract input expression must be DateField, DateTimeField, TimeField, or DurationField.'
  162. with self.assertRaisesMessage(ValueError, msg):
  163. list(DTModel.objects.annotate(extracted=Extract('name', 'hour')))
  164. with self.assertRaisesMessage(
  165. ValueError, "Cannot extract time component 'second' from DateField 'start_date'."):
  166. list(DTModel.objects.annotate(extracted=Extract('start_date', 'second')))
  167. self.assertQuerysetEqual(
  168. DTModel.objects.annotate(extracted=Extract('start_datetime', 'year')).order_by('start_datetime'),
  169. [(start_datetime, start_datetime.year), (end_datetime, end_datetime.year)],
  170. lambda m: (m.start_datetime, m.extracted)
  171. )
  172. self.assertQuerysetEqual(
  173. DTModel.objects.annotate(extracted=Extract('start_datetime', 'quarter')).order_by('start_datetime'),
  174. [(start_datetime, 2), (end_datetime, 2)],
  175. lambda m: (m.start_datetime, m.extracted)
  176. )
  177. self.assertQuerysetEqual(
  178. DTModel.objects.annotate(extracted=Extract('start_datetime', 'month')).order_by('start_datetime'),
  179. [(start_datetime, start_datetime.month), (end_datetime, end_datetime.month)],
  180. lambda m: (m.start_datetime, m.extracted)
  181. )
  182. self.assertQuerysetEqual(
  183. DTModel.objects.annotate(extracted=Extract('start_datetime', 'day')).order_by('start_datetime'),
  184. [(start_datetime, start_datetime.day), (end_datetime, end_datetime.day)],
  185. lambda m: (m.start_datetime, m.extracted)
  186. )
  187. self.assertQuerysetEqual(
  188. DTModel.objects.annotate(extracted=Extract('start_datetime', 'week')).order_by('start_datetime'),
  189. [(start_datetime, 25), (end_datetime, 24)],
  190. lambda m: (m.start_datetime, m.extracted)
  191. )
  192. self.assertQuerysetEqual(
  193. DTModel.objects.annotate(extracted=Extract('start_datetime', 'week_day')).order_by('start_datetime'),
  194. [
  195. (start_datetime, (start_datetime.isoweekday() % 7) + 1),
  196. (end_datetime, (end_datetime.isoweekday() % 7) + 1)
  197. ],
  198. lambda m: (m.start_datetime, m.extracted)
  199. )
  200. self.assertQuerysetEqual(
  201. DTModel.objects.annotate(
  202. extracted=Extract('start_datetime', 'iso_week_day'),
  203. ).order_by('start_datetime'),
  204. [
  205. (start_datetime, start_datetime.isoweekday()),
  206. (end_datetime, end_datetime.isoweekday()),
  207. ],
  208. lambda m: (m.start_datetime, m.extracted)
  209. )
  210. self.assertQuerysetEqual(
  211. DTModel.objects.annotate(extracted=Extract('start_datetime', 'hour')).order_by('start_datetime'),
  212. [(start_datetime, start_datetime.hour), (end_datetime, end_datetime.hour)],
  213. lambda m: (m.start_datetime, m.extracted)
  214. )
  215. self.assertQuerysetEqual(
  216. DTModel.objects.annotate(extracted=Extract('start_datetime', 'minute')).order_by('start_datetime'),
  217. [(start_datetime, start_datetime.minute), (end_datetime, end_datetime.minute)],
  218. lambda m: (m.start_datetime, m.extracted)
  219. )
  220. self.assertQuerysetEqual(
  221. DTModel.objects.annotate(extracted=Extract('start_datetime', 'second')).order_by('start_datetime'),
  222. [(start_datetime, start_datetime.second), (end_datetime, end_datetime.second)],
  223. lambda m: (m.start_datetime, m.extracted)
  224. )
  225. self.assertEqual(DTModel.objects.filter(start_datetime__year=Extract('start_datetime', 'year')).count(), 2)
  226. self.assertEqual(DTModel.objects.filter(start_datetime__hour=Extract('start_datetime', 'hour')).count(), 2)
  227. self.assertEqual(DTModel.objects.filter(start_date__month=Extract('start_date', 'month')).count(), 2)
  228. self.assertEqual(DTModel.objects.filter(start_time__hour=Extract('start_time', 'hour')).count(), 2)
  229. def test_extract_none(self):
  230. self.create_model(None, None)
  231. for t in (Extract('start_datetime', 'year'), Extract('start_date', 'year'), Extract('start_time', 'hour')):
  232. with self.subTest(t):
  233. self.assertIsNone(DTModel.objects.annotate(extracted=t).first().extracted)
  234. @skipUnlessDBFeature('has_native_duration_field')
  235. def test_extract_duration(self):
  236. start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
  237. end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
  238. if settings.USE_TZ:
  239. start_datetime = timezone.make_aware(start_datetime, is_dst=False)
  240. end_datetime = timezone.make_aware(end_datetime, is_dst=False)
  241. self.create_model(start_datetime, end_datetime)
  242. self.create_model(end_datetime, start_datetime)
  243. self.assertQuerysetEqual(
  244. DTModel.objects.annotate(extracted=Extract('duration', 'second')).order_by('start_datetime'),
  245. [
  246. (start_datetime, (end_datetime - start_datetime).seconds % 60),
  247. (end_datetime, (start_datetime - end_datetime).seconds % 60)
  248. ],
  249. lambda m: (m.start_datetime, m.extracted)
  250. )
  251. self.assertEqual(
  252. DTModel.objects.annotate(
  253. duration_days=Extract('duration', 'day'),
  254. ).filter(duration_days__gt=200).count(),
  255. 1
  256. )
  257. @skipIfDBFeature('has_native_duration_field')
  258. def test_extract_duration_without_native_duration_field(self):
  259. msg = 'Extract requires native DurationField database support.'
  260. with self.assertRaisesMessage(ValueError, msg):
  261. list(DTModel.objects.annotate(extracted=Extract('duration', 'second')))
  262. def test_extract_duration_unsupported_lookups(self):
  263. msg = "Cannot extract component '%s' from DurationField 'duration'."
  264. for lookup in (
  265. 'year', 'iso_year', 'month', 'week', 'week_day', 'iso_week_day',
  266. 'quarter',
  267. ):
  268. with self.subTest(lookup):
  269. with self.assertRaisesMessage(ValueError, msg % lookup):
  270. DTModel.objects.annotate(extracted=Extract('duration', lookup))
  271. def test_extract_year_func(self):
  272. start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
  273. end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
  274. if settings.USE_TZ:
  275. start_datetime = timezone.make_aware(start_datetime, is_dst=False)
  276. end_datetime = timezone.make_aware(end_datetime, is_dst=False)
  277. self.create_model(start_datetime, end_datetime)
  278. self.create_model(end_datetime, start_datetime)
  279. self.assertQuerysetEqual(
  280. DTModel.objects.annotate(extracted=ExtractYear('start_datetime')).order_by('start_datetime'),
  281. [(start_datetime, start_datetime.year), (end_datetime, end_datetime.year)],
  282. lambda m: (m.start_datetime, m.extracted)
  283. )
  284. self.assertQuerysetEqual(
  285. DTModel.objects.annotate(extracted=ExtractYear('start_date')).order_by('start_datetime'),
  286. [(start_datetime, start_datetime.year), (end_datetime, end_datetime.year)],
  287. lambda m: (m.start_datetime, m.extracted)
  288. )
  289. self.assertEqual(DTModel.objects.filter(start_datetime__year=ExtractYear('start_datetime')).count(), 2)
  290. def test_extract_iso_year_func(self):
  291. start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
  292. end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
  293. if settings.USE_TZ:
  294. start_datetime = timezone.make_aware(start_datetime, is_dst=False)
  295. end_datetime = timezone.make_aware(end_datetime, is_dst=False)
  296. self.create_model(start_datetime, end_datetime)
  297. self.create_model(end_datetime, start_datetime)
  298. self.assertQuerysetEqual(
  299. DTModel.objects.annotate(extracted=ExtractIsoYear('start_datetime')).order_by('start_datetime'),
  300. [(start_datetime, start_datetime.year), (end_datetime, end_datetime.year)],
  301. lambda m: (m.start_datetime, m.extracted)
  302. )
  303. self.assertQuerysetEqual(
  304. DTModel.objects.annotate(extracted=ExtractIsoYear('start_date')).order_by('start_datetime'),
  305. [(start_datetime, start_datetime.year), (end_datetime, end_datetime.year)],
  306. lambda m: (m.start_datetime, m.extracted)
  307. )
  308. # Both dates are from the same week year.
  309. self.assertEqual(DTModel.objects.filter(start_datetime__iso_year=ExtractIsoYear('start_datetime')).count(), 2)
  310. def test_extract_iso_year_func_boundaries(self):
  311. end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
  312. if settings.USE_TZ:
  313. end_datetime = timezone.make_aware(end_datetime, is_dst=False)
  314. week_52_day_2014 = datetime(2014, 12, 27, 13, 0) # Sunday
  315. week_1_day_2014_2015 = datetime(2014, 12, 31, 13, 0) # Wednesday
  316. week_53_day_2015 = datetime(2015, 12, 31, 13, 0) # Thursday
  317. if settings.USE_TZ:
  318. week_1_day_2014_2015 = timezone.make_aware(week_1_day_2014_2015, is_dst=False)
  319. week_52_day_2014 = timezone.make_aware(week_52_day_2014, is_dst=False)
  320. week_53_day_2015 = timezone.make_aware(week_53_day_2015, is_dst=False)
  321. days = [week_52_day_2014, week_1_day_2014_2015, week_53_day_2015]
  322. self.create_model(week_53_day_2015, end_datetime)
  323. self.create_model(week_52_day_2014, end_datetime)
  324. self.create_model(week_1_day_2014_2015, end_datetime)
  325. qs = DTModel.objects.filter(start_datetime__in=days).annotate(
  326. extracted=ExtractIsoYear('start_datetime'),
  327. ).order_by('start_datetime')
  328. self.assertQuerysetEqual(qs, [
  329. (week_52_day_2014, 2014),
  330. (week_1_day_2014_2015, 2015),
  331. (week_53_day_2015, 2015),
  332. ], lambda m: (m.start_datetime, m.extracted))
  333. def test_extract_month_func(self):
  334. start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
  335. end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
  336. if settings.USE_TZ:
  337. start_datetime = timezone.make_aware(start_datetime, is_dst=False)
  338. end_datetime = timezone.make_aware(end_datetime, is_dst=False)
  339. self.create_model(start_datetime, end_datetime)
  340. self.create_model(end_datetime, start_datetime)
  341. self.assertQuerysetEqual(
  342. DTModel.objects.annotate(extracted=ExtractMonth('start_datetime')).order_by('start_datetime'),
  343. [(start_datetime, start_datetime.month), (end_datetime, end_datetime.month)],
  344. lambda m: (m.start_datetime, m.extracted)
  345. )
  346. self.assertQuerysetEqual(
  347. DTModel.objects.annotate(extracted=ExtractMonth('start_date')).order_by('start_datetime'),
  348. [(start_datetime, start_datetime.month), (end_datetime, end_datetime.month)],
  349. lambda m: (m.start_datetime, m.extracted)
  350. )
  351. self.assertEqual(DTModel.objects.filter(start_datetime__month=ExtractMonth('start_datetime')).count(), 2)
  352. def test_extract_day_func(self):
  353. start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
  354. end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
  355. if settings.USE_TZ:
  356. start_datetime = timezone.make_aware(start_datetime, is_dst=False)
  357. end_datetime = timezone.make_aware(end_datetime, is_dst=False)
  358. self.create_model(start_datetime, end_datetime)
  359. self.create_model(end_datetime, start_datetime)
  360. self.assertQuerysetEqual(
  361. DTModel.objects.annotate(extracted=ExtractDay('start_datetime')).order_by('start_datetime'),
  362. [(start_datetime, start_datetime.day), (end_datetime, end_datetime.day)],
  363. lambda m: (m.start_datetime, m.extracted)
  364. )
  365. self.assertQuerysetEqual(
  366. DTModel.objects.annotate(extracted=ExtractDay('start_date')).order_by('start_datetime'),
  367. [(start_datetime, start_datetime.day), (end_datetime, end_datetime.day)],
  368. lambda m: (m.start_datetime, m.extracted)
  369. )
  370. self.assertEqual(DTModel.objects.filter(start_datetime__day=ExtractDay('start_datetime')).count(), 2)
  371. def test_extract_week_func(self):
  372. start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
  373. end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
  374. if settings.USE_TZ:
  375. start_datetime = timezone.make_aware(start_datetime, is_dst=False)
  376. end_datetime = timezone.make_aware(end_datetime, is_dst=False)
  377. self.create_model(start_datetime, end_datetime)
  378. self.create_model(end_datetime, start_datetime)
  379. self.assertQuerysetEqual(
  380. DTModel.objects.annotate(extracted=ExtractWeek('start_datetime')).order_by('start_datetime'),
  381. [(start_datetime, 25), (end_datetime, 24)],
  382. lambda m: (m.start_datetime, m.extracted)
  383. )
  384. self.assertQuerysetEqual(
  385. DTModel.objects.annotate(extracted=ExtractWeek('start_date')).order_by('start_datetime'),
  386. [(start_datetime, 25), (end_datetime, 24)],
  387. lambda m: (m.start_datetime, m.extracted)
  388. )
  389. # both dates are from the same week.
  390. self.assertEqual(DTModel.objects.filter(start_datetime__week=ExtractWeek('start_datetime')).count(), 2)
  391. def test_extract_quarter_func(self):
  392. start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
  393. end_datetime = datetime(2016, 8, 15, 14, 10, 50, 123)
  394. if settings.USE_TZ:
  395. start_datetime = timezone.make_aware(start_datetime, is_dst=False)
  396. end_datetime = timezone.make_aware(end_datetime, is_dst=False)
  397. self.create_model(start_datetime, end_datetime)
  398. self.create_model(end_datetime, start_datetime)
  399. self.assertQuerysetEqual(
  400. DTModel.objects.annotate(extracted=ExtractQuarter('start_datetime')).order_by('start_datetime'),
  401. [(start_datetime, 2), (end_datetime, 3)],
  402. lambda m: (m.start_datetime, m.extracted)
  403. )
  404. self.assertQuerysetEqual(
  405. DTModel.objects.annotate(extracted=ExtractQuarter('start_date')).order_by('start_datetime'),
  406. [(start_datetime, 2), (end_datetime, 3)],
  407. lambda m: (m.start_datetime, m.extracted)
  408. )
  409. self.assertEqual(DTModel.objects.filter(start_datetime__quarter=ExtractQuarter('start_datetime')).count(), 2)
  410. def test_extract_quarter_func_boundaries(self):
  411. end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
  412. if settings.USE_TZ:
  413. end_datetime = timezone.make_aware(end_datetime, is_dst=False)
  414. last_quarter_2014 = datetime(2014, 12, 31, 13, 0)
  415. first_quarter_2015 = datetime(2015, 1, 1, 13, 0)
  416. if settings.USE_TZ:
  417. last_quarter_2014 = timezone.make_aware(last_quarter_2014, is_dst=False)
  418. first_quarter_2015 = timezone.make_aware(first_quarter_2015, is_dst=False)
  419. dates = [last_quarter_2014, first_quarter_2015]
  420. self.create_model(last_quarter_2014, end_datetime)
  421. self.create_model(first_quarter_2015, end_datetime)
  422. qs = DTModel.objects.filter(start_datetime__in=dates).annotate(
  423. extracted=ExtractQuarter('start_datetime'),
  424. ).order_by('start_datetime')
  425. self.assertQuerysetEqual(qs, [
  426. (last_quarter_2014, 4),
  427. (first_quarter_2015, 1),
  428. ], lambda m: (m.start_datetime, m.extracted))
  429. def test_extract_week_func_boundaries(self):
  430. end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
  431. if settings.USE_TZ:
  432. end_datetime = timezone.make_aware(end_datetime, is_dst=False)
  433. week_52_day_2014 = datetime(2014, 12, 27, 13, 0) # Sunday
  434. week_1_day_2014_2015 = datetime(2014, 12, 31, 13, 0) # Wednesday
  435. week_53_day_2015 = datetime(2015, 12, 31, 13, 0) # Thursday
  436. if settings.USE_TZ:
  437. week_1_day_2014_2015 = timezone.make_aware(week_1_day_2014_2015, is_dst=False)
  438. week_52_day_2014 = timezone.make_aware(week_52_day_2014, is_dst=False)
  439. week_53_day_2015 = timezone.make_aware(week_53_day_2015, is_dst=False)
  440. days = [week_52_day_2014, week_1_day_2014_2015, week_53_day_2015]
  441. self.create_model(week_53_day_2015, end_datetime)
  442. self.create_model(week_52_day_2014, end_datetime)
  443. self.create_model(week_1_day_2014_2015, end_datetime)
  444. qs = DTModel.objects.filter(start_datetime__in=days).annotate(
  445. extracted=ExtractWeek('start_datetime'),
  446. ).order_by('start_datetime')
  447. self.assertQuerysetEqual(qs, [
  448. (week_52_day_2014, 52),
  449. (week_1_day_2014_2015, 1),
  450. (week_53_day_2015, 53),
  451. ], lambda m: (m.start_datetime, m.extracted))
  452. def test_extract_weekday_func(self):
  453. start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
  454. end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
  455. if settings.USE_TZ:
  456. start_datetime = timezone.make_aware(start_datetime, is_dst=False)
  457. end_datetime = timezone.make_aware(end_datetime, is_dst=False)
  458. self.create_model(start_datetime, end_datetime)
  459. self.create_model(end_datetime, start_datetime)
  460. self.assertQuerysetEqual(
  461. DTModel.objects.annotate(extracted=ExtractWeekDay('start_datetime')).order_by('start_datetime'),
  462. [
  463. (start_datetime, (start_datetime.isoweekday() % 7) + 1),
  464. (end_datetime, (end_datetime.isoweekday() % 7) + 1),
  465. ],
  466. lambda m: (m.start_datetime, m.extracted)
  467. )
  468. self.assertQuerysetEqual(
  469. DTModel.objects.annotate(extracted=ExtractWeekDay('start_date')).order_by('start_datetime'),
  470. [
  471. (start_datetime, (start_datetime.isoweekday() % 7) + 1),
  472. (end_datetime, (end_datetime.isoweekday() % 7) + 1),
  473. ],
  474. lambda m: (m.start_datetime, m.extracted)
  475. )
  476. self.assertEqual(DTModel.objects.filter(start_datetime__week_day=ExtractWeekDay('start_datetime')).count(), 2)
  477. def test_extract_iso_weekday_func(self):
  478. start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
  479. end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
  480. if settings.USE_TZ:
  481. start_datetime = timezone.make_aware(start_datetime, is_dst=False)
  482. end_datetime = timezone.make_aware(end_datetime, is_dst=False)
  483. self.create_model(start_datetime, end_datetime)
  484. self.create_model(end_datetime, start_datetime)
  485. self.assertQuerysetEqual(
  486. DTModel.objects.annotate(
  487. extracted=ExtractIsoWeekDay('start_datetime'),
  488. ).order_by('start_datetime'),
  489. [
  490. (start_datetime, start_datetime.isoweekday()),
  491. (end_datetime, end_datetime.isoweekday()),
  492. ],
  493. lambda m: (m.start_datetime, m.extracted)
  494. )
  495. self.assertQuerysetEqual(
  496. DTModel.objects.annotate(
  497. extracted=ExtractIsoWeekDay('start_date'),
  498. ).order_by('start_datetime'),
  499. [
  500. (start_datetime, start_datetime.isoweekday()),
  501. (end_datetime, end_datetime.isoweekday()),
  502. ],
  503. lambda m: (m.start_datetime, m.extracted)
  504. )
  505. self.assertEqual(
  506. DTModel.objects.filter(
  507. start_datetime__week_day=ExtractWeekDay('start_datetime'),
  508. ).count(),
  509. 2,
  510. )
  511. def test_extract_hour_func(self):
  512. start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
  513. end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
  514. if settings.USE_TZ:
  515. start_datetime = timezone.make_aware(start_datetime, is_dst=False)
  516. end_datetime = timezone.make_aware(end_datetime, is_dst=False)
  517. self.create_model(start_datetime, end_datetime)
  518. self.create_model(end_datetime, start_datetime)
  519. self.assertQuerysetEqual(
  520. DTModel.objects.annotate(extracted=ExtractHour('start_datetime')).order_by('start_datetime'),
  521. [(start_datetime, start_datetime.hour), (end_datetime, end_datetime.hour)],
  522. lambda m: (m.start_datetime, m.extracted)
  523. )
  524. self.assertQuerysetEqual(
  525. DTModel.objects.annotate(extracted=ExtractHour('start_time')).order_by('start_datetime'),
  526. [(start_datetime, start_datetime.hour), (end_datetime, end_datetime.hour)],
  527. lambda m: (m.start_datetime, m.extracted)
  528. )
  529. self.assertEqual(DTModel.objects.filter(start_datetime__hour=ExtractHour('start_datetime')).count(), 2)
  530. def test_extract_minute_func(self):
  531. start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
  532. end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
  533. if settings.USE_TZ:
  534. start_datetime = timezone.make_aware(start_datetime, is_dst=False)
  535. end_datetime = timezone.make_aware(end_datetime, is_dst=False)
  536. self.create_model(start_datetime, end_datetime)
  537. self.create_model(end_datetime, start_datetime)
  538. self.assertQuerysetEqual(
  539. DTModel.objects.annotate(extracted=ExtractMinute('start_datetime')).order_by('start_datetime'),
  540. [(start_datetime, start_datetime.minute), (end_datetime, end_datetime.minute)],
  541. lambda m: (m.start_datetime, m.extracted)
  542. )
  543. self.assertQuerysetEqual(
  544. DTModel.objects.annotate(extracted=ExtractMinute('start_time')).order_by('start_datetime'),
  545. [(start_datetime, start_datetime.minute), (end_datetime, end_datetime.minute)],
  546. lambda m: (m.start_datetime, m.extracted)
  547. )
  548. self.assertEqual(DTModel.objects.filter(start_datetime__minute=ExtractMinute('start_datetime')).count(), 2)
  549. def test_extract_second_func(self):
  550. start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
  551. end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
  552. if settings.USE_TZ:
  553. start_datetime = timezone.make_aware(start_datetime, is_dst=False)
  554. end_datetime = timezone.make_aware(end_datetime, is_dst=False)
  555. self.create_model(start_datetime, end_datetime)
  556. self.create_model(end_datetime, start_datetime)
  557. self.assertQuerysetEqual(
  558. DTModel.objects.annotate(extracted=ExtractSecond('start_datetime')).order_by('start_datetime'),
  559. [(start_datetime, start_datetime.second), (end_datetime, end_datetime.second)],
  560. lambda m: (m.start_datetime, m.extracted)
  561. )
  562. self.assertQuerysetEqual(
  563. DTModel.objects.annotate(extracted=ExtractSecond('start_time')).order_by('start_datetime'),
  564. [(start_datetime, start_datetime.second), (end_datetime, end_datetime.second)],
  565. lambda m: (m.start_datetime, m.extracted)
  566. )
  567. self.assertEqual(DTModel.objects.filter(start_datetime__second=ExtractSecond('start_datetime')).count(), 2)
  568. def test_trunc_func(self):
  569. start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
  570. end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
  571. if settings.USE_TZ:
  572. start_datetime = timezone.make_aware(start_datetime, is_dst=False)
  573. end_datetime = timezone.make_aware(end_datetime, is_dst=False)
  574. self.create_model(start_datetime, end_datetime)
  575. self.create_model(end_datetime, start_datetime)
  576. msg = 'output_field must be either DateField, TimeField, or DateTimeField'
  577. with self.assertRaisesMessage(ValueError, msg):
  578. list(DTModel.objects.annotate(truncated=Trunc('start_datetime', 'year', output_field=IntegerField())))
  579. with self.assertRaisesMessage(AssertionError, "'name' isn't a DateField, TimeField, or DateTimeField."):
  580. list(DTModel.objects.annotate(truncated=Trunc('name', 'year', output_field=DateTimeField())))
  581. with self.assertRaisesMessage(ValueError, "Cannot truncate DateField 'start_date' to DateTimeField"):
  582. list(DTModel.objects.annotate(truncated=Trunc('start_date', 'second')))
  583. with self.assertRaisesMessage(ValueError, "Cannot truncate TimeField 'start_time' to DateTimeField"):
  584. list(DTModel.objects.annotate(truncated=Trunc('start_time', 'month')))
  585. with self.assertRaisesMessage(ValueError, "Cannot truncate DateField 'start_date' to DateTimeField"):
  586. list(DTModel.objects.annotate(truncated=Trunc('start_date', 'month', output_field=DateTimeField())))
  587. with self.assertRaisesMessage(ValueError, "Cannot truncate TimeField 'start_time' to DateTimeField"):
  588. list(DTModel.objects.annotate(truncated=Trunc('start_time', 'second', output_field=DateTimeField())))
  589. def test_datetime_kind(kind):
  590. self.assertQuerysetEqual(
  591. DTModel.objects.annotate(
  592. truncated=Trunc('start_datetime', kind, output_field=DateTimeField())
  593. ).order_by('start_datetime'),
  594. [
  595. (start_datetime, truncate_to(start_datetime, kind)),
  596. (end_datetime, truncate_to(end_datetime, kind))
  597. ],
  598. lambda m: (m.start_datetime, m.truncated)
  599. )
  600. def test_date_kind(kind):
  601. self.assertQuerysetEqual(
  602. DTModel.objects.annotate(
  603. truncated=Trunc('start_date', kind, output_field=DateField())
  604. ).order_by('start_datetime'),
  605. [
  606. (start_datetime, truncate_to(start_datetime.date(), kind)),
  607. (end_datetime, truncate_to(end_datetime.date(), kind))
  608. ],
  609. lambda m: (m.start_datetime, m.truncated)
  610. )
  611. def test_time_kind(kind):
  612. self.assertQuerysetEqual(
  613. DTModel.objects.annotate(
  614. truncated=Trunc('start_time', kind, output_field=TimeField())
  615. ).order_by('start_datetime'),
  616. [
  617. (start_datetime, truncate_to(start_datetime.time(), kind)),
  618. (end_datetime, truncate_to(end_datetime.time(), kind))
  619. ],
  620. lambda m: (m.start_datetime, m.truncated)
  621. )
  622. test_date_kind('year')
  623. test_date_kind('quarter')
  624. test_date_kind('month')
  625. test_date_kind('week')
  626. test_date_kind('day')
  627. test_time_kind('hour')
  628. test_time_kind('minute')
  629. test_time_kind('second')
  630. test_datetime_kind('year')
  631. test_datetime_kind('quarter')
  632. test_datetime_kind('month')
  633. test_datetime_kind('week')
  634. test_datetime_kind('day')
  635. test_datetime_kind('hour')
  636. test_datetime_kind('minute')
  637. test_datetime_kind('second')
  638. qs = DTModel.objects.filter(start_datetime__date=Trunc('start_datetime', 'day', output_field=DateField()))
  639. self.assertEqual(qs.count(), 2)
  640. def test_trunc_none(self):
  641. self.create_model(None, None)
  642. for t in (Trunc('start_datetime', 'year'), Trunc('start_date', 'year'), Trunc('start_time', 'hour')):
  643. with self.subTest(t):
  644. self.assertIsNone(DTModel.objects.annotate(truncated=t).first().truncated)
  645. def test_trunc_year_func(self):
  646. start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
  647. end_datetime = truncate_to(datetime(2016, 6, 15, 14, 10, 50, 123), 'year')
  648. if settings.USE_TZ:
  649. start_datetime = timezone.make_aware(start_datetime, is_dst=False)
  650. end_datetime = timezone.make_aware(end_datetime, is_dst=False)
  651. self.create_model(start_datetime, end_datetime)
  652. self.create_model(end_datetime, start_datetime)
  653. self.assertQuerysetEqual(
  654. DTModel.objects.annotate(extracted=TruncYear('start_datetime')).order_by('start_datetime'),
  655. [
  656. (start_datetime, truncate_to(start_datetime, 'year')),
  657. (end_datetime, truncate_to(end_datetime, 'year')),
  658. ],
  659. lambda m: (m.start_datetime, m.extracted)
  660. )
  661. self.assertQuerysetEqual(
  662. DTModel.objects.annotate(extracted=TruncYear('start_date')).order_by('start_datetime'),
  663. [
  664. (start_datetime, truncate_to(start_datetime.date(), 'year')),
  665. (end_datetime, truncate_to(end_datetime.date(), 'year')),
  666. ],
  667. lambda m: (m.start_datetime, m.extracted)
  668. )
  669. self.assertEqual(DTModel.objects.filter(start_datetime=TruncYear('start_datetime')).count(), 1)
  670. with self.assertRaisesMessage(ValueError, "Cannot truncate TimeField 'start_time' to DateTimeField"):
  671. list(DTModel.objects.annotate(truncated=TruncYear('start_time')))
  672. with self.assertRaisesMessage(ValueError, "Cannot truncate TimeField 'start_time' to DateTimeField"):
  673. list(DTModel.objects.annotate(truncated=TruncYear('start_time', output_field=TimeField())))
  674. def test_trunc_quarter_func(self):
  675. start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
  676. end_datetime = truncate_to(datetime(2016, 10, 15, 14, 10, 50, 123), 'quarter')
  677. last_quarter_2015 = truncate_to(datetime(2015, 12, 31, 14, 10, 50, 123), 'quarter')
  678. first_quarter_2016 = truncate_to(datetime(2016, 1, 1, 14, 10, 50, 123), 'quarter')
  679. if settings.USE_TZ:
  680. start_datetime = timezone.make_aware(start_datetime, is_dst=False)
  681. end_datetime = timezone.make_aware(end_datetime, is_dst=False)
  682. last_quarter_2015 = timezone.make_aware(last_quarter_2015, is_dst=False)
  683. first_quarter_2016 = timezone.make_aware(first_quarter_2016, is_dst=False)
  684. self.create_model(start_datetime=start_datetime, end_datetime=end_datetime)
  685. self.create_model(start_datetime=end_datetime, end_datetime=start_datetime)
  686. self.create_model(start_datetime=last_quarter_2015, end_datetime=end_datetime)
  687. self.create_model(start_datetime=first_quarter_2016, end_datetime=end_datetime)
  688. self.assertQuerysetEqual(
  689. DTModel.objects.annotate(extracted=TruncQuarter('start_date')).order_by('start_datetime'),
  690. [
  691. (start_datetime, truncate_to(start_datetime.date(), 'quarter')),
  692. (last_quarter_2015, truncate_to(last_quarter_2015.date(), 'quarter')),
  693. (first_quarter_2016, truncate_to(first_quarter_2016.date(), 'quarter')),
  694. (end_datetime, truncate_to(end_datetime.date(), 'quarter')),
  695. ],
  696. lambda m: (m.start_datetime, m.extracted)
  697. )
  698. self.assertQuerysetEqual(
  699. DTModel.objects.annotate(extracted=TruncQuarter('start_datetime')).order_by('start_datetime'),
  700. [
  701. (start_datetime, truncate_to(start_datetime, 'quarter')),
  702. (last_quarter_2015, truncate_to(last_quarter_2015, 'quarter')),
  703. (first_quarter_2016, truncate_to(first_quarter_2016, 'quarter')),
  704. (end_datetime, truncate_to(end_datetime, 'quarter')),
  705. ],
  706. lambda m: (m.start_datetime, m.extracted)
  707. )
  708. with self.assertRaisesMessage(ValueError, "Cannot truncate TimeField 'start_time' to DateTimeField"):
  709. list(DTModel.objects.annotate(truncated=TruncQuarter('start_time')))
  710. with self.assertRaisesMessage(ValueError, "Cannot truncate TimeField 'start_time' to DateTimeField"):
  711. list(DTModel.objects.annotate(truncated=TruncQuarter('start_time', output_field=TimeField())))
  712. def test_trunc_month_func(self):
  713. start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
  714. end_datetime = truncate_to(datetime(2016, 6, 15, 14, 10, 50, 123), 'month')
  715. if settings.USE_TZ:
  716. start_datetime = timezone.make_aware(start_datetime, is_dst=False)
  717. end_datetime = timezone.make_aware(end_datetime, is_dst=False)
  718. self.create_model(start_datetime, end_datetime)
  719. self.create_model(end_datetime, start_datetime)
  720. self.assertQuerysetEqual(
  721. DTModel.objects.annotate(extracted=TruncMonth('start_datetime')).order_by('start_datetime'),
  722. [
  723. (start_datetime, truncate_to(start_datetime, 'month')),
  724. (end_datetime, truncate_to(end_datetime, 'month')),
  725. ],
  726. lambda m: (m.start_datetime, m.extracted)
  727. )
  728. self.assertQuerysetEqual(
  729. DTModel.objects.annotate(extracted=TruncMonth('start_date')).order_by('start_datetime'),
  730. [
  731. (start_datetime, truncate_to(start_datetime.date(), 'month')),
  732. (end_datetime, truncate_to(end_datetime.date(), 'month')),
  733. ],
  734. lambda m: (m.start_datetime, m.extracted)
  735. )
  736. self.assertEqual(DTModel.objects.filter(start_datetime=TruncMonth('start_datetime')).count(), 1)
  737. with self.assertRaisesMessage(ValueError, "Cannot truncate TimeField 'start_time' to DateTimeField"):
  738. list(DTModel.objects.annotate(truncated=TruncMonth('start_time')))
  739. with self.assertRaisesMessage(ValueError, "Cannot truncate TimeField 'start_time' to DateTimeField"):
  740. list(DTModel.objects.annotate(truncated=TruncMonth('start_time', output_field=TimeField())))
  741. def test_trunc_week_func(self):
  742. start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
  743. end_datetime = truncate_to(datetime(2016, 6, 15, 14, 10, 50, 123), 'week')
  744. if settings.USE_TZ:
  745. start_datetime = timezone.make_aware(start_datetime, is_dst=False)
  746. end_datetime = timezone.make_aware(end_datetime, is_dst=False)
  747. self.create_model(start_datetime, end_datetime)
  748. self.create_model(end_datetime, start_datetime)
  749. self.assertQuerysetEqual(
  750. DTModel.objects.annotate(extracted=TruncWeek('start_datetime')).order_by('start_datetime'),
  751. [
  752. (start_datetime, truncate_to(start_datetime, 'week')),
  753. (end_datetime, truncate_to(end_datetime, 'week')),
  754. ],
  755. lambda m: (m.start_datetime, m.extracted)
  756. )
  757. self.assertEqual(DTModel.objects.filter(start_datetime=TruncWeek('start_datetime')).count(), 1)
  758. with self.assertRaisesMessage(ValueError, "Cannot truncate TimeField 'start_time' to DateTimeField"):
  759. list(DTModel.objects.annotate(truncated=TruncWeek('start_time')))
  760. with self.assertRaisesMessage(ValueError, "Cannot truncate TimeField 'start_time' to DateTimeField"):
  761. list(DTModel.objects.annotate(truncated=TruncWeek('start_time', output_field=TimeField())))
  762. def test_trunc_date_func(self):
  763. start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
  764. end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
  765. if settings.USE_TZ:
  766. start_datetime = timezone.make_aware(start_datetime, is_dst=False)
  767. end_datetime = timezone.make_aware(end_datetime, is_dst=False)
  768. self.create_model(start_datetime, end_datetime)
  769. self.create_model(end_datetime, start_datetime)
  770. self.assertQuerysetEqual(
  771. DTModel.objects.annotate(extracted=TruncDate('start_datetime')).order_by('start_datetime'),
  772. [
  773. (start_datetime, start_datetime.date()),
  774. (end_datetime, end_datetime.date()),
  775. ],
  776. lambda m: (m.start_datetime, m.extracted)
  777. )
  778. self.assertEqual(DTModel.objects.filter(start_datetime__date=TruncDate('start_datetime')).count(), 2)
  779. with self.assertRaisesMessage(ValueError, "Cannot truncate TimeField 'start_time' to DateField"):
  780. list(DTModel.objects.annotate(truncated=TruncDate('start_time')))
  781. with self.assertRaisesMessage(ValueError, "Cannot truncate TimeField 'start_time' to DateField"):
  782. list(DTModel.objects.annotate(truncated=TruncDate('start_time', output_field=TimeField())))
  783. def test_trunc_date_none(self):
  784. self.create_model(None, None)
  785. self.assertIsNone(DTModel.objects.annotate(truncated=TruncDate('start_datetime')).first().truncated)
  786. def test_trunc_time_func(self):
  787. start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
  788. end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
  789. if settings.USE_TZ:
  790. start_datetime = timezone.make_aware(start_datetime, is_dst=False)
  791. end_datetime = timezone.make_aware(end_datetime, is_dst=False)
  792. self.create_model(start_datetime, end_datetime)
  793. self.create_model(end_datetime, start_datetime)
  794. self.assertQuerysetEqual(
  795. DTModel.objects.annotate(extracted=TruncTime('start_datetime')).order_by('start_datetime'),
  796. [
  797. (start_datetime, start_datetime.time()),
  798. (end_datetime, end_datetime.time()),
  799. ],
  800. lambda m: (m.start_datetime, m.extracted)
  801. )
  802. self.assertEqual(DTModel.objects.filter(start_datetime__time=TruncTime('start_datetime')).count(), 2)
  803. with self.assertRaisesMessage(ValueError, "Cannot truncate DateField 'start_date' to TimeField"):
  804. list(DTModel.objects.annotate(truncated=TruncTime('start_date')))
  805. with self.assertRaisesMessage(ValueError, "Cannot truncate DateField 'start_date' to TimeField"):
  806. list(DTModel.objects.annotate(truncated=TruncTime('start_date', output_field=DateField())))
  807. def test_trunc_time_none(self):
  808. self.create_model(None, None)
  809. self.assertIsNone(DTModel.objects.annotate(truncated=TruncTime('start_datetime')).first().truncated)
  810. def test_trunc_day_func(self):
  811. start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
  812. end_datetime = truncate_to(datetime(2016, 6, 15, 14, 10, 50, 123), 'day')
  813. if settings.USE_TZ:
  814. start_datetime = timezone.make_aware(start_datetime, is_dst=False)
  815. end_datetime = timezone.make_aware(end_datetime, is_dst=False)
  816. self.create_model(start_datetime, end_datetime)
  817. self.create_model(end_datetime, start_datetime)
  818. self.assertQuerysetEqual(
  819. DTModel.objects.annotate(extracted=TruncDay('start_datetime')).order_by('start_datetime'),
  820. [
  821. (start_datetime, truncate_to(start_datetime, 'day')),
  822. (end_datetime, truncate_to(end_datetime, 'day')),
  823. ],
  824. lambda m: (m.start_datetime, m.extracted)
  825. )
  826. self.assertEqual(DTModel.objects.filter(start_datetime=TruncDay('start_datetime')).count(), 1)
  827. with self.assertRaisesMessage(ValueError, "Cannot truncate TimeField 'start_time' to DateTimeField"):
  828. list(DTModel.objects.annotate(truncated=TruncDay('start_time')))
  829. with self.assertRaisesMessage(ValueError, "Cannot truncate TimeField 'start_time' to DateTimeField"):
  830. list(DTModel.objects.annotate(truncated=TruncDay('start_time', output_field=TimeField())))
  831. def test_trunc_hour_func(self):
  832. start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
  833. end_datetime = truncate_to(datetime(2016, 6, 15, 14, 10, 50, 123), 'hour')
  834. if settings.USE_TZ:
  835. start_datetime = timezone.make_aware(start_datetime, is_dst=False)
  836. end_datetime = timezone.make_aware(end_datetime, is_dst=False)
  837. self.create_model(start_datetime, end_datetime)
  838. self.create_model(end_datetime, start_datetime)
  839. self.assertQuerysetEqual(
  840. DTModel.objects.annotate(extracted=TruncHour('start_datetime')).order_by('start_datetime'),
  841. [
  842. (start_datetime, truncate_to(start_datetime, 'hour')),
  843. (end_datetime, truncate_to(end_datetime, 'hour')),
  844. ],
  845. lambda m: (m.start_datetime, m.extracted)
  846. )
  847. self.assertQuerysetEqual(
  848. DTModel.objects.annotate(extracted=TruncHour('start_time')).order_by('start_datetime'),
  849. [
  850. (start_datetime, truncate_to(start_datetime.time(), 'hour')),
  851. (end_datetime, truncate_to(end_datetime.time(), 'hour')),
  852. ],
  853. lambda m: (m.start_datetime, m.extracted)
  854. )
  855. self.assertEqual(DTModel.objects.filter(start_datetime=TruncHour('start_datetime')).count(), 1)
  856. with self.assertRaisesMessage(ValueError, "Cannot truncate DateField 'start_date' to DateTimeField"):
  857. list(DTModel.objects.annotate(truncated=TruncHour('start_date')))
  858. with self.assertRaisesMessage(ValueError, "Cannot truncate DateField 'start_date' to DateTimeField"):
  859. list(DTModel.objects.annotate(truncated=TruncHour('start_date', output_field=DateField())))
  860. def test_trunc_minute_func(self):
  861. start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
  862. end_datetime = truncate_to(datetime(2016, 6, 15, 14, 10, 50, 123), 'minute')
  863. if settings.USE_TZ:
  864. start_datetime = timezone.make_aware(start_datetime, is_dst=False)
  865. end_datetime = timezone.make_aware(end_datetime, is_dst=False)
  866. self.create_model(start_datetime, end_datetime)
  867. self.create_model(end_datetime, start_datetime)
  868. self.assertQuerysetEqual(
  869. DTModel.objects.annotate(extracted=TruncMinute('start_datetime')).order_by('start_datetime'),
  870. [
  871. (start_datetime, truncate_to(start_datetime, 'minute')),
  872. (end_datetime, truncate_to(end_datetime, 'minute')),
  873. ],
  874. lambda m: (m.start_datetime, m.extracted)
  875. )
  876. self.assertQuerysetEqual(
  877. DTModel.objects.annotate(extracted=TruncMinute('start_time')).order_by('start_datetime'),
  878. [
  879. (start_datetime, truncate_to(start_datetime.time(), 'minute')),
  880. (end_datetime, truncate_to(end_datetime.time(), 'minute')),
  881. ],
  882. lambda m: (m.start_datetime, m.extracted)
  883. )
  884. self.assertEqual(DTModel.objects.filter(start_datetime=TruncMinute('start_datetime')).count(), 1)
  885. with self.assertRaisesMessage(ValueError, "Cannot truncate DateField 'start_date' to DateTimeField"):
  886. list(DTModel.objects.annotate(truncated=TruncMinute('start_date')))
  887. with self.assertRaisesMessage(ValueError, "Cannot truncate DateField 'start_date' to DateTimeField"):
  888. list(DTModel.objects.annotate(truncated=TruncMinute('start_date', output_field=DateField())))
  889. def test_trunc_second_func(self):
  890. start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
  891. end_datetime = truncate_to(datetime(2016, 6, 15, 14, 10, 50, 123), 'second')
  892. if settings.USE_TZ:
  893. start_datetime = timezone.make_aware(start_datetime, is_dst=False)
  894. end_datetime = timezone.make_aware(end_datetime, is_dst=False)
  895. self.create_model(start_datetime, end_datetime)
  896. self.create_model(end_datetime, start_datetime)
  897. self.assertQuerysetEqual(
  898. DTModel.objects.annotate(extracted=TruncSecond('start_datetime')).order_by('start_datetime'),
  899. [
  900. (start_datetime, truncate_to(start_datetime, 'second')),
  901. (end_datetime, truncate_to(end_datetime, 'second'))
  902. ],
  903. lambda m: (m.start_datetime, m.extracted)
  904. )
  905. self.assertQuerysetEqual(
  906. DTModel.objects.annotate(extracted=TruncSecond('start_time')).order_by('start_datetime'),
  907. [
  908. (start_datetime, truncate_to(start_datetime.time(), 'second')),
  909. (end_datetime, truncate_to(end_datetime.time(), 'second'))
  910. ],
  911. lambda m: (m.start_datetime, m.extracted)
  912. )
  913. self.assertEqual(DTModel.objects.filter(start_datetime=TruncSecond('start_datetime')).count(), 1)
  914. with self.assertRaisesMessage(ValueError, "Cannot truncate DateField 'start_date' to DateTimeField"):
  915. list(DTModel.objects.annotate(truncated=TruncSecond('start_date')))
  916. with self.assertRaisesMessage(ValueError, "Cannot truncate DateField 'start_date' to DateTimeField"):
  917. list(DTModel.objects.annotate(truncated=TruncSecond('start_date', output_field=DateField())))
  918. def test_trunc_subquery_with_parameters(self):
  919. author_1 = Author.objects.create(name='J. R. R. Tolkien')
  920. author_2 = Author.objects.create(name='G. R. R. Martin')
  921. fan_since_1 = datetime(2016, 2, 3, 15, 0, 0)
  922. fan_since_2 = datetime(2015, 2, 3, 15, 0, 0)
  923. fan_since_3 = datetime(2017, 2, 3, 15, 0, 0)
  924. if settings.USE_TZ:
  925. fan_since_1 = timezone.make_aware(fan_since_1, is_dst=False)
  926. fan_since_2 = timezone.make_aware(fan_since_2, is_dst=False)
  927. fan_since_3 = timezone.make_aware(fan_since_3, is_dst=False)
  928. Fan.objects.create(author=author_1, name='Tom', fan_since=fan_since_1)
  929. Fan.objects.create(author=author_1, name='Emma', fan_since=fan_since_2)
  930. Fan.objects.create(author=author_2, name='Isabella', fan_since=fan_since_3)
  931. inner = Fan.objects.filter(
  932. author=OuterRef('pk'),
  933. name__in=('Emma', 'Isabella', 'Tom')
  934. ).values('author').annotate(newest_fan=Max('fan_since')).values('newest_fan')
  935. outer = Author.objects.annotate(
  936. newest_fan_year=TruncYear(Subquery(inner, output_field=DateTimeField()))
  937. )
  938. tz = pytz.UTC if settings.USE_TZ else None
  939. self.assertSequenceEqual(
  940. outer.order_by('name').values('name', 'newest_fan_year'),
  941. [
  942. {'name': 'G. R. R. Martin', 'newest_fan_year': datetime(2017, 1, 1, 0, 0, tzinfo=tz)},
  943. {'name': 'J. R. R. Tolkien', 'newest_fan_year': datetime(2016, 1, 1, 0, 0, tzinfo=tz)},
  944. ]
  945. )
  946. @override_settings(USE_TZ=True, TIME_ZONE='UTC')
  947. class DateFunctionWithTimeZoneTests(DateFunctionTests):
  948. def test_extract_func_with_timezone(self):
  949. start_datetime = datetime(2015, 6, 15, 23, 30, 1, 321)
  950. end_datetime = datetime(2015, 6, 16, 13, 11, 27, 123)
  951. start_datetime = timezone.make_aware(start_datetime, is_dst=False)
  952. end_datetime = timezone.make_aware(end_datetime, is_dst=False)
  953. self.create_model(start_datetime, end_datetime)
  954. melb = pytz.timezone('Australia/Melbourne')
  955. delta_tzinfo_pos = datetime_timezone(timedelta(hours=5))
  956. delta_tzinfo_neg = datetime_timezone(timedelta(hours=-5, minutes=17))
  957. qs = DTModel.objects.annotate(
  958. day=Extract('start_datetime', 'day'),
  959. day_melb=Extract('start_datetime', 'day', tzinfo=melb),
  960. week=Extract('start_datetime', 'week', tzinfo=melb),
  961. isoyear=ExtractIsoYear('start_datetime', tzinfo=melb),
  962. weekday=ExtractWeekDay('start_datetime'),
  963. weekday_melb=ExtractWeekDay('start_datetime', tzinfo=melb),
  964. isoweekday=ExtractIsoWeekDay('start_datetime'),
  965. isoweekday_melb=ExtractIsoWeekDay('start_datetime', tzinfo=melb),
  966. quarter=ExtractQuarter('start_datetime', tzinfo=melb),
  967. hour=ExtractHour('start_datetime'),
  968. hour_melb=ExtractHour('start_datetime', tzinfo=melb),
  969. hour_with_delta_pos=ExtractHour('start_datetime', tzinfo=delta_tzinfo_pos),
  970. hour_with_delta_neg=ExtractHour('start_datetime', tzinfo=delta_tzinfo_neg),
  971. minute_with_delta_neg=ExtractMinute('start_datetime', tzinfo=delta_tzinfo_neg),
  972. ).order_by('start_datetime')
  973. utc_model = qs.get()
  974. self.assertEqual(utc_model.day, 15)
  975. self.assertEqual(utc_model.day_melb, 16)
  976. self.assertEqual(utc_model.week, 25)
  977. self.assertEqual(utc_model.isoyear, 2015)
  978. self.assertEqual(utc_model.weekday, 2)
  979. self.assertEqual(utc_model.weekday_melb, 3)
  980. self.assertEqual(utc_model.isoweekday, 1)
  981. self.assertEqual(utc_model.isoweekday_melb, 2)
  982. self.assertEqual(utc_model.quarter, 2)
  983. self.assertEqual(utc_model.hour, 23)
  984. self.assertEqual(utc_model.hour_melb, 9)
  985. self.assertEqual(utc_model.hour_with_delta_pos, 4)
  986. self.assertEqual(utc_model.hour_with_delta_neg, 18)
  987. self.assertEqual(utc_model.minute_with_delta_neg, 47)
  988. with timezone.override(melb):
  989. melb_model = qs.get()
  990. self.assertEqual(melb_model.day, 16)
  991. self.assertEqual(melb_model.day_melb, 16)
  992. self.assertEqual(melb_model.week, 25)
  993. self.assertEqual(melb_model.isoyear, 2015)
  994. self.assertEqual(melb_model.weekday, 3)
  995. self.assertEqual(melb_model.isoweekday, 2)
  996. self.assertEqual(melb_model.quarter, 2)
  997. self.assertEqual(melb_model.weekday_melb, 3)
  998. self.assertEqual(melb_model.isoweekday_melb, 2)
  999. self.assertEqual(melb_model.hour, 9)
  1000. self.assertEqual(melb_model.hour_melb, 9)
  1001. def test_extract_func_explicit_timezone_priority(self):
  1002. start_datetime = datetime(2015, 6, 15, 23, 30, 1, 321)
  1003. end_datetime = datetime(2015, 6, 16, 13, 11, 27, 123)
  1004. start_datetime = timezone.make_aware(start_datetime, is_dst=False)
  1005. end_datetime = timezone.make_aware(end_datetime, is_dst=False)
  1006. self.create_model(start_datetime, end_datetime)
  1007. melb = pytz.timezone('Australia/Melbourne')
  1008. with timezone.override(melb):
  1009. model = DTModel.objects.annotate(
  1010. day_melb=Extract('start_datetime', 'day'),
  1011. day_utc=Extract('start_datetime', 'day', tzinfo=timezone.utc),
  1012. ).order_by('start_datetime').get()
  1013. self.assertEqual(model.day_melb, 16)
  1014. self.assertEqual(model.day_utc, 15)
  1015. def test_trunc_timezone_applied_before_truncation(self):
  1016. start_datetime = datetime(2016, 1, 1, 1, 30, 50, 321)
  1017. end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
  1018. start_datetime = timezone.make_aware(start_datetime, is_dst=False)
  1019. end_datetime = timezone.make_aware(end_datetime, is_dst=False)
  1020. self.create_model(start_datetime, end_datetime)
  1021. melb = pytz.timezone('Australia/Melbourne')
  1022. pacific = pytz.timezone('US/Pacific')
  1023. model = DTModel.objects.annotate(
  1024. melb_year=TruncYear('start_datetime', tzinfo=melb),
  1025. pacific_year=TruncYear('start_datetime', tzinfo=pacific),
  1026. ).order_by('start_datetime').get()
  1027. self.assertEqual(model.start_datetime, start_datetime)
  1028. self.assertEqual(model.melb_year, truncate_to(start_datetime, 'year', melb))
  1029. self.assertEqual(model.pacific_year, truncate_to(start_datetime, 'year', pacific))
  1030. self.assertEqual(model.start_datetime.year, 2016)
  1031. self.assertEqual(model.melb_year.year, 2016)
  1032. self.assertEqual(model.pacific_year.year, 2015)
  1033. def test_trunc_ambiguous_and_invalid_times(self):
  1034. sao = pytz.timezone('America/Sao_Paulo')
  1035. utc = pytz.timezone('UTC')
  1036. start_datetime = utc.localize(datetime(2016, 10, 16, 13))
  1037. end_datetime = utc.localize(datetime(2016, 2, 21, 1))
  1038. self.create_model(start_datetime, end_datetime)
  1039. with timezone.override(sao):
  1040. with self.assertRaisesMessage(pytz.NonExistentTimeError, '2016-10-16 00:00:00'):
  1041. model = DTModel.objects.annotate(truncated_start=TruncDay('start_datetime')).get()
  1042. with self.assertRaisesMessage(pytz.AmbiguousTimeError, '2016-02-20 23:00:00'):
  1043. model = DTModel.objects.annotate(truncated_end=TruncHour('end_datetime')).get()
  1044. model = DTModel.objects.annotate(
  1045. truncated_start=TruncDay('start_datetime', is_dst=False),
  1046. truncated_end=TruncHour('end_datetime', is_dst=False),
  1047. ).get()
  1048. self.assertEqual(model.truncated_start.dst(), timedelta(0))
  1049. self.assertEqual(model.truncated_end.dst(), timedelta(0))
  1050. model = DTModel.objects.annotate(
  1051. truncated_start=TruncDay('start_datetime', is_dst=True),
  1052. truncated_end=TruncHour('end_datetime', is_dst=True),
  1053. ).get()
  1054. self.assertEqual(model.truncated_start.dst(), timedelta(0, 3600))
  1055. self.assertEqual(model.truncated_end.dst(), timedelta(0, 3600))
  1056. def test_trunc_func_with_timezone(self):
  1057. """
  1058. If the truncated datetime transitions to a different offset (daylight
  1059. saving) then the returned value will have that new timezone/offset.
  1060. """
  1061. start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
  1062. end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
  1063. start_datetime = timezone.make_aware(start_datetime, is_dst=False)
  1064. end_datetime = timezone.make_aware(end_datetime, is_dst=False)
  1065. self.create_model(start_datetime, end_datetime)
  1066. self.create_model(end_datetime, start_datetime)
  1067. melb = pytz.timezone('Australia/Melbourne')
  1068. def test_datetime_kind(kind):
  1069. self.assertQuerysetEqual(
  1070. DTModel.objects.annotate(
  1071. truncated=Trunc('start_datetime', kind, output_field=DateTimeField(), tzinfo=melb)
  1072. ).order_by('start_datetime'),
  1073. [
  1074. (start_datetime, truncate_to(start_datetime.astimezone(melb), kind, melb)),
  1075. (end_datetime, truncate_to(end_datetime.astimezone(melb), kind, melb))
  1076. ],
  1077. lambda m: (m.start_datetime, m.truncated)
  1078. )
  1079. def test_date_kind(kind):
  1080. self.assertQuerysetEqual(
  1081. DTModel.objects.annotate(
  1082. truncated=Trunc('start_date', kind, output_field=DateField(), tzinfo=melb)
  1083. ).order_by('start_datetime'),
  1084. [
  1085. (start_datetime, truncate_to(start_datetime.date(), kind)),
  1086. (end_datetime, truncate_to(end_datetime.date(), kind))
  1087. ],
  1088. lambda m: (m.start_datetime, m.truncated)
  1089. )
  1090. def test_time_kind(kind):
  1091. self.assertQuerysetEqual(
  1092. DTModel.objects.annotate(
  1093. truncated=Trunc('start_time', kind, output_field=TimeField(), tzinfo=melb)
  1094. ).order_by('start_datetime'),
  1095. [
  1096. (start_datetime, truncate_to(start_datetime.time(), kind)),
  1097. (end_datetime, truncate_to(end_datetime.time(), kind))
  1098. ],
  1099. lambda m: (m.start_datetime, m.truncated)
  1100. )
  1101. test_date_kind('year')
  1102. test_date_kind('quarter')
  1103. test_date_kind('month')
  1104. test_date_kind('week')
  1105. test_date_kind('day')
  1106. test_time_kind('hour')
  1107. test_time_kind('minute')
  1108. test_time_kind('second')
  1109. test_datetime_kind('year')
  1110. test_datetime_kind('quarter')
  1111. test_datetime_kind('month')
  1112. test_datetime_kind('week')
  1113. test_datetime_kind('day')
  1114. test_datetime_kind('hour')
  1115. test_datetime_kind('minute')
  1116. test_datetime_kind('second')
  1117. qs = DTModel.objects.filter(start_datetime__date=Trunc('start_datetime', 'day', output_field=DateField()))
  1118. self.assertEqual(qs.count(), 2)