'How to use the admin autocomplete field in a custom form?
In the Django admin interface you can set a field to be a autocomplete field, e.g.:
autocomplete_fields = ('countries', )
This works great for admin pages, how does one use the autocomplete field in a custom view/form? My research points towards django-autocomplete-light, but it seems non-ideal to install a 3rd party package when Django already has the functionality built-in.
Solution 1:[1]
Indeed it is possible to use the select2 from the admin.
You simply need to use the AutocompleteSelect
widget. The AutocompleteSelect widget expects an relation and an admin site.
If you have model A
that has a ForeignKey field pointing to model B
for which you want to use the AutocompleteSelect, you simply can use (as suggested by cuto).
from django.contrib.admin.widgets import AutocompleteSelect
from myapp.model import ModelA, ModelB
from django.contrib import admin
class MyForm(form.Form):
model_b = forms.ModelChoiceField(
queryset=ModelB.objects.all(),
widget=AutocompleteSelect(ModelA._meta.get_field('model_b').remote_field, admin.AdminSite)
)
As I did not have a relation I used a FakeRelation
class, as only the model property is used by the get_url(self)
function.
The usage of the AutocompleteSelect widget is bound to the same condition as the the usage of the autocomplete_fields widget.
from django.contrib.admin.widgets import AutocompleteSelect
from django.contrib import admin
from django import forms
from myapp.models import countries
class FakeRelation:
def __init__(self, model):
self.model = model
class CustomAutocompleteSelect (AutocompleteSelect):
def __init__(self, model, admin_site, attrs=None, choices=(), using=None):
rel = FakeRelation(model)
super().__init__(rel, admin_site, attrs=attrs, choices=choices, using=using)
class PreventionPlanForm(form.Form):
DateFrom = forms.DateField(label="From")
DateTo = forms.DateField(label="To")
PE1_Name = forms.ModelChoiceField(
queryset=countries.objects.all(),
widget=CustomAutocompleteSelect(countries, admin.AdminSite)
)
As jenniwren pointed out: Make sure to load the correct Javascripts/CSS files in your template (path could be subject to change in different django versions):
- admin/css/vendor/select2/select2.css
- admin/js/vendor/select2/select2.full.js
- admin/css/autocomplete.css
- admin/js/autocomplete.js
Solution 2:[2]
The FakeRelation solution proposed before does not work anymore with Django 2.2 which requires a field instance for the AutocompleteSelect constructor.
I had a suitable field in my project, so I could use it, but had to pass a field instance instead of a relation. Here is the code for a custom AutocompleteSelect which also add the option to pass a specific placeholder to Select2:
class CustomAutocompleteSelect(AutocompleteSelect):
def __init__(self, field, prompt="", admin_site=None, attrs=None, choices=(), using=None):
self.prompt = prompt
super().__init__(field, admin_site, attrs=attrs, choices=choices, using=using)
def build_attrs(self, base_attrs, extra_attrs=None):
attrs = super().build_attrs(base_attrs, extra_attrs=extra_attrs)
attrs.update({
'data-ajax--delay': 250,
'data-placeholder': self.prompt,
'style': 'width: 30em;'
})
return attrs
class AddLittermateForm(forms.Form):
new_littermate = forms.ModelChoiceField(
queryset=Dog.objects.all(),
widget=CustomAutocompleteSelect(LitterDog._meta.get_field(
'dog'), "Search for a littermate here", admin.site)
)
If one did not have a model with a suitable relation, they would have to declare one that is not managed:
...
class Meta:
managed = False
Solution 3:[3]
I spent a few hours trying to understand why my code would not work, until I stumble on @jenniwren comment about reference to css/js files. Here is a complete working solution.
To use the AutocompleteSelect widget in any custom form for signed-in users having both 'staff' and 'view' access to the given model, you could declare the following:
from django.urls import reverse
from django.contrib.admin.widgets import AutocompleteSelect
from django.contrib import admin
class UserAutocompleteSelect(AutocompleteSelect):
def get_url(self):
model = CustomUser
return reverse(self.url_name % (self.admin_site.name, model._meta.app_label, model._meta.model_name))
class UserChoiceField(forms.ModelChoiceField):
def __init__(self, queryset=None, widget=None, **kwargs):
if queryset is None:
queryset = CustomUser.objects.all()
if widget is None:
widget = UserAutocompleteSelect(None, admin.site) # pass `None` for `rel`
super().__init__(queryset, widget=widget, **kwargs)
class UserAutocompleteSelectForm(forms.ModelForm):
"""
for changing user on Play objects
using amdin module autocomplete
"""
user = UserChoiceField(
# queryset=CustomUser.objects.all(),
help_text=_('Select the user to replace the current one')
)
class Meta:
model = Play
fields = ('user', )
You can use the same, replacing CustomUser
and Play
by your own models
And if this is not working out-of-the-box with the html template you're using, that means that you need to include the required css/js files to your template. Here is a simple way to do it :
Providing that the form is declared as such in the view:
form = UserAutocompleteSelectForm()
...
context = {
'form': form,
...
}
return render(request, 'users/change_user.html', context)
you should add the following lines to the html template to include the required css/js files:
{% block extrahead %}
{{ block.super }}
{{ form.media }}
{% endblock %}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | jphilip |
Solution 3 | Skratt |