Django表单有选项,但也有自由文本选项?

Django表单有选项,但也有自由文本选项?,django,django-forms,Django,Django Forms,我要找的是:一个小部件,它为用户提供了一个下拉选择列表,但下面还有一个文本输入框,用户可以输入一个新值 后端模型将有一组默认选项(但不会在模型上使用choices关键字)。我知道我可以(而且我已经)通过让表单同时具有ChoicesField和CharField来实现这一点,如果ChoicesField保留为默认值,则让代码使用CharField,但这感觉像是“unDjango” 是否有一种方法(使用Django内置或Django插件)为表单定义类似ChoiceEntryField(仿照IIRC执

我要找的是:一个小部件,它为用户提供了一个下拉选择列表,但下面还有一个文本输入框,用户可以输入一个新值

后端模型将有一组默认选项(但不会在模型上使用choices关键字)。我知道我可以(而且我已经)通过让表单同时具有ChoicesField和CharField来实现这一点,如果ChoicesField保留为默认值,则让代码使用CharField,但这感觉像是“unDjango”

是否有一种方法(使用Django内置或Django插件)为表单定义类似ChoiceEntryField(仿照IIRC执行此操作的GTKComboboboxentry)的内容


如果有人发现了这一点,请注意,有一个类似的问题,即如何从用户体验的角度最好地完成我在

中所寻找的内容。输入类型在选项和文本字段中是否相同?如果是这样,我将在类中创建一个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)