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