16

I have some fields in page disabled as for example:(using jinja2 templating system)

<html>
<body>
<form action="" method=POST>
    {{ form.name(disabled=True) }}
    {{ form.title }}
    -- submit button --
</form>
</body>
</html>

Field is disabled in the form as expected.

In my views.py: On doing validate_on_submit() on form submit, it fails with validation error on 'name' field which is disabled. I was hoping that validation ignores disabled field. Is it the right behaviour? If so, can you please let know how to handle such a case?

Updated:

class TeamForm(wtf.Form):
    name = wtf.TextField("Team Name", validators=[validators.Required()])
    title = wtf.TextField("Title", validators=[validators.Required()])
Andrew
  • 366
  • 2
  • 8
rajpy
  • 2,436
  • 5
  • 29
  • 43
  • can you share your forms code please ? – codegeek May 10 '13 at 13:24
  • @codegeek: Updated with forms code. – rajpy May 10 '13 at 13:40
  • Can you provide some background on what your goal is? `name` is a required field, but you aren't allowing a value to be provided. Are you planning to inject one programmatically? – dirn May 10 '13 at 13:53
  • I have two kind of users, admin and manager. Admin only can change 'name' field. For manager, I just want to show 'name' as disabled, but manager can edit/change other fields. I want validate_on_submit() to ignore disabled fields, so that update from manger on other fields should work, – rajpy May 10 '13 at 14:00

4 Answers4

30

This is actually an interesting problem, and the way WTForms solves it is intentionally something that requires explicitness, because it has to do with security and not allowing users to fake input.

So the intent is, that "managers" cannot edit the name, while "admins" can.

At first glance this seems obvious, just disable the field in HTML, and write your view like this:

def edit_team():
    form = TeamForm(request.POST, obj=team)
    if request.POST and form.validate():
        form.populate_obj(team) # <-- This is the dangerous part here
        return redirect('/teams')
    return render('edit_team.html')

As written, this is a major security risk, because the disabled property in HTML forms is client-side only. Anyone with an HTML inspector (ie FireBug, webkit document inspector, etc) can remove this property, or someone could simply make a request like so:

POST /edit_team/7 HTTP/1.0
Content-Type: application/x-urlencoded

team=EVILTEAMNAME&title=foo

The issue then is of course, how do we gate this properly on the server-side, corresponding to the appropriate way of doing this? The correct approach with WTForms is to not have the field in the first place. There's a few ways to do this, one is to use form composition and have e.g. ManagerTeamForm and AdminTeamForm (sometimes this is better) but other times it's easier to use del to remove specific fields.

So here's how you would write your view, and not have the validation issues:

def edit_team():
    form = TeamForm(request.POST, obj=team)
    if user.role == 'manager':
        del form.name
    if request.POST and form.validate():
        form.populate_obj(team)
        return redirect('/teams')
    return render('edit_team.html')

And a quick modification to the template:

<html>
<body>
<form action="" method=POST>
    {% if 'name' in form %}
        {{ form.name() }}
    {% else %}
        {{ team.name|e }}
    {% endif %}
    {{ form.title }}
    -- submit button --
</form>
</body>
</html>

Some pieces of reference for wtforms best-practices:

Community
  • 1
  • 1
Crast
  • 15,996
  • 5
  • 45
  • 53
  • Thanks for the detailed explanation.I ended up in deleting form fields as you said. But ideally 'disabled' field will not be sent for form processing. Isn't it right? – rajpy May 16 '13 at 05:13
  • WTForms doesn't interpret display attributes in your template like disabled, display, visibility, readonly, etc (there's so many and they're so varied), also consider that rendering happens after validation (and rendering generally doesn't happen if the form validates) so the only way to tell wtforms it's not to be looked at is to delete the field. – Crast May 20 '13 at 22:28
  • 2
    @rajpy, perhaps "disabled" is a misnomer. The attribute controls whether the user is allowed to change the value--not whether it actually affects the behavior on the server. Sometimes it is helpful to show the user what data is being sent to the server but not allow them to change the value. – MageWind Jul 21 '14 at 21:27
3

You need to make the name field optional when defining the form.

name = wtf.TextField("Team Name", validators=[validators.Optional()])

Then in your views, pass a variable called "role" and set it to either manager or admin depending on the user.

<form action="" method=POST>
{% if role == 'manager' % }
    {{ form.name(disabled=True) }}
{% else % }
    {{ form.name() }}
{{ form.title }}
-- submit button --
</form>
codegeek
  • 32,236
  • 12
  • 63
  • 63
  • In this case, it will work. But I want name to be supplied by admin while creating team and it shouldn't be optional. – rajpy May 10 '13 at 14:08
  • 2
    This is also a security risk, because a mailicious 'manager' could still submit the data, even if the field is disabled client-side, and anyone is allowed to clear out the data. – Crast May 15 '13 at 22:34
  • It works but make a security validation before saving the values. – Thiago Aug 25 '14 at 06:59
  • This is the approach I took. I have a few lines to manually validate the data in my view code and some JS code to disable/enable the field on the client side based on the value of other fields. – Corey Jun 08 '23 at 14:19
1

I defined my own validator for this problem:

from wtforms.validators import Optional

class OptionalIfDisabled(Optional):

    def __call__(self, form, field):
        if field.render_kw is not None and field.render_kw.get('disabled', False):
            field.flags.disabled = True
            super(OptionalIfDisabled, self).__call__(form, field)

And then I defined a new base for my forms:

from wtforms.form import Form

class BaseForm(Form):

    def populate_obj(self, obj):
        for name, field in self._fields.items():
            if not field.flags.disabled:
                field.populate_obj(obj, name)

Now every form can extend the BaseForm and disable fields like this:

from wtforms.fields import StringField, SubmitField

class TeamForm(BaseForm):
    team = StringField(label='Team Name', 
                       validators=[OptionalIfDisabled(), InputRequired()]
    submit = SubmitField(label='Submit')

    def __init__(self, *args, **kwargs):
        super(TeamForm, self).__init__(*args, **kwargs)
        # disable the fields if you want to
        if some_condition:
            self.team.render_kw = {'disabled': True}

After validation of the TeamForm, you can use populate_obj to copy the enabled form data in any object. It will ignore the disabled fields.

Y4sper
  • 107
  • 1
  • 9
1
  1. Create a custom validator
from wtforms.validators import Optional
class DisabledValidator(Optional):
    """
    do nothing
    """
    pass
  1. Let's create a custom rule basing on the form.rule
from flask_admin.form.rules import Field
class EasyCustomFieldRule(Field):
    def __init__(self, field_name, render_field='lib.render_field', field_args={}):
        super(self.__class__, self).__init__(field_name, render_field)
        self.extra_field_args = field_args

    def __call__(self, form, form_opts=None, field_args={}):
        field = getattr(form, self.field_name)
        if self.extra_field_args.get('disabled'):
            field.validators.append(DisabledValidator())

        field_args.update(self.extra_field_args)
        return super(self.__class__, self).__call__(form, form_opts, field_args)
  1. Override write some functions of wtforms.form
from wtforms.form import Form
from wtforms.compat import iteritems
class BaseForm(Form):
    """
    重写部分方法,以适应disabled的Field
    """

    def validate(self):
        """
        Validates the form by calling `validate` on each field, passing any
        extra `Form.validate_<fieldname>` validators to the field validator.
        """
        extra = {}
        for name in self._fields:
            inline = getattr(self.__class__, 'validate_%s' % name, None)
            if inline is not None:
                extra[name] = [inline]

        return self.validate_(extra)

    def validate_(self, extra_validators=None):
        self._errors = None
        success = True
        for name, field in iteritems(self._fields):
            is_disabled = False
            for v in field.validators:
                if isinstance(v, DisabledValidator):
                    field.flags.disabled = True
                    is_disabled = True
                    break
            if is_disabled:
                continue

            if extra_validators is not None and name in extra_validators:
                extra = extra_validators[name]
            else:
                extra = tuple()
            if not field.validate(self, extra):
                success = False
        return success

    def populate_obj(self, obj):
        for name, field in self._fields.items():
            if not field.flags.disabled:
                field.populate_obj(obj, name)
  1. set the form_base_class in your ModelView, and set the form_edit_rules or the form_create_rules with EasyCustomFieldRule
from flask_admin.contrib.sqla import ModelView
class MyTestModelView(ModelView):
    ...
    form_base_class = BaseForm
    ...

    form_edit_rules = (
        EasyCustomFieldRule('column0', field_args={'disabled': True}),
        'column1', 'column2'
    )
  1. Just testing...