Using a Select filter to narrow choices for a form.

I have three models: Schools, Courses, and Students.

Every Course is a subject that belongs to the Schools model.

Every Student takes a course from the Course model. In this way they relate to each other. What I’m trying to do is add a new student, that selects a course from a drop-down list. However, the Course model is too large for this to be practical. I therefore wish to filter the courses by having them select a school, and that drastically lowers the list of available courses to a reasonable number. I’m not sure how to finish this.

models:

class Courses(models.Model):

        id = models.CharField(primary_key=True, max_length=12)
        created_at = models.DateTimeField(auto_now_add=True)
        school = models.ForeignKey(School,  on_delete=models.PROTECT, related_name='school')
        name = models.CharField(max_length=150, null=True, blank=True)
        cost = models.CharField(max_length=75, null=True, blank=True)

class Meta:
                ordering = ['id']
                indexes = [
                models.Index(fields=['school']),
                models.Index(fields=['name', 'id'])
                ]

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

class School(models.Model):

        id = models.CharField(primary_key=True, max_length=10)
        school_name = models.CharField(max_length=80)

        def __str__(self):
                return(f"{self.id} {self.school_name}")
                return self.id

        class Meta:
                ordering = ['id']
                indexes = [
                models.Index(fields=['id', 'school_name']),
        ]
class Student(models.Model):

        course = models.ForeignKey(Courses,  on_delete=models.PROTECT)
        created_at = models.DateTimeField(auto_now_add=True)
        student_id = models.CharField(max_length=20, null=True, blank=True)

class Meta:
                ordering = ['course']
                indexes = [
                models.Index(fields=['course']),
                
                ]

        def __str__(self):
                return(f"{self.course} {self.student_id}")

views:

def add_student1(request):


        schools = Courses.objects.all()

        form = SelectSchoolForm(request.POST)
        context = {'form':form, 'schools':schools}
        if request.method == "POST":

                if form.is_valid():
                        data = form.cleaned_data
                        school = data['school']
                        chosen_school = Courses.objects.filter(school=school).values()
                        context2 = {'form':form, 'schools':schools, 'chosen_school':chosen_school}
                        messages.success(request, 'OK')
                        return render(request, 'add_student2.html', {'school':school})

                return render(request, 'add_student1.html', context)
def add_student2(request, school):

        chosen_school = Courses.objects.filter(chosen_school=school).values()
                school1 = chosen_school

                form =AddStudentForm(request.POST)

                context = {'form':form, 'school':school, 'school1':school1}
                template = loader.get_template('add_student2.html')
                return HttpResponse(template.render(context, request))

forms:

class SelectSchoolForm(forms.ModelForm):

        class Meta:
                model = Courses
                fields = ('school', )
                labels = {'school':'School', }
                widgets = {'school': forms.Select(attrs={"placeholder":"School", "class":"form-select"}),

                 }

class  AddStudentForm(forms.ModelForm):


class Meta:
                model = Student
                fields = ('course', 'student_id' )
                labels = {'course':'','student_id':'',}
                widgets = {'course': forms.widgets.Select(attrs={"placeholder":"Course", "class":"form-control"}),
                'student_id':forms.widgets.TextInput(attrs={"placeholder":"Student ID", "class":"form-control"}),
                }

html:

add_student1.html

<h1>Add Student</h1>
<br/>

<form class="row g-3" action="{% url 'add_student1' %}" method="POST" enctype="multipart/form-data">
        {% csrf_token %}
<div class="col-md-6">


        {{ form.as_p }}

</div>

add_student2.html

<h1>Add Student2</h1>
<br/>
{% if school %}

        {{ school }}
{% endif %}

When I make a selection, the add_student2.html template is rendered, and it displays the selection. How do I use this to add the student with the filtered data? Alternatively, am I making this more complicated than it needs to be? I should add that a filter on the data is necessary due to the large number of course in the Course model.

This fits into a category generally referred to as either “cascading selects”, “chained selects”, “dependent dropdowns” or some such mix to that effect.

See the post (and the related links) at Django 5: Within the form, filter data with another field of the same form - #4 by KenWhitesell. You might also want to look at Django: how to choose from a list based on the previous selection? for an actual implementation. (You might also want to check out djangopackages.org for other options.)

.
you need 3 steps. 1. choose school, 2. valid school choose, 3. choose course.

.
use the form like example:

# school choose
# forms.py
from django.forms import modelform_factory, forms
class s:
 school = forms.ModelChoiceField(queryset=School.objects.all())

# views.py
from .forms import s
form = s()
# valid school choose
a = s(data=request.POST)
if a.is_valid():
  redirect({course choose url})
# course choose
from django.forms import modelform_factory, forms
def asd(request, school):
  b = Courses.objects.filter(school=school)
  form = modelform_factory(Courses, widgets={'Course': forms.ModelChoiceField(queryset=b)

I’m sorry, I’m not quite getting this. What do you mean by use the form like example? Is the top part here a form, and you define the class s: in the form? If I try it like you typed it django tells me ‘s’ is not defined. Could you be a little more explicit.

Thanks for the reply. I tried the first suggestion - but then discovered it doesn’t work in Django 5. As for the second suggestion, Django: how to choose from a list based on the previous selection?, does that actually filter prior to hitting the database? I’m not clear on whether that’s the case.

I’m not sure I understand what you’re referring to as the “first suggestion”, nor do I think that modifying an existing solution for Django 5 would be all that difficult. Posting specifics here would be helpful regarding what you’ve tried and what is not working.

I’m not sure I’m following what you’re asking here. (Again, I’m not sure what you’re referring to as the “second suggestion.”)

My reply doesn’t present two separate options - they’re both descriptions of the same general technique, where you handle the selection event of one drop-down, and use that information to populate the next drop-down. (The second link is one implementation to give you an idea of how it’s done.)

Whichever direction you go, none of these are going to be direct “drop-in” solutions. There are always going to be customizations that you’ll need to make to adjust for your specific situation.

What I told you is just an example.
Look at it and apply it well according to your situation.

I’ve been really trying to get this, but it’s not quite getting there. Here’s what I have now.

models:

from django.db import models

# Create your models here.
class School(models.Model):

        id = models.CharField(primary_key=True, max_length=10)
        school_name = models.CharField(max_length=80)

        def __str__(self):
                return(f"{self.id} {self.school_name}")
                return self.id

        class Meta:
                ordering = ['id']
                indexes = [
                models.Index(fields=['id', 'school_name']),
                ]




class Courses(models.Model):

		id = models.CharField(primary_key=True, max_length=12)
		created_at = models.DateTimeField(auto_now_add=True)
		school = models.ForeignKey(School,  on_delete=models.PROTECT, related_name='school')
		name = models.CharField(max_length=150, null=True, blank=True)
		cost = models.CharField(max_length=75, null=True, blank=True)

		class Meta:
			ordering = ['id']
			indexes = [	models.Index(fields=['school']), models.Index(fields=['name', 'id'])]

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



class Student(models.Model):

        course = models.ForeignKey(Courses,  on_delete=models.PROTECT)
        created_at = models.DateTimeField(auto_now_add=True)
        student_id = models.CharField(max_length=20, null=True, blank=True)

class Meta:
        ordering = ['course']
        indexes = [
        models.Index(fields=['course']),
             
        ]

views:


def choose_school(request):

	aschool = Courses.objects.all()
	form=SelectSchoolForm(request.POST)
	if request.method == "POST":
		if form.is_valid():
			data = form.cleaned_data
			choice = data['school']
			selected_school = Courses.objects.filter(school=choice).values()
			context = {'choice': choice}
			return render(request, 'add_student2.html', context)
	context = {'form': form, 'aschool':aschool}  
	return render(request, 'choose_school.html', context)
def add_student2(request, choice):

		chosen_school = Courses.objects.filter(school=choice).values()
		form = AddStudentForm(request.POST)
		context = {'form':form, 'choice':choice, 'chosen_school':chosen_school}
		return render(request, 'add_student2.html', context)
def add_student3(request, pk):
                choice = School.objects.get(id=pk)
		courses = Courses.objects.filter(school=choice)
                form = AddStudentForm(request.POST)
                context = {'courses':courses, 'choice':choice, 'form':form}
                return render(request, 'add_student3.html', context)

forms:

class SelectSchoolForm(forms.ModelForm):

        class Meta:
                model = Courses
                fields = ('school', )
                labels = {'school':'School', }
                widgets = {'school': forms.Select(attrs={"placeholder":"School", "class":"form-select"}),

                 }
class  AddStudentForm(forms.ModelForm):


	class Meta:
                model = Student
                fields = ('course', 'student_id' )
                labels = {'course':'','student_id':'',}
                widgets = {'course': forms.widgets.Select(attrs={"placeholder":"Course", "class":"form-control"}),
                'student_id':forms.widgets.TextInput(attrs={"placeholder":"Student ID", "class":"form-control"}),
                }

and html:
choose_school.html:

<h1 class="display-4">Add Student</h1>

<br/>
{% if aschool %}

        {{ aschool }}
{% endif %}

<form class="row g-3" action="{% url 'choose_school' %}" method="POST" enctype="multipart/form-data">
	{% csrf_token %}
<div class="col-md-6">
	
	
	{{ form.as_p }}
	
</div>

add_student2.html:

<h1 class="display-4">Add Student2</h1>

<br/>

Selected School:   
{% if choice %}
	{{choice.id}}
{% endif %}
<form class="row g-3" action="{% url 'choose_school' %}" method="POST" enctype="multipart/form-data">
	{% csrf_token %}
<div class="col-md-6">
	
	
	{{ form.as_p }}
	
</div>
<div class="col-md-6">

<a class="btn btn-secondary btn-lg" href="{% url 'add_student3' choice.id  %}" role="button">Add Student</a>

<a href="{% url 'school' %}" class="btn btn-success">Back</a>
</div>

</form>

and add_student3.html:

<h1 class="display-4">Add Student3</h1>

<br/>

Selected School:   
{% if choice %}
	{{choice.id}}
{% endif %}

<br/>
List Courses:
<br/>
{% for course in courses %}

	{{ course }}

{% endfor %}

<form class="row g-3" action="{% url 'choose_school' %}" method="POST" enctype="multipart/form-data">
	{% csrf_token %}
<div class="col-md-6">
	
	
	{{ form.as_p }}
	
</div>

<div class="col-md-6">

<a class="btn btn-secondary btn-lg" href="{% url 'add_student3' choice.id  %}" role="button">Add Student</a>

<a href="{% url 'school' %}" class="btn btn-success">Back</a>
</div>

</form>

When I choose a school now, I get a list of the available courses, and only the available courses. However, the select function displays all of the courses still. I don’t understand why they have not been filtered from the select.

Any help on this would be appreciated.

Oh, I also tried this:

form = forms.ModelChoiceField(queryset=Courses.objects.filter(school=choice))

in my view, but when I do, I get the error:

module ‘django.forms.forms’ has no attribute ‘ModelChoiceField’

so I don’t know how to implement this either.

Thanks again.

Follow-up.

I realized how to use the values I have in a select. Here’s the modified html:

<form class="row g-3" action="{% url 'choose_school' %}" method="POST" enctype="multipart/form-data">
	{% csrf_token %}
<div class="col-md-6">


<select name="select1" class="form-select" aria-label="Select Course">
	<option selected>Open this select menu</option>
	
   

		{% for course in courses %}
  			
   			<option value={{course }} >{{ course }} </option>
   		{% endfor %}


	
	{{ form.as_p }}

</div>

This does indeed provide me with a select box with only the values I’m looking for. I also know that the values should be accessible to my view as

(request.POST['select1'])

Now I don’t know how to use that to save a new record. I feel that I’m close - I just don’t know how to get there. Thanks.

If you’re not familiar with working with model forms, then you’ll want to review the docs at Creating forms from models. Django does a lot of this work for you.

with send AddStudentForm(initial=request.POST)

class  AddStudentForm(forms.ModelForm):


	class Meta:
                model = Student
                fields = ('course', 'student_id' , **'school'**)
                labels = {'course':'','student_id':'',}
                widgets = {'course': forms.widgets.Select(attrs={"placeholder":"Course", "class":"form-control",
**'school': forms.widgets.HiddenInput**
}),
                'student_id':forms.widgets.TextInput(attrs={"placeholder":"Student ID", "class":"form-control"}),
                }

I tried doing this:

class AddStudentForm2FormSet(BaseModelFormSet):


			def __init__(self, *args, **kwargs):

				super().__init__(self, *args, **kwargs)
				self.fields['course'].queryset = Student.objects.filter(all)

with this view:

def add_student5(request, pk):

                choice = School.objects.get(id=pk)
		courses = Courses.objects.filter(school=choice)
                form = modelformset_factory(Student, fields=['course','student_id'], formset=AddStudentForm2FormSet)
                context = {'courses':courses, 'choice':choice, 'form':form}
                return render(request, 'add_student5.html', context)

based on another answer you made, but I got this

 "cannot unpack non-iterable builtin_function_or_method object"

referring to my template’s {{ form.as_p }} tag.

I’m sorry, I’m still in the weeds.

I’m still not clear on what you’re trying to achieve here from a UI/UX perspective. (I think I’m clear from a functional description, but I’m not clear on what pages and actions you’re trying to implement.)

You wrote:

So, this reads to me that you have a page with a form for adding a new Student. On this page there is a drop-down list for School. When they select a School, you want to get the list of courses, and use that to populate the courses list.

Now, my question is, do you want this list updated on the current page, or are you transferring to a new page with this reduced list? (Or are you just trying to reload the current page with the existing selections and the updated drop-down?)
If you’re trying to update the current page, what are you using in the browser to manage this?

I now also see where you’re trying to add a formset into this? I don’t understand where this fits into the picture.

So for me to be able to try and help you with the specific here, I need an overall road map for all these views and templates. What are the views and templates that I need to pay attention to here?

Actually, what I’d probably suggest is that we go back to the very begining where you had this form working, with the only exception that the Courses list is too big, and we address only that issue - but starting with the clarification of how you want to address it.

Thanks for your patience. As previously stated, I wish to add a student record with a form, and the courses available to the student are dependent on the school. That is, all of the available courses belong to a particular school. Since there are so many courses available, the page takes way too long to render.

My idea, then, is to narrow the options significantly by first having the user select the school to which the course belongs. It is important, then, that the records for the courses not be retrieved until it is first filtered by the chosen school. What I wish to do is when the user selects the add student button, a modal pops up and has the user select a school. Then they’ll be redirected to a new page with a form where the available courses have been filtered by the previous school selection. I have managed to get this far, but my implementation is rather inelegant. I could probably finish it off, but I wish to do it better. My method is rather difficult to follow.

The existing code for adding a student without any filtering is as follows (and please note, the models are unchanged from previously listed so I have not listed them here):

views.py:

def add_student6(request):

		students = Student.objects.all()
		form =AddStudentForm2(request.POST)
		context = {'form': form, 'students':students}
		if request.method == "POST":
			if form.is_valid():
				form.save()
				messages.success(request, "Record updated successfully!")
				return redirect('school')
		return render(request, 'add_student6.html',  context)

forms.py:

class  AddStudentForm2(forms.ModelForm):

class Meta:
				model = Student
				fields = ( 'student_id', 'course')
				labels = {'student_id':'', 'course':''}
				widgets = {'student_id':forms.widgets.TextInput(attrs={"placeholder":"Student ID", "class":"form-control"}),
               			'course':forms.widgets.Select(attrs={"placeholder":"Course", "class":"form-control"}),
              
               
                }

and the add_student6.html:

<form class="row g-3" action="" method="POST" enctype="multipart/form-data">
	{% csrf_token %}
<div class="col-md-6">


Start Form as p
	
	{{ form.as_p }}

End Form as p

</div>

<button type="submit" class="btn btn-secondary">Add Record</button>
</form>

I’ve used it. It works. I can add student records with this.

Thanks again.

I’m still not clear on how the code you’ve posted fits with the description. Your AddStudentForm2 has a course field, but doesn’t show a school field. Where is the school selected here?

Also, you mention going to a new page for selecting the course, but you have this course field in this current page.

Ignoring these details for now, when you’re looking to create a new page based on information posted in the current page, you need to pass the information along to the new page to allow it to be created properly.

This is typically done in the redirect by passing that data through the url to the new view.

So, assuming for the moment that you have a field named “school” in the form, but it’s not a field in the model, your if form.is_valid(): block needs to look something like this:

if form.is_valid():
    student = form.save()
    school = form.cleaned_data['school']
    messages.success(request, "Record updated successfully!")
    return redirect('courses', student=student.id, school=school)

This implies that your url definition for the next view would be:

path('student/<int:student_id>/<int:school_id>/', views.course_selection, name='courses')

That makes the view start to look like this:

def course_selection(request, student_id, school_id):
    student = Student.objects.get(id=student_id)
    form = SelectCourseForm(school_id)
    ...

And in your form, you’ll do something like

def __init__(self, school, *args, **kwargs):
    super().__init__(self, *args, **kwargs)
    self.fields['courses'].queryset = Courses.objects.filter(school=school)

(Again, this is a skeleton and not a complete solution, but hopefully it gives you enough information to proceed.)

That helps. I think it’s close. Based on your feedback, here’s what I have now:

views:

def choose_school7(request):
	
	
		aschool = Courses.objects.all()
		form=SelectSchoolForm(request.POST)
		if request.method == "POST":
			if form.is_valid():
				school = form.cleaned_data['school']
				messages.success(request, "School chosen successfully!")
				return redirect('courses', school=school)
		context = {'form': form, 'aschool':aschool}  
		return render(request, 'choose_school7.html', context)	
	
def course_selection(request, school_id):

		form = SelectCourseForm(school_id)
		if request.method == "POST":
			if form.is_valid():
				form.save()
				messages.success(request, "Record added successfully!")
				return redirect('school')
		context = {'form':form}
		return render(request, 'course_selection.html', context)

forms:

class SelectSchoolForm(forms.ModelForm):

        class Meta:
                model = Courses
                fields = ('school', )
                labels = {'school':'School', }
                widgets = {'school': forms.Select(attrs={"placeholder":"School", "class":"form-select"}),

                 }
class SelectCourseForm(forms.ModelForm):

			def __init__(self, school, *args, **kwargs):
				super().__init__(self, *args, **kwargs)
				self.fields['course'].queryset = Courses.objects.filter(school=school)
                            
			class Meta:
				model = Student
				fields = ( 'student_id', 'course')
				labels = {'student_id':'', 'course':''}
				widgets = {'student_id':forms.widgets.TextInput(attrs={"placeholder":"Student ID", "class":"form-control"}),
               			'course':forms.widgets.Select(attrs={"placeholder":"Course", "class":"form-select"}),
              
               
                }

and the url:

path('student/<str:school_id>', views.course_selection, name='courses'),

The issue now is, I get “Reverse for ‘courses’ with keyword arguments ‘{‘school’: <School: UoW Waterloo>}’ not found”

I’m not sure what’s wrong here. Thanks again.

You want to pass the id field of the school object, not the complete object as the parameter to the redirect.

And how would one do that? school is the foreign key to the School.id field. I can’t find documentation that explains how this is done. I’m really trying hard to get it, but I just don’t.

In your views you can pass the the school id instead of school:

def choose_school7(request):
    #....
     return redirect('courses', school_id=school.id)

In the course_selection view:

def course_selection(request, school_id):
	form = SelectCourseForm(school_id, request.POST or None) # passing the school ID to the form
	if form.is_valid():
		form.save()
		messages.success(request, "Record added successfully!")
		return redirect('school')
		
	context = {'form': form}
	return render(request, 'course_selection.html', context)

In the SelectCourseForm now we are filtering courses by school ID.

class SelectCourseForm(forms.ModelForm):
	def init(self, school, *args, **kwargs):
	   super().init(*args, **kwargs)
	   self.fields['course'].queryset = Courses.objects.filter(school_id=school)

And finally in the urls.py we must use integers instead of strings:

urlpatterns = [
path('student/int:school_id', views.course_selection, name='courses'), # Use int instead of str
]

Give it a try and tell us if it works!

I did this and got: " NameError at /student/UoW

name ‘SelectCourseForm’ is not defined"

I kept to the str instead of int, since my id is a CharField and not an int. I presume this is why. Must it be an int? If so, I’m not sure how I’m going to fix it. I very much appreciate the help.