Django form field choices, adding an attribute
You'd have to subclass the field to take whatever means of specifying the title you'd like and the widget to display the new attribute.
If you had something like this (note: entirely untested):
from django import forms
from django.utils.html import escape
from django.utils.encoding import force_unicode
class SelectWithTitles(forms.Select):
def __init__(self, *args, **kwargs):
super(SelectWithTitles, self).__init__(*args, **kwargs)
# Ensure the titles dict exists
self.titles = {}
def render_option(self, selected_choices, option_value, option_label):
title_html = (option_label in self.titles) and \
u' title="%s" ' % escape(force_unicode(self.titles[option_label])) or ''
option_value = force_unicode(option_value)
selected_html = (option_value in selected_choices) and u' selected="selected"' or ''
return u'<option value="%s"%s%s>%s</option>' % (
escape(option_value), title_html, selected_html,
conditional_escape(force_unicode(option_label)))
class ChoiceFieldWithTitles(forms.ChoiceField):
widget = SelectWithTitles
def __init__(self, choices=(), *args, **kwargs):
choice_pairs = [(c[0], c[1]) for c in choices]
super(ChoiceFieldWithTitles, self).__init__(choices=choice_pairs, *args, **kwargs)
self.widget.titles = dict([(c[1], c[2]) for c in choices])
...you should be able to do this:
PLANNING_CHOICES_WITH_TITLES = (
('0', 'Every morning', 'bla1'),
('1', 'Every night', 'bla2'),
('2', 'Never', 'bla3'),
)
planning = forms.ChoiceFieldWithTitles(
required=True, choices=PLANNING_CHOICES_WITH_TITLES)
Here is a general solution to allow attributes in options in a Select widget, SelectMultiple widget and also in Select2 widgets.
It has been tested on Django 2.1 and should work for other versions as well (tell me in comments).
from django.forms.widgets import Select, SelectMultiple
class SelectWOA(Select):
"""
Select With Option Attributes:
subclass of Django's Select widget that allows attributes in options,
like disabled="disabled", title="help text", class="some classes",
style="background: color;"...
Pass a dict instead of a string for its label:
choices = [ ('value_1', 'label_1'),
...
('value_k', {'label': 'label_k', 'foo': 'bar', ...}),
... ]
The option k will be rendered as:
<option value="value_k" foo="bar" ...>label_k</option>
"""
def create_option(self, name, value, label, selected, index,
subindex=None, attrs=None):
if isinstance(label, dict):
opt_attrs = label.copy()
label = opt_attrs.pop('label')
else:
opt_attrs = {}
option_dict = super(SelectWOA, self).create_option(name, value,
label, selected, index, subindex=subindex, attrs=attrs)
for key,val in opt_attrs.items():
option_dict['attrs'][key] = val
return option_dict
Here is an example that you can try in your forms.py:
choices = [('b', 'blue'),
('g', {'label': 'green', 'disabled': 'disabled'}),
('c', {'label': 'cyan',
'title': 'Kind of violet',
'style': 'background: cyan;',
}),
('r', 'red'), ]
colors = forms.ChoiceField(
label="Colors",
choices=choices,
widget=SelectWOA)
The colors
field can be rendered in Django shell to check the result:
(myvenv) $ ./manage.py shell
>>> from myapp.forms import *
>>> choices = ...
>>> colors = ...
>>> colors.widget.render('mycolors','')
'''<select name="mycolors">
<option value="b">blue</option>
<option value="g" disabled="disabled">green</option>
<option value="c" title="Kind of violet" style="background: cyan;">cyan</option>
<option value="r">red</option>
</select>'''
To allow multiple selections, add this:
class SelectMultipleWOA(SelectWOA, SelectMultiple):
"""
SelectMultipleWOA widget works like SelectMultiple, with options attrs.
See SelectWOA.
"""
pass
colors = forms.MultipleChoiceField(
label="Colors",
choices=choices,
widget=SelectMultipleWOA)
It will render <select name="mycolors" multiple>...<\select>
.
You can use SelectWOA and SelectMultipleWOA to extend the Select2 widgets:
from django_select2.forms import Select2Mixin
class Select2MultipleWidgetWOA(Select2Mixin, SelectMultipleWOA):
"""
Select2 drop in widget for multiple select.
Works just like Select2MultipleWidget but with options attrs.
"""
pass
colors = forms.MultipleChoiceField(
label="Colors",
choices=choices,
widget=Select2MultipleWidgetWOA(
attrs={'data-placeholder': 'Any color',
'data-close-on-select': 'false',
'style': 'width:280px; height:36px;',
'title': 'Type a word to filter the menu',}
)
)
It will render something like:
'''<select name="mycolors" data-placeholder="Any color"
class="django-select2" data-minimum-input-length="0" multiple
style="width:280px; height:36px;" data-close-on-select="false"
data-allow-clear="false" title="Type a word to filter the menu">
<option value="b">blue</option>
<option value="g" disabled="disabled">green</option>
<option value="c" title="Kind of violet" style="background: cyan;">cyan</option>
<option value="r">red</option>
</select>'''
This is how I solved the problem
#models.py
class Publisher(models.Model):
slug = models.Slugfield()
class Book(forms.ModelForm):
publisher = models.ForeignKey(Publisher)
#forms.py
from django import forms
from django.utils.encoding import force_unicode
from django.utils.html import escape, conditional_escape
class SelectWithData(forms.Select):
def render_option(self, selected_choices, option_value, option_label):
obj_data = {
obj.id: {
data_attr: getattr(obj, data_attr) for data_attr in self.data_attrs
} for obj in self.queryset
}
data_text = u''
for data_attr in self.data_attrs:
data_text += u' data-{}="{}" '.format(
data_attr,
escape(force_unicode(obj_data[option_value][data_attr]))
)
option_value = force_unicode(option_value)
selected_html = (option_value in selected_choices) and u' selected="selected"' or ''
return u'<option value="{}"{}{}>{}</option>'.format(
escape(option_value),
data_text,
selected_html,
conditional_escape(force_unicode(option_label))
)
class ModelChoiceFieldWithData(forms.ModelChoiceField):
widget = SelectWithData
def __init__(self, queryset, **kwargs):
data_attrs = kwargs.pop('data_attrs')
super(ModelChoiceFieldWithData, self).__init__(queryset, **kwargs)
self.widget.queryset = queryset
self.widget.data_attrs = data_attrs
class BookForm(forms.ModelForm):
publisher = ModelChoiceFieldWithData(
queryset=Publisher.objects.all(),
data_attrs=('slug',),
)
#html
<select id="id_publisher" name="publisher" required="required" title="">
<option value="1" data-slug="first" selected="selected">First Publisher</option>
<option value="2" data-slug="second">Second Publisher</option>
</select>
render_option
has been removed from Django 1.11 onwards. This is what I did to achieve this. A little bit of digging and this seems straightforward. Works with Django 2.0+
class CustomSelect(forms.Select):
def __init__(self, attrs=None, choices=()):
self.custom_attrs = {}
super().__init__(attrs, choices)
def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
index = str(index) if subindex is None else "%s_%s" % (index, subindex)
if attrs is None:
attrs = {}
option_attrs = self.build_attrs(self.attrs, attrs) if self.option_inherits_attrs else {}
if selected:
option_attrs.update(self.checked_attribute)
if 'id' in option_attrs:
option_attrs['id'] = self.id_for_label(option_attrs['id'], index)
# setting the attributes here for the option
if len(self.custom_attrs) > 0:
if value in self.custom_attrs:
custom_attr = self.custom_attrs[value]
for k, v in custom_attr.items():
option_attrs.update({k: v})
return {
'name': name,
'value': value,
'label': label,
'selected': selected,
'index': index,
'attrs': option_attrs,
'type': self.input_type,
'template_name': self.option_template_name,
}
class MyModelChoiceField(ModelChoiceField):
# custom method to label the option field
def label_from_instance(self, obj):
# since the object is accessible here you can set the extra attributes
if hasattr(obj, 'type'):
self.widget.custom_attrs.update({obj.pk: {'type': obj.type}})
return obj.get_display_name()
The form:
class BookingForm(forms.ModelForm):
customer = MyModelChoiceField(required=True,
queryset=Customer.objects.filter(is_active=True).order_by('name'),
widget=CustomSelect(attrs={'class': 'chosen-select'}))
The output which I needed is as:
<select name="customer" class="chosen-select" required="" id="id_customer">
<option value="" selected="">---------</option>
<option value="242" type="CNT">AEC Transcolutions Private Limited</option>
<option value="243" type="CNT">BBC FREIGHT CARRIER</option>
<option value="244" type="CNT">Blue Dart Express Limited</option>
You can't. Not without too much hackery, at least.
You are probably good using form_utils.BetterForm, an external package rather than django.forms.Form
if you want to have this and more control over the markup generated.