Flask-Exts 0.1.2__tar.gz → 0.1.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flask_exts-0.1.4/.readthedocs.yaml +39 -0
- {flask_exts-0.1.2 → flask_exts-0.1.4}/CHANGES.rst +14 -0
- {flask_exts-0.1.2 → flask_exts-0.1.4}/PKG-INFO +2 -26
- {flask_exts-0.1.2 → flask_exts-0.1.4}/README.rst +0 -23
- flask_exts-0.1.4/babel.cfg +5 -0
- flask_exts-0.1.4/docs/develop.rst +43 -0
- flask_exts-0.1.4/docs/field.rst +33 -0
- flask_exts-0.1.4/docs/form.rst +9 -0
- {flask_exts-0.1.2 → flask_exts-0.1.4}/docs/index.rst +3 -0
- flask_exts-0.1.4/docs/translation.rst +55 -0
- {flask_exts-0.1.2 → flask_exts-0.1.4}/pyproject.toml +1 -2
- flask_exts-0.1.4/src/flask_exts/__about__.py +1 -0
- flask_exts-0.1.4/src/flask_exts/__init__.py +1 -0
- flask_exts-0.1.4/src/flask_exts/babel/__init__.py +1 -0
- flask_exts-0.1.4/src/flask_exts/babel/admin_babel.py +14 -0
- flask_exts-0.1.4/src/flask_exts/babel/babel_setting.py +41 -0
- flask_exts-0.1.4/src/flask_exts/babel/babel_text.py +2 -0
- flask_exts-0.1.4/src/flask_exts/form/__init__.py +3 -0
- flask_exts-0.1.4/src/flask_exts/form/babel.py +1 -0
- flask_exts-0.1.4/src/flask_exts/form/fields/datetime.py +102 -0
- flask_exts-0.1.4/src/flask_exts/form/fields/file.py +29 -0
- flask_exts-0.1.4/src/flask_exts/form/fields/json.py +21 -0
- flask_exts-0.1.4/src/flask_exts/form/fields/recaptcha.py +14 -0
- flask_exts-0.1.4/src/flask_exts/form/fields/select.py +123 -0
- flask_exts-0.1.4/src/flask_exts/form/fields/taglist.py +10 -0
- flask_exts-0.1.4/src/flask_exts/form/fields/upload.py +415 -0
- flask_exts-0.1.4/src/flask_exts/form/form/__init__.py +0 -0
- flask_exts-0.1.4/src/flask_exts/form/form/base_form.py +17 -0
- flask_exts-0.1.4/src/flask_exts/form/form/csrf.py +59 -0
- flask_exts-0.1.4/src/flask_exts/form/form/flask_form.py +25 -0
- flask_exts-0.1.4/src/flask_exts/form/form/meta.py +62 -0
- flask_exts-0.1.4/src/flask_exts/form/form/secure_form.py +34 -0
- flask_exts-0.1.4/src/flask_exts/form/helpers.py +70 -0
- flask_exts-0.1.4/src/flask_exts/form/rules.py +557 -0
- flask_exts-0.1.4/src/flask_exts/form/validators/__init__.py +0 -0
- flask_exts-0.1.4/src/flask_exts/form/validators/field_list.py +13 -0
- flask_exts-0.1.4/src/flask_exts/form/validators/file.py +103 -0
- flask_exts-0.1.4/src/flask_exts/form/validators/recaptcha.py +72 -0
- flask_exts-0.1.4/src/flask_exts/form/widgets/__init__.py +0 -0
- flask_exts-0.1.4/src/flask_exts/form/widgets/datetime.py +30 -0
- flask_exts-0.1.4/src/flask_exts/form/widgets/recaptcha.py +40 -0
- flask_exts-0.1.4/src/flask_exts/form/widgets/select.py +26 -0
- flask_exts-0.1.4/src/flask_exts/form/widgets/template.py +28 -0
- flask_exts-0.1.4/src/flask_exts/form/widgets/upload.py +96 -0
- flask_exts-0.1.4/src/flask_exts/helpers.py +47 -0
- flask_exts-0.1.4/src/flask_exts/manager.py +23 -0
- flask_exts-0.1.4/src/flask_exts/translations/__init__.py +0 -0
- flask_exts-0.1.4/src/flask_exts/translations/admin.pot +23 -0
- flask_exts-0.1.4/src/flask_exts/translations/en/LC_MESSAGES/admin.po +24 -0
- flask_exts-0.1.4/src/flask_exts/translations/zh_CN/LC_MESSAGES/admin.po +24 -0
- flask_exts-0.1.4/tests/conftest.py +19 -0
- flask_exts-0.1.4/tests/data/copyleft.gif +0 -0
- flask_exts-0.1.4/tests/data/copyleft.jpeg +0 -0
- flask_exts-0.1.4/tests/data/copyleft.jpg +0 -0
- flask_exts-0.1.4/tests/data/copyleft.png +0 -0
- flask_exts-0.1.4/tests/data/copyleft.tiff +0 -0
- flask_exts-0.1.4/tests/test_babel.py +56 -0
- flask_exts-0.1.4/tests/test_base.py +6 -0
- flask_exts-0.1.4/tests/test_form.py +47 -0
- flask_exts-0.1.4/tests/test_form_upload.py +306 -0
- flask_exts-0.1.4/tests/test_helpers.py +19 -0
- flask_exts-0.1.4/tests/translations/en/LC_MESSAGES/messages.po +24 -0
- flask_exts-0.1.4/tests/translations/messages.pot +23 -0
- flask_exts-0.1.4/tests/translations/zh_CN/LC_MESSAGES/messages.po +24 -0
- flask_exts-0.1.2/.readthedocs.yaml +0 -21
- flask_exts-0.1.2/src/flask_exts/__about__.py +0 -1
- {flask_exts-0.1.2 → flask_exts-0.1.4}/.github/workflows/release.yml +0 -0
- {flask_exts-0.1.2 → flask_exts-0.1.4}/.gitignore +0 -0
- {flask_exts-0.1.2 → flask_exts-0.1.4}/LICENSE.rst +0 -0
- {flask_exts-0.1.2 → flask_exts-0.1.4}/docs/Makefile +0 -0
- {flask_exts-0.1.2 → flask_exts-0.1.4}/docs/api.rst +0 -0
- {flask_exts-0.1.2 → flask_exts-0.1.4}/docs/changes.rst +0 -0
- {flask_exts-0.1.2 → flask_exts-0.1.4}/docs/conf.py +0 -0
- {flask_exts-0.1.2 → flask_exts-0.1.4}/docs/make.bat +0 -0
- {flask_exts-0.1.2 → flask_exts-0.1.4}/docs/requirements.txt +0 -0
- {flask_exts-0.1.2 → flask_exts-0.1.4}/docs/usage.rst +0 -0
- {flask_exts-0.1.2 → flask_exts-0.1.4}/requirements/build.in +0 -0
- {flask_exts-0.1.2 → flask_exts-0.1.4}/requirements/test.in +0 -0
- {flask_exts-0.1.2/src/flask_exts → flask_exts-0.1.4/src/flask_exts/form/fields}/__init__.py +0 -0
- {flask_exts-0.1.2 → flask_exts-0.1.4}/tests/__init__.py +0 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# .readthedocs.yaml
|
|
2
|
+
# Read the Docs configuration file
|
|
3
|
+
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
|
4
|
+
|
|
5
|
+
# Required
|
|
6
|
+
version: 2
|
|
7
|
+
|
|
8
|
+
# Set the OS, Python version and other tools you might need
|
|
9
|
+
build:
|
|
10
|
+
os: "ubuntu-22.04"
|
|
11
|
+
tools:
|
|
12
|
+
python: "3.12"
|
|
13
|
+
# You can also specify other tool versions:
|
|
14
|
+
# nodejs: "19"
|
|
15
|
+
# rust: "1.64"
|
|
16
|
+
# golang: "1.19"
|
|
17
|
+
|
|
18
|
+
# Build documentation in the "docs/" directory with Sphinx
|
|
19
|
+
sphinx:
|
|
20
|
+
configuration: docs/conf.py
|
|
21
|
+
|
|
22
|
+
# Optionally build your docs in additional formats such as PDF and ePub
|
|
23
|
+
# formats:
|
|
24
|
+
# - pdf
|
|
25
|
+
# - epub
|
|
26
|
+
|
|
27
|
+
# Optional but recommended, declare the Python requirements required
|
|
28
|
+
# to build your documentation
|
|
29
|
+
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
|
|
30
|
+
# python:
|
|
31
|
+
# install:
|
|
32
|
+
# - requirements: docs/requirements.txt
|
|
33
|
+
|
|
34
|
+
python:
|
|
35
|
+
install:
|
|
36
|
+
- method: pip
|
|
37
|
+
path: .
|
|
38
|
+
- requirements: docs/requirements.txt
|
|
39
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: Flask-Exts
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Flask extensions with admin, SQLAlchemy, babel, forms, fields, widgets, and so on.
|
|
5
5
|
Project-URL: Documentation, https://flask-exts.readthedocs.io
|
|
6
6
|
Project-URL: Source, https://github.com/ojso/flask-exts
|
|
@@ -42,10 +42,9 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
42
42
|
Classifier: Topic :: Software Development :: Build Tools
|
|
43
43
|
Requires-Python: >=3.10
|
|
44
44
|
Requires-Dist: flask-babel>=4.0
|
|
45
|
-
Requires-Dist: flask-migrate>=4.0
|
|
46
45
|
Requires-Dist: flask-sqlalchemy>=3.0
|
|
47
46
|
Requires-Dist: flask>=3.0
|
|
48
|
-
Requires-Dist:
|
|
47
|
+
Requires-Dist: pillow>=10.0
|
|
49
48
|
Requires-Dist: wtforms>=3.0
|
|
50
49
|
Description-Content-Type: text/x-rst
|
|
51
50
|
|
|
@@ -75,26 +74,3 @@ Install and update using pip:
|
|
|
75
74
|
|
|
76
75
|
$ pip install -U Flask-Exts
|
|
77
76
|
|
|
78
|
-
Developing
|
|
79
|
-
-----------
|
|
80
|
-
|
|
81
|
-
.. code-block:: console
|
|
82
|
-
|
|
83
|
-
$ pip install -e .
|
|
84
|
-
|
|
85
|
-
Test
|
|
86
|
-
----
|
|
87
|
-
Tests are run with `pytest <https://pytest.org/>`_.
|
|
88
|
-
To run the tests, from the project directory:
|
|
89
|
-
|
|
90
|
-
.. code-block:: console
|
|
91
|
-
|
|
92
|
-
$ pip install -r requirements/test.in
|
|
93
|
-
$ pytest
|
|
94
|
-
|
|
95
|
-
Docs
|
|
96
|
-
----
|
|
97
|
-
|
|
98
|
-
.. code-block:: console
|
|
99
|
-
|
|
100
|
-
$ pip install -r docs/requirements.txt
|
|
@@ -24,26 +24,3 @@ Install and update using pip:
|
|
|
24
24
|
|
|
25
25
|
$ pip install -U Flask-Exts
|
|
26
26
|
|
|
27
|
-
Developing
|
|
28
|
-
-----------
|
|
29
|
-
|
|
30
|
-
.. code-block:: console
|
|
31
|
-
|
|
32
|
-
$ pip install -e .
|
|
33
|
-
|
|
34
|
-
Test
|
|
35
|
-
----
|
|
36
|
-
Tests are run with `pytest <https://pytest.org/>`_.
|
|
37
|
-
To run the tests, from the project directory:
|
|
38
|
-
|
|
39
|
-
.. code-block:: console
|
|
40
|
-
|
|
41
|
-
$ pip install -r requirements/test.in
|
|
42
|
-
$ pytest
|
|
43
|
-
|
|
44
|
-
Docs
|
|
45
|
-
----
|
|
46
|
-
|
|
47
|
-
.. code-block:: console
|
|
48
|
-
|
|
49
|
-
$ pip install -r docs/requirements.txt
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Develop
|
|
2
|
+
=======
|
|
3
|
+
|
|
4
|
+
Install
|
|
5
|
+
-------
|
|
6
|
+
|
|
7
|
+
.. code-block:: console
|
|
8
|
+
|
|
9
|
+
$ pip install -e .
|
|
10
|
+
|
|
11
|
+
Test
|
|
12
|
+
----
|
|
13
|
+
Tests are run with `pytest <https://pytest.org/>`_.
|
|
14
|
+
To run the tests, from the project directory:
|
|
15
|
+
|
|
16
|
+
.. code-block:: console
|
|
17
|
+
|
|
18
|
+
# requirements
|
|
19
|
+
$ pip install -r requirements/test.in
|
|
20
|
+
|
|
21
|
+
# update translation
|
|
22
|
+
$ pybabel compile -d src/flask_exts/translations -D admin
|
|
23
|
+
$ pybabel compile -d tests/translations
|
|
24
|
+
|
|
25
|
+
# test
|
|
26
|
+
$ pytest
|
|
27
|
+
|
|
28
|
+
Docs
|
|
29
|
+
----
|
|
30
|
+
|
|
31
|
+
.. code-block:: console
|
|
32
|
+
|
|
33
|
+
$ pip install -r docs/requirements.txt
|
|
34
|
+
$ cd docs
|
|
35
|
+
$ make html
|
|
36
|
+
|
|
37
|
+
Publish
|
|
38
|
+
--------
|
|
39
|
+
|
|
40
|
+
.. code-block:: console
|
|
41
|
+
|
|
42
|
+
$ python -m build
|
|
43
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
======
|
|
2
|
+
field
|
|
3
|
+
======
|
|
4
|
+
|
|
5
|
+
.. module:: flask_exts.form.fields.upload
|
|
6
|
+
|
|
7
|
+
The FileUploadField class
|
|
8
|
+
-----------------------------
|
|
9
|
+
|
|
10
|
+
.. class:: FileUploadField
|
|
11
|
+
|
|
12
|
+
Example usage::
|
|
13
|
+
|
|
14
|
+
from flask_exts.form.form.base_form import BaseForm
|
|
15
|
+
from flask_exts.form.fields.upload import FileUploadField
|
|
16
|
+
|
|
17
|
+
class TestForm(BaseForm):
|
|
18
|
+
upload = FileUploadField("Upload", base_path=path)
|
|
19
|
+
|
|
20
|
+
The FileUploadField class
|
|
21
|
+
-----------------------------
|
|
22
|
+
|
|
23
|
+
.. class:: ImageUploadField
|
|
24
|
+
|
|
25
|
+
Example usage::
|
|
26
|
+
|
|
27
|
+
from flask_exts.form.fields.upload import ImageUploadField
|
|
28
|
+
|
|
29
|
+
class TestForm(BaseForm):
|
|
30
|
+
upload = ImageUploadField(
|
|
31
|
+
"Upload", base_path=path, thumbnail_size=(100, 100, True)
|
|
32
|
+
)
|
|
33
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
Translations
|
|
2
|
+
============
|
|
3
|
+
|
|
4
|
+
src
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
pybabel::
|
|
8
|
+
|
|
9
|
+
# extract messages from source files and generate a POT file
|
|
10
|
+
pybabel extract -F babel.cfg -o src/flask_exts/translations/admin.pot src/
|
|
11
|
+
|
|
12
|
+
# create new message catalogs from a POT file
|
|
13
|
+
pybabel init -i src/flask_exts/translations/admin.pot -d src/flask_exts/translations -D admin -l en
|
|
14
|
+
pybabel init -i src/flask_exts/translations/admin.pot -d src/flask_exts/translations -D admin -l zh_CN
|
|
15
|
+
|
|
16
|
+
# update existing message catalogs from a POT file
|
|
17
|
+
pybabel update -i src/flask_exts/translations/admin.pot -d src/flask_exts/translations -D admin
|
|
18
|
+
|
|
19
|
+
# edit
|
|
20
|
+
open the '.po' file
|
|
21
|
+
|
|
22
|
+
# compile message catalogs to MO files
|
|
23
|
+
|
|
24
|
+
$ pybabel compile -d src/flask_exts/translations -D admin
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
tests
|
|
28
|
+
-----
|
|
29
|
+
|
|
30
|
+
pybabel::
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
cd tests/
|
|
34
|
+
|
|
35
|
+
# extract messages from source files and generate a POT file
|
|
36
|
+
pybabel extract -o translations/messages.pot .
|
|
37
|
+
|
|
38
|
+
# create new message catalogs from a POT file
|
|
39
|
+
pybabel init -i translations/messages.pot -d translations -l en
|
|
40
|
+
pybabel init -i translations/messages.pot -d translations -l zh_CN
|
|
41
|
+
|
|
42
|
+
# update existing message catalogs from a POT file
|
|
43
|
+
pybabel update -i translations/messages.pot -d translations
|
|
44
|
+
|
|
45
|
+
# edit
|
|
46
|
+
open the '.po' file
|
|
47
|
+
|
|
48
|
+
# compile message catalogs to MO files
|
|
49
|
+
|
|
50
|
+
pybabel compile -d translations
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.4"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .manager import Manager
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .admin_babel import gettext, ngettext, lazy_gettext
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from flask_babel import Domain
|
|
2
|
+
from .. import translations
|
|
3
|
+
|
|
4
|
+
class AdminDomain(Domain):
|
|
5
|
+
def __init__(self):
|
|
6
|
+
super().__init__(translations.__path__[0], domain="admin")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
admin_domain = AdminDomain()
|
|
10
|
+
|
|
11
|
+
gettext = admin_domain.gettext
|
|
12
|
+
ngettext = admin_domain.ngettext
|
|
13
|
+
lazy_gettext = admin_domain.lazy_gettext
|
|
14
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from flask import request
|
|
2
|
+
from flask import session
|
|
3
|
+
from flask import current_app
|
|
4
|
+
from flask_babel import Babel
|
|
5
|
+
|
|
6
|
+
babel = Babel()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def locale_selector():
|
|
10
|
+
if not request:
|
|
11
|
+
return
|
|
12
|
+
# save lang to session if lang exists in args
|
|
13
|
+
if request.args.get("lang"):
|
|
14
|
+
session["lang"] = request.args.get("lang")
|
|
15
|
+
# from session
|
|
16
|
+
if session.get("lang"):
|
|
17
|
+
session_lang = session.get("lang")
|
|
18
|
+
return session_lang
|
|
19
|
+
# try to guess the language from the user accept header the browser transmits.
|
|
20
|
+
if current_app.config.get("BABEL_ACCEPT_LANGUAGES"):
|
|
21
|
+
accept_languages = current_app.config["BABEL_ACCEPT_LANGUAGES"].split(";")
|
|
22
|
+
accept_language = request.accept_languages.best_match(accept_languages)
|
|
23
|
+
return accept_language
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def timezone_selector():
|
|
27
|
+
# save timezone to session if timezone exists in args
|
|
28
|
+
if request.args.get("timezone"):
|
|
29
|
+
session["timezone"] = request.args.get("timezone")
|
|
30
|
+
# from session
|
|
31
|
+
if session.get("timezone"):
|
|
32
|
+
return session.get("timezone")
|
|
33
|
+
# from app.config
|
|
34
|
+
if current_app.config.get("BABEL_DEFAULT_TIMEZONE"):
|
|
35
|
+
return current_app.config.get("BABEL_DEFAULT_TIMEZONE")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def flask_babel_init_app(app):
|
|
39
|
+
babel.init_app(
|
|
40
|
+
app, locale_selector=locale_selector, timezone_selector=timezone_selector
|
|
41
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from ..babel import gettext, ngettext
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import datetime
|
|
3
|
+
from wtforms import fields as wtforms_fields
|
|
4
|
+
from ..widgets.datetime import DateTimePickerWidget
|
|
5
|
+
from ..widgets.datetime import TimePickerWidget
|
|
6
|
+
from ..babel import gettext
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DateTimeField(wtforms_fields.DateTimeField):
|
|
10
|
+
"""
|
|
11
|
+
Allows modifying the datetime format of a DateTimeField using form_args.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
widget = DateTimePickerWidget()
|
|
15
|
+
|
|
16
|
+
def __init__(self, label=None, validators=None, format=None, **kwargs):
|
|
17
|
+
"""Constructor
|
|
18
|
+
|
|
19
|
+
:param label:
|
|
20
|
+
Label
|
|
21
|
+
:param validators:
|
|
22
|
+
Field validators
|
|
23
|
+
:param format:
|
|
24
|
+
Format for text to date conversion. Defaults to '%Y-%m-%d %H:%M:%S'
|
|
25
|
+
:param kwargs:
|
|
26
|
+
Any additional parameters
|
|
27
|
+
"""
|
|
28
|
+
super().__init__(
|
|
29
|
+
label, validators, format or "%Y-%m-%d %H:%M:%S", **kwargs
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TimeField(wtforms_fields.Field):
|
|
34
|
+
"""
|
|
35
|
+
A text field which stores a `datetime.time` object.
|
|
36
|
+
Accepts time string in multiple formats: 20:10, 20:10:00, 10:00 am, 9:30pm, etc.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
widget = TimePickerWidget()
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
label=None,
|
|
44
|
+
validators=None,
|
|
45
|
+
formats=None,
|
|
46
|
+
default_format=None,
|
|
47
|
+
widget_format=None,
|
|
48
|
+
**kwargs
|
|
49
|
+
):
|
|
50
|
+
"""
|
|
51
|
+
Constructor
|
|
52
|
+
|
|
53
|
+
:param label:
|
|
54
|
+
Label
|
|
55
|
+
:param validators:
|
|
56
|
+
Field validators
|
|
57
|
+
:param formats:
|
|
58
|
+
Supported time formats, as a enumerable.
|
|
59
|
+
:param default_format:
|
|
60
|
+
Default time format. Defaults to '%H:%M:%S'
|
|
61
|
+
:param kwargs:
|
|
62
|
+
Any additional parameters
|
|
63
|
+
"""
|
|
64
|
+
super(TimeField, self).__init__(label, validators, **kwargs)
|
|
65
|
+
|
|
66
|
+
self.formats = formats or (
|
|
67
|
+
"%H:%M:%S",
|
|
68
|
+
"%H:%M",
|
|
69
|
+
"%I:%M:%S%p",
|
|
70
|
+
"%I:%M%p",
|
|
71
|
+
"%I:%M:%S %p",
|
|
72
|
+
"%I:%M %p",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
self.default_format = default_format or "%H:%M:%S"
|
|
76
|
+
|
|
77
|
+
def _value(self):
|
|
78
|
+
if self.raw_data:
|
|
79
|
+
return " ".join(self.raw_data)
|
|
80
|
+
elif self.data is not None:
|
|
81
|
+
return self.data.strftime(self.default_format)
|
|
82
|
+
else:
|
|
83
|
+
return ""
|
|
84
|
+
|
|
85
|
+
def process_formdata(self, valuelist):
|
|
86
|
+
if valuelist:
|
|
87
|
+
date_str = " ".join(valuelist)
|
|
88
|
+
|
|
89
|
+
if date_str.strip():
|
|
90
|
+
for format in self.formats:
|
|
91
|
+
try:
|
|
92
|
+
timetuple = time.strptime(date_str, format)
|
|
93
|
+
self.data = datetime.time(
|
|
94
|
+
timetuple.tm_hour, timetuple.tm_min, timetuple.tm_sec
|
|
95
|
+
)
|
|
96
|
+
return
|
|
97
|
+
except ValueError:
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
raise ValueError(gettext("Invalid time format"))
|
|
101
|
+
else:
|
|
102
|
+
self.data = None
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from werkzeug.datastructures import FileStorage
|
|
2
|
+
from wtforms import fields as wtforms_fields
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class FileField(wtforms_fields.FileField):
|
|
6
|
+
"""Werkzeug-aware subclass of :class:`wtforms.fields.FileField`."""
|
|
7
|
+
|
|
8
|
+
def process_formdata(self, valuelist):
|
|
9
|
+
valuelist = (x for x in valuelist if isinstance(x, FileStorage) and x)
|
|
10
|
+
data = next(valuelist, None)
|
|
11
|
+
|
|
12
|
+
if data is not None:
|
|
13
|
+
self.data = data
|
|
14
|
+
else:
|
|
15
|
+
self.raw_data = ()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MultipleFileField(wtforms_fields.MultipleFileField):
|
|
19
|
+
"""Werkzeug-aware subclass of :class:`wtforms.fields.MultipleFileField`.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def process_formdata(self, valuelist):
|
|
23
|
+
valuelist = (x for x in valuelist if isinstance(x, FileStorage) and x)
|
|
24
|
+
data = list(valuelist) or None
|
|
25
|
+
|
|
26
|
+
if data is not None:
|
|
27
|
+
self.data = data
|
|
28
|
+
else:
|
|
29
|
+
self.raw_data = ()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from wtforms.fields import TextAreaField
|
|
3
|
+
from ..babel import gettext
|
|
4
|
+
|
|
5
|
+
class JSONField(TextAreaField):
|
|
6
|
+
def process_formdata(self, valuelist):
|
|
7
|
+
if valuelist:
|
|
8
|
+
if not valuelist[0]:
|
|
9
|
+
self.data = None
|
|
10
|
+
return
|
|
11
|
+
try:
|
|
12
|
+
self.data = json.loads(valuelist[0])
|
|
13
|
+
except ValueError:
|
|
14
|
+
# raise ValueError(self.gettext('Invalid JSON'))
|
|
15
|
+
raise ValueError(gettext("Invalid JSON"))
|
|
16
|
+
|
|
17
|
+
def _value(self):
|
|
18
|
+
if self.data:
|
|
19
|
+
return json.dumps(self.data, ensure_ascii=False)
|
|
20
|
+
else:
|
|
21
|
+
return ""
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from wtforms.fields import Field
|
|
2
|
+
from ..widgets.recaptcha import RecaptchaWidget
|
|
3
|
+
from ..validators.recaptcha import Recaptcha
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RecaptchaField(Field):
|
|
7
|
+
widget = RecaptchaWidget()
|
|
8
|
+
|
|
9
|
+
# error message if recaptcha validation fails
|
|
10
|
+
recaptcha_error = None
|
|
11
|
+
|
|
12
|
+
def __init__(self, label="", validators=None, **kwargs):
|
|
13
|
+
validators = validators or [Recaptcha()]
|
|
14
|
+
super().__init__(label, validators, **kwargs)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from wtforms import fields as wtforms_fields
|
|
3
|
+
from ..widgets.select import Select2Widget
|
|
4
|
+
from ..widgets.select import Select2TagsWidget
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Select2Field(wtforms_fields.SelectField):
|
|
8
|
+
"""
|
|
9
|
+
`Select2 <https://github.com/ivaynberg/select2>`_ styled select widget.
|
|
10
|
+
|
|
11
|
+
You must include select2.js, form-x.x.x.js and select2 stylesheet for it to
|
|
12
|
+
work.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
widget = Select2Widget()
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
label=None,
|
|
20
|
+
validators=None,
|
|
21
|
+
coerce=str,
|
|
22
|
+
choices=None,
|
|
23
|
+
allow_blank=False,
|
|
24
|
+
blank_text=None,
|
|
25
|
+
**kwargs
|
|
26
|
+
):
|
|
27
|
+
super(Select2Field, self).__init__(label, validators, coerce, choices, **kwargs)
|
|
28
|
+
self.allow_blank = allow_blank
|
|
29
|
+
self.blank_text = blank_text or " "
|
|
30
|
+
|
|
31
|
+
def iter_choices(self):
|
|
32
|
+
if self.allow_blank:
|
|
33
|
+
yield ("__None", self.blank_text, self.data is None)
|
|
34
|
+
|
|
35
|
+
for choice in self.choices:
|
|
36
|
+
if isinstance(choice, tuple):
|
|
37
|
+
yield (choice[0], choice[1], self.coerce(choice[0]) == self.data)
|
|
38
|
+
else:
|
|
39
|
+
yield (
|
|
40
|
+
choice.value,
|
|
41
|
+
choice.name,
|
|
42
|
+
self.coerce(choice.value) == self.data,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def process_data(self, value):
|
|
46
|
+
if value is None:
|
|
47
|
+
self.data = None
|
|
48
|
+
else:
|
|
49
|
+
try:
|
|
50
|
+
self.data = self.coerce(value)
|
|
51
|
+
except (ValueError, TypeError):
|
|
52
|
+
self.data = None
|
|
53
|
+
|
|
54
|
+
def process_formdata(self, valuelist):
|
|
55
|
+
if valuelist:
|
|
56
|
+
if valuelist[0] == "__None":
|
|
57
|
+
self.data = None
|
|
58
|
+
else:
|
|
59
|
+
try:
|
|
60
|
+
self.data = self.coerce(valuelist[0])
|
|
61
|
+
except ValueError:
|
|
62
|
+
raise ValueError(self.gettext("Invalid Choice: could not coerce"))
|
|
63
|
+
|
|
64
|
+
def pre_validate(self, form):
|
|
65
|
+
if self.allow_blank and self.data is None:
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
super(Select2Field, self).pre_validate(form)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Select2TagsField(wtforms_fields.StringField):
|
|
72
|
+
"""`Select2 <http://ivaynberg.github.com/select2/#tags>`_ styled text field.
|
|
73
|
+
You must include select2.js, form-x.x.x.js and select2 stylesheet for it to work.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
widget = Select2TagsWidget()
|
|
77
|
+
_strip_regex = re.compile(
|
|
78
|
+
r"#\d+(?:(,)|\s$)"
|
|
79
|
+
) # e.g., 'tag#123, anothertag#425 ' => 'tag, anothertag'
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
label=None,
|
|
84
|
+
validators=None,
|
|
85
|
+
save_as_list=False,
|
|
86
|
+
coerce=str,
|
|
87
|
+
allow_duplicates=False,
|
|
88
|
+
**kwargs
|
|
89
|
+
):
|
|
90
|
+
"""Initialization
|
|
91
|
+
|
|
92
|
+
:param save_as_list:
|
|
93
|
+
If `True` then populate ``obj`` using list else string
|
|
94
|
+
:param allow_duplicates
|
|
95
|
+
If `True` then duplicate tags are allowed in the field.
|
|
96
|
+
"""
|
|
97
|
+
self.save_as_list = save_as_list
|
|
98
|
+
self.allow_duplicates = allow_duplicates
|
|
99
|
+
self.coerce = coerce
|
|
100
|
+
|
|
101
|
+
super(Select2TagsField, self).__init__(label, validators, **kwargs)
|
|
102
|
+
|
|
103
|
+
def process_formdata(self, valuelist):
|
|
104
|
+
if valuelist:
|
|
105
|
+
entrylist = valuelist[0]
|
|
106
|
+
if self.allow_duplicates and entrylist.endswith(" "):
|
|
107
|
+
# This means this is an allowed duplicate (see form.js, `createSearchChoice`), so its ID was modified.
|
|
108
|
+
# Hence, we need to restore the original IDs.
|
|
109
|
+
entrylist = re.sub(self._strip_regex, "\\1", entrylist)
|
|
110
|
+
if self.save_as_list:
|
|
111
|
+
self.data = [
|
|
112
|
+
self.coerce(v.strip()) for v in entrylist.split(",") if v.strip()
|
|
113
|
+
]
|
|
114
|
+
else:
|
|
115
|
+
self.data = self.coerce(entrylist)
|
|
116
|
+
|
|
117
|
+
def _value(self):
|
|
118
|
+
if isinstance(self.data, (list, tuple)):
|
|
119
|
+
return ",".join(v for v in self.data)
|
|
120
|
+
elif self.data:
|
|
121
|
+
return self.data
|
|
122
|
+
else:
|
|
123
|
+
return ""
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from wtforms import fields as wtforms_fields
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TagListField(wtforms_fields.StringField):
|
|
5
|
+
def process_formdata(self, valuelist):
|
|
6
|
+
if valuelist:
|
|
7
|
+
self.data = [x.strip() for x in valuelist[0].split(",")]
|
|
8
|
+
|
|
9
|
+
def _value(self):
|
|
10
|
+
return ",".join(self.data) if self.data is not None else ""
|