datetime.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. from datetime import datetime
  2. from django.conf import settings
  3. from django.db.models.expressions import Func
  4. from django.db.models.fields import (
  5. DateField,
  6. DateTimeField,
  7. DurationField,
  8. Field,
  9. IntegerField,
  10. TimeField,
  11. )
  12. from django.db.models.lookups import (
  13. Transform,
  14. YearExact,
  15. YearGt,
  16. YearGte,
  17. YearLt,
  18. YearLte,
  19. )
  20. from django.utils import timezone
  21. class TimezoneMixin:
  22. tzinfo = None
  23. def get_tzname(self):
  24. # Timezone conversions must happen to the input datetime *before*
  25. # applying a function. 2015-12-31 23:00:00 -02:00 is stored in the
  26. # database as 2016-01-01 01:00:00 +00:00. Any results should be
  27. # based on the input datetime not the stored datetime.
  28. tzname = None
  29. if settings.USE_TZ:
  30. if self.tzinfo is None:
  31. tzname = timezone.get_current_timezone_name()
  32. else:
  33. tzname = timezone._get_timezone_name(self.tzinfo)
  34. return tzname
  35. class Extract(TimezoneMixin, Transform):
  36. lookup_name = None
  37. output_field = IntegerField()
  38. def __init__(self, expression, lookup_name=None, tzinfo=None, **extra):
  39. if self.lookup_name is None:
  40. self.lookup_name = lookup_name
  41. if self.lookup_name is None:
  42. raise ValueError("lookup_name must be provided")
  43. self.tzinfo = tzinfo
  44. super().__init__(expression, **extra)
  45. def as_sql(self, compiler, connection):
  46. sql, params = compiler.compile(self.lhs)
  47. lhs_output_field = self.lhs.output_field
  48. if isinstance(lhs_output_field, DateTimeField):
  49. tzname = self.get_tzname()
  50. sql, params = connection.ops.datetime_extract_sql(
  51. self.lookup_name, sql, tuple(params), tzname
  52. )
  53. elif self.tzinfo is not None:
  54. raise ValueError("tzinfo can only be used with DateTimeField.")
  55. elif isinstance(lhs_output_field, DateField):
  56. sql, params = connection.ops.date_extract_sql(
  57. self.lookup_name, sql, tuple(params)
  58. )
  59. elif isinstance(lhs_output_field, TimeField):
  60. sql, params = connection.ops.time_extract_sql(
  61. self.lookup_name, sql, tuple(params)
  62. )
  63. elif isinstance(lhs_output_field, DurationField):
  64. if not connection.features.has_native_duration_field:
  65. raise ValueError(
  66. "Extract requires native DurationField database support."
  67. )
  68. sql, params = connection.ops.time_extract_sql(
  69. self.lookup_name, sql, tuple(params)
  70. )
  71. else:
  72. # resolve_expression has already validated the output_field so this
  73. # assert should never be hit.
  74. assert False, "Tried to Extract from an invalid type."
  75. return sql, params
  76. def resolve_expression(
  77. self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False
  78. ):
  79. copy = super().resolve_expression(
  80. query, allow_joins, reuse, summarize, for_save
  81. )
  82. field = getattr(copy.lhs, "output_field", None)
  83. if field is None:
  84. return copy
  85. if not isinstance(field, (DateField, DateTimeField, TimeField, DurationField)):
  86. raise ValueError(
  87. "Extract input expression must be DateField, DateTimeField, "
  88. "TimeField, or DurationField."
  89. )
  90. # Passing dates to functions expecting datetimes is most likely a mistake.
  91. if type(field) is DateField and copy.lookup_name in (
  92. "hour",
  93. "minute",
  94. "second",
  95. ):
  96. raise ValueError(
  97. "Cannot extract time component '%s' from DateField '%s'."
  98. % (copy.lookup_name, field.name)
  99. )
  100. if isinstance(field, DurationField) and copy.lookup_name in (
  101. "year",
  102. "iso_year",
  103. "month",
  104. "week",
  105. "week_day",
  106. "iso_week_day",
  107. "quarter",
  108. ):
  109. raise ValueError(
  110. "Cannot extract component '%s' from DurationField '%s'."
  111. % (copy.lookup_name, field.name)
  112. )
  113. return copy
  114. class ExtractYear(Extract):
  115. lookup_name = "year"
  116. class ExtractIsoYear(Extract):
  117. """Return the ISO-8601 week-numbering year."""
  118. lookup_name = "iso_year"
  119. class ExtractMonth(Extract):
  120. lookup_name = "month"
  121. class ExtractDay(Extract):
  122. lookup_name = "day"
  123. class ExtractWeek(Extract):
  124. """
  125. Return 1-52 or 53, based on ISO-8601, i.e., Monday is the first of the
  126. week.
  127. """
  128. lookup_name = "week"
  129. class ExtractWeekDay(Extract):
  130. """
  131. Return Sunday=1 through Saturday=7.
  132. To replicate this in Python: (mydatetime.isoweekday() % 7) + 1
  133. """
  134. lookup_name = "week_day"
  135. class ExtractIsoWeekDay(Extract):
  136. """Return Monday=1 through Sunday=7, based on ISO-8601."""
  137. lookup_name = "iso_week_day"
  138. class ExtractQuarter(Extract):
  139. lookup_name = "quarter"
  140. class ExtractHour(Extract):
  141. lookup_name = "hour"
  142. class ExtractMinute(Extract):
  143. lookup_name = "minute"
  144. class ExtractSecond(Extract):
  145. lookup_name = "second"
  146. DateField.register_lookup(ExtractYear)
  147. DateField.register_lookup(ExtractMonth)
  148. DateField.register_lookup(ExtractDay)
  149. DateField.register_lookup(ExtractWeekDay)
  150. DateField.register_lookup(ExtractIsoWeekDay)
  151. DateField.register_lookup(ExtractWeek)
  152. DateField.register_lookup(ExtractIsoYear)
  153. DateField.register_lookup(ExtractQuarter)
  154. TimeField.register_lookup(ExtractHour)
  155. TimeField.register_lookup(ExtractMinute)
  156. TimeField.register_lookup(ExtractSecond)
  157. DateTimeField.register_lookup(ExtractHour)
  158. DateTimeField.register_lookup(ExtractMinute)
  159. DateTimeField.register_lookup(ExtractSecond)
  160. ExtractYear.register_lookup(YearExact)
  161. ExtractYear.register_lookup(YearGt)
  162. ExtractYear.register_lookup(YearGte)
  163. ExtractYear.register_lookup(YearLt)
  164. ExtractYear.register_lookup(YearLte)
  165. ExtractIsoYear.register_lookup(YearExact)
  166. ExtractIsoYear.register_lookup(YearGt)
  167. ExtractIsoYear.register_lookup(YearGte)
  168. ExtractIsoYear.register_lookup(YearLt)
  169. ExtractIsoYear.register_lookup(YearLte)
  170. class Now(Func):
  171. template = "CURRENT_TIMESTAMP"
  172. output_field = DateTimeField()
  173. def as_postgresql(self, compiler, connection, **extra_context):
  174. # PostgreSQL's CURRENT_TIMESTAMP means "the time at the start of the
  175. # transaction". Use STATEMENT_TIMESTAMP to be cross-compatible with
  176. # other databases.
  177. return self.as_sql(
  178. compiler, connection, template="STATEMENT_TIMESTAMP()", **extra_context
  179. )
  180. def as_mysql(self, compiler, connection, **extra_context):
  181. return self.as_sql(
  182. compiler, connection, template="CURRENT_TIMESTAMP(6)", **extra_context
  183. )
  184. def as_sqlite(self, compiler, connection, **extra_context):
  185. return self.as_sql(
  186. compiler,
  187. connection,
  188. template="STRFTIME('%%%%Y-%%%%m-%%%%d %%%%H:%%%%M:%%%%f', 'NOW')",
  189. **extra_context,
  190. )
  191. def as_oracle(self, compiler, connection, **extra_context):
  192. return self.as_sql(
  193. compiler, connection, template="LOCALTIMESTAMP", **extra_context
  194. )
  195. class TruncBase(TimezoneMixin, Transform):
  196. kind = None
  197. tzinfo = None
  198. def __init__(
  199. self,
  200. expression,
  201. output_field=None,
  202. tzinfo=None,
  203. **extra,
  204. ):
  205. self.tzinfo = tzinfo
  206. super().__init__(expression, output_field=output_field, **extra)
  207. def as_sql(self, compiler, connection):
  208. sql, params = compiler.compile(self.lhs)
  209. tzname = None
  210. if isinstance(self.lhs.output_field, DateTimeField):
  211. tzname = self.get_tzname()
  212. elif self.tzinfo is not None:
  213. raise ValueError("tzinfo can only be used with DateTimeField.")
  214. if isinstance(self.output_field, DateTimeField):
  215. sql, params = connection.ops.datetime_trunc_sql(
  216. self.kind, sql, tuple(params), tzname
  217. )
  218. elif isinstance(self.output_field, DateField):
  219. sql, params = connection.ops.date_trunc_sql(
  220. self.kind, sql, tuple(params), tzname
  221. )
  222. elif isinstance(self.output_field, TimeField):
  223. sql, params = connection.ops.time_trunc_sql(
  224. self.kind, sql, tuple(params), tzname
  225. )
  226. else:
  227. raise ValueError(
  228. "Trunc only valid on DateField, TimeField, or DateTimeField."
  229. )
  230. return sql, params
  231. def resolve_expression(
  232. self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False
  233. ):
  234. copy = super().resolve_expression(
  235. query, allow_joins, reuse, summarize, for_save
  236. )
  237. field = copy.lhs.output_field
  238. # DateTimeField is a subclass of DateField so this works for both.
  239. if not isinstance(field, (DateField, TimeField)):
  240. raise TypeError(
  241. "%r isn't a DateField, TimeField, or DateTimeField." % field.name
  242. )
  243. # If self.output_field was None, then accessing the field will trigger
  244. # the resolver to assign it to self.lhs.output_field.
  245. if not isinstance(copy.output_field, (DateField, DateTimeField, TimeField)):
  246. raise ValueError(
  247. "output_field must be either DateField, TimeField, or DateTimeField"
  248. )
  249. # Passing dates or times to functions expecting datetimes is most
  250. # likely a mistake.
  251. class_output_field = (
  252. self.__class__.output_field
  253. if isinstance(self.__class__.output_field, Field)
  254. else None
  255. )
  256. output_field = class_output_field or copy.output_field
  257. has_explicit_output_field = (
  258. class_output_field or field.__class__ is not copy.output_field.__class__
  259. )
  260. if type(field) is DateField and (
  261. isinstance(output_field, DateTimeField)
  262. or copy.kind in ("hour", "minute", "second", "time")
  263. ):
  264. raise ValueError(
  265. "Cannot truncate DateField '%s' to %s."
  266. % (
  267. field.name,
  268. output_field.__class__.__name__
  269. if has_explicit_output_field
  270. else "DateTimeField",
  271. )
  272. )
  273. elif isinstance(field, TimeField) and (
  274. isinstance(output_field, DateTimeField)
  275. or copy.kind in ("year", "quarter", "month", "week", "day", "date")
  276. ):
  277. raise ValueError(
  278. "Cannot truncate TimeField '%s' to %s."
  279. % (
  280. field.name,
  281. output_field.__class__.__name__
  282. if has_explicit_output_field
  283. else "DateTimeField",
  284. )
  285. )
  286. return copy
  287. def convert_value(self, value, expression, connection):
  288. if isinstance(self.output_field, DateTimeField):
  289. if not settings.USE_TZ:
  290. pass
  291. elif value is not None:
  292. value = value.replace(tzinfo=None)
  293. value = timezone.make_aware(value, self.tzinfo)
  294. elif not connection.features.has_zoneinfo_database:
  295. raise ValueError(
  296. "Database returned an invalid datetime value. Are time "
  297. "zone definitions for your database installed?"
  298. )
  299. elif isinstance(value, datetime):
  300. if value is None:
  301. pass
  302. elif isinstance(self.output_field, DateField):
  303. value = value.date()
  304. elif isinstance(self.output_field, TimeField):
  305. value = value.time()
  306. return value
  307. class Trunc(TruncBase):
  308. def __init__(
  309. self,
  310. expression,
  311. kind,
  312. output_field=None,
  313. tzinfo=None,
  314. **extra,
  315. ):
  316. self.kind = kind
  317. super().__init__(expression, output_field=output_field, tzinfo=tzinfo, **extra)
  318. class TruncYear(TruncBase):
  319. kind = "year"
  320. class TruncQuarter(TruncBase):
  321. kind = "quarter"
  322. class TruncMonth(TruncBase):
  323. kind = "month"
  324. class TruncWeek(TruncBase):
  325. """Truncate to midnight on the Monday of the week."""
  326. kind = "week"
  327. class TruncDay(TruncBase):
  328. kind = "day"
  329. class TruncDate(TruncBase):
  330. kind = "date"
  331. lookup_name = "date"
  332. output_field = DateField()
  333. def as_sql(self, compiler, connection):
  334. # Cast to date rather than truncate to date.
  335. sql, params = compiler.compile(self.lhs)
  336. tzname = self.get_tzname()
  337. return connection.ops.datetime_cast_date_sql(sql, tuple(params), tzname)
  338. class TruncTime(TruncBase):
  339. kind = "time"
  340. lookup_name = "time"
  341. output_field = TimeField()
  342. def as_sql(self, compiler, connection):
  343. # Cast to time rather than truncate to time.
  344. sql, params = compiler.compile(self.lhs)
  345. tzname = self.get_tzname()
  346. return connection.ops.datetime_cast_time_sql(sql, tuple(params), tzname)
  347. class TruncHour(TruncBase):
  348. kind = "hour"
  349. class TruncMinute(TruncBase):
  350. kind = "minute"
  351. class TruncSecond(TruncBase):
  352. kind = "second"
  353. DateTimeField.register_lookup(TruncDate)
  354. DateTimeField.register_lookup(TruncTime)