Search code examples
djangodjango-autocomplete-light

Free text in Select2ListView django-autocomplete-light


I'll expose my case as detailed as possible:

I'm using the functionality of autocomplete to automatically populate a value that is dynamic into the form. The form has several values, when you fill the first 3 I'm able to run an operation (with those values) and return a suggested value to the user. I needed to adapt a bit the return of parameters but I achieved it with Select2ListView

class MyFunctionAutocomplete(autocomplete.Select2ListView):
    def get_list(self):
        if not self.request.user.is_authenticated:
            return [""]
        try:
            field1_pk = self.forwarded["field1"]
            field2_pk = self.forwarded["field2"]
            field3_pk = self.forwarded["field3"]
        except AttributeError:
            return [""]
        try:
            ObjField1 = ModelField1.objects.get(pk=field1_pk)
            ObjField2 = ModelField2.objects.get(pk=field2_pk)
            ObjField3 = ModelField3.objects.get(pk=field3_pk)
            return_value = ObjField1.get_result(param1=ObjField2, param2=ObjField3)
        except (ModelField1.DoesNotExist, ModelField2.DoesNotExist, ModelField3.DoesNotExist):
            return [""]
        return [return_value]

With this code I'm able to do what I need when the requirements (forwarded value) are present. So far, so good

My problem: I cannot ADD any value as an extra option. This value IS NOT a ForeignKey, it's a simple Float. I want the user to be able to use my suggested value or instead just replace it by another value of their choice. Let's say in the dropdown they have the value 3.22221 and they want to write 12.11.

Since it can be practically ANY value I was thinking free text makes sense (I'll take care of cleaning data if necessary later). I checked and it looks like https://select2.org/tagging is what I want but not sure if I can use it here.

My form field in the form in case that helps is like:

result_field = Select2ListCreateChoiceField(
        widget=autocomplete.Select2(
            url="myapp:result_field_autocomplete",
            forward=["field1", "field2", "field3"],
            attrs={"data-container-css-class": ""},
        ),
        help_text=constants.RESULT_HELP,
)

Desired functionality:

result_field = Select2ListCreateChoiceField(
        widget=autocomplete.Select2(
            url="myapp:result_field_autocomplete",
            forward=["field1", "field2", "field3"],
            attrs={"data-container-css-class": ""},
            **free_text=True,**
        ),
        help_text=constants.RESULT_HELP,
)

And that the flag will allow me to enter ANY text of my choice.

I can get somewhere by changing the widget to TaggingSelect2 but that for sure it's not the option, is clumsy and the user will have multiple option which is NOT what I want.

Ideally all I would need (if someone can think of another option) is the info to appear as initial data in the form once I filled the other 3 elements by running that function. If that can be done with just another method it will be great as well.

I hope it makes sense :)


Solution

  • In case someone finds the same problem, my experience with Javascript is very limited but I compromised on having a button and creating a Javascript function that calls a view (in the same way django-autocomplete-light does):

    Final code:

    urls.py:

    path(
        "result-autocomplete/",
        views.MyFunctionAutocomplete,
        name="result_autocomplete",
    ),
    

    views.py:

    def MyFunctionAutocomplete(request):
        if not request.user.is_authenticated:
            return HttpResponse("")
        try:
            field1 = request.GET["field1"]
            field2 = request.GET["field2"]
            field3_name = request.GET["field3"]
            obj_Field3 = ModelField3.objects.get(name=field3_name)
        except AttributeError:
            return HttpResponse("")
        except ModelField3.DoesNotExist:
            return HttpResponse("")
    
        current_result = obj_Field3.get_result(f"{field1}{field2}")
        return HttpResponse(current_result["result"])
    

    getresult.js:

    function getResult(url){
     var field1 = document.querySelector('[id=select2-id_field1-container]').innerText
     var field2 = document.querySelector('[id=select2-id_field2-container]').innerText
     var field3_name = document.querySelector("#id_field3").selectedOptions[0].innerHTML
     if ( field1 && field2 && field3_name){
       $.ajax({                       // initialize an AJAX request
        url: url,
        data: {
          'field1': field1,       // add the field1 to the GET parameters
          'field2': field2,           // add the field2 to the GET parameters
          'field3_name': field3_name    // add the field3_name to the GET parameters
        },
        success: function (data) {   // `data` is the return of the `MyFunctionAutocomplete` view function
          $("#id_result")[0].value = data ;  // replace the contents of the price input with the data that came from the server
          $("#id_result")[0].placeholder = data // Not needed really
        }
      });
     }
    }
    

    django-template.html:

    [...]
          <form method="post">
            {% csrf_token %}
          {% for field in form %}
            {% if field.name == "result" %}
              <script defer src="{% static 'js/getresult.js' %}"></script>
              <button type="button" id="getresult" onclick="getresult('{% url "result_autocomplete"%}')">Get live result</button>
              {% bootstrap_field field show_label=True label_class="result" %}
            {% else %}
              {% bootstrap_field field %}
            {% endif %}
          {% endfor %}
    [...]
    

    I hope this helps other people like me with not really great JS skills and are stuck with something similar. I deployed this solution and it works, but if someone sees something wrong, let me know :-)