How to render my select field with WTForms?
Asked Answered
D

2

5

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)
Delate answered 11/12, 2011 at 10:58 Comment(12)
Also what templating engine are you using to output this? YOu should need to write out all the option and select tags -- that's one of the points of using WTForms. You should be able to just do: ad_form = AdForm() and then in your HTML output just print the value of "ad_form.category" and "ad_form.name" and it'll return the underlying html widget for your particular selected field.Noyes
(Ran out of room), but here's an example using Jinja: {{ data.form.time(id="time", value=data.date.strftime("%H:%M"))}}. time is a TextField in the form, so this returns <input type="text" id="time" name="time" value="[current time formatted as HH:MM]"> You can also get a label for your field with {{ data.form.time.label }} which will return <label for="time'>Time</label>Noyes
Thank you @Noyes for the detailed answer and comments. I'm going to try coding it like you mention now. My templating engine is Jinja just like you mention.Delate
I had some problems adding the code, I think I must manipulate the tuples my_choices = [('1', _('VEHICLES')), ('2', _('Cars')), ('3', _('Motorcycles'))] and I don't really know how.Delate
The code below is expecting to receieve a four item tuple: (value, name, selected (True/False), disabled (True/False))Noyes
It works now after I changed some details and the code and used the Option class with a special widget. Thank you for helping me solving an important problem.Delate
This being really old, I doubt I'll get much help, but it would be nice to post what you did that got it working. I'm stuck where you were.Subsist
@Subsist iirc, I resorted to hardcoding the disabled fields. If we have advanced dropdowns, it can be better to render it with CSS and Js. You are welcome to my repository at github if you want to look closely at how I did it, or have a look at my result where I render the options using the semantic-ui CSS library at this wtforms-driven webpage.Delate
Cheers, in the end I did figure out how to get the code you were trying to work. I'll try to post a comment when I'm at my desk as to what I needed to change.Subsist
I'd love to look at the repo though, it seems like I could learn a lot from the rest of that form.Subsist
@Subsist Welcome..The repo is private but I might make it open source if I move some APi keys to the datastore. You are most welcome to become a project member if you let me know your github username, then I will add you as a project member with full access to the code.Delate
Thanks! Same username as here.Subsist
N
4

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

Noyes answered 11/12, 2011 at 12:1 Comment(4)
Thank you for the answer. I would also know a bit more about the details. I suppose I can do it all with Javascript but that is not ideal.Delate
I gave you two ways to do this: one to override the field and the widget in python by subclassing into a new field that allows disabled to be added to the tuple used for choices and then examples on how you'd control the display of the widget in CSS as well as as how to disable elements using javascript instead.Noyes
Thank you for the very helpful answer. It looks very interesting and something I must try at once.Delate
excellemt! wouldn't mind an accept if it works for you too :)Noyes
S
1

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.

Subsist answered 28/6, 2016 at 3:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.