django-display-ids 0.3.2__py3-none-any.whl → 0.4.0__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/exceptions.py +7 -7
- django_display_ids/models.py +5 -5
- django_display_ids/views.py +10 -16
- {django_display_ids-0.3.2.dist-info → django_display_ids-0.4.0.dist-info}/METADATA +5 -5
- {django_display_ids-0.3.2.dist-info → django_display_ids-0.4.0.dist-info}/RECORD +12 -12
- {django_display_ids-0.3.2.dist-info → django_display_ids-0.4.0.dist-info}/WHEEL +0 -0
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/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/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/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.0
|
|
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
|
|
@@ -66,9 +66,9 @@ INSTALLED_APPS = [
|
|
|
66
66
|
|
|
67
67
|
```python
|
|
68
68
|
from django.views.generic import DetailView
|
|
69
|
-
from django_display_ids import
|
|
69
|
+
from django_display_ids import DisplayIDMixin
|
|
70
70
|
|
|
71
|
-
class InvoiceDetailView(
|
|
71
|
+
class InvoiceDetailView(DisplayIDMixin, DetailView):
|
|
72
72
|
model = Invoice
|
|
73
73
|
lookup_param = "id"
|
|
74
74
|
lookup_strategies = ("display_id", "uuid", "slug")
|
|
@@ -79,9 +79,9 @@ class InvoiceDetailView(DisplayIDObjectMixin, DetailView):
|
|
|
79
79
|
|
|
80
80
|
```python
|
|
81
81
|
from rest_framework.viewsets import ModelViewSet
|
|
82
|
-
from django_display_ids.contrib.rest_framework import
|
|
82
|
+
from django_display_ids.contrib.rest_framework import DisplayIDMixin
|
|
83
83
|
|
|
84
|
-
class InvoiceViewSet(
|
|
84
|
+
class InvoiceViewSet(DisplayIDMixin, ModelViewSet):
|
|
85
85
|
queryset = Invoice.objects.all()
|
|
86
86
|
serializer_class = InvoiceSerializer
|
|
87
87
|
lookup_url_kwarg = "id"
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
django_display_ids/__init__.py,sha256=
|
|
2
|
-
django_display_ids/admin.py,sha256=
|
|
1
|
+
django_display_ids/__init__.py,sha256=zFd0XNNOeVgpIImWBe_b-HD59svbVwkDVYq8X6lsLug,3256
|
|
2
|
+
django_display_ids/admin.py,sha256=UPmU-kGsZ5x_-r9n99P17La5lr-3BLvB9qTfWJgklt8,2569
|
|
3
3
|
django_display_ids/apps.py,sha256=UqblGiYNONOIEH-giEAuKp1YDgxl2yf0jS0ELMj1iig,315
|
|
4
|
-
django_display_ids/conf.py,sha256=
|
|
4
|
+
django_display_ids/conf.py,sha256=RXcWGyRfLS9J5fyUaoM5ASq2rwOFSxX22tvSX-TB2ig,2215
|
|
5
5
|
django_display_ids/contrib/__init__.py,sha256=sxGJK8Whb6cL7RqACqGRYrIZvaMwP3l6dYk3mIYWzDY,62
|
|
6
|
-
django_display_ids/contrib/drf_spectacular/__init__.py,sha256=
|
|
7
|
-
django_display_ids/contrib/rest_framework/__init__.py,sha256=
|
|
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
8
|
django_display_ids/contrib/rest_framework/serializers.py,sha256=Jp-z7qHafxkGNYv30YC9rrqLt936SrhONJv3rqfQaC0,4049
|
|
9
|
-
django_display_ids/contrib/rest_framework/views.py,sha256=
|
|
9
|
+
django_display_ids/contrib/rest_framework/views.py,sha256=I6jpsdpM1sRKrPBVyrjypsKgFyUdu6WljGcffHXDiCw,5961
|
|
10
10
|
django_display_ids/converters.py,sha256=ElwrfA7DXiadSZ-Sjvl6ZALgH7tfEZ-tLI7UdE6MsAs,5797
|
|
11
11
|
django_display_ids/encoding.py,sha256=csIwUZaQKSOLwRU6-DWGTNGvSxmroyK0Yt7TBCo0AFE,2945
|
|
12
12
|
django_display_ids/examples.py,sha256=gap5NNPTmE7B5uxiYKoMoK8G-OEtL1Ek0W039l6oJ9I,2689
|
|
13
|
-
django_display_ids/exceptions.py,sha256=
|
|
13
|
+
django_display_ids/exceptions.py,sha256=ATIW6SLM9zQ0s-26c3qdQfnPDbuiMpMZK6fxdyQN0tA,3085
|
|
14
14
|
django_display_ids/managers.py,sha256=PymcK4BZL6UsUOtoloHP34MCRNmvNHSKEcOImhZxGag,9779
|
|
15
|
-
django_display_ids/models.py,sha256=
|
|
15
|
+
django_display_ids/models.py,sha256=_hmgAR4HC3I-5wU2DND6uNLjEllu1Y9eaXeBQ9dWMNI,4313
|
|
16
16
|
django_display_ids/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
17
|
django_display_ids/resolver.py,sha256=ZlDVoxX0PmVf0MSwPyiNNwQVzdqJGDGE8fm2iyV7QjE,2848
|
|
18
18
|
django_display_ids/strategies.py,sha256=Rq00-AW_FB8-K04u2oBK5J6kPiYgsE3TdYlLyK_zro0,4436
|
|
19
19
|
django_display_ids/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
20
|
django_display_ids/templatetags/display_ids.py,sha256=4KHE8r8mgSKb7LgIuXJaJB_3UGrzRZvTdLqSCYQtb5I,1157
|
|
21
21
|
django_display_ids/typing.py,sha256=2O3kT7XKkiE7WI9A5KkILPM-Zi7-zCy5gVvXQL_J2mI,478
|
|
22
|
-
django_display_ids/views.py,sha256
|
|
23
|
-
django_display_ids-0.
|
|
24
|
-
django_display_ids-0.
|
|
25
|
-
django_display_ids-0.
|
|
22
|
+
django_display_ids/views.py,sha256=uAXQJryWQNnTSHIjTLhisrYnNiaTo1pLzkUdvYyOLRs,4992
|
|
23
|
+
django_display_ids-0.4.0.dist-info/WHEEL,sha256=fAguSjoiATBe7TNBkJwOjyL1Tt4wwiaQGtNtjRPNMQA,80
|
|
24
|
+
django_display_ids-0.4.0.dist-info/METADATA,sha256=La5mR7VliD_sGMKjFazUtnrgPFPQ1anLrsxWou9_ej8,5259
|
|
25
|
+
django_display_ids-0.4.0.dist-info/RECORD,,
|
|
File without changes
|