autocomplete using Select2

In my model I have this class

class NodeTag(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    node = models.ForeignKey(Node, on_delete=models.CASCADE, related_name='node_tags', related_query_name='node_tag')
    tag = models.CharField(max_length=64)

In forms.py I have a form

class NodeTagForm(ModelForm):
    tag = ModelChoiceField(queryset=models.Tag.objects.none(), to_field_name="name")

    class Meta:
        model = models.NodeTag
        fields = (
            'tag',
            'order_by',
        )

Using Select2 I made the tag field an autocomplete field. The problem is that when I submit the form, I get the following error:

Select a valid choice. That choice is not one of the available choices.

The issue is solved by setting none() to all() in the form

tag = ModelChoiceField(queryset=models.Tag.objects.all(), to_field_name="name")

However, this results in a with a 1000+ options. I wonder whether there is a better way to implement the autocomplete.

Kind regards,

Johanna

How are you using Select2? Are you using the django-select2 integration library?

What does your JavaScript look like for that page?

What does your template look like?

Hi Ken,

Thanks for your reply.

No, I have these links in my _base.html template:

<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/jquery@3.5.0/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>

This is the JavaScript:

<script type="text/javascript">
      $(document).ready(function() {
        $('#id_tag').select2({
          ajax: {
            url: "{% url 'tag-autocomplete' %}",
            dataType: "json",
          },
          minimumInputLength: 2,
          placeholder: "Tag",
          theme: "bootstrap-5",
        }); //.id_tag
      }); //.document
</script>

And this is the template:

{% extends '_base.html' %}
{% load crispy_forms_tags %}

{% block title %}{{ title }}{% endblock title %}

{% block content %}
<div class="container">
  <h2>{{ title }}</h2>
  <form class="" action="" method="post">
    {% csrf_token %}
    {{ form|crispy }}
    <button type="submit" class="btn btn-primary btn-sm me-2" name="button">Save</button>
    <a href="#" onclick="window.history.back(); return false;" class="btn btn-primary btn-sm" role="button">Cancel</a>
  </form>
</div> <!-- /.container -->
{% endblock content %}

Furthermore, I have the form in my initial post, and class TagAutocompleteView() in views.py

I hope I provided you with sufficient information.

So the core issue here is that you’re using standard widgets to render the form field - which by default is going to include the contents of the queryset as the select options.

What I’d recommend would be to leave the queryset the same (using all so that validation works correctly), and modify the __init__ method of the form class to set the choices attribute of the tag field to be an empty list.

In the future, I would recommend using django-select2 for integrating Select2 in your Django projects. (I’m just not sure it’s worth the effort to do that now given the work you’ve already invested in your current solution.)

1 Like

Thanks for making clear what the issue is here.

Before investing the work in my current solution I explored django-select2 amongst others. Since I need a text editor as well, I also had a look at django-tinymce.

Reading the docs I realized that all these django- packages have their own approach to solving the issue at hand. After having a look at Django admin’s implementation of autocomplete, I decided to implement my current solution.

After modifying the __init()__ method, everything works fine.

def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['tag'].choices = [(self.instance.tag, self.instance.tag),]

Kind regards,

Johanna

I changed the tag field in the model above to:

tag = models.ForeignKey(Tag, on_delete=models.CASCADE, related_name='node_tag_tags', related_query_name='node_tag_tag')

Consequently, I had to make some changes to the code in forms.py and views.py. Everything works, except this line of code in the form’s __init__ method.

def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if self.instance:
            self.fields['tag'].choices = [(self.instance.tag_id, self.instance.tag_id),]
        else:
            self.fields['tag'].choices = []

I need the second occurrence of self.instance.tag_id to be string the representation of the tag not the id.

Kind regards,

Johanna

If self.instance.tag_id is the FK to Tag, then self.instance.tag is a reference to the Tag object - which means you can call any model method on that object or access any field.

So all these are available:
self.instance.tag.name
str(self.instance.tag)
self.instance.tag.__str__()
etc, etc.

Hi Ken,

Problem solved. I tried:

str(self.instance.tag_id)
self.instance.tag_id.__str__()

But that didn’t work.

One more thing; when do I use tag and when tag_id. I’ve got:

tag = ModelChoiceField()

and

self.fields['tag'].choices =

and

(self.instance.tag_id, self.instance.tag.name)

tag is the reference to the Tag object.
tag_id is the numerical foreign key of the object referred to as tag.

So if your Tag model has an entry like this:

 ID  |    name
_____|___________
   1 |  xyz

then tag would be something like Tag(id=1, name="xyz"), while tag_id = 1.

Thanks for your explanation, it’s clear now.