Форма Django с вариантами, но также и с параметром freetext?

Что я ищу: Один виджет, который предоставляет пользователю выпадающий список вариантов, а затем также содержит текстовое поле ввода для ввода пользователем нового значения.

Бэкэнд-модель будет иметь набор вариантов по умолчанию (но не будет использовать ключевое слово выбора в модели). Я знаю, что могу (и у меня) реализовать это, получив форму как ChoicesField, так и CharField, и использовать код CharField, если ChoicesField остается по умолчанию, но это похоже на "un-django".

Есть ли способ (используя Django-builtins или Django-плагин), чтобы определить что-то вроде ChoiceEntryField (смоделированное после GtkComboboxEntry, которое IIRC делает) для формы?

В случае, если кто-либо найдет это, обратите внимание, что есть аналогичный вопрос о том, как наилучшим образом выполнять то, что я искал с точки зрения UX, в https://ux.stackexchange.com/info/85980/is-there-a-ux-pattern-for-drop-down-preferred-but-free-text-allowed

Ответ 1

Я бы порекомендовал собственный подход Widget, 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)

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

Ответ 2

Я знаю, что я немного опаздываю на вечеринку, но есть еще одно решение, которое я недавно использовал.

Я использовал Input виджет django-floppyforms с аргументом datalist. Это генерирует элемент HTML5 <datalist>, для которого ваш браузер автоматически создает список предложений (см. Также этот ответ SO).

Вот что может выглядеть тогда в виде модели:

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

Ответ 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

( Пример 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})

(Пример фиктивных представлений .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

Ответ 4

У меня было такое же требование, как OP, но с базовым полем было DecimalField. Таким образом, пользователь может ввести действительное число с плавающей запятой или выбрать из списка дополнительных вариантов.

Мне понравился ответ Остина Фокса в том смысле, что он следует структуре 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

Следовательно, Остин Фокс переопределяет метод format_value для возврата к версии базового метода класса Input. Работает для charfield, но не для полей Decimal или Float, потому что мы теряем все специальное форматирование в виджете чисел.

Таким образом, мое решение состояло в том, чтобы наследовать непосредственно от десятичного поля, но добавить только свойство выбора (снято с 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 datalist, поэтому я оставляю их как 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__'

Ответ 5

Будет ли тип ввода идентичным как в поле выбора, так и в текстовом? Если это так, я сделал бы один CharField (или Textfield) в классе и имел бы javascript/jquery переднего конца, заботясь о том, какие данные будут переданы, применяя предложение "если нет информации в выпадающем списке, используйте данные в текстовом поле".

Я сделал 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());
    }
};

вы можете, после отправки, указать, какое значение будет передано вашему представлению.

Ответ 6

Вот как я решил эту проблему. Я извлекаю варианты из переданного в объект 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 %}

Ответ 7

Я знаю, что это старый, но думал, что это может быть полезно для других. Следующие результаты аналогичны ответу Виктора eXe, но работают с моделями, наборами запросов и внешними ключами с помощью нативных методов django.

В Forms.py подкласс форм. Выберите и 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>

И теперь вы можете использовать виджет или поле в вашем 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

Виджет и поле также работают в form.ModelForm Forms и будут принимать атрибуты.

Ответ 8

На эту вечеринку я опоздал на 5 лет, но сам ответ @Foon OptionalChoiceWidget был именно тем, что я искал, и, надеюсь, другие люди, думающие задать тот же вопрос, будут направлены сюда алгоритмами поиска ответов Qaru, как и я.

Я хотел, чтобы поле ввода текста исчезло, если в выпадающем меню опций был выбран ответ, и это легко сделать. Поскольку это может быть полезно для других:

{% 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 таким образом!