Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DataRequired and InputRequired do not add required attribute to RadioField objects #477

Closed
chivalry opened this issue Mar 20, 2019 · 9 comments · Fixed by #615
Closed

DataRequired and InputRequired do not add required attribute to RadioField objects #477

chivalry opened this issue Mar 20, 2019 · 9 comments · Fixed by #615
Labels
bug Unexpected behavior

Comments

@chivalry
Copy link

chivalry commented Mar 20, 2019

When adding a DataRequired or InputRequired validator to a RadioField, the required attribute of the input tag.

Here's the function I wrote that reveals the problem:

    @bp.route('/<string:slug>/survey', methods=['GET', 'POST'])
    def survey(slug):
        survey = Survey.query.filter_by(slug=slug).first_or_404()
        questions = survey.questions
        for question in questions:
            if question.category == 'word':
                field = StringField(question.question, validators=[InputRequired()])
            elif question.category == 'likert':
                choices = [('1', 'Strongly Agree'),
                           ('2', 'Agree'),
                           ('3', 'Neutral'),
                           ('4', 'Disagree'),
                           ('5', 'Strongly Disagree')]
                field = RadioField(question.question, choices=choices, validators=[InputRequired()])
            setattr(FlaskForm, str(question.id), field)
        setattr(FlaskForm, 'submit', SubmitField('Submit'))
        form = FlaskForm()
        if form.validate_on_submit():
            response = Response(survey=survey)
            db.session.add(response)
            for question in questions:
                answer = Answer(question=question, response=response)
                field = getattr(form, str(question.id))
                if question.category == 'word':
                    answer.answer = field.data
                elif question.category == 'likert':
                    choices = {'1': 'Strongly Agree',
                               '2': 'Agree',
                               '3': 'Neutral',
                               '4': 'Disagree',
                               '5': 'Strongly Disagree'}
                    answer.answer = choices[field.data]
                db.session.add(answer)
            db.session.commit()
            with open('app/static/ty.txt', 'r') as f:
                ty = [x.strip() for x in f.readlines()]
            return render_template('ty.html', ty=ty)
        return render_template('survey.html', form=form, questions=questions)

I understand there might be other issues with the above (such as me being informed I should subclass FlaskForm).

The above function gives the following input tag for a StringField:

    <input class="form-control" id="4" name="4" required="" type="text" value="">

This is the input tag for a RadioField:

    <input id="1-0" name="1" type="radio" value="1">

While in the browser's debugger, if I edit the above to <input id="1-0" name="1" type="radio" value="1" required="">, the radio button requires a selection.

@enthalpychange
Copy link

enthalpychange commented Mar 27, 2019

I am seeing the same issue.

from wtforms import Form, StringField, RadioField
from wtforms.validators import InputRequired

COLORS = [
    ('blue', 'Blue'),
    ('red', 'Red')
]

class MyForm(Form):
    name = StringField('name', validators=[InputRequired()])
    color = RadioField('color', choices=COLORS, validators=[InputRequired()])

form = MyForm()

for field in form._fields.keys():
    print(getattr(form, field))

Output:

<input id="name" name="name" required type="text" value="">
<ul id="color"><li><input id="color-0" name="color" type="radio" value="blue"> <label for="color-0">Blue</label></li><li><input id="color-1" name="color" type="radio" value="red"> <label for="color-1">Red</label></li></ul>

@ftm
Copy link
Contributor

ftm commented May 8, 2019

Thanks for raising this issue, I can confirm I get the same problem as well. I've done a bit of digging and it seems as if the validator passed in to RadioField does not get propagated to the underlying SelectFieldBase._Option field that is actually used to render the radio buttons.

The code in question is this function:

def __iter__(self):
opts = dict(
widget=self.option_widget, _name=self.name, _form=None, _meta=self.meta
)
for i, (value, label, checked) in enumerate(self.iter_choices()):
opt = self._Option(label=label, id="%s-%d" % (self.id, i), **opts)
opt.process(None, value)
opt.checked = checked
yield opt

You can see that the opts dictionary does not contain an entry for the fields validators so they never get passed to the RadioInputs that actually get rendered.

A proposed fix would be to simply propagate the validators down to these subfields in the same manner:

opts = dict(
    widget=self.option_widget,
    validators=self.validators,
    _name=self.name,
    _form=None,
    _meta=self.meta,
)

This produces the desired results:

<ul id="color">
  <li>
    <input id="color-0" name="color" required type="radio" value="blue">
    <label for="color-0">Blue</label>
  </li>
  <li>
    <input id="color-1" name="color" required type="radio" value="red">
    <label for="color-1">Red</label>
  </li>
</ul>

@ftm ftm added the bug Unexpected behavior label May 8, 2019
@chivalry
Copy link
Author

chivalry commented May 11, 2019

I'll make your suggested correction, give it some tests and offer it as a pull request.

In the meantime, I've been using this:

class FieldsRequiredForm(FlaskForm):
    """Require all fields to have content. This works around the bug that WTForms radio
    fields don't honor the `DataRequired` or `InputRequired` validators.
    """

    class Meta:
        def render_field(self, field, render_kw):
            render_kw.setdefault('required', True)
            return super().render_field(field, render_kw)

I don't think I came up with it, but probably found it on Stack Overflow. I'd enjoy your opinion of it.

@jpgerber
Copy link

This is the best fix, thanks so much for it. I've searched everywhere for something like it (radio buttons still don't validate in Feb 2020). For those who need advice on how to use it...

Put the FieldsRequiredForm as a class in your forms.py file and then make each form with a radio button inherit the class FieldsRequiredForm e.g. def RadioForm(FieldsRequiredForm):

@tvb
Copy link

tvb commented Apr 3, 2020

I just ran into this as well, not sure what to proposed fix is?

@jpgerber
Copy link

jpgerber commented Apr 4, 2020

As above,
"Put the FieldsRequiredForm as a class in your forms.py file and then make each form with a radio button inherit the class FieldsRequiredForm e.g. def RadioForm(FieldsRequiredForm): "

@danielrosenbaum
Copy link

Thank you very much for this quick fix and explanation!

@dneyirp
Copy link

dneyirp commented Oct 26, 2020

I'll make your suggested correction, give it some tests and offer it as a pull request.

In the meantime, I've been using this:

class FieldsRequiredForm(FlaskForm):
    """Require all fields to have content. This works around the bug that WTForms radio
    fields don't honor the `DataRequired` or `InputRequired` validators.
    """

    class Meta:
        def render_field(self, field, render_kw):
            render_kw.setdefault('required', True)
            return super().render_field(field, render_kw)

I don't think I came up with it, but probably found it on Stack Overflow. I'd enjoy your opinion of it.

Hi.

This fix partially worked for me but I ran into a problem that all fields then became 'required' so no 'optional' fields.

I modified this to :

class FieldsRequiredForm(FlaskForm):
    """Require all fields to have content. This works around the bug that WTForms radio
    fields don't honor the `DataRequired` or `InputRequired` validators.
    """

    class Meta:
        def render_field(self, field, render_kw):
            if field.type == "_Option":
                render_kw.setdefault("required", True)
            return super().render_field(field, render_kw)

This will look for specifically "_Option" fields like radio buttons and make them required.

Usage example:

class TransactionEntryForm(FieldsRequiredForm):
    transactionType = RadioField(
        "Entry Type",
        [validators.Required("Type of entry is required")],
        choices=[("input", "Input"), ("output", "Output"), ("change", "Change")],
    )
    date = StringField(
        "Date", [validators.Required("Date of entry, e.g. 2020-10-28")], default=datetime.now().strftime("%Y-%m-%d")
    )
    description = StringField("Description", [validators.Required("Description for entry")])
    amount = DecimalField("Amount", [validators.Required("Enter amount, e.g. 12345.67")])
    submit = SubmitField("Submit")

@MarlonMrN

This comment has been minimized.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Unexpected behavior
Development

Successfully merging a pull request may close this issue.

8 participants