# Autocomplete Views
This module provides the view classes that handle autocomplete requests from TomSelect widgets. These views manage data loading, searching, filtering, and permission checks.
## Model Autocomplete Views
### AutocompleteModelView Processing
```{mermaid}
sequenceDiagram
participant User
participant Widget
participant AutocompleteView
participant QuerySet
participant Paginator
User->>Widget: Type Search Term
Widget->>AutocompleteView: GET Request with Query
activate AutocompleteView
AutocompleteView->>AutocompleteView: hook_queryset()
AutocompleteView->>QuerySet: apply_filters()
QuerySet-->>AutocompleteView: Filtered Results
AutocompleteView->>QuerySet: search()
QuerySet-->>AutocompleteView: Search Results
AutocompleteView->>QuerySet: order_queryset()
QuerySet-->>AutocompleteView: Ordered Results
AutocompleteView->>Paginator: paginate_queryset()
Paginator-->>AutocompleteView: Paginated Results
AutocompleteView->>AutocompleteView: prepare_results()
AutocompleteView->>AutocompleteView: hook_prepare_results()
AutocompleteView-->>Widget: JSON Response
deactivate AutocompleteView
Widget-->>User: Update Dropdown
note over AutocompleteView: Hooks allow customization
at various stages
```
### AutocompleteModelView
```{eval-rst}
.. autoclass:: django_tomselect.autocompletes.AutocompleteModelView
:members:
:show-inheritance:
```
The `AutocompleteModelView` is the primary view for handling model-based autocomplete requests. It provides a flexible foundation for creating searchable, paginated, and permission-controlled autocomplete endpoints.
#### Basic Usage
```python
from django_tomselect.autocompletes import AutocompleteModelView
from myapp.models import Book
class BookAutocomplete(AutocompleteModelView):
model = Book
search_lookups = ['title__icontains', 'author__name__icontains']
ordering = 'title'
page_size = 20
# Important: Include any fields you'll use as label_field in the widget
value_fields = ['id', 'title', 'author__name']
virtual_fields = ['computed_field'] # Fields that should not be queried from the database
# Optional: Restrict which fields users can filter/order by via request parameters
allowed_filter_fields = ['category', 'author']
allowed_ordering_fields = ['title', 'publication_date']
```
#### URL Configuration
```python
# urls.py
from django.urls import path
from .views import BookAutocomplete
urlpatterns = [
path('book-autocomplete/', BookAutocomplete.as_view(), name='book-autocomplete'),
]
```
#### Advanced Configuration
```python
from django.db.models import Prefetch
from myapp.models import Book, Author
class BookAutocomplete(AutocompleteModelView):
model = Book
search_lookups = ['title__icontains', 'author__name__icontains']
ordering = ['title', 'publication_date']
page_size = 20
# Include all fields needed for display and filtering
value_fields = ['id', 'title', 'author__name', 'publication_date', 'isbn']
# URLs for CRUD operations
list_url = 'book-list'
create_url = 'book-create'
detail_url = 'book-detail'
update_url = 'book-update'
delete_url = 'book-delete'
# Custom permission settings
permission_required = ('myapp.view_book', 'myapp.search_book')
allow_anonymous = False
def hook_queryset(self, queryset):
"""Customize the base queryset before filtering and searching."""
return queryset.select_related('author').prefetch_related(
Prefetch('categories', queryset=Category.objects.only('name'))
)
def hook_prepare_results(self, results):
"""Customize the prepared results before sending to the client."""
for result in results:
result['author_name'] = result.pop('author__name', '')
result['category_count'] = len(result.get('categories', []))
return results
```
#### Key Features
1. **Restricting Filter and Ordering Fields**
By default, users can pass arbitrary field names via `filter_by`, `exclude_by`, and `ordering` request parameters. To restrict which fields are accepted, set `allowed_filter_fields` and `allowed_ordering_fields`:
```python
class BookAutocomplete(AutocompleteModelView):
model = Book
search_lookups = ['title__icontains']
value_fields = ['id', 'title', 'category__name', 'publication_date']
# Only allow filtering/excluding by these fields
allowed_filter_fields = ['category', 'author', 'status']
# Only allow ordering by these fields
allowed_ordering_fields = ['title', 'publication_date']
```
When `allowed_filter_fields` is set, any `filter_by` or `exclude_by` parameter referencing a field not in the list is silently rejected (the base field name is checked, so `category__id` is allowed when `category` is in the list). When `allowed_ordering_fields` is set, disallowed ordering fields fall back to the view's default `ordering`.
When these attributes are `None` (the default), all valid model fields are accepted.
```{tip}
Setting these attributes is recommended for production deployments to prevent users from probing your model structure through filter/ordering parameters.
```
2. **Search Configuration**
The `search_lookups` attribute defines how searching works:
```python
class AuthorAutocomplete(AutocompleteModelView):
model = Author
# Search in multiple fields
search_lookups = [
'name__icontains', # Case-insensitive name search
'email__istartswith', # Email starting with query
'books__title__icontains', # Search in related books
]
```
3. **Value Fields Configuration**
The `value_fields` attribute determines which fields are included in the autocomplete results. This is critical for the widget's functionality:
```python
class AuthorAutocomplete(AutocompleteModelView):
model = Author
# Define fields to include in results
value_fields = [
'id', # Primary key (always required)
'name', # For use as label_field in widget
'email', # Optional additional field
'profile_picture', # Optional additional field
'books__count', # Can include annotations
]
virtual_fields = [
'books__count', # Computed or annotated fields
]
```
⚠️ **Important**: The `value_fields` attribute should include all fields you need in your results, but for fields that don't exist in the database (computed properties, annotations added later, etc.), add them to `virtual_fields` to prevent database query errors. The widget will automatically add your `label_field` to `value_fields` and detect if it should be in `virtual_fields`.
For example, if your widget uses:
```python
config=TomSelectConfig(label_field="name")
```
Then your autocomplete view should include "name" in its `value_fields` list.
````{warning}
**Do not use `label_field="__str__"`** (or any Python dunder). django-tomselect
serializes options with `QuerySet.values()`, and `__str__` is not a selectable
column, so option labels would render empty. This now raises `ImproperlyConfigured`
at configuration time.
When you are tempted to reach for the model's `__str__`, use a **real field** or an
**annotation** instead. For a composite/computed label, reproduce it as a queryable
annotation in `hook_queryset()` and point `label_field` at that annotation:
```python
from django.db.models import CharField, Value
from django.db.models.functions import Concat
class SubscriptionAutocomplete(AutocompleteModelView):
model = Subscription
value_fields = ["id", "display_name"]
search_lookups = ["monitor__name__icontains"]
def hook_queryset(self, queryset):
# Reproduce the model's __str__ as a queryable annotation. Pass an
# explicit output_field so Django can resolve the type even when a
# concatenated part is not itself text (an int/date column otherwise
# raises FieldError: unknown output_field).
return queryset.annotate(
display_name=Concat(
"monitor__name", Value(" - "), "metric",
output_field=CharField(),
),
)
# Widget config:
config = TomSelectConfig(url="subscription-autocomplete", label_field="display_name")
```
Because `display_name` is a real annotation it flows through both the AJAX results
(`.values()`) and the pre-selected option rendering, and it stays searchable and
sortable. If a single column already captures the label, just use it directly
(e.g. `label_field="name"`) - no annotation needed. (The annotation's relation
traversal, `monitor__name`, is JOINed automatically; only add `select_related`
in `hook_queryset()` if you also access the related object on the result
instances for other reasons.)
````
4. **Queryset Customization**
Use `hook_queryset` to optimize or customize the queryset:
```python
def hook_queryset(self, queryset):
"""Customize the base queryset before filtering and searching.
This is the ideal place to add select_related, prefetch_related,
or annotations that should apply to all results.
"""
return queryset.select_related('publisher')\
.prefetch_related('categories')\
.annotate(book_count=Count('books'))\
.filter(is_active=True)
```
```{note}
When the widget's `label_field` points to a relation (e.g., `label_field="author"`), the widget automatically adds `select_related()` for that field to prevent N+1 queries when rendering selected options. You do not need to add it manually in `hook_queryset` for this case.
```
5. **Permission Handling**
Multiple ways to configure permissions:
```python
class BookAutocomplete(AutocompleteModelView):
# Option 1: Specify required permissions
permission_required = ('myapp.view_book', 'myapp.search_book')
# Option 2: Allow anonymous access
allow_anonymous = True
# Option 3: Skip all permission checks
skip_authorization = False
# Option 4: Custom permission checking
def has_permission(self, request, action="view"):
if action == "create":
return request.user.is_staff
return super().has_permission(request, action)
```
6. **Result Preparation**
Customize the data sent to the client:
```python
def hook_prepare_results(self, results):
for result in results:
# Add computed fields
result['display_label'] = f"{result['title']} ({result['year']})"
# Transform data
result['author_info'] = {
'name': result.pop('author__name'),
'email': result.pop('author__email')
}
# Add custom URLs
result['preview_url'] = reverse('book-preview', args=[result['id']])
return results
```
7. **Custom JSON Encoder**
If your model has fields with non-serializable types (e.g., a `PhoneNumber` from the django-phonenumber-field package), you can specify a custom JSON encoder:
```python
import json
class PhoneNumberEncoder(json.JSONEncoder):
"""Encoder that handles PhoneNumber objects."""
def default(self, obj):
if hasattr(obj, 'as_e164'):
return obj.as_e164
return super().default(obj)
class ContactAutocomplete(AutocompleteModelView):
model = Contact
search_lookups = ['name__icontains']
value_fields = ['id', 'name', 'phone']
# Use the custom encoder for this view
json_encoder = PhoneNumberEncoder
```
The encoder can also be specified as a dotted string path:
```python
class ContactAutocomplete(AutocompleteModelView):
model = Contact
json_encoder = 'myapp.encoders.PhoneNumberEncoder'
```
See {ref}`Configuration documentation ` for details on setting a global default encoder.
## Iterables Autocomplete Views
### AutocompleteIterablesView Processing
```{mermaid}
sequenceDiagram
participant User
participant Widget
participant AutocompleteIterablesView
participant Iterable
participant Paginator
User->>Widget: Type Search Term
Widget->>AutocompleteIterablesView: GET Request with Query
activate AutocompleteIterablesView
AutocompleteIterablesView->>AutocompleteIterablesView: get_iterable()
alt TextChoices/IntegerChoices
AutocompleteIterablesView->>Iterable: Access choices attribute
Iterable-->>AutocompleteIterablesView: Return choices list
AutocompleteIterablesView->>AutocompleteIterablesView: Format as {value, label}
else Tuple Iterables
AutocompleteIterablesView->>Iterable: Access tuple items
Iterable-->>AutocompleteIterablesView: Return tuple list
AutocompleteIterablesView->>AutocompleteIterablesView: Format as {value, label}
else Simple Iterables
AutocompleteIterablesView->>Iterable: Access items
Iterable-->>AutocompleteIterablesView: Return items
AutocompleteIterablesView->>AutocompleteIterablesView: Format as {value, label}
end
AutocompleteIterablesView->>AutocompleteIterablesView: search()
Note over AutocompleteIterablesView: Filter items based on query
AutocompleteIterablesView->>Paginator: paginate_iterable()
Paginator-->>AutocompleteIterablesView: Paginated Results
AutocompleteIterablesView-->>Widget: JSON Response
deactivate AutocompleteIterablesView
Widget-->>User: Update Dropdown
note over AutocompleteIterablesView: Handles three types of iterables:
1. Django Choices (Text/Integer)
2. Tuple Iterables
3. Simple Iterables
```
### AutocompleteIterablesView
```{eval-rst}
.. autoclass:: django_tomselect.autocompletes.AutocompleteIterablesView
:members:
:show-inheritance:
```
This view handles autocomplete for choices, iterables, and enums.
#### Basic Usage
```python
from django_tomselect.autocompletes import AutocompleteIterablesView
from django.db.models import TextChoices
class Status(TextChoices):
DRAFT = 'D', 'Draft'
PUBLISHED = 'P', 'Published'
ARCHIVED = 'A', 'Archived'
class StatusAutocomplete(AutocompleteIterablesView):
iterable = Status
page_size = 10
```
#### Custom Iterables
```python
class YearAutocomplete(AutocompleteIterablesView):
iterable = [
2020,
2021,
2022,
2023,
2024,
2025,
]
class TiersAutocomplete(AutocompleteIterablesView):
iterable = [
(1, "Tier 1"),
(2, "Tier 2"),
(3, "Tier 3"),
]
```
#### With Dictionary
```python
class ClassStandingsAutocomplete(AutocompleteIterablesView):
iterable = {
"FR": "Freshman",
"SO": "Sophomore",
"JR": "Junior",
"SR": "Senior",
"GR": "Graduate",
}
```
#### Custom Iterable Formatting
Autocompletes can be customized to modify formatting by overriding the `get_iterable` method.
For an example where we use this to display tuples as ranges, see the [autcompletes.py code](https://github.com/OmenApps/django-tomselect/blob/main/example_project/example/autocompletes.py#L66) in the example app.
## Response Format
Both view types return JSON responses in this format:
```python
{
"results": [
{
"id": "1",
"name": "Example Item",
"can_view": true, # Permission flags for the current user
"can_update": true,
"can_delete": false,
"detail_url": "/items/1/",
"update_url": "/items/1/update/",
# No delete_url since can_delete is false
# ... additional fields from hook_prepare_results
}
],
"page": 1,
"has_more": true,
"next_page": 2,
"total_pages": 5
}
```
## Error Handling
The views include built-in error handling:
1. Invalid permissions return 403 Forbidden
2. Unauthenticated users are redirected to login
3. Invalid queries return empty results
4. Database errors return a 200 response with an error message and empty results
## Caching
The views support permission caching to improve performance:
```python
# settings.py
PERMISSION_CACHE = {
'TIMEOUT': 300, # Cache permissions for 5 minutes
'KEY_PREFIX': 'myapp',
'NAMESPACE': 'permissions'
}
```
To invalidate the cache:
```python
from django_tomselect.autocompletes import AutocompleteModelView
# Invalidate for specific user
AutocompleteModelView.invalidate_permissions(user=request.user)
# Invalidate all cached permissions
AutocompleteModelView.invalidate_permissions()
```
## Security Considerations
The package already includes built-in protections:
1. All user-provided values are automatically escaped using Django's `escape()` function
2. URLs are sanitized through the `safe_url()` utility which prevents unsafe schemes
3. Dictionary values are recursively sanitized via the `sanitize_dict()` utility
These protections work together to prevent XSS vulnerabilities when customizing rendering templates.
When creating custom templates and renderers for Tom Select widgets, always ensure proper escaping of user-provided values:
1. Use the `escape()` function for any user data inserted into HTML content
2. For URL attributes (href, src), always escape the URLs using the `escape()` function
3. Avoid using `new Function()` with user-provided content whenever possible
4. When customizing rendering templates, validate and sanitize all input
This is particularly important when using custom rendering templates with `data_template_option` and `data_template_item`.
## CompositeAutocompleteView (token widget backend)
Multiplexes multiple `AutocompleteModelView` and/or `AutocompleteIterablesView`
subclasses into a single endpoint that powers `TomSelectTokenWidget`. Three
GET routes by `mode=` query param:
- `?mode=operators` - JSON list of registered operator metadata.
- `?mode=value&op=&q=...` - delegates to the operator's bound view.
- `?mode=resolve&op=&id=[...]` - batch label resolution for chip rehydration.
```python
from django_tomselect import CompositeAutocompleteView, Operator
class ArticleTokenQueryView(CompositeAutocompleteView):
operators = [
Operator(
key="author",
view=AuthorAutocompleteView, # class ref or "url-name"
value_field="id", # JSON key in prepare_results()
label_field="name", # JSON key in prepare_results()
filter_lookup="authors__id", # parent-QS exact-match field path
label=_("Author"),
max_count=3,
),
Operator(
key="status",
view=ArticleStatusAutocompleteView, # iterables view
value_field="value",
label_field="label",
filter_lookup="status",
multi=True, # comma-separated: status:a,b
),
]
free_text_lookups = ["title__icontains"]
```
### `Operator` contract
- **Required:** `key`, `view`, `value_field`, `label_field`, exactly one of
`filter_lookup` or `q_translator`.
- **`bound_lookup`:** ORM lookup field for chip resolution. Defaults to
`value_field`. Override when `prepare_results()` projects renamed/computed keys.
- **`filter_lookup`:** exact-match field path (e.g. `"authors__id"`,
`"status"`). For `multi=True` the per-token lookup is `field__in=[values]`. For
non-exact behavior (icontains, gt/lt, custom expressions), use `q_translator`
instead.
- **`q_translator`:** callable `(operator, [values]) -> Q`. Maximum flexibility
for custom filtering.
- **`search_lookups`:** `None` inherits the bound view's lookups; `[]`
deliberately disables search; non-empty list overrides for this operator only
(instance-scoped per-request, no cross-request leakage).
- **`max_count` / `min_count`:** enforced by `TomSelectTokenField.clean()`.
### `split_search` flag (opt-in)
`AutocompleteModelView` gained a `split_search: bool = False` class attribute.
When `True`, `search()` splits the query on whitespace using a quote-aware
tokenizer; each term is OR'd across `search_lookups`, and per-term Qs are
ANDed together. Quoted phrases stay single terms. Default `False` preserves
existing behavior verbatim.
### Permission caveats
- **Iterables operators have no `has_permission()` hook** - they are
public-by-default. The composite view emits a one-time logger warning on
subclass registration. Gate sensitive iterables at the form/view layer.
- **Object-level permissions are NOT enforced row-by-row.** The resolve flow
honors queryset-level scoping (whatever your `get_queryset()` returns) and
dispatch-level `has_permission()`, but `has_object_permission()` is not
applied per-row by `prepare_results()`. Override `get_queryset()` for
per-row checks.
See {doc}`../example_app/article_token_search` for an end-to-end demo.