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.
Files changed (80) hide show
  1. flask_exts-0.1.4/.readthedocs.yaml +39 -0
  2. {flask_exts-0.1.2 → flask_exts-0.1.4}/CHANGES.rst +14 -0
  3. {flask_exts-0.1.2 → flask_exts-0.1.4}/PKG-INFO +2 -26
  4. {flask_exts-0.1.2 → flask_exts-0.1.4}/README.rst +0 -23
  5. flask_exts-0.1.4/babel.cfg +5 -0
  6. flask_exts-0.1.4/docs/develop.rst +43 -0
  7. flask_exts-0.1.4/docs/field.rst +33 -0
  8. flask_exts-0.1.4/docs/form.rst +9 -0
  9. {flask_exts-0.1.2 → flask_exts-0.1.4}/docs/index.rst +3 -0
  10. flask_exts-0.1.4/docs/translation.rst +55 -0
  11. {flask_exts-0.1.2 → flask_exts-0.1.4}/pyproject.toml +1 -2
  12. flask_exts-0.1.4/src/flask_exts/__about__.py +1 -0
  13. flask_exts-0.1.4/src/flask_exts/__init__.py +1 -0
  14. flask_exts-0.1.4/src/flask_exts/babel/__init__.py +1 -0
  15. flask_exts-0.1.4/src/flask_exts/babel/admin_babel.py +14 -0
  16. flask_exts-0.1.4/src/flask_exts/babel/babel_setting.py +41 -0
  17. flask_exts-0.1.4/src/flask_exts/babel/babel_text.py +2 -0
  18. flask_exts-0.1.4/src/flask_exts/form/__init__.py +3 -0
  19. flask_exts-0.1.4/src/flask_exts/form/babel.py +1 -0
  20. flask_exts-0.1.4/src/flask_exts/form/fields/datetime.py +102 -0
  21. flask_exts-0.1.4/src/flask_exts/form/fields/file.py +29 -0
  22. flask_exts-0.1.4/src/flask_exts/form/fields/json.py +21 -0
  23. flask_exts-0.1.4/src/flask_exts/form/fields/recaptcha.py +14 -0
  24. flask_exts-0.1.4/src/flask_exts/form/fields/select.py +123 -0
  25. flask_exts-0.1.4/src/flask_exts/form/fields/taglist.py +10 -0
  26. flask_exts-0.1.4/src/flask_exts/form/fields/upload.py +415 -0
  27. flask_exts-0.1.4/src/flask_exts/form/form/__init__.py +0 -0
  28. flask_exts-0.1.4/src/flask_exts/form/form/base_form.py +17 -0
  29. flask_exts-0.1.4/src/flask_exts/form/form/csrf.py +59 -0
  30. flask_exts-0.1.4/src/flask_exts/form/form/flask_form.py +25 -0
  31. flask_exts-0.1.4/src/flask_exts/form/form/meta.py +62 -0
  32. flask_exts-0.1.4/src/flask_exts/form/form/secure_form.py +34 -0
  33. flask_exts-0.1.4/src/flask_exts/form/helpers.py +70 -0
  34. flask_exts-0.1.4/src/flask_exts/form/rules.py +557 -0
  35. flask_exts-0.1.4/src/flask_exts/form/validators/__init__.py +0 -0
  36. flask_exts-0.1.4/src/flask_exts/form/validators/field_list.py +13 -0
  37. flask_exts-0.1.4/src/flask_exts/form/validators/file.py +103 -0
  38. flask_exts-0.1.4/src/flask_exts/form/validators/recaptcha.py +72 -0
  39. flask_exts-0.1.4/src/flask_exts/form/widgets/__init__.py +0 -0
  40. flask_exts-0.1.4/src/flask_exts/form/widgets/datetime.py +30 -0
  41. flask_exts-0.1.4/src/flask_exts/form/widgets/recaptcha.py +40 -0
  42. flask_exts-0.1.4/src/flask_exts/form/widgets/select.py +26 -0
  43. flask_exts-0.1.4/src/flask_exts/form/widgets/template.py +28 -0
  44. flask_exts-0.1.4/src/flask_exts/form/widgets/upload.py +96 -0
  45. flask_exts-0.1.4/src/flask_exts/helpers.py +47 -0
  46. flask_exts-0.1.4/src/flask_exts/manager.py +23 -0
  47. flask_exts-0.1.4/src/flask_exts/translations/__init__.py +0 -0
  48. flask_exts-0.1.4/src/flask_exts/translations/admin.pot +23 -0
  49. flask_exts-0.1.4/src/flask_exts/translations/en/LC_MESSAGES/admin.po +24 -0
  50. flask_exts-0.1.4/src/flask_exts/translations/zh_CN/LC_MESSAGES/admin.po +24 -0
  51. flask_exts-0.1.4/tests/conftest.py +19 -0
  52. flask_exts-0.1.4/tests/data/copyleft.gif +0 -0
  53. flask_exts-0.1.4/tests/data/copyleft.jpeg +0 -0
  54. flask_exts-0.1.4/tests/data/copyleft.jpg +0 -0
  55. flask_exts-0.1.4/tests/data/copyleft.png +0 -0
  56. flask_exts-0.1.4/tests/data/copyleft.tiff +0 -0
  57. flask_exts-0.1.4/tests/test_babel.py +56 -0
  58. flask_exts-0.1.4/tests/test_base.py +6 -0
  59. flask_exts-0.1.4/tests/test_form.py +47 -0
  60. flask_exts-0.1.4/tests/test_form_upload.py +306 -0
  61. flask_exts-0.1.4/tests/test_helpers.py +19 -0
  62. flask_exts-0.1.4/tests/translations/en/LC_MESSAGES/messages.po +24 -0
  63. flask_exts-0.1.4/tests/translations/messages.pot +23 -0
  64. flask_exts-0.1.4/tests/translations/zh_CN/LC_MESSAGES/messages.po +24 -0
  65. flask_exts-0.1.2/.readthedocs.yaml +0 -21
  66. flask_exts-0.1.2/src/flask_exts/__about__.py +0 -1
  67. {flask_exts-0.1.2 → flask_exts-0.1.4}/.github/workflows/release.yml +0 -0
  68. {flask_exts-0.1.2 → flask_exts-0.1.4}/.gitignore +0 -0
  69. {flask_exts-0.1.2 → flask_exts-0.1.4}/LICENSE.rst +0 -0
  70. {flask_exts-0.1.2 → flask_exts-0.1.4}/docs/Makefile +0 -0
  71. {flask_exts-0.1.2 → flask_exts-0.1.4}/docs/api.rst +0 -0
  72. {flask_exts-0.1.2 → flask_exts-0.1.4}/docs/changes.rst +0 -0
  73. {flask_exts-0.1.2 → flask_exts-0.1.4}/docs/conf.py +0 -0
  74. {flask_exts-0.1.2 → flask_exts-0.1.4}/docs/make.bat +0 -0
  75. {flask_exts-0.1.2 → flask_exts-0.1.4}/docs/requirements.txt +0 -0
  76. {flask_exts-0.1.2 → flask_exts-0.1.4}/docs/usage.rst +0 -0
  77. {flask_exts-0.1.2 → flask_exts-0.1.4}/requirements/build.in +0 -0
  78. {flask_exts-0.1.2 → flask_exts-0.1.4}/requirements/test.in +0 -0
  79. {flask_exts-0.1.2/src/flask_exts → flask_exts-0.1.4/src/flask_exts/form/fields}/__init__.py +0 -0
  80. {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,3 +1,17 @@
1
+ v0.1.4
2
+ ------
3
+
4
+ Unreleased
5
+
6
+ - form
7
+
8
+ v0.1.3
9
+ ------
10
+
11
+ Released 2024-03-25
12
+
13
+ - babel support
14
+
1
15
  v0.1.2
2
16
  ------
3
17
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: Flask-Exts
3
- Version: 0.1.2
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: python-dotenv>=1.0
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,5 @@
1
+ # Python
2
+ [python: **.py]
3
+ # Jinja2
4
+ [jinja2: **/templates/**.html]
5
+ encoding = utf-8
@@ -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,9 @@
1
+ =====
2
+ form
3
+ =====
4
+
5
+ .. toctree::
6
+ :maxdepth: 2
7
+ :caption: Contents:
8
+
9
+ field
@@ -14,6 +14,9 @@ Welcome to Flask-Exts's documentation!
14
14
 
15
15
  usage
16
16
  api
17
+ form
18
+ develop
19
+ translation
17
20
  changes
18
21
 
19
22
  Indices and tables
@@ -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
+
@@ -30,8 +30,7 @@ dependencies = [
30
30
  "WTForms>=3.0",
31
31
  "flask-babel>=4.0",
32
32
  "Flask-SQLAlchemy>=3.0",
33
- "Flask-Migrate>=4.0",
34
- "python-dotenv>=1.0",
33
+ "pillow>=10.0",
35
34
  ]
36
35
 
37
36
  [project.urls]
@@ -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,2 @@
1
+ from .admin_babel import gettext
2
+ gettext("Home")
@@ -0,0 +1,3 @@
1
+ from .form import *
2
+ from .fields import *
3
+ from .widgets import *
@@ -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 ""