Optgroup/ option select dropdown

Hello all, I am working on a form which has a select dropdown that requires an optgroup and select options… Optgroups are Regions (Asia, Europe etc) and options are countries belonging to the countries…

I have developed a Form for this and applied choicefield and has written the logic inside the form itself… and then applied the HTML using FormHelper Layout so that I can just pass {% crispy form %} in the template…

For some reason I cannot figure out, the data in the select option does not appear… I’d appreciate if you could help me debug the problem… Please note, there is no error message, just that when i go to webpage, the data in the dropdown isnt there…

Models.py

class CountryRegion(models.Model):
    id = models.AutoField(primary_key=True, editable=False)
    country_name = models.CharField(max_length=50, unique=True, verbose_name='Segment')
    region_name = models.CharField(max_length=50, verbose_name='Industry')
    

    class Meta:
        verbose_name_plural = 'RegionCountry'

    def __str__(self):
        return self.country_name + '-' + self.region_name

Forms.py

from django import forms
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit, Layout, Row, Column, HTML

from crispy_forms.bootstrap import FormActions
from django.contrib.auth.models import User
from django.forms import ModelForm


from .models import Portfolio, Project, SegmentIndustry, CountryRegion

class ProjectForm(forms.ModelForm):
    fk_project_location = forms.ChoiceField(choices=[])

    def __init__(self, *args, **kwargs):
        super(ProjectForm, self).__init__(*args, **kwargs)
        region_list = CountryRegion.objects.values_list(
            "region_name", flat=True
        ).distinct()
        choices = []
        for region in region_list:
            countries_in_region = CountryRegion.objects.filter(region_name=region)
            country_choices = [
                (country.id, country.country_name) for country in countries_in_region
            ]
            choices.append((region, country_choices))
        self.fields["fk_project_location"].choices = choices

    class Meta:
        model = Project
        fields = [         
            "fk_project_segment",
            "fk_project_location",
        ]

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.helper = FormHelper()
        self.helper.form_method = "post"
        self.helper.layout = Layout(
            Row(
                Column("fk_project_segment", css_class="form-group col-md-6 mb-0"),
                Column(
                    HTML(
                        """
                            <label for="id_fk_project_location" class="pb-2">Location</label>
                                <select name="fk_project_location" id="id_fk_project_location" class="form-select">
                                    {% for region, countries in form.fk_project_location.choices %}
                                        <optgroup label="{{ region }}">
                                            {% for id, country in countries %}
                                                <option value="{{ id }}">{{ country }}</option>
                                            {% endfor %}
                                        </optgroup>
                                    {% endfor %}
                                </select>
                        """
                    ),
                    css_class="form-group col-md-6 mb-0",
                ),
                css_class="form-row p-0",
            ),

        )

Template

<form method="post" class="form-group">
                            {% csrf_token %}
                            
                            {% crispy project_form %}
                            <button type="submit" class="bland-button-style"><i
                                class="fa-solid fa-cloud-arrow-down fa-lg header-icon-style portfolio-save-icon"
                                class="tooltip fade" data-title="Submit/ Update"></i></button>
                        </form>

You’ve got two __init__ functions in your form class. Only the second one being defined is going to get called.

ohh ok… so how do I move the first one to the second? can you provide some advise please… one option I could think is to move the 1st init to views and perhaps pass “choices” in the context? in that case, since the HTML is defined inside FormHelper Layout, will it recognize (i guess yes since template contains “crispy form”…

it may still be best to have all content in forms.py as I did now… Can you please advise?

You can just create one function from the code currently contained in both.

Or, if you really want to keep those functions separate, rename one and call it from the other.

For example, you effectively have:

def a(self):
    lines_of_code_1

def a(self):
    lines_of_code_2

You could make this either:

def a(self):
    lines_of_code_1
    lines_of_code_2

or

def a(self):
    lines_of_code_1
    self.b()

def b(self):
    lines_of_code_2

i have rearranged the Form as follows… not sure if the ordering matters and the below is correct… i still dont have the content in the dropdown…

class ProjectForm(forms.ModelForm):
    class Meta:
        model = Project
        fields = [         
            "fk_project_segment",
            "fk_project_location",
        ]    

    fk_project_location = forms.ChoiceField(choices=[])

    def __init__(self, *args, **kwargs):
        super(ProjectForm, self).__init__(*args, **kwargs)
        region_list = CountryRegion.objects.values_list(
            "region_name", flat=True
        ).distinct()
        choices = []
        for region in region_list:
            countries_in_region = CountryRegion.objects.filter(region_name=region)
            country_choices = [
                (country.id, country.country_name) for country in countries_in_region
            ]
            choices.append((region, country_choices))
        self.fields["fk_project_location"].choices = choices

   
        self.helper = FormHelper()
        self.helper.form_method = "post"
        self.helper.layout = Layout(
            Row(
                Column("fk_project_segment", css_class="form-group col-md-6 mb-0"),
                Column(
                    HTML(
                        """
                            <label for="id_fk_project_location" class="pb-2">Location</label>
                                <select name="fk_project_location" id="id_fk_project_location" class="form-select">
                                    {% for region, countries in form.fk_project_location.choices %}
                                        <optgroup label="{{ region }}">
                                            {% for id, country in countries %}
                                                <option value="{{ id }}">{{ country }}</option>
                                            {% endfor %}
                                        </optgroup>
                                    {% endfor %}
                                </select>
                        """
                    ),
                    css_class="form-group col-md-6 mb-0",
                ),
                css_class="form-row p-0",
            ),

        )

So the issue here is that you’re adding the data to the form in the form.

You can still reference it here, but since you’re rendering the options manually, you don’t want to set the choices attribute of the field to your choices list.

You can set an instance variable (e.g. self.choices) in the form, and then reference it in your template as form.choices.

Or, if you’re using a CBV, you could create that choices structure in the get_context_data function and reference it directly in the template.

I have used an alternate logic (code) and have been able to get the optgroup dropdown… for now, guess this can be considered closed…