Про radio buttons и ModelForm в Django
Допустим есть задача в форме давать возможность человеку указать свой пол. Конкретно для пола нам хватит поля django.forms.BooleanField
или django.forms.NullBooleanField
, если указывать пол не обязательно. Но поскольку данный пример взят в образовательных целях, а в реальной жизнии скорее всего будет больше, чем 2 варианта выбора, будем использовать django.forms.IntegerField
.
Например у нас есть такая модель
GENDER_MALE = 0
GENDER_FEMALE = 1
CHOICES_GENDER = ((GENDER_MALE, _("Male")),
(GENDER_FEMALE, _("Female")))
class Men:
gender = forms.IntegerField(_("Gender"),
choices=CHOICES_GENDER)
И такая форма
class MenForm(forms.ModelForm):
class Meta:
model = Men
widgets = {'gender': forms.RadioSelect()}
Если мы попробуем вывести в шаблоне эту форму, то обнаружим не два radio-button’а, а три. Первым будет пустой выбор. Давайте попробуем разобраться почему.
При создании полей для формы, вызывается метод django.forms.models.fields_for_model. В этот метод передаются параметры, которые вы могли бы описать в классе Meta класса ModelForm: model, fields, exclude, widgets, formfield_callback
В случае, если formfield_callback не Null, будет вызван этот метод для получения инстанса django.forms.IntegerField. В общем же случае, для каждого поля нашей модели вызывается метод formfield. Т.е. для получения объекта класса django.forms.IntegerField мы вызовем model.fields[“gender”].formfield(), который вернет там IntegerField с нужным нам виджетом (RadioSelect).
Посмотрим что происходит в методе formfield и где берется дополнительный элемент в списке choices. В этом методе есть такие строки
include_blank = self.blank or not (self.has_default() or 'initial' in kwargs)
defaults['choices'] = self.get_choices(include_blank=include_blank)
Как понятно из названия переменной, если include_blank==True
, то в choices добавляется “путое значение”. Попробуем разобраться для чего это сделано внимательно вчитавшись в первую строчку. Итак,
- если поле может быть пустым (
self.blank == True
), то добавить пустой поле - если нет значения по умолчанию у этого поля модели, либо если при создании формы не было передано initial значение для этого поля, то также добавляется пустое поле
С причиной мы разобрались. Подумаем теперь как добиться того, что нам нужно.
Вариант номер один
Наиболее простой вариант — это добавить значение по умолчанию в модель.
GENDER_MALE = 0
GENDER_FEMALE = 1
CHOICES_GENDER = ((GENDER_MALE, _("Male")),
(GENDER_FEMALE, _("Female")))
class Men:
gender = forms.IntegerField(_("Gender"),
choices=CHOICES_GENDER,
default=GENDER_MALE)
Недостаток данного метода, что у пользователя всегда будет выбрано одно из значений, и мы не заставим его внимательно прочитать что от него хотят и в большинстве случаев будем получать значение по умолчанию.
Метод номер два
Можно переопределить значение поля gender, которое создается метаклассом ModelForm. В этом случае forms.py у вас будет выглядеть примерно так.
class MenForm(forms.ModelForm):
gender = forms.IntegerField(widget=forms.RadioSelect(choices=CHOICES_GENDER))
class Meta:
model = Men
Явных недостатков не вижу, кроме как необходимо отдельно определять label для поля и др. значения, которые уже не возьмутся автоматически с определения модели.
Метод номер три
Ну и на закуску самый “хакерский” метод. Как вы помните, функция fields_for_model в качетсве параметра принимает formfield_callback — это метод, который будет вызываться для создания всех полей формы. Вызываться этот метод будет вместо formfield. Не буду дого рассказывать откуда что берется и как происходит внутри, сразу приведу пример. Подробности можно прочитать в файле django/forms/models.py
def formfield_cb(field, **kwargs):
if field.name == 'gender':
kwargs["choices"] = CHOICES_GENDER
return field.formfield(**kwargs)
class MenForm(forms.ModelForm):
formfield_callback = formfield_cb
class Meta:
model = Men
widgets = {'gender': forms.RadioSelect(choices=CHOICES_GENDER)}
Ну и традиционно про недостатки. Уж очень как-то много букв нужно писать чтоб все заработало. Ну и строчку formfield_callback = formfield_cb
нужно включать во всех наследников базовой формы. Если мы унаследуем форму и забудем добавить эту строчку, то новая форма снова будет иметь пустое значение в radio-select виджете.