FLASK-WTF(1) Flask-WTF FLASK-WTF(1) NAME flask-wtf - Flask-WTF 1.2.1 [image: Flask-WTF] [image] Simple integration of Flask and WTForms, including CSRF, file upload, and reCAPTCHA. FEATURES o Integration with WTForms. o Secure Form with CSRF token. o Global CSRF protection. o reCAPTCHA support. o File upload that works with Flask-Uploads. o Internationalization using Flask-Babel. USER'S GUIDE This part of the documentation, which is mostly prose, begins with some background information about Flask-WTF, then focuses on step-by-step instructions for getting the most out of Flask-WTF. Installation The Python Packaging Guide contains general information about how to manage your project and dependencies. Released version Install or upgrade using pip. pip install -U Flask-WTF Development The latest code is available from GitHub. Clone the repository then install using pip. git clone https://github.com/wtforms/flask-wtf pip install -e ./flask-wtf Or install the latest build from an archive. pip install -U https://github.com/wtforms/flask-wtf/archive/main.tar.gz Quickstart Eager to get started? This page gives a good introduction to Flask-WTF. It assumes you already have Flask-WTF installed. If you do not, head over to the Installation section. Creating Forms Flask-WTF provides your Flask application integration with WTForms. For example: from flask_wtf import FlaskForm from wtforms import StringField from wtforms.validators import DataRequired class MyForm(FlaskForm): name = StringField('name', validators=[DataRequired()]) NOTE: From version 0.9.0, Flask-WTF will not import anything from wtforms, you need to import fields from wtforms. In addition, a CSRF token hidden field is created automatically. You can render this in your template:
{{ form.csrf_token }} {{ form.name.label }} {{ form.name(size=20) }}
If your form has multiple hidden fields, you can render them in one block using hidden_tag().
{{ form.hidden_tag() }} {{ form.name.label }} {{ form.name(size=20) }}
Validating Forms Validating the request in your view handlers: @app.route('/submit', methods=['GET', 'POST']) def submit(): form = MyForm() if form.validate_on_submit(): return redirect('/success') return render_template('submit.html', form=form) Note that you don't have to pass request.form to Flask-WTF; it will load automatically. And the convenient validate_on_submit will check if it is a POST request and if it is valid. If your forms include validation, you'll need to add to your template to display any error messages. Using the form.name field from the example above, that would look like this: {% if form.name.errors %} {% endif %} Heading over to Creating Forms to learn more skills. Creating Forms Secure Form Without any configuration, the FlaskForm will be a session secure form with csrf protection. We encourage you not to change this. But if you want to disable the csrf protection, you can pass: form = FlaskForm(meta={'csrf': False}) You can disable it globally--though you really shouldn't--with the configuration: WTF_CSRF_ENABLED = False In order to generate the csrf token, you must have a secret key, this is usually the same as your Flask app secret key. If you want to use another secret key, config it: WTF_CSRF_SECRET_KEY = 'a random string' File Uploads The FileField provided by Flask-WTF differs from the WTForms-provided field. It will check that the file is a non-empty instance of FileStorage, otherwise data will be None. from flask_wtf import FlaskForm from flask_wtf.file import FileField, FileRequired from werkzeug.utils import secure_filename class PhotoForm(FlaskForm): photo = FileField(validators=[FileRequired()]) @app.route('/upload', methods=['GET', 'POST']) def upload(): form = PhotoForm() if form.validate_on_submit(): f = form.photo.data filename = secure_filename(f.filename) f.save(os.path.join( app.instance_path, 'photos', filename )) return redirect(url_for('index')) return render_template('upload.html', form=form) Similarly, you can use the MultipleFileField provided by Flask-WTF to handle multiple files. It will check that the files is a list of non-empty instance of FileStorage, otherwise data will be None. from flask_wtf import FlaskForm from flask_wtf.file import MultipleFileField, FileRequired from werkzeug.utils import secure_filename class PhotoForm(FlaskForm): photos = MultipleFileField(validators=[FileRequired()]) @app.route('/upload', methods=['GET', 'POST']) def upload(): form = PhotoForm() if form.validate_on_submit(): for f in form.photo.data: # form.photo.data return a list of FileStorage object filename = secure_filename(f.filename) f.save(os.path.join( app.instance_path, 'photos', filename )) return redirect(url_for('index')) return render_template('upload.html', form=form) Remember to set the enctype of the HTML form to multipart/form-data, otherwise request.files will be empty.
...
Flask-WTF handles passing form data to the form for you. If you pass in the data explicitly, remember that request.form must be combined with request.files for the form to see the file data. form = PhotoForm() # is equivalent to: from flask import request from werkzeug.datastructures import CombinedMultiDict form = PhotoForm(CombinedMultiDict((request.files, request.form))) Validation Flask-WTF supports validating file uploads with FileRequired, FileAllowed, and FileSize. They can be used with both Flask-WTF's and WTForms's FileField and MultipleFileField classes. FileAllowed works well with Flask-Uploads. from flask_uploads import UploadSet, IMAGES from flask_wtf import FlaskForm from flask_wtf.file import FileField, FileAllowed, FileRequired images = UploadSet('images', IMAGES) class UploadForm(FlaskForm): upload = FileField('image', validators=[ FileRequired(), FileAllowed(images, 'Images only!') ]) It can be used without Flask-Uploads by passing the extensions directly. class UploadForm(FlaskForm): upload = FileField('image', validators=[ FileRequired(), FileAllowed(['jpg', 'png'], 'Images only!') ]) Recaptcha Flask-WTF also provides Recaptcha support through a RecaptchaField: from flask_wtf import FlaskForm, RecaptchaField from wtforms import TextField class SignupForm(FlaskForm): username = TextField('Username') recaptcha = RecaptchaField() This comes with a number of configuration variables, some of which you have to configure. +----------------------+-------------------------------------------------------------------------------------------+ |RECAPTCHA_PUBLIC_KEY | required A public key. | +----------------------+-------------------------------------------------------------------------------------------+ |RECAPTCHA_PRIVATE_KEY | required A private key. | +----------------------+-------------------------------------------------------------------------------------------+ |RECAPTCHA_API_SERVER | optional Specify your Recaptcha API server. | +----------------------+-------------------------------------------------------------------------------------------+ |RECAPTCHA_PARAMETERS | optional A dict of JavaScript (api.js) parameters. | +----------------------+-------------------------------------------------------------------------------------------+ |RECAPTCHA_DATA_ATTRS | optional A dict of data attributes options. | | | https://developers.google.com/recaptcha/docs/display#javascript_resource_apijs_parameters | +----------------------+-------------------------------------------------------------------------------------------+ Example of RECAPTCHA_PARAMETERS, and RECAPTCHA_DATA_ATTRS: RECAPTCHA_PARAMETERS = {'hl': 'zh', 'render': 'explicit'} RECAPTCHA_DATA_ATTRS = {'theme': 'dark'} For your convenience, when testing your application, if app.testing is True, the recaptcha field will always be valid. And it can be easily setup in the templates:
{{ form.username }} {{ form.recaptcha }}
We have an example for you: recaptcha@github. CSRF Protection Any view using FlaskForm to process the request is already getting CSRF protection. If you have views that don't use FlaskForm or make AJAX requests, use the provided CSRF extension to protect those requests as well. Setup To enable CSRF protection globally for a Flask app, register the CSRFProtect extension. from flask_wtf.csrf import CSRFProtect csrf = CSRFProtect(app) Like other Flask extensions, you can apply it lazily: csrf = CSRFProtect() def create_app(): app = Flask(__name__) csrf.init_app(app) NOTE: CSRF protection requires a secret key to securely sign the token. By default this will use the Flask app's SECRET_KEY. If you'd like to use a separate token you can set WTF_CSRF_SECRET_KEY. HTML Forms When using a FlaskForm, render the form's CSRF field like normal.
{{ form.csrf_token }}
If the template doesn't use a FlaskForm, render a hidden input with the token in the form.
JavaScript Requests When sending an AJAX request, add the X-CSRFToken header to it. For example, in jQuery you can configure all requests to send the token. In Axios you can set the header for all requests with axios.defaults.headers.common. Customize the error response When CSRF validation fails, it will raise a CSRFError. By default this returns a response with the failure reason and a 400 code. You can customize the error response using Flask's errorhandler(). from flask_wtf.csrf import CSRFError @app.errorhandler(CSRFError) def handle_csrf_error(e): return render_template('csrf_error.html', reason=e.description), 400 Exclude views from protection We strongly suggest that you protect all your views with CSRF. But if needed, you can exclude some views using a decorator. @app.route('/foo', methods=('GET', 'POST')) @csrf.exempt def my_handler(): # ... return 'ok' You can exclude all the views of a blueprint. csrf.exempt(account_blueprint) You can disable CSRF protection in all views by default, by setting WTF_CSRF_CHECK_DEFAULT to False, and selectively call protect() only when you need. This also enables you to do some pre-processing on the requests before checking for the CSRF token. @app.before_request def check_csrf(): if not is_oauth(request): csrf.protect() Configuration +-----------------------+----------------------------+ |WTF_CSRF_ENABLED | Set to False to disable | | | all CSRF protection. | | | Default is True. | +-----------------------+----------------------------+ |WTF_CSRF_CHECK_DEFAULT | When using the CSRF | | | protection extension, this | | | controls whether every | | | view is protected by | | | default. Default is True. | +-----------------------+----------------------------+ |WTF_CSRF_SECRET_KEY | Random data for generating | | | secure tokens. If this is | | | not set then SECRET_KEY is | | | used. | +-----------------------+----------------------------+ |WTF_CSRF_METHODS | HTTP methods to protect | | | from CSRF. Default is | | | {'POST', 'PUT', 'PATCH', | | | 'DELETE'}. | +-----------------------+----------------------------+ |WTF_CSRF_FIELD_NAME | Name of the form field and | | | session key that holds the | | | CSRF token. Default is | | | csrf_token. | +-----------------------+----------------------------+ |WTF_CSRF_HEADERS | HTTP headers to search for | | | CSRF token when it is not | | | provided in the form. | | | Default is ['X-CSRFToken', | | | 'X-CSRF-Token']. | +-----------------------+----------------------------+ |WTF_CSRF_TIME_LIMIT | Max age in seconds for | | | CSRF tokens. Default is | | | 3600. If set to None, the | | | CSRF token is valid for | | | the life of the session. | +-----------------------+----------------------------+ |WTF_CSRF_SSL_STRICT | Whether to enforce the | | | same origin policy by | | | checking that the referrer | | | matches the host. Only | | | applies to HTTPS requests. | | | Default is True. | +-----------------------+----------------------------+ |WTF_I18N_ENABLED | Set to False to disable | | | Flask-Babel I18N support. | | | Also set to False if you | | | want to use WTForms's | | | built-in messages | | | directly, see more info | | | here. Default is True. | +-----------------------+----------------------------+ Recaptcha +------------------------+---------------------------------------------------+ |RECAPTCHA_PUBLIC_KEY | required A public key. | +------------------------+---------------------------------------------------+ |RECAPTCHA_PRIVATE_KEY | required A private key. | | | https://www.google.com/recaptcha/admin | +------------------------+---------------------------------------------------+ |RECAPTCHA_PARAMETERS | optional A dict of configuration options. | +------------------------+---------------------------------------------------+ |RECAPTCHA_HTML | optional Override default HTML template for | | | Recaptcha. | +------------------------+---------------------------------------------------+ |RECAPTCHA_DATA_ATTRS | optional A dict of data- attrs to use for | | | Recaptcha div | +------------------------+---------------------------------------------------+ |RECAPTCHA_SCRIPT | optional Override the default captcha script URI | | | in case an alternative service to reCAPtCHA, e.g. | | | hCaptcha is used. Default is | | | 'https://www.google.com/recaptcha/api.js' | +------------------------+---------------------------------------------------+ |RECAPTCHA_DIV_CLASS | optional Override the default class of the | | | captcha div in case an alternative captcha | | | service is used. Default is 'g-recaptcha' | +------------------------+---------------------------------------------------+ |RECAPTCHA_VERIFY_SERVER | optional Override the default verification server | | | in case an alternative service is used. Default | | | is | | | 'https://www.google.com/recaptcha/api/siteverify' | +------------------------+---------------------------------------------------+ Logging CSRF errors are logged at the INFO level to the flask_wtf.csrf logger. You still need to configure logging in your application in order to see these messages. API DOCUMENTATION If you are looking for information on a specific function, class or method, this part of the documentation is for you. Developer Interface Forms and Fields CSRF Protection ADDITIONAL NOTES Legal information and changelog are here. BSD-3-Clause License Copyright 2010 WTForms Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Changes Version 1.2.1 Released 2023-10-02 o Fix a bug introduced with #556 where file validators were editing the file fields content. #578 Version 1.2.0 Released 2023-10-01 o Add field MultipleFileField. FileRequired, FileAllowed, FileSize now can be used to validate multiple files #556 #338 Version 1.1.2 Released 2023-09-29 o Fixed Flask 2.3 deprecations of werkzeug.urls.url_encode and flask.Markup #565 #561 o Stop support for python 3.7 #574 o Use pyproject.toml instead of setup.cfg #576 o Fixed nested blueprint CSRF exemption #572 Version 1.1.1 Released 2023-01-17 o Fixed validate extra_validators parameter. #548 Version 1.1.0 Released 2023-01-15 o Drop support for Python 3.6. o validate_on_submit takes a extra_validators parameters #479 o Stop supporting Flask-Babelex #540 o Support for python 3.11 #542 o Remove unused call to JSONEncoder #536 Version 1.0.1 Released 2022-03-31 o Update compatibility with the latest Werkzeug release. #511 Version 1.0.0 Released 2021-11-07 o Deprecated items removal #484 o Support for alternatives captcha services #425 #342 #387 #384 Version 0.15.1 Released 2021-05-25 o Add python_requires metadata to avoid installing on unsupported Python versions. #442 Version 0.15.0 Released 2021-05-24 o Drop support for Python < 3.6. #416 o FileSize validator. #307#365 o Extra requirement email installs the email_validator package. #423 o Fixed Flask 2.0 warnings. #434 o Various documentation fixes. #315#321#335#344#386#400, #404#420#437 o Various CI fixes. #405#438 Version 0.14.3 Released 2020-02-06 o Fix deprecated imports from werkzeug and collections. Version 0.14.2 Released 2017-01-10 o Fix bug where FlaskForm assumed meta argument was not None if it was passed. #278 Version 0.14.1 Released 2017-01-10 o Fix bug where the file validators would incorrectly identify an empty file as valid data. #276, #277 o FileField is no longer deprecated. The data is checked during processing and only set if it's a valid file. o has_file is deprecated; it's now equivalent to bool(field.data). o FileRequired and FileAllowed work with both the Flask-WTF and WTForms FileField classes. o The Optional validator now works with FileField. Version 0.14 Released 2017-01-06 o Use ItsDangerous to sign CSRF tokens and check expiration instead of doing it ourselves. #264 o All tokens are URL safe, removing the url_safe parameter from generate_csrf. #206 o All tokens store a timestamp, which is checked in validate_csrf. The time_limit parameter of generate_csrf is removed. o Remove the app attribute from CsrfProtect, use current_app. #264 o CsrfProtect protects the DELETE method by default. #264 o The same CSRF token is generated for the lifetime of a request. It is exposed as g.csrf_token for use during testing. #227#264 o CsrfProtect.error_handler is deprecated. #264 o Handlers that return a response work in addition to those that raise an error. The behavior was not clear in previous docs. o #200#209#243#252 o Use Form.Meta instead of deprecated SecureForm for CSRF (and everything else). #216#271 o csrf_enabled parameter is still recognized but deprecated. All other attributes and methods from SecureForm are removed. #271 o Provide WTF_CSRF_FIELD_NAME to configure the name of the CSRF token. #271 o validate_csrf raises wtforms.ValidationError with specific messages instead of returning True or False. This breaks anything that was calling the method directly. #239#271 o CSRF errors are logged as well as raised. #239 o CsrfProtect is renamed to CSRFProtect. A deprecation warning is issued when using the old name. CsrfError is renamed to CSRFError without deprecation. #271 o FileField is deprecated because it no longer provides functionality over the provided validators. Use wtforms.FileField directly. #272 Version 0.13.1 Released 2016-10-6 o Deprecation warning for Form is shown during __init__ instead of immediately when subclassing. #262 o Don't use pkg_resources to get version, for compatibility with GAE. #261 Version 0.13 Released 2016-09-29 o Form is renamed to FlaskForm in order to avoid name collision with WTForms's base class. Using Form will show a deprecation warning. #250 o hidden_tag no longer wraps the hidden inputs in a hidden div. This is valid HTML5 and any modern HTML parser will behave correctly. #193#217 o flask_wtf.html5 is deprecated. Import directly from wtforms.fields.html5. #251 o is_submitted is true for PATCH and DELETE in addition to POST and PUT. #187 o generate_csrf takes a token_key parameter to specify the key stored in the session. #206 o generate_csrf takes a url_safe parameter to allow the token to be used in URLs. #206 o form.data can be accessed multiple times without raising an exception. #248 o File extension with multiple parts (.tar.gz) can be used in the FileAllowed validator. #201 Version 0.12 Released 2015-07-09 o Abstract protect_csrf() into a separate method. o Update reCAPTCHA configuration. o Fix reCAPTCHA error handle. Version 0.11 Released 2015-01-21 o Use the new reCAPTCHA API. #164 Version 0.10.3 Released 2014-11-16 o Add configuration: WTF_CSRF_HEADERS. #159 o Support customize hidden tags. #150 o And many more bug fixes. Version 0.10.2 Released 2014-09-03 o Update translation for reCaptcha. #146 Version 0.10.1 Released 2014-08-26 o Update RECAPTCHA_API_SERVER_URL. #145 o Update requirement Werkzeug >= 0.9.5. o Fix CsrfProtect exempt for blueprints. #143 Version 0.10.0 Released 2014-07-16 o Add configuration: WTF_CSRF_METHODS. o Support WTForms 2.0 now. o Fix CSRF validation without time limit (time_limit=False). o csrf_exempt supports blueprint. #111 Version 0.9.5 Released 2014-03-21 o csrf_token for all template types. #112 o Make FileRequired a subclass of InputRequired. #108 Version 0.9.4 Released 2013-12-20 o Bugfix for csrf module when form has a prefix. o Compatible support for WTForms 2. o Remove file API for FileField Version 0.9.3 Released 2013-10-02 o Fix validation of recaptcha when app in testing mode. #89 o Bugfix for csrf module. #91 Version 0.9.2 Released 2013-09-11 o Upgrade WTForms to 1.0.5. o No lazy string for i18n. #77 o No DateInput widget in HTML5. #81 o PUT and PATCH for CSRF. #86 Version 0.9.1 Released 2013-08-21 o Compatibility with Flask < 0.10. #82 Version 0.9.0 Released 2013-08-15 o Add i18n support. #65 o Use default HTML5 widgets and fields provided by WTForms. o Python 3.3+ support. o Redesign form, replace SessionSecureForm. o CSRF protection solution. o Drop WTForms imports. o Fix recaptcha i18n support. o Fix recaptcha validator for Python 3. o More test cases, it's 90%+ coverage now. o Redesign documentation. Version 0.8.4 Released 2013-03-28 o Recaptcha Validator now returns provided message. #66 o Minor doc fixes. o Fixed issue with tests barking because of nose/multiprocessing issue. Version 0.8.3 Released 2013-03-13 o Update documentation to indicate pending deprecation of WTForms namespace facade. o PEP8 fixes. #64 o Fix Recaptcha widget. #49 Version 0.8.2 and prior Initial development by Dan Jacob and Ron Duplain. How to contribute to Flask-WTF Thank you for considering contributing to Flask-WTF! Support questions Please don't use the issue tracker for this. The issue tracker is a tool to address bugs and feature requests in Flask-WTF itself. Use one of the following resources for questions about using Flask-WTF or issues with your own code: o The #get-help channel on our Discord chat: https://discord.gg/pallets o The mailing list flask@python.org for long term discussion or larger issues. o Ask on Stack Overflow. Search with Google first using: site:stackoverflow.com flask-wtf {search term, exception message, etc.} Reporting issues Include the following information in your post: o Describe what you expected to happen. o If possible, include a minimal reproducible example to help us identify the issue. This also helps check that the issue is not with your own code. o Describe what actually happened. Include the full traceback if there was an exception. o List your Python, Flask-WTF, and WTForms versions. If possible, check if this issue is already fixed in the latest releases or the latest code in the repository. Submitting patches If there is not an open issue for what you want to submit, prefer opening one for discussion before working on a PR. You can work on any issue that doesn't have an open PR linked to it or a maintainer assigned to it. These show up in the sidebar. No need to ask if you can work on an issue that interests you. Include the following in your patch: o Use Black to format your code. This and other tools will run automatically if you install pre-commit using the instructions below. o Include tests if your patch adds or changes code. Make sure the test fails without your patch. o Update any relevant docs pages and docstrings. Docs pages and docstrings should be wrapped at 72 characters. o Add an entry in CHANGES.rst. Use the same style as other entries. Also include .. versionchanged:: inline changelogs in relevant docstrings. First time setup o Download and install the latest version of git. o Configure git with your username and email. $ git config --global user.name 'your name' $ git config --global user.email 'your email' o Make sure you have a GitHub account. o Fork Flask-WTF to your GitHub account by clicking the Fork button. o Clone the main repository locally. $ git clone https://github.com/wtforms/flask-wtf $ cd flask-wtf o Add your fork as a remote to push your work to. Replace {username} with your username. This names the remote "fork", the default WTForms remote is "origin". $ git remote add fork https://github.com/{username}/flask-wtf o Create a virtualenv. $ python3 -m venv env $ . env/bin/activate On Windows, activating is different. > env\Scripts\activate o Upgrade pip and setuptools. $ python -m pip install --upgrade pip setuptools o Install the development dependencies, then install Flask-WTF in editable mode. $ pip install -r requirements/dev.txt && pip install -e . o Install the pre-commit hooks. $ pre-commit install Start coding o Create a branch to identify the issue you would like to work on. If you're submitting a bug or documentation fix, branch off of the latest ".x" branch. $ git fetch origin $ git checkout -b your-branch-name origin/1.0.x If you're submitting a feature addition or change, branch off of the "main" branch. $ git fetch origin $ git checkout -b your-branch-name origin/main o Using your favorite editor, make your changes, committing as you go. o Include tests that cover any code changes you make. Make sure the test fails without your patch. Run the tests as described below. o Push your commits to your fork on GitHub and create a pull request. Link to the issue being addressed with fixes #123 in the pull request. $ git push --set-upstream fork your-branch-name Running the tests Run the basic test suite with pytest. $ pytest This runs the tests for the current environment, which is usually sufficient. CI will run the full suite when you submit your pull request. You can run the full test suite with tox if you don't want to wait. $ tox Running test coverage Generating a report of lines that do not have test coverage can indicate where to start contributing. Run pytest using coverage and generate a report. $ pip install coverage $ coverage run -m pytest $ coverage html Open htmlcov/index.html in your browser to explore the report. Read more about coverage. Building the docs Build the docs in the docs directory using Sphinx. $ cd docs $ make html Open _build/html/index.html in your browser to view the docs. Read more about Sphinx. AUTHOR WTForms COPYRIGHT 2024 WTForms 1.2.x April 8, 2024 FLASK-WTF(1)