Djangospice Lookup
A reusable Django lookup widget and server-side lookup endpoint for selecting objects from large datasets.
djangospice_lookup provides an enhanced lookup experience for Django forms and django-filter while preserving Django's standard form and field behavior.
It supports server-side search, pagination, filtering, dependent lookups, multiple selection, authentication, access scoping, and HTMX-driven interfaces.
Features
- Django form widget integration
django-filterintegration- Server-side object search
- Server-side filtering
- Pagination
- Dependent/cascading lookups
- Single selection
- Multiple selection
- Initial Django form values
- Bound and submitted form values
- Access-controlled lookups
- Django session authentication
- JWT bearer authentication
- Short-lived widget capability tokens
- Configurable request parameters
- Configurable pagination
- Configurable authentication
- HTMX support
- Django template tags for assets
- Progressive enhancement
- JSON lookup endpoints
Requirements
- Python 3.12+
- Django 5.0+
django-filterwhen usingLookupWidgetwithdjango-filterPyJWTfor JWT authentication
Installation
Install the package:
pip install djangospice-lookup
Add it to INSTALLED_APPS:
INSTALLED_APPS = [
# ...
"djangospice_lookup",
]
If the project uses django-filter:
pip install django-filter
and add it to INSTALLED_APPS:
INSTALLED_APPS = [
# ...
"django_filters",
"djangospice_lookup",
]
Configuration
djangospice_lookup uses Django settings for package-wide configuration.
All package settings use the DJANGOSPICE_LOOKUP_ prefix.
Search
Configure the search parameter:
DJANGOSPICE_LOOKUP_SEARCH_PARAM = "q"
The default is:
q
For example:
/lookup/library/book/?q=django
Pagination
Configure pagination parameters and limits:
DJANGOSPICE_LOOKUP_PAGE_PARAM = "page"
DJANGOSPICE_LOOKUP_PAGE_SIZE_PARAM = "page_size"
DJANGOSPICE_LOOKUP_PAGE = 1
DJANGOSPICE_LOOKUP_PAGE_SIZE = 20
DJANGOSPICE_LOOKUP_MAX_PAGE_SIZE = 100
These settings control:
- default page number
- page query parameter
- page-size query parameter
- default page size
- maximum page size
Authentication Requirement
Authentication is required by default:
DJANGOSPICE_LOOKUP_REQUIRE_AUTHENTICATION = True
For intentionally public lookup data:
DJANGOSPICE_LOOKUP_REQUIRE_AUTHENTICATION = False
Only disable authentication when the lookup data is intentionally accessible without authentication.
JWT Authentication
The default JWT secret can use Django's SECRET_KEY:
DJANGOSPICE_LOOKUP_JWT_SECRET_KEY = SECRET_KEY
Configure the permitted algorithms:
DJANGOSPICE_LOOKUP_JWT_ALGORITHMS = (
"HS256",
)
The default user claim is:
DJANGOSPICE_LOOKUP_JWT_USER_CLAIM = "user_id"
A custom user resolver can be configured using a dotted Python path:
DJANGOSPICE_LOOKUP_JWT_USER_RESOLVER = (
"myproject.authentication.resolve_user"
)
Widget Capability Tokens
Configure the capability-token header:
DJANGOSPICE_LOOKUP_WIDGET_TOKEN_HEADER = (
"X-Lookup-Token"
)
Configure the signing salt:
DJANGOSPICE_LOOKUP_WIDGET_TOKEN_SALT = (
"djangospice_lookup.widget"
)
Configure the token lifetime in seconds:
DJANGOSPICE_LOOKUP_WIDGET_TOKEN_MAX_AGE = 300
The default lifetime is five minutes.
URL Configuration
Include the package URLs in your project:
from django.urls import include, path
urlpatterns = [
path("", include("djangospice_lookup.urls")),
]
The package provides the canonical lookup endpoint:
/lookup/<app_name>/<name>/
For example:
/lookup/library/book/
The URL identifies a registered lookup using the Django application label and model name.
Middleware
Add LookupMiddleware to your project's MIDDLEWARE:
MIDDLEWARE = [
# ...
"djangospice_lookup.middleware.LookupMiddleware",
]
When using Django session authentication, place it after Django's AuthenticationMiddleware:
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"djangospice_lookup.middleware.LookupMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
LookupMiddleware works alongside Django authentication. It does not replace:
"django.contrib.auth.middleware.AuthenticationMiddleware",
Django authentication remains responsible for establishing:
request.user
The lookup system can then use the authenticated request when applying lookup security and access rules.
For JWT and widget capability authentication, lookup security can authenticate the request independently of the Django session.
Registering Lookups
Lookups are explicitly registered.
This controls which models are exposed through lookup endpoints.
For example:
from djangospice_lookup.registry import register_lookup
from .models import Author, Book
register_lookup(Author)
register_lookup(Book)
A typical application can keep registrations in lookup.py:
library/
├── models.py
├── lookup.py
└── apps.py
Only explicitly registered models are exposed as lookups.
Lookup Configuration
A lookup can optionally be configured when it is registered.
For example:
register_lookup(
Book,
scope="account",
)
The scope identifies the relationship used to restrict results according to the authenticated user.
Django ORM relationship paths can be used:
register_lookup(
Book,
scope="account__user",
)
This allows access restrictions to follow related objects.
LookupWidget
The primary form integration is LookupWidget.
Import it with:
from djangospice_lookup.widget import LookupWidget
It can be used with standard Django form fields.
ModelChoiceField
from django import forms
from djangospice_lookup.widget import LookupWidget
from .models import Book
class BookForm(forms.Form):
book = forms.ModelChoiceField(
queryset=Book.objects.all(),
widget=LookupWidget(
model=Book,
),
)
The widget provides a searchable remote lookup without requiring the entire queryset to be rendered into the initial HTML.
ModelMultipleChoiceField
For multiple selections:
class BookForm(forms.Form):
books = forms.ModelMultipleChoiceField(
queryset=Book.objects.all(),
widget=LookupWidget(
model=Book,
),
)
The widget respects Django's native multiple-selection behavior.
Using LookupWidget with django-filter
LookupWidget can be used directly as the widget for a django-filter field.
For example:
import django_filters
from djangospice_lookup.widget import LookupWidget
from .models import Author, Book
class BookFilter(django_filters.FilterSet):
author = django_filters.ModelChoiceFilter(
queryset=Author.objects.all(),
widget=LookupWidget(
model=Author,
),
)
class Meta:
model = Book
fields = (
"author",
)
Render the filter normally:
<form method="get">
{{ filter.form.as_p }}
<button type="submit">
Filter
</button>
</form>
The author filter now uses LookupWidget instead of a conventional select containing every author.
This is especially useful when the filter queryset contains a large number of records.
Multiple Lookup Filters
Multiple filters can use LookupWidget:
class BookFilter(django_filters.FilterSet):
author = django_filters.ModelChoiceFilter(
queryset=Author.objects.all(),
widget=LookupWidget(
model=Author,
),
)
category = django_filters.ModelChoiceFilter(
queryset=Category.objects.all(),
widget=LookupWidget(
model=Category,
),
)
class Meta:
model = Book
fields = (
"author",
"category",
)
Multiple Selection with django-filter
Use ModelMultipleChoiceFilter for multiple selections:
class BookFilter(django_filters.FilterSet):
authors = django_filters.ModelMultipleChoiceFilter(
queryset=Author.objects.all(),
widget=LookupWidget(
model=Author,
),
)
class Meta:
model = Book
fields = (
"authors",
)
Dependent Lookups
A lookup can depend on another form field.
For example:
class BookForm(forms.Form):
author = forms.ModelChoiceField(
queryset=Author.objects.all(),
widget=LookupWidget(
model=Author,
),
)
book = forms.ModelChoiceField(
queryset=Book.objects.all(),
widget=LookupWidget(
model=Book,
depends_on=("author"),
),
)
The book lookup can then use the selected author when requesting results.
Conceptually:
Author
↓
Book
A request may look like:
/lookup/library/book/?author=12
Dependent django-filter Fields
Dependencies can also be used in a FilterSet:
class BookFilter(django_filters.FilterSet):
author = django_filters.ModelChoiceFilter(
queryset=Author.objects.all(),
widget=LookupWidget(
model=Author,
),
)
book = django_filters.ModelChoiceFilter(
queryset=Book.objects.all(),
widget=LookupWidget(
model=Book,
depends_on=("author"),
),
)
class Meta:
model = Book
fields = (
"author",
"book",
)
This supports cascading relationships such as:
Country → City
Department → Program
Program → Course
Category → Product
Author → Book
Relationship Paths
Dependencies can reference related fields using normal Django ORM relationship paths:
depends_on=(
"account__user",
)
This allows dependencies to resolve through related objects.
Search
Lookup fields support server-side text search.
For example:
/lookup/library/book/?q=django
Search is performed by the lookup endpoint, allowing the browser to request only the records required for the current query.
The search parameter is configurable through:
DJANGOSPICE_LOOKUP_SEARCH_PARAM = "q"
Filtering
Lookup requests can contain filter values.
For example:
/lookup/library/book/?author=12
Search and filtering can be combined:
/lookup/library/book/?q=django&author=12
When LookupWidget is used with django-filter, the filter form remains responsible for filter semantics while the widget provides efficient remote selection of filter values.
Pagination
Lookup results are paginated.
For example:
/lookup/library/book/?page=2
A page size can also be supplied:
/lookup/library/book/?page=2&page_size=50
Pagination prevents large querysets from being transferred to the browser in a single request.
The maximum page size is controlled by:
DJANGOSPICE_LOOKUP_MAX_PAGE_SIZE = 100
Initial Values
LookupWidget works with Django's normal initial and bound field behavior.
For example:
class BookForm(forms.Form):
book = forms.ModelChoiceField(
queryset=Book.objects.all(),
widget=LookupWidget(
model=Book,
),
initial=12,
)
This makes the widget suitable for both create and edit forms.
Django Form Validation
LookupWidget does not replace Django field validation.
For example:
class BookForm(forms.Form):
book = forms.ModelChoiceField(
queryset=Book.objects.all(),
widget=LookupWidget(
model=Book,
),
)
Django continues to validate the submitted value against the field's queryset.
The widget helps the user select an object; the Django form field remains responsible for validating the submitted value.
Access Control
Lookup results can be restricted according to the authenticated user.
For example:
register_lookup(
Book,
scope="account",
)
A scoped lookup only returns objects satisfying the configured relationship.
This is useful when users should only be able to select objects belonging to their permitted account or organization.
The lookup endpoint should not be used to bypass the application's authorization rules.
Authentication
The default security configuration supports:
- Widget capability tokens
- Django session authentication
- JWT bearer authentication
Django Session Authentication
Authenticated Django users can access lookup endpoints using their normal Django session.
No special authentication code is required in the widget.
JWT Authentication
API clients can authenticate using:
Authorization: Bearer <token>
JWT behavior is controlled through the package configuration.
Widget Capability Tokens
A widget can use a short-lived capability token:
X-Lookup-Token: <token>
Capability tokens provide short-lived access to a specific lookup.
Template Tags
The package provides template tags for loading its frontend assets.
Load the template tag library:
{% load djangospice_lookup %}
Load All Assets
The simplest approach is:
{% djangospice_lookup_assets %}
This loads both the lookup CSS and JavaScript.
A base template can use:
{% load djangospice_lookup %}
<head>
{% djangospice_lookup_assets %}
</head>
Load CSS Only
{% djangospice_lookup_css %}
Load JavaScript Only
{% djangospice_lookup_js %}
Load Assets Separately
Applications that need more control can load the assets independently:
{% load djangospice_lookup %}
<head>
{% djangospice_lookup_css %}
</head>
<body>
{% block content %}
{% endblock %}
{% djangospice_lookup_js %}
</body>
The template tags use Django's static-files system to resolve the package assets.
HTMX
LookupWidget works with HTMX-driven interfaces.
Lookup fields can be rendered inside:
- dynamically loaded forms
- modal dialogs
- filter forms
- partial templates
- inline forms
- dynamically replaced form sections
When HTMX replaces part of a page, newly rendered lookup widgets can be initialized as part of the HTMX lifecycle.
JSON Lookup Endpoint
The lookup endpoint returns JSON suitable for the widget and other clients.
A response has the general structure:
{
"results": [
{
"id": "8f3a...",
"text": "Introduction to Django"
},
{
"id": "91ab...",
"text": "Advanced Django"
}
],
"pagination": {
"page": 1,
"page_size": 20,
"has_next": true
}
}
The exact result values depend on the registered lookup and its configuration.
Endpoint Examples
Search
GET /lookup/library/book/?q=django
Filter
GET /lookup/library/book/?author=12
Search and Filter
GET /lookup/library/book/?q=django&author=12
Pagination
GET /lookup/library/book/?page=2&page_size=20
Dependent Lookup
GET /lookup/library/book/?author=12
Complete Example
The following example demonstrates a typical Django application using LookupWidget with django-filter.
Models
from django.db import models
class Author(models.Model):
name = models.CharField(
max_length=200,
)
def __str__(self):
return self.name
class Book(models.Model):
title = models.CharField(
max_length=200,
)
author = models.ForeignKey(
Author,
on_delete=models.CASCADE,
)
def __str__(self):
return self.title
Lookup Registration
from djangospice_lookup.registry import register_lookup
from .models import Author, Book
register_lookup(Author)
register_lookup(Book)
Filter
import django_filters
from djangospice_lookup.widget import LookupWidget
from .models import Author, Book
class BookFilter(django_filters.FilterSet):
author = django_filters.ModelChoiceFilter(
label="Author",
queryset=Author.objects.all(),
widget=LookupWidget(
model=Author,
placeholder="Select an author",
search_placeholder="Search authors...",
),
)
class Meta:
model = Book
fields = (
"author",
)
View
from django.views.generic import ListView
from .filters import BookFilter
from .models import Book
class BookListView(ListView):
model = Book
template_name = "library/book_list.html"
def get_queryset(self):
self.filter = BookFilter(
self.request.GET,
queryset=Book.objects.all(),
)
return self.filter.qs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["filter"] = self.filter
return context
Template
{% load djangospice_lookup %}
{% djangospice_lookup_assets %}
<form method="get">
{{ filter.form.as_p }}
<button type="submit">
Filter
</button>
</form>
{% for book in object_list %}
<article>
<h2>{{ book.title }}</h2>
<p>{{ book.author }}</p>
</article>
{% endfor %}
Progressive Enhancement
LookupWidget is built around Django's standard <select> widget.
The underlying form control remains part of the HTML form.
This preserves Django's existing:
- form fields
- validation
- initial values
- bound values
- submitted values
- required state
- disabled state
- multiple-selection behavior
The lookup interface enhances the field when the package assets are loaded.
Recommended Usage
djangospice_lookup is particularly useful when a form or filter needs to select from a large queryset.
For small static choice sets, a normal Django ChoiceField or ModelChoiceField may be sufficient.
For large or frequently changing datasets, LookupWidget provides a more scalable selection experience.
License
This package is licensed under the MIT License.
See LICENSE for the full license text.
Release files for djangospice-lookup 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| djangospice_lookup-0.2.0.tar.gz | 28.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| djangospice_lookup-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 66.8 kB
Release files / djangospice_lookup-0.2.0.tar.gz
| Download URL | djangospice_lookup-0.2.0.tar.gz |
|---|---|
| Size | 28.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
fb5d271d6602ace703635b84c2ce92928233de858b336f4dee13af3d978dd3ee
|
|
BLAKE2b-256 checksum How to use checksums |
6a227dcd382bdf25193dbae90e5f345f91c9c0ba96cc41606337fad214df6941
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 6, 2026.
Transparency logRelease files / djangospice_lookup-0.2.0-py3-none-any.whl
| Download URL | djangospice_lookup-0.2.0-py3-none-any.whl |
|---|---|
| Size | 37.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3471f2c40d4c980cf5e21fba7d4599132ef8ba356697e9fdd5b9757a49e19e99
|
|
BLAKE2b-256 checksum How to use checksums |
eb5cd2dd9ed5601b0c051b59decb5dca401560e06885e1c74e71e76d7733d32f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 6, 2026.
Transparency log