Django形式有选择,但也有自由文本选项?

时间:2014-07-16 14:21:18

标签: django django-forms

我正在寻找的内容:一个小工具,为用户提供一个下拉列表,但下方还有一个文本输入框,供用户输入新值。

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

是否有办法(使用Django-builtins或Django插件)为表单定义类似ChoiceEntryField(在IIRC为此执行此操作的GtkComboboxEntry之后建模)?

如果有人发现这一点,请注意在https://ux.stackexchange.com/questions/85980/is-there-a-ux-pattern-for-drop-down-preferred-but-free-text-allowed

从UX角度出发,如何最好地完成我想要的工作,这是一个类似的问题。

9 个答案:

答案 0 :(得分:22)

我建议使用自定义Widget方法,HTML5允许您使用下拉列表进行自由文本输入,该列表可用作选择一个或写入其他类型的字段,这就是我的方法:< / p>

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
    })

答案 1 :(得分:6)

我知道我参加聚会有点晚了,但我最近还有另一个解决方案。

我使用Input widgetdjango-floppyforms datalist参数。这会生成一个HTML5 <datalist>元素,您的浏览器会自动为其创建一个建议列表(另请参阅this SO answer)。

以下是模型表格的简单外观:

class MyProjectForm(ModelForm):
    class Meta:
        model = MyProject
        fields = "__all__" 
        widgets = {
            'name': floppyforms.widgets.Input(datalist=_get_all_proj_names())
        }

答案 2 :(得分:3)

编辑:更新以使其也适用于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]

使用示例

(的 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

示例虚拟模型.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})

示例虚拟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

答案 3 :(得分:3)

我的要求与OP类似,但基本字段为 DecimalField 。因此,用户可以输入有效的浮点数或从可选选项列表中进行选择。

我喜欢奥斯汀·福克斯(Austin Fox)的回答,因为它遵循django框架要比Viktor eXe的回答更好。从 ChoiceField 对象继承后,该字段可以管理选项小部件数组。因此尝试尝试很诱人;

class CustomField(Decimal, ChoiceField): # MRO Decimal->Integer->ChoiceField->Field
    ...
class CustomWidget(NumberInput, Select):

但是,前提是该字段必须包含出现在选择列表中的内容。有一个方便的 valid_value 方法,您可以重写该方法以允许任何值,但是存在一个更大的问题-绑定到十进制模型字段。

从根本上讲,所有ChoiceField对象都管理值列表,然后具有一个或多个表示该选择的选择索引。因此绑定的数据将在小部件中显示为;

[some_data] or [''] empty value

因此,奥斯丁·福克斯(Austin Fox)重写了format_value方法,以返回到基本的Input类方法版本。适用于charfield,但不适用于Decimal或Float字段,因为我们丢失了数字小部件中的所有特殊格式。

所以我的解决方案是直接从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模板

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" %}>

使用示例。请注意,HTML数据列表选项标签不需要选择元组的第二个元素,因此我将其保留为None。同样,第一个元组值可以是文本或本地十进制数-您可以看到小部件如何处理它们。

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__'

答案 4 :(得分:2)

输入类型在选择字段和文本字段中是否相同?如果是这样,我会在类中创建一个CharField(或Textfield)并使用一些前端javascript / jquery通过应用&#34来处理将传递的数据;如果下拉列表中没有信息,则使用textfield中的数据&# 34;子句。

我制作了一个jsFiddle来演示如何在前端执行此操作。

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());
    }
};

然后,您可以在提交时指定将哪个值传递给您的视图。

答案 5 :(得分:0)

以下是我如何解决这个问题。我从传递给模板form对象中检索选项并手动填充datalist

{% 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 %}

答案 6 :(得分:0)

我知道这很老了,但认为这可能对其他人有用。 以下实现了与Viktor eXe's答案类似的结果,但适用于使用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>

现在您可以在表格中使用窗口小部件或字段了。

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

小部件和字段也可以在form.ModelForm Forms中使用,并且可以接受属性。

答案 7 :(得分:0)

参加聚会晚了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_js块,(在我的base.html模板中,其他所有东西都继承了)

<script type="text/javascript"> $(document).ready(  function() {
{% block onready_js %}{% endblock onready_js %}   
   }); 
</script>

打败我,为什么不是每个人都这样做一点JQuery!

答案 8 :(得分:0)

这里是可重用的模型字段级别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)

在“ my_custom_fields.py”中定义的DatalistCharField(custom)。 从下到上都很容易阅读。

(DatalistCharField> ListTextField> ListTextWidget)

(((模型。字段>表格。字段>表格。小部件)结构)

# '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)