Skip to content

Instantly share code, notes, and snippets.

@cb109
Last active September 25, 2024 08:02
Show Gist options
  • Save cb109/d73dbe2833cb15c980684d81df5a51cd to your computer and use it in GitHub Desktop.
Save cb109/d73dbe2833cb15c980684d81df5a51cd to your computer and use it in GitHub Desktop.
Reusing the Django Admin AutocompleteSelect Widget in a custom Template
from typing import Iterable
from typing import Optional
from django.contrib import admin
from django.contrib.admin.widgets import AutocompleteSelect
from django.forms import ModelChoiceField
from myproject.myapp.models import MyCustomModel
class CustomUrlAutocompleteSelect(AutocompleteSelect):
"""URL is not based on url_name implicitly, instead passed in as a kwarg."""
def __init__(self, *args, **kwargs):
self.url = kwargs.pop("url")
super().__init__(*args, **kwargs)
def get_url(self):
return self.url
def build_modelchoicefield(
autocomplete_url: str,
model: type,
fieldname: str,
queryset: Iterable,
label: Optional[str] = None,
required: bool = False,
) -> ModelChoiceField:
# FIXME:
# Our shenanigans to use the AutocompleteSelect outside of the
# admin have a little drawback here: The queryset that we pass
# will be used to check for a valid selection, but it is not
# what the API returns, so it will not affect what we see in
# the dropdown, unfortunately. It will however be respected
# when we submit the form and display an error 'invalid
# selection'.
options = {
"queryset": queryset,
"widget": CustomUrlAutocompleteSelect(
model._meta.get_field(fieldname).remote_field,
admin.site,
url=autocomplete_url,
),
"required": required,
}
if label:
options["label"] = label
return ModelChoiceField(**options)
class MyCustomModelForm(forms.ModelForm):
"""Note: Make sure to include {{ form.media }} in the template."""
class Meta:
model = MyCustomModel
fields = ("id",)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["id"] = build_modelchoicefield(
autocomplete_url="/autocomplete/mycustommodel",
model=MyCustomModel,
fieldname="id",
queryset=MyCustomModel.objects.all(),
required=False,
)
# HACK: Rename field to use a less generic input name in html,
# otherwise the selet would have name="id".
self.fields["mycustommodel"] = self.fields["id"]
del self.fields["id"]
<!-- To make the AutocompleteSelect work we need to include some CSS and javascript -->
{{ autocomplete_form.media }}
<form id="mycustommodel_form">
{{ autocomplete_form }}
<button type="submit">Submit</button>
</form>
...
urlpatterns = [
...
url(
r"^autocomplete/mycustommodel$",
myproject.myapp.views.search_mycustommodel,
),
...
]
from typing import Callable
from typing import Iterable
from django.http import JsonResponse
from myproject.myapp.forms import MyCustomModelForm
from myproject.myapp.models import MyCustomModel
from myproject.myapp.utils import search
def make_autocomplete_response(
queryset: Iterable, instance_to_text_func: Callable
) -> JsonResponse:
return JsonResponse(
{
"results": [
{"id": instance.id, "text": instance_to_text_func(instance)}
for instance in queryset
],
"pagination": {"more": False},
}
)
def search_mycustommodel(request):
term = request.GET.get("term", "")
candidates = search(MyCustomModel.objects, term, ["name"]).order_by(
"name",
)
return make_autocomplete_response(
candidates, lambda instance: instance.name
)
def template(request, pk: int):
instance = MyCustomModel.objects.get(pk=pk)
autocomplete_form = MyCustomModelForm(
initial={"mycustommodel": instance},
)
return render_to_response(
"myapp/template.html", {"autocomplete_form": autocomplete_form}
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment