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.
@@ -9,7 +9,7 @@ Example:
9
9
  encode_display_id,
10
10
  decode_display_id,
11
11
  resolve_object,
12
- DisplayIDObjectMixin,
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(DisplayIDObjectMixin, DetailView):
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 DisplayIDSearchMixin
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 DisplayIDObjectMixin
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 == "DisplayIDMixin":
66
- from .models import DisplayIDMixin
68
+ if name == "DisplayIDModel":
69
+ from .models import DisplayIDModel
67
70
 
68
- return DisplayIDMixin
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
- # Errors
97
- "LookupError",
102
+ # Exceptions
103
+ "DisplayIDLookupError",
98
104
  "InvalidIdentifierError",
99
105
  "UnknownPrefixError",
100
106
  "MissingPrefixError",
101
107
  "ObjectNotFoundError",
102
108
  "AmbiguousIdentifierError",
103
- # Django mixins
104
- "DisplayIDObjectMixin",
105
- # Admin mixins
106
- "DisplayIDSearchMixin",
107
- # Model mixin
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
@@ -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__ = ["DisplayIDSearchMixin"]
14
+ __all__ = ["DisplayIDAdminSearchMixin"]
16
15
 
17
16
 
18
- class DisplayIDSearchMixin:
19
- """Mixin to enable searching by display_id or UUID in Django admin.
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") or raw UUID
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 identifier and searches by the UUID field.
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.admin import DisplayIDSearchMixin
31
+ from django_display_ids import DisplayIDAdminSearchMixin
30
32
 
31
33
  @admin.register(Invoice)
32
- class InvoiceAdmin(DisplayIDSearchMixin, admin.ModelAdmin):
34
+ class InvoiceAdmin(DisplayIDAdminSearchMixin, admin.ModelAdmin):
33
35
  list_display = ["id", "display_id", "name"]
34
- search_fields = ["name"] # display_id/UUID search is automatic
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 DisplayIDMixin, otherwise "id".
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 and raw UUIDs.
60
+ """Extend search to handle display IDs.
66
61
 
67
- Tries to match the search term as:
68
- 1. A display ID (prefix_base62uuid) if it contains an underscore
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
@@ -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 extension for DisplayIDField.
1
+ """drf-spectacular integration for django-display-ids.
2
2
 
3
- This extension auto-registers when drf-spectacular is installed, providing
4
- proper OpenAPI schema generation for DisplayIDField.
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 DisplayIDLookupMixin
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__ = [ # noqa: RUF022 - keep logical order for readability
6
+ __all__ = [
43
7
  "DisplayIDField",
44
- "DisplayIDLookupMixin",
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 get_setting
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
- "DisplayIDLookupMixin",
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 DisplayIDLookupMixin. "
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 DisplayIDLookupMixin:
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(DisplayIDLookupMixin, APIView):
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(DisplayIDLookupMixin, ModelViewSet):
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 = _NOT_SET
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
- if self.uuid_field is not None:
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
- if self.slug_field is not None:
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 _NOT_SET:
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 LookupError as e:
170
+ except DisplayIDLookupError as e:
172
171
  raise ParseError(str(e)) from e
173
172
 
174
173
  # Check object-level permissions
@@ -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 LookupError(Exception):
15
+ class DisplayIDLookupError(Exception):
16
16
  """Base exception for all lookup errors."""
17
17
 
18
18
  pass
19
19
 
20
20
 
21
- class InvalidIdentifierError(LookupError):
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(LookupError):
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(LookupError):
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(LookupError):
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(LookupError):
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,
@@ -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
- "DisplayIDMixin",
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 DisplayIDMixin(models.Model):
54
- """Mixin that adds display_id support to a Django model.
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(DisplayIDMixin):
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(DisplayIDMixin):
70
+ class Product(DisplayIDModel):
71
71
  display_id_prefix = "prod"
72
72
  uuid_field = "uid"
73
73
  slug_field = "handle"
@@ -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
- "DisplayIDObjectMixin",
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(DisplayIDObjectMixin, DetailView):
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 = _NOT_SET
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
- if self.uuid_field is not None:
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
- if self.slug_field is not None:
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 _NOT_SET:
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 LookupError as e:
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.2
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 DisplayIDObjectMixin
69
+ from django_display_ids import DisplayIDMixin
70
70
 
71
- class InvoiceDetailView(DisplayIDObjectMixin, DetailView):
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 DisplayIDLookupMixin
82
+ from django_display_ids.contrib.rest_framework import DisplayIDMixin
83
83
 
84
- class InvoiceViewSet(DisplayIDLookupMixin, ModelViewSet):
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=wwFvtGjdQia56uY893Jz5iSwaqyH4KXbgvd3nxdzGss,3496
2
- django_display_ids/admin.py,sha256=_voqWbr8AwPRC_uCTJWTcEhAhc7RZUgvs7DyVsutDuw,3046
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=6bHdUwDkuroYEPmOlPiAc49EcxlOG_QO0aHHawckOe8,1404
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=21n56CH7tp2lKq4EGW99KVEgVB9fJ5GnfllUW_KrIe8,4003
7
- django_display_ids/contrib/rest_framework/__init__.py,sha256=Xun6zMhCZzQJZQ7ywvBYoKhTJIZEV84AntrbSWfBjYI,1567
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=nDaze7MJwVqL0MrxQLOur3sPVrRXud_WT4ijGR02jDY,6087
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=nmyRfpsqVvz226Zcu_QANwr8MudbfoX09mAgOCwuPuQ,3022
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=XI73N4bvxy1Pr2oHeTaJP3uq3huyCX67CFZ2T8mefsA,4317
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=-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,,
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,,