AutocompleteSelectMultiple for each item on the same page

I am trying to use AutocompleteSelectMultiple from django.contrib.admin.widgets outside the Django Admin application and meanwhile it works fine as long as there is only one of it. But in my template, I iterate through an object_list and I want to show the AutocompleteSelectMultiple for each item.

So the situation is different than on the Admin app where multiple AutocompleteSelectMultiple can occur, but they differ by field name.The request send while using the AutocompleteSelectMultiple is something like

http://localhost:8000/admin/autocomplete/?app_label=ads&model_name=my_model_name&field_name=my_field_name

In my case, there must be something else to differentiate for wich item is selected, e.g. the id of the item. I found on Stack Overflow that it should be possible to give select2 a data-select2-id as an attribute. How can I do that in concrete with AutocompleteSelectMultiple?

Besides that, my code shows only one occurence of the AutocompleteSelectMultiple correctly. As soon as there are more, only the last one (?!) looks and works like it should.

Here is my code:

# model.py
class Placement(models.Model):
    placement = models.CharField(max_length=250, blank=False)
    …

class Topic(TreeNodeModel):
    treenode_display_field = "name"
    name = models.CharField(max_length=50, blank=False)
    bad_for_topic = models.ManyToManyField(
        "Topic", related_name="bad_placement", default=None, blank=True
    )
    …

#forms.py
from django.contrib.admin.widgets
import AutocompleteSelectMultiple
from django.contrib import admin
from .models import Topic, KeyWord

class CustomAutocompleteSelect(AutocompleteSelectMultiple):
    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: 100%;",
            }
        )
        return attrs


class AddTopicForm(forms.Form):
    new_topic = forms.ModelChoiceField(
        queryset=Topic.objects.all(),
        widget=CustomAutocompleteSelect(
            KeyWord._meta.get_field("bad_for_topic"),
            "Choose topic(s) …",
            admin.site,
        ),
        label="",
    )

In my template, the following code renders the form (very nicely):

{% block extrahead %}
    {{ block.super }}
    {{ form.media }}
{% endblock %}
{{ form }}

That’s the main source of my wisdom, by the way: django - How to use the admin autocomplete field in a custom form? - Stack Overflow – and that’s how the HTML output looks like:

<span class="select2 select2-container select2-container--admin-autocomplete"
    dir="ltr" data-select2-id="3" style="width: 100%;">
    <span
        class="selection">
        <span
            class="select2-selection select2-selection--multiple"
            role="combobox" aria-haspopup="true" aria-expanded="false"
            tabindex="-1" aria-disabled="false">
            <ul class="select2-selection__rendered">
                <li class="select2-search select2-search--inline"><input
                        class="select2-search__field" type="search" tabindex="0"
                        autocomplete="off" autocorrect="off"
                        autocapitalize="none" spellcheck="false"
                        role="searchbox" aria-autocomplete="list"
                        placeholder="Choose topic …" style="width: 687px;">
                </li>
            </ul>
        </span>
    </span>
    <span class="dropdown-wrapper" aria-hidden="true"></span>
</span>

Well, it took me a while to make everything work so far and I read a lot about alternative ways, meaning not using the admin widget, see e.g. this post and this post. I even played with django-autocomplete-light, but that felt too complex somehow.

Meanwhile, the idea of simply building the HTML above in my template and to write my own handler which returns the necessary data to select2 sounds more attractive than digging deeper into AutocompleteSelectMultiple from django.contrib.admin.widgets, what do you think?

Any idea that could help to solve this problem is very appreciated.

I tried to make things simple now, without AutocompleteSelectMultiple from django.contrib.admin.widgets but with pure select2 following this example. So my template now contains:

<select class="tags-multiple" name="bad_for_topics-{{ item.id }}" multiple="multiple" style="width: 100%;">                             
    {% for topic in topics %}
        <option value="{{ topic }}">{{ topic }}</option>
    {% endfor %}
</select>

<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script>$(document).ready(function() {
    $('.tags-multiple').select2();
});</script>

I was surprised myself, but that’s really all I need to make select2 working. I noticed that I even do not need any lookup for autocomplete, because I do not have that many topics and it’s no problem to have them all at once.

The only problem that I still have to solve now: when I send the form, my view gets only the last item selected, i.e. request.POST['bad_for_topics-88'] for example is only "Topic3" even if I have chosen 2 other topics.

That’s actually a select2 and not Django problem, I guess, so I am going to dig deeper there. Anyway, if someone has a hint for me or an idea how to do this differently (and better), please tell me. Thanks!

1 Like

That was easy: request.POST.getlist("bad_for_topics-88") delivers all I need. :grinning:

1 Like