如何用WTForms渲染选择字段?

5 投票
2 回答
18947 浏览
提问于 2025-04-17 08:09

我有一个下拉选择框,其中一些选项是灰色的并且无法选择,我想用WTForms来实现这个效果:

<select name="cg" id="cat" class="search_category">
<option value='' >{% trans %}All{% endtrans %}</option>  
<option value='' style='background-color:#dcdcc3' id='cat1'  disabled="disabled">-- {% trans %}VEHICLES{% endtrans %} --</option>
<option value='2'  {% if "2" == cg %} selected="selected" {% endif %} id='cat2' >{% trans %}Cars{% endtrans %}</option>
<option value='3' {% if "3" == cg %} selected="selected" {% endif %}   id='cat3' >{% trans %}Motorcycles{% endtrans %}</option>
<option value='4' {% if "4" == cg %} selected="selected" {% endif %}   id='cat4' >{% trans %}Accessories &amp; Parts{% endtrans %}</option>
...

我有一个可以正常工作的表单类,我开始实现一个本地化的分类变量,但我不知道怎么制作一个可以显示灰色背景(background-color:#dcdcc3)和禁用属性的选项元素的控件(widget):

class AdForm(Form):
    my_choices = [('1', _('VEHICLES')), ('2', _('Cars')), ('3', _('Bicycles'))]
    name = TextField(_('Name'), [validators.Required(message=_('Name is required'))], widget=MyTextInput())
    title = TextField(_('title'), [validators.Required(message=_('Subject is required'))], widget=MyTextInput())
    text = TextAreaField(_('Text'),[validators.Required(message=_('Text is required'))], widget=MyTextArea())
    phonenumber = TextField(_('Phone number'))
    phoneview = BooleanField(_('Display phone number on site'))
    price = TextField(_('Price'),[validators.Regexp('\d', message=_('This is not an integer number, please see the example and try again')),validators.Optional()] )
    password = PasswordField(_('Password'),[validators.Optional()], widget=PasswordInput())
    email = TextField(_('Email'), [validators.Required(message=_('Email is required')), validators.Email(message=_('Your email is invalid'))], widget=MyTextInput())
    category = SelectField(choices = my_choices, default = '1')

    def validate_name(form, field):
        if len(field.data) > 50:
            raise ValidationError(_('Name must be less than 50 characters'))

    def validate_email(form, field):
        if len(field.data) > 60:
            raise ValidationError(_('Email must be less than 60 characters'))

    def validate_price(form, field):
        if len(field.data) > 8:
            raise ValidationError(_('Price must be less than 9 integers'))

我可以使用上面的分类变量来渲染一个分类的下拉选择框。我还想实现特殊的渲染效果,也就是禁用的元素和灰色背景。你能告诉我该怎么做吗?

谢谢你

更新

在尝试回答中的解决方案以添加禁用属性时,我收到了这个错误信息:

Trace:

Traceback (most recent call last):
  File "/media/Lexar/montao/lib/webapp2/webapp2.py", line 545, in dispatch
    return method(*args, **kwargs)
  File "/media/Lexar/montao/montaoproject/i18n.py", line 438, in get
    current_user=self.current_user,
  File "/media/Lexar/montao/montaoproject/main.py", line 469, in render_jinja
    self.response.out.write(template.render(data))
  File "/media/Lexar/montao/montaoproject/jinja2/environment.py", line 894, in render
    return self.environment.handle_exception(exc_info, True)
  File "/media/Lexar/montao/montaoproject/templates/insert_jinja.html", line 221, in top-level template code
    {{ form.category|safe }}
ValueError: need more than 2 values to unpack

我尝试的代码是:

from wtforms.widgets import html_params
class SelectWithDisable(object):
    """
    Renders a select field.

    If `multiple` is True, then the `size` property should be specified on
    rendering to make the field useful.

    The field must provide an `iter_choices()` method which the widget will
    call on rendering; this method must yield tuples of 
    `(value, label, selected, disabled)`.
    """
    def __init__(self, multiple=False):
        self.multiple = multiple

    def __call__(self, field, **kwargs):
        kwargs.setdefault('id', field.id)
        if self.multiple:
            kwargs['multiple'] = 'multiple'
        html = [u'<select %s>' % html_params(name=field.name, **kwargs)]
        for val, label, selected, disabled in field.iter_choices():
            html.append(self.render_option(val, label, selected, disabled))
        html.append(u'</select>')
        return HTMLString(u''.join(html))

    @classmethod
    def render_option(cls, value, label, selected, disabled):
        options = {'value': value}
        if selected:
            options['selected'] = u'selected'
        if disabled:
            options['disabled'] = u'disabled'
        return HTMLString(u'<option %s>%s</option>' % (html_params(**options), escape(unicode(label))))


class SelectFieldWithDisable(SelectField):
    widget = SelectWithDisable()

    def iter_choices(self):
        for value, label, selected, disabled in self.choices:
            yield (value, label, selected, disabled, self.coerce(value) == self.data)


class AdForm(Form):
    my_choices = [('1', _('VEHICLES')), ('2', _('Cars')), ('3', _('Motorcycles'))]
    nouser = HiddenField(_('No user'))
    name = TextField(_('Name'), [validators.Required(message=_('Name is required'))], widget=MyTextInput())
    title = TextField(_('Subject'), [validators.Required(message=_('Subject is required'))], widget=MyTextInput())
    text = TextAreaField(_('Text'),[validators.Required(message=_('Text is required'))], widget=MyTextArea())
    phonenumber = TextField(_('Phone number'))
    phoneview = BooleanField(_('Display phone number on site'))
    price = TextField(_('Price'),[validators.Regexp('\d', message=_('This is not an integer number, please see the example and try again')),validators.Optional()] )
    password = PasswordField(_('Password'),validators=[RequiredIf('nouser', message=_('Password is required'))], widget=MyPasswordInput())
    email = TextField(_('Email'), [validators.Required(message=_('Email is required')), validators.Email(message=_('Your email is invalid'))], widget=MyTextInput())
    category = SelectFieldWithDisable(choices = my_choices)

    def validate_name(form, field):
        if len(field.data) > 50:
            raise ValidationError(_('Name must be less than 50 characters'))

    def validate_email(form, field):
        if len(field.data) > 60:
            raise ValidationError(_('Email must be less than 60 characters'))

    def validate_price(form, field):
        if len(field.data) > 8:
            raise ValidationError(_('Price must be less than 9 integers'))

我想我必须在某个地方设置“禁用”属性,但我该在哪里设置呢?

更新 2

这比我想的要复杂。我在wtforms邮件列表上看到有一个建议的解决方案,但我也没能让它工作(出现了一些关于语法错误和无法从wtforms导入ecscape的简单错误,所以我采取的措施是从hg仓库更新我的wtforms,看看那里是否有什么重要的变化)。

根据这里的回答,我要么得到Need more than 2 values to unpack,要么得到ValueError: too many values to unpack,所以我似乎无法正确实现。在我的模板中,我想渲染的是

{{ form.category }} 

而我的表单类是

class AdForm(Form):
    my_choices = [('1', _('VEHICLES'), False, True), ('2', _('Cars'), False, False), ('3', _('Motorcycles'), False, False)]

    ...
    category = SelectFieldWithDisable(choices = my_choices)

我从这里获得的附加类是:

class SelectWithDisable(object):
    """
    Renders a select field.

    If `multiple` is True, then the `size` property should be specified on
    rendering to make the field useful.

    The field must provide an `iter_choices()` method which the widget will
    call on rendering; this method must yield tuples of 
    `(value, label, selected, disabled)`.
    """
    def __init__(self, multiple=False):
        self.multiple = multiple

    def __call__(self, field, **kwargs):
        kwargs.setdefault('id', field.id)
        if self.multiple:
            kwargs['multiple'] = 'multiple'
        html = [u'<select %s>' % html_params(name=field.name, **kwargs)]
        for val, label, selected, disabled in field.iter_choices():
            html.append(self.render_option(val, label, selected, disabled))
        html.append(u'</select>')
        return HTMLString(u''.join(html))

    @classmethod
    def render_option(cls, value, label, selected, disabled):
        options = {'value': value}
        if selected:
            options['selected'] = u'selected'
        if disabled:
            options['disabled'] = u'disabled'
        return HTMLString(u'<option %s>%s</option>' % (html_params(**options), escape(unicode(label))))


class SelectFieldWithDisable(SelectField):
    widget = SelectWithDisable()

    def iter_choices(self):
        for value, label, selected, disabled in self.choices:
            yield (value, label, selected, disabled, self.coerce(value) == self.data)

2 个回答

1

过了一段时间,我终于弄明白了如何让@tkone的回答中的wtform部分正常工作。因为这个内容太长,不能放在评论里,所以我决定写个回答。另外,我是用SelectMultipleField来做这个,所以我的字段类是从这个类继承的,而不是SelectField

首先是小部件类:

class SelectWithDisable(object):
    """
    Renders a select field.

    If `multiple` is True, then the `size` property should be specified on
    rendering to make the field useful.

    The field must provide an `iter_choices()` method which the widget will
    call on rendering; this method must yield tuples of
    `(value, label, selected, disabled)`.
    """
    def __init__(self, multiple=False):
        self.multiple = multiple

    def __call__(self, field, **kwargs):
        kwargs.setdefault('id', field.id)
        if self.multiple:
            kwargs['multiple'] = 'multiple'
            kwargs['size'] = len(field.choices) if len(field.choices) < 15 else 15
        html = [u'<select %s>' % widgets.html_params(name=field.name, **kwargs)]
        for val, label, selected, disabled, coerced_value in field.iter_choices():
            html.append(self.render_option(val, label, selected, disabled))
        html.append(u'</select>')
        return widgets.HTMLString(u''.join(html))

    @classmethod
    def render_option(cls, value, label, selected, disabled):
        options = {'value': value}
        if selected:
            options['selected'] = u'selected'
        if disabled:
            options['disabled'] = u'disabled'
        return widgets.HTMLString(u'<option %s>%s</option>' % (widgets.html_params(**options), escape(unicode(label))))

这里唯一的变化是,我在forms.py的顶部加了from wtforms import widgets,这样我就可以用widgets.HTMLString来引用小部件等。我还在这里加了一个size参数,可能更好放在别的地方,这个参数的作用是设置元素的大小,大小要么是元素的数量,要么是15,取二者中较小的。我把这个放在了if self.multiple里面,以提醒自己如果以后在其他地方使用这个小部件时要重新检查一下大小的问题。

接下来是字段类:

class SelectMultipleFieldWithDisable(SelectMultipleField):
    widget = SelectWithDisable(multiple=True)

    def iter_choices(self):
        for value, label, selected, disabled in self.choices:
            yield (value, label, selected, disabled)

这里做了所有重要的修改。首先,正如之前提到的,这个字段是从SelectMultipleField类继承的,所以我在小部件声明中加了multiple=True这个参数。最后,我从iter_choices方法中去掉了最后一个元素(self.coerce(value) == self.data)。我不太确定这个是干嘛用的,但在我的情况下,它总是把一个整数和一个列表进行比较,结果返回False,这导致了

ValueError: Too many values to unpack

Need more than x values to unpack

这些错误,正是OP遇到的问题。如果这个返回了有用的东西,只需在小部件类的call方法中的for语句里加上那个额外的变量。

然后在定义选项时,我只需要把每个项目的选项元组设置为(value, label, selected, disabled),其中selected和disabled是布尔值,分别表示这个项目是否应该被选中和禁用。

希望这能帮助到曾经像我一样迷茫的人。

4

编辑:

如果你想让某个字段总是以特定的选项禁用状态显示,你需要创建一个自己的自定义小部件和字段,然后提供给渲染器。

目前的渲染器只接受三种选项,分别是:(值, 名称, 选中状态)

你需要修改它,让它接受一个第四个可选元素:禁用状态。

这个是基于wtforms.widget中的Select类:

class SelectWithDisable(object):
    """
    Renders a select field.

    If `multiple` is True, then the `size` property should be specified on
    rendering to make the field useful.

    The field must provide an `iter_choices()` method which the widget will
    call on rendering; this method must yield tuples of 
    `(value, label, selected, disabled)`.
    """
    def __init__(self, multiple=False):
        self.multiple = multiple

    def __call__(self, field, **kwargs):
        kwargs.setdefault('id', field.id)
        if self.multiple:
            kwargs['multiple'] = 'multiple'
        html = [u'<select %s>' % html_params(name=field.name, **kwargs)]
        for val, label, selected, disabled in field.iter_choices():
            html.append(self.render_option(val, label, selected, disabled))
        html.append(u'</select>')
        return HTMLString(u''.join(html))

    @classmethod
    def render_option(cls, value, label, selected, disabled):
        options = {'value': value}
        if selected:
            options['selected'] = u'selected'
        if disabled:
            options['disabled'] = u'disabled'
        return HTMLString(u'<option %s>%s</option>' % (html_params(**options), escape(unicode(label))))

然后根据wtforms.fields中的代码,继承已经存在的SelectField。

class SelectFieldWithDisable(SelectFiel):
    widget = widgets.SelectWithDisable()

    def iter_choices(self):
        for value, label, selected, disabled in self.choices:
            yield (value, label, selected, disabled, self.coerce(value) == self.data)

注意:这段代码没有经过测试,也没有运行过Python代码,只是根据问题和WTFORMS的底层代码快速写的一个小技巧。但这应该能给你一个不错的起步,加上之前的回答,完全控制这个字段。

使用CSS和JavaScript来控制页面上渲染的元素。

在你使用的任何模板渲染系统中(我用的是flask、jinja和wtforms),你渲染你的元素时要提供一个id或class属性。(我只是打印了form.select_field_variable_name

然后生成一个CSS文件来控制你的样式,并使用JavaScript来控制某些元素的自定义禁用等。

编辑:

如果你有:

<select id=selector>
    <option id=value1 value=1>Bananas</option>
    <option id=value2 value=2>Corn</option>
    <option id=value3 value=3>Lolcats</option>
</select>

你可以用以下代码应用背景颜色:

<style>
#selector {background-color: #beef99}
</style>

你可以用以下代码来启用/禁用:

<script>
option = document.getElementById('value3')
option.disabled = true
</script>

等等等等。

一旦你使用WTForms小部件渲染了你的元素,就像所有HTML元素一样,你应该用CSS和JavaScript来设置样式和控制元素的动态部分。

撰写回答