Open davidism opened 6 years ago
This is more broadly important if we want to validate other data sources such as JSON, or PATCH requests, which do not have the same constraints and behaviors as HTML forms.
We could check if any of the form keys match any of the field names before processing. But this is expensive and unnecessary for the most common case of a single form. It's also incorrect for validation, where "no form data" might legitimately be a validation issue.
Alternatively, we could have a Form.is_submitted(formdata)
method which just returns True
by default, or True
if any keys match submit field names and values. Flask-WTF already has this concept, although it's not implemented the way I just described, and it only applies to validation right now, not processing.
The most radical change would be to stop considering different data sources differently. Remove process_formdata
and only use process_data
. Combine data in layers, like form (incoming) data > keyword args > data dict > object > field defaults
and process it once. However, I want to fully understand why it's not done this way (there's an FAQ entry about it and multiple issues have been closed because of it) before we do this.
I'm not super familiar with PATCH requests (or if there's even a standard), but it wouldn't be a full solution for patching because it wouldn't handle merging lists or nested data, only overwriting it completely.
It is logical error. Doc https://wtforms.readthedocs.io/en/stable/forms.html The Form class -> class wtforms.form.Form -> Construction init(formdata=None, obj=None, prefix='', data=None, meta=None, **kwargs):
formdata – Used to pass data coming from the enduser, usually request.POST or equivalent. formdata should be some sort of request-data wrapper which can get multiple parameters from the form input, and values are unicode strings, e.g. a Werkzeug/Django/WebOb MultiDict
MultiDict is not None. MultiDict can be empty dict. Flask used Werkzeug as wrappers standart function.
Example:
@app.route('/url', methods=['GET', 'POST'])
def url_func():
form = UserForm(request.form, data={'var': 'val'})
if request.method=='POST':
...
return 'POST request'
request 'GET request. var={}'.format(form.var.data)
And GET request.form is type MultiDict (or ImmutableMultiDict) and equal empty list (is not None)
See the rest of my post, I address why truth testing instead of none doesn't solve the issue.
My english is bad. Do I need to suggest a solution? Or the solution is already found and will be in the next version?
It sounds like this issue should be tackled for 3.0. What do you think?
I would like to get something into 3.0, but as I've said above I don't know what that would look like. I don't think there's going to be a 100% satisfying solution to this.
We need to come up with a policy for data processing and apply it consistently. We need to keep in mind that WTForms is intended first for processing HTML form data, and account for the fact that HTML form data is ambiguous in some cases such as boolean, multi-select, and file fields.
There are multiple types of data:
formdata is None
) - #280 started using this instead of bool(formdata)
to decide to process data, since it's more accurate in indicating that form data wasn't submitted.bool(formdata)
) - This is unreliable if other form data is submitted such as API tokens, or if one form is submitted on a page with multiple forms. Technically this is true of formdata is None
too, but I think None
is a better indication that this isn't a post request.partial=True
flag on the form uses default values if the key is missing. This can come later, but should be kept in mind at least.StringField
, where the value might be ''
or None
depending on what was passed in. I think 3.0 should consistently set values to None
unless a value was actually given (for all fields, not just string).default=
for fields, or obj
, data
, and kwargs
for forms, is only used when no form data at all is provided. If this wasn't the case, there would be no way to submit an empty value for a field if it also had a default. It's also confusing which of obj
, data
, and kwargs
takes precedence. I think the way forward here is better documentation, no behavior changes.There are multiple places where processing happens:
data
in a validator instead, since that API is more well known.This post initially started as me collecting all the discussions once again, but turned into a big dump of my current thoughts on behavior of the entire processing stack. The amount of information here might make the situation look worse than it is. WTForms is already fairly consistent and works, but there's a lot of little inconsistencies and ambiguities that can be addressed.
Further down the line, a solution to ambiguous fields could be to render a hidden input with some sentinel value in order to force a submitted form to send a value for the field. For example a boolean field could render a hidden input with "value=unchecked" and then a checkbox with "value=true". If the list of values only contains the sentinel, it is false, otherwise it is true. If the name is not in the form data at all, we now know it was not sent and can safely assume the default. Similar schemes can be used for other field types. I'm not sure exactly how this would work, fields right now only render one input, and hidden fields are usually rendered in one block with form.hidden_field()
.
@tomchristie Any insight from TypeSystem on handling the ambiguity of missing HTML form data? I couldn't find a discussion of that in its docs.
Erm, I guess useful points are...
action="<some action name">
field, to determine which form is being submitted.None
or ""
. We run validation for all values, including ""
and None
- it's the responsibility of the field to handle validating those cases, rather than the responsibility of the containing schema/form/thingumy.allow_null
. If it is set to True, then we also set default=null
. Fields coerce ""
values to None
if set.allow_null
or allow_blank
and options, and coerce inputs one way or the other. If allow_blank is set then we also set default=""
.default=False
.Don't know if that's any help. 🤷♂️
Haha, sorry to drag you into this without warning, I just remembered you had worked on a form library recently, thanks for answering 😅
(I'm unsure this is useful, but, as WTForms user, this is the way I expect WTForms would behave:
Currently, it's possible to instanciate a Form
without filling the formadata
or obj
parameters. I don't understand what's the point: if none of them are not provided, what is the goal of Form().validate()
method? I wonder if it would help to change these optional parameters to force one of them to be filled. If both are None, an Exception would be raised.
Missing parameter should be accepted only if the Field accepts it. I think there could be a new parameter to define if it's ok to not post it. By default, it would be forbidden (like Tom Christie does with allow_null=False
parameter), except for RadioField
and CheckBoxField
because the browsers don't send the value if unchecked. In case of not send, the value should be set to the default
parameter. (allow_missing
or missing_ok
are better parameter names IMO.) However there would be a problem with PATCH
: a missing field is ok but the default value is not what we want, it need to be known that the value was not filled. Using an boolean attribute on Field
could be a solution (field.sent
?).
Default parameter should provide a compatible type to the formatted data: for example, an IntegerField should provide a default value like 0, an UUIDField should provide a UUID instance (or an equivalent object according to duck typing), etc.
For a start it should be possible to use the name and id of a form to be able to work around this missing feature with e.g. some javascript solution that calls the submit button of another form and handles the necessary data.
See also the lengthy discussion in https://stackoverflow.com/questions/18290142/multiple-forms-in-a-single-page-using-flask-and-wtforms
Related to at least #119, #242, #267, #280, #291, #355, #401
We do
if formdata is not None
before callingprocess_formdata
on each field in a form. This broke the rather common case where users were passingrequest.form
to a form even if the method wasn't POST becauserequest.form
isn'tNone
. Previously we checkedbool(formdata)
, which handled this case but caused processing to behave inconsistently depending on what was submitted.Flask-WTF somewhat handles this by only passing
request.form
if the method is actually a submit method (POST, PUT, DELETE).None of the approaches are really correct. Maybe you have a client that always sends some extra data (for example, an auth key). Or you have two forms on the same page, whichever was not submitted will still see the same non-empty form data as the one that was submitted.
Most built-in fields mask this issue because they only change
data
(which is set from the default / object data first) ifformdata.getlist('name')
is not empty. However,StringField
will setdata = ''
if nothing was submitted (which #355 changes). AndSelectMultipleField
andMultipleFileField
will overwrite with an empty list because there's no way to distinguish "selected zero values" with "didn't send data" (BooleanField
has a similar problem.)This can also cause weird issues during validation with
InputRequired
. If there are two forms, and you do something likeif f1.validate(): ... elif f2.validate(): ...
, andf1
was not submitted, it will show a "input required" error even if a default value was being displayed, which is confusing.I'm not sure what the solution is here, but am open to discussion. We can break things, I don't care about backwards compatibility for 3.0.
CC @lepture @alanhamlett