Integrating django-autocomplete-light¶
DjangoQL can suggest values for choices fields and for string columns out of the
box. The autocomplete value field lets a field's value suggestions come from an
arbitrary source — most usefully an existing
django-autocomplete-light (DAL)
endpoint your project already has — so a user can pick an object (e.g. an author) and
filter by it, without writing a new endpoint and without forking the completion widget.
The user picks a row from autocomplete; the resulting query filters unambiguously by the object's primary key, even across tens of thousands of rows with duplicate display names.
Server-side only
This feature is entirely server-side. There are no JavaScript changes. The
completion widget is the upstream djangoql-completion package and is not modified.
It inserts the chosen suggestion verbatim as a quoted string and shows exactly what it
inserts, so the server fully controls the inserted text.
How it works¶
Suggestions are formatted as "<label> [<id>]", for example:
Jan Kowalski [49990]
When the user picks that row, the widget inserts the whole string. The
AutocompleteField parses the trailing [<id>] back to an integer primary key and filters
<field> = pk. The embedded [id] is visible in the drop-down and in the query — that is
accepted by design and is what makes the match unambiguous.
If the user types free text without a bracketed id (e.g. author = "kowal"), the field
falls back to an icontains lookup over its search_fields, so a partially-typed value
still filters.
Because the field exposes a ForeignKey as a value picker, under that field name you
filter by the related object — you do not traverse into author.username. If you need
both, expose the relation under a second field name.
Three providers¶
Suggestions can come from one of three providers (priority high → low):
url— an existing autocomplete endpoint (a URL name or a local path). It is resolved and called in-process with the current request.queryset/get_queryset— a queryset or asearch -> querysetcallable. DAL-agnostic, full control.- A subclass override of
get_options()/format_label()/get_id().
1. URL provider (reuse a DAL endpoint)¶
Point the field at a DAL endpoint by its URL name (or local path). DjangoQL resolves the
URL and calls the view in-process with the current authenticated request — its
GET parameter q (configurable via search_param) is set to the search term. This
reuses the DAL view's queryset, permissions, and per-user filtering for free, with no
network round-trip and no cookie forwarding.
from django.contrib.auth.models import User
from djangoql.extras import AutocompleteField
from djangoql.schema import DjangoQLSchema
from .models import Record
class RecordSchema(DjangoQLSchema):
def get_field_instance(self, model, field_name):
if model is Record and field_name == 'author':
return AutocompleteField(
model=model,
name='author',
url='autocomplete-author', # a DAL url name (or '/path/')
search_fields=['last_name'], # used for the free-text fallback
)
return super().get_field_instance(model, field_name)
The endpoint must return Select2 JSON, exactly like DAL does:
{"results": [{"id": 42, "text": "Jan Kowalski"}], "pagination": {"more": false}}
DjangoQL maps results[] to "<text> [<id>]" (HTML tags in text are stripped).
2. Queryset provider¶
For full control without DAL, supply a queryset or a search -> queryset callable:
AutocompleteField(
model=Record,
name='reviewer',
queryset=lambda s: User.objects.filter(
is_active=True, last_name__icontains=s
).order_by('last_name')[:50],
search_fields=['last_name'],
label=lambda u: f'{u.first_name} {u.last_name}', # default: str(obj)
id_of=lambda u: u.pk, # default: obj.pk
)
3. Subclass override¶
For anything else, subclass and override the small overridable methods —
get_options(search), get_queryset(search), format_label(obj), get_id(obj),
parse_id(value):
class CityAutocompleteField(AutocompleteField):
def get_queryset(self, search):
return City.objects.filter(name__istartswith=search)
def format_label(self, obj):
return f'{obj.name}, {obj.country}'
The AutocompleteSchemaMixin map¶
Instead of overriding get_field_instance by hand, declare an autocomplete map of
{Model: {field_name: config}}. Each config is a dict of AutocompleteField kwargs, an
AutocompleteField instance, or a callable (model, field_name) -> AutocompleteField:
from djangoql.extras import AutocompleteSchemaMixin
from djangoql.schema import DjangoQLSchema
class RecordSchema(AutocompleteSchemaMixin, DjangoQLSchema):
autocomplete = {
Record: {
'author': {'url': 'autocomplete-author',
'search_fields': ['last_name']},
'reviewer': {'queryset': lambda s: User.objects.filter(
is_active=True, last_name__icontains=s)[:50],
'search_fields': ['last_name']},
},
}
The mixin works standalone, and is also included in the batteries-included
ExtrasSchema, so the derived-fields schema picks it up too.
Request threading¶
When the widget fetches suggestions asynchronously, SuggestionsAPIView threads the
current request into the field via a set_request(request) hook before calling
get_options(). The URL provider uses that bound request to call the DAL view in-process,
so the view sees the same authenticated user and session. Base fields don't define
set_request and are unaffected.
This makes per-user / per-request filtering reachable for free: a DAL view that already
filters by request.user keeps doing so when called through the autocomplete field.
Configuration reference¶
| kwarg | default | meaning |
|---|---|---|
url |
None |
URL name or local path of a Select2-JSON endpoint |
queryset |
None |
a queryset or a search -> queryset callable |
get_queryset |
None |
a search -> queryset callable (alternative to queryset) |
search_fields |
[] |
fields on the related model for the free-text fallback |
label |
str |
callable obj -> str for the display label |
id_of |
obj.pk |
callable obj -> id for the embedded id |
lookup_name |
None |
real model field to filter on (default: the field's own name); lets a picker live under a second name like <fk>__rel |
search_param |
'q' |
GET parameter set on the bound request for the URL provider |
limit |
50 |
maximum number of suggestions returned |
Exposing a FK as both a navigable relation and a value picker¶
By default a FK is a navigable relation — you traverse into it
(author.last_name, author.country.code). The picker above instead exposes it
as a value field under the same name, which removes traversal. To keep
both, expose the picker under a second name and point it back at the real
FK with lookup_name. The recommended convention is <fk>__rel (double
underscore, consistent with the derived-field family __count / __sum / …):
from django.contrib.auth.models import User
from djangoql.extras import AutocompleteSchemaMixin
from djangoql.schema import DjangoQLSchema
from .models import Book
class BookSchema(AutocompleteSchemaMixin, DjangoQLSchema):
include = (Book, User)
autocomplete = {
Book: {
# picker; `author` itself stays a navigable relation
'author__rel': {
'lookup_name': 'author', # filters the real FK
'url': 'user-autocomplete',
'search_fields': ['username'],
},
},
}
def get_fields(self, model):
# `author__rel` is synthetic (not a real model field), so it must be
# added explicitly or it won't be introspected / suggested.
fields = list(super().get_fields(model))
if model is Book:
fields.append('author__rel')
return fields
Now both work side by side:
author.username = "kowalski"— traversal into the related model (unchanged);author__rel = "Jan Kowalski [42]"— picker, filtersauthor_id = 42(with the usualicontainsfree-text fallback oversearch_fields).
Limitations¶
- The
[id]is visible in the drop-down and in the query (this is what keeps matches unambiguous). - v1 returns a single top-N page; deep pagination / infinite scroll against DAL is future work.
- External (non-local, unresolvable) URLs are not called over HTTP — use the
querysetprovider for those. - Grouped (optgroup) results are not supported; v1 handles a flat
resultslist.