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.
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! ๐