4-way Dependent/Chained Dropdown List with Django (Location dropdown list)

I want to build Location dropdown list for choose location. It will contain in Profile Edit.
I built 3-way dropdown in jQuery, it does not work good in my template but it works. However, I have an error “Select a valid choice. That choice is not one of the available choices”, when I save form.
Also, when I tried to build 4-way dropdown It has broken at all. I don’t know how to solve this.
Maybe it may build in other approach I will be happy to know it (for example, HTMX).

models.py

class Profile(models.Model):
    slug = models.SlugField(unique=True)
    user = models.OneToOneField(get_user_model(), on_delete=models.CASCADE)
    country = models.ForeignKey(
        "Country", on_delete=models.SET_NULL, null=True, blank=True)
    state = models.ForeignKey(
        'State', blank=True, on_delete=models.SET_NULL, null=True)
    city = models.ForeignKey(
        "City", on_delete=models.SET_NULL, null=True, blank=True)
    street = models.ForeignKey(
        'Street', on_delete=models.SET_NULL, null=True, blank=True)

class Country(models.Model):
    name = models.CharField(max_length=100, blank=True, null=True)

    def __str__(self):
        return self.name

    class Meta:
        ordering = ["-name"]


class State(models.Model):
    name = models.CharField(max_length=100, blank=True, null=True)
    country = models.ForeignKey(
        'Country', on_delete=models.CASCADE, blank=True, null=True)

    def __str__(self):
        return self.name

    class Meta:
        ordering = ["-name"]


class City(models.Model):
    name = models.CharField(max_length=100, blank=True, null=True)
    state = models.ForeignKey(
        'State', on_delete=models.CASCADE, blank=True, null=True)

    def __str__(self):
        return self.name

    class Meta:
        ordering = ["-name"]


class Street(models.Model):
    name = models.CharField(max_length=100, blank=True, null=True)
    city = models.ForeignKey(
        'City', on_delete=models.CASCADE, blank=True, null=True)

    def __str__(self):
        return self.name

    class Meta:
        ordering = ["-name"]

views.py

class ProfileDetailView(DetailView):
    model = Profile
    template_name = "users/profile.html"


def update_profile_about(request, slug):
    if request.method == "POST":
        user_form = UserForm(
            request.POST, instance=request.user, prefix="user")
        profile_form = ProfileForm(
            request.POST, request.FILES, instance=request.user.profile, prefix="profile"
        )
        if all([user_form.is_valid(), profile_form.is_valid()]):
            us = user_form.save()
            prof = profile_form.save()

            prof.save()

            messages.success(request, _(
                "Your profile was successfully updated!"))
            return redirect("profile", slug=slug)
        else:
            messages.error(request, _("Please correct the error below."))
    else:
        user_form = UserForm(instance=request.user, prefix="user")
        profile_form = ProfileForm(
            instance=request.user.profile, prefix="profile")
    return render(
        request,
        "users/profile_update_about.html",
        {"user_form": user_form, "profile_form": profile_form, },
    )

def load_states(request):
    country_id = request.GET.get('country')
    states = State.objects.filter(country_id=country_id).order_by('name')
    return render(request, 'users/partials/state_dropdown_list_options.html', {'states': states})


def load_cities(request):
    state_id = request.GET.get('state')
    cities = City.objects.filter(state_id=state_id).order_by('name')
    return render(request, 'users/partials/city_dropdown_list_options.html', {'cities': cities})


def load_streets(request):
    city_id = request.GET.get('city')
    streets = Street.objects.filter(city_id=city_id).order_by('name')
    return render(request, 'users/partials/street_dropdown_list_options.html', {'streets': streets})

urls.py

urlpatterns = [
    path("profile/<slug:slug>", ProfileDetailView.as_view(), name="profile"),
    path('ajax/load-states/', views.load_states, name='ajax_load_states'),
    path('ajax/load-cities/', views.load_cities, name='ajax_load_cities'),
    path('ajax/load-streets/', views.load_streets, name='ajax_load_streets'),
]

forms.py

class ProfileForm(forms.ModelForm):
    class Meta:
        model = Profile
        fields = (
            "country",
            "state",
            "city",
            "street",
        )
  # State
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['state'].queryset = State.objects.none()

        if 'country' in self.data:
            try:
                country_id = int(self.data.get('country'))
                self.fields['state'].queryset = State.objects.filter(
                    country_id=country_id).order_by('name')
            except (ValueError, TypeError):
                pass
        elif self.instance.pk and self.instance.country:
            self.fields['state'].queryset = self.instance.country.state_set.order_by(
                'name')

    # City
        self.fields['city'].queryset = City.objects.none()

        if 'state' in self.data:
            try:
                state_id = int(self.data.get('state'))
                self.fields['city'].queryset = City.objects.filter(
                    state_id=state_id).order_by('name')
            except (ValueError, TypeError):
                pass
        elif self.instance.pk and self.instance.state:
            self.fields['city'].queryset = self.instance.state.city_set.order_by(
                'name')

    # Street
        self.fields['street'].queryset = State.objects.none()

        if 'city' in self.data:
            try:
                city_id = int(self.data.get('city'))
                self.fields['street'].queryset = Street.objects.filter(
                    city_id=city_id).order_by('name')
            except (ValueError, TypeError):
                pass
        elif self.instance.pk and self.instance.city:
            self.fields['street'].queryset = self.instance.city.street_set.order_by(
                'name')

profile_update_about.html (HTML when content forms and Location dropdown list )

{% extends 'base.html' %}


{% block content %}
      <form method="POST" enctype="multipart/form-data" id="profileForm" data-states-url="{% url 'ajax_load_states' %}"
        novalidate>
        {% csrf_token %}
        <div class="form-group mb-3">
          <label for="{{ profile_form.state.id_for_label }}" class="form-label">State:</label>
          {{ profile_form.state }}

          {% if profile_form.state.errors %}
          <div class="alert alert-warning" role="alert">
            {% for error in profile_form.state.errors %}
            {{error}}
            {% endfor %}
          </div>
          {% endif %}
        </div>

        <div class="form-group mb-3">
          <label for="{{ profile_form.city.id_for_label }}" class="form-label">City:</label>
          {{ profile_form.city }}

          {% if profile_form.city.errors %}
          <div class="alert alert-warning" role="alert">
            {% for error in profile_form.city.errors %}
            {{error}}
            {% endfor %}
          </div>
          {% endif %}
        </div>

        {% comment %} <div class="form-group mb-3">
          <label for="{{ profile_form.street.id_for_label }}" class="form-label">Street:</label>
          {{ profile_form.street }}

          {% if profile_form.street.errors %}
          <div class="alert alert-warning" role="alert">
            {% for error in profile_form.street.errors %}
            {{error}}
            {% endfor %}
          </div>
          {% endif %}
        </div> {% endcomment %}

    </div>

  </div>


  <div class="d-flex justify-content-center align-items-center mb-3">
    <button class="btn btn-outline-primary">
      Update
    </button>
  </div>
  </form>

<script>
  // State
  $("#id_profile-country").change(function () {
    var url = $("#profileForm").attr("data-states-url"); 
    var countryId = $(this).val(); 

    $.ajax({ 
      url: '{% url '
      ajax_load_states ' %}', 
      data: {
        'country': countryId 
      },
      success: function (data) { 
        $("#id_profile-state").html(
          data); 
      }
    });

  });

  // City
  $("#id_profile-state").change(function () { 
    var url = $("#profileForm").attr("data-states-url"); 
    var stateId = $(this).val(); 

    $.ajax({ 
      url: '{% url '
      ajax_load_cities ' %}', 
      data: {
        'state': stateId 
      },
      success: function (data) { 
        $("#id_profile-city").html(
          data); 
      }
    });

  });

  // Street 
  $("#id_profile-city").change(function () { 
    var url = $("#profileForm").attr("data-states-url"); 
    var cityId = $(this).val(); 

    $.ajax({ 
      url: '{% url '
      ajax_load_streets ' %}', 
      data: {
        'city': cityId 
      },
      success: function (data) { 
        $("#id_profile-street").html(
          data); 
      }
    });

  });
</script>

{% endblock content %}

state_dropdown_list_options.html

<option value="">---------</option>
{% for state in states %}
<option value="{{ state.pk }}">{{ state.name }}</option>
{% endfor %}

I think the best approach is to use Django Smart Select library. I have use it and is great, and in that case that you will expose only name of places it fits perfectly.

1 Like

Hello!
Thanks for the answer! I built your approach and It works.
Can you give me advice? I want to add an autocomplete field in Country and City. For example, I type “Fra” and field gives me “France”. How can I build it?

for all dropdowns he used same url in html ajax code whereas to load the data we have created different html, are you sure this is working?