English 中文(简体)
ChoiceField doesn t display an empty label when using a tuple
原标题:

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 :)

最佳回答

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.

问题回答

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)

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, (  , ---------  ) )

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)

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 ),
)

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, (  , ---------  ) )

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

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>

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.





相关问题
Can Django models use MySQL functions?

Is there a way to force Django models to pass a field to a MySQL function every time the model data is read or loaded? To clarify what I mean in SQL, I want the Django model to produce something like ...

An enterprise scheduler for python (like quartz)

I am looking for an enterprise tasks scheduler for python, like quartz is for Java. Requirements: Persistent: if the process restarts or the machine restarts, then all the jobs must stay there and ...

How to remove unique, then duplicate dictionaries in a list?

Given the following list that contains some duplicate and some unique dictionaries, what is the best method to remove unique dictionaries first, then reduce the duplicate dictionaries to single ...

What is suggested seed value to use with random.seed()?

Simple enough question: I m using python random module to generate random integers. I want to know what is the suggested value to use with the random.seed() function? Currently I am letting this ...

How can I make the PyDev editor selectively ignore errors?

I m using PyDev under Eclipse to write some Jython code. I ve got numerous instances where I need to do something like this: import com.work.project.component.client.Interface.ISubInterface as ...

How do I profile `paster serve` s startup time?

Python s paster serve app.ini is taking longer than I would like to be ready for the first request. I know how to profile requests with middleware, but how do I profile the initialization time? I ...

Pragmatically adding give-aways/freebies to an online store

Our business currently has an online store and recently we ve been offering free specials to our customers. Right now, we simply display the special and give the buyer a notice stating we will add the ...

Converting Dictionary to List? [duplicate]

I m trying to convert a Python dictionary into a Python list, in order to perform some calculations. #My dictionary dict = {} dict[ Capital ]="London" dict[ Food ]="Fish&Chips" dict[ 2012 ]="...

热门标签