DJANGO and HTMX: Dependent dropdown is not populated, even though the values are valid in the console

Question:

(If anyone reads this question again, i’ve improved the code and the text, and add various new screenshot, including two below all: now it is clearer and more understandable)

I have three dropdowns and they are all dependent between them. The first and two dropdowns work fine, all ok, while the third dropdown doesn’t work: it’s empty with nothing inside.

The first dropdown menu, called trip_selector, correctly returns eg Spain, England, France. If I select Spain, then the second dropdown, called trip, is activated which correctly returns Madrid-Barcelona, Seville-Bilbao, etc. So, far so good, all ok, everything works fine.

THE PROBLEM: The problem is the third dropdown called extract_single_city in views.py, or in extract_single_city.html. In console i tried to print the items I would like to populate the dropdown (print(x), print(y) and print(options)), and they print for example Madrid and Barcelona values correctly, but I can’t populate them in the dropdown. I don’t get any errors.
enter image description here

WHAT I WANT:

  • I would like to make the second trips dropdown dependent on the third extract_single_city. As you can see I use HTMX. I wish that when I select in the second dropdown (called trips) for example Madrid-Barcelona then the third dropdown should be populated in extract_single_city.htmlwith Madrid and Barcelona, one for each item:
    enter image description here
    So I would like to populate extract_single_city.html dropdown with option (i.e. x and y) of the function def extract_single_city. I would like to use split() and loop (for) for educational purposes

  • For the sake of order and cleanliness, i would like to use all dropdowns in one file (forms.html). Currently in forms.html there is only first dropdown, then second and third dropdown are external html pages like trips.html and extract_single_city.html. So I would like to delete trips.html and extract_single_city.html and use all dropdowns in forms.html

IMPORTANT NOTES (FOR INFORMATIONAL PURPOSES ONLY):

I would use a for loop in the html page, and then in views.py I would use split() and options = [x, y]:

  • I know I may not use the loop (for) in html page, but if possible: for educational purposes I would like to use the loop (for)
  • I know I may not be using split, so write options = [trip.city_departure, trip.city_destination] directly, but let me explain: just for info, for example if in the third dropdown I select Madrid-Barcelona, I will need x = Madrid, and y = Barcelona to call it in future in web app code. So I thought about creating two separate variables like x and y, and use list like options = [x, y] to scroll in the dropdown. Either way, either I use one way or I use another way it doesn’t matter, because this doesn’t imply that the code of my question doesn’t currently work

What am I doing wrong? Am I doing something wrong in views.py, something in the html page or something in HTMX?

CODE:

extract_single_city.html

<select name="extract_single_city" id="id_extract_single_city" style="width:200px;" 
        hx-get="{% url 'extract_single_city' %}" 
        hx-target="#id_extract_single_city" 
        hx-indicator=".htmx-indicator" 
        hx-trigger="change" 
        hx-swap="none">

    <option value="">Please select</option>

    {% for i in options %}
        <option value="{{ i }}">{{ i }}</option>
    {% endfor %}
    
</select>

views.py

from django.shortcuts import render
from .models import Full_Record
import random
from django.db.models import F

def trip_selector(request):
    countries = Full_Record.objects.values(pk=F('country__id'), name=F('country__name')).distinct()
    trips = []
    return render(request, 'form.html', {'countries': countries, 'trips': trips})


def trips(request):
    if request.GET.get("country"):
        country = request.GET.get('country')
        trips = Full_Record.objects.filter(country=country)

    elif request.GET.get("trips"):
        selected_trip_id = int(request.GET.get('trips'))
        selected_trip = Full_Record.objects.get(id=selected_trip_id)
        request.session.update({"selected_trip_id": selected_trip_id})
        trips = Full_Record.objects.filter(country=selected_trip.country)

    return render(request, 'trips.html', {"trips": trips})
    
#THIRD DROPDOWN: ERROR???
def extract_single_city(request):
    if request.GET.get("trips"):
        trip = Full_Record.objects.get(id=request.session.get("selected_trip_id"))
        partita = f"{trip.city_departure}-{trip.city_destination}"

        x,y = trip.split('-')
        options = [x, y]

        context =  {"options": options}

        print("This is test of option", options) 
        print("This is test of single x", x) 
        print("This is test of single y", y) 

    return render(request, 'extract_single_city.html', context)

And more:

forms.html

{% extends 'base.html' %} 

{% block content %}

<!-- First Combobox -->
<label for="id_country">Country</label>

<select name="country" id="id_country" 
        hx-get="{% url 'trips' %}" 
        hx-target="#id_trip" 
        hx-indicator=".htmx-indicator" 
        hx-trigger="change"
        hx-swap="outerHTML">

  <option value="">Please select</option>
  {% for country in countries %}
  <option value="{{ country.pk }}">{{ country.name }}</option>
  {% endfor %}
</select>

<!-- Second Combobox ??????? (non lo so)-->
<label for="id_trip">Trip</label>
{% include "trips.html" %}


<!-- Third Combobox -->
<label for="id_extract_single_city">Soggetto</label>
<select name="extract_single_city" id="id_extract_single_city" style="width:200px;">
    <option value="">Please select</option>
</select>

{% endblock %}

trips.html

<!-- Second Combobox -->
<select name="trips" id="id_trip" style="width:200px;"  
        hx-get="{% url 'extract_single_city' %}?trip_id={{ trip.id }}" 
        hx-target="#id_extract_single_city" 
        hx-indicator=".htmx-indicator" 
        hx-trigger="change" 
        hx-swap="outerHTML">
    <option value="">Please select</option>
    {% for trip in trips %}
        <option value="{{ trip.id }}">{{ trip.city_departure }}-{{ trip.city_destination }}</option>
    {% endfor %}
</select>

urls.py

from django.urls import path
from . import views

urlpatterns = [
    path('', views.trip_selector, name="trip_selector"),
    #path('', views.trips, name='trips')
    path('trips', views.trips, name='trips'),
    path('result', views.result, name='result'),
    path('extract_single_city', views.extract_single_city, name='extract_single_city'),

]

UPDATE

I insert the code of models.py, of admin.py, and two screenshots below of admin panel. Sorry if I didn’t include this. I didn’t think it was important

models.py

from django.db import models

from django.db import models
from smart_selects.db_fields import ChainedForeignKey
from  django.contrib import admin

############ BASIC DATA ############

#Country
class Country(models.Model):
    name = models.CharField(max_length=40)

    def __str__(self):
        return self.name


#City
class City(models.Model):
    country = models.ForeignKey(Country, on_delete=models.CASCADE)

    name = models.CharField(max_length=40)
    def __str__(self):
        return self.name

############ FULL RECORD ############

class Full_Record(models.Model):
   
    country = models.ForeignKey(Country, on_delete=models.CASCADE)

    city_departure = ChainedForeignKey(
        City,
        related_name='city_departure_name',
        chained_field="country",
        chained_model_field="country",
        show_all=False,
        auto_choose=True,
        sort=True)
    
    city_destination = ChainedForeignKey(
        City,
        related_name='city_destination_name',
        chained_field="country",
        chained_model_field="country",
        show_all=False,
        auto_choose=True,
        sort=True)


    def trip(self):
        return f"{self.city_departure}-{self.city_destination}"
    

    def __str__(self):
        return self.country.name

NOTE: As you can see, first i insert the basic data Country and City, then in Full_Record I insert the line creating the combinations of trips (for example Madrid-Barcelona) from two dropdowns. As you can see, I use the data from Full_Record, because I get the countries and trip combinations from here

enter image description here

enter image description here

admin.py

from django.contrib import admin

# Register your models here.
from .models import Country, City, Full_Record

class Prossima(admin.ModelAdmin):
      list_display = ['id', 'country', 'trip']
admin.site.register(Full_Record, Prossima)


admin.site.register(Country)
admin.site.register(City)

base.html

{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>University</title>

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>

    <!-- HTMX -->
    <script src="https://unpkg.com/[email protected]/dist/htmx.min.js"></script>
</head>
<body>

    <div class="container mt-4">
        {% block content %}
        {% endblock %}
    </div>

    <!-- evitare che le richieste hx-post incorrano in errori CSRF con le viste Django -->
    <script>
        document.body.addEventListener('htmx:configRequest', (event) => {
            event.detail.headers['X-CSRFToken'] = '{{ csrf_token }}'; //insert csrf token when performing AJAX request
        })
    </script>

</body>
</html>

Thank you all!!!

Asked By: Horiatiki

||

Answers:

I have completelty redone my solution owing to your correction.

models.py
I have substituted ChainedForeignKey fields with Django’s nativeForeignKey fields as I don’t see a need for a library. Also because I have never used it.

I also set a property method departure_destination (which you named trip and implemented as a method).

from django.db import models

class Country(models.Model):
    name = models.CharField(max_length=40)

    def __str__(self):
        return self.name


class City(models.Model):
    country = models.ForeignKey(Country, on_delete=models.CASCADE)
    name = models.CharField(max_length=40)

    def __str__(self):
        return self.name


class FullRecord(models.Model):
    country = models.ForeignKey(Country, on_delete=models.CASCADE)
    city_departure = models.ForeignKey(City, on_delete=models.CASCADE, related_name="city_departure")
    city_destination = models.ForeignKey(City, on_delete=models.CASCADE, related_name="city_destination")

    @property
    def departure_destination(self):
        return f"{self.city_departure}-{self.city_destination}"

    def __str__(self):
        return self.country.name

Views.py

from django.shortcuts import render
from .models import FullRecord, Country


def trip_selector(request):
    if "Hx-Request" in request.headers:
        trips = FullRecord.objects.none()
        if request.headers["Hx-Trigger"] == "id_trip":
            country_id = request.GET.get("country")
            if country_id != "":
                trips = FullRecord.objects.filter(country_id=country_id)
            return render(request, "trips.html", {"trips": trips})
        elif request.headers["Hx-Trigger"] == "id_extract_single_city":
            selected_trip = request.GET.get("trips")
            extracted_cities = []
            if selected_trip != "":
                trip = FullRecord.objects.get(id=int(selected_trip))
                trip_with_combined_names = trip.departure_destination
                split_trip = trip_with_combined_names.split("-")
                extracted_cities = split_trip
            return render(request, "extract-single-city.html", {"options": extracted_cities})
    else:
        countries = Country.objects.all()
        return render(request, "form.html", {"countries": countries})
        

        

urls.py
from django.urls import path
from .views import trip_selector

urlpatterns = [
    path("", trip_selector, name="trips")
]

form.html

You can style it your way but you can take a look at the changes I made.

{% block main-content %}
    <div class="flex justify-between px-10 gap-5 mt-5">
        <div class="flex flex-col w-full">
            <!-- First Combobox -->
            <label for="id_country">Country</label>

            <select name="country" id="id_country">
                <option value="">Please select</option>
                {% for country in countries %}
                    <option value="{{ country.pk }}">{{ country.name }}</option>
                {% endfor %}
            </select>
        </div>

        <div class="flex flex-col w-full">
            <!-- Second Combobox ??????? (non lo so)-->
            <label for="id_trip">Trip</label>
            <select name="trips" id="id_trip"
                    hx-get="{% url 'trips' %}"
                    hx-include="[name=country]"
                    hx-indicator=".htmx-indicator"
                    hx-trigger="change from:#id_country">
            </select>
        </div>

        <!-- Third Combobox -->
        <div class="flex flex-col w-full">
            <label for="id_extract_single_city">Extract single city</label>
            <select name="extract_single_city" id="id_extract_single_city"
                    hx-get="{% url 'trips' %}"
                    hx-include="[name=trips]"
                    hx-indicator=".htmx-indicator"
                    hx-trigger="change from:#id_trip">

            </select>
        </div>
    </div>
{% endblock main-content %}

trips.html

<option value="">Please select</option>
{% for trip in trips %}
    <option value="{{ trip.id }}">{{ trip.city_departure }}-{{ trip.city_destination }}</option>
{% endfor %}

extract_single_city.html

<option value="">Please select</option>
{% for i in options %}
    <option value="{{ i }}">{{ i }}</option>
{% endfor %}
    


    

UPDATE

As per your wish to get rid of trips.html and extract_single_city.html and use only form.html, the following changes should have it work fine.

First, import format_html from django.utils.html. Because we will be creating string as HTML, that function will take care of the potential security vulnerabilities.

Next create the <option> markup as you would in the HTML template. Then with a for loop, you create your options.

See the updated code.

views.py

from django.shortcuts import render
from django.utils.html import format_html
from django.http import HttpResponse
from .models import FullRecord, Country

def trip_selector(request):
    if "Hx-Request" in request.headers:
        print(request.headers["Hx-Trigger"])
        if request.headers["Hx-Trigger"] == "id_trip":
            country_id = request.GET.get("country")
            trips = FullRecord.objects.none()
            trips_options = "<option value=''>Please select</option>"
            if country_id != "":
                trips = FullRecord.objects.filter(country_id=country_id)
                for option in trips:
                    new_option = f"<option value={option.id}>{option.departure_destination}</option>"
                    trips_options += new_option
            return HttpResponse(format_html(trips_options))

        elif request.headers["Hx-Trigger"] == "id_extract_single_city":
            selected_trip = request.GET.get("trips")
            extracted_cities = []
            extracted_options = "<option value="">Please select</option>"
            if selected_trip != "":
                trip = FullRecord.objects.get(id=int(selected_trip))
                trip_with_combined_names = trip.departure_destination
                split_trip = trip_with_combined_names.split("-")
                extracted_cities = split_trip
                for option in extracted_cities:
                    new_option = f"<option value={option}>{option}</option>"
                    extracted_options += new_option
            return HttpResponse(format_html(extracted_options))
    else:
        countries = Country.objects.all()
        return render(request, "form.html", {"countries": countries})

        

NOTE
You only need to change the views file only. You can now delete both trips.html and extract_single_city.html, leave everything else as it is.

Answered By: McPherson