Django forms – limiting options from fields based on the answer of another field

Question:

I have a Django form that receives entries from the users with information on a surgical procedure. Each Procedure will have only one (surgical) Technique and only one Diagnosis. Each Technique may be related to a limited number of Diagnosis, and each Diagnosis may be used on different Techniques. I want to limit which Diagnosis appear of the form based on which Technique the user selected previously on the form.

I tried using smart_selects ChainedManyToMany field with relative success, but it enable multiple Diagnosis to be selected, I only want to have one.

I`m also using DAL for autocompleting the Technique (over 1,6k options) as the user types.

My models:

# The "Technique" model
class Sigtap(models.Model):
    codigo = models.CharField(max_length=10)
    descricao = models.CharField(max_length=175, default='')


# The "Diagnosis" model
class Cid10(models.Model):
    codigo = models.CharField(max_length=4)
    descricao = models.CharField(max_length=270, default='')
    sigtap_compativel = models.ManyToManyField(Sigtap, blank=True)


# The "Surgical Procedure" model
class Cirurgia(models.Model):
    paciente = models.PositiveIntegerField(verbose_name='Número do prontuário')
    data = models.DateField(verbose_name='Data de realização')
    procedimento = models.ForeignKey(Sigtap, on_delete=models.CASCADE,
                                     verbose_name='Código do procedimento (SIGTAP)')
    cid = ChainedManyToManyField(
        Cid10, horizontal=True, chained_field='procedimento', chained_model_field='sigtap_compativel', auto_choose=True, verbose_name='CID-10')

My forms:

class CirurgiaModelForm(forms.ModelForm):
    class Meta:
        model = Cirurgia
        fields = ['paciente', 'data', 'procedimento', 'cid']
        widgets = {
            'procedimento': autocomplete.ModelSelect2(url='sigtap-autocomplete'),
        }

How can I get the form field to show only the Diagnosis related to the Technique selected and allow only one option?

Asked By: ffg88

||

Answers:

Your way of naming fields and classes confused me a lot, because the name of the classes does not represent what they really are…and also there is this:

procedimento = models.ForeignKey(Sigtap, on_delete=models.CASCADE,
                                     verbose_name='Código do procedimento (SIGTAP)')

where you change the name I don’t know for what reason, if you are going with codes like cid and sigtap just stick with it. Really bad practice overall.

Anyway, what you need is to use AJAX to send a request to a view where you filter a Model based on a given id, then use that data back in the template. I was not able to fit this solution into a ModelForm because of the nature of the relationship (will need to look into that) so instead I used a <select> tag inside the HTML form:

forms.py:

class CirurgiaModelForm(forms.ModelForm):
    procedimento = forms.ModelChoiceField(queryset=Sigtap.objects.all(), widget=forms.Select({'onchange' : "myFunction(this.value);"}))

    class Meta:
        model = Cirurgia
        fields = ['paciente', 'data', 'procedimento']
        widgets = {
          'data': forms.DateInput(attrs={'type': 'date'}),
        }

views.py

import json
from django.http import JsonResponse

def surgery(request):
    if request.method == 'POST':
        form = CirurgiaModelForm(request.POST)
        if form.is_valid():
            try:
                cid = Cid10.objects.get(id=request.POST.get('cid'))
                cirurgia = Cirurgia.objects.create(**form.cleaned_data)
                cirurgia.cid.add(cid)
            except ObjectDoesNotExist:
                pass

            return redirect('/surgery/')

    else:
        form = CirurgiaModelForm()
    
    return render(request, 'surgery.html', {'form': form})

def filter_diagnosis_ajax(request):
    data = json.loads(request.body)
    procedimento = Sigtap.objects.get(id=data['technique_id'])
    cid_list = list(Cid10.objects.filter(sigtap_compativel=procedimento).values())
    return JsonResponse({'diagnoses': cid_list})

surgery.html (populate select, clear select)

{% block content %}
<form action="surgery/" method="post">
    {% csrf_token %}
    {{form.as_p}}
    <label>diagnosis: </label><select name="cid" id="id_cid"></select>
    <br>
    <br>
    <button type="submit" >Create Procedure</button>
</form>

<script>
    function getCookie(name) {
    let cookieValue = null;
    if (document.cookie && document.cookie !== '') {
            const cookies = document.cookie.split(';');
            for (let i = 0; i < cookies.length; i++) {
                const cookie = cookies[i].trim();
                // Does this cookie string begin with the name we want?
                if (cookie.substring(0, name.length + 1) === (name + '=')) {
                    cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                    break;
                }
            }
        }
        return cookieValue;
    }

    function removeOptions(selectElement) {
        var i, L = selectElement.options.length - 1;
        for(i = L; i >= 0; i--) {
            selectElement.remove(i);
        }
    }

    function myFunction(value) {
        url = '/filter/technique/'
        const csrftoken = getCookie('csrftoken');

        fetch(url, {
            method: 'POST',
            headers: {
                'X-CSRFToken': csrftoken,
                'Content-Type': 'application/json'
            },
            mode: 'same-origin',
            body: JSON.stringify({'technique_id': value}),
        })
        .then((response) => response.json())
        .then((data) => {
            var diagnosis_select = document.getElementById("id_cid");
            removeOptions(diagnosis_select);
            for (var i = 0; i<=data.diagnoses.length-1; i++){
                var opt = document.createElement('option');
                opt.value = data.diagnoses[i]['id'];
                opt.innerHTML = data.diagnoses[i]['descricao'];
                diagnosis_select.appendChild(opt);
            }
            
        })
        .catch((error) => {
            console.error('Error:', error);
        });
    }
</script>
{% endblock %} 
Answered By: Niko

As I was already using django-autocomplete-light I managed to do it with little modifications on Django Autocomplete Light Documentation

Answered By: ffg88