I had the similar requirement as OP but with the base field being a DecimalField. So the user could enter a valid floating point number or select from a list of optional choices.
I liked Austin Fox's answer in that it follows the django framework better than Viktor eXe's answer. Inheriting from the ChoiceField object allows the field to manage an array of option widgets. So it would be tempting to try;
class CustomField(Decimal, ChoiceField): # MRO Decimal->Integer->ChoiceField->Field
...
class CustomWidget(NumberInput, Select):
But the assumption is that the field must contain something that appears in the choices list. There is a handy valid_value method that you can override to allow any value, but there is a bigger problem - binding to a decimal model field.
Fundamentally, all the ChoiceField objects manage lists of values and then have an index or multiple selection indices that represents the selection. So bound data will appear in the widget as;
[some_data] or [''] empty value
Hence Austin Fox overriding the format_value method to return back to a base Input class method version. Works for charfield but not for Decimal or Float fields because we lose all the special formatting in the number widget.
So my solution was to inherit directly from Decimal field but adding only the choice property (lifted from django CoiceField)....
First the custom widgets;
class ComboBoxWidget(Input):
"""
Abstract class
"""
input_type = None # must assigned by subclass
template_name = "datalist.html"
option_template_name = "datalist_option.html"
def __init__(self, attrs=None, choices=()):
super(ComboBoxWidget, self).__init__(attrs)
# choices can be any iterable, but we may need to render this widget
# multiple times. Thus, collapse it into a list so it can be consumed
# more than once.
self.choices = list(choices)
def __deepcopy__(self, memo):
obj = copy.copy(self)
obj.attrs = self.attrs.copy()
obj.choices = copy.copy(self.choices)
memo[id(self)] = obj
return obj
def optgroups(self, name):
"""Return a list of optgroups for this widget."""
groups = []
for index, (option_value, option_label) in enumerate(self.choices):
if option_value is None:
option_value = ''
subgroup = []
if isinstance(option_label, (list, tuple)):
group_name = option_value
subindex = 0
choices = option_label
else:
group_name = None
subindex = None
choices = [(option_value, option_label)]
groups.append((group_name, subgroup, index))
for subvalue, sublabel in choices:
subgroup.append(self.create_option(
name, subvalue
))
if subindex is not None:
subindex += 1
return groups
def create_option(self, name, value):
return {
'name': name,
'value': value,
'template_name': self.option_template_name,
}
def get_context(self, name, value, attrs):
context = super(ComboBoxWidget, self).get_context(name, value, attrs)
context['widget']['optgroups'] = self.optgroups(name)
context['wrap_label'] = True
return context
class NumberComboBoxWidget(ComboBoxWidget):
input_type = 'number'
class TextComboBoxWidget(ComboBoxWidget):
input_type = 'text'
The Custom Field Class
class OptionsField(forms.Field):
def __init__(self, choices=(), **kwargs):
super(OptionsField, self).__init__(**kwargs)
self.choices = list(choices)
def _get_choices(self):
return self._choices
def _set_choices(self, value):
"""
Assign choices to widget
"""
value = list(value)
self._choices = self.widget.choices = value
choices = property(_get_choices, _set_choices)
class DecimalOptionsField(forms.DecimalField, OptionsField):
widget = NumberComboBoxWidget
def __init__(self, choices=(), max_value=None, min_value=None, max_digits=None, decimal_places=None, **kwargs):
super(DecimalOptionsField, self).__init__(choices=choices, max_value=max_value, min_value=min_value,
max_digits=max_digits, decimal_places=decimal_places, **kwargs)
class CharOptionsField(forms.CharField, OptionsField):
widget = TextComboBoxWidget
def __init__(self, choices=(), max_length=None, min_length=None, strip=True, empty_value='', **kwargs):
super(CharOptionsField, self).__init__(choices=choices, max_length=max_length, min_length=min_length,
strip=strip, empty_value=empty_value, **kwargs)
The html templates
datalist.html
<input list="{{ widget.name }}_list" type="{{ widget.type }}" name="{{ widget.name }}"{% if widget.value != None %} value="{{ widget.value|stringformat:'s' }}"{% endif %}{% include "django/forms/widgets/attrs.html" %} />
<datalist id="{{ widget.name }}_list">{% for group_name, group_choices, group_index in widget.optgroups %}{% if group_name %}
<optgroup label="{{ group_name }}">{% endif %}{% for option in group_choices %}
{% include option.template_name with widget=option %}{% endfor %}{% if group_name %}
</optgroup>{% endif %}{% endfor %}
</datalist>
datalist_option.html
<option value="{{ widget.value|stringformat:'s' }}"{% include "django/forms/widgets/attrs.html" %}>
An example of use. Note that the second element of the choice tuple is not needed by the HTML datalist option tag, so I leave them as None. Also the first tuple value can be text or native decimal - you can see how the widget handles them.
class FrequencyDataForm(ModelForm):
frequency_measurement = DecimalOptionsField(
choices=(
('Low Freq', (
('11.11', None),
('22.22', None),
(33.33, None),
),
),
('High Freq', (
('66.0E+06', None),
(1.2E+09, None),
('2.4e+09', None)
),
)
),
required=False,
max_digits=15,
decimal_places=3,
)
class Meta:
model = FrequencyData
fields = '__all__'