I having trouble with a simple question : How to have some \"disabled\" field in a dropdown menu generated via a modelForm and choiceFied in the django Framework ?
field_choices = (
('','Make choice'),
(1,'first'),
(2,'second'),
(3,'third')
)
from django.forms import Select
class Select(Select):
def create_option(self, *args,**kwargs):
option = super().create_option(*args,**kwargs)
if not option.get('value'):
option['attrs']['disabled'] = 'disabled'
if option.get('value') == 2:
option['attrs']['disabled'] = 'disabled'
return option
This might be a late answer but this is a simplified version that can be modified using the form instance.
You can either pass a list of values to be disabled i.e
def __init__(self, disabled_choices, *args, **kwargs):
self.disabled_choices = disabled_choices
OR
from django.forms import Select
class SelectWidget(Select):
"""
Subclass of Django's select widget that allows disabling options.
"""
def __init__(self, *args, **kwargs):
self._disabled_choices = []
super(SelectWidget, self).__init__(*args, **kwargs)
@property
def disabled_choices(self):
return self._disabled_choices
@disabled_choices.setter
def disabled_choices(self, other):
self._disabled_choices = other
def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
option_dict = super(SelectWidget, self).create_option(
name, value, label, selected, index, subindex=subindex, attrs=attrs
)
if value in self.disabled_choices:
option_dict['attrs']['disabled'] = 'disabled'
return option_dict
To disabled an option based on a condition i.e user isn't a superuser.
class MyForm(forms.Form):
status = forms.ChoiceField(required=True, widget=SelectWidget, choices=(('on', 'On'), ('off', 'Off')))
def __init__(self, request, *args, **kwargs):
super().__init__(*args, **kwargs)
if not request.user.is_superuser:
self.fields['status'].widget.disabled_choices = ['off']
Django's form widgets offer a way to pass a list of attributes that should be rendered on the <option>
tag:
my_choices = ( ('one', 'One'), ('two', 'Two'))
class MyForm(forms.Form):
some_field = forms.ChoiceField(choices=my_choices,
widget=forms.Select(attrs={'disabled':'disabled'}))
Unfortunately, this won't work for you because the attribute will be applied to EVERY option tag that is rendered. Django has no way to automatically know which should be enabled and which should be disabled.
In your case, I recommend writing a custom widget. It's pretty easy to do, and you don't have that much custom logic to apply. The docs on this are here. In short though:
forms.Select
, which is the default select rendererrender(self, name, value, attrs)
method. Use your custom logic to determine if the value
qualifies as needing to be disabled. Have a look at the very short implementation of render
in django/forms/widgets.py
if you need inspriation.Then, define your form field to use your custom widget:
class MyForm(forms.Form):
some_field = forms.ChoiceField(choices=my_choices,
widget=MyWidget)
Are you trying to create a menu in which the list items are separated into categories, and you don't want the categories themselves to be selectable?
If so, you can achieve this by having your template render the field using tags, e.g.
<select name="my_field" id="id_my_field">
<optgroup label="-- Root 1 entry --">
<option value="1">Elt 1</option>
<option value="2">Elt 2</option>
<option value="3">Elt 3</option>
</optgroup>
<optgroup label="--- Root 2 entry ---">
<option value="4">Elt 4</option>
<option value="5">Elt 5</option>
</optgroup>
</select>
Simple questions sometimes have complex answers in Django. I spent a lot of time getting this to work well. Combining Jarrett's overview with an important note from jnns about render_option
and some help from #django on freenode I have a well-working complete example solution:
First, this example assumes a normally defined choices-type CharField in a model I call Rule. I subclass my own TimeStampedModel
but you can use a models.Model
:
class Rule(TimeStampedModel):
...
# Rule Type
SHORT_TERM_RULE = 'ST'
MAX_SIGHTINGS_PER_PERIOD_RULE = "MA"
WHITE_LIST_RULE = "WL"
BLACK_LIST_RULE = "BL"
RULE_CHOICES = (
(SHORT_TERM_RULE, 'Short Term Rule'),
(MAX_SIGHTINGS_PER_PERIOD_RULE, 'Max Sightings Per Period Rule'),
(WHITE_LIST_RULE, 'White List Rule'),
(BLACK_LIST_RULE, 'Black List Rule'),
)
rule_type = models.CharField(
max_length=2,
choices=RULE_CHOICES,
default=SHORT_TERM_RULE,
)
...
In forms.py, define the widget subclassing Select
that accepts disabled_choices
. It has a custom render_option()
that adds disabled
to the html output of option tags if their choice is included on the passed in disabled_choices
list. Note, I left most of the render_option()
code from Select
as-is:
class MySelect(Select):
def __init__(self, attrs=None, choices=(), disabled_choices=()):
super(MySelect, self).__init__(attrs, choices=choices)
self.disabled_choices = disabled_choices
def render_option(self, selected_choices, option_value, option_label):
if option_value is None:
option_value = ''
option_value = force_text(option_value)
if option_value in selected_choices:
selected_html = mark_safe(' selected="selected"')
if not self.allow_multiple_selected:
selected_choices.remove(option_value)
else:
selected_html = ''
for key, value in self.disabled_choices:
if option_value in key:
return format_html('<option disabled value="{}"{}>{}</option>', option_value, selected_html,
force_text(option_label))
return format_html('<option value="{}"{}>{}</option>', option_value, selected_html, force_text(option_label))
Then, in defining the form subclassing ModelForm
, check for a passed-in disabled_choices
list and initialize the field accordingly. In this example, I also sneak in a default choice.
class RuleChoiceForm(ModelForm):
class Meta:
model = Rule
fields = ['rule_type']
# Add a default choice to the list defined in the Rule model
default_choice = ('DF', 'Choose a Rule Type...')
choices = list(Rule.RULE_CHOICES)
choices.insert(0, default_choice)
choices = tuple(choices)
rule_type = forms.ChoiceField(widget=MySelect(attrs={'class': 'form-control'}, disabled_choices=[]),
choices=choices)
def __init__(self, *args, disabled_choices=None, **kwargs):
super(RuleChoiceForm, self).__init__(*args, **kwargs)
if disabled_choices:
self.fields['rule_type'].widget.disabled_choices = disabled_choices
Then in your view, define disabled_choices
as a list, appending choices from your _CHOICES
var in your model and pass it into the form instantiation. In my logic, I use list comprehension of RULE_CHOICES
to get the tuple of the choice I want to disable. Though there may be a simpler way, please feel free to post suggestions to simplify or improve this answer.
disabled_choices = []
# Logic disabling choices
if Rule.objects.filter(route=route, rule_type=Rule.SHORT_TERM_RULE).exists():
disabled_choices.append([item for item in Rule.RULE_CHOICES if Rule.SHORT_TERM_RULE in item][0])
disabled_choices.append([item for item in Rule.RULE_CHOICES if Rule.MAX_SIGHTINGS_PER_PERIOD_RULE in item][0])
rule_choice_form = RuleChoiceForm(disabled_choices=disabled_choices)
It seems django 1.1 will allow "optgroup": Django documentation
class MyForm(forms.Form):
some_field = forms.ChoiceField(choices=[
('Audio', (
('vinyl', 'Vinyl'),
('cd', 'CD'),
)
),
('Video', (
('vhs', 'VHS Tape'),
('dvd', 'DVD'),
)
),
('unknown', 'Unknown'),
])
This imho is a must have.