django-display-ids 0.3.2__py3-none-any.whl → 0.4.1__py3-none-any.whl
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/__init__.py +34 -45
- django_display_ids/admin.py +18 -33
- django_display_ids/conf.py +35 -1
- django_display_ids/contrib/drf_spectacular/__init__.py +52 -3
- django_display_ids/contrib/rest_framework/__init__.py +3 -42
- django_display_ids/contrib/rest_framework/views.py +17 -18
- django_display_ids/converters.py +8 -9
- django_display_ids/exceptions.py +7 -7
- django_display_ids/managers.py +42 -12
- django_display_ids/models.py +5 -5
- django_display_ids/resolver.py +14 -5
- django_display_ids/views.py +10 -16
- {django_display_ids-0.3.2.dist-info → django_display_ids-0.4.1.dist-info}/METADATA +6 -5
- django_display_ids-0.4.1.dist-info/RECORD +25 -0
- {django_display_ids-0.3.2.dist-info → django_display_ids-0.4.1.dist-info}/WHEEL +1 -1
- django_display_ids-0.3.2.dist-info/RECORD +0 -25
django_display_ids/__init__.py
CHANGED
|
@@ -9,7 +9,7 @@ Example:
|
|
|
9
9
|
encode_display_id,
|
|
10
10
|
decode_display_id,
|
|
11
11
|
resolve_object,
|
|
12
|
-
|
|
12
|
+
DisplayIDMixin,
|
|
13
13
|
)
|
|
14
14
|
|
|
15
15
|
# Encode a UUID to a display ID
|
|
@@ -17,15 +17,18 @@ Example:
|
|
|
17
17
|
# -> "inv_2aUyqjCzEIiEcYMKj7TZtw"
|
|
18
18
|
|
|
19
19
|
# Use in Django views
|
|
20
|
-
class InvoiceDetailView(
|
|
20
|
+
class InvoiceDetailView(DisplayIDMixin, DetailView):
|
|
21
21
|
model = Invoice
|
|
22
22
|
lookup_param = "id"
|
|
23
23
|
display_id_prefix = "inv"
|
|
24
24
|
"""
|
|
25
25
|
|
|
26
|
+
from importlib.metadata import version
|
|
26
27
|
from typing import Any
|
|
27
28
|
|
|
28
|
-
from .admin import
|
|
29
|
+
from .admin import DisplayIDAdminSearchMixin
|
|
30
|
+
|
|
31
|
+
__version__ = version("django-display-ids")
|
|
29
32
|
from .converters import (
|
|
30
33
|
DisplayIDConverter,
|
|
31
34
|
DisplayIDOrSlugConverter,
|
|
@@ -48,8 +51,8 @@ from .examples import (
|
|
|
48
51
|
)
|
|
49
52
|
from .exceptions import (
|
|
50
53
|
AmbiguousIdentifierError,
|
|
54
|
+
DisplayIDLookupError,
|
|
51
55
|
InvalidIdentifierError,
|
|
52
|
-
LookupError,
|
|
53
56
|
MissingPrefixError,
|
|
54
57
|
ObjectNotFoundError,
|
|
55
58
|
UnknownPrefixError,
|
|
@@ -57,15 +60,15 @@ from .exceptions import (
|
|
|
57
60
|
from .managers import DisplayIDManager, DisplayIDQuerySet
|
|
58
61
|
from .resolver import resolve_object
|
|
59
62
|
from .typing import DEFAULT_STRATEGIES, StrategyName
|
|
60
|
-
from .views import
|
|
63
|
+
from .views import DisplayIDMixin
|
|
61
64
|
|
|
62
65
|
|
|
63
66
|
def __getattr__(name: str) -> Any:
|
|
64
67
|
"""Lazy import for model-related items to avoid app registry issues."""
|
|
65
|
-
if name == "
|
|
66
|
-
from .models import
|
|
68
|
+
if name == "DisplayIDModel":
|
|
69
|
+
from .models import DisplayIDModel
|
|
67
70
|
|
|
68
|
-
return
|
|
71
|
+
return DisplayIDModel
|
|
69
72
|
if name == "get_model_for_prefix":
|
|
70
73
|
from .models import get_model_for_prefix
|
|
71
74
|
|
|
@@ -74,57 +77,43 @@ def __getattr__(name: str) -> Any:
|
|
|
74
77
|
|
|
75
78
|
|
|
76
79
|
__all__ = [ # noqa: RUF022 - keep categorized order for readability
|
|
80
|
+
# Model integration
|
|
81
|
+
"DisplayIDModel",
|
|
82
|
+
"DisplayIDManager",
|
|
83
|
+
"DisplayIDQuerySet",
|
|
84
|
+
"get_model_for_prefix",
|
|
85
|
+
# View mixins
|
|
86
|
+
"DisplayIDMixin",
|
|
87
|
+
"DisplayIDAdminSearchMixin",
|
|
88
|
+
# Encoding/decoding
|
|
89
|
+
"encode_display_id",
|
|
90
|
+
"decode_display_id",
|
|
91
|
+
"encode_uuid",
|
|
92
|
+
"decode_uuid",
|
|
77
93
|
# URL converters
|
|
78
94
|
"DisplayIDConverter",
|
|
79
|
-
"DisplayIDOrSlugConverter",
|
|
80
95
|
"DisplayIDOrUUIDConverter",
|
|
96
|
+
"DisplayIDOrSlugConverter",
|
|
81
97
|
"DisplayIDOrUUIDOrSlugConverter",
|
|
82
98
|
"make_display_id_or_slug_converter",
|
|
83
99
|
"make_display_id_or_uuid_or_slug_converter",
|
|
84
|
-
# Encoding
|
|
85
|
-
"encode_uuid",
|
|
86
|
-
"decode_uuid",
|
|
87
|
-
"encode_display_id",
|
|
88
|
-
"decode_display_id",
|
|
89
|
-
# Examples (for OpenAPI schemas, documentation)
|
|
90
|
-
"example_uuid",
|
|
91
|
-
"example_display_id",
|
|
92
|
-
"example_uuid_for_prefix", # alias
|
|
93
|
-
"example_display_id_for_prefix", # alias
|
|
94
100
|
# Core resolver
|
|
95
101
|
"resolve_object",
|
|
96
|
-
#
|
|
97
|
-
"
|
|
102
|
+
# Exceptions
|
|
103
|
+
"DisplayIDLookupError",
|
|
98
104
|
"InvalidIdentifierError",
|
|
99
105
|
"UnknownPrefixError",
|
|
100
106
|
"MissingPrefixError",
|
|
101
107
|
"ObjectNotFoundError",
|
|
102
108
|
"AmbiguousIdentifierError",
|
|
103
|
-
#
|
|
104
|
-
"
|
|
105
|
-
|
|
106
|
-
"
|
|
107
|
-
#
|
|
108
|
-
"DisplayIDMixin",
|
|
109
|
-
"get_model_for_prefix",
|
|
110
|
-
# Managers
|
|
111
|
-
"DisplayIDManager",
|
|
112
|
-
"DisplayIDQuerySet",
|
|
109
|
+
# Examples (for OpenAPI)
|
|
110
|
+
"example_display_id",
|
|
111
|
+
"example_uuid",
|
|
112
|
+
"example_display_id_for_prefix", # alias
|
|
113
|
+
"example_uuid_for_prefix", # alias
|
|
113
114
|
# Types
|
|
114
115
|
"StrategyName",
|
|
115
116
|
"DEFAULT_STRATEGIES",
|
|
117
|
+
# Version
|
|
118
|
+
"__version__",
|
|
116
119
|
]
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def get_drf_mixin() -> type:
|
|
120
|
-
"""Lazily import the DRF mixin to avoid hard dependency.
|
|
121
|
-
|
|
122
|
-
Returns:
|
|
123
|
-
DisplayIDLookupMixin class.
|
|
124
|
-
|
|
125
|
-
Raises:
|
|
126
|
-
ImportError: If Django REST Framework is not installed.
|
|
127
|
-
"""
|
|
128
|
-
from .contrib.rest_framework import DisplayIDLookupMixin
|
|
129
|
-
|
|
130
|
-
return DisplayIDLookupMixin
|
django_display_ids/admin.py
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import contextlib
|
|
6
|
-
import uuid
|
|
7
6
|
from typing import TYPE_CHECKING, Any
|
|
8
7
|
|
|
9
8
|
from .encoding import decode_display_id
|
|
@@ -12,30 +11,33 @@ if TYPE_CHECKING:
|
|
|
12
11
|
from django.db.models import Model, QuerySet
|
|
13
12
|
from django.http import HttpRequest
|
|
14
13
|
|
|
15
|
-
__all__ = ["
|
|
14
|
+
__all__ = ["DisplayIDAdminSearchMixin"]
|
|
16
15
|
|
|
17
16
|
|
|
18
|
-
class
|
|
19
|
-
"""Mixin to enable searching by
|
|
17
|
+
class DisplayIDAdminSearchMixin:
|
|
18
|
+
"""Mixin to enable searching by display ID in Django admin.
|
|
20
19
|
|
|
21
20
|
Add this mixin to your ModelAdmin to allow searching by display ID
|
|
22
|
-
(e.g., "inv_2aUyqjCzEIiEcYMKj7TZtw")
|
|
23
|
-
(e.g., "550e8400-e29b-41d4-a716-446655440000") in the admin search box.
|
|
21
|
+
(e.g., "inv_2aUyqjCzEIiEcYMKj7TZtw") in the admin search box.
|
|
24
22
|
|
|
25
|
-
The mixin decodes the
|
|
23
|
+
The mixin decodes the display ID and searches by the UUID field.
|
|
24
|
+
|
|
25
|
+
For raw UUID search, add the UUID field to ``search_fields`` instead::
|
|
26
|
+
|
|
27
|
+
search_fields = ["name", "id"] # "id" enables raw UUID search
|
|
26
28
|
|
|
27
29
|
Example:
|
|
28
30
|
from django.contrib import admin
|
|
29
|
-
from django_display_ids
|
|
31
|
+
from django_display_ids import DisplayIDAdminSearchMixin
|
|
30
32
|
|
|
31
33
|
@admin.register(Invoice)
|
|
32
|
-
class InvoiceAdmin(
|
|
34
|
+
class InvoiceAdmin(DisplayIDAdminSearchMixin, admin.ModelAdmin):
|
|
33
35
|
list_display = ["id", "display_id", "name"]
|
|
34
|
-
search_fields = ["name"] #
|
|
36
|
+
search_fields = ["name"] # display ID search is automatic
|
|
35
37
|
|
|
36
38
|
Attributes:
|
|
37
39
|
uuid_field: Name of the UUID field to search. Defaults to model's
|
|
38
|
-
uuid_field if using
|
|
40
|
+
uuid_field if using DisplayIDModel, otherwise "id".
|
|
39
41
|
"""
|
|
40
42
|
|
|
41
43
|
uuid_field: str | None = None
|
|
@@ -49,43 +51,26 @@ class DisplayIDSearchMixin:
|
|
|
49
51
|
uuid_field: str | None = getattr(self.model, "uuid_field", None)
|
|
50
52
|
return uuid_field or "id"
|
|
51
53
|
|
|
52
|
-
def _try_parse_uuid(self, value: str) -> uuid.UUID | None:
|
|
53
|
-
"""Try to parse a string as a UUID."""
|
|
54
|
-
try:
|
|
55
|
-
return uuid.UUID(value)
|
|
56
|
-
except (ValueError, TypeError):
|
|
57
|
-
return None
|
|
58
|
-
|
|
59
54
|
def get_search_results(
|
|
60
55
|
self,
|
|
61
56
|
request: HttpRequest,
|
|
62
57
|
queryset: QuerySet[Any],
|
|
63
58
|
search_term: str,
|
|
64
59
|
) -> tuple[QuerySet[Any], bool]:
|
|
65
|
-
"""Extend search to handle display IDs
|
|
60
|
+
"""Extend search to handle display IDs.
|
|
66
61
|
|
|
67
|
-
Tries to
|
|
68
|
-
|
|
69
|
-
2. A raw UUID if it looks like a UUID format
|
|
62
|
+
Tries to decode the search term as a display ID (prefix_base62uuid)
|
|
63
|
+
if it contains an underscore.
|
|
70
64
|
"""
|
|
71
65
|
queryset, use_distinct = super().get_search_results( # type: ignore[misc]
|
|
72
66
|
request, queryset, search_term
|
|
73
67
|
)
|
|
74
68
|
|
|
75
|
-
uuid_field = self._get_uuid_field()
|
|
76
|
-
uuid_val = None
|
|
77
|
-
|
|
78
69
|
# Try to decode as display_id if it contains an underscore
|
|
79
70
|
if "_" in search_term:
|
|
71
|
+
uuid_field = self._get_uuid_field()
|
|
80
72
|
with contextlib.suppress(ValueError, TypeError):
|
|
81
73
|
_prefix, uuid_val = decode_display_id(search_term)
|
|
82
|
-
|
|
83
|
-
# Try to parse as raw UUID if not already matched
|
|
84
|
-
if uuid_val is None:
|
|
85
|
-
uuid_val = self._try_parse_uuid(search_term)
|
|
86
|
-
|
|
87
|
-
# Add matching objects to queryset if we found a UUID
|
|
88
|
-
if uuid_val is not None:
|
|
89
|
-
queryset |= self.model._default_manager.filter(**{uuid_field: uuid_val})
|
|
74
|
+
queryset |= self.model._default_manager.filter(**{uuid_field: uuid_val})
|
|
90
75
|
|
|
91
76
|
return queryset, use_distinct
|
django_display_ids/conf.py
CHANGED
|
@@ -11,7 +11,7 @@ Settings can be configured in Django settings under the DISPLAY_IDS namespace:
|
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
|
-
from typing import TYPE_CHECKING
|
|
14
|
+
from typing import TYPE_CHECKING, Any
|
|
15
15
|
|
|
16
16
|
from django.conf import settings
|
|
17
17
|
from django.urls.converters import SlugConverter
|
|
@@ -21,10 +21,16 @@ if TYPE_CHECKING:
|
|
|
21
21
|
|
|
22
22
|
__all__ = [
|
|
23
23
|
"DEFAULTS",
|
|
24
|
+
"NOT_SET",
|
|
24
25
|
"SLUG_REGEX",
|
|
25
26
|
"get_setting",
|
|
27
|
+
"get_slug_field",
|
|
28
|
+
"get_uuid_field",
|
|
26
29
|
]
|
|
27
30
|
|
|
31
|
+
# Sentinel for distinguishing "not set" from None
|
|
32
|
+
NOT_SET: Any = object()
|
|
33
|
+
|
|
28
34
|
# Django's default slug regex pattern
|
|
29
35
|
SLUG_REGEX: str = SlugConverter.regex
|
|
30
36
|
|
|
@@ -56,3 +62,31 @@ def get_setting(name: str) -> str | tuple[StrategyName, ...]:
|
|
|
56
62
|
)
|
|
57
63
|
result = user_settings.get(name, DEFAULTS[name])
|
|
58
64
|
return result # type: ignore[return-value]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_uuid_field(override: str | None) -> str:
|
|
68
|
+
"""Get the UUID field name, with optional override.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
override: Explicit field name, or None to use settings default.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
The UUID field name.
|
|
75
|
+
"""
|
|
76
|
+
if override is not None:
|
|
77
|
+
return override
|
|
78
|
+
return str(get_setting("UUID_FIELD"))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_slug_field(override: str | None) -> str:
|
|
82
|
+
"""Get the slug field name, with optional override.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
override: Explicit field name, or None to use settings default.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
The slug field name.
|
|
89
|
+
"""
|
|
90
|
+
if override is not None:
|
|
91
|
+
return override
|
|
92
|
+
return str(get_setting("SLUG_FIELD"))
|
|
@@ -1,13 +1,62 @@
|
|
|
1
|
-
"""drf-spectacular
|
|
1
|
+
"""drf-spectacular integration for django-display-ids.
|
|
2
2
|
|
|
3
|
-
This
|
|
4
|
-
|
|
3
|
+
This module provides:
|
|
4
|
+
- OpenAPI schema extension for DisplayIDField (auto-registers when imported)
|
|
5
|
+
- Helper functions for documenting URL path parameters
|
|
5
6
|
"""
|
|
6
7
|
|
|
7
8
|
from __future__ import annotations
|
|
8
9
|
|
|
9
10
|
from typing import TYPE_CHECKING, Any
|
|
10
11
|
|
|
12
|
+
# OpenAPI parameter description helpers
|
|
13
|
+
# These work regardless of whether drf-spectacular is installed
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def id_param_description(
|
|
17
|
+
prefix: str, *, with_uuid: bool = True, with_slug: bool = False
|
|
18
|
+
) -> str:
|
|
19
|
+
"""Generate ID parameter description with the actual prefix.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
prefix: The display_id prefix (e.g., "user", "app").
|
|
23
|
+
with_uuid: Include UUID as an identifier option.
|
|
24
|
+
with_slug: Include slug as an identifier option.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Description string for OpenAPI parameter.
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
>>> id_param_description("user")
|
|
31
|
+
'Identifier: display_id (user_xxx) or UUID'
|
|
32
|
+
|
|
33
|
+
>>> id_param_description("user", with_uuid=False)
|
|
34
|
+
'Identifier: display_id (user_xxx)'
|
|
35
|
+
|
|
36
|
+
>>> id_param_description("app", with_slug=True)
|
|
37
|
+
'Identifier: display_id (app_xxx), UUID, or slug'
|
|
38
|
+
|
|
39
|
+
>>> id_param_description("app", with_uuid=False, with_slug=True)
|
|
40
|
+
'Identifier: display_id (app_xxx) or slug'
|
|
41
|
+
"""
|
|
42
|
+
parts = [f"display_id ({prefix}_xxx)"]
|
|
43
|
+
if with_uuid:
|
|
44
|
+
parts.append("UUID")
|
|
45
|
+
if with_slug:
|
|
46
|
+
parts.append("slug")
|
|
47
|
+
|
|
48
|
+
if len(parts) == 1:
|
|
49
|
+
return f"Identifier: {parts[0]}"
|
|
50
|
+
elif len(parts) == 2:
|
|
51
|
+
return f"Identifier: {parts[0]} or {parts[1]}"
|
|
52
|
+
else:
|
|
53
|
+
return f"Identifier: {', '.join(parts[:-1])}, or {parts[-1]}"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
"id_param_description",
|
|
58
|
+
]
|
|
59
|
+
|
|
11
60
|
try:
|
|
12
61
|
from drf_spectacular.extensions import OpenApiSerializerFieldExtension
|
|
13
62
|
except ImportError:
|
|
@@ -1,48 +1,9 @@
|
|
|
1
1
|
"""Django REST Framework integration for django-display-ids."""
|
|
2
2
|
|
|
3
|
-
import contextlib
|
|
4
|
-
|
|
5
3
|
from .serializers import DisplayIDField
|
|
6
|
-
from .views import
|
|
7
|
-
|
|
8
|
-
# Register drf-spectacular extension if available
|
|
9
|
-
# The extension auto-registers when the module is imported
|
|
10
|
-
with contextlib.suppress(ImportError):
|
|
11
|
-
from django_display_ids.contrib import (
|
|
12
|
-
drf_spectacular as _drf_spectacular, # noqa: F401
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
# OpenAPI parameter descriptions for consistent documentation
|
|
16
|
-
ID_PARAM_DESCRIPTION = "Identifier: display_id (prefix_xxx) or UUID"
|
|
17
|
-
ID_PARAM_DESCRIPTION_WITH_SLUG = "Identifier: display_id (prefix_xxx), UUID, or slug"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def id_param_description(prefix: str, *, with_slug: bool = False) -> str:
|
|
21
|
-
"""Generate ID parameter description with the actual prefix.
|
|
22
|
-
|
|
23
|
-
Args:
|
|
24
|
-
prefix: The display_id prefix (e.g., "user", "app").
|
|
25
|
-
with_slug: Include slug as an identifier option.
|
|
26
|
-
|
|
27
|
-
Returns:
|
|
28
|
-
Description string for OpenAPI parameter.
|
|
29
|
-
|
|
30
|
-
Example:
|
|
31
|
-
>>> id_param_description("user")
|
|
32
|
-
'Identifier: display_id (user_xxx) or UUID'
|
|
33
|
-
|
|
34
|
-
>>> id_param_description("app", with_slug=True)
|
|
35
|
-
'Identifier: display_id (app_xxx), UUID, or slug'
|
|
36
|
-
"""
|
|
37
|
-
if with_slug:
|
|
38
|
-
return f"Identifier: display_id ({prefix}_xxx), UUID, or slug"
|
|
39
|
-
return f"Identifier: display_id ({prefix}_xxx) or UUID"
|
|
40
|
-
|
|
4
|
+
from .views import DisplayIDMixin
|
|
41
5
|
|
|
42
|
-
__all__ = [
|
|
6
|
+
__all__ = [
|
|
43
7
|
"DisplayIDField",
|
|
44
|
-
"
|
|
45
|
-
"ID_PARAM_DESCRIPTION",
|
|
46
|
-
"ID_PARAM_DESCRIPTION_WITH_SLUG",
|
|
47
|
-
"id_param_description",
|
|
8
|
+
"DisplayIDMixin",
|
|
48
9
|
]
|
|
@@ -4,11 +4,16 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from typing import TYPE_CHECKING, Any
|
|
6
6
|
|
|
7
|
-
from django_display_ids.conf import
|
|
7
|
+
from django_display_ids.conf import (
|
|
8
|
+
NOT_SET,
|
|
9
|
+
get_setting,
|
|
10
|
+
get_slug_field,
|
|
11
|
+
get_uuid_field,
|
|
12
|
+
)
|
|
8
13
|
from django_display_ids.encoding import PREFIX_PATTERN
|
|
9
14
|
from django_display_ids.exceptions import (
|
|
15
|
+
DisplayIDLookupError,
|
|
10
16
|
InvalidIdentifierError,
|
|
11
|
-
LookupError,
|
|
12
17
|
ObjectNotFoundError,
|
|
13
18
|
UnknownPrefixError,
|
|
14
19
|
)
|
|
@@ -19,11 +24,9 @@ if TYPE_CHECKING:
|
|
|
19
24
|
from django.db import models
|
|
20
25
|
|
|
21
26
|
__all__ = [
|
|
22
|
-
"
|
|
27
|
+
"DisplayIDMixin",
|
|
23
28
|
]
|
|
24
29
|
|
|
25
|
-
_NOT_SET: Any = object()
|
|
26
|
-
|
|
27
30
|
|
|
28
31
|
def _get_drf_exceptions() -> tuple[type[Exception], type[Exception]]:
|
|
29
32
|
"""Lazily import DRF exceptions to avoid hard dependency."""
|
|
@@ -33,12 +36,12 @@ def _get_drf_exceptions() -> tuple[type[Exception], type[Exception]]:
|
|
|
33
36
|
return NotFound, ParseError
|
|
34
37
|
except ImportError:
|
|
35
38
|
raise ImportError(
|
|
36
|
-
"Django REST Framework is required for
|
|
39
|
+
"Django REST Framework is required for DisplayIDMixin. "
|
|
37
40
|
"Install it with: pip install djangorestframework"
|
|
38
41
|
) from None
|
|
39
42
|
|
|
40
43
|
|
|
41
|
-
class
|
|
44
|
+
class DisplayIDMixin:
|
|
42
45
|
"""Mixin for DRF views that resolves objects by display ID, UUID, or slug.
|
|
43
46
|
|
|
44
47
|
Works with APIView, GenericAPIView, and ViewSets. Does not require
|
|
@@ -52,7 +55,7 @@ class DisplayIDLookupMixin:
|
|
|
52
55
|
slug_field: Name of the slug field on the model.
|
|
53
56
|
|
|
54
57
|
Example:
|
|
55
|
-
class InvoiceView(
|
|
58
|
+
class InvoiceView(DisplayIDMixin, APIView):
|
|
56
59
|
lookup_url_kwarg = "id"
|
|
57
60
|
lookup_strategies = ("display_id", "uuid")
|
|
58
61
|
display_id_prefix = "inv"
|
|
@@ -62,7 +65,7 @@ class DisplayIDLookupMixin:
|
|
|
62
65
|
return Response({"id": str(invoice.id)})
|
|
63
66
|
|
|
64
67
|
Example with ViewSet:
|
|
65
|
-
class InvoiceViewSet(
|
|
68
|
+
class InvoiceViewSet(DisplayIDMixin, ModelViewSet):
|
|
66
69
|
queryset = Invoice.objects.all()
|
|
67
70
|
serializer_class = InvoiceSerializer
|
|
68
71
|
lookup_url_kwarg = "pk"
|
|
@@ -71,7 +74,7 @@ class DisplayIDLookupMixin:
|
|
|
71
74
|
|
|
72
75
|
lookup_url_kwarg: str = "pk"
|
|
73
76
|
lookup_strategies: tuple[StrategyName, ...] | None = None
|
|
74
|
-
display_id_prefix: str | None =
|
|
77
|
+
display_id_prefix: str | None = NOT_SET
|
|
75
78
|
uuid_field: str | None = None
|
|
76
79
|
slug_field: str | None = None
|
|
77
80
|
|
|
@@ -80,14 +83,10 @@ class DisplayIDLookupMixin:
|
|
|
80
83
|
request: Any
|
|
81
84
|
|
|
82
85
|
def _get_uuid_field(self) -> str:
|
|
83
|
-
|
|
84
|
-
return self.uuid_field
|
|
85
|
-
return str(get_setting("UUID_FIELD"))
|
|
86
|
+
return get_uuid_field(self.uuid_field)
|
|
86
87
|
|
|
87
88
|
def _get_slug_field(self) -> str:
|
|
88
|
-
|
|
89
|
-
return self.slug_field
|
|
90
|
-
return str(get_setting("SLUG_FIELD"))
|
|
89
|
+
return get_slug_field(self.slug_field)
|
|
91
90
|
|
|
92
91
|
def _get_strategies(self) -> tuple[StrategyName, ...]:
|
|
93
92
|
if self.lookup_strategies is not None:
|
|
@@ -101,7 +100,7 @@ class DisplayIDLookupMixin:
|
|
|
101
100
|
explicitly disable), otherwise falls back to the model's
|
|
102
101
|
display_id_prefix attribute.
|
|
103
102
|
"""
|
|
104
|
-
if self.display_id_prefix is not
|
|
103
|
+
if self.display_id_prefix is not NOT_SET:
|
|
105
104
|
if self.display_id_prefix is not None and not PREFIX_PATTERN.match(
|
|
106
105
|
self.display_id_prefix
|
|
107
106
|
):
|
|
@@ -168,7 +167,7 @@ class DisplayIDLookupMixin:
|
|
|
168
167
|
raise NotFound(str(e)) from e
|
|
169
168
|
except (InvalidIdentifierError, UnknownPrefixError) as e:
|
|
170
169
|
raise ParseError(str(e)) from e
|
|
171
|
-
except
|
|
170
|
+
except DisplayIDLookupError as e:
|
|
172
171
|
raise ParseError(str(e)) from e
|
|
173
172
|
|
|
174
173
|
# Check object-level permissions
|
django_display_ids/converters.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from .conf import SLUG_REGEX
|
|
5
|
+
from .conf import SLUG_REGEX, get_setting
|
|
6
6
|
|
|
7
7
|
__all__ = [
|
|
8
8
|
"DISPLAY_ID_REGEX",
|
|
@@ -19,6 +19,9 @@ __all__ = [
|
|
|
19
19
|
DISPLAY_ID_REGEX = r"[a-z]{1,16}_[0-9A-Za-z]{22}"
|
|
20
20
|
UUID_REGEX = r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
|
|
21
21
|
|
|
22
|
+
# Slug regex from settings (respects DISPLAY_IDS["SLUG_REGEX"] Django setting)
|
|
23
|
+
_SLUG_REGEX: str = str(get_setting("SLUG_REGEX"))
|
|
24
|
+
|
|
22
25
|
|
|
23
26
|
class BaseConverter:
|
|
24
27
|
"""Base class for URL path converters with pass-through conversion."""
|
|
@@ -78,7 +81,7 @@ class DisplayIDOrSlugConverter(BaseConverter):
|
|
|
78
81
|
|
|
79
82
|
Matches either format:
|
|
80
83
|
- Display ID: {prefix}_{base62}
|
|
81
|
-
- Slug:
|
|
84
|
+
- Slug: matches DISPLAY_IDS["SLUG_REGEX"] setting (default: [-a-zA-Z0-9_]+)
|
|
82
85
|
|
|
83
86
|
Example:
|
|
84
87
|
from django.urls import path, register_converter
|
|
@@ -91,7 +94,7 @@ class DisplayIDOrSlugConverter(BaseConverter):
|
|
|
91
94
|
]
|
|
92
95
|
"""
|
|
93
96
|
|
|
94
|
-
regex = rf"(?:{DISPLAY_ID_REGEX}|{
|
|
97
|
+
regex = rf"(?:{DISPLAY_ID_REGEX}|{_SLUG_REGEX})"
|
|
95
98
|
|
|
96
99
|
|
|
97
100
|
class DisplayIDOrUUIDOrSlugConverter(BaseConverter):
|
|
@@ -100,7 +103,7 @@ class DisplayIDOrUUIDOrSlugConverter(BaseConverter):
|
|
|
100
103
|
Matches any of:
|
|
101
104
|
- Display ID: {prefix}_{base62}
|
|
102
105
|
- UUID: hyphenated (e.g., 550e8400-e29b-41d4-a716-446655440000)
|
|
103
|
-
- Slug:
|
|
106
|
+
- Slug: matches DISPLAY_IDS["SLUG_REGEX"] setting (default: [-a-zA-Z0-9_]+)
|
|
104
107
|
|
|
105
108
|
Example:
|
|
106
109
|
from django.urls import path, register_converter
|
|
@@ -113,7 +116,7 @@ class DisplayIDOrUUIDOrSlugConverter(BaseConverter):
|
|
|
113
116
|
]
|
|
114
117
|
"""
|
|
115
118
|
|
|
116
|
-
regex = rf"(?:{DISPLAY_ID_REGEX}|{UUID_REGEX}|{
|
|
119
|
+
regex = rf"(?:{DISPLAY_ID_REGEX}|{UUID_REGEX}|{_SLUG_REGEX})"
|
|
117
120
|
|
|
118
121
|
|
|
119
122
|
def make_display_id_or_slug_converter(
|
|
@@ -140,8 +143,6 @@ def make_display_id_or_slug_converter(
|
|
|
140
143
|
path("products/<display_id_or_slug:id>/", ProductDetailView.as_view()),
|
|
141
144
|
]
|
|
142
145
|
"""
|
|
143
|
-
from .conf import get_setting
|
|
144
|
-
|
|
145
146
|
pattern = slug_regex if slug_regex is not None else get_setting("SLUG_REGEX")
|
|
146
147
|
|
|
147
148
|
class CustomDisplayIDOrSlugConverter(DisplayIDOrSlugConverter):
|
|
@@ -176,8 +177,6 @@ def make_display_id_or_uuid_or_slug_converter(
|
|
|
176
177
|
path("products/<identifier:id>/", ProductDetailView.as_view()),
|
|
177
178
|
]
|
|
178
179
|
"""
|
|
179
|
-
from .conf import get_setting
|
|
180
|
-
|
|
181
180
|
pattern = slug_regex if slug_regex is not None else get_setting("SLUG_REGEX")
|
|
182
181
|
|
|
183
182
|
class CustomDisplayIDOrUUIDOrSlugConverter(DisplayIDOrUUIDOrSlugConverter):
|
django_display_ids/exceptions.py
CHANGED
|
@@ -4,21 +4,21 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
__all__ = [
|
|
6
6
|
"AmbiguousIdentifierError",
|
|
7
|
+
"DisplayIDLookupError",
|
|
7
8
|
"InvalidIdentifierError",
|
|
8
|
-
"LookupError",
|
|
9
9
|
"MissingPrefixError",
|
|
10
10
|
"ObjectNotFoundError",
|
|
11
11
|
"UnknownPrefixError",
|
|
12
12
|
]
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
class
|
|
15
|
+
class DisplayIDLookupError(Exception):
|
|
16
16
|
"""Base exception for all lookup errors."""
|
|
17
17
|
|
|
18
18
|
pass
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
class InvalidIdentifierError(
|
|
21
|
+
class InvalidIdentifierError(DisplayIDLookupError):
|
|
22
22
|
"""Raised when an identifier has an invalid format.
|
|
23
23
|
|
|
24
24
|
This indicates the identifier string cannot be parsed as any
|
|
@@ -31,7 +31,7 @@ class InvalidIdentifierError(LookupError):
|
|
|
31
31
|
super().__init__(self.message)
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
class UnknownPrefixError(
|
|
34
|
+
class UnknownPrefixError(DisplayIDLookupError):
|
|
35
35
|
"""Raised when a display ID has an unexpected prefix.
|
|
36
36
|
|
|
37
37
|
This occurs when prefix enforcement is enabled and the
|
|
@@ -49,7 +49,7 @@ class UnknownPrefixError(LookupError):
|
|
|
49
49
|
super().__init__(message)
|
|
50
50
|
|
|
51
51
|
|
|
52
|
-
class MissingPrefixError(
|
|
52
|
+
class MissingPrefixError(DisplayIDLookupError):
|
|
53
53
|
"""Raised when a display ID lookup is attempted without a prefix.
|
|
54
54
|
|
|
55
55
|
This occurs when calling get_by_display_id() on a model that
|
|
@@ -68,7 +68,7 @@ class MissingPrefixError(LookupError):
|
|
|
68
68
|
super().__init__(message)
|
|
69
69
|
|
|
70
70
|
|
|
71
|
-
class ObjectNotFoundError(
|
|
71
|
+
class ObjectNotFoundError(DisplayIDLookupError):
|
|
72
72
|
"""Raised when no object matches the identifier.
|
|
73
73
|
|
|
74
74
|
This indicates the identifier was valid but no matching
|
|
@@ -85,7 +85,7 @@ class ObjectNotFoundError(LookupError):
|
|
|
85
85
|
super().__init__(message)
|
|
86
86
|
|
|
87
87
|
|
|
88
|
-
class AmbiguousIdentifierError(
|
|
88
|
+
class AmbiguousIdentifierError(DisplayIDLookupError):
|
|
89
89
|
"""Raised when an identifier matches multiple objects.
|
|
90
90
|
|
|
91
91
|
This can occur with slug lookups if slugs are not unique,
|
django_display_ids/managers.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import uuid
|
|
5
6
|
from typing import TYPE_CHECKING, Any, TypeVar
|
|
6
7
|
|
|
7
8
|
from django.db import models
|
|
@@ -51,14 +52,15 @@ class DisplayIDQuerySet(models.QuerySet[M]):
|
|
|
51
52
|
|
|
52
53
|
def get_by_display_id(
|
|
53
54
|
self,
|
|
54
|
-
value: str,
|
|
55
|
+
value: str | uuid.UUID,
|
|
55
56
|
*,
|
|
56
57
|
prefix: str | None = None,
|
|
57
58
|
) -> M:
|
|
58
59
|
"""Get an object by its display ID.
|
|
59
60
|
|
|
60
61
|
Args:
|
|
61
|
-
value: The display ID string (e.g., "inv_1a2B3c4D5e6F7g8H")
|
|
62
|
+
value: The display ID string (e.g., "inv_1a2B3c4D5e6F7g8H"),
|
|
63
|
+
or a UUID instance for direct UUID lookup.
|
|
62
64
|
prefix: Expected prefix for validation. If None, uses model's prefix.
|
|
63
65
|
|
|
64
66
|
Returns:
|
|
@@ -70,9 +72,19 @@ class DisplayIDQuerySet(models.QuerySet[M]):
|
|
|
70
72
|
UnknownPrefixError: If the prefix doesn't match expected.
|
|
71
73
|
ObjectNotFoundError: If no matching object exists.
|
|
72
74
|
"""
|
|
73
|
-
# Get model config
|
|
74
75
|
model = self.model
|
|
75
76
|
uuid_field = self._get_uuid_field()
|
|
77
|
+
|
|
78
|
+
# UUID objects skip display ID parsing entirely
|
|
79
|
+
if isinstance(value, uuid.UUID):
|
|
80
|
+
try:
|
|
81
|
+
return self.get(**{uuid_field: value})
|
|
82
|
+
except model.DoesNotExist: # type: ignore[attr-defined]
|
|
83
|
+
raise ObjectNotFoundError(
|
|
84
|
+
str(value), model_name=model.__name__
|
|
85
|
+
) from None
|
|
86
|
+
|
|
87
|
+
# Get model config
|
|
76
88
|
expected_prefix = prefix or self._get_model_prefix()
|
|
77
89
|
|
|
78
90
|
# Require a prefix for display ID lookups
|
|
@@ -99,7 +111,7 @@ class DisplayIDQuerySet(models.QuerySet[M]):
|
|
|
99
111
|
|
|
100
112
|
def get_by_identifier(
|
|
101
113
|
self,
|
|
102
|
-
value: str,
|
|
114
|
+
value: str | uuid.UUID,
|
|
103
115
|
*,
|
|
104
116
|
strategies: tuple[StrategyName, ...] | None = None,
|
|
105
117
|
prefix: str | None = None,
|
|
@@ -109,7 +121,8 @@ class DisplayIDQuerySet(models.QuerySet[M]):
|
|
|
109
121
|
Tries each strategy in order and returns the first match.
|
|
110
122
|
|
|
111
123
|
Args:
|
|
112
|
-
value: The identifier string (display ID, UUID, or slug)
|
|
124
|
+
value: The identifier string (display ID, UUID, or slug),
|
|
125
|
+
or a UUID instance for direct UUID lookup.
|
|
113
126
|
strategies: Strategies to try. Defaults to settings.
|
|
114
127
|
prefix: Expected display ID prefix for validation.
|
|
115
128
|
|
|
@@ -124,6 +137,16 @@ class DisplayIDQuerySet(models.QuerySet[M]):
|
|
|
124
137
|
"""
|
|
125
138
|
model = self.model
|
|
126
139
|
uuid_field = self._get_uuid_field()
|
|
140
|
+
|
|
141
|
+
# UUID objects skip strategy parsing entirely
|
|
142
|
+
if isinstance(value, uuid.UUID):
|
|
143
|
+
try:
|
|
144
|
+
return self.get(**{uuid_field: value})
|
|
145
|
+
except model.DoesNotExist: # type: ignore[attr-defined]
|
|
146
|
+
raise ObjectNotFoundError(
|
|
147
|
+
str(value), model_name=model.__name__
|
|
148
|
+
) from None
|
|
149
|
+
|
|
127
150
|
slug_field = self._get_slug_field()
|
|
128
151
|
expected_prefix = prefix or self._get_model_prefix()
|
|
129
152
|
lookup_strategies = strategies or self._get_strategies()
|
|
@@ -144,14 +167,14 @@ class DisplayIDQuerySet(models.QuerySet[M]):
|
|
|
144
167
|
try:
|
|
145
168
|
return self.get(**lookup)
|
|
146
169
|
except model.DoesNotExist: # type: ignore[attr-defined]
|
|
147
|
-
raise ObjectNotFoundError(value, model_name=model.__name__) from None
|
|
170
|
+
raise ObjectNotFoundError(str(value), model_name=model.__name__) from None
|
|
148
171
|
except model.MultipleObjectsReturned: # type: ignore[attr-defined]
|
|
149
172
|
count = self.filter(**lookup).count()
|
|
150
|
-
raise AmbiguousIdentifierError(value, count) from None
|
|
173
|
+
raise AmbiguousIdentifierError(str(value), count) from None
|
|
151
174
|
|
|
152
175
|
def get_by_identifiers(
|
|
153
176
|
self,
|
|
154
|
-
values: Sequence[str],
|
|
177
|
+
values: Sequence[str | uuid.UUID],
|
|
155
178
|
*,
|
|
156
179
|
strategies: tuple[StrategyName, ...] | None = None,
|
|
157
180
|
prefix: str | None = None,
|
|
@@ -162,7 +185,8 @@ class DisplayIDQuerySet(models.QuerySet[M]):
|
|
|
162
185
|
then executes a single database query using `__in` lookups.
|
|
163
186
|
|
|
164
187
|
Args:
|
|
165
|
-
values: A sequence of identifier strings (display IDs, UUIDs, or slugs)
|
|
188
|
+
values: A sequence of identifier strings (display IDs, UUIDs, or slugs)
|
|
189
|
+
or UUID instances. UUID instances skip strategy parsing.
|
|
166
190
|
strategies: Strategies to try. Defaults to settings.
|
|
167
191
|
prefix: Expected display ID prefix for validation.
|
|
168
192
|
|
|
@@ -178,6 +202,7 @@ class DisplayIDQuerySet(models.QuerySet[M]):
|
|
|
178
202
|
'inv_2aUyqjCzEIiEcYMKj7TZtw',
|
|
179
203
|
'inv_7kN3xPqRmLwYvTzJ5HfUaB',
|
|
180
204
|
'550e8400-e29b-41d4-a716-446655440000',
|
|
205
|
+
uuid.UUID('550e8400-e29b-41d4-a716-446655440000'),
|
|
181
206
|
])
|
|
182
207
|
"""
|
|
183
208
|
if not values:
|
|
@@ -193,6 +218,11 @@ class DisplayIDQuerySet(models.QuerySet[M]):
|
|
|
193
218
|
slugs: list[str] = []
|
|
194
219
|
|
|
195
220
|
for value in values:
|
|
221
|
+
# UUID objects skip strategy parsing entirely
|
|
222
|
+
if isinstance(value, uuid.UUID):
|
|
223
|
+
uuids.append(value)
|
|
224
|
+
continue
|
|
225
|
+
|
|
196
226
|
result = parse_identifier(
|
|
197
227
|
value, lookup_strategies, expected_prefix=expected_prefix
|
|
198
228
|
)
|
|
@@ -253,7 +283,7 @@ class DisplayIDManager(models.Manager[M]):
|
|
|
253
283
|
|
|
254
284
|
def get_by_display_id(
|
|
255
285
|
self,
|
|
256
|
-
value: str,
|
|
286
|
+
value: str | uuid.UUID,
|
|
257
287
|
*,
|
|
258
288
|
prefix: str | None = None,
|
|
259
289
|
) -> M:
|
|
@@ -265,7 +295,7 @@ class DisplayIDManager(models.Manager[M]):
|
|
|
265
295
|
|
|
266
296
|
def get_by_identifier(
|
|
267
297
|
self,
|
|
268
|
-
value: str,
|
|
298
|
+
value: str | uuid.UUID,
|
|
269
299
|
*,
|
|
270
300
|
strategies: tuple[StrategyName, ...] | None = None,
|
|
271
301
|
prefix: str | None = None,
|
|
@@ -280,7 +310,7 @@ class DisplayIDManager(models.Manager[M]):
|
|
|
280
310
|
|
|
281
311
|
def get_by_identifiers(
|
|
282
312
|
self,
|
|
283
|
-
values: Sequence[str],
|
|
313
|
+
values: Sequence[str | uuid.UUID],
|
|
284
314
|
*,
|
|
285
315
|
strategies: tuple[StrategyName, ...] | None = None,
|
|
286
316
|
prefix: str | None = None,
|
django_display_ids/models.py
CHANGED
|
@@ -10,7 +10,7 @@ from .conf import get_setting
|
|
|
10
10
|
from .encoding import PREFIX_PATTERN, encode_display_id
|
|
11
11
|
|
|
12
12
|
__all__ = [
|
|
13
|
-
"
|
|
13
|
+
"DisplayIDModel",
|
|
14
14
|
"get_model_for_prefix",
|
|
15
15
|
]
|
|
16
16
|
|
|
@@ -50,14 +50,14 @@ def _register_prefix(prefix: str, model_name: str) -> None:
|
|
|
50
50
|
_prefix_registry[prefix] = model_name
|
|
51
51
|
|
|
52
52
|
|
|
53
|
-
class
|
|
54
|
-
"""
|
|
53
|
+
class DisplayIDModel(models.Model):
|
|
54
|
+
"""Abstract base model that adds display_id support.
|
|
55
55
|
|
|
56
56
|
Subclasses must define `display_id_prefix` as a class attribute.
|
|
57
57
|
Optionally override `uuid_field` or `slug_field` if using non-default field names.
|
|
58
58
|
|
|
59
59
|
Example:
|
|
60
|
-
class Invoice(
|
|
60
|
+
class Invoice(DisplayIDModel):
|
|
61
61
|
display_id_prefix = "inv"
|
|
62
62
|
|
|
63
63
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
|
|
@@ -67,7 +67,7 @@ class DisplayIDMixin(models.Model):
|
|
|
67
67
|
invoice.display_id # -> "inv_2aUyqjCzEIiEcYMKj7TZtw"
|
|
68
68
|
|
|
69
69
|
Example with custom field names:
|
|
70
|
-
class Product(
|
|
70
|
+
class Product(DisplayIDModel):
|
|
71
71
|
display_id_prefix = "prod"
|
|
72
72
|
uuid_field = "uid"
|
|
73
73
|
slug_field = "handle"
|
django_display_ids/resolver.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import uuid
|
|
5
6
|
from typing import TYPE_CHECKING, Any, TypeVar
|
|
6
7
|
|
|
7
8
|
from django.db import models
|
|
@@ -23,7 +24,7 @@ M = TypeVar("M", bound=models.Model)
|
|
|
23
24
|
def resolve_object(
|
|
24
25
|
*,
|
|
25
26
|
model: type[M],
|
|
26
|
-
value: str,
|
|
27
|
+
value: str | uuid.UUID,
|
|
27
28
|
strategies: tuple[StrategyName, ...] = DEFAULT_STRATEGIES,
|
|
28
29
|
prefix: str | None = None,
|
|
29
30
|
uuid_field: str = "id",
|
|
@@ -36,7 +37,8 @@ def resolve_object(
|
|
|
36
37
|
|
|
37
38
|
Args:
|
|
38
39
|
model: The Django model class.
|
|
39
|
-
value: The identifier string (UUID, display ID, or slug)
|
|
40
|
+
value: The identifier string (UUID, display ID, or slug),
|
|
41
|
+
or a UUID instance for direct UUID lookup.
|
|
40
42
|
strategies: Tuple of strategy names to try in order.
|
|
41
43
|
prefix: Expected display ID prefix (for validation).
|
|
42
44
|
uuid_field: Name of the UUID field on the model.
|
|
@@ -53,9 +55,6 @@ def resolve_object(
|
|
|
53
55
|
AmbiguousIdentifierError: If multiple objects match (slug lookup).
|
|
54
56
|
TypeError: If queryset is not for the specified model.
|
|
55
57
|
"""
|
|
56
|
-
# Parse the identifier to determine type
|
|
57
|
-
result = parse_identifier(value, strategies, expected_prefix=prefix)
|
|
58
|
-
|
|
59
58
|
# Get the base queryset
|
|
60
59
|
if queryset is not None:
|
|
61
60
|
if queryset.model is not model:
|
|
@@ -67,6 +66,16 @@ def resolve_object(
|
|
|
67
66
|
else:
|
|
68
67
|
qs = model._default_manager.all()
|
|
69
68
|
|
|
69
|
+
# UUID objects skip strategy parsing entirely
|
|
70
|
+
if isinstance(value, uuid.UUID):
|
|
71
|
+
try:
|
|
72
|
+
return qs.get(**{uuid_field: value})
|
|
73
|
+
except model.DoesNotExist: # type: ignore[attr-defined]
|
|
74
|
+
raise ObjectNotFoundError(str(value), model_name=model.__name__) from None
|
|
75
|
+
|
|
76
|
+
# Parse the identifier to determine type
|
|
77
|
+
result = parse_identifier(value, strategies, expected_prefix=prefix)
|
|
78
|
+
|
|
70
79
|
# Build the lookup based on strategy
|
|
71
80
|
lookup: dict[str, Any]
|
|
72
81
|
if result.strategy in ("uuid", "display_id"):
|
django_display_ids/views.py
CHANGED
|
@@ -6,11 +6,11 @@ from typing import TYPE_CHECKING, Any
|
|
|
6
6
|
|
|
7
7
|
from django.http import Http404
|
|
8
8
|
|
|
9
|
-
from .conf import get_setting
|
|
9
|
+
from .conf import NOT_SET, get_setting, get_slug_field, get_uuid_field
|
|
10
10
|
from .encoding import PREFIX_PATTERN
|
|
11
11
|
from .exceptions import (
|
|
12
|
+
DisplayIDLookupError,
|
|
12
13
|
InvalidIdentifierError,
|
|
13
|
-
LookupError,
|
|
14
14
|
ObjectNotFoundError,
|
|
15
15
|
UnknownPrefixError,
|
|
16
16
|
)
|
|
@@ -21,13 +21,11 @@ if TYPE_CHECKING:
|
|
|
21
21
|
from django.db import models
|
|
22
22
|
|
|
23
23
|
__all__ = [
|
|
24
|
-
"
|
|
24
|
+
"DisplayIDMixin",
|
|
25
25
|
]
|
|
26
26
|
|
|
27
|
-
_NOT_SET: Any = object()
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
class DisplayIDObjectMixin:
|
|
28
|
+
class DisplayIDMixin:
|
|
31
29
|
"""Mixin for Django CBVs that resolves objects by display ID, UUID, or slug.
|
|
32
30
|
|
|
33
31
|
Drop-in replacement for SingleObjectMixin's get_object() method.
|
|
@@ -42,7 +40,7 @@ class DisplayIDObjectMixin:
|
|
|
42
40
|
slug_field: Name of the slug field on the model.
|
|
43
41
|
|
|
44
42
|
Example:
|
|
45
|
-
class InvoiceDetailView(
|
|
43
|
+
class InvoiceDetailView(DisplayIDMixin, DetailView):
|
|
46
44
|
model = Invoice
|
|
47
45
|
lookup_param = "id"
|
|
48
46
|
lookup_strategies = ("display_id", "uuid")
|
|
@@ -52,19 +50,15 @@ class DisplayIDObjectMixin:
|
|
|
52
50
|
model: type[models.Model] | None = None
|
|
53
51
|
lookup_param: str = "pk"
|
|
54
52
|
lookup_strategies: tuple[StrategyName, ...] | None = None
|
|
55
|
-
display_id_prefix: str | None =
|
|
53
|
+
display_id_prefix: str | None = NOT_SET
|
|
56
54
|
uuid_field: str | None = None
|
|
57
55
|
slug_field: str | None = None
|
|
58
56
|
|
|
59
57
|
def _get_uuid_field(self) -> str:
|
|
60
|
-
|
|
61
|
-
return self.uuid_field
|
|
62
|
-
return str(get_setting("UUID_FIELD"))
|
|
58
|
+
return get_uuid_field(self.uuid_field)
|
|
63
59
|
|
|
64
60
|
def _get_slug_field(self) -> str:
|
|
65
|
-
|
|
66
|
-
return self.slug_field
|
|
67
|
-
return str(get_setting("SLUG_FIELD"))
|
|
61
|
+
return get_slug_field(self.slug_field)
|
|
68
62
|
|
|
69
63
|
def _get_strategies(self) -> tuple[StrategyName, ...]:
|
|
70
64
|
if self.lookup_strategies is not None:
|
|
@@ -78,7 +72,7 @@ class DisplayIDObjectMixin:
|
|
|
78
72
|
explicitly disable), otherwise falls back to the model's
|
|
79
73
|
display_id_prefix attribute.
|
|
80
74
|
"""
|
|
81
|
-
if self.display_id_prefix is not
|
|
75
|
+
if self.display_id_prefix is not NOT_SET:
|
|
82
76
|
if self.display_id_prefix is not None and not PREFIX_PATTERN.match(
|
|
83
77
|
self.display_id_prefix
|
|
84
78
|
):
|
|
@@ -146,5 +140,5 @@ class DisplayIDObjectMixin:
|
|
|
146
140
|
raise Http404(str(e)) from e
|
|
147
141
|
except (InvalidIdentifierError, UnknownPrefixError) as e:
|
|
148
142
|
raise Http404(str(e)) from e
|
|
149
|
-
except
|
|
143
|
+
except DisplayIDLookupError as e:
|
|
150
144
|
raise Http404(str(e)) from e
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: django-display-ids
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Stripe-like prefixed IDs for Django. Works with existing UUIDs — no schema changes.
|
|
5
5
|
Keywords: django,stripe,uuid,base62,prefixed-id,drf,shortuuid,nanoid,ulid
|
|
6
6
|
License: MIT
|
|
@@ -19,6 +19,7 @@ Requires-Dist: django>=4.2
|
|
|
19
19
|
Requires-Python: >=3.12
|
|
20
20
|
Project-URL: Documentation, https://django-display-ids.readthedocs.io/
|
|
21
21
|
Project-URL: Repository, https://github.com/josephabrahams/django-display-ids
|
|
22
|
+
Project-URL: Changelog, https://github.com/josephabrahams/django-display-ids/blob/main/CHANGELOG.md
|
|
22
23
|
Description-Content-Type: text/markdown
|
|
23
24
|
|
|
24
25
|
# django-display-ids
|
|
@@ -66,9 +67,9 @@ INSTALLED_APPS = [
|
|
|
66
67
|
|
|
67
68
|
```python
|
|
68
69
|
from django.views.generic import DetailView
|
|
69
|
-
from django_display_ids import
|
|
70
|
+
from django_display_ids import DisplayIDMixin
|
|
70
71
|
|
|
71
|
-
class InvoiceDetailView(
|
|
72
|
+
class InvoiceDetailView(DisplayIDMixin, DetailView):
|
|
72
73
|
model = Invoice
|
|
73
74
|
lookup_param = "id"
|
|
74
75
|
lookup_strategies = ("display_id", "uuid", "slug")
|
|
@@ -79,9 +80,9 @@ class InvoiceDetailView(DisplayIDObjectMixin, DetailView):
|
|
|
79
80
|
|
|
80
81
|
```python
|
|
81
82
|
from rest_framework.viewsets import ModelViewSet
|
|
82
|
-
from django_display_ids.contrib.rest_framework import
|
|
83
|
+
from django_display_ids.contrib.rest_framework import DisplayIDMixin
|
|
83
84
|
|
|
84
|
-
class InvoiceViewSet(
|
|
85
|
+
class InvoiceViewSet(DisplayIDMixin, ModelViewSet):
|
|
85
86
|
queryset = Invoice.objects.all()
|
|
86
87
|
serializer_class = InvoiceSerializer
|
|
87
88
|
lookup_url_kwarg = "id"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
django_display_ids/__init__.py,sha256=zFd0XNNOeVgpIImWBe_b-HD59svbVwkDVYq8X6lsLug,3256
|
|
2
|
+
django_display_ids/admin.py,sha256=UPmU-kGsZ5x_-r9n99P17La5lr-3BLvB9qTfWJgklt8,2569
|
|
3
|
+
django_display_ids/apps.py,sha256=UqblGiYNONOIEH-giEAuKp1YDgxl2yf0jS0ELMj1iig,315
|
|
4
|
+
django_display_ids/conf.py,sha256=RXcWGyRfLS9J5fyUaoM5ASq2rwOFSxX22tvSX-TB2ig,2215
|
|
5
|
+
django_display_ids/contrib/__init__.py,sha256=sxGJK8Whb6cL7RqACqGRYrIZvaMwP3l6dYk3mIYWzDY,62
|
|
6
|
+
django_display_ids/contrib/drf_spectacular/__init__.py,sha256=9swSJge_dQldC4AZMkYI3M04LzU20eJ_2oz3XABviFA,5427
|
|
7
|
+
django_display_ids/contrib/rest_framework/__init__.py,sha256=pKV6BVB7k0dJm29C7XMr0M-wK2lXPUgW8Hf4BZHHwuE,198
|
|
8
|
+
django_display_ids/contrib/rest_framework/serializers.py,sha256=Jp-z7qHafxkGNYv30YC9rrqLt936SrhONJv3rqfQaC0,4049
|
|
9
|
+
django_display_ids/contrib/rest_framework/views.py,sha256=I6jpsdpM1sRKrPBVyrjypsKgFyUdu6WljGcffHXDiCw,5961
|
|
10
|
+
django_display_ids/converters.py,sha256=KGD5FYWkuXztO-6TvTtPwMP7xbnAXAaYuk9LvIpxkBM,5918
|
|
11
|
+
django_display_ids/encoding.py,sha256=csIwUZaQKSOLwRU6-DWGTNGvSxmroyK0Yt7TBCo0AFE,2945
|
|
12
|
+
django_display_ids/examples.py,sha256=gap5NNPTmE7B5uxiYKoMoK8G-OEtL1Ek0W039l6oJ9I,2689
|
|
13
|
+
django_display_ids/exceptions.py,sha256=ATIW6SLM9zQ0s-26c3qdQfnPDbuiMpMZK6fxdyQN0tA,3085
|
|
14
|
+
django_display_ids/managers.py,sha256=I7qY1V64lDPplhE56ueARICFOHEQqCJhme6LQfBWo_o,11031
|
|
15
|
+
django_display_ids/models.py,sha256=_hmgAR4HC3I-5wU2DND6uNLjEllu1Y9eaXeBQ9dWMNI,4313
|
|
16
|
+
django_display_ids/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
+
django_display_ids/resolver.py,sha256=YhgfkBEVKFsAA9UB66cbyND46sWYWBHPqOJy-yQWstA,3229
|
|
18
|
+
django_display_ids/strategies.py,sha256=Rq00-AW_FB8-K04u2oBK5J6kPiYgsE3TdYlLyK_zro0,4436
|
|
19
|
+
django_display_ids/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
+
django_display_ids/templatetags/display_ids.py,sha256=4KHE8r8mgSKb7LgIuXJaJB_3UGrzRZvTdLqSCYQtb5I,1157
|
|
21
|
+
django_display_ids/typing.py,sha256=2O3kT7XKkiE7WI9A5KkILPM-Zi7-zCy5gVvXQL_J2mI,478
|
|
22
|
+
django_display_ids/views.py,sha256=uAXQJryWQNnTSHIjTLhisrYnNiaTo1pLzkUdvYyOLRs,4992
|
|
23
|
+
django_display_ids-0.4.1.dist-info/WHEEL,sha256=iHtWm8nRfs0VRdCYVXocAWFW8ppjHL-uTJkAdZJKOBM,80
|
|
24
|
+
django_display_ids-0.4.1.dist-info/METADATA,sha256=irrOfygrirWeDRa2mCNHvsnKpb6VJRd_8Ty1orTkRGg,5359
|
|
25
|
+
django_display_ids-0.4.1.dist-info/RECORD,,
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
django_display_ids/__init__.py,sha256=wwFvtGjdQia56uY893Jz5iSwaqyH4KXbgvd3nxdzGss,3496
|
|
2
|
-
django_display_ids/admin.py,sha256=_voqWbr8AwPRC_uCTJWTcEhAhc7RZUgvs7DyVsutDuw,3046
|
|
3
|
-
django_display_ids/apps.py,sha256=UqblGiYNONOIEH-giEAuKp1YDgxl2yf0jS0ELMj1iig,315
|
|
4
|
-
django_display_ids/conf.py,sha256=6bHdUwDkuroYEPmOlPiAc49EcxlOG_QO0aHHawckOe8,1404
|
|
5
|
-
django_display_ids/contrib/__init__.py,sha256=sxGJK8Whb6cL7RqACqGRYrIZvaMwP3l6dYk3mIYWzDY,62
|
|
6
|
-
django_display_ids/contrib/drf_spectacular/__init__.py,sha256=21n56CH7tp2lKq4EGW99KVEgVB9fJ5GnfllUW_KrIe8,4003
|
|
7
|
-
django_display_ids/contrib/rest_framework/__init__.py,sha256=Xun6zMhCZzQJZQ7ywvBYoKhTJIZEV84AntrbSWfBjYI,1567
|
|
8
|
-
django_display_ids/contrib/rest_framework/serializers.py,sha256=Jp-z7qHafxkGNYv30YC9rrqLt936SrhONJv3rqfQaC0,4049
|
|
9
|
-
django_display_ids/contrib/rest_framework/views.py,sha256=nDaze7MJwVqL0MrxQLOur3sPVrRXud_WT4ijGR02jDY,6087
|
|
10
|
-
django_display_ids/converters.py,sha256=ElwrfA7DXiadSZ-Sjvl6ZALgH7tfEZ-tLI7UdE6MsAs,5797
|
|
11
|
-
django_display_ids/encoding.py,sha256=csIwUZaQKSOLwRU6-DWGTNGvSxmroyK0Yt7TBCo0AFE,2945
|
|
12
|
-
django_display_ids/examples.py,sha256=gap5NNPTmE7B5uxiYKoMoK8G-OEtL1Ek0W039l6oJ9I,2689
|
|
13
|
-
django_display_ids/exceptions.py,sha256=nmyRfpsqVvz226Zcu_QANwr8MudbfoX09mAgOCwuPuQ,3022
|
|
14
|
-
django_display_ids/managers.py,sha256=PymcK4BZL6UsUOtoloHP34MCRNmvNHSKEcOImhZxGag,9779
|
|
15
|
-
django_display_ids/models.py,sha256=XI73N4bvxy1Pr2oHeTaJP3uq3huyCX67CFZ2T8mefsA,4317
|
|
16
|
-
django_display_ids/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
-
django_display_ids/resolver.py,sha256=ZlDVoxX0PmVf0MSwPyiNNwQVzdqJGDGE8fm2iyV7QjE,2848
|
|
18
|
-
django_display_ids/strategies.py,sha256=Rq00-AW_FB8-K04u2oBK5J6kPiYgsE3TdYlLyK_zro0,4436
|
|
19
|
-
django_display_ids/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
-
django_display_ids/templatetags/display_ids.py,sha256=4KHE8r8mgSKb7LgIuXJaJB_3UGrzRZvTdLqSCYQtb5I,1157
|
|
21
|
-
django_display_ids/typing.py,sha256=2O3kT7XKkiE7WI9A5KkILPM-Zi7-zCy5gVvXQL_J2mI,478
|
|
22
|
-
django_display_ids/views.py,sha256=-y_Zwo4QLU0lPRPjABpijsze5vsG0CBvJtrVwVtuLwM,5127
|
|
23
|
-
django_display_ids-0.3.2.dist-info/WHEEL,sha256=fAguSjoiATBe7TNBkJwOjyL1Tt4wwiaQGtNtjRPNMQA,80
|
|
24
|
-
django_display_ids-0.3.2.dist-info/METADATA,sha256=qlsCntznDOhR1wMFyPhU2AAnWyodabq1nMHtQm2HczA,5283
|
|
25
|
-
django_display_ids-0.3.2.dist-info/RECORD,,
|