Programmer 400 Programmer 400 - 5 months ago 322
Python Question

How to render my select field with WTForms?

I have a select field that has certain elements faded and disabled that I would like to render with 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>
...


I have a form class that works and I started to implement to localized category variable but I don't know how to make the widget(?) that renders the faded (
background-color:#dcdcc3
) and the disabled attributes to an option element:

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


I can use the variable category from above to render a select for the categories. I also want to enable the special rendering ie disabled elements and faded background. Can you tell me how I should do?

Thank you

Update



When trying the solution from the answer to add the disabled attribute, I get this error message:

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


The code I tried was:

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


I guess I must set the 'disabled' attribute somewhere but where?

Update 2



This was trickier than I thought. There was also a solution suggested on the wtforms mailing list but I couldn't get that to work either (some trivial error about invalid syntax and not being able to import ecscape from wtforms so the action I took was updating my wtforms from the hg repository if something important changed there.

From the answer here I either get
Need more than 2 values to unpack
or
ValueError: too many values to unpack
so I canät seem to get it right. In my template what I'm trying to render is

{{ form.category }}


and my form class is

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

...
category = SelectFieldWithDisable(choices = my_choices)


with the added classes I got from here:

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)

Answer

EDIT:

If you want to always render the field with certain options disabled you'll have to create your own custom widget and field to provide to the renderer.

The current renderer only takes three options in it's choice tuple: (value, name, selected).

You'll need to modify that to accept a fourth optional element: disabled.

Based on the Select class in wtforms.widget:

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

And then based on the code in wtforms.fields, subclass the SelectField that already exists

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)

NOTE: THIS IS NOT TESTED NOR EVEN RUN PYTHON CODE BUT A VERY QUICK HACK GIVEN THE QUESTION AND THE UNDERLYING CODE FROM WTFORMS. But it should give you enough of a head start along with the previous answer to control the field entirely.

Use CSS and JavaScript to control the rendered element on the page.

In whatever template rendering system your using (I'm using flask, jinja and wtforms) you render your elemen and provide an id or class attribute when you render it. (I'm just printing form.select_field_variable_name)

Then generate a CSS file to control your styling and use JavaScript to control custom disabling of certain elements, etc.

EDIT:

If you've got:

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

You can apply a background color with:

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

And you enable/disable with:

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

Etc, etc etc.

Once you've got your element rendered using WTForms widgets, like all HTML elements, you should style and control any dynamic parts of the element with CSS and JavaScript

Comments