├── tests
├── fields
│ ├── __init__.py
│ ├── test_search_field.py
│ ├── test_string_field.py
│ ├── test_passive_hidden_field.py
│ ├── test_integer_field.py
│ ├── test_read_only_fields.py
│ ├── test_date_field.py
│ ├── test_date_time_field.py
│ ├── test_date_time_local_field.py
│ ├── test_decimal_field.py
│ ├── test_json_field.py
│ ├── test_select_multiple_field.py
│ ├── test_color_field.py
│ ├── test_int_interval_field.py
│ ├── test_time_field.py
│ ├── test_select_field.py
│ └── test_split_date_time_field.py
├── test_chain_validator.py
├── test_widgets.py
├── test_read_only_function.py
├── test_time_range_validator.py
├── test_date_range_validator.py
├── test_if_validator.py
├── __init__.py
└── test_email_validator.py
├── docs
├── requirements.in
├── requirements.txt
├── make.bat
├── Makefile
├── index.rst
└── conf.py
├── setup.cfg
├── MANIFEST.in
├── wtforms_components
├── fields
│ ├── passive_hidden.py
│ ├── json_field.py
│ ├── color.py
│ ├── time.py
│ ├── __init__.py
│ ├── select_multiple.py
│ ├── html5.py
│ ├── interval.py
│ ├── split_date_time.py
│ ├── select.py
│ └── ajax.py
├── __init__.py
├── validators.py
└── widgets.py
├── .readthedocs.yaml
├── pyproject.toml
├── .gitignore
├── .github
└── workflows
│ ├── docs.yml
│ ├── lint.yml
│ └── test.yml
├── tox.ini
├── README.rst
├── LICENSE
├── setup.py
└── CHANGES.rst
/tests/fields/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/requirements.in:
--------------------------------------------------------------------------------
1 | furo
2 | sphinx
3 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [bdist_wheel]
2 | universal = 1
3 |
4 | [metadata]
5 | license_file = LICENSE
6 |
--------------------------------------------------------------------------------
/tests/fields/test_search_field.py:
--------------------------------------------------------------------------------
1 | from tests import FieldTestCase
2 | from wtforms_components import SearchField
3 |
4 |
5 | class TestSearchField(FieldTestCase):
6 | field_class = SearchField
7 |
--------------------------------------------------------------------------------
/tests/fields/test_string_field.py:
--------------------------------------------------------------------------------
1 | from tests import FieldTestCase
2 | from wtforms_components import StringField
3 |
4 |
5 | class TestStringField(FieldTestCase):
6 | field_class = StringField
7 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include CHANGES.rst LICENSE README.rst
2 | recursive-include tests *
3 | recursive-exclude tests *.pyc
4 | recursive-include docs *
5 | recursive-exclude docs *.pyc
6 | prune docs/_build
7 | exclude docs/_themes/.git
8 |
--------------------------------------------------------------------------------
/wtforms_components/fields/passive_hidden.py:
--------------------------------------------------------------------------------
1 | from wtforms.fields import HiddenField
2 |
3 |
4 | class PassiveHiddenField(HiddenField):
5 | """
6 | HiddenField that does not populate obj values.
7 | """
8 |
9 | def populate_obj(self, obj, name):
10 | pass
11 |
--------------------------------------------------------------------------------
/.readthedocs.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | build:
4 | os: ubuntu-24.04
5 | tools:
6 | python: "3.12"
7 |
8 | sphinx:
9 | configuration: docs/conf.py
10 | fail_on_warning: true
11 |
12 | python:
13 | install:
14 | - method: pip
15 | path: .
16 | - requirements: docs/requirements.txt
17 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.ruff]
2 | target-version = "py39"
3 |
4 | [tool.ruff.format]
5 | docstring-code-format = true
6 |
7 | [tool.ruff.lint]
8 | select = [
9 | "C90", # mccabe
10 | "E", # pycodestyle errors
11 | "F", # Pyflakes
12 | "I", # isort
13 | "UP", # pyupgrade
14 | "W", # pycodestyle warnings
15 | ]
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.py[co]
2 |
3 | # Packages
4 | *.egg
5 | *.egg-info
6 | dist
7 | build
8 | eggs
9 | parts
10 | bin
11 | var
12 | sdist
13 | develop-eggs
14 | .installed.cfg
15 |
16 | # Installer logs
17 | pip-log.txt
18 |
19 | # Unit test / coverage reports
20 | .coverage
21 | .tox
22 |
23 | #Translations
24 | *.mo
25 |
26 | #Mr Developer
27 | .mr.developer.cfg
28 |
29 | # Built docs
30 | docs/_build/
31 |
--------------------------------------------------------------------------------
/.github/workflows/docs.yml:
--------------------------------------------------------------------------------
1 | name: Docs
2 |
3 | on:
4 | - push
5 | - pull_request
6 |
7 | jobs:
8 | docs:
9 | name: Docs
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v4
13 |
14 | - uses: actions/setup-python@v5
15 | with:
16 | python-version: "3.12"
17 |
18 | - name: Install dependencies
19 | run: |
20 | python -m pip install --upgrade pip
21 | pip install --upgrade tox
22 |
23 | - name: Build documentation
24 | run: tox -e docs
25 |
--------------------------------------------------------------------------------
/tests/fields/test_passive_hidden_field.py:
--------------------------------------------------------------------------------
1 | from wtforms import Form
2 | from wtforms_test import FormTestCase
3 |
4 | from tests import MultiDict
5 | from wtforms_components import PassiveHiddenField
6 |
7 |
8 | class TestPassiveHiddenField(FormTestCase):
9 | def test_does_not_populate_obj_values(self):
10 | class MyForm(Form):
11 | id = PassiveHiddenField()
12 |
13 | class A:
14 | id = None
15 |
16 | form = MyForm(MultiDict({"id": 12}))
17 | a = A()
18 | form.populate_obj(a)
19 | assert a.id is None
20 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Lint
2 |
3 | on:
4 | - push
5 | - pull_request
6 |
7 | jobs:
8 | test:
9 | name: Lint
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v4
13 |
14 | - uses: actions/setup-python@v5
15 | with:
16 | python-version: 3.12
17 |
18 | - name: Install dependencies
19 | run: |
20 | python -m pip install --upgrade pip
21 | pip install .[test]
22 |
23 | - name: Run linting
24 | run: |
25 | ruff check .
26 | ruff format --check
27 |
--------------------------------------------------------------------------------
/tests/test_chain_validator.py:
--------------------------------------------------------------------------------
1 | from wtforms import Form
2 | from wtforms.fields import StringField
3 | from wtforms.validators import DataRequired, Email
4 | from wtforms_test import FormTestCase
5 |
6 | from tests import MultiDict
7 | from wtforms_components import Chain
8 |
9 |
10 | class TestChainValidator(FormTestCase):
11 | def test_validates_whole_chain(self):
12 | class MyForm(Form):
13 | email = StringField(validators=[Chain([DataRequired(), Email()])])
14 |
15 | form = MyForm(MultiDict({"name": ""}))
16 | form.validate()
17 | assert "email" in form.errors
18 |
--------------------------------------------------------------------------------
/tests/fields/test_integer_field.py:
--------------------------------------------------------------------------------
1 | from wtforms.validators import NumberRange
2 |
3 | from tests import FieldTestCase, MultiDict
4 | from wtforms_components import IntegerField
5 |
6 |
7 | class TestIntegerField(FieldTestCase):
8 | field_class = IntegerField
9 |
10 | def test_assigns_min_and_max(self):
11 | form_class = self.init_form(validators=[NumberRange(min=2, max=10)])
12 | form = form_class(MultiDict(test_field=3))
13 | assert str(form.test_field) == (
14 | ''
16 | )
17 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | # Tox (http://tox.testrun.org/) is a tool for running tests
2 | # in multiple virtualenvs. This configuration file will run the
3 | # test suite on all supported python versions. To use it, "pip install tox"
4 | # and then run "tox" from this directory.
5 |
6 | [tox]
7 | envlist =
8 | {py39,py310,py311,py312}-{wtforms31,wtforms32}
9 | docs
10 |
11 | [testenv]
12 | commands =
13 | pytest {posargs}
14 | deps =
15 | .[test]
16 | wtforms31: WTForms>=3.1,<3.2
17 | wtforms32: WTForms>=3.2,<3.3
18 |
19 | [testenv:docs]
20 | basepython = py312
21 | deps = -r docs/requirements.txt
22 | commands = sphinx-build docs docs/_build --fail-on-warning
23 |
--------------------------------------------------------------------------------
/tests/fields/test_read_only_fields.py:
--------------------------------------------------------------------------------
1 | from wtforms.fields import BooleanField
2 |
3 | from tests import MultiDict, SimpleFieldTestCase
4 | from wtforms_components import read_only
5 |
6 |
7 | class TestReadOnlyCheckboxField(SimpleFieldTestCase):
8 | field_class = BooleanField
9 |
10 | def test_has_readonly_and_disabled_attributes_in_html(self):
11 | form_class = self.init_form()
12 | form = form_class(MultiDict(test_field="y"))
13 | read_only(form.test_field)
14 | assert (
15 | ''
17 | ) in str(form.test_field)
18 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | WTForms-Components
2 | ==================
3 |
4 | |Version Status| |Downloads|
5 |
6 | Additional fields, validators and widgets for WTForms.
7 |
8 |
9 | Resources
10 | ---------
11 |
12 | - `Documentation `_
13 | - `Issue Tracker `_
14 | - `Code `_
15 |
16 | .. |Version Status| image:: https://img.shields.io/pypi/v/WTForms-Components.svg
17 | :target: https://crate.io/packages/WTForms-Components/
18 | .. |Downloads| image:: https://img.shields.io/pypi/dm/WTForms-Components.svg
19 | :target: https://crate.io/packages/WTForms-Components/
20 |
--------------------------------------------------------------------------------
/tests/fields/test_date_field.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | from tests import FieldTestCase, MultiDict
4 | from wtforms_components import DateField, DateRange
5 |
6 |
7 | class TestDateField(FieldTestCase):
8 | field_class = DateField
9 |
10 | def test_assigns_min_and_max(self):
11 | form_class = self.init_form(
12 | validators=[DateRange(min=datetime(2000, 1, 1), max=datetime(2000, 10, 10))]
13 | )
14 | form = form_class(MultiDict(test_field="2000-2-2"))
15 | assert str(form.test_field) == (
16 | ''
18 | )
19 |
--------------------------------------------------------------------------------
/tests/fields/test_date_time_field.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | from tests import FieldTestCase, MultiDict
4 | from wtforms_components import DateRange, DateTimeField
5 |
6 |
7 | class TestDateTimeField(FieldTestCase):
8 | field_class = DateTimeField
9 |
10 | def test_assigns_min_and_max(self):
11 | form_class = self.init_form(
12 | validators=[DateRange(min=datetime(2000, 1, 1), max=datetime(2000, 10, 10))]
13 | )
14 | form = form_class(MultiDict(test_field="2000-2-2"))
15 | assert str(form.test_field) == (
16 | ''
19 | )
20 |
--------------------------------------------------------------------------------
/tests/fields/test_date_time_local_field.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | from tests import FieldTestCase, MultiDict
4 | from wtforms_components import DateRange, DateTimeLocalField
5 |
6 |
7 | class TestDateTimeLocalField(FieldTestCase):
8 | field_class = DateTimeLocalField
9 |
10 | def test_assigns_min_and_max(self):
11 | form_class = self.init_form(
12 | validators=[DateRange(min=datetime(2000, 1, 1), max=datetime(2000, 10, 10))]
13 | )
14 | form = form_class(MultiDict(test_field="2000-2-2"))
15 | assert str(form.test_field) == (
16 | ''
19 | )
20 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on:
4 | - push
5 | - pull_request
6 |
7 | jobs:
8 | tests:
9 | name: Python ${{ matrix.python }}
10 | runs-on: ubuntu-latest
11 | strategy:
12 | fail-fast: false
13 | matrix:
14 | include:
15 | - python: "3.12"
16 | - python: "3.11"
17 | - python: "3.10"
18 | - python: "3.9"
19 |
20 | steps:
21 | - uses: actions/checkout@v4
22 |
23 | - uses: actions/setup-python@v5
24 | with:
25 | python-version: ${{ matrix.python }}
26 |
27 | - name: Install dependencies
28 | run: |
29 | python -m pip install --upgrade pip
30 | pip install --upgrade tox
31 |
32 | - name: Run tests
33 | env:
34 | TOXENV: py-wtforms31, py-wtforms32
35 | run: tox
36 |
--------------------------------------------------------------------------------
/wtforms_components/fields/json_field.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | from wtforms import fields, widgets
4 |
5 |
6 | class JSONField(fields.StringField):
7 | """
8 | A text field which stores a `json`.
9 | """
10 |
11 | widget = widgets.TextArea()
12 |
13 | def _value(self):
14 | return json.dumps(self.data) if self.data else ""
15 |
16 | def process_formdata(self, valuelist):
17 | if valuelist:
18 | try:
19 | self.data = json.loads(valuelist[0])
20 | except ValueError:
21 | self.data = None
22 | raise ValueError("This field contains invalid JSON")
23 | else:
24 | self.data = None
25 |
26 | def pre_validate(self, form):
27 | if self.data:
28 | try:
29 | json.dumps(self.data)
30 | except TypeError:
31 | self.data = None
32 | raise ValueError("This field contains invalid JSON")
33 |
--------------------------------------------------------------------------------
/tests/fields/test_decimal_field.py:
--------------------------------------------------------------------------------
1 | from wtforms.validators import NumberRange
2 |
3 | from tests import FieldTestCase, MultiDict
4 | from wtforms_components import DecimalField
5 | from wtforms_components.widgets import NumberInput
6 |
7 |
8 | class TestDecimalField(FieldTestCase):
9 | field_class = DecimalField
10 |
11 | def test_assigns_min_and_max(self):
12 | form_class = self.init_form(validators=[NumberRange(min=2, max=10)])
13 | form = form_class(MultiDict(test_field=3))
14 | assert str(form.test_field) == (
15 | ''
17 | )
18 |
19 | def test_assigns_step(self):
20 | form_class = self.init_form(widget=NumberInput(step="0.1"))
21 | form = form_class(MultiDict(test_field=3))
22 | assert str(form.test_field) == (
23 | ''
25 | )
26 |
--------------------------------------------------------------------------------
/tests/fields/test_json_field.py:
--------------------------------------------------------------------------------
1 | from tests import MultiDict, SimpleFieldTestCase
2 | from wtforms_components import JSONField
3 |
4 |
5 | class TestJSONField(SimpleFieldTestCase):
6 | field_class = JSONField
7 |
8 | def setup_method(self, method):
9 | self.valid_jsons = [
10 | '{"a": {"b": true, "c": "lv", "d": 3}, "e": {"f": {"g": [85]}}}'
11 | ]
12 | self.invalid_jsons = [
13 | '{"a": {"b": bzz, "c": "lv", "d": 3}, "e": {"f": {"g": [85]}}}'
14 | ]
15 |
16 | def test_valid_times(self):
17 | form_class = self.init_form()
18 | for time_ in self.valid_jsons:
19 | form = form_class(MultiDict(test_field=time_))
20 | form.validate()
21 | assert len(form.errors) == 0
22 |
23 | def test_invalid_times(self):
24 | form_class = self.init_form()
25 | for time_ in self.invalid_jsons:
26 | form = form_class(MultiDict(test_field=time_))
27 | form.validate()
28 | assert len(form.errors["test_field"]) == 1
29 |
--------------------------------------------------------------------------------
/wtforms_components/fields/color.py:
--------------------------------------------------------------------------------
1 | from ..widgets import ColorInput
2 | from .html5 import StringField
3 |
4 |
5 | class ColorField(StringField):
6 | """
7 | A string field representing a Color object from python colour package.
8 |
9 | .. _colours:
10 | https://github.com/vaab/colour
11 |
12 | Represents an ````.
13 | """
14 |
15 | widget = ColorInput()
16 |
17 | error_msg = "Not a valid color."
18 |
19 | def _value(self):
20 | if self.raw_data:
21 | return self.raw_data[0]
22 | if self.data:
23 | return str(self.data)
24 | else:
25 | return ""
26 |
27 | def process_formdata(self, valuelist):
28 | from colour import Color
29 |
30 | if valuelist:
31 | if valuelist[0] == "" or valuelist[0] == "":
32 | self.data = None
33 | else:
34 | try:
35 | self.data = Color(valuelist[0])
36 | except AttributeError:
37 | self.data = None
38 | raise ValueError(self.gettext(self.error_msg))
39 |
--------------------------------------------------------------------------------
/wtforms_components/fields/time.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import time
3 |
4 | from wtforms.fields import Field
5 |
6 | from ..widgets import TimeInput
7 |
8 |
9 | class TimeField(Field):
10 | """
11 | A text field which stores a `datetime.time` matching a format.
12 | """
13 |
14 | widget = TimeInput()
15 | error_msg = "Not a valid time."
16 |
17 | def __init__(self, label=None, validators=None, format="%H:%M", **kwargs):
18 | super().__init__(label, validators, **kwargs)
19 | self.format = format
20 |
21 | def _value(self):
22 | if self.raw_data:
23 | return " ".join(self.raw_data)
24 | elif self.data is not None:
25 | return self.data.strftime(self.format)
26 | else:
27 | return ""
28 |
29 | def process_formdata(self, valuelist):
30 | if valuelist:
31 | time_str = " ".join(valuelist)
32 | try:
33 | self.data = datetime.time(*time.strptime(time_str, self.format)[3:6])
34 | except ValueError:
35 | self.data = None
36 | raise ValueError(self.gettext(self.error_msg))
37 |
--------------------------------------------------------------------------------
/tests/test_widgets.py:
--------------------------------------------------------------------------------
1 | from wtforms import Form
2 | from wtforms.fields import SelectField
3 | from wtforms_test import FormTestCase
4 |
5 | from wtforms_components import SelectWidget
6 |
7 |
8 | class Dummy:
9 | fruits = None
10 |
11 |
12 | class TestSelectWidgetWithNativeSelect(FormTestCase):
13 | choices = (
14 | ("apple", "Apple"),
15 | ("peach", "Peach"),
16 | ("pear", "Pear"),
17 | ("cucumber", "Cucumber"),
18 | ("potato", "Potato"),
19 | ("tomato", "Tomato"),
20 | )
21 |
22 | def init_form(self, **kwargs):
23 | class TestForm(Form):
24 | fruit = SelectField(widget=SelectWidget(), **kwargs)
25 |
26 | self.form_class = TestForm
27 | return self.form_class
28 |
29 | def test_option_selected(self):
30 | form_class = self.init_form(choices=self.choices)
31 |
32 | obj = Dummy()
33 | obj.fruit = "peach"
34 | form = form_class(obj=obj)
35 | assert '' in str(form.fruit)
36 |
37 | def test_default_value(self):
38 | form_class = self.init_form(choices=self.choices, default="pear")
39 | form = form_class()
40 | assert '' in str(form.fruit)
41 |
--------------------------------------------------------------------------------
/tests/test_read_only_function.py:
--------------------------------------------------------------------------------
1 | from wtforms import Form
2 | from wtforms.fields import StringField
3 | from wtforms_test import FormTestCase
4 |
5 | from tests import MultiDict
6 | from wtforms_components import read_only
7 |
8 |
9 | class TestReadOnlyFunction(FormTestCase):
10 | def test_prevents_value_changing(self):
11 | class MyForm(Form):
12 | name = StringField(default="")
13 |
14 | form = MyForm()
15 | read_only(form.name)
16 | form.process(MultiDict({"name": "New value"}))
17 | assert form.name.data == ""
18 |
19 | def test_preserves_previous_value(self):
20 | class MyForm(Form):
21 | name = StringField()
22 |
23 | form = MyForm()
24 | form.name.data = "Previous value"
25 | read_only(form.name)
26 | form.process(MultiDict({"name": "New value"}))
27 | assert form.name.data == "Previous value"
28 |
29 | def test_prevents_value_population(self):
30 | class MyForm(Form):
31 | name = StringField()
32 |
33 | class MyModel:
34 | pass
35 |
36 | form = MyForm()
37 | model = MyModel()
38 | form.name.data = "Existing value"
39 | read_only(form.name)
40 | form.populate_obj(model)
41 | assert not hasattr(model, "name")
42 |
--------------------------------------------------------------------------------
/wtforms_components/fields/__init__.py:
--------------------------------------------------------------------------------
1 | from .ajax import AjaxField
2 | from .color import ColorField
3 | from .html5 import (
4 | DateField,
5 | DateTimeField,
6 | DateTimeLocalField,
7 | DecimalField,
8 | DecimalSliderField,
9 | EmailField,
10 | IntegerField,
11 | IntegerSliderField,
12 | SearchField,
13 | StringField,
14 | )
15 | from .interval import (
16 | DateIntervalField,
17 | DateTimeIntervalField,
18 | DecimalIntervalField,
19 | FloatIntervalField,
20 | IntIntervalField,
21 | )
22 | from .json_field import JSONField
23 | from .passive_hidden import PassiveHiddenField
24 | from .select import SelectField
25 | from .select_multiple import SelectMultipleField
26 | from .split_date_time import SplitDateTimeField
27 | from .time import TimeField
28 |
29 | __all__ = (
30 | AjaxField,
31 | ColorField,
32 | DateField,
33 | DateIntervalField,
34 | DateTimeField,
35 | DateTimeIntervalField,
36 | DateTimeLocalField,
37 | DecimalField,
38 | DecimalIntervalField,
39 | DecimalSliderField,
40 | EmailField,
41 | FloatIntervalField,
42 | IntegerField,
43 | IntegerSliderField,
44 | IntIntervalField,
45 | JSONField,
46 | PassiveHiddenField,
47 | SearchField,
48 | SelectField,
49 | SelectMultipleField,
50 | SplitDateTimeField,
51 | StringField,
52 | TimeField,
53 | )
54 |
--------------------------------------------------------------------------------
/tests/test_time_range_validator.py:
--------------------------------------------------------------------------------
1 | from datetime import time
2 |
3 | from wtforms import Form
4 | from wtforms_test import FormTestCase
5 |
6 | from tests import MultiDict
7 | from wtforms_components import TimeField, TimeRange
8 |
9 |
10 | class TestTimeRangeValidator(FormTestCase):
11 | def init_form(self, **kwargs):
12 | class ModelTestForm(Form):
13 | time = TimeField(validators=[TimeRange(**kwargs)])
14 |
15 | self.form_class = ModelTestForm
16 | return self.form_class
17 |
18 | def test_time_greater_than_validator(self):
19 | form_class = self.init_form(min=time(12))
20 | form = form_class(MultiDict(time="11:12"))
21 | form.validate()
22 | error_msg = "Time must be greater than 12:00."
23 | assert form.errors["time"] == [error_msg]
24 |
25 | def test_time_less_than_validator(self):
26 | form_class = self.init_form(max=time(13, 30))
27 | form = form_class(MultiDict(time="13:40"))
28 | form.validate()
29 | error_msg = "Time must be less than 13:30."
30 | assert form.errors["time"] == [error_msg]
31 |
32 | def test_time_between_validator(self):
33 | form_class = self.init_form(min=time(12), max=time(13))
34 | form = form_class(MultiDict(time="14:30"))
35 | form.validate()
36 | error_msg = "Time must be between 12:00 and 13:00."
37 | assert form.errors["time"] == [error_msg]
38 |
--------------------------------------------------------------------------------
/tests/fields/test_select_multiple_field.py:
--------------------------------------------------------------------------------
1 | from wtforms import Form
2 | from wtforms_test import FormTestCase
3 |
4 | from tests import MultiDict
5 | from wtforms_components import SelectMultipleField
6 |
7 |
8 | class Dummy:
9 | fruits = []
10 |
11 |
12 | class TestSelectMultipleField(FormTestCase):
13 | choices = (
14 | ("Fruits", (("apple", "Apple"), ("peach", "Peach"), ("pear", "Pear"))),
15 | (
16 | "Vegetables",
17 | (
18 | ("cucumber", "Cucumber"),
19 | ("potato", "Potato"),
20 | ("tomato", "Tomato"),
21 | ),
22 | ),
23 | )
24 |
25 | def init_form(self, **kwargs):
26 | class TestForm(Form):
27 | fruits = SelectMultipleField(**kwargs)
28 |
29 | self.form_class = TestForm
30 | return self.form_class
31 |
32 | def test_understands_nested_choices(self):
33 | form_class = self.init_form(choices=self.choices)
34 | form = form_class(MultiDict([("fruits", "apple"), ("fruits", "invalid")]))
35 | form.validate()
36 |
37 | assert form.errors == {
38 | "fruits": ["'invalid' is not a valid choice for this field"]
39 | }
40 |
41 | def test_option_selected(self):
42 | form_class = self.init_form(choices=self.choices)
43 |
44 | obj = Dummy()
45 | obj.fruits = ["peach"]
46 | form = form_class(obj=obj)
47 | assert '' in str(form.fruits)
48 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2012, Konsta Vesterinen, Janne Vanhala
2 |
3 | All rights reserved.
4 |
5 | Redistribution and use in source and binary forms, with or without
6 | modification, are permitted provided that the following conditions are met:
7 |
8 | * Redistributions of source code must retain the above copyright notice, this
9 | list of conditions and the following disclaimer.
10 |
11 | * Redistributions in binary form must reproduce the above copyright notice,
12 | this list of conditions and the following disclaimer in the documentation
13 | and/or other materials provided with the distribution.
14 |
15 | * The names of the contributors may not be used to endorse or promote products
16 | derived from this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT,
22 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
23 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
25 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
26 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
27 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/wtforms_components/fields/select_multiple.py:
--------------------------------------------------------------------------------
1 | from wtforms.validators import ValidationError
2 |
3 | from ..widgets import SelectWidget
4 | from .select import SelectField
5 |
6 |
7 | class SelectMultipleField(SelectField):
8 | """
9 | No different from a normal select field, except this one can take (and
10 | validate) multiple choices. You'll need to specify the HTML `rows`
11 | attribute to the select field when rendering.
12 | """
13 |
14 | widget = SelectWidget(multiple=True)
15 |
16 | def process_data(self, value):
17 | try:
18 | self.data = list(self.coerce(v) for v in value)
19 | except (ValueError, TypeError):
20 | self.data = None
21 |
22 | def process_formdata(self, valuelist):
23 | try:
24 | self.data = list(self.coerce(x) for x in valuelist)
25 | except ValueError:
26 | raise ValueError(
27 | self.gettext(
28 | "Invalid choice(s): one or more data inputs " "could not be coerced"
29 | )
30 | )
31 |
32 | def pre_validate(self, form):
33 | if self.data:
34 | values = self.choice_values
35 | for value in self.data:
36 | if value not in values:
37 | raise ValidationError(
38 | self.gettext(
39 | "'%(value)s' is not a valid" " choice for this field"
40 | )
41 | % dict(value=value)
42 | )
43 |
--------------------------------------------------------------------------------
/tests/test_date_range_validator.py:
--------------------------------------------------------------------------------
1 | from datetime import date
2 |
3 | from wtforms import Form
4 | from wtforms.fields import DateField
5 | from wtforms_test import FormTestCase
6 |
7 | from tests import MultiDict
8 | from wtforms_components import DateRange
9 |
10 |
11 | class TestDateRangeValidator(FormTestCase):
12 | def init_form(self, **kwargs):
13 | class ModelTestForm(Form):
14 | date = DateField(validators=[DateRange(**kwargs)])
15 |
16 | self.form_class = ModelTestForm
17 | return self.form_class
18 |
19 | def test_date_greater_than_validator(self):
20 | form_class = self.init_form(min=date(1990, 1, 1))
21 | form = form_class(MultiDict(date="1980-1-1"))
22 | form.validate()
23 | error_msg = "Date must be equal to or later than 1990-01-01."
24 | assert form.errors["date"] == [error_msg]
25 |
26 | def test_date_less_than_validator(self):
27 | form_class = self.init_form(max=date(1990, 1, 1))
28 | form = form_class(MultiDict(date="1991-1-1"))
29 | form.validate()
30 | error_msg = "Date must be equal to or earlier than 1990-01-01."
31 | assert form.errors["date"] == [error_msg]
32 |
33 | def test_date_between_validator(self):
34 | form_class = self.init_form(min=date(1990, 1, 1), max=date(1991, 1, 1))
35 | form = form_class(MultiDict(date="1989-1-1"))
36 | form.validate()
37 | error_msg = "Date must be between 1990-01-01 and 1991-01-01."
38 | assert form.errors["date"] == [error_msg]
39 |
--------------------------------------------------------------------------------
/wtforms_components/fields/html5.py:
--------------------------------------------------------------------------------
1 | from wtforms.fields import (
2 | DateField,
3 | DateTimeField,
4 | DecimalField,
5 | DecimalRangeField,
6 | IntegerField,
7 | IntegerRangeField,
8 | SearchField,
9 | )
10 | from wtforms.fields import StringField as _StringField
11 |
12 | from ..widgets import (
13 | DateInput,
14 | DateTimeInput,
15 | DateTimeLocalInput,
16 | EmailInput,
17 | NumberInput,
18 | RangeInput,
19 | SearchInput,
20 | TextInput,
21 | )
22 |
23 |
24 | class EmailField(_StringField):
25 | widget = EmailInput()
26 |
27 |
28 | class IntegerField(IntegerField):
29 | widget = NumberInput(step="1")
30 |
31 |
32 | class DecimalField(DecimalField):
33 | widget = NumberInput(step="any")
34 |
35 |
36 | class DateTimeLocalField(DateTimeField):
37 | def __init__(
38 | self, label=None, validators=None, format="%Y-%m-%dT%H:%M:%S", **kwargs
39 | ):
40 | super().__init__(label, validators, format, **kwargs)
41 |
42 | widget = DateTimeLocalInput()
43 |
44 |
45 | class DateTimeField(DateTimeField):
46 | widget = DateTimeInput()
47 |
48 |
49 | class DateField(DateField):
50 | widget = DateInput()
51 |
52 |
53 | class IntegerSliderField(IntegerRangeField):
54 | widget = RangeInput(step="1")
55 |
56 |
57 | class DecimalSliderField(DecimalRangeField):
58 | widget = RangeInput(step="any")
59 |
60 |
61 | class SearchField(SearchField):
62 | widget = SearchInput()
63 |
64 |
65 | class StringField(_StringField):
66 | widget = TextInput()
67 |
--------------------------------------------------------------------------------
/docs/requirements.txt:
--------------------------------------------------------------------------------
1 | #
2 | # This file is autogenerated by pip-compile with Python 3.9
3 | # by the following command:
4 | #
5 | # pip-compile
6 | #
7 | alabaster==0.7.16
8 | # via sphinx
9 | babel==2.16.0
10 | # via sphinx
11 | beautifulsoup4==4.12.3
12 | # via furo
13 | certifi==2024.8.30
14 | # via requests
15 | charset-normalizer==3.4.0
16 | # via requests
17 | docutils==0.21.2
18 | # via sphinx
19 | furo==2024.8.6
20 | # via -r requirements.in
21 | idna==3.10
22 | # via requests
23 | imagesize==1.4.1
24 | # via sphinx
25 | importlib-metadata==8.5.0
26 | # via sphinx
27 | jinja2==3.1.6
28 | # via sphinx
29 | markupsafe==3.0.2
30 | # via jinja2
31 | packaging==24.2
32 | # via sphinx
33 | pygments==2.18.0
34 | # via
35 | # furo
36 | # sphinx
37 | requests==2.32.4
38 | # via sphinx
39 | snowballstemmer==2.2.0
40 | # via sphinx
41 | soupsieve==2.6
42 | # via beautifulsoup4
43 | sphinx==7.4.7
44 | # via
45 | # -r requirements.in
46 | # furo
47 | # sphinx-basic-ng
48 | sphinx-basic-ng==1.0.0b2
49 | # via furo
50 | sphinxcontrib-applehelp==2.0.0
51 | # via sphinx
52 | sphinxcontrib-devhelp==2.0.0
53 | # via sphinx
54 | sphinxcontrib-htmlhelp==2.1.0
55 | # via sphinx
56 | sphinxcontrib-jsmath==1.0.1
57 | # via sphinx
58 | sphinxcontrib-qthelp==2.0.0
59 | # via sphinx
60 | sphinxcontrib-serializinghtml==2.0.0
61 | # via sphinx
62 | tomli==2.1.0
63 | # via sphinx
64 | urllib3==2.6.0
65 | # via requests
66 | zipp==3.21.0
67 | # via importlib-metadata
68 |
--------------------------------------------------------------------------------
/tests/fields/test_color_field.py:
--------------------------------------------------------------------------------
1 | from wtforms import Form
2 | from wtforms_test import FormTestCase
3 |
4 | from tests import MultiDict
5 | from wtforms_components import ColorField
6 |
7 |
8 | class TestColorField(FormTestCase):
9 | def setup_method(self, method):
10 | self.valid_colors = [
11 | "#222222",
12 | "cyan",
13 | ]
14 | self.invalid_colors = [
15 | "abc",
16 | "#123123123",
17 | ]
18 |
19 | def init_form(self, **kwargs):
20 | class TestForm(Form):
21 | color = ColorField(**kwargs)
22 |
23 | self.form_class = TestForm
24 | return self.form_class
25 |
26 | def test_valid_colors(self):
27 | form_class = self.init_form()
28 | for color in self.valid_colors:
29 | form = form_class(MultiDict(color=color))
30 | form.validate()
31 | assert len(form.errors) == 0
32 |
33 | def test_invalid_number_ranges(self):
34 | form_class = self.init_form()
35 | for color in self.invalid_colors:
36 | form = form_class(MultiDict(color=color))
37 | form.validate()
38 | assert len(form.errors["color"]) == 1
39 |
40 | def test_field_rendering_when_validation_fails(self):
41 | form_class = self.init_form()
42 | form = form_class(MultiDict(color="invalid"))
43 | form.validate()
44 | assert 'value="invalid"' in str(form.color)
45 |
46 | def test_converts_empty_strings_to_none(self):
47 | form_class = self.init_form()
48 | form = form_class(MultiDict(color=""))
49 | assert form.data == {"color": None}
50 |
--------------------------------------------------------------------------------
/tests/test_if_validator.py:
--------------------------------------------------------------------------------
1 | from wtforms import Form
2 | from wtforms.fields import StringField
3 | from wtforms.validators import DataRequired
4 | from wtforms_test import FormTestCase
5 |
6 | from tests import MultiDict
7 | from wtforms_components import If
8 |
9 |
10 | class TestIfValidator(FormTestCase):
11 | def test_only_validates_if_condition_returns_true(self):
12 | class MyForm(Form):
13 | name = StringField(
14 | validators=[
15 | If(
16 | lambda form, field: False,
17 | DataRequired(),
18 | )
19 | ]
20 | )
21 |
22 | form = MyForm(MultiDict({"name": ""}))
23 | form.validate()
24 | assert not form.errors
25 |
26 | def test_encapsulates_given_validator(self):
27 | class MyForm(Form):
28 | name = StringField(
29 | validators=[
30 | If(
31 | lambda form, field: True,
32 | DataRequired(),
33 | )
34 | ]
35 | )
36 |
37 | form = MyForm(MultiDict({"name": ""}))
38 | form.validate()
39 | assert "name" in form.errors
40 |
41 | def test_supports_custom_error_messages(self):
42 | class MyForm(Form):
43 | name = StringField(
44 | validators=[
45 | If(
46 | lambda form, field: True,
47 | DataRequired(),
48 | message="Validation failed.",
49 | )
50 | ]
51 | )
52 |
53 | form = MyForm(MultiDict({"name": ""}))
54 | form.validate()
55 | assert form.errors["name"] == ["Validation failed."]
56 |
--------------------------------------------------------------------------------
/wtforms_components/fields/interval.py:
--------------------------------------------------------------------------------
1 | from intervals import (
2 | DateInterval,
3 | DateTimeInterval,
4 | DecimalInterval,
5 | FloatInterval,
6 | IntervalException,
7 | IntInterval,
8 | )
9 |
10 | from .html5 import StringField
11 |
12 |
13 | class IntervalField(StringField):
14 | """
15 | A string field representing an interval object from
16 | `intervals`_.
17 |
18 | .. _intervals:
19 | https://github.com/kvesteri/intervals
20 | """
21 |
22 | def _value(self):
23 | if self.raw_data:
24 | return self.raw_data[0]
25 | if self.data:
26 | return self.data.hyphenized
27 | else:
28 | return ""
29 |
30 | def process_formdata(self, valuelist):
31 | if valuelist:
32 | if valuelist[0] == "" or valuelist[0] == "":
33 | self.data = None
34 | else:
35 | try:
36 | self.data = self.interval_class.from_string(valuelist[0])
37 | except IntervalException:
38 | self.data = None
39 | raise ValueError(self.gettext(self.error_msg))
40 |
41 |
42 | class DecimalIntervalField(IntervalField):
43 | error_msg = "Not a valid decimal range value"
44 | interval_class = DecimalInterval
45 |
46 |
47 | class FloatIntervalField(IntervalField):
48 | error_msg = "Not a valid float range value"
49 | interval_class = FloatInterval
50 |
51 |
52 | class IntIntervalField(IntervalField):
53 | error_msg = "Not a valid int range value"
54 | interval_class = IntInterval
55 |
56 |
57 | class DateIntervalField(IntervalField):
58 | error_msg = "Not a valid date range value"
59 | interval_class = DateInterval
60 |
61 |
62 | class DateTimeIntervalField(IntervalField):
63 | error_msg = "Not a valid datetime range value"
64 | interval_class = DateTimeInterval
65 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | from wtforms import Form
2 | from wtforms.validators import DataRequired
3 | from wtforms_test import FormTestCase
4 |
5 |
6 | class MultiDict(dict):
7 | def getlist(self, key):
8 | return [self[key]]
9 |
10 |
11 | class SimpleFieldTestCase(FormTestCase):
12 | field_class = None
13 |
14 | def init_form(self, **kwargs):
15 | class TestForm(Form):
16 | test_field = self.field_class(**kwargs)
17 |
18 | self.form_class = TestForm
19 | return self.form_class
20 |
21 |
22 | class FieldTestCase(SimpleFieldTestCase):
23 | def test_assigns_required_from_validator(self):
24 | form_class = self.init_form(validators=[DataRequired()])
25 | form = form_class()
26 | assert (''
40 | )
41 |
42 | def test_renders_input_time_at_midnight(self):
43 | form_class = self.init_form()
44 | form = form_class(MultiDict(test_field="00:00"))
45 | assert str(form.test_field) == (
46 | ''
47 | )
48 |
49 |
50 | class TestTimeFieldWithSeconds(TestTimeField):
51 | def setup_method(self, method):
52 | self.valid_times = ["00:00:00", "11:11:11", "12:15:17"]
53 | self.invalid_times = [
54 | "00:61",
55 | "01:01:61",
56 | "unknown",
57 | ]
58 |
59 | def init_form(self, **kwargs):
60 | kwargs["format"] = "%H:%M:%S"
61 | return super().init_form(**kwargs)
62 |
--------------------------------------------------------------------------------
/wtforms_components/fields/split_date_time.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 | from wtforms import Form
4 | from wtforms.fields import FormField
5 | from wtforms.utils import unset_value
6 |
7 | from .html5 import DateField
8 | from .time import TimeField
9 |
10 |
11 | class Date:
12 | date = None
13 | time = None
14 |
15 |
16 | class SplitDateTimeField(FormField):
17 | def __init__(self, label=None, validators=None, separator="-", **kwargs):
18 | FormField.__init__(
19 | self,
20 | datetime_form(kwargs.pop("datetime_form", {})),
21 | label=label,
22 | validators=validators,
23 | separator=separator,
24 | **kwargs,
25 | )
26 |
27 | def process(self, formdata, data=unset_value, extra_filters=None):
28 | if data is unset_value:
29 | try:
30 | data = self.default()
31 | except TypeError:
32 | data = self.default
33 | if data:
34 | obj = Date()
35 | obj.date = data.date()
36 | obj.time = data.time()
37 | else:
38 | obj = None
39 |
40 | kwargs = dict()
41 | if extra_filters is not None:
42 | # do not enforce extra_filters=None injection to wtforms<3
43 | kwargs["extra_filters"] = extra_filters
44 | FormField.process(self, formdata, data=obj, **kwargs)
45 |
46 | def populate_obj(self, obj, name):
47 | if hasattr(obj, name):
48 | date = self.date.data
49 | hours, minutes = self.time.data.hour, self.time.data.minute
50 | setattr(
51 | obj,
52 | name,
53 | datetime.datetime(date.year, date.month, date.day, hours, minutes),
54 | )
55 |
56 |
57 | def datetime_form(options):
58 | options.setdefault("date", {})
59 | options.setdefault("time", {})
60 | options["date"].setdefault("label", "Date")
61 | options["time"].setdefault("label", "Time")
62 | base_form = options.get("base_form", Form)
63 |
64 | class DateTimeForm(base_form):
65 | date = DateField(**options["date"])
66 | time = TimeField(**options["time"])
67 |
68 | return DateTimeForm
69 |
--------------------------------------------------------------------------------
/wtforms_components/fields/select.py:
--------------------------------------------------------------------------------
1 | from copy import copy
2 |
3 | from wtforms import SelectField as _SelectField
4 | from wtforms.validators import ValidationError
5 |
6 | from ..widgets import SelectWidget
7 |
8 |
9 | class SelectField(_SelectField):
10 | """
11 | Add support of ``optgroup``'s' to default WTForms' ``SelectField`` class.
12 |
13 | So, next choices would be supported as well::
14 |
15 | (
16 | (
17 | "Fruits",
18 | (
19 | ("apple", "Apple"),
20 | ("peach", "Peach"),
21 | ("pear", "Pear"),
22 | ),
23 | ),
24 | (
25 | "Vegetables",
26 | (
27 | ("cucumber", "Cucumber"),
28 | ("potato", "Potato"),
29 | ("tomato", "Tomato"),
30 | ),
31 | ),
32 | )
33 |
34 | Also supports lazy choices (callables that return an iterable)
35 | """
36 |
37 | widget = SelectWidget()
38 |
39 | def __init__(self, *args, **kwargs):
40 | choices = kwargs.pop("choices", None)
41 | if callable(choices):
42 | super().__init__(*args, **kwargs)
43 | self.choices = copy(choices)
44 | else:
45 | super().__init__(*args, choices=choices, **kwargs)
46 |
47 | def iter_choices(self):
48 | """
49 | We should update how choices are iter to make sure that value from
50 | internal list or tuple should be selected.
51 | """
52 | for value, label in self.concrete_choices:
53 | yield (value, label, (self.coerce, self.data), {})
54 |
55 | @property
56 | def concrete_choices(self):
57 | if callable(self.choices):
58 | return self.choices()
59 | return self.choices
60 |
61 | @property
62 | def choice_values(self):
63 | values = []
64 | for value, label in self.concrete_choices:
65 | if isinstance(label, (list, tuple)):
66 | for subvalue, sublabel in label:
67 | values.append(subvalue)
68 | else:
69 | values.append(value)
70 | return values
71 |
72 | def pre_validate(self, form):
73 | """
74 | Don't forget to validate also values from embedded lists.
75 | """
76 | values = self.choice_values
77 | if (self.data is None and "" in values) or self.data in values:
78 | return True
79 |
80 | raise ValidationError(self.gettext("Not a valid choice"))
81 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | """
2 | WTForms-Components
3 | ------------------
4 |
5 | Additional fields, validators and widgets for WTForms.
6 | """
7 |
8 | import os
9 | import re
10 |
11 | from setuptools import setup
12 |
13 | HERE = os.path.dirname(os.path.abspath(__file__))
14 |
15 |
16 | def get_version():
17 | filename = os.path.join(HERE, "wtforms_components", "__init__.py")
18 | with open(filename) as f:
19 | contents = f.read()
20 | pattern = r'^__version__ = "(.*?)"$'
21 | return re.search(pattern, contents, re.MULTILINE).group(1)
22 |
23 |
24 | extras_require = {
25 | "test": [
26 | "pytest>=2.2.3",
27 | "flexmock>=0.9.7",
28 | "ruff==0.7.3",
29 | "WTForms-Test>=0.1.1",
30 | ],
31 | "color": ["colour>=0.0.4"],
32 | "ipaddress": [],
33 | "timezone": ["python-dateutil"],
34 | }
35 |
36 |
37 | # Add all optional dependencies to testing requirements.
38 | for name, requirements in extras_require.items():
39 | if name != "test":
40 | extras_require["test"] += requirements
41 |
42 |
43 | setup(
44 | name="WTForms-Components",
45 | version=get_version(),
46 | url="https://github.com/kvesteri/wtforms-components",
47 | license="BSD",
48 | author="Konsta Vesterinen",
49 | author_email="konsta@fastmonkeys.com",
50 | description="Additional fields, validators and widgets for WTForms.",
51 | long_description=__doc__,
52 | packages=["wtforms_components", "wtforms_components.fields"],
53 | zip_safe=False,
54 | include_package_data=True,
55 | platforms="any",
56 | install_requires=[
57 | "WTForms>=3.1.0",
58 | "validators>=0.21",
59 | "intervals>=0.6.0",
60 | "MarkupSafe>=1.0.0",
61 | ],
62 | extras_require=extras_require,
63 | classifiers=[
64 | "Environment :: Web Environment",
65 | "Intended Audience :: Developers",
66 | "License :: OSI Approved :: BSD License",
67 | "Operating System :: OS Independent",
68 | "Programming Language :: Python",
69 | "Programming Language :: Python :: 3",
70 | "Programming Language :: Python :: 3.9",
71 | "Programming Language :: Python :: 3.10",
72 | "Programming Language :: Python :: 3.11",
73 | "Programming Language :: Python :: 3.12",
74 | "Programming Language :: Python :: 3 :: Only",
75 | "Programming Language :: Python :: Implementation :: CPython",
76 | "Programming Language :: Python :: Implementation :: PyPy",
77 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
78 | "Topic :: Software Development :: Libraries :: Python Modules",
79 | ],
80 | python_requires=">=3.9",
81 | )
82 |
--------------------------------------------------------------------------------
/tests/test_email_validator.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from wtforms.validators import ValidationError
3 |
4 | from wtforms_components import Email
5 |
6 |
7 | class DummyTranslations:
8 | def gettext(self, string):
9 | return string
10 |
11 | def ngettext(self, singular, plural, n):
12 | if n == 1:
13 | return singular
14 |
15 | return plural
16 |
17 |
18 | class DummyForm(dict):
19 | pass
20 |
21 |
22 | class DummyField:
23 | _translations = DummyTranslations()
24 |
25 | def __init__(self, data, errors=(), raw_data=None):
26 | self.data = data
27 | self.errors = list(errors)
28 | self.raw_data = raw_data
29 |
30 | def gettext(self, string):
31 | return self._translations.gettext(string)
32 |
33 | def ngettext(self, singular, plural, n):
34 | return self._translations.ngettext(singular, plural, n)
35 |
36 |
37 | class TestEmailValidator:
38 | def setup_method(self, method):
39 | self.form = DummyForm()
40 |
41 | @pytest.mark.parametrize(
42 | "email",
43 | [
44 | "email@here.com",
45 | "weirder-email@here.and.there.com",
46 | "example@valid-----hyphens.com",
47 | "example@valid-with-hyphens.com",
48 | "test@domain.with.idn.tld.उदाहरण.परीक्षा",
49 | '"\\\011"@here.com',
50 | ],
51 | )
52 | def test_returns_none_on_valid_email(self, email):
53 | validate_email = Email()
54 | validate_email(self.form, DummyField(email))
55 |
56 | @pytest.mark.parametrize(
57 | ("email",),
58 | [
59 | (None,),
60 | ("",),
61 | ("abc",),
62 | ("abc@",),
63 | ("abc@bar",),
64 | ("a @x.cz",),
65 | ("abc@.com",),
66 | ("something@@somewhere.com",),
67 | ("email@127.0.0.1",),
68 | ("example@invalid-.com",),
69 | ("example@-invalid.com",),
70 | ("example@inv-.alid-.com",),
71 | ("example@inv-.-alid.com",),
72 | # Quoted-string format (CR not allowed)
73 | ('"\\\012"@here.com',),
74 | ],
75 | )
76 | def test_raises_validationerror_on_invalid_email(self, email):
77 | validate_email = Email()
78 | with pytest.raises(ValidationError):
79 | validate_email(self.form, DummyField(email))
80 |
81 | def test_default_validation_error_message(self):
82 | validate_email = Email()
83 | try:
84 | validate_email(self.form, DummyField("@@@"))
85 | assert False, "No validation error thrown."
86 | except ValidationError as e:
87 | assert str(e) == "Invalid email address."
88 |
--------------------------------------------------------------------------------
/wtforms_components/fields/ajax.py:
--------------------------------------------------------------------------------
1 | import operator
2 |
3 | from wtforms import fields, widgets
4 | from wtforms.validators import ValidationError
5 |
6 | anyjson = None
7 | try:
8 | import anyjson
9 | except ImportError:
10 | pass
11 |
12 |
13 | class ImproperlyConfigured(Exception):
14 | pass
15 |
16 |
17 | class AjaxField(fields.Field):
18 | widget = widgets.HiddenInput()
19 |
20 | def __init__(
21 | self,
22 | label=None,
23 | validators=None,
24 | data_url=None,
25 | get_object=None,
26 | get_pk=None,
27 | coerce=int,
28 | get_label=None,
29 | allow_blank=False,
30 | blank_text="",
31 | **kwargs,
32 | ):
33 | super().__init__(label, validators, **kwargs)
34 |
35 | if anyjson is None:
36 | raise ImproperlyConfigured(
37 | "AjaxField requires anyjson extension to be installed."
38 | )
39 |
40 | if data_url is None:
41 | raise Exception("data_url must be given")
42 |
43 | self.get_pk = get_pk
44 |
45 | if get_label is None:
46 | self.get_label = lambda x: x
47 | elif isinstance(get_label, str):
48 | self.get_label = operator.attrgetter(get_label)
49 | else:
50 | self.get_label = get_label
51 |
52 | self.coerce = coerce
53 | self.data_url = data_url
54 | self.get_object = get_object
55 | self.allow_blank = allow_blank
56 | self.blank_text = blank_text
57 |
58 | @property
59 | def data(self):
60 | if self._formdata is not None:
61 | try:
62 | pk = self.coerce(self._formdata)
63 | except ValueError:
64 | self.data = None
65 | else:
66 | self.data = self.get_object(pk)
67 | return self._data
68 |
69 | @data.setter
70 | def data(self, data):
71 | self._data = data
72 | self._formdata = None
73 |
74 | def process_formdata(self, valuelist):
75 | if valuelist:
76 | if self.allow_blank and not valuelist[0]:
77 | self.data = None
78 | else:
79 | self._data = None
80 | self._formdata = valuelist[0]
81 |
82 | def pre_validate(self, form):
83 | if self.data is None:
84 | if self._formdata or not self.allow_blank:
85 | raise ValidationError("Not a valid choice")
86 |
87 | def __call__(self, **kwargs):
88 | kwargs.setdefault("data-allow-clear", anyjson.serialize(self.allow_blank))
89 | kwargs.setdefault("data-placeholder", self.blank_text)
90 | kwargs.setdefault("data-url", self.data_url)
91 | if self.data is not None:
92 | kwargs.setdefault("data-initial-label", self.get_label(self.data))
93 | kwargs.setdefault("value", self.get_pk(self.data))
94 | else:
95 | kwargs.setdefault("value", "")
96 | return super().__call__(**kwargs)
97 |
--------------------------------------------------------------------------------
/tests/fields/test_select_field.py:
--------------------------------------------------------------------------------
1 | from wtforms import Form
2 | from wtforms_test import FormTestCase
3 |
4 | from tests import MultiDict
5 | from wtforms_components import SelectField
6 |
7 |
8 | class Dummy:
9 | fruits = []
10 |
11 |
12 | class TestSelectField(FormTestCase):
13 | choices = (
14 | ("Fruits", (("apple", "Apple"), ("peach", "Peach"), ("pear", "Pear"))),
15 | (
16 | "Vegetables",
17 | (
18 | ("cucumber", "Cucumber"),
19 | ("potato", "Potato"),
20 | ("tomato", "Tomato"),
21 | ),
22 | ),
23 | )
24 |
25 | def init_form(self, **kwargs):
26 | class TestForm(Form):
27 | fruit = SelectField(**kwargs)
28 |
29 | self.form_class = TestForm
30 | return self.form_class
31 |
32 | def test_understands_nested_choices(self):
33 | form_class = self.init_form(choices=self.choices)
34 | form = form_class(MultiDict([("fruit", "invalid")]))
35 | form.validate()
36 |
37 | assert len(form.errors["fruit"]) == 1
38 |
39 | def test_understands_mixing_of_choice_types(self):
40 | choices = (
41 | ("Fruits", (("apple", "Apple"), ("peach", "Peach"), ("pear", "Pear"))),
42 | ("cucumber", "Cucumber"),
43 | )
44 |
45 | form_class = self.init_form(choices=choices)
46 | form = form_class(MultiDict([("fruit", "cucumber")]))
47 | form.validate()
48 | assert len(form.errors) == 0
49 |
50 | def test_understands_callables_as_choices(self):
51 | form_class = self.init_form(choices=lambda: [])
52 | form = form_class(MultiDict([("fruit", "invalid")]))
53 | form.validate()
54 |
55 | assert len(form.errors["fruit"]) == 1
56 |
57 | def test_option_selected(self):
58 | form_class = self.init_form(choices=self.choices)
59 |
60 | obj = Dummy()
61 | obj.fruit = "peach"
62 | form = form_class(obj=obj)
63 | assert '' in str(form.fruit)
64 |
65 | def test_nested_option_selected_by_field_default_value(self):
66 | form_class = self.init_form(choices=self.choices, default="pear")
67 | form = form_class()
68 | assert '' in str(form.fruit)
69 |
70 | def test_option_selected_by_field_default_value(self):
71 | choices = [("apple", "Apple"), ("peach", "Peach"), ("pear", "Pear")]
72 | form_class = self.init_form(choices=choices, default="pear")
73 | form = form_class()
74 | assert '' in str(form.fruit)
75 |
76 | def test_callable_option_selected_by_field_default_value(self):
77 | def choices():
78 | return [("apple", "Apple"), ("peach", "Peach"), ("pear", "Pear")]
79 |
80 | form_class = self.init_form(choices=choices, default="pear")
81 | form = form_class()
82 | assert '' in str(form.fruit)
83 |
84 | def test_data_coercion(self):
85 | choices = (
86 | ("Fruits", ((0, "Apple"), (1, "Peach"), (2, "Pear"))),
87 | (3, "Cucumber"),
88 | )
89 |
90 | form_class = self.init_form(choices=choices, coerce=int)
91 | form = form_class(MultiDict([("fruit", "1")]))
92 | form.validate()
93 | assert not form.errors
94 |
--------------------------------------------------------------------------------
/tests/fields/test_split_date_time_field.py:
--------------------------------------------------------------------------------
1 | from datetime import date, datetime, time
2 |
3 | from wtforms import Form
4 | from wtforms.validators import DataRequired
5 |
6 | from tests import MultiDict, SimpleFieldTestCase
7 | from wtforms_components.fields import SplitDateTimeField
8 |
9 |
10 | class Obj:
11 | test_field = None
12 |
13 |
14 | class TestSplitDateTimeField(SimpleFieldTestCase):
15 | field_class = SplitDateTimeField
16 |
17 | def test_assigns_required_to_date(self):
18 | form_class = self.init_form(
19 | datetime_form={"date": {"validators": [DataRequired()]}}
20 | )
21 | form = form_class()
22 | assert str(form.test_field.date) == (
23 | ''
25 | )
26 |
27 | def test_renders_date_field(self):
28 | form_class = self.init_form()
29 | form = form_class()
30 | assert str(form.test_field.date) == (
31 | ''
33 | )
34 |
35 | def test_assigns_required_to_time(self):
36 | form_class = self.init_form(
37 | datetime_form={"time": {"validators": [DataRequired()]}}
38 | )
39 | form = form_class()
40 | assert str(form.test_field.time) == (
41 | ''
43 | )
44 |
45 | def test_renders_time_field(self):
46 | form_class = self.init_form()
47 | form = form_class()
48 | assert str(form.test_field.time) == (
49 | ''
51 | )
52 |
53 | def test_processes_values(self):
54 | form_class = self.init_form()
55 | form = form_class(
56 | MultiDict(
57 | {
58 | "test_field-date": "2000-3-2",
59 | "test_field-time": "19:10",
60 | }
61 | )
62 | )
63 | assert form.test_field.data["date"] == date(2000, 3, 2)
64 | assert form.test_field.data["time"] == time(19, 10)
65 |
66 | def test_populates_object(self):
67 | form_class = self.init_form()
68 | form = form_class(
69 | MultiDict(
70 | {
71 | "test_field-date": "2000-3-2",
72 | "test_field-time": "19:10",
73 | }
74 | )
75 | )
76 | obj = Obj()
77 | form.populate_obj(obj)
78 | assert obj.test_field == datetime(2000, 3, 2, 19, 10)
79 |
80 | def test_processes_values_when_format_is_set(self):
81 | form_class = self.init_form(
82 | datetime_form={
83 | "date": {"format": "%d.%m.%Y"},
84 | "time": {"format": "%H.%M"},
85 | }
86 | )
87 | form = form_class(
88 | MultiDict(
89 | {
90 | "test_field-date": "2.3.2000",
91 | "test_field-time": "19.10",
92 | }
93 | )
94 | )
95 | assert form.test_field.data["date"] == date(2000, 3, 2)
96 | assert form.test_field.data["time"] == time(19, 10)
97 |
98 | def test_default_base_form(self):
99 | form_class = self.init_form()
100 | form = form_class()
101 | assert form.test_field.form.__class__.__bases__ == (Form,)
102 |
103 | def test_custom_base_form(self):
104 | class A(Form):
105 | pass
106 |
107 | form_class = self.init_form(datetime_form={"base_form": A})
108 | form = form_class()
109 | assert form.test_field.form.__class__.__bases__ == (A,)
110 |
111 | def test_custom_base_form_with_two_instances(self):
112 | class A(Form):
113 | pass
114 |
115 | form_class = self.init_form(datetime_form={"base_form": A})
116 | form = form_class()
117 | form2 = form_class()
118 | assert form.test_field.form.__class__.__bases__ == (A,)
119 | assert form2.test_field.form.__class__.__bases__ == (A,)
120 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | REM Command file for Sphinx documentation
4 |
5 | if "%SPHINXBUILD%" == "" (
6 | set SPHINXBUILD=sphinx-build
7 | )
8 | set BUILDDIR=_build
9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
10 | set I18NSPHINXOPTS=%SPHINXOPTS% .
11 | if NOT "%PAPER%" == "" (
12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
14 | )
15 |
16 | if "%1" == "" goto help
17 |
18 | if "%1" == "help" (
19 | :help
20 | echo.Please use `make ^` where ^ is one of
21 | echo. html to make standalone HTML files
22 | echo. dirhtml to make HTML files named index.html in directories
23 | echo. singlehtml to make a single large HTML file
24 | echo. pickle to make pickle files
25 | echo. json to make JSON files
26 | echo. htmlhelp to make HTML files and a HTML help project
27 | echo. qthelp to make HTML files and a qthelp project
28 | echo. devhelp to make HTML files and a Devhelp project
29 | echo. epub to make an epub
30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
31 | echo. text to make text files
32 | echo. man to make manual pages
33 | echo. texinfo to make Texinfo files
34 | echo. gettext to make PO message catalogs
35 | echo. changes to make an overview over all changed/added/deprecated items
36 | echo. linkcheck to check all external links for integrity
37 | echo. doctest to run all doctests embedded in the documentation if enabled
38 | goto end
39 | )
40 |
41 | if "%1" == "clean" (
42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
43 | del /q /s %BUILDDIR%\*
44 | goto end
45 | )
46 |
47 | if "%1" == "html" (
48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
49 | if errorlevel 1 exit /b 1
50 | echo.
51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html.
52 | goto end
53 | )
54 |
55 | if "%1" == "dirhtml" (
56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
57 | if errorlevel 1 exit /b 1
58 | echo.
59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
60 | goto end
61 | )
62 |
63 | if "%1" == "singlehtml" (
64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
65 | if errorlevel 1 exit /b 1
66 | echo.
67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
68 | goto end
69 | )
70 |
71 | if "%1" == "pickle" (
72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
73 | if errorlevel 1 exit /b 1
74 | echo.
75 | echo.Build finished; now you can process the pickle files.
76 | goto end
77 | )
78 |
79 | if "%1" == "json" (
80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
81 | if errorlevel 1 exit /b 1
82 | echo.
83 | echo.Build finished; now you can process the JSON files.
84 | goto end
85 | )
86 |
87 | if "%1" == "htmlhelp" (
88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
89 | if errorlevel 1 exit /b 1
90 | echo.
91 | echo.Build finished; now you can run HTML Help Workshop with the ^
92 | .hhp project file in %BUILDDIR%/htmlhelp.
93 | goto end
94 | )
95 |
96 | if "%1" == "qthelp" (
97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
98 | if errorlevel 1 exit /b 1
99 | echo.
100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^
101 | .qhcp project file in %BUILDDIR%/qthelp, like this:
102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\WTForms-Components.qhcp
103 | echo.To view the help file:
104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\WTForms-Components.ghc
105 | goto end
106 | )
107 |
108 | if "%1" == "devhelp" (
109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
110 | if errorlevel 1 exit /b 1
111 | echo.
112 | echo.Build finished.
113 | goto end
114 | )
115 |
116 | if "%1" == "epub" (
117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
118 | if errorlevel 1 exit /b 1
119 | echo.
120 | echo.Build finished. The epub file is in %BUILDDIR%/epub.
121 | goto end
122 | )
123 |
124 | if "%1" == "latex" (
125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
126 | if errorlevel 1 exit /b 1
127 | echo.
128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
129 | goto end
130 | )
131 |
132 | if "%1" == "text" (
133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
134 | if errorlevel 1 exit /b 1
135 | echo.
136 | echo.Build finished. The text files are in %BUILDDIR%/text.
137 | goto end
138 | )
139 |
140 | if "%1" == "man" (
141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
142 | if errorlevel 1 exit /b 1
143 | echo.
144 | echo.Build finished. The manual pages are in %BUILDDIR%/man.
145 | goto end
146 | )
147 |
148 | if "%1" == "texinfo" (
149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
150 | if errorlevel 1 exit /b 1
151 | echo.
152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
153 | goto end
154 | )
155 |
156 | if "%1" == "gettext" (
157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
158 | if errorlevel 1 exit /b 1
159 | echo.
160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
161 | goto end
162 | )
163 |
164 | if "%1" == "changes" (
165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
166 | if errorlevel 1 exit /b 1
167 | echo.
168 | echo.The overview file is in %BUILDDIR%/changes.
169 | goto end
170 | )
171 |
172 | if "%1" == "linkcheck" (
173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
174 | if errorlevel 1 exit /b 1
175 | echo.
176 | echo.Link check complete; look for any errors in the above output ^
177 | or in %BUILDDIR%/linkcheck/output.txt.
178 | goto end
179 | )
180 |
181 | if "%1" == "doctest" (
182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
183 | if errorlevel 1 exit /b 1
184 | echo.
185 | echo.Testing of doctests in the sources finished, look at the ^
186 | results in %BUILDDIR%/doctest/output.txt.
187 | goto end
188 | )
189 |
190 | :end
191 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | PAPER =
8 | BUILDDIR = _build
9 |
10 | # Internal variables.
11 | PAPEROPT_a4 = -D latex_paper_size=a4
12 | PAPEROPT_letter = -D latex_paper_size=letter
13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
14 | # the i18n builder cannot share the environment and doctrees with the others
15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
16 |
17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
18 |
19 | help:
20 | @echo "Please use \`make ' where is one of"
21 | @echo " html to make standalone HTML files"
22 | @echo " dirhtml to make HTML files named index.html in directories"
23 | @echo " singlehtml to make a single large HTML file"
24 | @echo " pickle to make pickle files"
25 | @echo " json to make JSON files"
26 | @echo " htmlhelp to make HTML files and a HTML help project"
27 | @echo " qthelp to make HTML files and a qthelp project"
28 | @echo " devhelp to make HTML files and a Devhelp project"
29 | @echo " epub to make an epub"
30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
31 | @echo " latexpdf to make LaTeX files and run them through pdflatex"
32 | @echo " text to make text files"
33 | @echo " man to make manual pages"
34 | @echo " texinfo to make Texinfo files"
35 | @echo " info to make Texinfo files and run them through makeinfo"
36 | @echo " gettext to make PO message catalogs"
37 | @echo " changes to make an overview of all changed/added/deprecated items"
38 | @echo " linkcheck to check all external links for integrity"
39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)"
40 |
41 | clean:
42 | -rm -rf $(BUILDDIR)/*
43 |
44 | html:
45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
46 | @echo
47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
48 |
49 | dirhtml:
50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
51 | @echo
52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
53 |
54 | singlehtml:
55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
56 | @echo
57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
58 |
59 | pickle:
60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
61 | @echo
62 | @echo "Build finished; now you can process the pickle files."
63 |
64 | json:
65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
66 | @echo
67 | @echo "Build finished; now you can process the JSON files."
68 |
69 | htmlhelp:
70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
71 | @echo
72 | @echo "Build finished; now you can run HTML Help Workshop with the" \
73 | ".hhp project file in $(BUILDDIR)/htmlhelp."
74 |
75 | qthelp:
76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
77 | @echo
78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \
79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/WTForms-Components.qhcp"
81 | @echo "To view the help file:"
82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/WTForms-Components.qhc"
83 |
84 | devhelp:
85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
86 | @echo
87 | @echo "Build finished."
88 | @echo "To view the help file:"
89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/WTForms-Components"
90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/WTForms-Components"
91 | @echo "# devhelp"
92 |
93 | epub:
94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
95 | @echo
96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
97 |
98 | latex:
99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
100 | @echo
101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \
103 | "(use \`make latexpdf' here to do that automatically)."
104 |
105 | latexpdf:
106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
107 | @echo "Running LaTeX files through pdflatex..."
108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf
109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
110 |
111 | text:
112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
113 | @echo
114 | @echo "Build finished. The text files are in $(BUILDDIR)/text."
115 |
116 | man:
117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
118 | @echo
119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
120 |
121 | texinfo:
122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
123 | @echo
124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
125 | @echo "Run \`make' in that directory to run these through makeinfo" \
126 | "(use \`make info' here to do that automatically)."
127 |
128 | info:
129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
130 | @echo "Running Texinfo files through makeinfo..."
131 | make -C $(BUILDDIR)/texinfo info
132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
133 |
134 | gettext:
135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
136 | @echo
137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
138 |
139 | changes:
140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
141 | @echo
142 | @echo "The overview file is in $(BUILDDIR)/changes."
143 |
144 | linkcheck:
145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
146 | @echo
147 | @echo "Link check complete; look for any errors in the above output " \
148 | "or in $(BUILDDIR)/linkcheck/output.txt."
149 |
150 | doctest:
151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
152 | @echo "Testing of doctests in the sources finished, look at the " \
153 | "results in $(BUILDDIR)/doctest/output.txt."
154 |
--------------------------------------------------------------------------------
/wtforms_components/validators.py:
--------------------------------------------------------------------------------
1 | from validators import email
2 | from wtforms import ValidationError
3 | from wtforms.validators import StopValidation
4 |
5 |
6 | class ControlStructure:
7 | """
8 | Base object for validator control structures
9 | """
10 |
11 | message = None
12 |
13 | def reraise(self, exc):
14 | if not self.message:
15 | raise exc
16 | else:
17 | raise type(exc)(self.message)
18 |
19 |
20 | class Chain(ControlStructure):
21 | """
22 | Represents a chain of validators, useful when using multiple validators
23 | with If control structure.
24 |
25 | :param validators:
26 | list of validator objects
27 | :param message:
28 | custom validation error message, if this message is set and some of the
29 | child validators raise a ValidationError, an exception is being raised
30 | again with this custom message.
31 | """
32 |
33 | def __init__(self, validators, message=None):
34 | self.validators = validators
35 | if message:
36 | self.message = message
37 |
38 | def __call__(self, form, field):
39 | for validator in self.validators:
40 | try:
41 | validator(form, field)
42 | except ValidationError as exc:
43 | self.reraise(exc)
44 | except StopValidation as exc:
45 | self.reraise(exc)
46 |
47 |
48 | class If(ControlStructure):
49 | """
50 | Conditional validator.
51 |
52 | :param condition: callable which takes two arguments form and field
53 | :param validator: encapsulated validator, this validator is validated
54 | only if given condition returns true
55 | :param message: custom message, which overrides child validator's
56 | validation error message
57 | """
58 |
59 | def __init__(self, condition, validator, message=None):
60 | self.condition = condition
61 | self.validator = validator
62 |
63 | if message:
64 | self.message = message
65 |
66 | def __call__(self, form, field):
67 | if self.condition(form, field):
68 | try:
69 | self.validator(form, field)
70 | except ValidationError as exc:
71 | self.reraise(exc)
72 | except StopValidation as exc:
73 | self.reraise(exc)
74 |
75 |
76 | class BaseDateTimeRange:
77 | def __init__(self, min=None, max=None, format="%H:%M", message=None):
78 | self.min = min
79 | self.max = max
80 | self.format = format
81 | self.message = message
82 |
83 | def __call__(self, form, field):
84 | data = field.data
85 | min_ = self.min() if callable(self.min) else self.min
86 | max_ = self.max() if callable(self.max) else self.max
87 | if (
88 | data is None
89 | or (min_ is not None and data < min_)
90 | or (max_ is not None and data > max_)
91 | ):
92 | if self.message is None:
93 | if max_ is None:
94 | self.message = field.gettext(self.greater_than_msg)
95 | elif min_ is None:
96 | self.message = field.gettext(self.less_than_msg)
97 | else:
98 | self.message = field.gettext(self.between_msg)
99 |
100 | raise ValidationError(
101 | self.message
102 | % dict(
103 | field_label=field.label,
104 | min=min_.strftime(self.format) if min_ else "",
105 | max=max_.strftime(self.format) if max_ else "",
106 | )
107 | )
108 |
109 |
110 | class TimeRange(BaseDateTimeRange):
111 | """
112 | Same as wtforms.validators.NumberRange but validates date.
113 |
114 | :param min:
115 | The minimum required value of the time. If not provided, minimum
116 | value will not be checked.
117 | :param max:
118 | The maximum value of the time. If not provided, maximum value
119 | will not be checked.
120 | :param message:
121 | Error message to raise in case of a validation error. Can be
122 | interpolated using `%(min)s` and `%(max)s` if desired. Useful defaults
123 | are provided depending on the existence of min and max.
124 | """
125 |
126 | greater_than_msg = "Time must be greater than %(min)s."
127 |
128 | less_than_msg = "Time must be less than %(max)s."
129 |
130 | between_msg = "Time must be between %(min)s and %(max)s."
131 |
132 | def __init__(self, min=None, max=None, format="%H:%M", message=None):
133 | super().__init__(min=min, max=max, format=format, message=message)
134 |
135 |
136 | class DateRange(BaseDateTimeRange):
137 | """
138 | Same as wtforms.validators.NumberRange but validates date.
139 |
140 | :param min:
141 | The minimum required value of the date. If not provided, minimum
142 | value will not be checked.
143 | :param max:
144 | The maximum value of the date. If not provided, maximum value
145 | will not be checked.
146 | :param message:
147 | Error message to raise in case of a validation error. Can be
148 | interpolated using `%(min)s` and `%(max)s` if desired. Useful defaults
149 | are provided depending on the existence of min and max.
150 | """
151 |
152 | greater_than_msg = "Date must be equal to or later than %(min)s."
153 |
154 | less_than_msg = "Date must be equal to or earlier than %(max)s."
155 |
156 | between_msg = "Date must be between %(min)s and %(max)s."
157 |
158 | def __init__(self, min=None, max=None, format="%Y-%m-%d", message=None):
159 | super().__init__(min=min, max=max, format=format, message=message)
160 |
161 |
162 | class Email:
163 | """
164 | Validates an email address.
165 | This validator is is stricter than the standard email
166 | validator included in WTForms.
167 |
168 | :param message:
169 | Error message to raise in case of a validation error.
170 | """
171 |
172 | def __init__(self, message=None):
173 | self.message = message
174 |
175 | def __call__(self, form, field):
176 | if not email(field.data):
177 | message = self.message
178 | if message is None:
179 | message = field.gettext("Invalid email address.")
180 | raise ValidationError(message)
181 |
--------------------------------------------------------------------------------
/CHANGES.rst:
--------------------------------------------------------------------------------
1 | Changelog
2 | ---------
3 |
4 | Here you can see the full list of changes between each WTForms-Components
5 | release.
6 |
7 |
8 | 0.11.0 (2024-11-15)
9 | ^^^^^^^^^^^^^^^^^^^
10 |
11 | - Drop ``whitelist`` argument from ``email`` validator (#75, pull request courtesy tvuotila)
12 | - Add support for Python 3.9, 3.10, 3.11 and 3.12.
13 | - Drop support for Python 3.8 or older.
14 | - Add support for WTForms 3.2. The minimum supported WTForms version is now 3.1.0.
15 | - Remove ``email_validator`` and ``six`` dependencies.
16 | - Remove ``_compat`` and ``utils`` modules.
17 | - Remove ``try..except`` from email validator import.
18 |
19 |
20 | 0.10.5 (2021-01-17)
21 | ^^^^^^^^^^^^^^^^^^^
22 |
23 | - Added WTForms 3.0 support (#71, pull request courtesy of jpic)
24 |
25 |
26 | 0.10.4 (2019-04-01)
27 | ^^^^^^^^^^^^^^^^^^^
28 |
29 | - Added JSONField (#65, pull request courtesy fedExpress)
30 |
31 |
32 | 0.10.3 (2017-03-03)
33 | ^^^^^^^^^^^^^^^^^^^
34 |
35 | - Made SelectWidget backwards compatible (#52, pull request courtesy tvuotila)
36 |
37 |
38 | 0.10.2 (2016-12-05)
39 | ^^^^^^^^^^^^^^^^^^^
40 |
41 | - Made read_only also add disabled attribute (#51, pull request courtesy quantus)
42 |
43 |
44 | 0.10.1 (2016-11-22)
45 | ^^^^^^^^^^^^^^^^^^^
46 |
47 | - Added seconds support for TimeField (#48, pull request courtesy tvuotila)
48 |
49 |
50 | 0.10.0 (2016-01-28)
51 | ^^^^^^^^^^^^^^^^^^^
52 |
53 | - Moved GroupedQuerySelectField to WTForms-Alchemy
54 | - Moved PhoneNumber to WTForms-Alchemy
55 | - Moved WeekdaysField to WTForms-Alchemy
56 | - Moved Unique validator to WTForms-Alchemy
57 | - Remove AJAXField dependency on SQLAlchemy-Utils
58 | - Added PyPy support
59 | - Fixed IntervalFields to work with intervals 0.6.0
60 | - Updated intervals requirement to 0.6.0
61 |
62 |
63 | 0.9.9 (2016-01-10)
64 | ^^^^^^^^^^^^^^^^^^
65 |
66 | - Added sane error handling with Unique validator when Form is missing ``_obj`` attribute (#34)
67 |
68 |
69 | 0.9.8 (2015-09-28)
70 | ^^^^^^^^^^^^^^^^^^
71 |
72 | - Added isort and flake8 checks to ensure all code is PEP8 compliant
73 | - Fields marked read only are no longer processed and populated
74 |
75 |
76 | 0.9.7 (2014-12-22)
77 | ^^^^^^^^^^^^^^^^^^
78 |
79 | - Added blank_value option for GroupedQuerySelectField
80 | - Add py34 to test matrix
81 | - Fixed midnight handling for TimeField
82 |
83 |
84 | 0.9.6 (2014-09-04)
85 | ^^^^^^^^^^^^^^^^^^
86 |
87 | - Fixed Unique validator session checking (#19).
88 |
89 |
90 | 0.9.5 (2014-07-31)
91 | ^^^^^^^^^^^^^^^^^^
92 |
93 | - Fixed base_form option in SplitDateTimeField getting lost if form is initialized more than once.
94 |
95 |
96 | 0.9.4 (2014-07-29)
97 | ^^^^^^^^^^^^^^^^^^
98 |
99 | - Added base_form option to SplitDateTimeField
100 |
101 |
102 | 0.9.3 (2014-05-15)
103 | ^^^^^^^^^^^^^^^^^^
104 |
105 | - Fixed InstrumentedAttribute support for Unique validator, issue #13
106 | - Removed CountryField (now part of WTForms-Alchemy)
107 |
108 |
109 | 0.9.2 (2014-03-26)
110 | ^^^^^^^^^^^^^^^^^^
111 |
112 | - Added WeekDaysField
113 |
114 |
115 | 0.9.1 (2014-01-27)
116 | ^^^^^^^^^^^^^^^^^^
117 |
118 | - Added new unique validator
119 |
120 |
121 | 0.9.0 (2014-01-14)
122 | ^^^^^^^^^^^^^^^^^^
123 |
124 | - Deprecated NumberRangeField
125 | - Added IntIntervalField, FloatIntervalField, DecimalIntervalField, DateIntervalField, DateTimeIntervalField
126 | - Updated SQLAlchemy-Utils dependency to version 0.23.0
127 |
128 |
129 | 0.8.3 (2014-01-05)
130 | ^^^^^^^^^^^^^^^^^^
131 |
132 | - Updated SQLAlchemy-Utils dependency to version 0.22.1
133 |
134 |
135 | 0.8.2 (2013-12-12)
136 | ^^^^^^^^^^^^^^^^^^
137 |
138 | - Add default validation message for Email validator
139 |
140 |
141 | 0.8.1 (2013-11-30)
142 | ^^^^^^^^^^^^^^^^^^
143 |
144 | - Fix import error with new versions of ``validators`` package.
145 | - Added initial WTForms 2.0 support
146 |
147 |
148 | 0.8.0 (2013-10-11)
149 | ^^^^^^^^^^^^^^^^^^
150 |
151 | - Added Python 3 support
152 |
153 |
154 | 0.7.1 (2013-09-07)
155 | ^^^^^^^^^^^^^^^^^^
156 |
157 | - Added AjaxField
158 |
159 |
160 | 0.7.0 (2013-08-09)
161 | ^^^^^^^^^^^^^^^^^^
162 |
163 | - Added GroupedQuerySelectField
164 |
165 |
166 | 0.6.6 (2013-07-31)
167 | ^^^^^^^^^^^^^^^^^^
168 |
169 | - Added HTML5 compatible basic parameters (disabled, required, autofocus and readonly) for all widgets
170 |
171 |
172 | 0.6.5 (2013-07-30)
173 | ^^^^^^^^^^^^^^^^^^
174 |
175 | - Added step rendering for NumberInput and RangeInput widgets
176 |
177 |
178 | 0.6.4 (2013-07-22)
179 | ^^^^^^^^^^^^^^^^^^
180 |
181 | - Packages colour and phonenumbers are now lazy imported
182 |
183 |
184 | 0.6.3 (2013-05-24)
185 | ^^^^^^^^^^^^^^^^^^
186 |
187 | - Added EmailField to main import
188 | - Added SearchField, IntegerSliderField, DecimalSliderField
189 |
190 |
191 | 0.6.2 (2013-05-24)
192 | ^^^^^^^^^^^^^^^^^^
193 |
194 | - Added TimeInput, URLInput, ColorInput and TelInput
195 | - Added TimeRange validator
196 |
197 |
198 | 0.6.1 (2013-05-23)
199 | ^^^^^^^^^^^^^^^^^^
200 |
201 | - Added required flag for NumberInput, DateInput, DateTimeInput
202 | and DateTimeLocalInput whenever associated field has a DataRequired validator.
203 |
204 |
205 | 0.6.0 (2013-05-23)
206 | ^^^^^^^^^^^^^^^^^^
207 |
208 | - IntegerField and DecimalField which create HTML5 compatible min and max
209 | attributes based on attached NumberRange validators
210 | - DateField, DateTimeField and DateTimeLocalField classes which create HTML5
211 | compatible min and max attributes based on attached NumberRange validators
212 |
213 |
214 | 0.5.5 (2013-05-07)
215 | ^^^^^^^^^^^^^^^^^^
216 |
217 | - Made TimeField use HTML5 TimeInput
218 | - Made PhoneNumberField use HTML5 TelInput
219 | - Made ColorField use HTML5 ColorInput
220 | - Updated WTForms dependency to 1.0.4
221 |
222 |
223 | 0.5.4 (2013-04-29)
224 | ^^^^^^^^^^^^^^^^^^
225 |
226 | - Added ColorField
227 |
228 |
229 | 0.5.3 (2013-04-26)
230 | ^^^^^^^^^^^^^^^^^^
231 |
232 | - Added read_only field marker function
233 |
234 |
235 | 0.5.2 (2013-04-12)
236 | ^^^^^^^^^^^^^^^^^^
237 |
238 | - Added tests for TimeField
239 | - Added TimeField to main module import
240 |
241 |
242 | 0.5.1 (2013-04-12)
243 | ^^^^^^^^^^^^^^^^^^
244 |
245 | - Added PassiveHiddenField
246 |
247 |
248 | 0.5.0 (2013-04-04)
249 | ^^^^^^^^^^^^^^^^^^
250 |
251 | - Added Email validator
252 | - Fixed empty string handling with NumberRange fields
253 |
254 |
255 | 0.4.6 (2013-03-29)
256 | ^^^^^^^^^^^^^^^^^^
257 |
258 | - Fixed Unique validator when using Form constructor obj parameter
259 | - Updated docs
260 |
261 |
262 | 0.4.5 (2013-03-27)
263 | ^^^^^^^^^^^^^^^^^^
264 |
265 | - Fixed PhoneNumberField field rendering when validation fails
266 |
267 |
268 | 0.4.4 (2013-03-26)
269 | ^^^^^^^^^^^^^^^^^^
270 |
271 | - Fixed NumberRangeField field rendering when validation fails
272 |
273 |
274 | 0.4.3 (2013-03-26)
275 | ^^^^^^^^^^^^^^^^^^
276 |
277 | - Fixed NumberRangeField widget rendering
278 |
279 |
280 | 0.4.2 (2013-03-26)
281 | ^^^^^^^^^^^^^^^^^^
282 |
283 | - Removed NumberRangeInput
284 |
285 |
286 | 0.4.1 (2013-03-26)
287 | ^^^^^^^^^^^^^^^^^^
288 |
289 | - Changed empty phone number to be passed as None
290 |
291 |
292 | 0.4.0 (2013-03-26)
293 | ^^^^^^^^^^^^^^^^^^
294 |
295 | - Added NumberRangeField
296 |
297 |
298 | 0.3.0 (2013-03-26)
299 | ^^^^^^^^^^^^^^^^^^
300 |
301 | - Changed to use SQLAlchemy-Utils PhoneNumber class
302 |
303 |
304 | 0.2.0 (2013-03-20)
305 | ^^^^^^^^^^^^^^^^^^
306 |
307 | - Added PhoneNumberField
308 |
309 |
310 | 0.1.0 (2013-03-15)
311 | ^^^^^^^^^^^^^^^^^^
312 |
313 | - Initial public release
314 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | WTForms-Components
2 | ==================
3 |
4 | WTForms-Components provides various additional fields, validators and widgets
5 | for WTForms.
6 |
7 | Fields
8 | ======
9 |
10 |
11 | WTForms derived HTML5 Fields
12 | -----------------------------
13 |
14 | WTForms-Components provides enhanced versions of WTForms HTML5 fields. These fields support
15 | HTML5 compatible min and max validators. WTForms-Components is smart enough to automatically
16 | attach HTML5 min and max validators based on field's NumberRange and DateRange validators.
17 |
18 | Example:
19 | ::
20 |
21 |
22 | from wtforms import Form
23 | from wtforms_components import DateTimeField
24 | from werkzeug.datastructures import MultiDict
25 |
26 |
27 | class TestForm(Form):
28 | test_field = DateTimeField(
29 | 'Date',
30 | validators=[DateRange(
31 | min=datetime(2000, 1, 1),
32 | max=datetime(2000, 10, 10)
33 | )]
34 | )
35 |
36 |
37 | form = TestForm(MultiDict(test_field='2000-2-2'))
38 | form.test_field
39 |
40 | # '
41 |
42 |
43 | Same applies to IntegerField:
44 | ::
45 |
46 |
47 | from wtforms import Form
48 | from wtforms_components import IntegerField
49 | from werkzeug.datastructures import MultiDict
50 |
51 |
52 | class TestForm(Form):
53 | test_field = IntegerField(
54 | 'Date',
55 | validators=[NumberRange(
56 | min=1,
57 | max=4
58 | )]
59 | )
60 |
61 |
62 | form = TestForm(MultiDict(test_field='3'))
63 | form.test_field
64 |
65 | # '
66 |
67 |
68 |
69 | SelectField & SelectMultipleField
70 | ---------------------------------
71 |
72 | WTForms-Components provides enhanced versions of WTForms SelectFields. Both WTForms-Components
73 | SelectField and SelectMultipleField support the following enhancements:
74 |
75 | - Ability to generate `optgroup`_ elements.
76 | - ``choices`` can be a callable, which allows for dynamic choices. With the plain version of WTForms this has to be added manually, after instantiation of the form.
77 |
78 | .. _`optgroup`:
79 | https://developer.mozilla.org/en-US/docs/Web/HTML/Element/optgroup
80 |
81 | PhoneNumberField
82 | ----------------
83 |
84 | Older versions of WTForms-Components had a PhoneNumberField. As of version 0.10.0 this field has now been moved to `WTForms-Alchemy`_.
85 |
86 | .. _WTForms-Alchemy:
87 | https://github.com/kvesteri/wtforms-alchemy
88 |
89 |
90 | ColorField
91 | ----------
92 |
93 | ColorField is a string field representing a Color object from `colour`_ package.
94 |
95 | .. _colour:
96 | https://github.com/vaab/colour
97 |
98 | Example:
99 | ::
100 |
101 |
102 | from wtforms import Form
103 | from wtforms_components import ColorField
104 |
105 | class DocumentForm(Form):
106 | background_color = ColorField()
107 |
108 |
109 |
110 | NumberRangeField
111 | ----------------
112 |
113 | NumberRangeField is a string field representing a NumberRange object from
114 | `SQLAlchemy-Utils`_.
115 |
116 | .. _SQLAlchemy-Utils:
117 | https://github.com/kvesteri/sqlalchemy-utils
118 |
119 | Example:
120 | ::
121 |
122 |
123 | from wtforms import Form
124 | from wtforms_components import NumberRangeField
125 |
126 | class EventForm(Form):
127 | estimated_participants = NumberRangeField('Estimated participants')
128 |
129 |
130 |
131 | PassiveHiddenField
132 | ------------------
133 |
134 | PassiveHiddenField acts just like normal wtforms.fields.HiddenField except it
135 | doesn't populate object values with populate_obj function.
136 |
137 | Example:
138 | ::
139 |
140 |
141 | from wtforms import Form, TextField
142 | from wtforms_components import PassiveHiddenField
143 |
144 | class EventForm(Form):
145 | id = PassiveHiddenField()
146 | name = TextField('Name')
147 |
148 |
149 | TimeField
150 | ---------
151 |
152 | TimeField is a string field which stores a `datetime.time` matching a format.
153 | ::
154 |
155 |
156 | from wtforms import Form, DateField
157 | from wtforms_components import TimeField
158 |
159 | class EventForm(Form):
160 | start_date = DateField('Start date')
161 | start_time = TimeField('Start time')
162 |
163 |
164 | Read-only fields
165 | ----------------
166 |
167 | WTForms-Components provides a convenient function for making fields read-only.
168 |
169 | In the following example we define a form where name field is defined as read-only.
170 | ::
171 |
172 |
173 | from wtforms import Form, DateField, TextField
174 | from wtforms_components import TimeField, read_only
175 |
176 | class EventForm(Form):
177 | name = TextField('Name')
178 | start_date = DateField('Start date')
179 | start_time = TimeField('Start time')
180 |
181 | def __init__(self, *args, **kwargs):
182 | super(EventForm, self).__init__(*args, **kwargs)
183 | read_only(self.name)
184 |
185 |
186 | Validators
187 | ==========
188 |
189 | DateRange validator
190 | -------------------
191 |
192 | The DateRange validator is essentially the same as wtforms.validators.NumberRange validator but validates
193 | dates.
194 |
195 | In the following example we define a start_time and a start_date field, which do not accept dates in the past. ::
196 |
197 | from datetime import datetime, date
198 | from wtforms import Form
199 | from wtforms.fields import DateField
200 | from wtforms_components import DateRange
201 |
202 | class EventForm(Form):
203 | start_time = DateField(
204 | validators=[DateRange(min=datetime.now())]
205 | )
206 | start_date = DateField(
207 | validators=[DateRange(min=date.today())]
208 | )
209 |
210 |
211 | Email validator
212 | ---------------
213 |
214 | Validates an email address. This validator is based on Django's email validator and is stricter than the standard email validator included in WTForms.
215 |
216 | Example:
217 | ::
218 |
219 |
220 | from wtforms import Form
221 | from wtforms.fields import TextField
222 | from wtforms_components import Email
223 |
224 | class UserForm(Form):
225 | email = TextField(
226 | validators=[Email()]
227 | )
228 |
229 |
230 | If validator
231 | ------------
232 |
233 | The If validator provides means for having conditional validations. In the following example we only
234 | validate field email if field user_id is provided.
235 | ::
236 |
237 |
238 | from wtforms import Form
239 | from wtforms.fields import IntegerField, TextField
240 | from wtforms_components import If
241 |
242 | class SomeForm(Form):
243 | user_id = IntegerField()
244 | email = TextField(validators=[
245 | If(lambda form, field: form.user_id.data, Email())
246 | ])
247 |
248 |
249 | Chain validator
250 | ---------------
251 |
252 |
253 | Chain validator chains validators together. Chain validator can be combined with If validator
254 | to provide nested conditional validations.
255 | ::
256 |
257 |
258 | from wtforms import Form
259 | from wtforms.fields import IntegerField, TextField
260 | from wtforms_components import If
261 |
262 | class SomeForm(Form):
263 | user_id = IntegerField()
264 | email = TextField(validators=[
265 | If(
266 | lambda form, field: form.user_id.data,
267 | Chain(DataRequired(), Email())
268 | )
269 | ])
270 |
271 |
272 | Unique Validator
273 | ----------------
274 |
275 | Unique validator provides convenient way for checking the unicity of given field in database.
276 |
277 | As of WTForms-Components version 0.10.0 the Unique validator has been moved to WTForms-Alchemy due to its SQLAlchemy dependency.
278 |
--------------------------------------------------------------------------------
/wtforms_components/widgets.py:
--------------------------------------------------------------------------------
1 | from copy import copy
2 |
3 | from markupsafe import Markup, escape
4 | from wtforms.validators import DataRequired, NumberRange
5 | from wtforms.widgets import Input, html_params
6 | from wtforms.widgets import Select as _Select
7 |
8 | from .validators import DateRange, TimeRange
9 |
10 |
11 | def min_max(field, validator_class):
12 | """
13 | Returns maximum minimum and minimum maximum value for given validator class
14 | of given field.
15 |
16 | :param field: WTForms Field object
17 | :param validator_class: WTForms Validator class
18 |
19 | Example::
20 |
21 |
22 | class MyForm(Form):
23 | some_integer_field = IntegerField(
24 | validators=[Length(min=3, max=6), Length(min=4, max=7)]
25 | )
26 |
27 |
28 | form = MyForm()
29 |
30 | min_max(form.some_integer_field, Length)
31 | # {'min': 4, 'max': 6}
32 | """
33 | min_values = []
34 | max_values = []
35 | for validator in field.validators:
36 | if isinstance(validator, validator_class):
37 | if validator.min is not None:
38 | min_values.append(validator.min)
39 | if validator.max is not None:
40 | max_values.append(validator.max)
41 |
42 | data = {}
43 | if min_values:
44 | data["min"] = max(min_values)
45 | if max_values:
46 | data["max"] = min(max_values)
47 | return data
48 |
49 |
50 | def has_validator(field, validator_class):
51 | """
52 | Returns whether or not given field has an instance of given validator class
53 | in the validators property.
54 |
55 | :param field: WTForms Field object
56 | :param validator_class: WTForms Validator class
57 | """
58 | return any(
59 | [isinstance(validator, validator_class) for validator in field.validators]
60 | )
61 |
62 |
63 | class HTML5Input(Input):
64 | validation_attrs = ["required", "disabled"]
65 |
66 | def __init__(self, **kwargs):
67 | self.options = kwargs
68 |
69 | def __call__(self, field, **kwargs):
70 | if has_validator(field, DataRequired):
71 | kwargs.setdefault("required", True)
72 |
73 | for key, value in self.range_validators(field).items():
74 | kwargs.setdefault(key, value)
75 |
76 | if hasattr(field, "widget_options"):
77 | for key, value in self.field.widget_options:
78 | kwargs.setdefault(key, value)
79 |
80 | options_copy = copy(self.options)
81 | options_copy.update(kwargs)
82 | return super().__call__(field, **options_copy)
83 |
84 | def range_validators(self, field):
85 | return {}
86 |
87 |
88 | class BaseDateTimeInput(HTML5Input):
89 | """
90 | Base class for TimeInput, DateTimeLocalInput, DateTimeInput and
91 | DateInput widgets
92 | """
93 |
94 | range_validator_class = DateRange
95 |
96 | def range_validators(self, field):
97 | data = min_max(field, self.range_validator_class)
98 | if "min" in data:
99 | data["min"] = data["min"].strftime(self.format)
100 | if "max" in data:
101 | data["max"] = data["max"].strftime(self.format)
102 | return data
103 |
104 |
105 | class TextInput(HTML5Input):
106 | input_type = "text"
107 |
108 |
109 | class SearchInput(HTML5Input):
110 | """
111 | Renders an input with type "search".
112 | """
113 |
114 | input_type = "search"
115 |
116 |
117 | class MonthInput(HTML5Input):
118 | """
119 | Renders an input with type "month".
120 | """
121 |
122 | input_type = "month"
123 |
124 |
125 | class WeekInput(HTML5Input):
126 | """
127 | Renders an input with type "week".
128 | """
129 |
130 | input_type = "week"
131 |
132 |
133 | class RangeInput(HTML5Input):
134 | """
135 | Renders an input with type "range".
136 | """
137 |
138 | input_type = "range"
139 |
140 |
141 | class URLInput(HTML5Input):
142 | """
143 | Renders an input with type "url".
144 | """
145 |
146 | input_type = "url"
147 |
148 |
149 | class ColorInput(HTML5Input):
150 | """
151 | Renders an input with type "color".
152 | """
153 |
154 | input_type = "color"
155 |
156 |
157 | class TelInput(HTML5Input):
158 | """
159 | Renders an input with type "tel".
160 | """
161 |
162 | input_type = "tel"
163 |
164 |
165 | class EmailInput(HTML5Input):
166 | """
167 | Renders an input with type "email".
168 | """
169 |
170 | input_type = "email"
171 |
172 |
173 | class TimeInput(BaseDateTimeInput):
174 | """
175 | Renders an input with type "time".
176 |
177 | Adds min and max html5 field parameters based on field's TimeRange
178 | validator.
179 | """
180 |
181 | input_type = "time"
182 | range_validator_class = TimeRange
183 | format = "%H:%M:%S"
184 |
185 |
186 | class DateTimeLocalInput(BaseDateTimeInput):
187 | """
188 | Renders an input with type "datetime-local".
189 |
190 | Adds min and max html5 field parameters based on field's DateRange
191 | validator.
192 | """
193 |
194 | input_type = "datetime-local"
195 | format = "%Y-%m-%dT%H:%M:%S"
196 |
197 |
198 | class DateTimeInput(BaseDateTimeInput):
199 | """
200 | Renders an input with type "datetime".
201 |
202 | Adds min and max html5 field parameters based on field's DateRange
203 | validator.
204 | """
205 |
206 | input_type = "datetime"
207 | format = "%Y-%m-%dT%H:%M:%SZ"
208 |
209 |
210 | class DateInput(BaseDateTimeInput):
211 | """
212 | Renders an input with type "date".
213 |
214 | Adds min and max html5 field parameters based on field's DateRange
215 | validator.
216 | """
217 |
218 | input_type = "date"
219 | format = "%Y-%m-%d"
220 |
221 |
222 | class NumberInput(HTML5Input):
223 | """
224 | Renders an input with type "number".
225 |
226 | Adds min and max html5 field parameters based on field's NumberRange
227 | validator.
228 | """
229 |
230 | input_type = "number"
231 | range_validator_class = NumberRange
232 |
233 | def range_validators(self, field):
234 | return min_max(field, self.range_validator_class)
235 |
236 |
237 | class ReadOnlyWidgetProxy:
238 | def __init__(self, widget):
239 | self.widget = widget
240 |
241 | def __getattr__(self, name):
242 | return getattr(self.widget, name)
243 |
244 | def __call__(self, field, **kwargs):
245 | kwargs.setdefault("readonly", True)
246 | # Some html elements also need disabled attribute to achieve the
247 | # expected UI behaviour.
248 | kwargs.setdefault("disabled", True)
249 | return self.widget(field, **kwargs)
250 |
251 |
252 | class SelectWidget(_Select):
253 | """
254 | Add support of choices with ``optgroup`` to the ``Select`` widget.
255 | """
256 |
257 | @classmethod
258 | def render_optgroup(cls, value, label, mixed):
259 | children = []
260 |
261 | for item_value, item_label in label:
262 | item_html = cls.render_option(item_value, item_label, mixed)
263 | children.append(item_html)
264 |
265 | html = ''
266 | data = (escape(str(value)), "\n".join(children))
267 | return Markup(html % data)
268 |
269 | @classmethod
270 | def render_option(cls, value, label, mixed):
271 | """
272 | Render option as HTML tag, but not forget to wrap options into
273 | ``optgroup`` tag if ``label`` var is ``list`` or ``tuple``.
274 | """
275 | if isinstance(label, (list, tuple)):
276 | return cls.render_optgroup(value, label, mixed)
277 |
278 | try:
279 | coerce_func, data = mixed
280 | except TypeError:
281 | selected = mixed
282 | else:
283 | if isinstance(data, list) or isinstance(data, tuple):
284 | selected = coerce_func(value) in data
285 | else:
286 | selected = coerce_func(value) == data
287 |
288 | options = {"value": value}
289 |
290 | if selected:
291 | options["selected"] = True
292 |
293 | html = ""
294 | data = (html_params(**options), escape(str(label)))
295 |
296 | return Markup(html % data)
297 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | #
2 | # WTForms-Components documentation build configuration file, created by
3 | # sphinx-quickstart on Fri Mar 15 10:35:33 2013.
4 | #
5 | # This file is execfile()d with the current directory set to its containing dir.
6 | #
7 | # Note that not all possible configuration values are present in this
8 | # autogenerated file.
9 | #
10 | # All configuration values have a default; values that are commented out
11 | # serve to show the default.
12 |
13 | import pathlib
14 | import re
15 |
16 |
17 | def get_version():
18 | path = pathlib.Path(__file__).parent.parent / "wtforms_components" / "__init__.py"
19 | contents = path.read_text()
20 | pattern = r'^__version__ = "(.*?)"$'
21 | return re.search(pattern, contents, re.MULTILINE).group(1)
22 |
23 |
24 | # If extensions (or modules to document with autodoc) are in another directory,
25 | # add these directories to sys.path here. If the directory is relative to the
26 | # documentation root, use os.path.abspath to make it absolute, like shown here.
27 | # sys.path.insert(0, os.path.abspath('.'))
28 |
29 | # -- General configuration -----------------------------------------------------
30 |
31 | # If your documentation needs a minimal Sphinx version, state it here.
32 | # needs_sphinx = '1.0'
33 |
34 | # Add any Sphinx extension module names here, as strings. They can be extensions
35 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
36 | extensions = [
37 | "sphinx.ext.autodoc",
38 | "sphinx.ext.doctest",
39 | "sphinx.ext.intersphinx",
40 | "sphinx.ext.todo",
41 | "sphinx.ext.coverage",
42 | "sphinx.ext.ifconfig",
43 | "sphinx.ext.viewcode",
44 | ]
45 |
46 | # Add any paths that contain templates here, relative to this directory.
47 | templates_path = ["_templates"]
48 |
49 | # The suffix of source filenames.
50 | source_suffix = ".rst"
51 |
52 | # The encoding of source files.
53 | # source_encoding = 'utf-8-sig'
54 |
55 | # The master toctree document.
56 | master_doc = "index"
57 |
58 | # General information about the project.
59 | project = "WTForms-Components"
60 | copyright = "2013, Konsta Vesterinen, Janne Vanhala, Vesa Uimonen"
61 |
62 | # The version info for the project you're documenting, acts as replacement for
63 | # |version| and |release|, also used in various other places throughout the
64 | # built documents.
65 | #
66 | # The short X.Y version.
67 | version = get_version()
68 | # The full version, including alpha/beta/rc tags.
69 | release = version
70 |
71 | # The language for content autogenerated by Sphinx. Refer to documentation
72 | # for a list of supported languages.
73 | # language = None
74 |
75 | # There are two options for replacing |today|: either, you set today to some
76 | # non-false value, then it is used:
77 | # today = ''
78 | # Else, today_fmt is used as the format for a strftime call.
79 | # today_fmt = '%B %d, %Y'
80 |
81 | # List of patterns, relative to source directory, that match files and
82 | # directories to ignore when looking for source files.
83 | exclude_patterns = ["_build"]
84 |
85 | # The reST default role (used for this markup: `text`) to use for all documents.
86 | # default_role = None
87 |
88 | # If true, '()' will be appended to :func: etc. cross-reference text.
89 | # add_function_parentheses = True
90 |
91 | # If true, the current module name will be prepended to all description
92 | # unit titles (such as .. function::).
93 | # add_module_names = True
94 |
95 | # If true, sectionauthor and moduleauthor directives will be shown in the
96 | # output. They are ignored by default.
97 | # show_authors = False
98 |
99 | # The name of the Pygments (syntax highlighting) style to use.
100 | pygments_style = "sphinx"
101 |
102 | # A list of ignored prefixes for module index sorting.
103 | # modindex_common_prefix = []
104 |
105 |
106 | # -- Options for HTML output ---------------------------------------------------
107 |
108 | # The theme to use for HTML and HTML Help pages. See the documentation for
109 | # a list of builtin themes.
110 | html_theme = "furo"
111 |
112 | # Theme options are theme-specific and customize the look and feel of a theme
113 | # further. For a list of options available for each theme, see the
114 | # documentation.
115 | # html_theme_options = {}
116 |
117 | # Add any paths that contain custom themes here, relative to this directory.
118 | # html_theme_path = []
119 |
120 | # The name for this set of Sphinx documents. If None, it defaults to
121 | # " v documentation".
122 | # html_title = None
123 |
124 | # A shorter title for the navigation bar. Default is the same as html_title.
125 | # html_short_title = None
126 |
127 | # The name of an image file (relative to this directory) to place at the top
128 | # of the sidebar.
129 | # html_logo = None
130 |
131 | # The name of an image file (within the static path) to use as favicon of the
132 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
133 | # pixels large.
134 | # html_favicon = None
135 |
136 | # Add any paths that contain custom static files (such as style sheets) here,
137 | # relative to this directory. They are copied after the builtin static files,
138 | # so a file named "default.css" will overwrite the builtin "default.css".
139 | # html_static_path = ["_static"]
140 |
141 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
142 | # using the given strftime format.
143 | # html_last_updated_fmt = '%b %d, %Y'
144 |
145 | # If true, SmartyPants will be used to convert quotes and dashes to
146 | # typographically correct entities.
147 | # html_use_smartypants = True
148 |
149 | # Custom sidebar templates, maps document names to template names.
150 | # html_sidebars = {}
151 |
152 | # Additional templates that should be rendered to pages, maps page names to
153 | # template names.
154 | # html_additional_pages = {}
155 |
156 | # If false, no module index is generated.
157 | # html_domain_indices = True
158 |
159 | # If false, no index is generated.
160 | # html_use_index = True
161 |
162 | # If true, the index is split into individual pages for each letter.
163 | # html_split_index = False
164 |
165 | # If true, links to the reST sources are added to the pages.
166 | # html_show_sourcelink = True
167 |
168 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
169 | # html_show_sphinx = True
170 |
171 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
172 | # html_show_copyright = True
173 |
174 | # If true, an OpenSearch description file will be output, and all pages will
175 | # contain a tag referring to it. The value of this option must be the
176 | # base URL from which the finished HTML is served.
177 | # html_use_opensearch = ''
178 |
179 | # This is the file name suffix for HTML files (e.g. ".xhtml").
180 | # html_file_suffix = None
181 |
182 | # Output file base name for HTML help builder.
183 | htmlhelp_basename = "WTForms-Componentsdoc"
184 |
185 |
186 | # -- Options for LaTeX output --------------------------------------------------
187 |
188 | latex_elements = {
189 | # The paper size ('letterpaper' or 'a4paper').
190 | #'papersize': 'letterpaper',
191 | # The font size ('10pt', '11pt' or '12pt').
192 | #'pointsize': '10pt',
193 | # Additional stuff for the LaTeX preamble.
194 | #'preamble': '',
195 | }
196 |
197 | # Grouping the document tree into LaTeX files. List of tuples
198 | # (source start file, target name, title, author, documentclass [howto/manual]).
199 | latex_documents = [
200 | (
201 | "index",
202 | "WTForms-Components.tex",
203 | "WTForms-Components Documentation",
204 | "Konsta Vesterinen, Janne Vanhala, Vesa Uimonen",
205 | "manual",
206 | ),
207 | ]
208 |
209 | # The name of an image file (relative to this directory) to place at the top of
210 | # the title page.
211 | # latex_logo = None
212 |
213 | # For "manual" documents, if this is true, then toplevel headings are parts,
214 | # not chapters.
215 | # latex_use_parts = False
216 |
217 | # If true, show page references after internal links.
218 | # latex_show_pagerefs = False
219 |
220 | # If true, show URL addresses after external links.
221 | # latex_show_urls = False
222 |
223 | # Documents to append as an appendix to all manuals.
224 | # latex_appendices = []
225 |
226 | # If false, no module index is generated.
227 | # latex_domain_indices = True
228 |
229 |
230 | # -- Options for manual page output --------------------------------------------
231 |
232 | # One entry per manual page. List of tuples
233 | # (source start file, name, description, authors, manual section).
234 | man_pages = [
235 | (
236 | "index",
237 | "wtforms-components",
238 | "WTForms-Components Documentation",
239 | ["Konsta Vesterinen, Janne Vanhala, Vesa Uimonen"],
240 | 1,
241 | )
242 | ]
243 |
244 | # If true, show URL addresses after external links.
245 | # man_show_urls = False
246 |
247 |
248 | # -- Options for Texinfo output ------------------------------------------------
249 |
250 | # Grouping the document tree into Texinfo files. List of tuples
251 | # (source start file, target name, title, author,
252 | # dir menu entry, description, category)
253 | texinfo_documents = [
254 | (
255 | "index",
256 | "WTForms-Components",
257 | "WTForms-Components Documentation",
258 | "Konsta Vesterinen, Janne Vanhala, Vesa Uimonen",
259 | "WTForms-Components",
260 | "One line description of project.",
261 | "Miscellaneous",
262 | ),
263 | ]
264 |
265 | # Documents to append as an appendix to all manuals.
266 | # texinfo_appendices = []
267 |
268 | # If false, no module index is generated.
269 | # texinfo_domain_indices = True
270 |
271 | # How to display URL addresses: 'footnote', 'no', or 'inline'.
272 | # texinfo_show_urls = 'footnote'
273 |
274 |
275 | # Example configuration for intersphinx: refer to the Python standard library.
276 | intersphinx_mapping = {"python": ("http://docs.python.org/", None)}
277 |
--------------------------------------------------------------------------------