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¶
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<br/>at various stages
AutocompleteModelView¶
- class django_tomselect.autocompletes.AutocompleteModelView(**kwargs)[source]¶
Bases:
JSONEncoderMixin,ViewBase view for handling autocomplete requests.
Intended to be flexible enough for many use cases, but can be subclassed for more specific needs.
- search_lookups¶
List of field lookups to search against when the user types. Uses Django’s ORM lookup syntax. Example: [‘name__icontains’, ‘email__icontains’] Multiple lookups are combined with OR logic.
- ordering¶
Field(s) to order results by. Can be a string, list, or tuple. Example: ‘name’ or [‘-created’, ‘name’]
- value_fields¶
List of model field names to include in the JSON response. These fields will be available to JavaScript for custom rendering. Example: [‘id’, ‘name’, ‘email’, ‘avatar_url’]
- virtual_fields¶
List of non-model field names that will be computed dynamically. Use this for calculated/derived values that don’t exist on the model. To populate virtual fields, override prepare_results() or define a prepare_{field_name} method. Example: [‘full_name’, ‘display_label’]
- list_url¶
URL name for the list view. Rendered as a “View All” link in the dropdown footer when
show_list=TrueandPluginDropdownFooterare set in the widget’sTomSelectConfig.- Type:
str | None
- create_url¶
URL name for the create view. Used in two contexts: (a) rendered as a “Create New” link in the dropdown footer when
show_create=TrueandPluginDropdownFooterare set in TomSelectConfig; (b) used as the HTMX POST target for inline creation whencreate=Trueandcreate_with_htmx=Trueare set in TomSelectConfig. Both require this attribute to be set to a valid URL name.- Type:
str | None
- permission_required¶
Permission string(s) required to access this view. Can be a single string or list/tuple of permission strings.
Note
Filter/Exclude Syntax
The
filter_byandexclude_byURL parameters allow dynamic filtering of results based on another form field’s value. This is useful for dependent dropdowns.Format:
'dependent_field__lookup_field=value'Where:
dependent_field: The name of the form field that triggers filteringlookup_field: The model field to filter on (can include lookups like__id)value: The value to filter by (usually from the dependent field)
Example URL parameters:
?filter_by=category__category_id=5 - Filter where category_id equals 5 ?exclude_by=author__author_id=3 - Exclude where author_id equals 3
In JavaScript/HTML, use data attributes on the widget:
data-filter-by="category__category_id" - Will filter by selected category data-exclude-by="author__author_id" - Will exclude by selected author
- classmethod __init_subclass__(**kwargs)[source]¶
Initialize subclass with default mutable attributes if not already set.
This prevents mutable class attributes (lists) from being shared across subclasses, which could cause unexpected behavior when one subclass modifies the list.
- Parameters:
kwargs (Any)
- Return type:
None
- apply_filters(queryset)[source]¶
Apply additional filters to the queryset.
Supports multiple filter_by and exclude_by parameters. Each parameter can be in one of: - Field filter: ‘dependent_field__lookup_field=value’ - Constant filter: ‘__const__lookup_field=value’
Multiple filters are combined with AND logic.
- get_permission_required()[source]¶
Get the permissions required for this view.
If permission_required is None, no permissions are required. Otherwise, use the specified permissions or fall back to model-based defaults.
- handle_no_permission(request)[source]¶
Handle cases where permission is denied.
Can be overridden to customize behavior.
- Parameters:
request (HttpRequest)
- Return type:
HttpResponse
- has_object_permission(request, obj, action='view')[source]¶
Check object-level permissions.
Can be overridden for custom object-level permissions.
- has_permission(request, action='view')[source]¶
Check if user has all required permissions.
Supports custom auth backends via Django’s auth system.
- hook_prepare_results(results)[source]¶
Hook method for customizing the prepared results.
This method is called at the end of prepare_results after all standard processing is complete. Override this method to modify the results without losing the base functionality.
- hook_queryset(queryset)[source]¶
Hook to allow for additional queryset manipulation before filtering, searching, and ordering.
For example, this could be used to prefetch related objects or add annotations that will later be used in filtering, searching, or ordering.
- classmethod invalidate_permissions(user=None)[source]¶
Invalidate cached permissions.
If user is provided, only invalidate that user’s permissions.
- Parameters:
user (User | None)
- Return type:
None
- order_queryset(queryset)[source]¶
Apply ordering to the queryset.
Handles string and list/tuple ordering values correctly. For strings: Converts single field string to list For lists/tuples: Uses as-is If no ordering specified: Falls back to model default
- paginate_queryset(queryset)[source]¶
Paginate the queryset with improved page handling.
- Parameters:
queryset (QuerySet)
- Return type:
PaginatedResponse
- prepare_results(results)[source]¶
Prepare the results for JSON serialization.
This method: 1. Gets values for specified fields 2. Ensures each result has an ‘id’ key 3. Adds view/update/delete URLs if configured 4. Calls hook_prepare_results for any custom processing
Important: This method should not reorder results, as order is already established by order_queryset.
- search(queryset, query)[source]¶
Apply search filtering to the queryset.
With
split_search=False(default) the entirequeryis OR’d acrosssearch_lookupsas a single icontains.With
split_search=Truethe query is whitespace-split via the shared quote-aware tokenizer (django_tomselect._tokenize); each term is OR’d acrosssearch_lookupsand the per-term Qs are ANDed together. Quoted phrases remain single terms.
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¶
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¶
# urls.py
from django.urls import path
from .views import BookAutocomplete
urlpatterns = [
path('book-autocomplete/', BookAutocomplete.as_view(), name='book-autocomplete'),
]
Advanced Configuration¶
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¶
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:
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.
Search Configuration
The search_lookups attribute defines how searching works:
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
]
Value Fields Configuration
The value_fields attribute determines which fields are included in the autocomplete results. This is critical for the widget’s functionality:
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:
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:
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.)
Queryset Customization
Use hook_queryset to optimize or customize the queryset:
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.
Permission Handling
Multiple ways to configure permissions:
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)
Result Preparation
Customize the data sent to the client:
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
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:
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:
class ContactAutocomplete(AutocompleteModelView):
model = Contact
json_encoder = 'myapp.encoders.PhoneNumberEncoder'
See Configuration documentation for details on setting a global default encoder.
Iterables Autocomplete Views¶
AutocompleteIterablesView Processing¶
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:<br/>1. Django Choices (Text/Integer)<br/>2. Tuple Iterables<br/>3. Simple Iterables
AutocompleteIterablesView¶
- class django_tomselect.autocompletes.AutocompleteIterablesView(**kwargs)[source]¶
Bases:
JSONEncoderMixin,ViewAutocomplete view for iterables and django choices classes.
This view handles autocomplete for choices, iterables, and enums.
Basic Usage¶
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¶
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¶
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 in the example app.
Response Format¶
Both view types return JSON responses in this format:
{
"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:
Invalid permissions return 403 Forbidden
Unauthenticated users are redirected to login
Invalid queries return empty results
Database errors return a 200 response with an error message and empty results
Caching¶
The views support permission caching to improve performance:
# settings.py
PERMISSION_CACHE = {
'TIMEOUT': 300, # Cache permissions for 5 minutes
'KEY_PREFIX': 'myapp',
'NAMESPACE': 'permissions'
}
To invalidate the cache:
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:
All user-provided values are automatically escaped using Django’s
escape()functionURLs are sanitized through the
safe_url()utility which prevents unsafe schemesDictionary 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:
Use the
escape()function for any user data inserted into HTML contentFor URL attributes (href, src), always escape the URLs using the
escape()functionAvoid using
new Function()with user-provided content whenever possibleWhen 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=<key>&q=...- delegates to the operator’s bound view.?mode=resolve&op=<k>&id=<v>[...]- batch label resolution for chip rehydration.
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 offilter_lookuporq_translator.bound_lookup: ORM lookup field for chip resolution. Defaults tovalue_field. Override whenprepare_results()projects renamed/computed keys.filter_lookup: exact-match field path (e.g."authors__id","status"). Formulti=Truethe per-token lookup isfield__in=[values]. For non-exact behavior (icontains, gt/lt, custom expressions), useq_translatorinstead.q_translator: callable(operator, [values]) -> Q. Maximum flexibility for custom filtering.search_lookups:Noneinherits 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 byTomSelectTokenField.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-levelhas_permission(), buthas_object_permission()is not applied per-row byprepare_results(). Overrideget_queryset()for per-row checks.
See Article Token-Style Search for an end-to-end demo.