Metadata-Version: 2.3
Name: django-display-ids
Version: 0.1.0
Summary: Resolve external identifiers (display ID, UUID, slug) to Django model instances
Keywords: django,uuid,display-id,identifier,lookup,drf
Author: Joseph Abrahams
Author-email: Joseph Abrahams <hello@josephabrahams.com>
License: ISC
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: Django
Classifier: Framework :: Django :: 4.2
Classifier: Framework :: Django :: 5.2
Classifier: Framework :: Django :: 6.0
Classifier: License :: OSI Approved :: ISC License (ISCL)
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Typing :: Typed
Requires-Dist: django>=4.2
Requires-Python: >=3.12
Project-URL: Homepage, https://github.com/josephabrahams/django-display-ids
Project-URL: Repository, https://github.com/josephabrahams/django-display-ids
Description-Content-Type: text/markdown

# django-display-ids

Resolve external identifiers (display ID, UUID, slug) to Django model instances.

Handles lookup only — not ID generation, persistence, or serialization.

## Installation

```bash
pip install django-display-ids
```

## Quick Start

```python
from django.views.generic import DetailView
from django_display_ids import DisplayIDObjectMixin

class InvoiceDetailView(DisplayIDObjectMixin, DetailView):
    model = Invoice
    lookup_param = "id"
    lookup_strategies = ("display_id", "uuid")
    display_id_prefix = "inv"
```

```python
# urls.py
urlpatterns = [
    path("invoices/<str:id>/", InvoiceDetailView.as_view()),
]
```

Now your view accepts both formats:
- `inv_1a2B3c4D5e6F7g8H9i0J1k` (display ID)
- `550e8400-e29b-41d4-a716-446655440000` (UUID)

## Features

- **Multiple identifier formats**: display ID (`prefix_base62uuid`), UUID (v4/v7), slug
- **Framework support**: Django CBVs and Django REST Framework
- **Zero model changes required**: Works with any existing UUID field
- **Stateless**: Pure lookup, no database writes

## Usage

### Django Class-Based Views

```python
from django.views.generic import DetailView, UpdateView, DeleteView
from django_display_ids import DisplayIDObjectMixin

class InvoiceDetailView(DisplayIDObjectMixin, DetailView):
    model = Invoice
    lookup_param = "id"
    lookup_strategies = ("display_id", "uuid")
    display_id_prefix = "inv"

# Works with any view that uses get_object()
class InvoiceUpdateView(DisplayIDObjectMixin, UpdateView):
    model = Invoice
    lookup_param = "id"
    display_id_prefix = "inv"
```

### Django REST Framework

```python
from rest_framework.viewsets import ModelViewSet
from django_display_ids.contrib.rest_framework import DisplayIDLookupMixin

class InvoiceViewSet(DisplayIDLookupMixin, ModelViewSet):
    queryset = Invoice.objects.all()
    serializer_class = InvoiceSerializer
    lookup_url_kwarg = "id"
    lookup_strategies = ("display_id", "uuid")
    display_id_prefix = "inv"
```

Or with APIView:

```python
from rest_framework.views import APIView
from rest_framework.response import Response
from django_display_ids.contrib.rest_framework import DisplayIDLookupMixin

class InvoiceView(DisplayIDLookupMixin, APIView):
    lookup_url_kwarg = "id"
    lookup_strategies = ("display_id", "uuid")
    display_id_prefix = "inv"

    def get_queryset(self):
        return Invoice.objects.all()

    def get(self, request, *args, **kwargs):
        invoice = self.get_object()
        return Response({"id": str(invoice.id)})
```

### Model Mixin

Add a `display_id` property to your models:

```python
import uuid
from django.db import models
from django_display_ids import DisplayIDMixin

class Invoice(DisplayIDMixin, models.Model):
    display_id_prefix = "inv"
    id = models.UUIDField(primary_key=True, default=uuid.uuid4)

invoice = Invoice.objects.first()
invoice.display_id  # -> "inv_1a2B3c4D5e6F7g8H9i0J1k"
```

### Model Manager

```python
from django_display_ids import DisplayIDMixin, DisplayIDManager

class Invoice(DisplayIDMixin, models.Model):
    display_id_prefix = "inv"
    objects = DisplayIDManager()
    id = models.UUIDField(primary_key=True, default=uuid.uuid4)

# Get by display ID
invoice = Invoice.objects.get_by_display_id("inv_1a2B3c4D5e6F7g8H9i0J1k")

# Get by any identifier type
invoice = Invoice.objects.get_by_identifier("inv_1a2B3c4D5e6F7g8H9i0J1k")
invoice = Invoice.objects.get_by_identifier("550e8400-e29b-41d4-a716-446655440000")

# Works with filtered querysets
invoice = Invoice.objects.filter(active=True).get_by_identifier("inv_xxx")
```

### Encoding and Decoding

```python
import uuid
from django_display_ids import encode_display_id, decode_display_id

# Create a display ID from a UUID
invoice_id = uuid.uuid4()
display_id = encode_display_id("inv", invoice_id)
# -> "inv_1a2B3c4D5e6F7g8H9i0J1k"

# Decode back to prefix and UUID
prefix, decoded_uuid = decode_display_id(display_id)
```

### Direct Resolution

```python
from django_display_ids import resolve_object

invoice = resolve_object(
    model=Invoice,
    value="inv_1a2B3c4D5e6F7g8H9i0J1k",
    strategies=("display_id", "uuid", "slug"),
    prefix="inv",
)
```

## Identifier Formats

| Format | Example | Description |
|--------|---------|-------------|
| Display ID | `inv_1a2B3c4D5e6F7g8H9i0J1k` | Prefix + base62-encoded UUID |
| UUID | `550e8400-e29b-41d4-a716-446655440000` | Standard UUID (v4/v7) |
| Slug | `my-invoice-slug` | Human-readable identifier |

Display ID format:
- Prefix: 1-16 lowercase letters
- Separator: underscore
- Encoded UUID: 22 base62 characters (fixed length)

## Lookup Strategies

Strategies are tried in order. The first successful match is returned.

| Strategy | Description |
|----------|-------------|
| `display_id` | Decode display ID, lookup by UUID field |
| `uuid` | Parse as UUID, lookup by UUID field |
| `slug` | Lookup by slug field |

Default: `("display_id", "uuid")`

The slug strategy is a catch-all, so it should always be last.

The `display_id` strategy requires a prefix. If no prefix is configured, the strategy is skipped.

## Configuration

### View/Mixin Attributes

| Attribute | Default | Description |
|-----------|---------|-------------|
| `lookup_param` / `lookup_url_kwarg` | `"pk"` | URL parameter name |
| `lookup_strategies` | from settings | Strategies to try |
| `display_id_prefix` | `None` | Expected prefix |
| `uuid_field` | `"id"` | UUID field name on model |
| `slug_field` | `"slug"` | Slug field name on model |

### Django Settings

```python
# settings.py
DISPLAY_IDS = {
    "UUID_FIELD": "id",                     # default
    "SLUG_FIELD": "slug",                   # default
    "STRATEGIES": ("display_id", "uuid"),   # default
}
```

## Error Handling

| Exception | When Raised |
|-----------|-------------|
| `InvalidIdentifierError` | Identifier cannot be parsed |
| `UnknownPrefixError` | Display ID prefix doesn't match expected |
| `ObjectNotFoundError` | No matching database record |

In views, errors are converted to HTTP responses:
- Django CBV: `Http404`
- DRF: `NotFound` (404) or `ParseError` (400)

## Requirements

- Python 3.12+
- Django 4.2+
- Django REST Framework 3.14+ (optional)

## Development

Clone the repository and install dependencies:

```bash
git clone https://github.com/josephabrahams/django-display-ids.git
cd django-display-ids
uv sync
```

Run tests:

```bash
uv run pytest
```

Run tests across Python and Django versions:

```bash
uvx nox -p
```

Lint and format:

```bash
uvx pre-commit run --all-files
```

## License

ISC
