Customize/remove Django select box blank option
Asked Answered
D

15

84

I'm using Django 1.0.2. I've written a ModelForm backed by a Model. This model has a ForeignKey where blank=False. When Django generates HTML for this form it creates a select box with one option for each row in the table referenced by the ForeignKey. It also creates an option at the top of the list that has no value and displays as a series of dashes:

<option value="">---------</option>

What I'd like to know is:

  1. What is the cleanest way to remove this auto-generated option from the select box?
  2. What is the cleanest way to customize it so that it shows as:

    <option value="">Select Item</option>
    

In searching for a solution I came across Django ticket 4653 which gave me the impression that others had the same question and that the default behavior of Django may have been modified. This ticket is over a year old so I was hoping there might be a cleaner way to accomplish these things.

Thanks for any help,

Jeff

Edit: I've configured the ForeignKey field as such:

verb = models.ForeignKey(Verb, blank=False, default=get_default_verb)

This does set the default so that it's no longer the empty/dashes option but unfortunately it doesn't seem to resolve either of my questions. That is, the empty/dashes option still appears in the list.

Dedrick answered 11/4, 2009 at 0:44 Comment(1)
Turns out it's considered a bug in Django; should remove the empty option automatically in this case: code.djangoproject.com/ticket/10792Plugboard
P
104

Haven't tested this, but based on reading Django's code here and here I believe it should work:

class ThingForm(forms.ModelForm):
  class Meta:
    model = Thing
  
  def __init__(self, *args, **kwargs):
    super(ThingForm, self).__init__(*args, **kwargs)
    self.fields['verb'].empty_label = None

EDIT: This is documented, though you wouldn't necessarily know to look for ModelChoiceField if you're working with an auto-generated ModelForm.

EDIT: As jlpp notes in his answer, this isn't complete - you have to re-assign the choices to the widgets after changing the empty_label attribute. Since that's a bit hacky, the other option that might be easier to understand is just overriding the entire ModelChoiceField:

class ThingForm(forms.ModelForm):
  verb = ModelChoiceField(Verb.objects.all(), empty_label=None)

  class Meta:
    model = Thing
Plugboard answered 11/4, 2009 at 12:40 Comment(7)
Thanks for the help Carl. Your example almost worked but I had to refresh the widget choice list as the last step. See my answer. And Wow; fast turnaround on the bug report!Dedrick
Sidebar: do you know how your last approach would fit in with using Meta: fields to choose which model fields to include on the form?Dedrick
I'm not sure what would happen if you define a form field manually but don't include it in the Meta list of model fields. I'm sure it would appear, but it would it be saved on the model? My guess is yes - try it and see.Plugboard
I think the 2nd way to do it using the ModelChoiceField definition is more clear. Easier to spot if looking at someone elses code :)Baseless
The first solution is working fine for me without having to re-assign the choices.Cliftonclim
@CarlMeyer what I would expect as a naive programmer is that I can control showing "-----" from a widget, but unfortunately that is not the case. Do you think this is something we should actually address in Django?Necessitate
@WimFeijen I think you're right. The default may be intelligent and depend on data from the field (i.e. are blank values allowed?) but it should be overridable by changing only the widget.Plugboard
C
39

from the docs

The blank choice will not be included if the model field has blank=False and an explicit default value (the default value will be initially selected instead).

so set the default and you're ok

Caldron answered 11/4, 2009 at 0:58 Comment(3)
See Edit in question. Thanks for your help.Dedrick
Doing this for a ForeignKey field can be dangerous. It might work of it is a CharField with choices. The problem is ForeignKey don't have well established defaults.Wardroom
Yeah this was exactly what I was looking for... for a non-ForeignKey choice fieldsAllisonallissa
D
24

With Carl's answer as a guide and after rooting around the Django source for a couple hours I think this is the complete solution:

  1. To remove the empty option (extending Carl's example):

    class ThingForm(models.ModelForm):
      class Meta:
        model = Thing
    
      def __init__(self, *args, **kwargs):
        super(ThingForm, self).__init__(*args, **kwargs)
        self.fields['verb'].empty_label = None
        # following line needed to refresh widget copy of choice list
        self.fields['verb'].widget.choices =
          self.fields['verb'].choices
    
  2. To customize the empty option label is essentially the same:

    class ThingForm(models.ModelForm):
      class Meta:
        model = Thing
    
      def __init__(self, *args, **kwargs):
        super(ThingForm, self).__init__(*args, **kwargs)
        self.fields['verb'].empty_label = "Select a Verb"
        # following line needed to refresh widget copy of choice list
        self.fields['verb'].widget.choices =
          self.fields['verb'].choices
    

I think this approach applies to all scenarios where ModelChoiceFields are rendered as HTML but I'm not positive. I found that when these fields are initialized, their choices are passed to the Select widget (see django.forms.fields.ChoiceField._set_choices). Setting the empty_label after initialization does not refresh the Select widget's list of choices. I'm not familiar enough with Django to know if this should be considered a bug.

Dedrick answered 12/4, 2009 at 5:13 Comment(2)
Probably not a bug; I guess there's a reason the empty_label argument is documented, but not re-setting of the attribute. Since this is kind of hacky the better option might be to just override the entire form field; see my edited answer.Plugboard
This is odd, but I just tested this on my application and I did not need to reset the fields to get the empty label to be removed...Cramoisy
C
22

You can use this on your model:

class MyModel(models.Model):
    name = CharField('fieldname', max_length=10, default=None)

default=None is the answer :D

NOTE: I tried this on Django 1.7

Coucal answered 11/4, 2009 at 0:44 Comment(3)
Excellent. Like stated at this link, a model field with blank=False and any default value set will not render a blank choices option.Ackack
A nice simple solution! All the others verge on the crazy in terms of complexity.Discordance
Fantastic. So much better than the other ~9 sort of working answers on the ~3 versions of this question.Legalese
D
8

As for the django 1.4 all you need is to set the "default" value and "blank=False" on the choices field

class MyModel(models.Model):
    CHOICES = (
        (0, 'A'), 
        (1, 'B'),
    )
    choice_field = models.IntegerField(choices=CHOICES, blank=False, default=0)
Diversiform answered 11/4, 2009 at 0:44 Comment(0)
R
6

you can do this in admin:

formfield_overrides = {
    models.ForeignKey: {'empty_label': None},
}
Referendum answered 11/4, 2009 at 0:44 Comment(1)
Where models.ForeignKey is django.db.models.ForeignKeyCongratulation
A
5

self.fields['xxx'].empty_value = None would not work If you field type is TypedChoiceField which do not have empty_label property.

What we should do is to remove first choice:

1 . If you want to build a BaseForm auto detect TypedChoiceField

class BaseForm(forms.ModelForm):

    def __init__(self, *args, **kwargs):
        super(BaseForm, self).__init__(*args, **kwargs)

        for field_name in self.fields:
            field = self.fields.get(field_name)
            if field and isinstance(field , forms.TypedChoiceField):
                field.choices = field.choices[1:]
            # code to process other Field
            # ....

class AddClientForm(BaseForm):
     pass

2.only a few form, you can use:

class AddClientForm(forms.ModelForm):

    def __init__(self, *args, **kwargs):
        super(AddClientForm, self).__init__(*args, **kwargs)
        self.fields['xxx'].choices = self.fields['xxx'].choices[1:]
Ammeter answered 11/4, 2009 at 0:44 Comment(2)
It is a real problem, thanks for explaining. But seems like "for" is not mandatory, we can simply do self.fields['xxx'].choices = self.fields['xxx'].choices[1:] if we want to remove empty choice. But if you want to replace title you need to "recreate" first choice as tuple with first empty element and title in second, e.g. self.fields['xxx'].choices = [('', 'Nothing')] + self.fields['xxx'].choices[1:]Nominalism
@user3479125 I am making a BaseForm to customize all form, so using for loop.Ammeter
G
5

See here for the complete debate on and methods for resolution of this issue.

Glossographer answered 11/4, 2009 at 0:44 Comment(0)
J
4

For a ForeignKey field, setting the default value to '' on the model will remove the blank option.

verb = models.ForeignKey(Verb, on_delete=models.CASCADE, default='')

For other fields like CharField you could set the default to None, but this does not work for ForeignKey fields in Django 1.11.

Jiggerypokery answered 11/4, 2009 at 0:44 Comment(0)
C
3

Since Django 1.7, you can customize the label for the blank value by adding a value to your choices list in your model field definition. From the documentation on configuring field choices:

Unless blank=False is set on the field along with a default then a label containing "---------" will be rendered with the select box. To override this behavior, add a tuple to choices containing None; e.g. (None, 'Your String For Display'). Alternatively, you can use an empty string instead of None where this makes sense - such as on a CharField.

I checked the documentation for different versions of Django and found that this was added in Django 1.7.

Crake answered 11/4, 2009 at 0:44 Comment(1)
This is what I was looking for to change the label for the not-selected option, thanks :) I could then create an unselected_label parameter and use the following. This sets a default if not supplied (removing '----') otherwise if not required, changes the '----' label to a passed parameter: if required and not default: default = choices[0][0] elif not required: choices.insert(0, (None, unselected_label))Hylomorphism
A
3

I was messing around with this today and just came up with a coward hack nifty solution:

# Cowardly handle ModelChoiceField empty label
# we all hate that '-----' thing
class ModelChoiceField_init_hack(object):
    @property
    def empty_label(self):
        return self._empty_label

    @empty_label.setter
    def empty_label(self, value):
        self._empty_label = value
        if value and value.startswith('-'):
            self._empty_label = 'Select an option'
ModelChoiceField.__bases__ += (ModelChoiceField_init_hack,)

Now you can tweak the default ModelChoiceField empty label to anything you'd like. :-)

PS: No need for downvotes, non-harmful monkey patches are always handy.

Ackack answered 11/4, 2009 at 0:44 Comment(0)
T
2

For the latest version of django the first answer should be like this

class ThingForm(models.ModelForm):
class Meta:
 model = Thing

  def __init__(self, *args, **kwargs):
    self.base_fields['cargo'].empty_label = None
    super(ThingForm, self).__init__(*args, **kwargs)`
Tore answered 11/4, 2009 at 0:44 Comment(0)
I
1

I find SOLUTION!!

But not for ForeignKey :-)

Maybe I can help you. I looked in Django source code and discovered that in django.forms.extras.widgets.SelecteDateWidget() is a property called none_value that equals (0, '-----') so I did in my code this

class StudentForm(ModelForm):
    class Meta:
        this_year = int(datetime.datetime.today().strftime('%Y')) 
        birth_years = []
        years = []

        for year in range(this_year - 2, this_year + 3 ):
            years.append(year)
        for year in range(this_year - 60, this_year+2):
            birth_years.append(year)

        model = Student
        exclude = ['user', 'fullname']
        date_widget = SelectDateWidget(years=years)

        date_widget.__setattr__('none_value', (0, 'THERE WAS THAT "-----" NO THERES THIS:-)'))
        widgets = {
            'beginning': date_widget,
            'birth': SelectDateWidget(years=birth_years),
        }
Island answered 11/4, 2009 at 0:44 Comment(1)
Doesn't exactly address the question, but led to me to the solution for the problem I was having. In my case, I was just trying to change the default display for a null value that isn't tied to a model. This defaults to "--------" which I wanted changed. If that's the case then just do: self.fields[field_name].widget.choices[0] = ('', SOME_FIRST_CHOICE_DISPLAY_VALUE)Plashy
B
0

This becomes more complicated when the choices are foreign keys and if you want to filter the choices based on some criteria. In such case if you set the empty_label and then re-assign the choices (you can apply filtration here too) the empty label will be blank:

class ThingForm(models.ModelForm):

    class Meta:
    model = Thing

    def __init__(self, *args, **kwargs):
        super(ThingForm, self).__init__(*args, **kwargs)
        self.fields['verb'].empty_label = None
        self.fields['verb'].queryset=Verb.objects.all()

bbasically, the first line under init can be applied for all of the fields in the form with a loop or inline loop:

def __init__(self,user, *args, **kwargs):
    super(NewTicket, self).__init__(*args, **kwargs)
    for f in self.fields:
       self.fields[f].empty_label = None # or "Please Select" etc
Boggart answered 11/4, 2009 at 0:44 Comment(0)
T
0

There are lots of great answers here, but I'm still not entirely satisfied with the implementations. I'm also a bit frustrated that select widgets from different sources (foreign keys, choices) yield different behaviours.

I have a design I'm working with where select fields always have a blank option, and if they're required they will have a star next to them and the form will simply not validate if they're left empty. That said, I can only properly override the empty_label for fields that are not TypedChoiceFields.

Here's what the result should look like. The first result is always the name of the field - in my case, the label.

select widget

Here's what I ended up doing. The following is an overridden __init__ method of my form:

def __init__(self, *args, **kwargs):
    super().__init__(*args, **kwargs)
    for _, field in self.fields.items():
        if hasattr(field, 'empty_label'):
            field.empty_label = field.label
        if isinstance(field, forms.TypedChoiceField):
            field.choices = [('', field.label)] + [choice for choice in field.choices if choice[0]]
Telegraphy answered 11/4, 2009 at 0:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.