19
19
import os
20
20
import sys
21
21
import typing as t
22
+ from datetime import date , time
22
23
from functools import partial
23
24
from pathlib import Path
24
25
from types import MethodType
32
33
from django .db .models import (
33
34
CharField ,
34
35
DateField ,
36
+ DateTimeField ,
35
37
DecimalField ,
36
38
Field ,
37
39
FileField ,
44
46
Model ,
45
47
Q ,
46
48
TextField ,
49
+ TimeField ,
47
50
UUIDField ,
48
51
)
49
52
from django .db .models .query import QuerySet
@@ -77,6 +80,8 @@ class ModelObjectCompleter:
77
80
- `FilePathField <https://docs.djangoproject.com/en/stable/ref/models/fields/#filepathfield>`_
78
81
- `TextField <https://docs.djangoproject.com/en/stable/ref/models/fields/#textfield>`_
79
82
- `DateField <https://docs.djangoproject.com/en/stable/ref/models/fields/#datefield>`_ **(Must use ISO 8601: YYYY-MM-DD)**
83
+ - `TimeField <https://docs.djangoproject.com/en/stable/ref/models/fields/#timefield>`_ **(Must use ISO 8601: HH:MM:SS.ssssss)**
84
+ - `DateTimeField <https://docs.djangoproject.com/en/stable/ref/models/fields/#datetimefield>`_ **(Must use ISO 8601: YYYY-MM-DDTHH:MM:SS.ssssss±HH:MM)**
80
85
- `UUIDField <https://docs.djangoproject.com/en/stable/ref/models/fields/#uuidfield>`_
81
86
- `FloatField <https://docs.djangoproject.com/en/stable/ref/models/fields/#floatfield>`_
82
87
- `DecimalField <https://docs.djangoproject.com/en/stable/ref/models/fields/#decimalfield>`_
@@ -158,6 +163,14 @@ def queryset(self) -> t.Union[QuerySet, Manager[Model]]:
158
163
return self ._queryset or self .model_cls .objects
159
164
160
165
def to_str (self , obj : t .Any ) -> str :
166
+ from datetime import datetime
167
+
168
+ if isinstance (obj , datetime ):
169
+ return obj .isoformat ()
170
+ elif isinstance (obj , time ):
171
+ return obj .isoformat ()
172
+ elif isinstance (obj , date ):
173
+ return obj .isoformat ()
161
174
return str (obj )
162
175
163
176
def int_query (self , context : Context , parameter : Parameter , incomplete : str ) -> Q :
@@ -280,21 +293,14 @@ def uuid_query(self, context: Context, parameter: Parameter, incomplete: str) ->
280
293
** {f"{ self .lookup_field } __lte" : max_uuid }
281
294
)
282
295
283
- def date_query (self , context : Context , parameter : Parameter , incomplete : str ) -> Q :
296
+ def _get_date_bounds (self , incomplete : str ) -> t . Tuple [ date , date ] :
284
297
"""
285
- Default completion query builder for date fields. This method will return a Q object that
286
- will match any value that starts with the incomplete date string. All dates must be in
287
- ISO8601 format (YYYY-MM-DD).
298
+ Turn an incomplete YYYY-MM-DD date string into upper and lower bound date objects.
288
299
289
- :param context: The click context.
290
- :param parameter: The click parameter.
291
- :param incomplete: The incomplete string.
292
- :return: A Q object to use for filtering the queryset.
293
- :raises ValueError: If the incomplete string is not a valid partial date.
294
- :raises AssertionError: If the incomplete string is not a valid partial date.
300
+ :param incomplete: The incomplete time string.
301
+ :return: A 2-tuple of (lower, upper) date object boundaries.
295
302
"""
296
303
import calendar
297
- from datetime import date
298
304
299
305
parts = incomplete .split ("-" )
300
306
year_low = max (int (parts [0 ] + "0" * (4 - len (parts [0 ]))), 1 )
@@ -320,6 +326,127 @@ def date_query(self, context: Context, parameter: Parameter, incomplete: str) ->
320
326
month = month_high ,
321
327
day = day_high or calendar .monthrange (year_high , month_high )[1 ],
322
328
)
329
+ return lower_bound , upper_bound
330
+
331
+ def _get_time_bounds (self , incomplete : str ) -> t .Tuple [time , time ]:
332
+ """
333
+ Turn an incomplete HH::MM::SS.ssssss time string into upper and lower bound time
334
+ objects.
335
+
336
+ :param incomplete: The incomplete time string.
337
+ :return: A 2-tuple of (lower, upper) time object boundaries.
338
+ """
339
+ time_parts = incomplete .split (":" )
340
+ if len (time_parts ) > 0 :
341
+ hours_low = int (time_parts [0 ] + "0" * (2 - len (time_parts [0 ])))
342
+ hours_high = min (int (time_parts [0 ] + "9" * (2 - len (time_parts [0 ]))), 23 )
343
+ minutes_low = 0
344
+ minutes_high = 59
345
+ seconds_low = 0
346
+ seconds_high = 59
347
+ microseconds_low = 0
348
+ microseconds_high = 999999
349
+ if len (time_parts ) > 1 :
350
+ assert len (time_parts [0 ]) > 1 # Hours must be 2 digits
351
+ minutes_low = int (time_parts [1 ] + "0" * (2 - len (time_parts [1 ])))
352
+ minutes_high = min (
353
+ int (time_parts [1 ] + "9" * (2 - len (time_parts [1 ]))), 59
354
+ )
355
+ if len (time_parts ) > 2 :
356
+ seconds_parts = time_parts [2 ].split ("." )
357
+ int_seconds = seconds_parts [0 ]
358
+ assert len (time_parts [1 ]) > 1 # Minutes must be 2 digits
359
+ seconds_low = int (int_seconds + "0" * (2 - len (int_seconds )))
360
+ seconds_high = min (
361
+ int (int_seconds + "9" * (2 - len (int_seconds ))), 59
362
+ )
363
+ if len (seconds_parts ) > 1 :
364
+ microseconds = seconds_parts [1 ]
365
+ microseconds_low = int (
366
+ microseconds + "0" * (6 - len (microseconds ))
367
+ )
368
+ microseconds_high = int (
369
+ microseconds + "9" * (6 - len (microseconds ))
370
+ )
371
+ return time (
372
+ hour = hours_low ,
373
+ minute = minutes_low ,
374
+ second = seconds_low ,
375
+ microsecond = microseconds_low ,
376
+ ), time (
377
+ hour = hours_high ,
378
+ minute = minutes_high ,
379
+ second = seconds_high ,
380
+ microsecond = microseconds_high ,
381
+ )
382
+ return time .min , time .max
383
+
384
+ def date_query (self , context : Context , parameter : Parameter , incomplete : str ) -> Q :
385
+ """
386
+ Default completion query builder for date fields. This method will return a Q object that
387
+ will match any value that starts with the incomplete date string. All dates must be in
388
+ ISO8601 format (YYYY-MM-DD).
389
+
390
+ :param context: The click context.
391
+ :param parameter: The click parameter.
392
+ :param incomplete: The incomplete string.
393
+ :return: A Q object to use for filtering the queryset.
394
+ :raises ValueError: If the incomplete string is not a valid partial date.
395
+ :raises AssertionError: If the incomplete string is not a valid partial date.
396
+ """
397
+ lower_bound , upper_bound = self ._get_date_bounds (incomplete )
398
+ return Q (** {f"{ self .lookup_field } __gte" : lower_bound }) & Q (
399
+ ** {f"{ self .lookup_field } __lte" : upper_bound }
400
+ )
401
+
402
+ def time_query (self , context : Context , parameter : Parameter , incomplete : str ) -> Q :
403
+ """
404
+ Default completion query builder for time fields. This method will return a Q object that
405
+ will match any value that starts with the incomplete time string. All times must be in
406
+ ISO 8601 format (HH:MM:SS.ssssss).
407
+
408
+ :param context: The click context.
409
+ :param parameter: The click parameter.
410
+ :param incomplete: The incomplete string.
411
+ :return: A Q object to use for filtering the queryset.
412
+ :raises ValueError: If the incomplete string is not a valid partial time.
413
+ :raises AssertionError: If the incomplete string is not a valid partial time.
414
+ """
415
+ lower_bound , upper_bound = self ._get_time_bounds (incomplete )
416
+ return Q (** {f"{ self .lookup_field } __gte" : lower_bound }) & Q (
417
+ ** {f"{ self .lookup_field } __lte" : upper_bound }
418
+ )
419
+
420
+ def datetime_query (
421
+ self , context : Context , parameter : Parameter , incomplete : str
422
+ ) -> Q :
423
+ """
424
+ Default completion query builder for datetime fields. This method will return a Q object that
425
+ will match any value that starts with the incomplete datetime string. All dates must be in
426
+ ISO8601 format (YYYY-MM-DDTHH:MM:SS.ssssss±HH:MM).
427
+
428
+ :param context: The click context.
429
+ :param parameter: The click parameter.
430
+ :param incomplete: The incomplete string.
431
+ :return: A Q object to use for filtering the queryset.
432
+ :raises ValueError: If the incomplete string is not a valid partial datetime.
433
+ :raises AssertionError: If the incomplete string is not a valid partial datetime.
434
+ """
435
+ import re
436
+ from datetime import datetime
437
+
438
+ parts = incomplete .split ("T" )
439
+ lower_bound , upper_bound = self ._get_date_bounds (parts [0 ])
440
+
441
+ time_lower = datetime .min .time ()
442
+ time_upper = datetime .max .time ()
443
+ if len (parts ) > 1 :
444
+ time_parts = re .split (r"[+-]" , parts [1 ])
445
+ time_lower , time_upper = self ._get_time_bounds (time_parts [0 ])
446
+ # if len(time_parts) > 1:
447
+ # TODO - handle timezone??
448
+ lower_bound = datetime .combine (lower_bound , time_lower )
449
+ upper_bound = datetime .combine (upper_bound , time_upper )
323
450
return Q (** {f"{ self .lookup_field } __gte" : lower_bound }) & Q (
324
451
** {f"{ self .lookup_field } __lte" : upper_bound }
325
452
)
@@ -370,8 +497,12 @@ def __init__(
370
497
self .query = self .uuid_query
371
498
elif isinstance (self ._field , (FloatField , DecimalField )):
372
499
self .query = self .float_query
500
+ elif isinstance (self ._field , DateTimeField ):
501
+ self .query = self .datetime_query
373
502
elif isinstance (self ._field , DateField ):
374
503
self .query = self .date_query
504
+ elif isinstance (self ._field , TimeField ):
505
+ self .query = self .time_query
375
506
else :
376
507
raise ValueError (
377
508
_ ("Unsupported lookup field class: {cls}" ).format (
0 commit comments