[Fixed]-How to use the admin autocomplete field in a custom form?

18👍

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.site)
    )

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):
    date_from = forms.DateField(label="From")
    date_to = 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

Or simple add {{ form.media }} to your template as pointed out by software engineer.

👤Kound

6👍

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

0👍

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 %}
👤Skratt

0👍

<link rel="stylesheet" href="{% static 'admin/css/vendor/select2/select2.css' %}" />
<link rel="stylesheet" href="{% static 'admin/css/autocomplete.css' %}"/>
<script src="{% static 'admin/js/vendor/select2/select2.full.js' %}" async></script>
<script src="{% static 'admin/js/autocomplete.js' %}" async></script>

When you have one form

class ExampleForm(forms.Form):
    prefix = 'examplePrefix'

Manually use select2, you can use Ajax a data source

<script type="text/javascript">
$(document).ready(function () {
    $('#id_{{custom_form.prefix}}-device_types').select2();
});
</script>

In this way if you have multiple form in same view you can use prefixed field name and select2 with or without datasource.

Hence no need to include 3rd party pip or CDN of any kind

Leave a comment