Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions docs/fields/autocomplete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
title: Autocomplete fields
order: 0
description: Guide to adding efficient, AJAX-powered autocomplete fields to Django admin with Unfold.
---

# Autocomplete fields

To add autocomplete functionality to `ModelChoiceField` and `ModelMultipleChoiceField`, use `UnfoldAdminAutocompleteModelChoiceField` and `UnfoldAdminMultipleAutocompleteModelChoiceField`.

**Steps to create autocomplete fields**

- Start by creating a custom view that subclasses `BaseAutocompleteView` and returns a JSON response containing the available select options. Be sure to perform all necessary permission checks within this view to control user access to the data.
- Next, register each custom view as a URL in your `ModelAdmin` class using the `custom_urls` attribute.
- Finally, in your custom form fields, set the `url_path` to the URL pattern name that points to your custom autocomplete view.

```python
from django import forms
from django.contrib import admin
from django.utils.translation import gettext_lazy as _

from unfold.admin import ModelAdmin
from unfold.views import BaseAutocompleteView
from unfold.fields import (
UnfoldAdminAutocompleteModelChoiceField,
UnfoldAdminMultipleAutocompleteModelChoiceField,
)

from .models import MyModel

# Custom ListView returning JSON with available select options
class MyAutocompleteView(BaseAutocompleteView):
model = MyModel

def dispatch(self, request, *args, **kwargs):
# DO THE PERMISSIONS CHECKS HERE
return super().dispatch(request, *args, **kwargs)

def get_queryset(self):
# ADDITIONAL FILTERS AND PERMISSIONS CHECKS HERE
return super().get_queryset()


@admin.register(MyModel)
class MyModelAdmin(ModelAdmin):
# Register custom ListView above
custom_urls = (
(
"autocomplete-url-path",
"custom_autocomplete_path_name",
MyAutocompleteView.as_view()
),
)

class MyForm(forms.Form):
one_object = UnfoldAdminAutocompleteModelChoiceField(
label=_("Object - Single value"),
# Important for validation. Make sure it offers same results as the custom view
queryset=MyModel.objects.all(),
# Map autocomplete results to the custom view
url_path="admin:custom_autocomplete_path_name",
)
multiple_objects = UnfoldAdminMultipleAutocompleteModelChoiceField(
label=_("Objects - Multiple values"),
queryset=MyModel.objects.all(),
url_path="admin:custon_autocomplete_path_name",
)
```
34 changes: 31 additions & 3 deletions src/unfold/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,24 @@
ManyToManyRel,
OneToOneField,
)
from django.forms import ModelChoiceField, ModelMultipleChoiceField, Widget
from django.forms.utils import flatatt
from django.template.defaultfilters import linebreaksbr
from django.urls import NoReverseMatch, reverse
from django.urls import NoReverseMatch, reverse, reverse_lazy
from django.utils.html import conditional_escape, format_html
from django.utils.module_loading import import_string
from django.utils.safestring import SafeText, mark_safe
from django.utils.text import capfirst

from unfold.settings import get_config
from unfold.utils import display_for_field, prettify_json
from unfold.widgets import CHECKBOX_LABEL_CLASSES, INPUT_CLASSES, LABEL_CLASSES
from unfold.widgets import (
CHECKBOX_LABEL_CLASSES,
INPUT_CLASSES,
LABEL_CLASSES,
UnfoldAdminAutocompleteWidget,
UnfoldAdminMultipleAutocompleteWidget,
)


class UnfoldAdminReadonlyField(helpers.AdminReadonlyField):
Expand Down Expand Up @@ -193,7 +200,7 @@ def _resolve_field(self) -> bool | list:


class UnfoldAdminField(helpers.AdminField):
def __init__(self, *args, **kwargs):
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)

try:
Expand Down Expand Up @@ -233,3 +240,24 @@ def label_tag(self) -> SafeText:
attrs=attrs,
label_suffix=required if self.field.field.required else "",
)


class AutocompleteFieldMixin:
def __init__(self, url_path: str, *args: Any, **kwargs: Any) -> None:
self.url_path = url_path
super().__init__(*args, **kwargs)

def widget_attrs(self, widget: Widget) -> dict[str, Any]:
return {
"data-ajax--url": reverse_lazy(self.url_path),
}


class UnfoldAdminAutocompleteModelChoiceField(AutocompleteFieldMixin, ModelChoiceField):
widget = UnfoldAdminAutocompleteWidget


class UnfoldAdminMultipleAutocompleteModelChoiceField(
AutocompleteFieldMixin, ModelMultipleChoiceField
):
widget = UnfoldAdminMultipleAutocompleteWidget
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% for name, value in widget.attrs.items %}
{% if name == "selected" %}
<option value="{{ value|stringformat:'s' }}"{% include "django/forms/widgets/attrs.html" %}>{{ widget.label }}</option>
{% endif %}
{% endfor %}
26 changes: 25 additions & 1 deletion src/unfold/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
from django.contrib.admin.views.main import ERROR_FLAG, PAGE_VAR
from django.contrib.admin.views.main import ChangeList as BaseChangeList
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.http import HttpRequest
from django.http import HttpRequest, JsonResponse
from django.views.generic import ListView

from unfold.exceptions import UnfoldException
from unfold.forms import DatasetChangeListSearchForm
Expand Down Expand Up @@ -79,3 +80,26 @@ def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
"model_admin": self.model_admin,
},
)


class BaseAutocompleteView(ListView):
paginate_by = 20

def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> JsonResponse:
super().get(request, *args, **kwargs)
context = self.get_context_data()

return JsonResponse(
{
"results": [
{
"id": obj.pk,
"text": str(obj),
}
for obj in self.object_list
],
"pagination": {
"more": context["page_obj"].has_next(),
},
}
)
28 changes: 28 additions & 0 deletions src/unfold/widgets.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
from collections.abc import Callable
from typing import Any

Expand Down Expand Up @@ -906,3 +907,30 @@ def __init__(self, attrs=None, render_value=False):
},
render_value,
)


class AutocompleteWidgetMixin:
def __init__(self, attrs: dict | None = None, choices: tuple = ()) -> None:
if not attrs:
attrs = {}

attrs.update(
{
"data-ajax--cache": "true",
"data-ajax--delay": 250,
"data-ajax--type": "GET",
"data-theme": "admin-autocomplete",
"data-allow-clear": json.dumps(not self.is_required),
"data-placeholder": "",
"class": "unfold-admin-autocomplete admin-autocomplete",
}
)
super().__init__(attrs, choices)


class UnfoldAdminAutocompleteWidget(AutocompleteWidgetMixin, Select):
option_template_name = "unfold/widgets/select_option_autocomplete.html"


class UnfoldAdminMultipleAutocompleteWidget(AutocompleteWidgetMixin, SelectMultiple):
option_template_name = "unfold/widgets/select_option_autocomplete.html"