ChoiceField doesn't display an empty label when using a tuple

Question:

What I’m trying to do

I’m going to be keeping data about competitions in my database. I want to be able to search the competitions by certain criteria – competition type in particular.

About competition types

Competition types are kept in a tuple. A slightly shortened example:

COMPETITION_TYPE_CHOICES = (
    (1, 'Olympic Games'),
    (2, 'ISU Championships'),
    (3, 'Grand Prix Series'),
)

These are used in the model like so (again – this is a shortened/simplified version of the model):

class Competition(models.Model):
    name = models.CharField(max_length=256)
    type = models.IntegerField(choices=COMPETITION_TYPE_CHOICES) 

The search form

I don’t want the fields to be required in the search form, so the form is defined like this:

class CompetitionSearchForm(forms.Form):
    name = forms.CharField(required=False)
    type = forms.ChoiceField(choices=COMPETITION_TYPE_CHOICES,required=False)

The problem

I’d like the select widget in ChoiceField to display an empty label, but I don’t get one. Any help with this would be much appreciated 🙂

Asked By: Monika Sulik

||

Answers:

Try adding blank=True to the model fields (assuming that’s the behavior you want), then changing the form to a ModelForm and removing the field definitions. Note that any fields for which you set blank=True won’t be required when validating or saving the model. Again, this may not be what you want but if it is it’ll allow Django to take care of a few things automatically.

Otherwise just change your COMPETITION_TYPE_CHOICES to:

COMPETITION_TYPE_CHOICES = (
    ('', '---------'),
    ('1', 'Olympic Games'),
    ('2', 'ISU Championships'),
    ('3', 'Grand Prix Series'),
)
Answered By: John Debs

I’ve found a solution that works the way I want it to without violating the DRY principle. Not very clean, but it’ll have to do I suppose.

According to the documentation choices don’t have to be a tuple:

Finally, note that choices can be any
iterable object — not necessarily a
list or tuple. This lets you construct
choices dynamically. But if you find
yourself hacking choices to be
dynamic, you’re probably better off
using a proper database table with a
ForeignKey. choices is meant for
static data that doesn’t change much,
if ever.

So the solution I’m going with for the moment is:

COMPETITION_TYPE_CHOICES = [
     (1, 'Olympic Games'),
     (2, 'ISU Championships'),
     (3, 'Grand Prix Series'),
]

COMP_TYPE_CHOICES_AND_EMPTY = [('','All')] + COMPETITION_TYPE_CHOICES

And then:

class CompetitionSearchForm(forms.Form):
    name = forms.CharField(required=False)
    type = forms.ChoiceField(choices=COMP_TYPE_CHOICES_AND_EMPTY, required=False)

The model stays the same as it was.

Answered By: Monika Sulik

Better choice is to update field choices in form init method

COMPETITION_TYPE_CHOICES = (
    (1, 'Olympic Games'),
    (2, 'ISU Championships'),
    (3, 'Grand Prix Series'),
)


class CompetitionSearchForm(forms.Form):
    name = forms.CharField(required=False)
    type = forms.ChoiceField(choices=COMPETITION_TYPE_CHOICES,required=False)

    def __init__(self, *args, **kwargs):
        super(CompetitionSearchForm, self).__init__(*args, **kwargs)
        self.fields['type'].choices.insert(0, ('','---------' ) )
Answered By: Evgeniy

Just a small change to Evgeniy’s answer that checks if the blank alternative is not already added.

Without the check (at least when running the builtin runserver) one extra empty label is added for each page reload.

COMPETITION_TYPE_CHOICES = (
    (1, 'Olympic Games'),
    (2, 'ISU Championships'),
    (3, 'Grand Prix Series'),
)

class CompetitionSearchForm(forms.Form):
    name = forms.CharField(required=False)
    type = forms.ChoiceField(choices=COMPETITION_TYPE_CHOICES,required=False)

    def __init__(self, *args, **kwargs):
        super(CompetitionSearchForm, self).__init__(*args, **kwargs)
        if not self.fields['type'].choices[0][0] == '':
            self.fields['type'].choices.insert(0, ('','---------' ) )
Answered By: sigurdga

Why don’t you use ModelForm if you are already have model class?

Best solution:

forms.py

class CompetitionSearchForm(ModelForm):

    class Meta:
        model = Competition

models.py

class Competition(models.Model):
    name = models.CharField(max_length=256)
    type = models.IntegerField(choices=COMPETITION_TYPE_CHOICES, default=COMPETITION_TYPE_CHOICES[0][0], blank=True)

You can set blank=False to remove empty_label from list

Answered By: 2xS

I tried both Monika’s and Evgeniy’s solutions with no success, but Monika has a good point in that the choices do not need to be tuples. Therefore, the easiest (and DRYest) solution is to simply do what Django does already in the Model Field. Simply add the blank choice and the tuples together after converting them to a list:

from django.db.models.fields import BLANK_CHOICE_DASH

...

type = forms.ChoiceField(choices=BLANK_CHOICE_DASH + list(COMPETITION_TYPE_CHOICES), required=False)
Answered By: Scott

According to the documentation:

Either an iterable (e.g., a list or tuple) of 2-tuples to use as choices for this field, or a callable that returns such an iterable. (https://docs.djangoproject.com/en/dev/ref/forms/fields/)

So, you can simple:

sample_field = forms.ChoiceField(choices=(('', '---'),) + Model.YOUR_CHOICES)
Answered By: Edson Dota

A little late to the party..

How about not modifying the choices at all and just handling it with a widget?

from django.db.models import BLANK_CHOICE_DASH

class EmptySelect(Select):
    empty_value = BLANK_CHOICE_DASH[0]
    empty_label = BLANK_CHOICE_DASH[1]

    @property
    def choices(self):
        yield (self.empty_value, self.empty_label,)
        for choice in self._choices:
            yield choice

    @choices.setter
    def choices(self, val):
        self._choices = val

Then just call it:

class CompetitionSearchForm(forms.Form):
    name = forms.CharField(required=False)
    type = forms.ChoiceField(choices=COMPETITION_TYPE_CHOICES,required=False, widget=EmptySelect)

This is what you end up with:

print(CompetitionSearchForm().as_p())
<p>
    <label for="id_name">Name:</label>
    <input id="id_name" name="name" type="text" />
</p>
<p>
    <label for="id_type">Type:</label>
    <select id="id_type" name="type">
        <option value="" selected="selected">------</option>
        <option value="1">Olympic Games</option>
        <option value="2">ISU Championships</option>
        <option value="3">Grand Prix Series</option>
    </select>
</p>
Answered By: Javier Buzzi

Extending Javier’s answer.

Instead of customizing the signature of choices which would fail in mypy checking, its better to use a custom property and change it only in the display options.

class EmptySelect(Select):
    @property
    def custom_choices(self):
        yield BLANK_CHOICE_DASH[0]
        yield from self.choices

    def optgroups(self, name, value, attrs=None):
        """Return a list of optgroups for this widget."""
        groups = []
        has_selected = False
        # START_CHANGES
        for index, (option_value, option_label) in enumerate(self.custom_choices):
            # END_CHANGES
            if option_value is None:
                option_value = ""

            subgroup = []
            if isinstance(option_label, (list, tuple)):
                group_name = option_value
                subindex = 0
                choices = option_label
            else:
                group_name = None
                subindex = None
                choices = [(option_value, option_label)]
            groups.append((group_name, subgroup, index))

            for subvalue, sublabel in choices:
                selected = str(subvalue) in value and (not has_selected or self.allow_multiple_selected)
                has_selected |= selected
                subgroup.append(
                    self.create_option(
                        name,
                        subvalue,
                        sublabel,
                        selected,
                        index,
                        subindex=subindex,
                        attrs=attrs,
                    )
                )
                if subindex is not None:
                    subindex += 1
        return groups

And use this widget anywhere like:

class CompetitionSearchForm(forms.Form):
    name = forms.CharField(required=False)
    type = forms.ChoiceField(choices=COMPETITION_TYPE_CHOICES,required=False, widget=EmptySelect)

Note: Don’t use type as a filed name as it’s python built-in keyword instead name it something else for good practice.

Answered By: Rashid Mahmood
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.