Last active
September 25, 2024 08:02
-
-
Save cb109/d73dbe2833cb15c980684d81df5a51cd to your computer and use it in GitHub Desktop.
Reusing the Django Admin AutocompleteSelect Widget in a custom Template
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- 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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
... | |
urlpatterns = [ | |
... | |
url( | |
r"^autocomplete/mycustommodel$", | |
myproject.myapp.views.search_mycustommodel, | |
), | |
... | |
] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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