diff --git a/Flask_WTF.egg-info/PKG-INFO b/Flask_WTF.egg-info/PKG-INFO index 7608b400d4b98babfd38b5ae775c76e29283f193..68513104bbfb984c0743cd2af5996b4d54e4710e 100644 --- a/Flask_WTF.egg-info/PKG-INFO +++ b/Flask_WTF.egg-info/PKG-INFO @@ -1,23 +1,24 @@ -Metadata-Version: 1.1 +Metadata-Version: 2.1 Name: Flask-WTF -Version: 0.14.2 +Version: 0.14.3 Summary: Simple integration of Flask and WTForms. Home-page: https://github.com/lepture/flask-wtf -Author: Hsiaoming Yang -Author-email: me@lepture.com +Author: Dan Jacob +Author-email: danjac354@gmail.com +Maintainer: Hsiaoming Yang +Maintainer-email: me@lepture.com License: BSD Description: Flask-WTF ========= .. image:: https://travis-ci.org/lepture/flask-wtf.svg?branch=master - :target: https://travis-ci.org/lepture/flask-wtf - :alt: Travis CI Status - .. image:: https://coveralls.io/repos/lepture/flask-wtf/badge.svg?branch=master - :target: https://coveralls.io/r/lepture/flask-wtf - :alt: Coverage Status + :target: https://travis-ci.org/lepture/flask-wtf + :alt: Test Status + .. image:: https://codecov.io/gh/lepture/flask-wtf/branch/master/graph/badge.svg + :target: https://codecov.io/gh/lepture/flask-wtf + :alt: Coverage Status - Simple integration of Flask and WTForms, including CSRF, file upload, - and reCAPTCHA. + Simple integration of Flask and WTForms, including CSRF, file upload, and reCAPTCHA. Links ----- @@ -35,14 +36,15 @@ Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 -Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Software Development :: Libraries :: Python Modules +Description-Content-Type: text/x-rst diff --git a/Flask_WTF.egg-info/SOURCES.txt b/Flask_WTF.egg-info/SOURCES.txt index 8b9d8f6203f228fbcaa3b8f8f0f820a0d1fe54b6..4a77b7c7e74e5b186abbc793f3ef09177190fa06 100644 --- a/Flask_WTF.egg-info/SOURCES.txt +++ b/Flask_WTF.egg-info/SOURCES.txt @@ -4,7 +4,6 @@ MANIFEST.in README.rst setup.cfg setup.py -test-requirements.txt tox.ini Flask_WTF.egg-info/PKG-INFO Flask_WTF.egg-info/SOURCES.txt @@ -29,16 +28,6 @@ docs/upgrade.rst docs/_static/flask-wtf.png docs/_templates/brand.html docs/_templates/useful-links.html -docs/_themes/LICENSE -docs/_themes/README.rst -docs/_themes/flask_theme_support.py -docs/_themes/flask/layout.html -docs/_themes/flask/relations.html -docs/_themes/flask/theme.conf -docs/_themes/flask/static/flasky.css_t -docs/_themes/flask_small/layout.html -docs/_themes/flask_small/theme.conf -docs/_themes/flask_small/static/flasky.css_t flask_wtf/__init__.py flask_wtf/_compat.py flask_wtf/csrf.py @@ -50,20 +39,11 @@ flask_wtf/recaptcha/__init__.py flask_wtf/recaptcha/fields.py flask_wtf/recaptcha/validators.py flask_wtf/recaptcha/widgets.py -tests/__init__.py -tests/base.py -tests/flask.png -tests/flask.txt -tests/test_csrf.py -tests/test_deprecated.py +tests/conftest.py +tests/test_csrf_extension.py +tests/test_csrf_form.py +tests/test_file.py +tests/test_form.py +tests/test_html5.py tests/test_i18n.py -tests/test_recaptcha.py -tests/test_uploads.py -tests/test_validation.py -tests/templates/csrf.html -tests/templates/csrf_macro.html -tests/templates/hidden.html -tests/templates/import_csrf.html -tests/templates/index.html -tests/templates/recaptcha.html -tests/templates/upload.html \ No newline at end of file +tests/test_recaptcha.py \ No newline at end of file diff --git a/Flask_WTF.egg-info/requires.txt b/Flask_WTF.egg-info/requires.txt index 658cc2d9d659fbed9334e3ebb47ca9ab10d9f957..0d7ceaaaf380f47538787cf52f69a4e4c4d6276b 100644 --- a/Flask_WTF.egg-info/requires.txt +++ b/Flask_WTF.egg-info/requires.txt @@ -1,2 +1,3 @@ Flask WTForms +itsdangerous diff --git a/PKG-INFO b/PKG-INFO index 7608b400d4b98babfd38b5ae775c76e29283f193..68513104bbfb984c0743cd2af5996b4d54e4710e 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,23 +1,24 @@ -Metadata-Version: 1.1 +Metadata-Version: 2.1 Name: Flask-WTF -Version: 0.14.2 +Version: 0.14.3 Summary: Simple integration of Flask and WTForms. Home-page: https://github.com/lepture/flask-wtf -Author: Hsiaoming Yang -Author-email: me@lepture.com +Author: Dan Jacob +Author-email: danjac354@gmail.com +Maintainer: Hsiaoming Yang +Maintainer-email: me@lepture.com License: BSD Description: Flask-WTF ========= .. image:: https://travis-ci.org/lepture/flask-wtf.svg?branch=master - :target: https://travis-ci.org/lepture/flask-wtf - :alt: Travis CI Status - .. image:: https://coveralls.io/repos/lepture/flask-wtf/badge.svg?branch=master - :target: https://coveralls.io/r/lepture/flask-wtf - :alt: Coverage Status + :target: https://travis-ci.org/lepture/flask-wtf + :alt: Test Status + .. image:: https://codecov.io/gh/lepture/flask-wtf/branch/master/graph/badge.svg + :target: https://codecov.io/gh/lepture/flask-wtf + :alt: Coverage Status - Simple integration of Flask and WTForms, including CSRF, file upload, - and reCAPTCHA. + Simple integration of Flask and WTForms, including CSRF, file upload, and reCAPTCHA. Links ----- @@ -35,14 +36,15 @@ Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 -Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Software Development :: Libraries :: Python Modules +Description-Content-Type: text/x-rst diff --git a/README.rst b/README.rst index dec4232842d027b84c0c49a9adf74386057ad83c..50a680b5124b9d451d3dd5b68ca253961a614e91 100644 --- a/README.rst +++ b/README.rst @@ -2,14 +2,13 @@ Flask-WTF ========= .. image:: https://travis-ci.org/lepture/flask-wtf.svg?branch=master - :target: https://travis-ci.org/lepture/flask-wtf - :alt: Travis CI Status -.. image:: https://coveralls.io/repos/lepture/flask-wtf/badge.svg?branch=master - :target: https://coveralls.io/r/lepture/flask-wtf - :alt: Coverage Status + :target: https://travis-ci.org/lepture/flask-wtf + :alt: Test Status +.. image:: https://codecov.io/gh/lepture/flask-wtf/branch/master/graph/badge.svg + :target: https://codecov.io/gh/lepture/flask-wtf + :alt: Coverage Status -Simple integration of Flask and WTForms, including CSRF, file upload, -and reCAPTCHA. +Simple integration of Flask and WTForms, including CSRF, file upload, and reCAPTCHA. Links ----- diff --git a/docs/_themes/LICENSE b/docs/_themes/LICENSE deleted file mode 100644 index 8daab7ee6efe3b6f3c2a2594b711e7a9026eb5e2..0000000000000000000000000000000000000000 --- a/docs/_themes/LICENSE +++ /dev/null @@ -1,37 +0,0 @@ -Copyright (c) 2010 by Armin Ronacher. - -Some rights reserved. - -Redistribution and use in source and binary forms of the theme, with or -without modification, are permitted provided that the following conditions -are met: - -* Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - -* 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. - -* The names of the contributors may not be used to endorse or - promote products derived from this software without specific - prior written permission. - -We kindly ask you to only use these themes in an unmodified manner just -for Flask and Flask-related products, not for unrelated projects. If you -like the visual style and want to use it for your own projects, please -consider making some larger changes to the themes (such as changing -font faces, sizes, colors or margins). - -THIS THEME 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 OWNER 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 THEME, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/_themes/README.rst b/docs/_themes/README.rst deleted file mode 100644 index 427d62c82af812e340ddd17fb647909697e62135..0000000000000000000000000000000000000000 --- a/docs/_themes/README.rst +++ /dev/null @@ -1,71 +0,0 @@ -Flask Sphinx Themes -=================== - -This repository contains Sphinx themes for Flask and Flask related -projects. To use this theme in your Sphinx documentation: - -1. Put this folder as ``_themes`` in the docs folder. Alternatively - you can use git submodules to check out the contents there. - -2. Add this to ``conf.py``: - - .. code-block:: python - - sys.path.append(os.path.join(os.path.dirname(__file__), '_themes')) - html_theme_path = ['_themes'] - html_theme = 'flask' - -Themes ------- - -The following themes exist for ``html_theme``. - -======================= =============================================== -flask The standard Flask documentation theme for - large projects - -flask_small Small single page theme. Intended to be used - by very small addon libraries for Flask. -======================= =============================================== - -Options -------- - -The following options can be set with ``html_theme_options``. - -======================= =============================================== -index_logo Filename of a picture in ``_static`` to be used - as replacement for the ``h1`` in the - ``index.rst`` file. - *Default unset.* - -index_logo_height Height of the index logo. - *Default 120px*. - -touch_icon Filename of a picture in ``_static`` to be use - as the app icon on Apple devices. - *Default unset.* - -github_fork Repository name on GitHub for the "Fork Me" - badge. - *Default unset.* - -github_ribbon_color Color for the "Fork Me" badge. - *Default darkblue_121621.* -======================= =============================================== - -Sidebar Templates ------------------ - -The following sidebar templates can be included in ``html_sidebars``. - -======================= =============================================== -relations.html Show parent, previous, and next links. -======================= =============================================== - -Pygments Style --------------- - -The theme automatically sets ``pygments_style`` to the provided style. -Make sure you remove any override from ``conf.py`` or set it to -``flask_theme_support.FlaskyStyle``. diff --git a/docs/_themes/flask/layout.html b/docs/_themes/flask/layout.html deleted file mode 100644 index 8398eba660e260a9bc614730230ced88a79d5f1c..0000000000000000000000000000000000000000 --- a/docs/_themes/flask/layout.html +++ /dev/null @@ -1,29 +0,0 @@ -{% extends 'basic/layout.html' %} - -{% block extrahead %} - {{ super() }} - {% if theme_touch_icon %} - <link rel="apple-touch-icon" href="{{ pathto('_static/' ~ theme_touch_icon, 1) }}"> - {% endif %} - <meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9"> -{% endblock %} - -{% block relbar2 %} - {% if theme_github_fork %} - <a href="http://github.com/{{ theme_github_fork }}"> - <img style="position: fixed; top: 0; right: 0; border: 0;" - src="http://s3.amazonaws.com/github/ribbons/forkme_right_{{ theme_github_ribbon_color }}.png" - alt="Fork me on GitHub"> - </a> - {% endif %} -{% endblock %} - -{% block header %} - {{ super() }} - {% if pagename == 'index' %}<div class=indexwrapper>{% endif %} -{% endblock %} - -{% block footer %} - {{ super() }} - {% if pagename == 'index' %}</div>{% endif %} -{% endblock %} diff --git a/docs/_themes/flask/relations.html b/docs/_themes/flask/relations.html deleted file mode 100644 index 3bbcde85bb48d3f8bdfd066104d8f2eae584e72d..0000000000000000000000000000000000000000 --- a/docs/_themes/flask/relations.html +++ /dev/null @@ -1,19 +0,0 @@ -<h3>Related Topics</h3> -<ul> - <li><a href="{{ pathto(master_doc) }}">Documentation overview</a><ul> - {%- for parent in parents %} - <li><a href="{{ parent.link|e }}">{{ parent.title }}</a><ul> - {%- endfor %} - {%- if prev %} - <li>Previous: <a href="{{ prev.link|e }}" title="{{ _('previous chapter') - }}">{{ prev.title }}</a></li> - {%- endif %} - {%- if next %} - <li>Next: <a href="{{ next.link|e }}" title="{{ _('next chapter') - }}">{{ next.title }}</a></li> - {%- endif %} - {%- for parent in parents %} - </ul></li> - {%- endfor %} - </ul></li> -</ul> diff --git a/docs/_themes/flask/static/flasky.css_t b/docs/_themes/flask/static/flasky.css_t deleted file mode 100644 index 5906e751b7a9c21a5c295e3779b0fbe7541c02cb..0000000000000000000000000000000000000000 --- a/docs/_themes/flask/static/flasky.css_t +++ /dev/null @@ -1,577 +0,0 @@ -/* - * flasky.css_t - * ~~~~~~~~~~~~ - * - * :copyright: Copyright 2010 by Armin Ronacher. - * :license: Flask Design License, see LICENSE for details. - */ - -{% set page_width = '940px' %} -{% set sidebar_width = '220px' %} - -@import url("basic.css"); - -/* -- page layout ----------------------------------------------------------- */ - -body { - font-family: 'Georgia', serif; - font-size: 17px; - background-color: white; - color: #000; - margin: 0; - padding: 0; -} - -div.document { - width: {{ page_width }}; - margin: 30px auto 0 auto; -} - -div.documentwrapper { - float: left; - width: 100%; -} - -div.bodywrapper { - margin: 0 0 0 {{ sidebar_width }}; -} - -div.sphinxsidebar { - width: {{ sidebar_width }}; -} - -hr { - border: 1px solid #B1B4B6; -} - -div.body { - background-color: #ffffff; - color: #3E4349; - padding: 0 30px 0 30px; -} - -img.floatingflask { - padding: 0 0 10px 10px; - float: right; -} - -div.footer { - width: {{ page_width }}; - margin: 20px auto 30px auto; - font-size: 14px; - color: #888; - text-align: right; -} - -div.footer a { - color: #888; -} - -div.related { - display: none; -} - -div.sphinxsidebar a { - color: #444; - text-decoration: none; - border-bottom: 1px dotted #999; -} - -div.sphinxsidebar a:hover { - border-bottom: 1px solid #999; -} - -div.sphinxsidebar { - font-size: 14px; - line-height: 1.5; -} - -div.sphinxsidebarwrapper { - padding: 18px 10px; -} - -div.sphinxsidebarwrapper p.logo { - padding: 0 0 20px 0; - margin: 0; - text-align: center; -} - -div.sphinxsidebar h3, -div.sphinxsidebar h4 { - font-family: 'Garamond', 'Georgia', serif; - color: #444; - font-size: 24px; - font-weight: normal; - margin: 0 0 5px 0; - padding: 0; -} - -div.sphinxsidebar h4 { - font-size: 20px; -} - -div.sphinxsidebar h3 a { - color: #444; -} - -div.sphinxsidebar p.logo a, -div.sphinxsidebar h3 a, -div.sphinxsidebar p.logo a:hover, -div.sphinxsidebar h3 a:hover { - border: none; -} - -div.sphinxsidebar p { - color: #555; - margin: 10px 0; -} - -div.sphinxsidebar ul { - margin: 10px 0; - padding: 0; - color: #000; -} - -div.sphinxsidebar input { - border: 1px solid #ccc; - font-family: 'Georgia', serif; - font-size: 1em; -} - -/* -- body styles ----------------------------------------------------------- */ - -a { - color: #004B6B; - text-decoration: underline; -} - -a:hover { - color: #6D4100; - text-decoration: underline; -} - -div.body h1, -div.body h2, -div.body h3, -div.body h4, -div.body h5, -div.body h6 { - font-family: 'Garamond', 'Georgia', serif; - font-weight: normal; - margin: 30px 0px 10px 0px; - padding: 0; -} - -{% if theme_index_logo %} -div.indexwrapper h1 { - text-indent: -999999px; - background: url({{ theme_index_logo }}) no-repeat center center; - height: {{ theme_index_logo_height }}; -} -{% endif %} -div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } -div.body h2 { font-size: 180%; } -div.body h3 { font-size: 150%; } -div.body h4 { font-size: 130%; } -div.body h5 { font-size: 100%; } -div.body h6 { font-size: 100%; } - -a.headerlink { - color: #ddd; - padding: 0 4px; - text-decoration: none; -} - -a.headerlink:hover { - color: #444; - background: #eaeaea; -} - -div.body p, div.body dd, div.body li { - line-height: 1.4em; -} - -div.admonition { - background: #fafafa; - margin: 20px -30px; - padding: 10px 30px; - border-top: 1px solid #ccc; - border-bottom: 1px solid #ccc; -} - -div.admonition tt.xref, div.admonition a tt { - border-bottom: 1px solid #fafafa; -} - -dd div.admonition { - margin-left: -60px; - padding-left: 60px; -} - -div.admonition p.admonition-title { - font-family: 'Garamond', 'Georgia', serif; - font-weight: normal; - font-size: 24px; - margin: 0 0 10px 0; - padding: 0; - line-height: 1; -} - -div.admonition p.last { - margin-bottom: 0; -} - -div.highlight { - background-color: white; -} - -dt:target, .highlight { - background: #FAF3E8; -} - -div.note { - background-color: #eee; - border: 1px solid #ccc; -} - -div.seealso { - background-color: #ffc; - border: 1px solid #ff6; -} - -div.topic { - background-color: #eee; -} - -p.admonition-title { - display: inline; -} - -p.admonition-title:after { - content: ":"; -} - -pre, tt { - font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; - font-size: 0.9em; -} - -img.screenshot { -} - -tt.descname, tt.descclassname { - font-size: 0.95em; -} - -tt.descname { - padding-right: 0.08em; -} - -img.screenshot { - -moz-box-shadow: 2px 2px 4px #eee; - -webkit-box-shadow: 2px 2px 4px #eee; - box-shadow: 2px 2px 4px #eee; -} - -table.docutils { - border: 1px solid #888; - -moz-box-shadow: 2px 2px 4px #eee; - -webkit-box-shadow: 2px 2px 4px #eee; - box-shadow: 2px 2px 4px #eee; -} - -table.docutils td, table.docutils th { - border: 1px solid #888; - padding: 0.25em 0.7em; -} - -table.field-list, table.footnote { - border: none; - -moz-box-shadow: none; - -webkit-box-shadow: none; - box-shadow: none; -} - -table.footnote { - margin: 15px 0; - width: 100%; - border: 1px solid #eee; - background: #fdfdfd; - font-size: 0.9em; -} - -table.footnote + table.footnote { - margin-top: -15px; - border-top: none; -} - -table.field-list th { - padding: 0 0.8em 0 0; -} - -table.field-list td { - padding: 0; -} - -table.footnote td.label { - width: 0px; - padding: 0.3em 0 0.3em 0.5em; -} - -table.footnote td { - padding: 0.3em 0.5em; -} - -dl { - margin: 0; - padding: 0; -} - -dl dd { - margin-left: 30px; -} - -blockquote { - margin: 0 0 0 30px; - padding: 0; -} - -ul, ol { - margin: 10px 0 10px 30px; - padding: 0; -} - -pre { - background: #eee; - padding: 7px 30px; - margin: 15px -30px; - line-height: 1.3em; -} - -dl pre, blockquote pre, li pre { - margin-left: -60px; - padding-left: 60px; -} - -dl dl pre { - margin-left: -90px; - padding-left: 90px; -} - -tt { - background-color: #ecf0f3; - color: #222; - /* padding: 1px 2px; */ -} - -tt.xref, a tt { - background-color: #FBFBFB; - border-bottom: 1px solid white; -} - -a.reference { - text-decoration: none; - border-bottom: 1px dotted #004B6B; -} - -a.reference:hover { - border-bottom: 1px solid #6D4100; -} - -a.footnote-reference { - text-decoration: none; - font-size: 0.7em; - vertical-align: top; - border-bottom: 1px dotted #004B6B; -} - -a.footnote-reference:hover { - border-bottom: 1px solid #6D4100; -} - -a:hover tt { - background: #EEE; -} - - -@media screen and (max-width: 870px) { - - div.sphinxsidebar { - display: none; - } - - div.document { - width: 100%; - - } - - div.documentwrapper { - margin-left: 0; - margin-top: 0; - margin-right: 0; - margin-bottom: 0; - } - - div.bodywrapper { - margin-top: 0; - margin-right: 0; - margin-bottom: 0; - margin-left: 0; - } - - ul { - margin-left: 0; - } - - .document { - width: auto; - } - - .footer { - width: auto; - } - - .bodywrapper { - margin: 0; - } - - .footer { - width: auto; - } - - .github { - display: none; - } - - - -} - - - -@media screen and (max-width: 875px) { - - body { - margin: 0; - padding: 20px 30px; - } - - div.documentwrapper { - float: none; - background: white; - } - - div.sphinxsidebar { - display: block; - float: none; - width: 102.5%; - margin: 50px -30px -20px -30px; - padding: 10px 20px; - background: #333; - color: white; - } - - div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, - div.sphinxsidebar h3 a { - color: white; - } - - div.sphinxsidebar a { - color: #aaa; - } - - div.sphinxsidebar p.logo { - display: none; - } - - div.document { - width: 100%; - margin: 0; - } - - div.related { - display: block; - margin: 0; - padding: 10px 0 20px 0; - } - - div.related ul, - div.related ul li { - margin: 0; - padding: 0; - } - - div.footer { - display: none; - } - - div.bodywrapper { - margin: 0; - } - - div.body { - min-height: 0; - padding: 0; - } - - .rtd_doc_footer { - display: none; - } - - .document { - width: auto; - } - - .footer { - width: auto; - } - - .footer { - width: auto; - } - - .github { - display: none; - } -} - - -/* scrollbars */ - -::-webkit-scrollbar { - width: 6px; - height: 6px; -} - -::-webkit-scrollbar-button:start:decrement, -::-webkit-scrollbar-button:end:increment { - display: block; - height: 10px; -} - -::-webkit-scrollbar-button:vertical:increment { - background-color: #fff; -} - -::-webkit-scrollbar-track-piece { - background-color: #eee; - -webkit-border-radius: 3px; -} - -::-webkit-scrollbar-thumb:vertical { - height: 50px; - background-color: #ccc; - -webkit-border-radius: 3px; -} - -::-webkit-scrollbar-thumb:horizontal { - width: 50px; - background-color: #ccc; - -webkit-border-radius: 3px; -} - -/* misc. */ - -.revsys-inline { - display: none!important; -} \ No newline at end of file diff --git a/docs/_themes/flask/theme.conf b/docs/_themes/flask/theme.conf deleted file mode 100644 index ba22bcc7b19e6ec40b3edad02595ce0ba6240b9b..0000000000000000000000000000000000000000 --- a/docs/_themes/flask/theme.conf +++ /dev/null @@ -1,11 +0,0 @@ -[theme] -inherit = basic -stylesheet = flasky.css -pygments_style = flask_theme_support.FlaskyStyle - -[options] -index_logo = -index_logo_height = 120px -touch_icon = -github_fork = -github_ribbon_color = darkblue_121621 diff --git a/docs/_themes/flask_small/layout.html b/docs/_themes/flask_small/layout.html deleted file mode 100644 index 67478c3bf1f51b4d85abda7073abf3246e2d145b..0000000000000000000000000000000000000000 --- a/docs/_themes/flask_small/layout.html +++ /dev/null @@ -1,38 +0,0 @@ -{% extends 'basic/layout.html' %} - -{% block extrahead %} - {{ super() }} - {% if theme_touch_icon %} - <link rel="apple-touch-icon" href="{{ pathto('_static/' ~ theme_touch_icon, 1) }}"> - {% endif %} - <meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9"> -{% endblock %} - -{% block header %} - {{ super() }} - {% if pagename == 'index' %} - <div class=indexwrapper> - {% endif %} -{% endblock %} - -{% block footer %} - {% if pagename == 'index' %}</div>{% endif %} -{% endblock %} - -{# do not display relbars or sidebars #} - -{% block relbar1 %}{% endblock %} - -{% block relbar2 %} - {% if theme_github_fork %} - <a href="http://github.com/{{ theme_github_fork }}"> - <img style="position: fixed; top: 0; right: 0; border: 0;" - src="http://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png" - alt="Fork me on GitHub"> - </a> - {% endif %} -{% endblock %} - -{% block sidebar1 %}{% endblock %} - -{% block sidebar2 %}{% endblock %} diff --git a/docs/_themes/flask_small/static/flasky.css_t b/docs/_themes/flask_small/static/flasky.css_t deleted file mode 100644 index fe2141c565e6976b6a2a5b73f318ad7c6dc3d58b..0000000000000000000000000000000000000000 --- a/docs/_themes/flask_small/static/flasky.css_t +++ /dev/null @@ -1,287 +0,0 @@ -/* - * flasky.css_t - * ~~~~~~~~~~~~ - * - * Sphinx stylesheet -- flasky theme based on nature theme. - * - * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * - */ - -@import url("basic.css"); - -/* -- page layout ----------------------------------------------------------- */ - -body { - font-family: 'Georgia', serif; - font-size: 17px; - color: #000; - background: white; - margin: 0; - padding: 0; -} - -div.documentwrapper { - float: left; - width: 100%; -} - -div.bodywrapper { - margin: 40px auto 0 auto; - width: 700px; -} - -hr { - border: 1px solid #B1B4B6; -} - -div.body { - background-color: #ffffff; - color: #3E4349; - padding: 0 30px 30px 30px; -} - -img.floatingflask { - padding: 0 0 10px 10px; - float: right; -} - -div.footer { - text-align: right; - color: #888; - padding: 10px; - font-size: 14px; - width: 650px; - margin: 0 auto 40px auto; -} - -div.footer a { - color: #888; - text-decoration: underline; -} - -div.related { - line-height: 32px; - color: #888; -} - -div.related ul { - padding: 0 0 0 10px; -} - -div.related a { - color: #444; -} - -/* -- body styles ----------------------------------------------------------- */ - -a { - color: #004B6B; - text-decoration: underline; -} - -a:hover { - color: #6D4100; - text-decoration: underline; -} - -div.body { - padding-bottom: 40px; /* saved for footer */ -} - -div.body h1, -div.body h2, -div.body h3, -div.body h4, -div.body h5, -div.body h6 { - font-family: 'Garamond', 'Georgia', serif; - font-weight: normal; - margin: 30px 0px 10px 0px; - padding: 0; -} - -{% if theme_index_logo %} -div.indexwrapper h1 { - text-indent: -999999px; - background: url({{ theme_index_logo }}) no-repeat center center; - height: {{ theme_index_logo_height }}; -} -{% endif %} - -div.body h2 { font-size: 180%; } -div.body h3 { font-size: 150%; } -div.body h4 { font-size: 130%; } -div.body h5 { font-size: 100%; } -div.body h6 { font-size: 100%; } - -a.headerlink { - color: white; - padding: 0 4px; - text-decoration: none; -} - -a.headerlink:hover { - color: #444; - background: #eaeaea; -} - -div.body p, div.body dd, div.body li { - line-height: 1.4em; -} - -div.admonition { - background: #fafafa; - margin: 20px -30px; - padding: 10px 30px; - border-top: 1px solid #ccc; - border-bottom: 1px solid #ccc; -} - -div.admonition p.admonition-title { - font-family: 'Garamond', 'Georgia', serif; - font-weight: normal; - font-size: 24px; - margin: 0 0 10px 0; - padding: 0; - line-height: 1; -} - -div.admonition p.last { - margin-bottom: 0; -} - -div.highlight{ - background-color: white; -} - -dt:target, .highlight { - background: #FAF3E8; -} - -div.note { - background-color: #eee; - border: 1px solid #ccc; -} - -div.seealso { - background-color: #ffc; - border: 1px solid #ff6; -} - -div.topic { - background-color: #eee; -} - -div.warning { - background-color: #ffe4e4; - border: 1px solid #f66; -} - -p.admonition-title { - display: inline; -} - -p.admonition-title:after { - content: ":"; -} - -pre, tt { - font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; - font-size: 0.85em; -} - -img.screenshot { -} - -tt.descname, tt.descclassname { - font-size: 0.95em; -} - -tt.descname { - padding-right: 0.08em; -} - -img.screenshot { - -moz-box-shadow: 2px 2px 4px #eee; - -webkit-box-shadow: 2px 2px 4px #eee; - box-shadow: 2px 2px 4px #eee; -} - -table.docutils { - border: 1px solid #888; - -moz-box-shadow: 2px 2px 4px #eee; - -webkit-box-shadow: 2px 2px 4px #eee; - box-shadow: 2px 2px 4px #eee; -} - -table.docutils td, table.docutils th { - border: 1px solid #888; - padding: 0.25em 0.7em; -} - -table.field-list, table.footnote { - border: none; - -moz-box-shadow: none; - -webkit-box-shadow: none; - box-shadow: none; -} - -table.footnote { - margin: 15px 0; - width: 100%; - border: 1px solid #eee; -} - -table.field-list th { - padding: 0 0.8em 0 0; -} - -table.field-list td { - padding: 0; -} - -table.footnote td { - padding: 0.5em; -} - -dl { - margin: 0; - padding: 0; -} - -dl dd { - margin-left: 30px; -} - -pre { - padding: 0; - margin: 15px -30px; - padding: 8px; - line-height: 1.3em; - padding: 7px 30px; - background: #eee; - border-radius: 2px; - -moz-border-radius: 2px; - -webkit-border-radius: 2px; -} - -dl pre { - margin-left: -60px; - padding-left: 60px; -} - -tt { - background-color: #ecf0f3; - color: #222; - /* padding: 1px 2px; */ -} - -tt.xref, a tt { - background-color: #FBFBFB; -} - -a:hover tt { - background: #EEE; -} diff --git a/docs/_themes/flask_small/theme.conf b/docs/_themes/flask_small/theme.conf deleted file mode 100644 index 128c969649dfc3b66f28ce637a9fb1e2f75d067f..0000000000000000000000000000000000000000 --- a/docs/_themes/flask_small/theme.conf +++ /dev/null @@ -1,12 +0,0 @@ -[theme] -inherit = basic -stylesheet = flasky.css -nosidebar = true -pygments_style = flask_theme_support.FlaskyStyle - -[options] -index_logo = -index_logo_height = 120px -touch_icon = -github_fork = -github_ribbon_color = darkblue_121621 diff --git a/docs/_themes/flask_theme_support.py b/docs/_themes/flask_theme_support.py deleted file mode 100644 index 33f47449c11d241e36c83d7f95d819bbe56e2c8f..0000000000000000000000000000000000000000 --- a/docs/_themes/flask_theme_support.py +++ /dev/null @@ -1,86 +0,0 @@ -# flasky extensions. flasky pygments style based on tango style -from pygments.style import Style -from pygments.token import Keyword, Name, Comment, String, Error, \ - Number, Operator, Generic, Whitespace, Punctuation, Other, Literal - - -class FlaskyStyle(Style): - background_color = "#f8f8f8" - default_style = "" - - styles = { - # No corresponding class for the following: - #Text: "", # class: '' - Whitespace: "underline #f8f8f8", # class: 'w' - Error: "#a40000 border:#ef2929", # class: 'err' - Other: "#000000", # class 'x' - - Comment: "italic #8f5902", # class: 'c' - Comment.Preproc: "noitalic", # class: 'cp' - - Keyword: "bold #004461", # class: 'k' - Keyword.Constant: "bold #004461", # class: 'kc' - Keyword.Declaration: "bold #004461", # class: 'kd' - Keyword.Namespace: "bold #004461", # class: 'kn' - Keyword.Pseudo: "bold #004461", # class: 'kp' - Keyword.Reserved: "bold #004461", # class: 'kr' - Keyword.Type: "bold #004461", # class: 'kt' - - Operator: "#582800", # class: 'o' - Operator.Word: "bold #004461", # class: 'ow' - like keywords - - Punctuation: "bold #000000", # class: 'p' - - # because special names such as Name.Class, Name.Function, etc. - # are not recognized as such later in the parsing, we choose them - # to look the same as ordinary variables. - Name: "#000000", # class: 'n' - Name.Attribute: "#c4a000", # class: 'na' - to be revised - Name.Builtin: "#004461", # class: 'nb' - Name.Builtin.Pseudo: "#3465a4", # class: 'bp' - Name.Class: "#000000", # class: 'nc' - to be revised - Name.Constant: "#000000", # class: 'no' - to be revised - Name.Decorator: "#888", # class: 'nd' - to be revised - Name.Entity: "#ce5c00", # class: 'ni' - Name.Exception: "bold #cc0000", # class: 'ne' - Name.Function: "#000000", # class: 'nf' - Name.Property: "#000000", # class: 'py' - Name.Label: "#f57900", # class: 'nl' - Name.Namespace: "#000000", # class: 'nn' - to be revised - Name.Other: "#000000", # class: 'nx' - Name.Tag: "bold #004461", # class: 'nt' - like a keyword - Name.Variable: "#000000", # class: 'nv' - to be revised - Name.Variable.Class: "#000000", # class: 'vc' - to be revised - Name.Variable.Global: "#000000", # class: 'vg' - to be revised - Name.Variable.Instance: "#000000", # class: 'vi' - to be revised - - Number: "#990000", # class: 'm' - - Literal: "#000000", # class: 'l' - Literal.Date: "#000000", # class: 'ld' - - String: "#4e9a06", # class: 's' - String.Backtick: "#4e9a06", # class: 'sb' - String.Char: "#4e9a06", # class: 'sc' - String.Doc: "italic #8f5902", # class: 'sd' - like a comment - String.Double: "#4e9a06", # class: 's2' - String.Escape: "#4e9a06", # class: 'se' - String.Heredoc: "#4e9a06", # class: 'sh' - String.Interpol: "#4e9a06", # class: 'si' - String.Other: "#4e9a06", # class: 'sx' - String.Regex: "#4e9a06", # class: 'sr' - String.Single: "#4e9a06", # class: 's1' - String.Symbol: "#4e9a06", # class: 'ss' - - Generic: "#000000", # class: 'g' - Generic.Deleted: "#a40000", # class: 'gd' - Generic.Emph: "italic #000000", # class: 'ge' - Generic.Error: "#ef2929", # class: 'gr' - Generic.Heading: "bold #000080", # class: 'gh' - Generic.Inserted: "#00A000", # class: 'gi' - Generic.Output: "#888", # class: 'go' - Generic.Prompt: "#745334", # class: 'gp' - Generic.Strong: "bold #000000", # class: 'gs' - Generic.Subheading: "bold #800080", # class: 'gu' - Generic.Traceback: "bold #a40000", # class: 'gt' - } diff --git a/docs/authors.rst b/docs/authors.rst index 7166c483002cdc25cd0fd9ea36cda13e67700693..08e64d3e9938577725bed0bc53a0237ab29a5ecb 100644 --- a/docs/authors.rst +++ b/docs/authors.rst @@ -12,4 +12,4 @@ People who send patches and suggestions: Find more contributors on GitHub_. -.. _GitHub: http://github.com/lepture/flask-wtf/contributors +.. _GitHub: https://github.com/lepture/flask-wtf/graphs/contributors diff --git a/docs/changelog.rst b/docs/changelog.rst index 63c505786874dc6056dee5b33f7f89716fcccd8c..df64d5cbcdf597fc23795a77cada6cf6f8550483 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,14 @@ Flask-WTF Changelog =================== +Version 0.14.3 +-------------- + +Released 2020-02-06 + +- Fix deprecated imports from ``werkzeug`` and ``collections``. + + Version 0.14.2 -------------- @@ -46,7 +54,7 @@ Released 2017-01-06 (`#264`_) - ``CsrfProtect`` protects the ``DELETE`` method by default. (`#264`_) - The same CSRF token is generated for the lifetime of a request. It is exposed - as ``request.csrf_token`` for use during testing. (`#227`_, `#264`_) + as ``g.csrf_token`` for use during testing. (`#227`_, `#264`_) - ``CsrfProtect.error_handler`` is deprecated. (`#264`_) - Handlers that return a response work in addition to those that raise an @@ -191,8 +199,8 @@ Released 2014/03/21 - ``csrf_token`` for all template types `#112`_. - Make FileRequired a subclass of InputRequired `#108`_. -.. _`#108`: https://github.com/lepture/flask-wtf/issues/108 -.. _`#112`: https://github.com/lepture/flask-wtf/issues/112 +.. _`#108`: https://github.com/lepture/flask-wtf/pull/108 +.. _`#112`: https://github.com/lepture/flask-wtf/pull/112 Version 0.9.4 ------------- @@ -212,8 +220,8 @@ Released 2013/10/02 - Fix validation of recaptcha when app in testing mode `#89`_. - Bugfix for csrf module `#91`_ -.. _`#89`: https://github.com/lepture/flask-wtf/issues/89 -.. _`#91`: https://github.com/lepture/flask-wtf/issues/91 +.. _`#89`: https://github.com/lepture/flask-wtf/pull/89 +.. _`#91`: https://github.com/lepture/flask-wtf/pull/91 Version 0.9.2 diff --git a/docs/conf.py b/docs/conf.py index 0352c653acec9e4440aac2d44bae8c120ceb8b9e..4940ed0945f82ccbc640ec4e9582e874ada1f02d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -114,7 +114,7 @@ todo_include_todos = False # a list of builtin themes. try: - __import__('flask_theme_support') + __import__('flask_sphinx_themes') html_theme = 'flask' html_theme_options = { 'index_logo': 'flask-wtf.png', @@ -122,12 +122,12 @@ try: } except ImportError: print('-' * 72) - print('Flask theme not found. Run "git submodule update --init" to get it.') + print('Flask theme not found. Run "pip install flask-sphinx-themes" to get it.') print('-' * 72) html_theme = 'default' # Add any paths that contain custom themes here, relative to this directory. -html_theme_path = ['_themes'] +# html_theme_path = ['_themes'] # The name for this set of Sphinx documents. # "<project> v<release> documentation" by default. @@ -166,10 +166,10 @@ html_static_path = ['_static'] # # html_last_updated_fmt = None -# If true, SmartyPants will be used to convert quotes and dashes to +# If true, Smart Quotes will be used to convert quotes and dashes to # typographically correct entities. # -html_use_smartypants = False +smartquotes = False # Custom sidebar templates, maps document names to template names. # diff --git a/docs/config.rst b/docs/config.rst index 683e3b228535d38956a02c83f7ab6c2026f46777..c3e09299298eb77f32ca58a9e9001a305a76e256 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -28,12 +28,14 @@ Recaptcha --------- ========================= ============================================== -``RECAPTCHA_USE_SSL`` Enable/disable recaptcha through SSL. Default is - ``False``. ``RECAPTCHA_PUBLIC_KEY`` **required** A public key. ``RECAPTCHA_PRIVATE_KEY`` **required** A private key. - https://www.google.com/recaptcha/admin/create -``RECAPTCHA_OPTIONS`` **optional** A dict of configuration options. + 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 ========================= ============================================== Logging diff --git a/docs/csrf.rst b/docs/csrf.rst index a29eae5a74d1b2c307ae05a239faa554d770ddb4..b2236b0d666c0147da9e2d530201d3d1a5cdc0dc 100644 --- a/docs/csrf.rst +++ b/docs/csrf.rst @@ -1,4 +1,4 @@ -.. module:: flask_wtf.csrf +.. currentmodule:: flask_wtf.csrf .. _csrf: diff --git a/docs/form.rst b/docs/form.rst index 527954bdc930ca92514b340ca490c0f4d76a7a06..2c02e04deb4aa63ee089cb1be2e21e55b262e69e 100644 --- a/docs/form.rst +++ b/docs/form.rst @@ -4,14 +4,14 @@ Creating Forms Secure Form ----------- -.. module:: flask_wtf +.. currentmodule:: flask_wtf Without any configuration, the :class:`FlaskForm` will be a session secure form with csrf protection. We encourage you do nothing. But if you want to disable the csrf protection, you can pass:: - form = FlaskForm(csrf_enabled=False) + form = FlaskForm(meta={'csrf': False}) You can disable it globally—though you really shouldn't—with the configuration:: @@ -28,7 +28,7 @@ another secret key, config it:: File Uploads ------------ -.. module:: flask_wtf.file +.. currentmodule:: flask_wtf.file The :class:`FileField` provided by Flask-WTF differs from the WTForms-provided field. It will check that the file is a non-empty instance of @@ -44,6 +44,8 @@ field. It will check that the file is a non-empty instance of @app.route('/upload', methods=['GET', 'POST']) def upload(): + form = PhotoForm() + if form.validate_on_submit(): f = form.photo.data filename = secure_filename(f.filename) @@ -110,7 +112,7 @@ It can be used without Flask-Uploads by passing the extensions directly. :: Recaptcha --------- -.. module:: flask_wtf.recaptcha +.. currentmodule:: flask_wtf.recaptcha Flask-WTF also provides Recaptcha support through a :class:`RecaptchaField`:: diff --git a/docs/index.rst b/docs/index.rst index 1a2fb1645dde7b5e57f3ee5f37a9463248e51bfc..861a92697d1ba4c723eb14339b02c7d8bc6b6e39 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -4,9 +4,8 @@ Flask-WTF Simple integration of `Flask`_ and `WTForms`_, including CSRF, file upload, and reCAPTCHA. -.. _Flask: https://palletsprojects.com/p/flask -.. _WTForms: https://wtforms.readthedocs.io/ - +.. _Flask: https://www.palletsprojects.com/p/flask +.. _WTForms: https://wtforms.readthedocs.io/en/latest/ Features -------- diff --git a/docs/install.rst b/docs/install.rst index de08bc2fc2500d8bee5a9ce533512e2668b52678..6cfcb16e9c1ca6e1847dedf6a2673c07bf0a1df0 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -1,44 +1,30 @@ Installation ============ -This part of the documentation covers the installation of Flask-WTF. -The first step to using any software package is getting it properly installed. +The `Python Packaging Guide`_ contains general information about how to manage +your project and dependencies. +.. _Python Packaging Guide: https://packaging.python.org/current/ -Distribute & Pip +Released version ---------------- -Installing Flask-WTF is simple with `pip <http://www.pip-installer.org/>`_:: +Install or upgrade using pip. :: - $ pip install Flask-WTF + pip install -U Flask-WTF -or, with `easy_install <http://pypi.python.org/pypi/setuptools>`_:: +Development +----------- - $ easy_install Flask-WTF +The latest code is available from `GitHub`_. Clone the repository then install +using pip. :: -But, you really `shouldn't do that <https://python-packaging-user-guide.readthedocs.io/en/latest/pip_easy_install/>`_. + git clone https://github.com/lepture/flask-wtf + pip install -e ./flask-wtf +Or install the latest build from an `archive`_. :: -Get the Code ------------- + pip install -U https://github.com/lepture/flask-wtf/tarball/master -Flask-WTF is actively developed on GitHub, where the code is -`always available <https://github.com/lepture/flask-wtf>`_. - -You can either clone the public repository:: - - git clone git://github.com/lepture/flask-wtf.git - -Download the `tarball <https://github.com/lepture/flask-wtf/tarball/master>`_:: - - $ curl -OL https://github.com/lepture/flask-wtf/tarball/master - -Or, download the `zipball <https://github.com/lepture/flask-wtf/zipball/master>`_:: - - $ curl -OL https://github.com/lepture/flask-wtf/zipball/master - - -Once you have a copy of the source, you can embed it in your Python package, -or install it into your site-packages easily:: - - $ python setup.py install +.. _GitHub: https://github.com/lepture/flask-wtf +.. _archive: https://github.com/lepture/flask-wtf/archive/master.tar.gz diff --git a/docs/quickstart.rst b/docs/quickstart.rst index fa34ca7847b939937635cc3df086722dbd79762f..881402fb16ea975102f389acb7ee01457b020a27 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -52,7 +52,7 @@ Validating Forms Validating the request in your view handlers:: - @app.route('/submit', methods=('GET', 'POST')) + @app.route('/submit', methods=['GET', 'POST']) def submit(): form = MyForm() if form.validate_on_submit(): diff --git a/flask_wtf/__init__.py b/flask_wtf/__init__.py index 30ec506d0d65349bfb04b40f6bdf56a0ef5bfda2..d4e6f7db6d768d1d2507b55895e4d3c3a26cf85c 100644 --- a/flask_wtf/__init__.py +++ b/flask_wtf/__init__.py @@ -1,19 +1,7 @@ -# -*- coding: utf-8 -*- -""" - flask_wtf - ~~~~~~~~~ - - Flask-WTF extension - - :copyright: (c) 2010 by Dan Jacob. - :copyright: (c) 2013 - 2015 by Hsiaoming Yang. - :license: BSD, see LICENSE for more details. -""" -# flake8: noqa from __future__ import absolute_import from .csrf import CSRFProtect, CsrfProtect from .form import FlaskForm, Form from .recaptcha import * -__version__ = '0.14.2' +__version__ = '0.14.3' diff --git a/flask_wtf/_compat.py b/flask_wtf/_compat.py index e5956e00a6214068e03199808db41d6248158b78..c5a99234984ac8a2489eea717b8417044e4befc0 100644 --- a/flask_wtf/_compat.py +++ b/flask_wtf/_compat.py @@ -6,10 +6,12 @@ PY2 = sys.version_info[0] == 2 if not PY2: text_type = str string_types = (str,) + from collections import abc from urllib.parse import urlparse else: text_type = unicode string_types = (str, unicode) + import collections as abc from urlparse import urlparse @@ -32,4 +34,6 @@ class FlaskWTFDeprecationWarning(DeprecationWarning): warnings.simplefilter('always', FlaskWTFDeprecationWarning) -warnings.filterwarnings('ignore', category=FlaskWTFDeprecationWarning, module='wtforms|flask_wtf') +warnings.filterwarnings( + 'ignore', category=FlaskWTFDeprecationWarning, module='wtforms|flask_wtf' +) diff --git a/flask_wtf/csrf.py b/flask_wtf/csrf.py index 7ca569bb4ef038d463a3fab0bc930160830d0535..1d70ed28557d351952a456baf0db9234a9db03a3 100644 --- a/flask_wtf/csrf.py +++ b/flask_wtf/csrf.py @@ -40,11 +40,18 @@ def generate_csrf(secret_key=None, token_key=None): ) if field_name not in g: + s = URLSafeTimedSerializer(secret_key, salt='wtf-csrf-token') + if field_name not in session: session[field_name] = hashlib.sha1(os.urandom(64)).hexdigest() - s = URLSafeTimedSerializer(secret_key, salt='wtf-csrf-token') - setattr(g, field_name, s.dumps(session[field_name])) + try: + token = s.dumps(session[field_name]) + except TypeError: + session[field_name] = hashlib.sha1(os.urandom(64)).hexdigest() + token = s.dumps(session[field_name]) + + setattr(g, field_name, token) return g.get(field_name) @@ -118,7 +125,7 @@ def _get_config( value = current_app.config.get(config_name, default) if required and value is None: - raise KeyError(message) + raise RuntimeError(message) return value @@ -157,7 +164,7 @@ class CSRFProtect(object): :: app = Flask(__name__) - csrf = CsrfProtect(app) + csrf = CSRFProtect(app) Checks the ``csrf_token`` field sent with forms, or the ``X-CSRFToken`` header sent with JavaScript requests. Render the token in templates using @@ -205,15 +212,11 @@ class CSRFProtect(object): if not request.endpoint: return - view = app.view_functions.get(request.endpoint) - - if not view: - return - if request.blueprint in self._exempt_blueprints: return - dest = '%s.%s' % (view.__module__, view.__name__) + view = app.view_functions.get(request.endpoint) + dest = '{0}.{1}'.format(view.__module__, view.__name__) if dest in self._exempt_views: return @@ -221,11 +224,14 @@ class CSRFProtect(object): self.protect() def _get_csrf_token(self): - # find the ``csrf_token`` field in the subitted form - # if the form had a prefix, the name will be - # ``{prefix}-csrf_token`` + # find the token in the form data field_name = current_app.config['WTF_CSRF_FIELD_NAME'] + base_token = request.form.get(field_name) + + if base_token: + return base_token + # if the form has a prefix, the name will be {prefix}-csrf_token for key in request.form: if key.endswith(field_name): csrf_token = request.form[key] @@ -233,6 +239,7 @@ class CSRFProtect(object): if csrf_token: return csrf_token + # find the token in the headers for header_name in current_app.config['WTF_CSRF_HEADERS']: csrf_token = request.headers.get(header_name) @@ -286,7 +293,7 @@ class CSRFProtect(object): if isinstance(view, string_types): view_location = view else: - view_location = '%s.%s' % (view.__module__, view.__name__) + view_location = '.'.join((view.__module__, view.__name__)) self._exempt_views.add(view_location) return view @@ -302,8 +309,8 @@ class CSRFProtect(object): ``@app.errorhandler(CSRFError)`` instead. This will be removed in version 1.0. - The function will be passed one argument, ``reason``. By default it will - raise a :class:`~flask_wtf.csrf.CSRFError`. :: + The function will be passed one argument, ``reason``. By default it + will raise a :class:`~flask_wtf.csrf.CSRFError`. :: @csrf.error_handler def csrf_error(reason): @@ -314,15 +321,15 @@ class CSRFProtect(object): """ warnings.warn(FlaskWTFDeprecationWarning( - '"@csrf.error_handler" is deprecated. Use the standard Flask error ' - 'system with "@app.errorhandler(CSRFError)" instead. This will be' - 'removed in 1.0.' + '"@csrf.error_handler" is deprecated. Use the standard Flask ' + 'error system with "@app.errorhandler(CSRFError)" instead. This ' + 'will be removed in 1.0.' ), stacklevel=2) @wraps(view) def handler(reason): response = current_app.make_response(view(reason)) - raise CSRFError(response.get_data(as_text=True), response=response) + raise CSRFError(response=response) self._error_response = handler return view diff --git a/flask_wtf/file.py b/flask_wtf/file.py index e5851f52c225eeb29e5ce87774baf33b6e386a66..267268b534a860195fc5045bb00ba2ceb579cf29 100644 --- a/flask_wtf/file.py +++ b/flask_wtf/file.py @@ -1,10 +1,10 @@ import warnings -from collections import Iterable from werkzeug.datastructures import FileStorage from wtforms import FileField as _FileField from wtforms.validators import DataRequired, StopValidation +from ._compat import abc from ._compat import FlaskWTFDeprecationWarning @@ -47,12 +47,10 @@ class FileRequired(DataRequired): def __call__(self, form, field): if not (isinstance(field.data, FileStorage) and field.data): - if self.message is None: - message = field.gettext('This field is required.') - else: - message = self.message + raise StopValidation(self.message or field.gettext( + 'This field is required.' + )) - raise StopValidation(message) file_required = FileRequired @@ -78,7 +76,7 @@ class FileAllowed(object): filename = field.data.filename.lower() - if isinstance(self.upload_set, Iterable): + if isinstance(self.upload_set, abc.Iterable): if any(filename.endswith('.' + x) for x in self.upload_set): return @@ -91,4 +89,5 @@ class FileAllowed(object): 'File does not have an approved extension.' )) + file_allowed = FileAllowed diff --git a/flask_wtf/form.py b/flask_wtf/form.py index b8a3d96edd139838748bfd1f46671f531e0555eb..085d51ce7e874347dceef732a1c1377793f1df2b 100644 --- a/flask_wtf/form.py +++ b/flask_wtf/form.py @@ -1,7 +1,6 @@ import warnings -from flask import current_app, request -from flask import session +from flask import current_app, request, session from jinja2 import Markup from werkzeug.datastructures import CombinedMultiDict, ImmutableMultiDict from werkzeug.utils import cached_property @@ -70,7 +69,7 @@ class FlaskForm(Form): def get_translations(self, form): if not current_app.config.get('WTF_I18N_ENABLED', True): - return None + return super(FlaskForm.Meta, self).get_translations(form) return translations @@ -80,7 +79,7 @@ class FlaskForm(Form): if csrf_enabled is not None: warnings.warn(FlaskWTFDeprecationWarning( '"csrf_enabled" is deprecated and will be removed in 1.0. ' - 'Set "meta.csrf" instead.' + "Pass meta={'csrf': False} instead." ), stacklevel=3) kwargs['meta'] = kwargs.get('meta') or {} kwargs['meta'].setdefault('csrf', csrf_enabled) diff --git a/flask_wtf/html5.py b/flask_wtf/html5.py index 23e7927832fef1d815dbcac1e3c33857946592ec..a80cf67897f0af8369d106b6a84b185660114a01 100644 --- a/flask_wtf/html5.py +++ b/flask_wtf/html5.py @@ -1,7 +1,6 @@ -# coding: utf-8 -# flake8: noqa import warnings -from flask_wtf._compat import FlaskWTFDeprecationWarning + +from ._compat import FlaskWTFDeprecationWarning warnings.warn(FlaskWTFDeprecationWarning( '"flask_wtf.html5" will be removed in 1.0. ' diff --git a/flask_wtf/i18n.py b/flask_wtf/i18n.py index 4a6dda1edcbf3fe6860dd0999fd1913e92f804d3..78cfcb29ff2082f516747c96d8129bed8574eb03 100644 --- a/flask_wtf/i18n.py +++ b/flask_wtf/i18n.py @@ -1,24 +1,11 @@ -# coding: utf-8 -""" - flask_wtf.i18n - ~~~~~~~~~~~~~~ - - Internationalization support for Flask WTF. - - :copyright: (c) 2013 by Hsiaoming Yang. -""" - -from flask import _request_ctx_stack from babel import support +from flask import current_app, request +from wtforms.i18n import messages_path + try: from flask_babel import get_locale except ImportError: from flask_babelex import get_locale -try: - from wtforms.i18n import messages_path -except ImportError: - from wtforms.ext.i18n.utils import messages_path - __all__ = ('Translations', 'translations') @@ -27,43 +14,37 @@ def _get_translations(): """Returns the correct gettext translations. Copy from flask-babel with some modifications. """ - ctx = _request_ctx_stack.top - if ctx is None: + + if not request: return None + # babel should be in extensions for get_locale - if 'babel' not in ctx.app.extensions: + if 'babel' not in current_app.extensions: return None - translations = getattr(ctx, 'wtforms_translations', None) + + translations = getattr(request, 'wtforms_translations', None) + if translations is None: - dirname = messages_path() translations = support.Translations.load( - dirname, [get_locale()], domain='wtforms' + messages_path(), [get_locale()], domain='wtforms' ) - ctx.wtforms_translations = translations + request.wtforms_translations = translations + return translations class Translations(object): def gettext(self, string): t = _get_translations() - if t is None: - return string - if hasattr(t, 'ugettext'): - return t.ugettext(string) - # Python 3 has no ugettext - return t.gettext(string) + return string if t is None else t.ugettext(string) def ngettext(self, singular, plural, n): t = _get_translations() + if t is None: - if n == 1: - return singular - return plural - - if hasattr(t, 'ungettext'): - return t.ungettext(singular, plural, n) - # Python 3 has no ungettext - return t.ngettext(singular, plural, n) + return singular if n == 1 else plural + + return t.ungettext(singular, plural, n) translations = Translations() diff --git a/flask_wtf/recaptcha/validators.py b/flask_wtf/recaptcha/validators.py index bcff23d34cca52fab79be7edb62a977d5a10556d..86bb5d9674b941ab16d076a0e37d6dfcf79f67ad 100644 --- a/flask_wtf/recaptcha/validators.py +++ b/flask_wtf/recaptcha/validators.py @@ -4,11 +4,13 @@ except ImportError: # Python 3 from urllib import request as http -from flask import request, current_app +import json + +from flask import current_app, request +from werkzeug.urls import url_encode from wtforms import ValidationError -from werkzeug import url_encode + from .._compat import to_bytes, to_unicode -import json RECAPTCHA_VERIFY_SERVER = 'https://www.google.com/recaptcha/api/siteverify' RECAPTCHA_ERROR_CODES = { diff --git a/flask_wtf/recaptcha/widgets.py b/flask_wtf/recaptcha/widgets.py index 71a100650ab86891ef4770abdb0d4681e217a540..9cdc8b18bdf92fd6d6c349ef9d33e21c245565a7 100644 --- a/flask_wtf/recaptcha/widgets.py +++ b/flask_wtf/recaptcha/widgets.py @@ -1,18 +1,17 @@ # -*- coding: utf-8 -*- -from flask import current_app, Markup -from flask import json -from werkzeug import url_encode +from flask import Markup, current_app, json +from werkzeug.urls import url_encode + JSONEncoder = json.JSONEncoder RECAPTCHA_SCRIPT = u'https://www.google.com/recaptcha/api.js' - RECAPTCHA_TEMPLATE = u''' <script src='%s' async defer></script> <div class="g-recaptcha" %s></div> ''' -__all__ = ["RecaptchaWidget"] +__all__ = ['RecaptchaWidget'] class RecaptchaWidget(object): @@ -37,6 +36,6 @@ class RecaptchaWidget(object): try: public_key = current_app.config['RECAPTCHA_PUBLIC_KEY'] except KeyError: - raise RuntimeError("RECAPTCHA_PUBLIC_KEY config not set") + raise RuntimeError('RECAPTCHA_PUBLIC_KEY config not set') return self.recaptcha_html(public_key) diff --git a/setup.cfg b/setup.cfg index 6d926a06d1f02a0333e59af3365f52068a5fd203..10f47629adca9ac51237265ffde7a25b8321a98d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,14 +1,15 @@ -[egg_info] -tag_build = -tag_date = 0 -tag_svn_revision = 0 - -[aliases] -release = egg_info -RDb '' - [bdist_wheel] universal = 1 [metadata] license_file = LICENSE +long_description_content_type = text/x-rst + +[tool:pytest] +minversion = 3.0 +testpaths = tests + +[egg_info] +tag_build = +tag_date = 0 diff --git a/setup.py b/setup.py index 365d5b904d717ca022eeac5ff9aed50a5a6dcba0..a67877eccd3d74b07d0a2dd677b58eb5e3ce7572 100755 --- a/setup.py +++ b/setup.py @@ -1,12 +1,12 @@ #!/usr/bin/env python -from setuptools import setup, find_packages +from setuptools import find_packages, setup with open('README.rst') as f: readme = f.read() setup( name='Flask-WTF', - version='0.14.2', + version='0.14.3', url='https://github.com/lepture/flask-wtf', license='BSD', author='Dan Jacob', @@ -21,6 +21,7 @@ setup( install_requires=[ 'Flask', 'WTForms', + 'itsdangerous', ], classifiers=[ 'Development Status :: 5 - Production/Stable', @@ -31,13 +32,13 @@ setup( 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', diff --git a/test-requirements.txt b/test-requirements.txt deleted file mode 100644 index 4d4397a5ff73722d5ddbae67f67ab8c0ae87a8be..0000000000000000000000000000000000000000 --- a/test-requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ --e . -flask-babel -flask-uploads -nose -coverage diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/tests/base.py b/tests/base.py deleted file mode 100644 index 70684f0b1f5de90220016bb922fc9046776cddff..0000000000000000000000000000000000000000 --- a/tests/base.py +++ /dev/null @@ -1,133 +0,0 @@ -from __future__ import with_statement - -from contextlib import contextmanager -from unittest import TestCase as _TestCase - -import logging -from flask import Flask, jsonify, render_template -from wtforms import HiddenField, StringField, SubmitField -from wtforms.validators import DataRequired - -from flask_wtf import FlaskForm -from flask_wtf._compat import text_type - - -def to_unicode(text): - if not isinstance(text, text_type): - return text.decode('utf-8') - return text - - -class MyForm(FlaskForm): - SECRET_KEY = "a poorly kept secret." - name = StringField("Name", validators=[DataRequired()]) - submit = SubmitField("Submit") - - -class HiddenFieldsForm(FlaskForm): - SECRET_KEY = "a poorly kept secret." - name = HiddenField() - url = HiddenField() - method = HiddenField() - secret = HiddenField() - submit = SubmitField("Submit") - - def __init__(self, *args, **kwargs): - super(HiddenFieldsForm, self).__init__(*args, **kwargs) - self.method.name = '_method' - - -class SimpleForm(FlaskForm): - SECRET_KEY = "a poorly kept secret." - pass - - -class CaptureHandler(logging.Handler): - def __init__(self): - self.records = [] - logging.Handler.__init__(self, logging.DEBUG) - - def emit(self, record): - self.records.append(record) - - def __iter__(self): - return iter(self.records) - - def __len__(self): - return len(self.records) - - def __getitem__(self, item): - return self.records[item] - - -@contextmanager -def capture_logging(logger): - handler = CaptureHandler() - - try: - logger.addHandler(handler) - yield handler - finally: - logger.removeHandler(handler) - - -class TestCase(_TestCase): - def setUp(self): - self.app = self.create_app() - self.client = self.app.test_client() - - def create_app(self): - app = Flask(__name__) - app.secret_key = "secret" - - @app.route("/", methods=("GET", "POST")) - def index(): - - form = MyForm() - if form.validate_on_submit(): - name = form.name.data.upper() - else: - name = '' - - return render_template("index.html", - form=form, - name=name) - - @app.route("/simple/", methods=("POST",)) - def simple(): - form = SimpleForm() - form.validate() - assert form.meta.csrf - assert not form.validate() - return "OK" - - @app.route("/two_forms/", methods=("POST",)) - def two_forms(): - form = SimpleForm() - assert form.meta.csrf - assert form.validate() - assert form.validate() - form2 = SimpleForm() - assert form2.meta.csrf - assert form2.validate() - return "OK" - - @app.route("/hidden/") - def hidden(): - - form = HiddenFieldsForm() - return render_template("hidden.html", form=form) - - @app.route("/ajax/", methods=("POST",)) - def ajax_submit(): - form = MyForm() - if form.validate_on_submit(): - return jsonify(name=form.name.data, - success=True, - errors=None) - - return jsonify(name=None, - #errors=form.errors, - success=False) - - return app diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..61582a5866f26faed2f410f5a559ef7679604957 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,36 @@ +import pytest +from flask import Flask as _Flask + + +class Flask(_Flask): + testing = True + secret_key = __name__ + + def make_response(self, rv): + if rv is None: + rv = '' + + return super(Flask, self).make_response(rv) + + +@pytest.fixture +def app(): + app = Flask(__name__) + return app + + +@pytest.yield_fixture +def app_ctx(app): + with app.app_context() as ctx: + yield ctx + + +@pytest.yield_fixture +def req_ctx(app): + with app.test_request_context() as ctx: + yield ctx + + +@pytest.fixture +def client(app): + return app.test_client() diff --git a/tests/flask.png b/tests/flask.png deleted file mode 100644 index f98ce466e407b10035f04aff906a34a5ffe8ef96..0000000000000000000000000000000000000000 Binary files a/tests/flask.png and /dev/null differ diff --git a/tests/flask.txt b/tests/flask.txt deleted file mode 100644 index b5c4d04e99e907e3b05b056b18473b314d16fc02..0000000000000000000000000000000000000000 --- a/tests/flask.txt +++ /dev/null @@ -1 +0,0 @@ -Hello Flask. diff --git a/tests/templates/csrf.html b/tests/templates/csrf.html deleted file mode 100644 index 42257486b95f21e9cee4aea7a8ec927821ec667f..0000000000000000000000000000000000000000 --- a/tests/templates/csrf.html +++ /dev/null @@ -1,7 +0,0 @@ -<!DOCTYPE html> - -<html> - <body> - token: {{ csrf_token() }} - </body> -</html> diff --git a/tests/templates/csrf_macro.html b/tests/templates/csrf_macro.html deleted file mode 100644 index 3e68e62d7f9ff6dca93b150b188267468996d7a3..0000000000000000000000000000000000000000 --- a/tests/templates/csrf_macro.html +++ /dev/null @@ -1,3 +0,0 @@ -{% macro render_csrf_token() %} - <input name="csrf_token" type="hidden" value="{{ csrf_token() }}"> -{% endmacro %} diff --git a/tests/templates/hidden.html b/tests/templates/hidden.html deleted file mode 100644 index 3cd7f9b7dd6b3b68c21779e2d123cb9aef0d5214..0000000000000000000000000000000000000000 --- a/tests/templates/hidden.html +++ /dev/null @@ -1,18 +0,0 @@ - -<!DOCTYPE html> - -<html> - <body> - {% if name %} - <p>{{ name }}</p> - {% endif %} - - {{ form.errors }} - <form method="POST" enctype="multipart/form-data"> - {{ form.hidden_tag() }} - <p> - {{ form.submit }} - </p> - </form> - </body> -</html> diff --git a/tests/templates/import_csrf.html b/tests/templates/import_csrf.html deleted file mode 100644 index bec50fa74f5c4a59d524f2bb755250e210305291..0000000000000000000000000000000000000000 --- a/tests/templates/import_csrf.html +++ /dev/null @@ -1,2 +0,0 @@ -{% import "csrf_macro.html" as h %} -{{ h.render_csrf_token() }} diff --git a/tests/templates/index.html b/tests/templates/index.html deleted file mode 100644 index 229f09d701fa2d98dff0bd1156529c4d5f6b16bf..0000000000000000000000000000000000000000 --- a/tests/templates/index.html +++ /dev/null @@ -1,22 +0,0 @@ - -<!DOCTYPE html> - -<html> - <body> - {% if name %} - <p>{{ name }}</p> - {% endif %} - - {{ form.errors }} - <form method="POST" enctype="multipart/form-data"> - {{ form.hidden_tag() }} - <p> - {{ form.name.label }} - {{ form.name }} - </p> - <p> - {{ form.submit }} - </p> - </form> - </body> -</html> diff --git a/tests/templates/recaptcha.html b/tests/templates/recaptcha.html deleted file mode 100644 index d138f16e4ea03f323448118b04c6b7d82cd53999..0000000000000000000000000000000000000000 --- a/tests/templates/recaptcha.html +++ /dev/null @@ -1,17 +0,0 @@ -<!DOCTYPE html> - -<html> - <body> - {{ form.errors }} - <form method="POST"> - {{ form.hidden_tag() }} - <p> - {{ form.recaptcha }} - </p> - <p> - {{ form.submit }} - </p> - </form> - </body> -</html> - diff --git a/tests/templates/upload.html b/tests/templates/upload.html deleted file mode 100644 index ab24a180600cd0252558eec3b16653dff37ef0e3..0000000000000000000000000000000000000000 --- a/tests/templates/upload.html +++ /dev/null @@ -1,20 +0,0 @@ - -<!DOCTYPE html> - -<html> - <body> - {% if filedata %} - <h3>{{ filedata.filename }}</h3> - {% endif %} - <form method="POST" enctype="multipart/form-data"> - {{ form.hidden_tag() }} - <p> - {{ form.upload.label }} - {{ form.upload }} - </p> - <p> - <input type="submit" value="Submit"> - </p> - </form> - </body> -</html> diff --git a/tests/test_csrf.py b/tests/test_csrf.py deleted file mode 100644 index 942e68f5a4ec794a56513ff3bacc4da02505ff53..0000000000000000000000000000000000000000 --- a/tests/test_csrf.py +++ /dev/null @@ -1,321 +0,0 @@ -from __future__ import with_statement - -import re -import warnings - -from flask import Blueprint, abort, render_template, request -from flask import g -from wtforms import ValidationError - -from flask_wtf._compat import FlaskWTFDeprecationWarning -from flask_wtf.csrf import CSRFError, CSRFProtect, generate_csrf, validate_csrf -from .base import MyForm, TestCase - - -class TestCSRF(TestCase): - def setUp(self): - app = self.create_app() - app.config['WTF_CSRF_SECRET_KEY'] = "a poorly kept secret." - csrf = CSRFProtect(app) - self.csrf = csrf - - @csrf.exempt - @app.route('/csrf-exempt', methods=['GET', 'POST']) - def csrf_exempt(): - form = MyForm() - if form.validate_on_submit(): - name = form.name.data.upper() - else: - name = '' - - return render_template( - "index.html", form=form, name=name - ) - - @csrf.exempt - @app.route('/csrf-protect-method', methods=['GET', 'POST']) - def csrf_protect_method(): - csrf.protect() - return 'protected' - - bp = Blueprint('csrf', __name__) - - @bp.route('/foo', methods=['GET', 'POST']) - def foo(): - return 'foo' - - app.register_blueprint(bp, url_prefix='/bar') - self.bp = bp - self.app = app - self.client = self.app.test_client() - - def test_invalid_csrf(self): - response = self.client.post("/", data={"name": "danny"}) - assert response.status_code == 400 - - @self.app.errorhandler(CSRFError) - def handle_csrf_error(e): - return e, 200 - - response = self.client.post("/", data={"name": "danny"}) - assert response.status_code == 200 - assert b'The CSRF token is missing.' in response.data - - def test_invalid_csrf2(self): - # tests with bad token - response = self.client.post("/", data={ - "name": "danny", - "csrf_token": "9999999999999##test" - # will work only if greater than time.time() - }) - assert response.status_code == 400 - - def test_invalid_secure_csrf3(self): - # test with multiple separators - response = self.client.post("/", data={ - "name": "danny", - "csrf_token": "1378915137.722##foo##bar##and" - # will work only if greater than time.time() - }) - assert response.status_code == 400 - - def test_valid_csrf(self): - with self.client: - self.client.get('/') - csrf_token = g.csrf_token - - response = self.client.post("/", data={ - "name": "danny", - "csrf_token": csrf_token - }) - assert b'DANNY' in response.data - - def test_prefixed_csrf(self): - with self.client: - self.client.get('/') - csrf_token = g.csrf_token - - response = self.client.post('/', data={ - 'prefix-name': 'David', - 'prefix-csrf_token': csrf_token, - }) - assert response.status_code == 200 - - def test_invalid_secure_csrf(self): - with self.client: - self.client.get('/', base_url='https://localhost/') - csrf_token = g.csrf_token - - response = self.client.post( - "/", - data={"name": "danny"}, - headers={'X-CSRFToken': csrf_token}, - base_url='https://localhost/', - ) - assert response.status_code == 400 - assert b'The referrer header is missing.' in response.data - - response = self.client.post( - "/", - data={"name": "danny"}, - headers={ - 'X-CSRFToken': csrf_token, - }, - environ_base={ - 'HTTP_REFERER': 'https://example.com/', - }, - base_url='https://localhost/', - ) - assert response.status_code == 400 - assert b'The referrer does not match the host.' in response.data - - response = self.client.post( - "/", - data={"name": "danny"}, - headers={ - 'X-CSRFToken': csrf_token, - }, - environ_base={ - 'HTTP_REFERER': 'http://localhost/', - }, - base_url='https://localhost/', - ) - assert response.status_code == 400 - assert b'The referrer does not match the host.' in response.data - - response = self.client.post( - "/", - data={"name": "danny"}, - headers={ - 'X-CSRFToken': csrf_token, - }, - environ_base={ - 'HTTP_REFERER': 'https://localhost:3000/', - }, - base_url='https://localhost/', - ) - assert response.status_code == 400 - assert b'The referrer does not match the host.' in response.data - - def test_valid_secure_csrf(self): - with self.client: - self.client.get('/', base_url='https://localhost/') - csrf_token = g.csrf_token - - response = self.client.post( - "/", - data={"name": "danny"}, - headers={ - 'X-CSRFToken': csrf_token, - }, - environ_base={ - 'HTTP_REFERER': 'https://localhost/', - }, - base_url='https://localhost/', - ) - assert response.status_code == 200 - - def test_valid_csrf_method(self): - with self.client: - self.client.get('/') - csrf_token = g.csrf_token - - response = self.client.post("/csrf-protect-method", data={ - "csrf_token": csrf_token - }) - assert response.status_code == 200 - - def test_empty_csrf_headers(self): - with self.client: - self.client.get('/', base_url='https://localhost/') - csrf_token = g.csrf_token - - self.app.config['WTF_CSRF_HEADERS'] = list() - response = self.client.post( - "/", - data={"name": "danny"}, - headers={ - 'X-CSRFToken': csrf_token, - }, - environ_base={ - 'HTTP_REFERER': 'https://localhost/', - }, - base_url='https://localhost/', - ) - assert response.status_code == 400 - - def test_custom_csrf_headers(self): - with self.client: - self.client.get('/', base_url='https://localhost/') - csrf_token = g.csrf_token - - self.app.config['WTF_CSRF_HEADERS'] = ['X-XSRF-TOKEN'] - response = self.client.post( - "/", - data={"name": "danny"}, - headers={ - 'X-XSRF-TOKEN': csrf_token, - }, - environ_base={ - 'HTTP_REFERER': 'https://localhost/', - }, - base_url='https://localhost/', - ) - assert response.status_code == 200 - - def test_not_endpoint(self): - response = self.client.post('/not-endpoint') - assert response.status_code == 404 - - def test_testing(self): - self.app.testing = True - self.client.post("/", data={"name": "danny"}) - - def test_csrf_exempt_view_with_form(self): - with self.client: - self.client.get('/', base_url='https://localhost/') - csrf_token = g.csrf_token - - response = self.client.post("/csrf-exempt", data={ - "name": "danny", - "csrf_token": csrf_token - }) - assert b'DANNY' in response.data - - def test_validate_csrf(self): - with self.app.test_request_context(): - self.assertRaises(ValidationError, validate_csrf, None) - self.assertRaises(ValidationError, validate_csrf, 'invalid') - validate_csrf(generate_csrf()) - - def test_validate_not_expiring_csrf(self): - with self.app.test_request_context(): - csrf_token = generate_csrf() - validate_csrf(csrf_token, time_limit=False) - - def test_csrf_token_helper(self): - @self.app.route("/token") - def withtoken(): - return render_template("csrf.html") - - with self.client: - response = self.client.get('/token') - assert re.search(br'token: ([0-9a-zA-Z\-._]+)', response.data) - - def test_csrf_blueprint(self): - response = self.client.post('/bar/foo') - assert response.status_code == 400 - - self.csrf.exempt(self.bp) - response = self.client.post('/bar/foo') - assert response.status_code == 200 - - def test_csrf_token_macro(self): - @self.app.route("/token") - def withtoken(): - return render_template("import_csrf.html") - - with self.client: - response = self.client.get('/token') - assert g.csrf_token in response.data.decode('utf8') - - def test_csrf_custom_token_key(self): - with self.app.test_request_context(): - # Generate a normal and a custom CSRF token - default_csrf_token = generate_csrf() - custom_csrf_token = generate_csrf(token_key='oauth_state') - - # Verify they are different due to using different session keys - self.assertNotEqual(default_csrf_token, custom_csrf_token) - - # However, the custom key can validate as well - validate_csrf(custom_csrf_token, token_key='oauth_state') - - def test_old_error_handler(self): - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter('always', FlaskWTFDeprecationWarning) - - @self.csrf.error_handler - def handle_csrf_error(reason): - return 'caught csrf return' - - self.assertEqual(len(w), 1) - assert issubclass(w[0].category, FlaskWTFDeprecationWarning) - assert 'app.errorhandler(CSRFError)' in str(w[0].message) - - rv = self.client.post('/', data={'name': 'david'}) - assert b'caught csrf return' in rv.data - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter('always', FlaskWTFDeprecationWarning) - - @self.csrf.error_handler - def handle_csrf_error(reason): - abort(401, 'caught csrf abort') - - self.assertEqual(len(w), 1) - assert issubclass(w[0].category, FlaskWTFDeprecationWarning) - assert 'app.errorhandler(CSRFError)' in str(w[0].message) - - rv = self.client.post('/', data={'name': 'david'}) - assert b'caught csrf abort' in rv.data diff --git a/tests/test_csrf_extension.py b/tests/test_csrf_extension.py new file mode 100644 index 0000000000000000000000000000000000000000..456405d4b5effc1eaa4a2da967984c6f33e74b01 --- /dev/null +++ b/tests/test_csrf_extension.py @@ -0,0 +1,194 @@ +import pytest +from flask import Blueprint, abort, g, render_template_string, request + +from flask_wtf import FlaskForm +from flask_wtf._compat import FlaskWTFDeprecationWarning +from flask_wtf.csrf import CSRFError, CSRFProtect, CsrfProtect, generate_csrf + + +@pytest.fixture +def app(app): + CSRFProtect(app) + + @app.route('/', methods=['GET', 'POST']) + def index(): + pass + + @app.after_request + def add_csrf_header(response): + response.headers.set('X-CSRF-Token', generate_csrf()) + return response + + return app + + +@pytest.fixture +def csrf(app): + return app.extensions['csrf'] + + +def test_render_token(req_ctx): + token = generate_csrf() + assert render_template_string('{{ csrf_token() }}') == token + + +def test_protect(app, client, app_ctx): + response = client.post('/') + assert response.status_code == 400 + assert 'The CSRF token is missing.' in response.get_data(as_text=True) + + app.config['WTF_CSRF_ENABLED'] = False + assert client.post('/').get_data() == b'' + app.config['WTF_CSRF_ENABLED'] = True + + app.config['WTF_CSRF_CHECK_DEFAULT'] = False + assert client.post('/').get_data() == b'' + app.config['WTF_CSRF_CHECK_DEFAULT'] = True + + assert client.options('/').status_code == 200 + assert client.post('/not-found').status_code == 404 + + response = client.get('/') + assert response.status_code == 200 + token = response.headers['X-CSRF-Token'] + assert client.post('/', data={'csrf_token': token}).status_code == 200 + assert client.post( + '/', data={'prefix-csrf_token': token} + ).status_code == 200 + assert client.post('/', data={'prefix-csrf_token': ''}).status_code == 400 + assert client.post('/', headers={'X-CSRF-Token': token}).status_code == 200 + + +def test_same_origin(client): + token = client.get('/').headers['X-CSRF-Token'] + response = client.post('/', base_url='https://localhost', headers={ + 'X-CSRF-Token': token + }) + data = response.get_data(as_text=True) + assert 'The referrer header is missing.' in data + + response = client.post('/', base_url='https://localhost', headers={ + 'X-CSRF-Token': token, 'Referer': 'http://localhost/' + }) + data = response.get_data(as_text=True) + assert 'The referrer does not match the host.' in data + + response = client.post('/', base_url='https://localhost', headers={ + 'X-CSRF-Token': token, 'Referer': 'https://other/' + }) + data = response.get_data(as_text=True) + assert 'The referrer does not match the host.' in data + + response = client.post('/', base_url='https://localhost', headers={ + 'X-CSRF-Token': token, 'Referer': 'https://localhost:8080/' + }) + data = response.get_data(as_text=True) + assert 'The referrer does not match the host.' in data + + response = client.post('/', base_url='https://localhost', headers={ + 'X-CSRF-Token': token, 'Referer': 'https://localhost/' + }) + assert response.status_code == 200 + + +def test_form_csrf_short_circuit(app, client): + @app.route('/skip', methods=['POST']) + def skip(): + assert g.get('csrf_valid') + # don't pass the token, then validate the form + # this would fail if CSRFProtect didn't run + form = FlaskForm(None) + assert form.validate() + + token = client.get('/').headers['X-CSRF-Token'] + response = client.post('/skip', headers={'X-CSRF-Token': token}) + assert response.status_code == 200 + + +def test_exempt_view(app, csrf, client): + @app.route('/exempt', methods=['POST']) + @csrf.exempt + def exempt(): + pass + + response = client.post('/exempt') + assert response.status_code == 200 + + csrf.exempt('test_csrf_extension.index') + response = client.post('/') + assert response.status_code == 200 + + +def test_manual_protect(app, csrf, client): + @app.route('/manual', methods=['GET', 'POST']) + @csrf.exempt + def manual(): + csrf.protect() + + response = client.get('/manual') + assert response.status_code == 200 + + response = client.post('/manual') + assert response.status_code == 400 + + +def test_exempt_blueprint(app, csrf, client): + bp = Blueprint('exempt', __name__, url_prefix='/exempt') + csrf.exempt(bp) + + @bp.route('/', methods=['POST']) + def index(): + pass + + app.register_blueprint(bp) + response = client.post('/exempt/') + assert response.status_code == 200 + + +def test_error_handler(app, client): + @app.errorhandler(CSRFError) + def handle_csrf_error(e): + return e.description.lower() + + response = client.post('/') + assert response.get_data(as_text=True) == 'the csrf token is missing.' + + +def test_validate_error_logged(client, monkeypatch): + from flask_wtf.csrf import logger + + messages = [] + + def assert_info(message): + messages.append(message) + + monkeypatch.setattr(logger, 'info', assert_info) + + client.post('/') + assert len(messages) == 1 + assert messages[0] == 'The CSRF token is missing.' + + +def test_deprecated_csrfprotect(recwarn): + CsrfProtect() + w = recwarn.pop(FlaskWTFDeprecationWarning) + assert 'CSRFProtect' in str(w.message) + + +def test_deprecated_error_handler(csrf, client, recwarn): + @csrf.error_handler + def handle_csrf_error(reason): + if 'abort' in request.form: + abort(418) + + return 'return' + + w = recwarn.pop(FlaskWTFDeprecationWarning) + assert '@app.errorhandler' in str(w.message) + + response = client.post('/', data={'abort': '1'}) + assert response.status_code == 418 + + response = client.post('/') + assert response.status_code == 200 + assert 'return' in response.get_data(as_text=True) diff --git a/tests/test_csrf_form.py b/tests/test_csrf_form.py new file mode 100644 index 0000000000000000000000000000000000000000..011edaced584a26fee8c032145db2d81dcedb58f --- /dev/null +++ b/tests/test_csrf_form.py @@ -0,0 +1,96 @@ +import pytest +from flask import g, session +from wtforms import ValidationError + +from flask_wtf import FlaskForm +from flask_wtf.csrf import generate_csrf, validate_csrf + + +def test_csrf_requires_secret_key(app, req_ctx): + # use secret key set by test setup + generate_csrf() + # fail with no key + app.secret_key = None + pytest.raises(RuntimeError, generate_csrf) + # use WTF_CSRF config + app.config['WTF_CSRF_SECRET_KEY'] = 'wtf_secret' + generate_csrf() + del app.config['WTF_CSRF_SECRET_KEY'] + # use direct argument + generate_csrf(secret_key='direct') + + +def test_token_stored_by_generate(req_ctx): + generate_csrf() + assert 'csrf_token' in session + assert 'csrf_token' in g + + +def test_custom_token_key(req_ctx): + generate_csrf(token_key='oauth_token') + assert 'oauth_token' in session + assert 'oauth_token' in g + + +def test_token_cached(req_ctx): + assert generate_csrf() == generate_csrf() + + +def test_validate(req_ctx): + validate_csrf(generate_csrf()) + + +def test_validation_errors(req_ctx): + e = pytest.raises(ValidationError, validate_csrf, None) + assert str(e.value) == 'The CSRF token is missing.' + + e = pytest.raises(ValidationError, validate_csrf, 'no session') + assert str(e.value) == 'The CSRF session token is missing.' + + token = generate_csrf() + e = pytest.raises(ValidationError, validate_csrf, token, time_limit=-1) + assert str(e.value) == 'The CSRF token has expired.' + + e = pytest.raises(ValidationError, validate_csrf, 'invalid') + assert str(e.value) == 'The CSRF token is invalid.' + + other_token = generate_csrf(token_key='other_csrf') + e = pytest.raises(ValidationError, validate_csrf, other_token) + assert str(e.value) == 'The CSRF tokens do not match.' + + +def test_form_csrf(app, client, app_ctx): + @app.route('/', methods=['GET', 'POST']) + def index(): + f = FlaskForm() + + if f.validate_on_submit(): + return 'good' + + if f.errors: + return f.csrf_token.errors[0] + + return f.csrf_token.current_token + + response = client.get('/') + assert response.get_data(as_text=True) == g.csrf_token + + response = client.post('/') + assert response.get_data(as_text=True) == 'The CSRF token is missing.' + + response = client.post('/', data={'csrf_token': g.csrf_token}) + assert response.get_data(as_text=True) == 'good' + + +def test_validate_error_logged(req_ctx, monkeypatch): + from flask_wtf.csrf import logger + + messages = [] + + def assert_info(message): + messages.append(message) + + monkeypatch.setattr(logger, 'info', assert_info) + FlaskForm().validate() + assert len(messages) == 1 + assert messages[0] == 'The CSRF token is missing.' diff --git a/tests/test_deprecated.py b/tests/test_deprecated.py deleted file mode 100644 index 7c72c5686d7f90f94b26a6f1f24601df438fe22d..0000000000000000000000000000000000000000 --- a/tests/test_deprecated.py +++ /dev/null @@ -1,45 +0,0 @@ -import warnings -from unittest import TestCase - -from wtforms.compat import with_metaclass -from wtforms.form import FormMeta - -from flask_wtf import CsrfProtect, FlaskForm, Form -from flask_wtf._compat import FlaskWTFDeprecationWarning - - -class TestDeprecated(TestCase): - def test_deprecated_form(self): - with warnings.catch_warnings(): - warnings.simplefilter('error', FlaskWTFDeprecationWarning) - - class F1(Form): - pass - - self.assertRaises(FlaskWTFDeprecationWarning, F1) - - class FMeta(FormMeta): - pass - - class F2(with_metaclass(FMeta, Form)): - pass - - self.assertRaises(FlaskWTFDeprecationWarning, F2) - - def test_deprecated_html5(self): - with warnings.catch_warnings(): - warnings.simplefilter('error', FlaskWTFDeprecationWarning) - self.assertRaises(FlaskWTFDeprecationWarning, __import__, 'flask_wtf.html5') - - def test_deprecated_csrf_enabled(self): - class F(FlaskForm): - pass - - with warnings.catch_warnings(): - warnings.simplefilter('error', FlaskWTFDeprecationWarning) - self.assertRaises(FlaskWTFDeprecationWarning, F, csrf_enabled=False) - - def test_deprecated_csrfprotect(self): - with warnings.catch_warnings(): - warnings.simplefilter('error', FlaskWTFDeprecationWarning) - self.assertRaises(FlaskWTFDeprecationWarning, CsrfProtect) diff --git a/tests/test_file.py b/tests/test_file.py new file mode 100644 index 0000000000000000000000000000000000000000..4843101b52510c2959668bde6d6019928afe436d --- /dev/null +++ b/tests/test_file.py @@ -0,0 +1,95 @@ +import pytest +from werkzeug.datastructures import FileStorage, MultiDict +from wtforms import FileField as BaseFileField + +from flask_wtf import FlaskForm +from flask_wtf._compat import FlaskWTFDeprecationWarning +from flask_wtf.file import FileAllowed, FileField, FileRequired + + +@pytest.fixture +def form(req_ctx): + class UploadForm(FlaskForm): + class Meta: + csrf = False + + file = FileField() + + return UploadForm + + +def test_process_formdata(form): + assert form(MultiDict((('file', FileStorage()),))).file.data is None + assert form( + MultiDict((('file', FileStorage(filename='real')),)) + ).file.data is not None + + +def test_file_required(form): + form.file.kwargs['validators'] = [FileRequired()] + + f = form() + assert not f.validate() + assert f.file.errors[0] == 'This field is required.' + + f = form(file='not a file') + assert not f.validate() + assert f.file.errors[0] == 'This field is required.' + + f = form(file=FileStorage()) + assert not f.validate() + + f = form(file=FileStorage(filename='real')) + assert f.validate() + + +def test_file_allowed(form): + form.file.kwargs['validators'] = [FileAllowed(('txt',))] + + f = form() + assert f.validate() + + f = form(file=FileStorage(filename='test.txt')) + assert f.validate() + + f = form(file=FileStorage(filename='test.png')) + assert not f.validate() + assert f.file.errors[0] == 'File does not have an approved extension: txt' + + +def test_file_allowed_uploadset(app, form): + pytest.importorskip('flask_uploads') + from flask_uploads import UploadSet, configure_uploads + + app.config['UPLOADS_DEFAULT_DEST'] = 'uploads' + txt = UploadSet('txt', extensions=('txt',)) + configure_uploads(app, (txt,)) + form.file.kwargs['validators'] = [FileAllowed(txt)] + + f = form() + assert f.validate() + + f = form(file=FileStorage(filename='test.txt')) + assert f.validate() + + f = form(file=FileStorage(filename='test.png')) + assert not f.validate() + assert f.file.errors[0] == 'File does not have an approved extension.' + + +def test_validate_base_field(req_ctx): + class F(FlaskForm): + class Meta: + csrf = False + + f = BaseFileField(validators=[FileRequired()]) + + assert not F().validate() + assert not F(f=FileStorage()).validate() + assert F(f=FileStorage(filename='real')).validate() + + +def test_deprecated_filefield(recwarn, form): + assert not form().file.has_file() + w = recwarn.pop(FlaskWTFDeprecationWarning) + assert 'has_file' in str(w.message) diff --git a/tests/test_form.py b/tests/test_form.py new file mode 100644 index 0000000000000000000000000000000000000000..12d439a45680a1815d5ea6758613302c5ed52253 --- /dev/null +++ b/tests/test_form.py @@ -0,0 +1,165 @@ +from io import BytesIO + +from flask import json, request +from wtforms import FileField, HiddenField, IntegerField, StringField +from wtforms.compat import with_metaclass +from wtforms.form import FormMeta +from wtforms.validators import DataRequired +from wtforms.widgets import HiddenInput + +from flask_wtf import FlaskForm, Form +from flask_wtf._compat import FlaskWTFDeprecationWarning + + +class BasicForm(FlaskForm): + class Meta: + csrf = False + + name = StringField(validators=[DataRequired()]) + avatar = FileField() + + +def test_populate_from_form(app, client): + @app.route('/', methods=['POST']) + def index(): + form = BasicForm() + assert form.name.data == 'form' + + client.post('/', data={'name': 'form'}) + + +def test_populate_from_files(app, client): + @app.route('/', methods=['POST']) + def index(): + form = BasicForm() + assert form.avatar.data is not None + assert form.avatar.data.filename == 'flask.png' + + client.post('/', data={ + 'name': 'files', 'avatar': (BytesIO(), 'flask.png') + }) + + +def test_populate_from_json(app, client): + @app.route('/', methods=['POST']) + def index(): + form = BasicForm() + assert form.name.data == 'json' + + client.post( + '/', data=json.dumps({'name': 'json'}), + content_type='application/json' + ) + + +def test_populate_manually(app, client): + @app.route('/', methods=['POST']) + def index(): + form = BasicForm(request.args) + assert form.name.data == 'args' + + client.post('/', query_string={'name': 'args'}) + + +def test_populate_none(app, client): + @app.route('/', methods=['POST']) + def index(): + form = BasicForm(None) + assert form.name.data is None + + client.post('/', data={'name': 'ignore'}) + + +def test_validate_on_submit(app, client): + @app.route('/', methods=['POST']) + def index(): + form = BasicForm() + assert form.is_submitted() + assert not form.validate_on_submit() + assert 'name' in form.errors + + client.post('/') + + +def test_no_validate_on_get(app, client): + @app.route('/', methods=['GET', 'POST']) + def index(): + form = BasicForm() + assert not form.validate_on_submit() + assert 'name' not in form.errors + + client.get('/') + + +def test_hidden_tag(req_ctx): + class F(BasicForm): + class Meta: + csrf = True + + key = HiddenField() + count = IntegerField(widget=HiddenInput()) + + f = F() + out = f.hidden_tag() + assert all(x in out for x in ('csrf_token', 'count', 'key')) + assert 'avatar' not in out + assert 'csrf_token' not in f.hidden_tag('count', 'key') + + +def test_deprecated_form(req_ctx, recwarn): + class F(Form): + pass + + F() + w = recwarn.pop(FlaskWTFDeprecationWarning) + assert 'FlaskForm' in str(w.message) + + +def test_custom_meta_with_deprecated_form(req_ctx, recwarn): + class FMeta(FormMeta): + pass + + class F(with_metaclass(FMeta, Form)): + pass + + F() + recwarn.pop(FlaskWTFDeprecationWarning) + + +def test_deprecated_csrf_enabled(req_ctx, recwarn): + class F(FlaskForm): + pass + + F(csrf_enabled=False) + w = recwarn.pop(FlaskWTFDeprecationWarning) + assert "meta={'csrf': False}" in str(w.message) + + +def test_set_default_message_language(app, client): + + @app.route('/default', methods=['POST']) + def default(): + form = BasicForm() + assert not form.validate_on_submit() + assert 'This field is required.' in form.name.errors + + client.post('/default', data={'name': ' '}) + + @app.route('/es', methods=['POST']) + def es(): + app.config['WTF_I18N_ENABLED'] = False + + class MyBaseForm(FlaskForm): + class Meta: + csrf = False + locales = ['es'] + + class NameForm(MyBaseForm): + name = StringField(validators=[DataRequired()]) + + form = NameForm() + assert form.meta.locales == ['es'] + assert not form.validate_on_submit() + assert 'Este campo es obligatorio.' in form.name.errors + + client.post('/es', data={'name': ' '}) diff --git a/tests/test_html5.py b/tests/test_html5.py new file mode 100644 index 0000000000000000000000000000000000000000..b4ced05d91b12435037bffcddb42d72cd2c033f5 --- /dev/null +++ b/tests/test_html5.py @@ -0,0 +1,7 @@ +from flask_wtf._compat import FlaskWTFDeprecationWarning + + +def test_deprecated_html5(recwarn): + __import__('flask_wtf.html5') + w = recwarn.pop(FlaskWTFDeprecationWarning) + assert 'wtforms.fields.html5' in str(w.message) diff --git a/tests/test_i18n.py b/tests/test_i18n.py index 310047f41f06cfac8c754754df25bfecedccd7c3..a2d5eae132c4eb6ee3591e4d9550f10047c49e09 100644 --- a/tests/test_i18n.py +++ b/tests/test_i18n.py @@ -1,35 +1,74 @@ -from __future__ import with_statement +# coding=utf8 +import pytest +from flask import request +from wtforms import StringField +from wtforms.validators import DataRequired, Length -from .base import TestCase, to_unicode +from flask_wtf import FlaskForm -class TestI18NCase(TestCase): - def test_i18n_disabled(self): - self.app.config['WTF_CSRF_ENABLED'] = False - response = self.client.post( - "/", - headers={'Accept-Language': 'zh-CN,zh;q=0.8'}, - data={} - ) - assert b'This field is required.' in response.data +class NameForm(FlaskForm): + class Meta: + csrf = False - def test_i18n_enabled(self): - from flask import request + name = StringField(validators=[DataRequired(), Length(min=8)]) + + +def test_no_extension(app, client): + @app.route('/', methods=['POST']) + def index(): + form = NameForm() + form.validate() + assert form.name.errors[0] == 'This field is required.' + + client.post( + '/', headers={'Accept-Language': 'zh-CN,zh;q=0.8'} + ) + + +def test_i18n(app, client): + try: from flask_babel import Babel - babel = Babel(self.app) + except ImportError: + try: + from flask_babelex import Babel + except ImportError: + pytest.skip('Flask-Babel or Flask-BabelEx must be installed.') + + babel = Babel(app) + + @babel.localeselector + def get_locale(): + return request.accept_languages.best_match(['en', 'zh'], 'en') + + @app.route('/', methods=['POST']) + def index(): + form = NameForm() + form.validate() + + if not app.config.get('WTF_I18N_ENABLED', True): + assert form.name.errors[0] == 'This field is required.' + elif not form.name.data: + assert form.name.errors[0] == u'è¯¥å—æ®µæ˜¯å¿…å¡«å—æ®µã€‚' + else: + assert form.name.errors[0] == u'å—æ®µé•¿åº¦å¿…须至少 8 个å—符。' + + client.post('/', headers={'Accept-Language': 'zh-CN,zh;q=0.8'}) + client.post( + '/', headers={'Accept-Language': 'zh'}, data={'name': 'short'} + ) + app.config['WTF_I18N_ENABLED'] = False + client.post('/', headers={'Accept-Language': 'zh'}) - @babel.localeselector - def get_locale(): - return request.accept_languages.best_match(['en', 'zh'], 'en') - self.app.config['WTF_CSRF_ENABLED'] = False +def test_outside_request(): + pytest.importorskip('babel') + from flask_wtf.i18n import translations - response = self.client.post( - "/", - headers={'Accept-Language': 'zh-CN,zh;q=0.8'}, - data={} - ) - assert '\u8be5\u5b57\u6bb5\u662f' in to_unicode(response.data) + s = 'This field is required.' + assert translations.gettext(s) == s - response = self.client.post("/", data={}) - assert b'This field is required.' in response.data + ss = 'Field must be at least %(min)d character long.' + sp = 'Field must be at least %(min)d character long.' + assert translations.ngettext(ss, sp, 1) == ss + assert translations.ngettext(ss, sp, 2) == sp diff --git a/tests/test_recaptcha.py b/tests/test_recaptcha.py index bd8c93afdafd678b8ee90c741e09661d84ff28ae..457f119fa22605cd06eec9e7ba5a72a6be12d2d9 100644 --- a/tests/test_recaptcha.py +++ b/tests/test_recaptcha.py @@ -1,71 +1,151 @@ -from __future__ import with_statement - -from flask import Flask, json, render_template +import pytest +from flask import json +from markupsafe import Markup from flask_wtf import FlaskForm +from flask_wtf._compat import to_bytes from flask_wtf.recaptcha import RecaptchaField -from .base import TestCase - +from flask_wtf.recaptcha.validators import Recaptcha, http -RECAPTCHA_PUBLIC_KEY = '6LeYIbsSAAAAACRPIllxA7wvXjIE411PfdB2gt2J' -RECAPTCHA_PRIVATE_KEY = '6LeYIbsSAAAAAJezaIq3Ft_hSTo0YtyeFG-JgRtu' +class RecaptchaForm(FlaskForm): + class Meta: + csrf = False -class RecaptchaFrom(FlaskForm): - SECRET_KEY = "a poorly kept secret." recaptcha = RecaptchaField() -class TestRecaptcha(TestCase): - def create_app(self): - app = Flask(__name__) - app.secret_key = "secret" - app.config['RECAPTCHA_PUBLIC_KEY'] = RECAPTCHA_PUBLIC_KEY - app.config['RECAPTCHA_PRIVATE_KEY'] = RECAPTCHA_PRIVATE_KEY - - @app.route("/", methods=("GET", "POST")) - def index(): - form = RecaptchaFrom(meta={'csrf': False}) - if form.validate_on_submit(): - return 'OK' - return render_template("recaptcha.html", form=form) - return app - - def test_recaptcha(self): - response = self.client.get('/') - assert b'//www.google.com/recaptcha/api.js' in response.data - - def test_invalid_recaptcha(self): - response = self.client.post('/', data={}) - assert b'missing' in response.data - - def test_send_recaptcha_request(self): - response = self.client.post('/', data={ - 'g-recaptcha-response': 'test' - }) - assert b'invalid' in response.data +@pytest.fixture +def app(app): + app.testing = False + app.config['PROPAGATE_EXCEPTIONS'] = True + app.config['RECAPTCHA_PUBLIC_KEY'] = 'public' + app.config['RECAPTCHA_PRIVATE_KEY'] = 'private' + return app - response = self.client.post('/', data=json.dumps({ - 'g-recaptcha-response': 'test' - }), content_type='application/json') - assert b'invalid' in response.data - def test_testing(self): - self.app.testing = True - response = self.client.post('/', data={ - 'g-recaptcha-response': 'test' - }) - assert b'invalid' not in response.data +@pytest.yield_fixture(autouse=True) +def req_ctx(app): + with app.test_request_context( + data={'g-recaptcha-response': 'pass'} + ) as ctx: + yield ctx + + +def test_config(app, monkeypatch): + f = RecaptchaForm() + monkeypatch.setattr(app, 'testing', True) + f.validate() + assert not f.recaptcha.errors + monkeypatch.undo() + + monkeypatch.delitem(app.config, 'RECAPTCHA_PUBLIC_KEY') + pytest.raises(RuntimeError, f.recaptcha) + monkeypatch.undo() + + monkeypatch.delitem(app.config, 'RECAPTCHA_PRIVATE_KEY') + pytest.raises(RuntimeError, f.validate) + + +def test_render_has_js(): + f = RecaptchaForm() + render = f.recaptcha() + assert 'https://www.google.com/recaptcha/api.js' in render + + +def test_render_custom_html(app): + app.config['RECAPTCHA_HTML'] = 'custom' + f = RecaptchaForm() + render = f.recaptcha() + assert render == 'custom' + assert isinstance(render, Markup) + + +def test_render_custom_args(app): + app.config['RECAPTCHA_PARAMETERS'] = {'key': '(value)'} + app.config['RECAPTCHA_DATA_ATTRS'] = {'red': 'blue'} + f = RecaptchaForm() + render = f.recaptcha() + assert '?key=%28value%29' in render + assert 'data-red="blue"' in render + + +def test_missing_response(app): + with app.test_request_context(): + f = RecaptchaForm() + f.validate() + assert f.recaptcha.errors[0] == 'The response parameter is missing.' + - def test_no_private_key(self): - self.app.testing = False - self.app.config.pop('RECAPTCHA_PRIVATE_KEY', None) - response = self.client.post('/', data={ - 'g-recaptcha-response': 'test' +class MockResponse(object): + def __init__(self, code, error='invalid-input-response', read_bytes=False): + self.code = code + self.data = json.dumps({ + 'success': not error, + 'error-codes': [error] if error else [] }) - assert response.status_code == 500 + self.read_bytes = read_bytes + + def read(self): + if self.read_bytes: + return to_bytes(self.data) + + return self.data + + +def test_send_invalid_request(monkeypatch): + def mock_urlopen(url, data): + return MockResponse(200) + + monkeypatch.setattr(http, 'urlopen', mock_urlopen) + f = RecaptchaForm() + f.validate() + assert f.recaptcha.errors[0] == ( + 'The response parameter is invalid or malformed.' + ) + + +def test_response_from_json(app, monkeypatch): + def mock_urlopen(url, data): + return MockResponse(200) + + monkeypatch.setattr(http, 'urlopen', mock_urlopen) + + with app.test_request_context( + data=json.dumps({'g-recaptcha-response': 'pass'}), + content_type='application/json' + ): + f = RecaptchaForm() + f.validate() + assert f.recaptcha.errors[0] != 'The response parameter is missing.' + + +def test_request_fail(monkeypatch): + def mock_urlopen(url, data): + return MockResponse(400) + + monkeypatch.setattr(http, 'urlopen', mock_urlopen) + f = RecaptchaForm() + f.validate() + assert f.recaptcha.errors + + +def test_request_success(monkeypatch): + def mock_urlopen(url, data): + return MockResponse(200, '') + + monkeypatch.setattr(http, 'urlopen', mock_urlopen) + f = RecaptchaForm() + f.validate() + assert not f.recaptcha.errors + + +def test_request_unmatched_error(monkeypatch): + def mock_urlopen(url, data): + return MockResponse(200, 'not-an-error', True) - def test_no_public_key(self): - self.app.config.pop('RECAPTCHA_PUBLIC_KEY', None) - response = self.client.get('/') - assert response.status_code == 500 + monkeypatch.setattr(http, 'urlopen', mock_urlopen) + f = RecaptchaForm() + f.recaptcha.validators = [Recaptcha('custom')] + f.validate() + assert f.recaptcha.errors[0] == 'custom' diff --git a/tests/test_uploads.py b/tests/test_uploads.py deleted file mode 100644 index 385db3dfb7d27d08d527b35e2636cbe8c5289880..0000000000000000000000000000000000000000 --- a/tests/test_uploads.py +++ /dev/null @@ -1,177 +0,0 @@ -from __future__ import with_statement - -try: - from io import BytesIO -except ImportError: - from StringIO import StringIO as BytesIO - -from flask import render_template, request - -from wtforms import StringField, FieldList -from flask_wtf import FlaskForm -from flask_wtf.file import FileField -from flask_wtf.file import file_required, file_allowed - -from .base import TestCase - - -class UploadSet(object): - def __init__(self, name='files', extensions=None): - self.name = name - self.extensions = extensions - - def file_allowed(self, storage, basename): - if not self.extensions: - return True - - ext = basename.rsplit('.', 1)[-1] - return ext in self.extensions - -images = UploadSet('images', ['jpg', 'png']) - - -class FileUploadForm(FlaskForm): - upload = FileField("Upload file") - - -class MultipleFileUploadForm(FlaskForm): - uploads = FieldList(FileField("upload"), min_entries=3) - - -class ImageUploadForm(FlaskForm): - upload = FileField("Upload file", - validators=[file_required(), - file_allowed(images)]) - - -class TextUploadForm(FlaskForm): - upload = FileField("Upload file", - validators=[file_required(), - file_allowed(['txt'])]) - - -class TestFileUpload(TestCase): - def create_app(self): - app = super(TestFileUpload, self).create_app() - app.config['WTF_CSRF_ENABLED'] = False - - @app.route("/upload-image/", methods=("POST",)) - def upload_image(): - form = ImageUploadForm() - if form.validate_on_submit(): - return "OK" - return "invalid" - - @app.route("/upload-text/", methods=("POST",)) - def upload_text(): - form = TextUploadForm() - if form.validate_on_submit(): - return "OK" - return "invalid" - - @app.route("/upload-multiple/", methods=("POST",)) - def upload_multiple(): - form = MultipleFileUploadForm() - if form.validate_on_submit(): - assert len(form.uploads.entries) == 3 - for upload in form.uploads.entries: - assert upload.has_file() - - return "OK" - - @app.route("/upload/", methods=("POST",)) - def upload(): - form = FileUploadForm() - if form.validate_on_submit(): - filedata = form.upload.data - else: - filedata = None - - return render_template("upload.html", - filedata=filedata, - form=form) - - return app - - def test_multiple_files(self): - fps = [self.app.open_resource("flask.png") for i in range(3)] - data = [("uploads-%d" % i, fp) for i, fp in enumerate(fps)] - response = self.client.post("/upload-multiple/", data=dict(data)) - assert response.status_code == 200 - - def test_valid_file(self): - with self.app.open_resource("flask.png") as fp: - response = self.client.post( - "/upload-image/", - data={'upload': fp} - ) - - assert b'OK' in response.data - - def test_missing_file(self): - response = self.client.post( - "/upload-image/", - data={'upload': "test"} - ) - - assert b'invalid' in response.data - - def test_invalid_file(self): - with self.app.open_resource("flask.png") as fp: - response = self.client.post( - "/upload-text/", - data={'upload': fp} - ) - - assert b'invalid' in response.data - - def test_invalid_file_2(self): - response = self.client.post( - "/upload/", - data={'upload': 'flask.png'} - ) - - assert b'flask.png</h3>' not in response.data - - def test_valid_txt_file(self): - with self.app.open_resource("flask.txt") as fp: - response = self.client.post( - "/upload-text/", - data={'upload': fp} - ) - - assert b'OK' in response.data - - def test_invalid_image_file(self): - with self.app.open_resource("flask.txt") as fp: - response = self.client.post( - "/upload-image/", - data={'upload': fp} - ) - - assert b'invalid' in response.data - - -class BrokenForm(FlaskForm): - text_fields = FieldList(StringField()) - file_fields = FieldList(FileField()) - -text_data = [('text_fields-0', 'First input'), - ('text_fields-1', 'Second input')] - -file_data = [('file_fields-0', (BytesIO(b'contents 0'), 'file0.txt')), - ('file_fields-1', (BytesIO(b'contents 1'), 'file1.txt'))] - - -class TestFileList(TestCase): - def test_multiple_upload(self): - data = dict(text_data + file_data) - with self.app.test_request_context(method='POST', data=data): - assert len(request.files) # the files have been added to the - # request - - f = BrokenForm(meta={'csrf': False}) - - assert f.validate_on_submit() - assert len(text_data) == len(f.text_fields) - assert len(file_data) == len(f.file_fields) diff --git a/tests/test_validation.py b/tests/test_validation.py deleted file mode 100644 index a5560b8c70b3059d01aed8d25a9b70d89fbaf85f..0000000000000000000000000000000000000000 --- a/tests/test_validation.py +++ /dev/null @@ -1,113 +0,0 @@ -from __future__ import with_statement - -from flask import g, json - -from flask_wtf.csrf import generate_csrf -from .base import MyForm, TestCase, capture_logging, to_unicode - - -class TestValidateOnSubmit(TestCase): - - def test_not_submitted(self): - response = self.client.get("/") - assert b'DANNY' not in response.data - - def test_submitted_not_valid(self): - self.app.config['WTF_CSRF_ENABLED'] = False - response = self.client.post("/", data={}) - assert b'DANNY' not in response.data - - def test_submitted_and_valid(self): - self.app.config['WTF_CSRF_ENABLED'] = False - response = self.client.post("/", data={"name": "danny"}) - assert b'DANNY' in response.data - - def test_json_data(self): - self.app.config['WTF_CSRF_ENABLED'] = False - response = self.client.post( - '/', content_type='application/json', - data=json.dumps({'name': 'Flask-WTF'}) - ) - assert b'FLASK-WTF' in response.data - - -class TestValidateWithoutSubmit(TestCase): - def test_unsubmitted_valid(self): - class obj: - name = 'foo' - - with self.app.test_request_context(): - assert MyForm(obj=obj, meta={'csrf': False}).validate() - t = generate_csrf() - assert MyForm(obj=obj, csrf_token=t).validate() - - -class TestHiddenTag(TestCase): - - def test_hidden_tag(self): - - response = self.client.get("/hidden/") - assert to_unicode(response.data).count('type="hidden"') == 5 - assert b'name="_method"' in response.data - - -class TestCSRF(TestCase): - - def test_csrf_token(self): - - response = self.client.get("/") - snippet = '<input id="csrf_token" name="csrf_token" type="hidden" value' - assert snippet in to_unicode(response.data) - - def test_invalid_csrf(self): - from flask_wtf.csrf import logger - - with capture_logging(logger) as handler: - response = self.client.post("/", data={"name": "danny"}) - - assert b'DANNY' not in response.data - assert b'The CSRF token is missing.' in response.data - self.assertEqual(1, len(handler)) - self.assertEqual('The CSRF token is missing.', handler[0].message) - - def test_csrf_disabled(self): - - self.app.config['WTF_CSRF_ENABLED'] = False - - response = self.client.post("/", data={"name": "danny"}) - assert b'DANNY' in response.data - - def test_validate_twice(self): - - response = self.client.post("/simple/", data={}) - assert response.status_code == 200 - - def test_ajax(self): - - response = self.client.post( - "/ajax/", data={"name": "danny"}, - headers={'X-Requested-With': 'XMLHttpRequest'} - ) - assert response.status_code == 200 - - def test_valid_csrf(self): - with self.client: - self.client.get('/') - csrf_token = g.csrf_token - - response = self.client.post('/', data={ - 'name': 'danny', - 'csrf_token': csrf_token - }) - assert b'DANNY' in response.data - - def test_double_csrf(self): - with self.client: - self.client.get('/') - csrf_token = g.csrf_token - - response = self.client.post("/two_forms/", data={ - "name": "danny", - "csrf_token": csrf_token - }) - assert response.data == b'OK' diff --git a/tox.ini b/tox.ini index 58a3b14a7935942bf7e74f3bde1f07c3eb69c2d1..2e7cda47f543dc71c47b1dc98970ca7aa85cdeda 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,58 @@ [tox] -envlist = py{26,27,33,34,35,36},pypy +envlist = + py{27,34,35,36,37,38} + pypy + py-babelex + py-no-babel + docs-html + coverage-report [testenv] -deps = -rtest-requirements.txt -commands = nosetests tests +deps = + coverage + pytest>=3 + flask-babel + flask-uploads +commands = coverage run -p -m pytest + +[testenv:py-babelex] +deps = + coverage + pytest>=3 + flask-babelex +commands = coverage run -p -m pytest + +[testenv:py-no-babel] +deps = + coverage + pytest>=3 +commands = coverage run -p -m pytest + +[testenv:docs-html] +deps = + sphinx + flask-sphinx-themes +commands = + sphinx-build -W -b html -d docs/_build/doctrees docs docs/_build/html + +[testenv:docs-linkcheck] +deps = sphinx +commands = + sphinx-build -W -b linkcheck -d docs/_build/doctrees docs docs/_build/linkcheck + +[testenv:coverage-report] +deps = coverage +skip_install = true +commands = + coverage combine + coverage report + coverage html + +[testenv:codecov] +passenv = CI TRAVIS TRAVIS_* +deps = codecov +skip_install = true +commands = + coverage combine + coverage report + codecov