Skip to content

Commit bad591f

Browse files
committed
fix #14 fix #160
1 parent a3cfdd3 commit bad591f

File tree

7 files changed

+265
-9
lines changed

7 files changed

+265
-9
lines changed

django_typer/completers.py

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,12 @@
1616

1717
# pylint: disable=line-too-long
1818

19-
import inspect
2019
import os
21-
import pkgutil
2220
import sys
2321
import typing as t
2422
from functools import partial
2523
from pathlib import Path
2624
from types import MethodType
27-
from uuid import UUID
2825

2926
from click import Context, Parameter
3027
from click.core import ParameterSource
@@ -34,6 +31,7 @@
3431
from django.core.management import get_commands
3532
from django.db.models import (
3633
CharField,
34+
DateField,
3735
DecimalField,
3836
Field,
3937
FileField,
@@ -78,6 +76,7 @@ class ModelObjectCompleter:
7876
- `ImageField <https://docs.djangoproject.com/en/stable/ref/models/fields/#imagefield>`_
7977
- `FilePathField <https://docs.djangoproject.com/en/stable/ref/models/fields/#filepathfield>`_
8078
- `TextField <https://docs.djangoproject.com/en/stable/ref/models/fields/#textfield>`_
79+
- `DateField <https://docs.djangoproject.com/en/stable/ref/models/fields/#datefield>`_ **(Must use ISO 8601 format (YYYY-MM-DD)**
8180
- `UUIDField <https://docs.djangoproject.com/en/stable/ref/models/fields/#uuidfield>`_
8281
- `FloatField <https://docs.djangoproject.com/en/stable/ref/models/fields/#floatfield>`_
8382
- `DecimalField <https://docs.djangoproject.com/en/stable/ref/models/fields/#decimalfield>`_
@@ -247,6 +246,7 @@ def uuid_query(self, context: Context, parameter: Parameter, incomplete: str) ->
247246
:raises ValueError: If the incomplete string is too long or contains invalid
248247
UUID characters. Anything other than (0-9a-fA-F).
249248
"""
249+
from uuid import UUID
250250

251251
# the offset futzing is to allow users to ignore the - in the UUID
252252
# as a convenience of its implementation any non-alpha numeric character
@@ -280,6 +280,50 @@ def uuid_query(self, context: Context, parameter: Parameter, incomplete: str) ->
280280
**{f"{self.lookup_field}__lte": max_uuid}
281281
)
282282

283+
def date_query(self, context: Context, parameter: Parameter, incomplete: str) -> Q:
284+
"""
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).
288+
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.
295+
"""
296+
import calendar
297+
from datetime import date
298+
299+
parts = incomplete.split("-")
300+
year_low = max(int(parts[0] + "0" * (4 - len(parts[0]))), 1)
301+
year_high = int(parts[0] + "9" * (4 - len(parts[0])))
302+
month_high = 12
303+
month_low = 1
304+
day_low = 1
305+
day_high = None
306+
if len(parts) > 1:
307+
assert len(parts[0]) > 3, _("Year must be 4 digits")
308+
month_high = min(int(parts[1] + "9" * (2 - len(parts[1]))), 12)
309+
month_low = max(int(parts[1] + "0" * (2 - len(parts[1]))), 1)
310+
if len(parts) > 2:
311+
assert len(parts[1]) > 1, _("Month must be 2 digits")
312+
day_low = max(int(parts[2] + "0" * (2 - len(parts[2]))), 1)
313+
day_high = min(
314+
int(parts[2] + "9" * (2 - len(parts[2]))),
315+
calendar.monthrange(year_high, month_high)[1],
316+
)
317+
lower_bound = date(year=year_low, month=month_low, day=day_low)
318+
upper_bound = date(
319+
year=year_high,
320+
month=month_high,
321+
day=day_high or calendar.monthrange(year_high, month_high)[1],
322+
)
323+
return Q(**{f"{self.lookup_field}__gte": lower_bound}) & Q(
324+
**{f"{self.lookup_field}__lte": upper_bound}
325+
)
326+
283327
def __init__(
284328
self,
285329
model_or_qry: t.Union[t.Type[Model], QuerySet],
@@ -290,6 +334,8 @@ def __init__(
290334
case_insensitive: bool = case_insensitive,
291335
distinct: bool = distinct,
292336
):
337+
import inspect
338+
293339
if inspect.isclass(model_or_qry) and issubclass(model_or_qry, Model):
294340
self.model_cls = model_or_qry
295341
elif isinstance(model_or_qry, QuerySet):
@@ -324,6 +370,8 @@ def __init__(
324370
self.query = self.uuid_query
325371
elif isinstance(self._field, (FloatField, DecimalField)):
326372
self.query = self.float_query
373+
elif isinstance(self._field, DateField):
374+
self.query = self.date_query
327375
else:
328376
raise ValueError(
329377
_("Unsupported lookup field class: {cls}").format(
@@ -348,14 +396,14 @@ def __call__(
348396
:return: A list of CompletionItem objects.
349397
"""
350398

351-
completion_qry = Q()
399+
completion_qry = Q(**{self.lookup_field + "__isnull": False})
352400

353401
if incomplete:
354402
try:
355403
completion_qry &= self.query( # pylint: disable=not-callable
356404
context, parameter, incomplete
357405
)
358-
except (ValueError, TypeError):
406+
except (ValueError, TypeError, AssertionError):
359407
return []
360408

361409
excluded: t.List[t.Type[Model]] = []
@@ -454,6 +502,8 @@ def complete_import_path(
454502
:param incomplete: The incomplete string.
455503
:return: A list of available matching import paths
456504
"""
505+
import pkgutil
506+
457507
incomplete = incomplete.strip()
458508
completions = []
459509
packages = [pkg for pkg in incomplete.split(".") if pkg]

django_typer/parsers.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"""
2424

2525
import typing as t
26+
from datetime import date
2627
from uuid import UUID
2728

2829
from click import Context, Parameter, ParamType
@@ -96,6 +97,10 @@ def _get_metavar(self) -> str:
9697
return "UUID"
9798
elif isinstance(self._field, (models.FloatField, models.DecimalField)):
9899
return "FLOAT"
100+
elif isinstance(self._field, (models.FileField, models.FilePathField)):
101+
return "PATH"
102+
elif isinstance(self._field, models.DateField):
103+
return "YYYY-MM-DD"
99104
return "TXT"
100105

101106
def __init__(
@@ -141,12 +146,14 @@ def convert(
141146
try:
142147
if isinstance(value, self.model_cls):
143148
return value
144-
if isinstance(self._field, models.UUIDField):
149+
elif isinstance(self._field, models.UUIDField):
145150
uuid = ""
146151
for char in value:
147152
if char.isalnum():
148153
uuid += char
149154
value = UUID(uuid)
155+
elif isinstance(self._field, models.DateField):
156+
value = date.fromisoformat(value)
150157
return self.model_cls.objects.get(
151158
**{f"{self.lookup_field}{self._lookup}": value}
152159
)

doc/source/changelog.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Change Log
77
v3.0.0 (202X-XX-XX)
88
===================
99

10+
* Fixed `Model objects with null lookup fields should not be included in model field completion output <https://github.com/django-commons/django-typer/issues/160>`_
1011
* Implemented `Add security scans to CI. <https://github.com/django-commons/django-typer/issues/158>`_
1112
* Implemented `Add a performance regression. <https://github.com/django-commons/django-typer/issues/157>`_
1213
* Implemented `Use in-house shell completer classes. <https://github.com/django-commons/django-typer/issues/156>`_
@@ -25,6 +26,8 @@ v3.0.0 (202X-XX-XX)
2526
* Fixed `Installed shellcompletion scripts do not pass values of --settings or --pythonpath <https://github.com/django-commons/django-typer/issues/68>`_
2627
* Fixed `shellcompletion complete should print to the command's stdout. <https://github.com/django-commons/django-typer/issues/19>`_
2728
* Implemented `Add completer/parser for FileField and FilePathField <https://github.com/django-commons/django-typer/issues/17>`_
29+
* Implemented `Add completer/parser for DateField <https://github.com/django-commons/django-typer/issues/14>`_
30+
2831

2932
Migrating from 2.x to 3.x
3033
-------------------------

tests/apps/test_app/management/commands/model_fields.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,13 @@ def test(
163163
help=t.cast(str, _("Fetch objects by their file path fields.")),
164164
),
165165
] = None,
166+
date: Annotated[
167+
t.Optional[ShellCompleteTester],
168+
typer.Option(
169+
**model_parser_completer(ShellCompleteTester, "date_field"),
170+
help=t.cast(str, _("Fetch objects by their date fields.")),
171+
),
172+
] = None,
166173
):
167174
assert self.__class__ is Command
168175
objects = {}
@@ -204,6 +211,9 @@ def test(
204211
assert isinstance(file, ShellCompleteTester)
205212
objects["file"] = {file.id: str(file.file_field)}
206213
if file_path is not None:
207-
assert isinstance(file, ShellCompleteTester)
214+
assert isinstance(file_path, ShellCompleteTester)
208215
objects["file_path"] = {file_path.id: str(file_path.file_path_field)}
216+
if date is not None:
217+
assert isinstance(date, ShellCompleteTester)
218+
objects["date"] = {date.id: str(date.date_field)}
209219
return json.dumps(objects)

tests/apps/test_app/migrations/0001_initial.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Generated by Django 4.2.18 on 2025-01-17 02:41
1+
# Generated by Django 4.2.18 on 2025-01-17 05:46
22

33
from django.db import migrations, models
44

@@ -65,6 +65,10 @@ class Migration(migrations.Migration):
6565
"file_path_field",
6666
models.FilePathField(db_index=True, default=None, null=True),
6767
),
68+
(
69+
"date_field",
70+
models.DateField(db_index=True, default=None, null=True),
71+
),
6872
],
6973
),
7074
]

tests/apps/test_app/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,5 @@ class ShellCompleteTester(models.Model):
2323
)
2424

2525
file_path_field = models.FilePathField(null=True, default=None, db_index=True)
26+
27+
date_field = models.DateField(null=True, default=None, db_index=True)

0 commit comments

Comments
 (0)