django-display-ids 0.4.1__py3-none-any.whl → 0.5.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/conf.py +2 -2
- django_display_ids/contrib/rest_framework/views.py +0 -6
- django_display_ids/exceptions.py +40 -6
- django_display_ids/managers.py +147 -42
- django_display_ids/resolver.py +7 -0
- django_display_ids/typing.py +4 -3
- django_display_ids/views.py +1 -11
- {django_display_ids-0.4.1.dist-info → django_display_ids-0.5.1.dist-info}/METADATA +1 -3
- {django_display_ids-0.4.1.dist-info → django_display_ids-0.5.1.dist-info}/RECORD +10 -10
- {django_display_ids-0.4.1.dist-info → django_display_ids-0.5.1.dist-info}/WHEEL +0 -0
django_display_ids/conf.py
CHANGED
|
@@ -5,7 +5,7 @@ Settings can be configured in Django settings under the DISPLAY_IDS namespace:
|
|
|
5
5
|
DISPLAY_IDS = {
|
|
6
6
|
"UUID_FIELD": "uid",
|
|
7
7
|
"SLUG_FIELD": "slug",
|
|
8
|
-
"STRATEGIES": ("display_id", "uuid"),
|
|
8
|
+
"STRATEGIES": ("display_id", "uuid", "slug"),
|
|
9
9
|
}
|
|
10
10
|
"""
|
|
11
11
|
|
|
@@ -37,7 +37,7 @@ SLUG_REGEX: str = SlugConverter.regex
|
|
|
37
37
|
DEFAULTS: dict[str, str | tuple[str, ...]] = {
|
|
38
38
|
"UUID_FIELD": "id",
|
|
39
39
|
"SLUG_FIELD": "slug",
|
|
40
|
-
"STRATEGIES": ("display_id", "uuid"),
|
|
40
|
+
"STRATEGIES": ("display_id", "uuid", "slug"),
|
|
41
41
|
"SLUG_REGEX": SLUG_REGEX,
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -13,9 +13,7 @@ from django_display_ids.conf import (
|
|
|
13
13
|
from django_display_ids.encoding import PREFIX_PATTERN
|
|
14
14
|
from django_display_ids.exceptions import (
|
|
15
15
|
DisplayIDLookupError,
|
|
16
|
-
InvalidIdentifierError,
|
|
17
16
|
ObjectNotFoundError,
|
|
18
|
-
UnknownPrefixError,
|
|
19
17
|
)
|
|
20
18
|
from django_display_ids.resolver import resolve_object
|
|
21
19
|
from django_display_ids.typing import StrategyName # noqa: TC001 - used at runtime
|
|
@@ -57,7 +55,6 @@ class DisplayIDMixin:
|
|
|
57
55
|
Example:
|
|
58
56
|
class InvoiceView(DisplayIDMixin, APIView):
|
|
59
57
|
lookup_url_kwarg = "id"
|
|
60
|
-
lookup_strategies = ("display_id", "uuid")
|
|
61
58
|
display_id_prefix = "inv"
|
|
62
59
|
|
|
63
60
|
def get(self, request, *args, **kwargs):
|
|
@@ -69,7 +66,6 @@ class DisplayIDMixin:
|
|
|
69
66
|
queryset = Invoice.objects.all()
|
|
70
67
|
serializer_class = InvoiceSerializer
|
|
71
68
|
lookup_url_kwarg = "pk"
|
|
72
|
-
lookup_strategies = ("display_id", "uuid")
|
|
73
69
|
"""
|
|
74
70
|
|
|
75
71
|
lookup_url_kwarg: str = "pk"
|
|
@@ -165,8 +161,6 @@ class DisplayIDMixin:
|
|
|
165
161
|
)
|
|
166
162
|
except ObjectNotFoundError as e:
|
|
167
163
|
raise NotFound(str(e)) from e
|
|
168
|
-
except (InvalidIdentifierError, UnknownPrefixError) as e:
|
|
169
|
-
raise ParseError(str(e)) from e
|
|
170
164
|
except DisplayIDLookupError as e:
|
|
171
165
|
raise ParseError(str(e)) from e
|
|
172
166
|
|
django_display_ids/exceptions.py
CHANGED
|
@@ -1,7 +1,26 @@
|
|
|
1
|
-
"""Typed exceptions for identifier lookup errors.
|
|
1
|
+
"""Typed exceptions for identifier lookup errors.
|
|
2
|
+
|
|
3
|
+
All exceptions inherit from both ``DisplayIDLookupError`` and a standard
|
|
4
|
+
Django/Python exception, so they integrate naturally with existing error
|
|
5
|
+
handling patterns::
|
|
6
|
+
|
|
7
|
+
# Catch with library-specific base
|
|
8
|
+
except DisplayIDLookupError: ...
|
|
9
|
+
|
|
10
|
+
# Or catch with standard Django/Python exceptions
|
|
11
|
+
except ObjectDoesNotExist: ... # catches ObjectNotFoundError
|
|
12
|
+
except ValueError: ... # catches InvalidIdentifierError, UnknownPrefixError
|
|
13
|
+
except ImproperlyConfigured: ... # catches MissingPrefixError
|
|
14
|
+
"""
|
|
2
15
|
|
|
3
16
|
from __future__ import annotations
|
|
4
17
|
|
|
18
|
+
from django.core.exceptions import (
|
|
19
|
+
ImproperlyConfigured,
|
|
20
|
+
MultipleObjectsReturned,
|
|
21
|
+
ObjectDoesNotExist,
|
|
22
|
+
)
|
|
23
|
+
|
|
5
24
|
__all__ = [
|
|
6
25
|
"AmbiguousIdentifierError",
|
|
7
26
|
"DisplayIDLookupError",
|
|
@@ -18,11 +37,14 @@ class DisplayIDLookupError(Exception):
|
|
|
18
37
|
pass
|
|
19
38
|
|
|
20
39
|
|
|
21
|
-
class InvalidIdentifierError(DisplayIDLookupError):
|
|
40
|
+
class InvalidIdentifierError(DisplayIDLookupError, ValueError):
|
|
22
41
|
"""Raised when an identifier has an invalid format.
|
|
23
42
|
|
|
24
43
|
This indicates the identifier string cannot be parsed as any
|
|
25
44
|
of the supported formats (UUID, display ID, or slug).
|
|
45
|
+
|
|
46
|
+
Inherits from ``ValueError`` because it represents bad input — the
|
|
47
|
+
caller provided a value that isn't a valid identifier.
|
|
26
48
|
"""
|
|
27
49
|
|
|
28
50
|
def __init__(self, value: str, message: str | None = None) -> None:
|
|
@@ -31,11 +53,14 @@ class InvalidIdentifierError(DisplayIDLookupError):
|
|
|
31
53
|
super().__init__(self.message)
|
|
32
54
|
|
|
33
55
|
|
|
34
|
-
class UnknownPrefixError(DisplayIDLookupError):
|
|
56
|
+
class UnknownPrefixError(DisplayIDLookupError, ValueError):
|
|
35
57
|
"""Raised when a display ID has an unexpected prefix.
|
|
36
58
|
|
|
37
59
|
This occurs when prefix enforcement is enabled and the
|
|
38
60
|
display ID's prefix doesn't match the expected value.
|
|
61
|
+
|
|
62
|
+
Inherits from ``ValueError`` because it represents bad input — the
|
|
63
|
+
caller provided a display ID with the wrong prefix.
|
|
39
64
|
"""
|
|
40
65
|
|
|
41
66
|
def __init__(self, value: str, actual: str, expected: str | None = None) -> None:
|
|
@@ -49,11 +74,14 @@ class UnknownPrefixError(DisplayIDLookupError):
|
|
|
49
74
|
super().__init__(message)
|
|
50
75
|
|
|
51
76
|
|
|
52
|
-
class MissingPrefixError(DisplayIDLookupError):
|
|
77
|
+
class MissingPrefixError(DisplayIDLookupError, ImproperlyConfigured):
|
|
53
78
|
"""Raised when a display ID lookup is attempted without a prefix.
|
|
54
79
|
|
|
55
80
|
This occurs when calling get_by_display_id() on a model that
|
|
56
81
|
doesn't have display_id_prefix configured.
|
|
82
|
+
|
|
83
|
+
Inherits from ``ImproperlyConfigured`` because it represents a
|
|
84
|
+
configuration problem — the model is missing a required setting.
|
|
57
85
|
"""
|
|
58
86
|
|
|
59
87
|
def __init__(self, model_name: str | None = None) -> None:
|
|
@@ -68,11 +96,14 @@ class MissingPrefixError(DisplayIDLookupError):
|
|
|
68
96
|
super().__init__(message)
|
|
69
97
|
|
|
70
98
|
|
|
71
|
-
class ObjectNotFoundError(DisplayIDLookupError):
|
|
99
|
+
class ObjectNotFoundError(DisplayIDLookupError, ObjectDoesNotExist):
|
|
72
100
|
"""Raised when no object matches the identifier.
|
|
73
101
|
|
|
74
102
|
This indicates the identifier was valid but no matching
|
|
75
103
|
database record exists.
|
|
104
|
+
|
|
105
|
+
Inherits from ``ObjectDoesNotExist`` so it integrates with Django's
|
|
106
|
+
built-in error handling (e.g., ``get_object_or_404``).
|
|
76
107
|
"""
|
|
77
108
|
|
|
78
109
|
def __init__(self, value: str, model_name: str | None = None) -> None:
|
|
@@ -85,11 +116,14 @@ class ObjectNotFoundError(DisplayIDLookupError):
|
|
|
85
116
|
super().__init__(message)
|
|
86
117
|
|
|
87
118
|
|
|
88
|
-
class AmbiguousIdentifierError(DisplayIDLookupError):
|
|
119
|
+
class AmbiguousIdentifierError(DisplayIDLookupError, MultipleObjectsReturned):
|
|
89
120
|
"""Raised when an identifier matches multiple objects.
|
|
90
121
|
|
|
91
122
|
This can occur with slug lookups if slugs are not unique,
|
|
92
123
|
or in edge cases with identifier collisions.
|
|
124
|
+
|
|
125
|
+
Inherits from ``MultipleObjectsReturned`` because the semantics
|
|
126
|
+
are identical — a lookup that expected one result found many.
|
|
93
127
|
"""
|
|
94
128
|
|
|
95
129
|
def __init__(self, value: str, count: int) -> None:
|
django_display_ids/managers.py
CHANGED
|
@@ -3,19 +3,17 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import uuid
|
|
6
|
-
from typing import TYPE_CHECKING, Any, TypeVar
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Self, TypeVar
|
|
7
7
|
|
|
8
|
+
from django.core.exceptions import FieldDoesNotExist
|
|
8
9
|
from django.db import models
|
|
9
10
|
from django.db.models import Q
|
|
10
11
|
|
|
11
12
|
from .conf import get_setting
|
|
12
13
|
from .encoding import decode_display_id
|
|
13
14
|
from .exceptions import (
|
|
14
|
-
|
|
15
|
-
InvalidIdentifierError,
|
|
15
|
+
DisplayIDLookupError,
|
|
16
16
|
MissingPrefixError,
|
|
17
|
-
ObjectNotFoundError,
|
|
18
|
-
UnknownPrefixError,
|
|
19
17
|
)
|
|
20
18
|
from .strategies import parse_identifier
|
|
21
19
|
|
|
@@ -50,6 +48,33 @@ class DisplayIDQuerySet(models.QuerySet[M]):
|
|
|
50
48
|
invoice = Invoice.objects.get_by_display_id("inv_1a2B3c4D5e6F7g8H")
|
|
51
49
|
"""
|
|
52
50
|
|
|
51
|
+
# Re-annotate inherited QuerySet methods with -> Self so that
|
|
52
|
+
# display ID methods remain visible to type checkers after chaining
|
|
53
|
+
# (e.g. Invoice.objects.filter(...).get_by_identifier(...)).
|
|
54
|
+
def filter(self, *args: Any, **kwargs: Any) -> Self:
|
|
55
|
+
return super().filter(*args, **kwargs)
|
|
56
|
+
|
|
57
|
+
def exclude(self, *args: Any, **kwargs: Any) -> Self:
|
|
58
|
+
return super().exclude(*args, **kwargs)
|
|
59
|
+
|
|
60
|
+
def select_related(self, *fields: Any) -> Self:
|
|
61
|
+
return super().select_related(*fields)
|
|
62
|
+
|
|
63
|
+
def prefetch_related(self, *lookups: Any) -> Self:
|
|
64
|
+
return super().prefetch_related(*lookups)
|
|
65
|
+
|
|
66
|
+
def order_by(self, *fields: Any) -> Self:
|
|
67
|
+
return super().order_by(*fields)
|
|
68
|
+
|
|
69
|
+
def distinct(self, *fields: Any) -> Self:
|
|
70
|
+
return super().distinct(*fields)
|
|
71
|
+
|
|
72
|
+
def all(self) -> Self:
|
|
73
|
+
return super().all()
|
|
74
|
+
|
|
75
|
+
def none(self) -> Self:
|
|
76
|
+
return super().none()
|
|
77
|
+
|
|
53
78
|
def get_by_display_id(
|
|
54
79
|
self,
|
|
55
80
|
value: str | uuid.UUID,
|
|
@@ -67,22 +92,16 @@ class DisplayIDQuerySet(models.QuerySet[M]):
|
|
|
67
92
|
The matching model instance.
|
|
68
93
|
|
|
69
94
|
Raises:
|
|
95
|
+
Model.DoesNotExist: If no matching object exists, if the display ID
|
|
96
|
+
format is invalid, or if the prefix doesn't match.
|
|
70
97
|
MissingPrefixError: If no prefix is configured on the model.
|
|
71
|
-
InvalidIdentifierError: If the display ID format is invalid.
|
|
72
|
-
UnknownPrefixError: If the prefix doesn't match expected.
|
|
73
|
-
ObjectNotFoundError: If no matching object exists.
|
|
74
98
|
"""
|
|
75
99
|
model = self.model
|
|
76
100
|
uuid_field = self._get_uuid_field()
|
|
77
101
|
|
|
78
102
|
# UUID objects skip display ID parsing entirely
|
|
79
103
|
if isinstance(value, uuid.UUID):
|
|
80
|
-
|
|
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
|
|
104
|
+
return self.get(**{uuid_field: value})
|
|
86
105
|
|
|
87
106
|
# Get model config
|
|
88
107
|
expected_prefix = prefix or self._get_model_prefix()
|
|
@@ -91,23 +110,22 @@ class DisplayIDQuerySet(models.QuerySet[M]):
|
|
|
91
110
|
if expected_prefix is None:
|
|
92
111
|
raise MissingPrefixError(model_name=model.__name__)
|
|
93
112
|
|
|
94
|
-
# Decode the display ID
|
|
113
|
+
# Decode the display ID and validate prefix
|
|
95
114
|
try:
|
|
96
115
|
decoded_prefix, uuid_value = decode_display_id(value)
|
|
97
116
|
except ValueError as e:
|
|
98
|
-
raise
|
|
117
|
+
raise model.DoesNotExist( # type: ignore[attr-defined]
|
|
118
|
+
f"{model.__name__}: invalid display ID: {value!r}"
|
|
119
|
+
) from e
|
|
99
120
|
|
|
100
|
-
# Validate prefix
|
|
101
121
|
if decoded_prefix != expected_prefix:
|
|
102
|
-
raise
|
|
103
|
-
|
|
122
|
+
raise model.DoesNotExist( # type: ignore[attr-defined]
|
|
123
|
+
f"{model.__name__}: unknown prefix {decoded_prefix!r} "
|
|
124
|
+
f"in {value!r}, expected {expected_prefix!r}"
|
|
104
125
|
)
|
|
105
126
|
|
|
106
127
|
# Query the database
|
|
107
|
-
|
|
108
|
-
return self.get(**{uuid_field: uuid_value})
|
|
109
|
-
except model.DoesNotExist: # type: ignore[attr-defined]
|
|
110
|
-
raise ObjectNotFoundError(value, model_name=model.__name__) from None
|
|
128
|
+
return self.get(**{uuid_field: uuid_value})
|
|
111
129
|
|
|
112
130
|
def get_by_identifier(
|
|
113
131
|
self,
|
|
@@ -130,31 +148,34 @@ class DisplayIDQuerySet(models.QuerySet[M]):
|
|
|
130
148
|
The matching model instance.
|
|
131
149
|
|
|
132
150
|
Raises:
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
AmbiguousIdentifierError: If multiple objects match (slug).
|
|
151
|
+
Model.DoesNotExist: If the identifier cannot be parsed or
|
|
152
|
+
no matching object exists.
|
|
153
|
+
Model.MultipleObjectsReturned: If multiple objects match (slug).
|
|
137
154
|
"""
|
|
138
155
|
model = self.model
|
|
139
156
|
uuid_field = self._get_uuid_field()
|
|
140
157
|
|
|
141
158
|
# UUID objects skip strategy parsing entirely
|
|
142
159
|
if isinstance(value, uuid.UUID):
|
|
143
|
-
|
|
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
|
|
160
|
+
return self.get(**{uuid_field: value})
|
|
149
161
|
|
|
150
162
|
slug_field = self._get_slug_field()
|
|
151
163
|
expected_prefix = prefix or self._get_model_prefix()
|
|
152
164
|
lookup_strategies = strategies or self._get_strategies()
|
|
153
165
|
|
|
166
|
+
# Skip slug strategy if the model has no slug field
|
|
167
|
+
if not self._has_slug_field(slug_field):
|
|
168
|
+
lookup_strategies = tuple(s for s in lookup_strategies if s != "slug")
|
|
169
|
+
|
|
154
170
|
# Parse the identifier
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
171
|
+
try:
|
|
172
|
+
result = parse_identifier(
|
|
173
|
+
value, lookup_strategies, expected_prefix=expected_prefix
|
|
174
|
+
)
|
|
175
|
+
except DisplayIDLookupError as e:
|
|
176
|
+
raise model.DoesNotExist( # type: ignore[attr-defined]
|
|
177
|
+
f"{model.__name__}: {e}"
|
|
178
|
+
) from e
|
|
158
179
|
|
|
159
180
|
# Build the lookup
|
|
160
181
|
lookup: dict[str, Any]
|
|
@@ -164,13 +185,70 @@ class DisplayIDQuerySet(models.QuerySet[M]):
|
|
|
164
185
|
lookup = {slug_field: result.slug}
|
|
165
186
|
|
|
166
187
|
# Execute the query
|
|
188
|
+
return self.get(**lookup)
|
|
189
|
+
|
|
190
|
+
def resolve_identifier(
|
|
191
|
+
self,
|
|
192
|
+
value: str | uuid.UUID,
|
|
193
|
+
*,
|
|
194
|
+
strategies: tuple[StrategyName, ...] | None = None,
|
|
195
|
+
prefix: str | None = None,
|
|
196
|
+
) -> uuid.UUID:
|
|
197
|
+
"""Resolve an identifier to a UUID without fetching the object.
|
|
198
|
+
|
|
199
|
+
For UUID and display_id identifiers, the UUID is extracted by parsing
|
|
200
|
+
alone — no database query is needed. Only slug identifiers require a
|
|
201
|
+
database lookup.
|
|
202
|
+
|
|
203
|
+
This is useful for cursor-based pagination where you need the UUID
|
|
204
|
+
value to build a WHERE clause but don't need the full model instance.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
value: The identifier string (display ID, UUID, or slug),
|
|
208
|
+
or a UUID instance (returned as-is).
|
|
209
|
+
strategies: Strategies to try. Defaults to settings.
|
|
210
|
+
prefix: Expected display ID prefix for validation.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
The resolved UUID value.
|
|
214
|
+
|
|
215
|
+
Raises:
|
|
216
|
+
Model.DoesNotExist: If the identifier cannot be parsed or
|
|
217
|
+
no matching object exists (slug lookup).
|
|
218
|
+
Model.MultipleObjectsReturned: If multiple objects match (slug).
|
|
219
|
+
"""
|
|
220
|
+
model = self.model
|
|
221
|
+
uuid_field = self._get_uuid_field()
|
|
222
|
+
|
|
223
|
+
# UUID objects are returned as-is
|
|
224
|
+
if isinstance(value, uuid.UUID):
|
|
225
|
+
return value
|
|
226
|
+
|
|
227
|
+
slug_field = self._get_slug_field()
|
|
228
|
+
expected_prefix = prefix or self._get_model_prefix()
|
|
229
|
+
lookup_strategies = strategies or self._get_strategies()
|
|
230
|
+
|
|
231
|
+
# Skip slug strategy if the model has no slug field
|
|
232
|
+
if not self._has_slug_field(slug_field):
|
|
233
|
+
lookup_strategies = tuple(s for s in lookup_strategies if s != "slug")
|
|
234
|
+
|
|
235
|
+
# Parse the identifier
|
|
167
236
|
try:
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
except
|
|
172
|
-
|
|
173
|
-
|
|
237
|
+
result = parse_identifier(
|
|
238
|
+
value, lookup_strategies, expected_prefix=expected_prefix
|
|
239
|
+
)
|
|
240
|
+
except DisplayIDLookupError as e:
|
|
241
|
+
raise model.DoesNotExist( # type: ignore[attr-defined]
|
|
242
|
+
f"{model.__name__}: {e}"
|
|
243
|
+
) from e
|
|
244
|
+
|
|
245
|
+
# UUID and display_id strategies yield a UUID directly — no DB query
|
|
246
|
+
if result.strategy in ("uuid", "display_id"):
|
|
247
|
+
return result.uuid # type: ignore[return-value]
|
|
248
|
+
|
|
249
|
+
# Slug strategy requires a DB lookup
|
|
250
|
+
obj = self.get(**{slug_field: result.slug})
|
|
251
|
+
return getattr(obj, uuid_field) # type: ignore[no-any-return]
|
|
174
252
|
|
|
175
253
|
def get_by_identifiers(
|
|
176
254
|
self,
|
|
@@ -213,6 +291,10 @@ class DisplayIDQuerySet(models.QuerySet[M]):
|
|
|
213
291
|
expected_prefix = prefix or self._get_model_prefix()
|
|
214
292
|
lookup_strategies = strategies or self._get_strategies()
|
|
215
293
|
|
|
294
|
+
# Skip slug strategy if the model has no slug field
|
|
295
|
+
if not self._has_slug_field(slug_field):
|
|
296
|
+
lookup_strategies = tuple(s for s in lookup_strategies if s != "slug")
|
|
297
|
+
|
|
216
298
|
# Collect UUIDs and slugs separately
|
|
217
299
|
uuids: list[Any] = []
|
|
218
300
|
slugs: list[str] = []
|
|
@@ -258,6 +340,14 @@ class DisplayIDQuerySet(models.QuerySet[M]):
|
|
|
258
340
|
"""Get the default strategies."""
|
|
259
341
|
return get_setting("STRATEGIES") # type: ignore[return-value]
|
|
260
342
|
|
|
343
|
+
def _has_slug_field(self, slug_field: str) -> bool:
|
|
344
|
+
"""Check whether the model has the configured slug field."""
|
|
345
|
+
try:
|
|
346
|
+
self.model._meta.get_field(slug_field)
|
|
347
|
+
return True
|
|
348
|
+
except FieldDoesNotExist:
|
|
349
|
+
return False
|
|
350
|
+
|
|
261
351
|
def _get_model_prefix(self) -> str | None:
|
|
262
352
|
"""Get the display ID prefix from the model, if defined."""
|
|
263
353
|
if hasattr(self.model, "get_display_id_prefix"):
|
|
@@ -308,6 +398,21 @@ class DisplayIDManager(models.Manager[M]):
|
|
|
308
398
|
value, strategies=strategies, prefix=prefix
|
|
309
399
|
)
|
|
310
400
|
|
|
401
|
+
def resolve_identifier(
|
|
402
|
+
self,
|
|
403
|
+
value: str | uuid.UUID,
|
|
404
|
+
*,
|
|
405
|
+
strategies: tuple[StrategyName, ...] | None = None,
|
|
406
|
+
prefix: str | None = None,
|
|
407
|
+
) -> uuid.UUID:
|
|
408
|
+
"""Resolve an identifier to a UUID without fetching the object.
|
|
409
|
+
|
|
410
|
+
See DisplayIDQuerySet.resolve_identifier for details.
|
|
411
|
+
"""
|
|
412
|
+
return self.get_queryset().resolve_identifier(
|
|
413
|
+
value, strategies=strategies, prefix=prefix
|
|
414
|
+
)
|
|
415
|
+
|
|
311
416
|
def get_by_identifiers(
|
|
312
417
|
self,
|
|
313
418
|
values: Sequence[str | uuid.UUID],
|
django_display_ids/resolver.py
CHANGED
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import uuid
|
|
6
6
|
from typing import TYPE_CHECKING, Any, TypeVar
|
|
7
7
|
|
|
8
|
+
from django.core.exceptions import FieldDoesNotExist
|
|
8
9
|
from django.db import models
|
|
9
10
|
|
|
10
11
|
from .exceptions import AmbiguousIdentifierError, ObjectNotFoundError
|
|
@@ -73,6 +74,12 @@ def resolve_object(
|
|
|
73
74
|
except model.DoesNotExist: # type: ignore[attr-defined]
|
|
74
75
|
raise ObjectNotFoundError(str(value), model_name=model.__name__) from None
|
|
75
76
|
|
|
77
|
+
# Skip slug strategy if the model has no slug field
|
|
78
|
+
try:
|
|
79
|
+
model._meta.get_field(slug_field)
|
|
80
|
+
except FieldDoesNotExist:
|
|
81
|
+
strategies = tuple(s for s in strategies if s != "slug")
|
|
82
|
+
|
|
76
83
|
# Parse the identifier to determine type
|
|
77
84
|
result = parse_identifier(value, strategies, expected_prefix=prefix)
|
|
78
85
|
|
django_display_ids/typing.py
CHANGED
|
@@ -12,6 +12,7 @@ __all__ = [
|
|
|
12
12
|
# Supported lookup strategy names
|
|
13
13
|
StrategyName = Literal["uuid", "display_id", "slug"]
|
|
14
14
|
|
|
15
|
-
# Default strategy order: display_id first (most specific), then uuid
|
|
16
|
-
# Slug is
|
|
17
|
-
|
|
15
|
+
# Default strategy order: display_id first (most specific), then uuid, then slug
|
|
16
|
+
# Slug is a catch-all — it's safe to include by default because the manager
|
|
17
|
+
# and resolver automatically skip it for models without a slug field.
|
|
18
|
+
DEFAULT_STRATEGIES: tuple[StrategyName, ...] = ("display_id", "uuid", "slug")
|
django_display_ids/views.py
CHANGED
|
@@ -8,12 +8,7 @@ from django.http import Http404
|
|
|
8
8
|
|
|
9
9
|
from .conf import NOT_SET, get_setting, get_slug_field, get_uuid_field
|
|
10
10
|
from .encoding import PREFIX_PATTERN
|
|
11
|
-
from .exceptions import
|
|
12
|
-
DisplayIDLookupError,
|
|
13
|
-
InvalidIdentifierError,
|
|
14
|
-
ObjectNotFoundError,
|
|
15
|
-
UnknownPrefixError,
|
|
16
|
-
)
|
|
11
|
+
from .exceptions import DisplayIDLookupError
|
|
17
12
|
from .resolver import resolve_object
|
|
18
13
|
from .typing import StrategyName # noqa: TC001 - used at runtime in type hints
|
|
19
14
|
|
|
@@ -43,7 +38,6 @@ class DisplayIDMixin:
|
|
|
43
38
|
class InvoiceDetailView(DisplayIDMixin, DetailView):
|
|
44
39
|
model = Invoice
|
|
45
40
|
lookup_param = "id"
|
|
46
|
-
lookup_strategies = ("display_id", "uuid")
|
|
47
41
|
display_id_prefix = "inv"
|
|
48
42
|
"""
|
|
49
43
|
|
|
@@ -136,9 +130,5 @@ class DisplayIDMixin:
|
|
|
136
130
|
slug_field=self._get_slug_field(),
|
|
137
131
|
queryset=qs,
|
|
138
132
|
)
|
|
139
|
-
except ObjectNotFoundError as e:
|
|
140
|
-
raise Http404(str(e)) from e
|
|
141
|
-
except (InvalidIdentifierError, UnknownPrefixError) as e:
|
|
142
|
-
raise Http404(str(e)) from e
|
|
143
133
|
except DisplayIDLookupError as e:
|
|
144
134
|
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.5.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
|
|
@@ -72,7 +72,6 @@ from django_display_ids import DisplayIDMixin
|
|
|
72
72
|
class InvoiceDetailView(DisplayIDMixin, DetailView):
|
|
73
73
|
model = Invoice
|
|
74
74
|
lookup_param = "id"
|
|
75
|
-
lookup_strategies = ("display_id", "uuid", "slug")
|
|
76
75
|
display_id_prefix = "inv"
|
|
77
76
|
```
|
|
78
77
|
|
|
@@ -86,7 +85,6 @@ class InvoiceViewSet(DisplayIDMixin, ModelViewSet):
|
|
|
86
85
|
queryset = Invoice.objects.all()
|
|
87
86
|
serializer_class = InvoiceSerializer
|
|
88
87
|
lookup_url_kwarg = "id"
|
|
89
|
-
lookup_strategies = ("display_id", "uuid", "slug")
|
|
90
88
|
display_id_prefix = "inv"
|
|
91
89
|
```
|
|
92
90
|
|
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
django_display_ids/__init__.py,sha256=zFd0XNNOeVgpIImWBe_b-HD59svbVwkDVYq8X6lsLug,3256
|
|
2
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=cBL_b8AW13hUYmN7mMViNQyndZisBxXU-1vSLLIxzv4,2231
|
|
5
5
|
django_display_ids/contrib/__init__.py,sha256=sxGJK8Whb6cL7RqACqGRYrIZvaMwP3l6dYk3mIYWzDY,62
|
|
6
6
|
django_display_ids/contrib/drf_spectacular/__init__.py,sha256=9swSJge_dQldC4AZMkYI3M04LzU20eJ_2oz3XABviFA,5427
|
|
7
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=88qe3w5QL0WNCOY17WjbXl8vrVzLDD149QIEfSl6tCY,5689
|
|
10
10
|
django_display_ids/converters.py,sha256=KGD5FYWkuXztO-6TvTtPwMP7xbnAXAaYuk9LvIpxkBM,5918
|
|
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=
|
|
14
|
-
django_display_ids/managers.py,sha256=
|
|
13
|
+
django_display_ids/exceptions.py,sha256=68Kmp9Qdyg3LtzmXfDHIWRuI7MW3M2acZ3C6Wi8zFEw,4489
|
|
14
|
+
django_display_ids/managers.py,sha256=Oy0uOb2n7OSXXH2yCN2Cec5O2tvYty4cYIzJkmHbj_8,14716
|
|
15
15
|
django_display_ids/models.py,sha256=_hmgAR4HC3I-5wU2DND6uNLjEllu1Y9eaXeBQ9dWMNI,4313
|
|
16
16
|
django_display_ids/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
-
django_display_ids/resolver.py,sha256=
|
|
17
|
+
django_display_ids/resolver.py,sha256=blyLo06tIYrYqOA9Ir260tzs9njX1GMAE6ICDJTwfGk,3485
|
|
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
|
-
django_display_ids/typing.py,sha256=
|
|
22
|
-
django_display_ids/views.py,sha256=
|
|
23
|
-
django_display_ids-0.
|
|
24
|
-
django_display_ids-0.
|
|
25
|
-
django_display_ids-0.
|
|
21
|
+
django_display_ids/typing.py,sha256=hxX2QVYtkvFXZBH9ZskbV98N-uOhX_XAMoO-UOBRnEA,568
|
|
22
|
+
django_display_ids/views.py,sha256=OOhv5G6ake8Siv-yAFFA1s2wOuNng9NYETS1feDygio,4662
|
|
23
|
+
django_display_ids-0.5.1.dist-info/WHEEL,sha256=iHtWm8nRfs0VRdCYVXocAWFW8ppjHL-uTJkAdZJKOBM,80
|
|
24
|
+
django_display_ids-0.5.1.dist-info/METADATA,sha256=WImPRXMdSGsx6FU099koONUtetahmJhAV1LVgzNi1jQ,5249
|
|
25
|
+
django_display_ids-0.5.1.dist-info/RECORD,,
|
|
File without changes
|