Unveiling the Secrets: Crafting Effortless Dependent Forms in Django

Unveiling the Secrets: Crafting Effortless Dependent Forms in Django

ยท

7 min read

Confession time: My first attempt at dependent forms in Django was, uh, messy. But, we learn by doing, right? And guess what? My persistence paid off! I cracked the code (metaphorically, of course) and found a much better and efficient way to create dependent form in Django. Want to know my how? continue reading...

I wrote about this same topic earlier this year, where I used a not so efficient method, but that was what I knew then, you can read about it here.

A basic knowledge of Django is required.

You can follow along by downloading the code examples Here

Let's get started.

Create a new folder dependent_form and cd into the newly created folder.

mkdir dependent_form
cd dependent_form/

Next, create a new virtual environment, and activate it.

python -m venv venv
source venv/Scripts/activate

Install Django, start a new project, then create a new app.

pip install django
django-admin startproject d_form .
python manage.py startapp core

In the project settings.py, add the newly created app to the INSTALLED_APPS list.

# d_form/settings.py
    ...
    'core.apps.CoreConfig',

We are still going to use the Person, State and Town example I used in the first tutorial. A person can be from a state, and a town from the state. We have many states, but each states has many towns.

In your app core/models.py copy and paste the following code in it.

# core/models.py
from django.db import models


class State(models.Model):
    name = models.CharField(max_length=155)

    def __str__(self):
        return self.name


class Town(models.Model):
    name = models.CharField(max_length=155)
    state = models.ForeignKey(State, on_delete=models.CASCADE)

    def __str__(self):
        return f"{self.name} - {self.state}"

class Person(models.Model):
    name = models.CharField(max_length=155)
    state = models.ForeignKey(State, on_delete=models.CASCADE)
    town = models.ForeignKey(Town, on_delete=models.CASCADE)

    def __str__(self):
        return self.name

Next, In the app core create a new file forms.py, copy and paste the following into it.

# core/forms.py
from django import forms

from core.models import Person, Town


class CreatePersonForm(forms.ModelForm):
    class Meta:
        model = Person
        fields = ['name', 'state', 'town']

Next is to create a View for creating the Person instance. For this, we are going to be using the Django generic view CreateView which can be gotten from django.views.generic. copy and paste the following in your core/views.py

# core/views.py
from django.shortcuts import render
from django.views.generic import CreateView
from .forms import CreatePersonForm


class CreatePersonView(CreateView):
    form_class = CreatePersonForm
    template_name = 'core/create_person.html'

Create a new folder in your app core templates/core, and in the core sub-folder, create a file create_person.html. The structure should now be templates/core/create_person.html.

in the create_person.html file, copy and paste the HTML code below ;

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <form action="" method="post">
        {% csrf_token %}
        {{ form.as_p }}
        <input type="submit" value="Create Person">
    </form>
</body>
</html>

Create a new file urls.py in your core app directory.

In your project urls.py, we need to include the newly created app urls.py Your project urls.py should look similar to;

# d_form/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('core.urls'))
]

update your app urls.py to;

# core/urls.py
from django.urls import path
from . import views


urlpatterns = [
    path('create-person', views.CreatePersonView.as_view(), name='create_person'),
]

copy and paste the following in your app core/admin.py

# core/admin.py
from django.contrib import admin
from .models import State, Town, Person


admin.site.register(State)
admin.site.register(Town)
admin.site.register(Person)

Run python manage.py makemigrations and python manage.py migrate to create the migrations table.

Create a superuser to create the model instances using the admin panel.

python manage.py createsuperuser --username 'admin' --email 'admin@gmail.com'

Enter the password when prompted, run the development server python manage.py runserver then log into the admin panel using your details by visiting http://127.0.0.1:8000/admin/

You can manually Create instances of State and Town in that state using the admin panel, but I would be using a management script for that.

To create a management script, create a folder that look similar to;

core/
|-- __init__.py
|-- management/
|   |-- __init__.py
|   |-- commands/
|       |-- __init__.py
|       |-- load_town_state.py

Copy and paste the following into the load_town_state.py file.

# management/commands/load_town_state.py
from django.core.management import BaseCommand
from core.models import Town, State

nigerian_states = {
    'Lagos': ['Lagos', 'Ikeja', 'Victoria Island', 'Surulere', 'Lekki'],
    'Kano': ['Kano', 'Dala', 'Gwale', 'Fagge', 'Kumbotso'],
    'Oyo': ['Ibadan', 'Ogbomosho', 'Iseyin', 'Saki', 'Eruwa'],
    'Rivers': ['Port Harcourt', 'Obio-Akpor', 'Ikwerre', 'Eleme', 'Oyigbo'],
    'Kaduna': ['Kaduna', 'Zaria', 'Sabon Gari', 'Kafanchan', 'Makarfi'],
    'Katsina': ['Katsina', 'Daura', 'Funtua', 'Malumfashi', 'Mani'],
    'Delta': ['Asaba', 'Warri', 'Sapele', 'Ughelli', 'Kwale'],
    'Ogun': ['Abeokuta', 'Ijebu-Ode', 'Sagamu', 'Ilaro', 'Ota'],
    'Jigawa': ['Dutse', 'Hadejia', 'Birnin Kudu', 'Kazaure', 'Gumel'],
    'Kwara': ['Ilorin', 'Offa', 'Kaiama', 'Jebba', 'Omu-Aran'],
    'Benue': ['Makurdi', 'Gboko', 'Otukpo', 'Adikpo', 'Katsina-Ala'],
    'Sokoto': ['Sokoto', 'Tambuwal', 'Wurno', 'Goronyo', 'Isa'],
    'Anambra': ['Awka', 'Onitsha', 'Nnewi', 'Aguata', 'Orumba'],
    'Bauchi': ['Bauchi', 'Azare', 'Misau', 'Jamaare', 'Darazo'],
    'Enugu': ['Enugu', 'Nsukka', 'Nsukka', 'Agbani', 'Awgu'],
    'Osun': ['Osogbo', 'Ile-Ife', 'Iwo', 'Ede', 'Ikire'],
    'Nasarawa': ['Lafia', 'Keffi', 'Akwanga', 'Nasarawa', 'Toto'],
    'Kebbi': ['Birnin Kebbi', 'Argungu', 'Yauri', 'Zuru', 'Jega'],
    'Bayelsa': ['Yenagoa', 'Brass', 'Ogbia', 'Nembe', 'Sagbama'],
    'Imo': ['Owerri', 'Orlu', 'Okigwe', 'Mbaise', 'Nkwerre'],
}


class Command(BaseCommand):
    def handle(self, *args, **options):
        for state, towns in nigerian_states.items():
            st, _ = State.objects.get_or_create(name=state)
            for town in towns:
                Town.objects.get_or_create(state=st, name=town)
        print("--------done---------")

Run python manage.py load_town_state to run the management command.

Confirm everything is fine to this point by visiting http://127.0.0.1:8000/create-person to create Person objects.

Now, what we want to do is when creating a Person, whenever a state is selected, we want to display only the towns in that state in the select field. For that, we are going to be using jQuery.

In the create_person.html add the following code before the closing body tag.

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js" integrity="sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script type="text/javascript">
$(document).ready(function() {
    console.log('sanity test...')
})
</script>

Refresh the page, and open your console, you should see "sanity test..." printed out on the console. That means jQuery was successfully loaded.

update your JavaScript code with the following.

<script type="text/javascript">
$(document).ready(function() {  // after the DOM is fully loaded
    $("#id_state").change(function() {  // we are using a change event on the state field
        $("#id_town").html("") // set the options empty for the town field
        const state_id = $(this).val();     // get the id of the selected state
        $.ajax({
            url: 'load-towns',    // url path to load the towns
            type: 'POST',    // type of request
            datatype: 'json',
            data: {'csrfmiddlewaretoken': getCookie('csrftoken'), 'sid': state_id},    // data
            success: function (resp) {
                const data = resp.data;
                options = ''
                for (let i=0; i < data.length; i++) {
                    let pk = data[i].pk
                    let name = data[i].name
                    options += "<option value=" + pk + ">" + name + "</option>"
                };
                $("#id_town").html(options);
            },
            error: function (xhr) {
                console.log(xhr);
            }
        })

    })
})
/* function to get csrfmiddlewaretoken, since we are using a POST method, django requires it */
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;
}
</script>

copy and paste the following in your core/views.py

# core/views.py

def load_towns(request):
    state_id = request.GET.get('sid')
    state = State.objects.get(id=state_id)
    towns = list(state.town_set.values('pk', 'name'))
    return JsonResponse({'data': towns})

Open the core/urls.py and add the following URL path to load the towns.

# core/urls.py
...
path('load-towns', views.load_towns),

visit http://127.0.0.1:8000/create-person and refresh the page. Now, If everything is right, on clicking on a State, The Towns associated with that State are going to be loaded into the Town field.

Creating Person

Play around with it, and confirm everything work as intended.

And there you have it! We've successfully crafted a dependent form that's not only functional but also takes the complexity down a notch. By leveraging Django's capabilities and some jQuery, we've made the user experience seamless. Whether you're a seasoned developer or just starting, this method offers a clear and concise way to handle dependent drop-down in your Django projects.

Curious to know: In what exciting project would you be incorporating this feature? Share your thoughts in the comments below! Let's discuss and brainstorm ideas together.

Also, don't forget to connect with me on LinkedIn. I'd love to stay in the loop with your innovative projects and coding adventures. Happy coding! ๐Ÿš€

ย