django-display-ids 0.4.1__tar.gz → 0.5.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. {django_display_ids-0.4.1 → django_display_ids-0.5.1}/PKG-INFO +1 -3
  2. {django_display_ids-0.4.1 → django_display_ids-0.5.1}/README.md +0 -2
  3. {django_display_ids-0.4.1 → django_display_ids-0.5.1}/pyproject.toml +1 -1
  4. {django_display_ids-0.4.1 → django_display_ids-0.5.1}/src/django_display_ids/conf.py +2 -2
  5. {django_display_ids-0.4.1 → django_display_ids-0.5.1}/src/django_display_ids/contrib/rest_framework/views.py +0 -6
  6. {django_display_ids-0.4.1 → django_display_ids-0.5.1}/src/django_display_ids/exceptions.py +40 -6
  7. {django_display_ids-0.4.1 → django_display_ids-0.5.1}/src/django_display_ids/managers.py +147 -42
  8. {django_display_ids-0.4.1 → django_display_ids-0.5.1}/src/django_display_ids/resolver.py +7 -0
  9. {django_display_ids-0.4.1 → django_display_ids-0.5.1}/src/django_display_ids/typing.py +4 -3
  10. {django_display_ids-0.4.1 → django_display_ids-0.5.1}/src/django_display_ids/views.py +1 -11
  11. {django_display_ids-0.4.1 → django_display_ids-0.5.1}/src/django_display_ids/__init__.py +0 -0
  12. {django_display_ids-0.4.1 → django_display_ids-0.5.1}/src/django_display_ids/admin.py +0 -0
  13. {django_display_ids-0.4.1 → django_display_ids-0.5.1}/src/django_display_ids/apps.py +0 -0
  14. {django_display_ids-0.4.1 → django_display_ids-0.5.1}/src/django_display_ids/contrib/__init__.py +0 -0
  15. {django_display_ids-0.4.1 → django_display_ids-0.5.1}/src/django_display_ids/contrib/drf_spectacular/__init__.py +0 -0
  16. {django_display_ids-0.4.1 → django_display_ids-0.5.1}/src/django_display_ids/contrib/rest_framework/__init__.py +0 -0
  17. {django_display_ids-0.4.1 → django_display_ids-0.5.1}/src/django_display_ids/contrib/rest_framework/serializers.py +0 -0
  18. {django_display_ids-0.4.1 → django_display_ids-0.5.1}/src/django_display_ids/converters.py +0 -0
  19. {django_display_ids-0.4.1 → django_display_ids-0.5.1}/src/django_display_ids/encoding.py +0 -0
  20. {django_display_ids-0.4.1 → django_display_ids-0.5.1}/src/django_display_ids/examples.py +0 -0
  21. {django_display_ids-0.4.1 → django_display_ids-0.5.1}/src/django_display_ids/models.py +0 -0
  22. {django_display_ids-0.4.1 → django_display_ids-0.5.1}/src/django_display_ids/py.typed +0 -0
  23. {django_display_ids-0.4.1 → django_display_ids-0.5.1}/src/django_display_ids/strategies.py +0 -0
  24. {django_display_ids-0.4.1 → django_display_ids-0.5.1}/src/django_display_ids/templatetags/__init__.py +0 -0
  25. {django_display_ids-0.4.1 → django_display_ids-0.5.1}/src/django_display_ids/templatetags/display_ids.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-display-ids
3
- Version: 0.4.1
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
 
@@ -48,7 +48,6 @@ from django_display_ids import DisplayIDMixin
48
48
  class InvoiceDetailView(DisplayIDMixin, DetailView):
49
49
  model = Invoice
50
50
  lookup_param = "id"
51
- lookup_strategies = ("display_id", "uuid", "slug")
52
51
  display_id_prefix = "inv"
53
52
  ```
54
53
 
@@ -62,7 +61,6 @@ class InvoiceViewSet(DisplayIDMixin, ModelViewSet):
62
61
  queryset = Invoice.objects.all()
63
62
  serializer_class = InvoiceSerializer
64
63
  lookup_url_kwarg = "id"
65
- lookup_strategies = ("display_id", "uuid", "slug")
66
64
  display_id_prefix = "inv"
67
65
  ```
68
66
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "django-display-ids"
3
- version = "0.4.1"
3
+ version = "0.5.1"
4
4
  description = "Stripe-like prefixed IDs for Django. Works with existing UUIDs — no schema changes."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -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
 
@@ -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:
@@ -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
- AmbiguousIdentifierError,
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
- 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
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 InvalidIdentifierError(value, str(e)) from e
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 UnknownPrefixError(
103
- value, actual=decoded_prefix, expected=expected_prefix
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
- try:
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
- InvalidIdentifierError: If no strategy can parse the identifier.
134
- UnknownPrefixError: If display ID prefix doesn't match expected.
135
- ObjectNotFoundError: If no matching object exists.
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
- 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
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
- result = parse_identifier(
156
- value, lookup_strategies, expected_prefix=expected_prefix
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
- return self.get(**lookup)
169
- except model.DoesNotExist: # type: ignore[attr-defined]
170
- raise ObjectNotFoundError(str(value), model_name=model.__name__) from None
171
- except model.MultipleObjectsReturned: # type: ignore[attr-defined]
172
- count = self.filter(**lookup).count()
173
- raise AmbiguousIdentifierError(str(value), count) from None
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],
@@ -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
 
@@ -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 excluded by default since it's a catch-all that matches any string
17
- DEFAULT_STRATEGIES: tuple[StrategyName, ...] = ("display_id", "uuid")
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")
@@ -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