Django表单有选项,但也有自由文本选项?
我要找的是:一个小部件,它为用户提供了一个下拉选择列表,但下面还有一个文本输入框,用户可以输入一个新值 后端模型将有一组默认选项(但不会在模型上使用choices关键字)。我知道我可以(而且我已经)通过让表单同时具有ChoicesField和CharField来实现这一点,如果ChoicesField保留为默认值,则让代码使用CharField,但这感觉像是“unDjango” 是否有一种方法(使用Django内置或Django插件)为表单定义类似ChoiceEntryField(仿照IIRC执行此操作的GTKComboboboxentry)的内容Django表单有选项,但也有自由文本选项?,django,django-forms,Django,Django Forms,我要找的是:一个小部件,它为用户提供了一个下拉选择列表,但下面还有一个文本输入框,用户可以输入一个新值 后端模型将有一组默认选项(但不会在模型上使用choices关键字)。我知道我可以(而且我已经)通过让表单同时具有ChoicesField和CharField来实现这一点,如果ChoicesField保留为默认值,则让代码使用CharField,但这感觉像是“unDjango” 是否有一种方法(使用Django内置或Django插件)为表单定义类似ChoiceEntryField(仿照IIRC执
如果有人发现了这一点,请注意,有一个类似的问题,即如何从用户体验的角度最好地完成我在中所寻找的内容。输入类型在选项和文本字段中是否相同?如果是这样,我将在类中创建一个CharField(或Textfield),并让一些前端javascript/jquery通过应用“If no information in dropdown,use data in Textfield”子句来处理将要传递的数据 我做了一个演示,演示如何在前端执行此操作 HTML:
然后,您可以在提交时指定将传递给视图的值。编辑:更新以使其与UpdateView一起工作 所以我要找的似乎是 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]
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
示例使用
(forms.py)
(示例虚拟模型.py:)
(示例虚拟视图.py:)
我推荐一种自定义小部件方法,HTML5允许您使用下拉列表进行自由文本输入,该下拉列表将用作选择或写入其他类型的字段,我就是这样做的: 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)
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
})
我知道我参加聚会有点晚了,但我最近使用了另一种解决办法 我将of与
datalist
参数一起使用。这将生成一个HTML5
元素,浏览器会自动为其创建一个建议列表(另请参见)
下面是模型表单的简单外观:
类MyProjectForm(ModelForm):
类元:
模型=我的项目
fields=“\uuuu all\uuuuuu”
小部件={
“name”:floppyforms.widgets.Input(datalist=\u get\u all\u proj\u names())
}
以下是我解决这个问题的方法。我从传递到模板的表单
对象中检索选项,并手动填写数据列表
:
{% 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 %}
{%用于表单%]中的字段
{{field.label_tag}
{field.subwidgets%中的选项为%s}
{%endfor%}
{%endfor%}
我知道这很旧,但我认为这可能对其他人有用。
下面实现了与answer类似的结果,但适用于使用django本机方法的模型、查询集和外键
在forms.py子类forms.Select和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()
然后在模板中创建listtxt.html:
<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>
小部件和字段也在form.ModelForm表单中工作,并将接受属性。我的要求与OP类似,但基本字段是一个小数字段。因此,用户可以输入有效的浮点数或从可选选项列表中进行选择 我喜欢Austin Fox的答案,因为它比Viktor eXe的答案更符合django框架。从ChoiceField对象继承允许该字段管理选项小部件数组。因此,尝试是很有诱惑力的
class CustomField(Decimal, ChoiceField): # MRO Decimal->Integer->ChoiceField->Field
...
class CustomWidget(NumberInput, Select):
但假设字段必须包含出现在选项列表中的内容。有一个方便的有效的_值方法,您可以覆盖它以允许任何值,但还有一个更大的问题-绑定到十进制模型字段
基本上,所有ChoiceField对象都管理值列表,然后具有一个或多个表示选择的选择索引。因此,绑定的数据将在小部件中显示为
[some_data] or [''] empty value
因此,Austin Fox重写format_value方法以返回基本输入类方法版本。适用于charfield,但不适用于Decimal或Float字段,因为我们丢失了number小部件中的所有特殊格式
所以我的解决方案是直接从Decimal字段继承,但只添加choice属性(从django CoiceField中提取)
首先是自定义小部件强>
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'
自定义字段类
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)
html模板
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)
datalist.html
这次聚会晚了5年,但@Foon的自我回答
OptionalChoiceWidget
正是我所寻找的,希望其他想问同样问题的人也能像我一样通过StackOverflow的答案查找算法找到答案
如果从选项下拉列表中选择了答案,我希望文本输入框消失,这很容易实现。因为它可能对其他人有用:
{% 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 %}
任何想知道onready\u js
块的人,我有(在我的base.html
模板中,其他所有东西都继承了这个模板)
$(文档).ready(函数(){
{%block onready_js%}{%endblock onready_js%}
});
我不明白为什么不是每个人都这样做JQuery!
以下是可重用模型字段级DataListCharField的用法。我修改了@Viktor eXe和@Ryan Skene的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(自定义)在“my_custom_fields.py”中定义。
它很容易从下到上阅读
(DatalistCharField>ListTextField>ListTextWidget)
((models.Field>forms.Field>forms.Widget)结构)
#'my_custom_fields.py'
类ListTextWidget(forms.TextInput):#表单widget
定义初始化(self,*args,**kwargs):
self.datalist=None
超级(ListTextWidget,self)。\uuuuuuuuuu初始化(*args,**kwargs)
def render(self、name、value、attrs=None、renderer=None):
默认属性=
[some_data] or [''] empty value
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'
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)
<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>
<option value="{{ widget.value|stringformat:'s' }}"{% include "django/forms/widgets/attrs.html" %}>
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__'
{% 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 %}
<script type="text/javascript"> $(document).ready( function() {
{% block onready_js %}{% endblock onready_js %}
});
</script>
# '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)
# '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)