django-display-ids 0.1.4__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_display_ids-0.3.0/PKG-INFO +130 -0
- django_display_ids-0.3.0/README.md +107 -0
- {django_display_ids-0.1.4 → django_display_ids-0.3.0}/pyproject.toml +28 -6
- {django_display_ids-0.1.4 → django_display_ids-0.3.0}/src/django_display_ids/__init__.py +22 -4
- {django_display_ids-0.1.4 → django_display_ids-0.3.0}/src/django_display_ids/admin.py +8 -6
- django_display_ids-0.3.0/src/django_display_ids/apps.py +11 -0
- {django_display_ids-0.1.4 → django_display_ids-0.3.0}/src/django_display_ids/conf.py +5 -2
- {django_display_ids-0.1.4 → django_display_ids-0.3.0}/src/django_display_ids/contrib/drf_spectacular/__init__.py +10 -3
- {django_display_ids-0.1.4 → django_display_ids-0.3.0}/src/django_display_ids/contrib/rest_framework/serializers.py +4 -3
- {django_display_ids-0.1.4 → django_display_ids-0.3.0}/src/django_display_ids/contrib/rest_framework/views.py +1 -1
- django_display_ids-0.3.0/src/django_display_ids/converters.py +111 -0
- {django_display_ids-0.1.4 → django_display_ids-0.3.0}/src/django_display_ids/examples.py +1 -1
- {django_display_ids-0.1.4 → django_display_ids-0.3.0}/src/django_display_ids/managers.py +88 -6
- {django_display_ids-0.1.4 → django_display_ids-0.3.0}/src/django_display_ids/models.py +3 -3
- {django_display_ids-0.1.4 → django_display_ids-0.3.0}/src/django_display_ids/resolver.py +2 -2
- django_display_ids-0.3.0/src/django_display_ids/templatetags/__init__.py +0 -0
- django_display_ids-0.3.0/src/django_display_ids/templatetags/display_ids.py +48 -0
- {django_display_ids-0.1.4 → django_display_ids-0.3.0}/src/django_display_ids/views.py +1 -1
- django_display_ids-0.1.4/PKG-INFO +0 -422
- django_display_ids-0.1.4/README.md +0 -400
- {django_display_ids-0.1.4 → django_display_ids-0.3.0}/src/django_display_ids/contrib/__init__.py +0 -0
- {django_display_ids-0.1.4 → django_display_ids-0.3.0}/src/django_display_ids/contrib/rest_framework/__init__.py +0 -0
- {django_display_ids-0.1.4 → django_display_ids-0.3.0}/src/django_display_ids/encoding.py +0 -0
- {django_display_ids-0.1.4 → django_display_ids-0.3.0}/src/django_display_ids/exceptions.py +0 -0
- {django_display_ids-0.1.4 → django_display_ids-0.3.0}/src/django_display_ids/py.typed +0 -0
- {django_display_ids-0.1.4 → django_display_ids-0.3.0}/src/django_display_ids/strategies.py +0 -0
- {django_display_ids-0.1.4 → django_display_ids-0.3.0}/src/django_display_ids/typing.py +0 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: django-display-ids
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Stripe-like prefixed IDs for Django. Works with existing UUIDs — no schema changes.
|
|
5
|
+
Keywords: django,stripe,uuid,base62,prefixed-id,drf,shortuuid,nanoid,ulid
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Framework :: Django
|
|
9
|
+
Classifier: Framework :: Django :: 4.2
|
|
10
|
+
Classifier: Framework :: Django :: 5.2
|
|
11
|
+
Classifier: Framework :: Django :: 6.0
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Dist: django>=4.2
|
|
19
|
+
Requires-Python: >=3.12
|
|
20
|
+
Project-URL: Documentation, https://django-display-ids.readthedocs.io/
|
|
21
|
+
Project-URL: Repository, https://github.com/josephabrahams/django-display-ids
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# django-display-ids
|
|
25
|
+
|
|
26
|
+
[](https://pypi.org/project/django-display-ids/)
|
|
27
|
+
[](https://pypi.org/project/django-display-ids/)
|
|
28
|
+
[](https://pypi.org/project/django-display-ids/)
|
|
29
|
+
[](https://github.com/josephabrahams/django-display-ids/actions/workflows/ci.yml)
|
|
30
|
+
[](https://codecov.io/gh/josephabrahams/django-display-ids)
|
|
31
|
+
[](https://django-display-ids.readthedocs.io/)
|
|
32
|
+
[](https://github.com/josephabrahams/django-display-ids/blob/main/LICENSE)
|
|
33
|
+
|
|
34
|
+
Stripe-like prefixed IDs for Django. Works with existing UUIDs — no schema changes.
|
|
35
|
+
|
|
36
|
+
## Why?
|
|
37
|
+
|
|
38
|
+
UUIDv7 (native in Python 3.14+) offers excellent database performance with time-ordered indexing. But they lack context — seeing `550e8400-e29b-41d4-a716-446655440000` in a URL or log doesn't tell you what kind of object it refers to.
|
|
39
|
+
|
|
40
|
+
Display IDs like `inv_2aUyqjCzEIiEcYMKj7TZtw` are more useful: the prefix identifies the object type at a glance, and they're compact and URL-safe. But storing display IDs in the database is far less efficient than native UUIDs.
|
|
41
|
+
|
|
42
|
+
Different consumers have different needs:
|
|
43
|
+
- **Humans** prefer slugs (`my-invoice`) or display IDs (`inv_xxx`)
|
|
44
|
+
- **APIs and integrations** work well with UUIDs
|
|
45
|
+
|
|
46
|
+
This library gives you the best of both worlds: accept any format in your URLs and API endpoints, then translate to an efficient UUID lookup in the database. Store UUIDs, expose whatever format your users need.
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install django-display-ids
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Add to `INSTALLED_APPS`:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
INSTALLED_APPS = [
|
|
58
|
+
# ...
|
|
59
|
+
"django_display_ids",
|
|
60
|
+
]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Quick Start
|
|
64
|
+
|
|
65
|
+
**Django views:**
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from django.views.generic import DetailView
|
|
69
|
+
from django_display_ids import DisplayIDObjectMixin
|
|
70
|
+
|
|
71
|
+
class InvoiceDetailView(DisplayIDObjectMixin, DetailView):
|
|
72
|
+
model = Invoice
|
|
73
|
+
lookup_param = "id"
|
|
74
|
+
lookup_strategies = ("display_id", "uuid", "slug")
|
|
75
|
+
display_id_prefix = "inv"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Django REST Framework:**
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from rest_framework.viewsets import ModelViewSet
|
|
82
|
+
from django_display_ids.contrib.rest_framework import DisplayIDLookupMixin
|
|
83
|
+
|
|
84
|
+
class InvoiceViewSet(DisplayIDLookupMixin, ModelViewSet):
|
|
85
|
+
queryset = Invoice.objects.all()
|
|
86
|
+
serializer_class = InvoiceSerializer
|
|
87
|
+
lookup_url_kwarg = "id"
|
|
88
|
+
lookup_strategies = ("display_id", "uuid", "slug")
|
|
89
|
+
display_id_prefix = "inv"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Now your views accept:
|
|
93
|
+
- `inv_2aUyqjCzEIiEcYMKj7TZtw` (display ID)
|
|
94
|
+
- `550e8400-e29b-41d4-a716-446655440000` (UUID)
|
|
95
|
+
- `my-invoice-slug` (slug)
|
|
96
|
+
|
|
97
|
+
**Templates:**
|
|
98
|
+
|
|
99
|
+
```django
|
|
100
|
+
{% load display_ids %}
|
|
101
|
+
|
|
102
|
+
{{ invoice.display_id }} {# inv_2aUyqjCzEIiEcYMKj7TZtw #}
|
|
103
|
+
{{ order.customer_id|display_id:"cust" }} {# encode any UUID #}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Features
|
|
107
|
+
|
|
108
|
+
- **Multiple identifier formats**: display ID (`prefix_base62uuid`), UUID (v4/v7), slug
|
|
109
|
+
- **Framework support**: Django CBVs and Django REST Framework
|
|
110
|
+
- **Template filter**: Encode UUIDs as display IDs in templates
|
|
111
|
+
- **Zero model changes required**: Works with any existing UUID field
|
|
112
|
+
- **OpenAPI integration**: Automatic schema generation with drf-spectacular
|
|
113
|
+
|
|
114
|
+
## Documentation
|
|
115
|
+
|
|
116
|
+
Full documentation at [django-display-ids.readthedocs.io](https://django-display-ids.readthedocs.io/).
|
|
117
|
+
|
|
118
|
+
## Contributing
|
|
119
|
+
|
|
120
|
+
See the [contributing guide](https://django-display-ids.readthedocs.io/en/latest/contributing.html).
|
|
121
|
+
|
|
122
|
+
## Related Projects
|
|
123
|
+
|
|
124
|
+
If you need ID generation and storage (custom model fields), consider:
|
|
125
|
+
|
|
126
|
+
- **[django-prefix-id](https://github.com/jaddison/django-prefix-id)** — PrefixIDField that generates and stores base62-encoded UUIDs
|
|
127
|
+
- **[django-spicy-id](https://github.com/mik3y/django-spicy-id)** — Drop-in AutoField replacement
|
|
128
|
+
- **[django-charid-field](https://github.com/yunojuno/django-charid-field)** — CharField wrapper supporting cuid, ksuid, ulid
|
|
129
|
+
|
|
130
|
+
**django-display-ids** works with existing UUID fields and handles resolution only — no migrations required.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# django-display-ids
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/django-display-ids/)
|
|
4
|
+
[](https://pypi.org/project/django-display-ids/)
|
|
5
|
+
[](https://pypi.org/project/django-display-ids/)
|
|
6
|
+
[](https://github.com/josephabrahams/django-display-ids/actions/workflows/ci.yml)
|
|
7
|
+
[](https://codecov.io/gh/josephabrahams/django-display-ids)
|
|
8
|
+
[](https://django-display-ids.readthedocs.io/)
|
|
9
|
+
[](https://github.com/josephabrahams/django-display-ids/blob/main/LICENSE)
|
|
10
|
+
|
|
11
|
+
Stripe-like prefixed IDs for Django. Works with existing UUIDs — no schema changes.
|
|
12
|
+
|
|
13
|
+
## Why?
|
|
14
|
+
|
|
15
|
+
UUIDv7 (native in Python 3.14+) offers excellent database performance with time-ordered indexing. But they lack context — seeing `550e8400-e29b-41d4-a716-446655440000` in a URL or log doesn't tell you what kind of object it refers to.
|
|
16
|
+
|
|
17
|
+
Display IDs like `inv_2aUyqjCzEIiEcYMKj7TZtw` are more useful: the prefix identifies the object type at a glance, and they're compact and URL-safe. But storing display IDs in the database is far less efficient than native UUIDs.
|
|
18
|
+
|
|
19
|
+
Different consumers have different needs:
|
|
20
|
+
- **Humans** prefer slugs (`my-invoice`) or display IDs (`inv_xxx`)
|
|
21
|
+
- **APIs and integrations** work well with UUIDs
|
|
22
|
+
|
|
23
|
+
This library gives you the best of both worlds: accept any format in your URLs and API endpoints, then translate to an efficient UUID lookup in the database. Store UUIDs, expose whatever format your users need.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install django-display-ids
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Add to `INSTALLED_APPS`:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
INSTALLED_APPS = [
|
|
35
|
+
# ...
|
|
36
|
+
"django_display_ids",
|
|
37
|
+
]
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
**Django views:**
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from django.views.generic import DetailView
|
|
46
|
+
from django_display_ids import DisplayIDObjectMixin
|
|
47
|
+
|
|
48
|
+
class InvoiceDetailView(DisplayIDObjectMixin, DetailView):
|
|
49
|
+
model = Invoice
|
|
50
|
+
lookup_param = "id"
|
|
51
|
+
lookup_strategies = ("display_id", "uuid", "slug")
|
|
52
|
+
display_id_prefix = "inv"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Django REST Framework:**
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from rest_framework.viewsets import ModelViewSet
|
|
59
|
+
from django_display_ids.contrib.rest_framework import DisplayIDLookupMixin
|
|
60
|
+
|
|
61
|
+
class InvoiceViewSet(DisplayIDLookupMixin, ModelViewSet):
|
|
62
|
+
queryset = Invoice.objects.all()
|
|
63
|
+
serializer_class = InvoiceSerializer
|
|
64
|
+
lookup_url_kwarg = "id"
|
|
65
|
+
lookup_strategies = ("display_id", "uuid", "slug")
|
|
66
|
+
display_id_prefix = "inv"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Now your views accept:
|
|
70
|
+
- `inv_2aUyqjCzEIiEcYMKj7TZtw` (display ID)
|
|
71
|
+
- `550e8400-e29b-41d4-a716-446655440000` (UUID)
|
|
72
|
+
- `my-invoice-slug` (slug)
|
|
73
|
+
|
|
74
|
+
**Templates:**
|
|
75
|
+
|
|
76
|
+
```django
|
|
77
|
+
{% load display_ids %}
|
|
78
|
+
|
|
79
|
+
{{ invoice.display_id }} {# inv_2aUyqjCzEIiEcYMKj7TZtw #}
|
|
80
|
+
{{ order.customer_id|display_id:"cust" }} {# encode any UUID #}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Features
|
|
84
|
+
|
|
85
|
+
- **Multiple identifier formats**: display ID (`prefix_base62uuid`), UUID (v4/v7), slug
|
|
86
|
+
- **Framework support**: Django CBVs and Django REST Framework
|
|
87
|
+
- **Template filter**: Encode UUIDs as display IDs in templates
|
|
88
|
+
- **Zero model changes required**: Works with any existing UUID field
|
|
89
|
+
- **OpenAPI integration**: Automatic schema generation with drf-spectacular
|
|
90
|
+
|
|
91
|
+
## Documentation
|
|
92
|
+
|
|
93
|
+
Full documentation at [django-display-ids.readthedocs.io](https://django-display-ids.readthedocs.io/).
|
|
94
|
+
|
|
95
|
+
## Contributing
|
|
96
|
+
|
|
97
|
+
See the [contributing guide](https://django-display-ids.readthedocs.io/en/latest/contributing.html).
|
|
98
|
+
|
|
99
|
+
## Related Projects
|
|
100
|
+
|
|
101
|
+
If you need ID generation and storage (custom model fields), consider:
|
|
102
|
+
|
|
103
|
+
- **[django-prefix-id](https://github.com/jaddison/django-prefix-id)** — PrefixIDField that generates and stores base62-encoded UUIDs
|
|
104
|
+
- **[django-spicy-id](https://github.com/mik3y/django-spicy-id)** — Drop-in AutoField replacement
|
|
105
|
+
- **[django-charid-field](https://github.com/yunojuno/django-charid-field)** — CharField wrapper supporting cuid, ksuid, ulid
|
|
106
|
+
|
|
107
|
+
**django-display-ids** works with existing UUID fields and handles resolution only — no migrations required.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "django-display-ids"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "Stripe-like prefixed IDs for Django. Works with existing UUIDs — no schema changes."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
7
|
-
license = { text = "
|
|
7
|
+
license = { text = "MIT" }
|
|
8
8
|
keywords = [
|
|
9
9
|
"django",
|
|
10
10
|
"stripe",
|
|
@@ -22,7 +22,7 @@ classifiers = [
|
|
|
22
22
|
"Framework :: Django :: 4.2",
|
|
23
23
|
"Framework :: Django :: 5.2",
|
|
24
24
|
"Framework :: Django :: 6.0",
|
|
25
|
-
"License :: OSI Approved ::
|
|
25
|
+
"License :: OSI Approved :: MIT License",
|
|
26
26
|
"Programming Language :: Python :: 3",
|
|
27
27
|
"Programming Language :: Python :: 3.12",
|
|
28
28
|
"Programming Language :: Python :: 3.13",
|
|
@@ -34,7 +34,8 @@ dependencies = [
|
|
|
34
34
|
]
|
|
35
35
|
|
|
36
36
|
[project.urls]
|
|
37
|
-
|
|
37
|
+
Documentation = "https://django-display-ids.readthedocs.io/"
|
|
38
|
+
Repository = "https://github.com/josephabrahams/django-display-ids"
|
|
38
39
|
|
|
39
40
|
[dependency-groups]
|
|
40
41
|
dev = [
|
|
@@ -47,14 +48,32 @@ dev = [
|
|
|
47
48
|
"nox>=2024.0",
|
|
48
49
|
"ruff>=0.8",
|
|
49
50
|
"pre-commit>=4.0",
|
|
51
|
+
"mypy>=1.13",
|
|
52
|
+
"django-stubs>=5.1",
|
|
53
|
+
"djangorestframework-stubs>=3.15",
|
|
54
|
+
"sphinx-autobuild",
|
|
55
|
+
]
|
|
56
|
+
docs = [
|
|
57
|
+
"sphinx>=7.0",
|
|
58
|
+
"furo",
|
|
59
|
+
"sphinx-copybutton",
|
|
50
60
|
]
|
|
51
61
|
|
|
52
62
|
[build-system]
|
|
53
63
|
requires = ["uv_build>=0.9.26,<0.10.0"]
|
|
54
64
|
build-backend = "uv_build"
|
|
55
65
|
|
|
56
|
-
[tool.
|
|
57
|
-
|
|
66
|
+
[tool.django-stubs]
|
|
67
|
+
django_settings_module = "tests.settings"
|
|
68
|
+
|
|
69
|
+
[tool.mypy]
|
|
70
|
+
python_version = "3.12"
|
|
71
|
+
plugins = ["mypy_django_plugin.main", "mypy_drf_plugin.main"]
|
|
72
|
+
strict = true
|
|
73
|
+
|
|
74
|
+
[[tool.mypy.overrides]]
|
|
75
|
+
module = "tests.*"
|
|
76
|
+
disable_error_code = ["no-untyped-def"]
|
|
58
77
|
|
|
59
78
|
[tool.pytest.ini_options]
|
|
60
79
|
DJANGO_SETTINGS_MODULE = "tests.settings"
|
|
@@ -93,3 +112,6 @@ known-first-party = ["django_display_ids"]
|
|
|
93
112
|
[tool.ruff.format]
|
|
94
113
|
quote-style = "double"
|
|
95
114
|
indent-style = "space"
|
|
115
|
+
|
|
116
|
+
[tool.uv]
|
|
117
|
+
default-groups = ["dev", "docs"]
|
|
@@ -23,7 +23,10 @@ Example:
|
|
|
23
23
|
display_id_prefix = "inv"
|
|
24
24
|
"""
|
|
25
25
|
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
26
28
|
from .admin import DisplayIDSearchMixin
|
|
29
|
+
from .converters import DisplayIDConverter, DisplayIDOrUUIDConverter, UUIDConverter
|
|
27
30
|
from .encoding import (
|
|
28
31
|
decode_display_id,
|
|
29
32
|
decode_uuid,
|
|
@@ -45,12 +48,29 @@ from .exceptions import (
|
|
|
45
48
|
UnknownPrefixError,
|
|
46
49
|
)
|
|
47
50
|
from .managers import DisplayIDManager, DisplayIDQuerySet
|
|
48
|
-
from .models import DisplayIDMixin, get_model_for_prefix
|
|
49
51
|
from .resolver import resolve_object
|
|
50
52
|
from .typing import DEFAULT_STRATEGIES, StrategyName
|
|
51
53
|
from .views import DisplayIDObjectMixin
|
|
52
54
|
|
|
55
|
+
|
|
56
|
+
def __getattr__(name: str) -> Any:
|
|
57
|
+
"""Lazy import for model-related items to avoid app registry issues."""
|
|
58
|
+
if name == "DisplayIDMixin":
|
|
59
|
+
from .models import DisplayIDMixin
|
|
60
|
+
|
|
61
|
+
return DisplayIDMixin
|
|
62
|
+
if name == "get_model_for_prefix":
|
|
63
|
+
from .models import get_model_for_prefix
|
|
64
|
+
|
|
65
|
+
return get_model_for_prefix
|
|
66
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
67
|
+
|
|
68
|
+
|
|
53
69
|
__all__ = [ # noqa: RUF022 - keep categorized order for readability
|
|
70
|
+
# URL converters
|
|
71
|
+
"DisplayIDConverter",
|
|
72
|
+
"UUIDConverter",
|
|
73
|
+
"DisplayIDOrUUIDConverter",
|
|
54
74
|
# Encoding
|
|
55
75
|
"encode_uuid",
|
|
56
76
|
"decode_uuid",
|
|
@@ -85,10 +105,8 @@ __all__ = [ # noqa: RUF022 - keep categorized order for readability
|
|
|
85
105
|
"DEFAULT_STRATEGIES",
|
|
86
106
|
]
|
|
87
107
|
|
|
88
|
-
__version__ = "0.1.1"
|
|
89
|
-
|
|
90
108
|
|
|
91
|
-
def get_drf_mixin():
|
|
109
|
+
def get_drf_mixin() -> type:
|
|
92
110
|
"""Lazily import the DRF mixin to avoid hard dependency.
|
|
93
111
|
|
|
94
112
|
Returns:
|
|
@@ -4,12 +4,12 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import contextlib
|
|
6
6
|
import uuid
|
|
7
|
-
from typing import TYPE_CHECKING
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
8
|
|
|
9
9
|
from .encoding import decode_display_id
|
|
10
10
|
|
|
11
11
|
if TYPE_CHECKING:
|
|
12
|
-
from django.db.models import QuerySet
|
|
12
|
+
from django.db.models import Model, QuerySet
|
|
13
13
|
from django.http import HttpRequest
|
|
14
14
|
|
|
15
15
|
__all__ = ["DisplayIDSearchMixin"]
|
|
@@ -39,13 +39,15 @@ class DisplayIDSearchMixin:
|
|
|
39
39
|
"""
|
|
40
40
|
|
|
41
41
|
uuid_field: str | None = None
|
|
42
|
+
model: type[Model]
|
|
42
43
|
|
|
43
44
|
def _get_uuid_field(self) -> str:
|
|
44
45
|
"""Get the UUID field name to search."""
|
|
45
46
|
if self.uuid_field is not None:
|
|
46
47
|
return self.uuid_field
|
|
47
48
|
# Try to get from model's uuid_field attribute
|
|
48
|
-
|
|
49
|
+
uuid_field: str | None = getattr(self.model, "uuid_field", None)
|
|
50
|
+
return uuid_field or "id"
|
|
49
51
|
|
|
50
52
|
def _try_parse_uuid(self, value: str) -> uuid.UUID | None:
|
|
51
53
|
"""Try to parse a string as a UUID."""
|
|
@@ -57,16 +59,16 @@ class DisplayIDSearchMixin:
|
|
|
57
59
|
def get_search_results(
|
|
58
60
|
self,
|
|
59
61
|
request: HttpRequest,
|
|
60
|
-
queryset: QuerySet,
|
|
62
|
+
queryset: QuerySet[Any],
|
|
61
63
|
search_term: str,
|
|
62
|
-
) -> tuple[QuerySet, bool]:
|
|
64
|
+
) -> tuple[QuerySet[Any], bool]:
|
|
63
65
|
"""Extend search to handle display IDs and raw UUIDs.
|
|
64
66
|
|
|
65
67
|
Tries to match the search term as:
|
|
66
68
|
1. A display ID (prefix_base62uuid) if it contains an underscore
|
|
67
69
|
2. A raw UUID if it looks like a UUID format
|
|
68
70
|
"""
|
|
69
|
-
queryset, use_distinct = super().get_search_results(
|
|
71
|
+
queryset, use_distinct = super().get_search_results( # type: ignore[misc]
|
|
70
72
|
request, queryset, search_term
|
|
71
73
|
)
|
|
72
74
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Django app configuration for django-display-ids."""
|
|
2
|
+
|
|
3
|
+
from django.apps import AppConfig
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DjangoDisplayIdsConfig(AppConfig):
|
|
7
|
+
"""App configuration for django-display-ids."""
|
|
8
|
+
|
|
9
|
+
name = "django_display_ids"
|
|
10
|
+
verbose_name = "Django Display IDs"
|
|
11
|
+
default_auto_field = "django.db.models.BigAutoField"
|
|
@@ -45,5 +45,8 @@ def get_setting(name: str) -> str | tuple[StrategyName, ...]:
|
|
|
45
45
|
if name not in DEFAULTS:
|
|
46
46
|
raise KeyError(f"Unknown setting: {name}")
|
|
47
47
|
|
|
48
|
-
user_settings = getattr(
|
|
49
|
-
|
|
48
|
+
user_settings: dict[str, str | tuple[str, ...]] = getattr(
|
|
49
|
+
settings, "DISPLAY_IDS", {}
|
|
50
|
+
)
|
|
51
|
+
result = user_settings.get(name, DEFAULTS[name])
|
|
52
|
+
return result # type: ignore[return-value]
|
|
@@ -6,16 +6,21 @@ proper OpenAPI schema generation for DisplayIDField.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
9
11
|
try:
|
|
10
12
|
from drf_spectacular.extensions import OpenApiSerializerFieldExtension
|
|
11
13
|
except ImportError:
|
|
12
14
|
# drf-spectacular not installed, skip extension registration
|
|
13
15
|
pass
|
|
14
16
|
else:
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from drf_spectacular.openapi import AutoSchema
|
|
19
|
+
|
|
15
20
|
from django_display_ids.encoding import ENCODED_UUID_LENGTH, encode_uuid
|
|
16
21
|
from django_display_ids.examples import example_uuid_for_prefix
|
|
17
22
|
|
|
18
|
-
class DisplayIDFieldExtension(OpenApiSerializerFieldExtension):
|
|
23
|
+
class DisplayIDFieldExtension(OpenApiSerializerFieldExtension): # type: ignore[no-untyped-call]
|
|
19
24
|
"""OpenAPI schema extension for DisplayIDField.
|
|
20
25
|
|
|
21
26
|
Generates schema with correct prefix example based on the field's
|
|
@@ -27,7 +32,7 @@ else:
|
|
|
27
32
|
)
|
|
28
33
|
match_subclasses = True
|
|
29
34
|
|
|
30
|
-
def _get_model_from_view(self, auto_schema):
|
|
35
|
+
def _get_model_from_view(self, auto_schema: AutoSchema | None) -> Any:
|
|
31
36
|
"""Try to get model from the view's queryset."""
|
|
32
37
|
if auto_schema is None:
|
|
33
38
|
return None
|
|
@@ -48,7 +53,9 @@ else:
|
|
|
48
53
|
return queryset.model
|
|
49
54
|
return None
|
|
50
55
|
|
|
51
|
-
def map_serializer_field(
|
|
56
|
+
def map_serializer_field(
|
|
57
|
+
self, auto_schema: AutoSchema, direction: str
|
|
58
|
+
) -> dict[str, Any]:
|
|
52
59
|
"""Generate OpenAPI schema for DisplayIDField."""
|
|
53
60
|
# Get prefix from field override or try to get from model
|
|
54
61
|
prefix = self.target._prefix_override
|
|
@@ -95,9 +95,9 @@ class DisplayIDField(serializers.SerializerMethodField):
|
|
|
95
95
|
# If using prefix override, generate display_id with that prefix
|
|
96
96
|
if self._prefix_override is not None:
|
|
97
97
|
# Get uuid_field name from model, then fall back to settings
|
|
98
|
-
uuid_field_name = getattr(obj, "uuid_field", None)
|
|
98
|
+
uuid_field_name: str | None = getattr(obj, "uuid_field", None)
|
|
99
99
|
if uuid_field_name is None:
|
|
100
|
-
uuid_field_name = get_setting("UUID_FIELD")
|
|
100
|
+
uuid_field_name = str(get_setting("UUID_FIELD"))
|
|
101
101
|
uuid_value = getattr(obj, uuid_field_name, None)
|
|
102
102
|
if uuid_value is None:
|
|
103
103
|
raise ValueError(
|
|
@@ -108,7 +108,8 @@ class DisplayIDField(serializers.SerializerMethodField):
|
|
|
108
108
|
|
|
109
109
|
# Use the model's display_id property
|
|
110
110
|
if hasattr(obj, "display_id"):
|
|
111
|
-
|
|
111
|
+
display_id: str = obj.display_id
|
|
112
|
+
return display_id
|
|
112
113
|
|
|
113
114
|
raise ValueError(
|
|
114
115
|
f"Cannot generate display_id: {obj.__class__.__name__} "
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Django URL path converters for display IDs and UUIDs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"DisplayIDConverter",
|
|
7
|
+
"DisplayIDOrUUIDConverter",
|
|
8
|
+
"UUIDConverter",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DisplayIDConverter:
|
|
13
|
+
"""Path converter for display IDs.
|
|
14
|
+
|
|
15
|
+
Matches the format: {prefix}_{base62} where prefix is 1-16 lowercase
|
|
16
|
+
letters and base62 is exactly 22 alphanumeric characters.
|
|
17
|
+
|
|
18
|
+
Example:
|
|
19
|
+
from django.urls import path, register_converter
|
|
20
|
+
from django_display_ids.converters import DisplayIDConverter
|
|
21
|
+
|
|
22
|
+
register_converter(DisplayIDConverter, "display_id")
|
|
23
|
+
|
|
24
|
+
urlpatterns = [
|
|
25
|
+
path("invoices/<display_id:id>/", InvoiceDetailView.as_view()),
|
|
26
|
+
]
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
regex = r"[a-z]{1,16}_[0-9A-Za-z]{22}"
|
|
30
|
+
|
|
31
|
+
def to_python(self, value: str) -> str:
|
|
32
|
+
"""Convert the URL value to a Python object."""
|
|
33
|
+
return value
|
|
34
|
+
|
|
35
|
+
def to_url(self, value: str) -> str:
|
|
36
|
+
"""Convert a Python object to a URL string."""
|
|
37
|
+
return value
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class UUIDConverter:
|
|
41
|
+
"""Path converter for UUIDs.
|
|
42
|
+
|
|
43
|
+
Matches UUIDs in both hyphenated and unhyphenated formats:
|
|
44
|
+
- 550e8400-e29b-41d4-a716-446655440000 (hyphenated)
|
|
45
|
+
- 550e8400e29b41d4a716446655440000 (unhyphenated)
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
from django.urls import path, register_converter
|
|
49
|
+
from django_display_ids.converters import UUIDConverter
|
|
50
|
+
|
|
51
|
+
register_converter(UUIDConverter, "uuid")
|
|
52
|
+
|
|
53
|
+
urlpatterns = [
|
|
54
|
+
path("invoices/<uuid:id>/", InvoiceDetailView.as_view()),
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
Note:
|
|
58
|
+
Django's built-in UUIDConverter only accepts hyphenated UUIDs.
|
|
59
|
+
This converter is more permissive.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
# Hyphenated: 8-4-4-4-12 hex chars with hyphens
|
|
63
|
+
# Unhyphenated: 32 hex chars
|
|
64
|
+
# Note: Parentheses group the alternatives so ^ and $ anchor correctly
|
|
65
|
+
regex = (
|
|
66
|
+
r"(?:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|[0-9a-f]{32})"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def to_python(self, value: str) -> str:
|
|
70
|
+
"""Convert the URL value to a Python object."""
|
|
71
|
+
return value
|
|
72
|
+
|
|
73
|
+
def to_url(self, value: str) -> str:
|
|
74
|
+
"""Convert a Python object to a URL string."""
|
|
75
|
+
return value
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class DisplayIDOrUUIDConverter:
|
|
79
|
+
"""Path converter for display IDs or UUIDs.
|
|
80
|
+
|
|
81
|
+
Matches either format:
|
|
82
|
+
- Display ID: {prefix}_{base62}
|
|
83
|
+
- UUID: hyphenated or unhyphenated
|
|
84
|
+
|
|
85
|
+
Example:
|
|
86
|
+
from django.urls import path, register_converter
|
|
87
|
+
from django_display_ids.converters import DisplayIDOrUUIDConverter
|
|
88
|
+
|
|
89
|
+
register_converter(DisplayIDOrUUIDConverter, "display_id_or_uuid")
|
|
90
|
+
|
|
91
|
+
urlpatterns = [
|
|
92
|
+
path("invoices/<display_id_or_uuid:id>/", InvoiceDetailView.as_view()),
|
|
93
|
+
]
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
# Note: Parentheses group the alternatives so ^ and $ anchor correctly
|
|
97
|
+
regex = (
|
|
98
|
+
r"(?:"
|
|
99
|
+
r"[a-z]{1,16}_[0-9A-Za-z]{22}"
|
|
100
|
+
r"|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
|
|
101
|
+
r"|[0-9a-f]{32}"
|
|
102
|
+
r")"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def to_python(self, value: str) -> str:
|
|
106
|
+
"""Convert the URL value to a Python object."""
|
|
107
|
+
return value
|
|
108
|
+
|
|
109
|
+
def to_url(self, value: str) -> str:
|
|
110
|
+
"""Convert a Python object to a URL string."""
|
|
111
|
+
return value
|
|
@@ -26,7 +26,7 @@ def _get_prefix(prefix_or_model: str | type[Model]) -> str:
|
|
|
26
26
|
if isinstance(prefix_or_model, str):
|
|
27
27
|
return prefix_or_model
|
|
28
28
|
# It's a model class
|
|
29
|
-
prefix = getattr(prefix_or_model, "display_id_prefix", None)
|
|
29
|
+
prefix: str | None = getattr(prefix_or_model, "display_id_prefix", None)
|
|
30
30
|
if prefix is None:
|
|
31
31
|
raise ValueError(f"Model {prefix_or_model.__name__} has no display_id_prefix")
|
|
32
32
|
return prefix
|