46
I would recommend a custom Widget approach, HTML5 allows you to have a free text input with a dropdown list which would work as a pick-one-or-write-other type of field, this is how I made it:
fields.py
from django import forms
class ListTextWidget(forms.TextInput):
def __init__(self, data_list, name, *args, **kwargs):
super(ListTextWidget, self).__init__(*args, **kwargs)
self._name = name
self._list = data_list
self.attrs.update({'list':'list__%s' % self._name})
def render(self, name, value, attrs=None, renderer=None):
text_html = super(ListTextWidget, self).render(name, value, attrs=attrs)
data_list = '<datalist id="list__%s">' % self._name
for item in self._list:
data_list += '<option value="%s">' % item
data_list += '</datalist>'
return (text_html + data_list)
forms.py
from django import forms
from myapp.fields import ListTextWidget
class FormForm(forms.Form):
char_field_with_list = forms.CharField(required=True)
def __init__(self, *args, **kwargs):
_country_list = kwargs.pop('data_list', None)
super(FormForm, self).__init__(*args, **kwargs)
# the "name" parameter will allow you to use the same widget more than once in the same
# form, not setting this parameter differently will cuse all inputs display the
# same list.
self.fields['char_field_with_list'].widget = ListTextWidget(data_list=_country_list, name='country-list')
views.py
from myapp.forms import FormForm
def country_form(request):
# instead of hardcoding a list you could make a query of a model, as long as
# it has a __str__() method you should be able to display it.
country_list = ('Mexico', 'USA', 'China', 'France')
form = FormForm(data_list=country_list)
return render(request, 'my_app/country-form.html', {
'form': form
})
11
I know I’m a bit late to the party but there is another solution which I have recently used.
I have used the Input
widget of django-floppyforms with a datalist
argument. This generates an HTML5 <datalist>
element for which your browser automatically creates a list of suggestions (see also this SO answer).
Here’s what a model form could then simply look like:
class MyProjectForm(ModelForm):
class Meta:
model = MyProject
fields = "__all__"
widgets = {
'name': floppyforms.widgets.Input(datalist=_get_all_proj_names())
}
- [Django]-How to use custom manager with related objects?
- [Django]-How can I use Bootstrap with Django?
- [Django]-What is the equivalent of "none" in django templates?
5
Edit: updated to make it work with UpdateView as well
So what I was looking for appears to be
utils.py:
from django.core.exceptions import ValidationError
from django import forms
class OptionalChoiceWidget(forms.MultiWidget):
def decompress(self,value):
#this might need to be tweaked if the name of a choice != value of a choice
if value: #indicates we have a updating object versus new one
if value in [x[0] for x in self.widgets[0].choices]:
return [value,""] # make it set the pulldown to choice
else:
return ["",value] # keep pulldown to blank, set freetext
return ["",""] # default for new object
class OptionalChoiceField(forms.MultiValueField):
def __init__(self, choices, max_length=80, *args, **kwargs):
""" sets the two fields as not required but will enforce that (at least) one is set in compress """
fields = (forms.ChoiceField(choices=choices,required=False),
forms.CharField(required=False))
self.widget = OptionalChoiceWidget(widgets=[f.widget for f in fields])
super(OptionalChoiceField,self).__init__(required=False,fields=fields,*args,**kwargs)
def compress(self,data_list):
""" return the choicefield value if selected or charfield value (if both empty, will throw exception """
if not data_list:
raise ValidationError('Need to select choice or enter text for this field')
return data_list[0] or data_list[1]
Example use
(forms.py)
from .utils import OptionalChoiceField
from django import forms
from .models import Dummy
class DemoForm(forms.ModelForm):
name = OptionalChoiceField(choices=(("","-----"),("1","1"),("2","2")))
value = forms.CharField(max_length=100)
class Meta:
model = Dummy
(Sample dummy model.py
from django.db import models
from django.core.urlresolvers import reverse
class Dummy(models.Model):
name = models.CharField(max_length=80)
value = models.CharField(max_length=100)
def get_absolute_url(self):
return reverse('dummy-detail', kwargs={'pk': self.pk})
(Sample dummy views.py:)
from .forms import DemoForm
from .models import Dummy
from django.views.generic.detail import DetailView
from django.views.generic.edit import CreateView, UpdateView
class DemoCreateView(CreateView):
form_class = DemoForm
model = Dummy
class DemoUpdateView(UpdateView):
form_class = DemoForm
model = Dummy
class DemoDetailView(DetailView):
model = Dummy
- [Django]-Django rest framework – how do you flatten nested data?
- [Django]-Django, ReactJS, Webpack hot reload
- [Django]-Is it possible to store an array in Django model?
3
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__'
- [Django]-What is the canonical way to find out if a Django model is saved to db?
- [Django]-AccessDenied when calling the CreateMultipartUpload operation in Django using django-storages and boto3
- [Django]-Getting TypeError: __init__() missing 1 required positional argument: 'on_delete' when trying to add parent table after child table with entries
2
Would the input type be identical in both the choice and text fields? If so, I would make a single CharField (or Textfield) in the class and have some front end javascript/jquery take care of what data will be passed by applying a “if no information in dropdown, use data in textfield” clause.
I made a jsFiddle to demonstrate how you can do this on the frontend.
HTML:
<div class="formarea">
<select id="dropdown1">
<option value="One">"One"</option>
<option value="Two">"Two"</option>
<option value="Three">or just write your own</option>
</select>
<form><input id="txtbox" type="text"></input></form>
<input id="inputbutton" type="submit" value="Submit"></input>
</div>
JS:
var txt = document.getElementById('txtbox');
var btn = document.getElementById('inputbutton');
txt.disabled=true;
$(document).ready(function() {
$('#dropdown1').change(function() {
if($(this).val() == "Three"){
document.getElementById('txtbox').disabled=false;
}
else{
document.getElementById('txtbox').disabled=true;
}
});
});
btn.onclick = function () {
if((txt).disabled){
alert('input is: ' + $('#dropdown1').val());
}
else{
alert('input is: ' + $(txt).val());
}
};
you can then, on submit, specify which value will be passed to your view.
- [Django]-Django Rest Framework – Could not resolve URL for hyperlinked relationship using view name "user-detail"
- [Django]-Django template display item value or empty string
- [Django]-How to use python2.7 pip instead of default pip
2
I know this is old but thought this might be useful to others.
The following achieves a similar outcome to Viktor eXe’s answer but works for models, querysets, and foreign keys with django native methods.
In forms.py subclass forms.Select and forms.ModelChoiceField:
from django import forms
class ListTextWidget(forms.Select):
template_name = 'listtxt.html'
def format_value(self, value):
# Copied from forms.Input - makes sure value is rendered properly
if value == '' or value is None:
return ''
if self.is_localized:
return formats.localize_input(value)
return str(value)
class ChoiceTxtField(forms.ModelChoiceField):
widget=ListTextWidget()
Then create the listtxt.html in templates:
<input list="{{ widget.name }}"
{% if widget.value != None %} name="{{ widget.name }}" value="{{ widget.value|stringformat:'s' }}"{% endif %}
{% include "django/forms/widgets/attrs.html" %}>
<datalist id="{{ widget.name }}">
{% 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>
And now you can use either the widget or field in your forms.py:
from .fields import *
from django import forms
Class ListTxtForm(forms.Form):
field = ChoiceTxtField(queryset=YourModel.objects.all()) # Using new field
field2 = ModelChoiceField(queryset=YourModel.objects.all(),
widget=ListTextWidget()) # Using Widget
The widget and field also work in form.ModelForm Forms and will accept attributes.
- [Django]-Easiest way to rename a model using Django/South?
- [Django]-Remove the default delete action in Django admin
- [Django]-For Django models, is there a shortcut for seeing if a record exists?
2
5 years late to this party, but @Foon ‘s self-answer OptionalChoiceWidget
was exactly what I was looking for and hopefully other people thinking of asking the same question will be directed here by StackOverflow’s answer-finding algorithms as I was.
I wanted the text input box to disappear if an answer was selected from the options pull-down, and that’s easy to accomplish. Since it may be useful to others:
{% block onready_js %}
{{block.super}}
/* hide the text input box if an answer for "name" is selected via the pull-down */
$('#id_name_0').click( function(){
if ($(this).find('option:selected').val() === "") {
$('#id_name_1').show(); }
else {
$('#id_name_1').hide(); $('#id_name_1').val(""); }
});
$('#id_name_0').trigger("click"); /* sets initial state right */
{% endblock %}
Anybody wondering about the onready_js
block, I have (in my base.html
template that everything else inherits)
<script type="text/javascript"> $(document).ready( function() {
{% block onready_js %}{% endblock onready_js %}
});
</script>
Beats me why not everybody does little bits of JQuery this way!
- [Django]-How to add column in ManyToMany Table (Django)
- [Django]-Multiple annotate Sum terms yields inflated answer
- [Django]-How to update() a single model instance retrieved by get() on Django ORM?
0
Here’s how i solved this problem. I retrieve choices from passed to template form
object and fill datalist
manually:
{% for field in form %}
<div class="form-group">
{{ field.label_tag }}
<input list="options" name="test-field" required="" class="form-control" id="test-field-add">
<datalist id="options">
{% for option in field.subwidgets %}
<option value="{{ option.choice_label }}"/>
{% endfor %}
</datalist>
</div>
{% endfor %}
- [Django]-How to change User representation in Django Admin when used as Foreign Key?
- [Django]-Use only some parts of Django?
- [Django]-Suppress "?next=blah" behavior in django's login_required decorator
0
Here’s usage of the reusable Model Field level DataListCharField. I modified @Viktor eXe & @Ryan Skene’s ListTextWidget.
# 'models.py'
from my_custom_fields import DatalistCharField
class MyModel(models.Model):
class MyChoices(models.TextChoices):
ans1 = 'ans1', 'ans1'
ans2 = 'ans2', 'ans2'
my_model_field = DatalistCharField('label of this field', datalist=MyChoices.choices, max_length=30)
DatalistCharField(custom) defined at ‘my_custom_fields.py’.
it is easy to read from bottom to up.
(DatalistCharField > ListTextField > ListTextWidget)
((models.Field > forms.Field > forms.Widget) structure)
# 'my_custom_fields.py'
class ListTextWidget(forms.TextInput): # form widget
def __init__(self, *args, **kwargs):
self.datalist = None
super(ListTextWidget, self).__init__(*args, **kwargs)
def render(self, name, value, attrs=None, renderer=None):
default_attrs = {'list': 'list__%s' % name}
default_attrs.update(attrs)
text_html = super(ListTextWidget, self).render(name, value, attrs=default_attrs) # TextInput rendered
data_list = '<datalist id="list__%s">' % name # append <datalist> under the <input> elem.
if self.datalist:
for _, value in self.datalist:
data_list += '<option value="%s">' % value
else:
data_list += '<option value="%s">' % 'no' # default datalist option
data_list += '</datalist>'
return text_html + data_list
class ListTextField(forms.CharField): # form field
widget = ListTextWidget
def __init__(self, *, max_length=None, min_length=None, strip=True, empty_value='', datalist=None, **kwargs):
super().__init__(max_length=max_length, min_length=min_length, strip=strip, empty_value=empty_value, **kwargs)
self.widget.datalist = datalist
class DatalistCharField(models.CharField): # model field
def __init__(self, *args, **kwargs):
self.datalist = kwargs.pop('datalist', None) # custom parameters should be poped here to bypass super().__init__() or it will raise an error of wrong parameter
super().__init__(*args, **kwargs)
def formfield(self, **kwargs):
defaults = {'form_class': ListTextField, 'datalist': self.datalist} # bypassed custom parameters arrived here
defaults.update(**kwargs)
return super().formfield(**defaults)
- [Django]-How to show a many-to-many field with "list_display" in Django Admin?
- [Django]-Allow only positive decimal numbers
- [Django]-Giving email account a name when sending emails with Django through Google Apps