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.