Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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)
like image 519
Niklas Rosencrantz Avatar asked Dec 11 '11 10:12

Niklas Rosencrantz


2 Answers

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

like image 76
tkone Avatar answered Sep 21 '22 18:09

tkone


Long after the fact, I've gone in and figured out how to make the wtform part of @tkone's answer work. I'll add an answer to this since it won't fit in a comment. Additionally, I was trying to do this with a SelectMultipleField so my field class is inheriting from that instead of SelectField

First the widget class:

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

The only change of import here is that I have from wtforms import widgets at the top of my forms.py so I refer to the widgets using widgets.HTMLString, etc. I also added a size argument in here, that perhaps would better be implemented somewhere else, that just sets the size of the element to the number of elements or 15, whichever is lower. I've stuck that inside the if self.multiple to remind myself to go reexamine the size thing if I start using this widget in other ways.

Now the field class:

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

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

This is where all the important changes were made. First as mentioned earlier, the field is inheriting from the SelectMultipleField class, so I add the multiple=True argument to the widget declaration. Finally, I remove the last element from the iter_choices method (self.coerce(value) == self.data). I'm not really sure what that was supposed to do, but in my case it always compared an integer to a list and returned False, and resulted in the

ValueError: Too many values to unpack

and

Need more than x values to unpack

error OP was seeing. If it is returning something valuable, just add that extra variable to the for statement in the call method of the widget class.

Then when I'm defining the choices I just need to set the choices tuple for each item to be (value, label, selected, disabled) where selected and disabled are boolean values indicating whether the item should be selected and disabled respectively.

I hope that helps someone as lost as I was at some point down the line.

like image 28
UltraBob Avatar answered Sep 22 '22 18:09

UltraBob