Skip to content

Commit 7725919

Browse files
committed
db_fields: Add String Related forms fields options and logic + related tests
1 parent d81b27a commit 7725919

File tree

2 files changed

+262
-36
lines changed

2 files changed

+262
-36
lines changed

flask_mongoengine/db_fields.py

Lines changed: 153 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,30 @@
6161
wtf_validators_ = None
6262

6363

64+
@wtf_required
65+
def _setup_strings_common_validators(options: dict, obj: fields.StringField) -> dict:
66+
"""
67+
Extend :attr:`base_options` with common validators for string types.
68+
69+
:param options: dict, usually from :class:`WtfFieldMixin.wtf_generated_options`
70+
:param obj: Any :class:`mongoengine.fields.StringField` subclass instance.
71+
"""
72+
assert isinstance(obj, fields.StringField), "Improperly configured"
73+
if obj.min_length or obj.max_length:
74+
options["validators"].insert(
75+
0,
76+
wtf_validators_.Length(
77+
min=obj.min_length or -1,
78+
max=obj.max_length or -1,
79+
),
80+
)
81+
82+
if obj.regex:
83+
options["validators"].insert(0, wtf_validators_.Regexp(regex=obj.regex))
84+
85+
return options
86+
87+
6488
class WtfFieldMixin:
6589
"""
6690
Extension wrapper class for mongoengine BaseField.
@@ -469,24 +493,21 @@ class EmailField(WtfFieldMixin, fields.EmailField):
469493
All arguments should be passed as keyword arguments, to exclude unexpected behaviour.
470494
471495
.. versionchanged:: 2.0.0
472-
Default field output changed from :class:`.NoneStringField` to
473-
:class:`wtforms.fields.EmailField`
496+
Default form field output changed from :class:`.NoneStringField` to
497+
:class:`flask_mongoengine.wtf.fields.MongoEmailField`
474498
"""
475499

476-
DEFAULT_WTF_FIELD = wtf_fields.EmailField if wtf_fields else None
500+
DEFAULT_WTF_FIELD = custom_fields.MongoEmailField if custom_fields else None
477501

478-
def to_wtf_field(
479-
self,
480-
*,
481-
model: Optional[Type] = None,
482-
field_kwargs: Optional[dict] = None,
483-
):
484-
"""
485-
Protection from execution of :func:`to_wtf_field` in form generation.
502+
@property
503+
@wtf_required
504+
def wtf_generated_options(self) -> dict:
505+
"""Extend form validators with :class:`wtforms.validators.Email`"""
506+
options = super().wtf_generated_options
507+
options = _setup_strings_common_validators(options, self)
508+
options["validators"].insert(0, wtf_validators_.Email())
486509

487-
:raises NotImplementedError: Field converter to WTForm Field not implemented.
488-
"""
489-
raise NotImplementedError("Field converter to WTForm Field not implemented.")
510+
return options
490511

491512

492513
class EmbeddedDocumentField(WtfFieldMixin, fields.EmbeddedDocumentField):
@@ -1085,22 +1106,112 @@ class StringField(WtfFieldMixin, fields.StringField):
10851106
10861107
For full list of arguments and keyword arguments, look parent field docs.
10871108
All arguments should be passed as keyword arguments, to exclude unexpected behaviour.
1109+
1110+
.. versionchanged:: 2.0.0
1111+
Default form field output changed from :class:`.NoneStringField` to
1112+
:class:`flask_mongoengine.wtf.fields.MongoTextAreaField`
10881113
"""
10891114

1090-
DEFAULT_WTF_FIELD = wtf_fields.TextAreaField if wtf_fields else None
1115+
DEFAULT_WTF_FIELD = custom_fields.MongoTextAreaField if custom_fields else None
10911116

1092-
def to_wtf_field(
1117+
def __init__(
10931118
self,
10941119
*,
1095-
model: Optional[Type] = None,
1096-
field_kwargs: Optional[dict] = None,
1120+
password: bool = False,
1121+
textarea: bool = False,
1122+
validators: Optional[Union[List, Callable]] = None,
1123+
filters: Optional[Union[List, Callable]] = None,
1124+
wtf_field_class: Optional[Type] = None,
1125+
wtf_filters: Optional[Union[List, Callable]] = None,
1126+
wtf_validators: Optional[Union[List, Callable]] = None,
1127+
wtf_choices_coerce: Optional[Callable] = None,
1128+
wtf_options: Optional[dict] = None,
1129+
**kwargs,
10971130
):
10981131
"""
1099-
Protection from execution of :func:`to_wtf_field` in form generation.
1132+
Extended :func:`__init__` method for mongoengine db field with WTForms options.
11001133
1101-
:raises NotImplementedError: Field converter to WTForm Field not implemented.
1134+
:param password:
1135+
DEPRECATED: Force to use :class:`~.MongoPasswordField` for field generation.
1136+
In case of :attr:`password` and :attr:`wtf_field_class` both set, then
1137+
:attr:`wtf_field_class` will be used.
1138+
:param textarea:
1139+
DEPRECATED: Force to use :class:`~.MongoTextAreaField` for field generation.
1140+
In case of :attr:`textarea` and :attr:`wtf_field_class` both set, then
1141+
:attr:`wtf_field_class` will be used.
1142+
:param filters: DEPRECATED: wtf form field filters.
1143+
:param validators: DEPRECATED: wtf form field validators.
1144+
:param wtf_field_class: Any subclass of :class:`wtforms.forms.core.Field` that
1145+
can be used for form field generation. Takes precedence over
1146+
:attr:`DEFAULT_WTF_FIELD` and :attr:`DEFAULT_WTF_CHOICES_FIELD`
1147+
:param wtf_filters: wtf form field filters.
1148+
:param wtf_validators: wtf form field validators.
1149+
:param wtf_choices_coerce: Callable function to replace
1150+
:attr:`DEFAULT_WTF_CHOICES_COERCE` for choices fields.
1151+
:param wtf_options: Dictionary with WTForm Field settings.
1152+
Applied last, takes precedence over any generated field options.
1153+
:param kwargs: keyword arguments silently bypassed to normal mongoengine fields
11021154
"""
1103-
raise NotImplementedError("Field converter to WTForm Field not implemented.")
1155+
if password:
1156+
if textarea:
1157+
raise ValueError("Password field cannot use TextAreaField class.")
1158+
1159+
warnings.warn(
1160+
(
1161+
"Passing 'password' keyword argument to field definition is "
1162+
"deprecated and will be removed in version 3.0.0. "
1163+
"Please use 'wtf_field_class' parameter to specify correct field "
1164+
"class. If both values set, 'wtf_field_class' is used."
1165+
),
1166+
DeprecationWarning,
1167+
stacklevel=2,
1168+
)
1169+
wtf_field_class = wtf_field_class or custom_fields.MongoPasswordField
1170+
1171+
if textarea:
1172+
warnings.warn(
1173+
(
1174+
"Passing 'textarea' keyword argument to field definition is "
1175+
"deprecated and will be removed in version 3.0.0. "
1176+
"Please use 'wtf_field_class' parameter to specify correct field "
1177+
"class. If both values set, 'wtf_field_class' is used."
1178+
),
1179+
DeprecationWarning,
1180+
stacklevel=2,
1181+
)
1182+
wtf_field_class = wtf_field_class or custom_fields.MongoTextAreaField
1183+
1184+
super().__init__(
1185+
validators=validators,
1186+
filters=filters,
1187+
wtf_field_class=wtf_field_class,
1188+
wtf_filters=wtf_filters,
1189+
wtf_validators=wtf_validators,
1190+
wtf_choices_coerce=wtf_choices_coerce,
1191+
wtf_options=wtf_options,
1192+
**kwargs,
1193+
)
1194+
1195+
@property
1196+
def wtf_field_class(self) -> Type:
1197+
"""Parent class overwrite with support of class adjustment by field size."""
1198+
if self._wtf_field_class:
1199+
return self._wtf_field_class
1200+
if self.max_length or self.min_length:
1201+
return custom_fields.MongoStringField
1202+
return super().wtf_field_class
1203+
1204+
@property
1205+
@wtf_required
1206+
def wtf_generated_options(self) -> dict:
1207+
"""
1208+
Extend form validators with :class:`wtforms.validators.Regexp` and
1209+
:class:`wtforms.validators.Length`.
1210+
"""
1211+
options = super().wtf_generated_options
1212+
options = _setup_strings_common_validators(options, self)
1213+
1214+
return options
11041215

11051216

11061217
class URLField(WtfFieldMixin, fields.URLField):
@@ -1109,22 +1220,31 @@ class URLField(WtfFieldMixin, fields.URLField):
11091220
11101221
For full list of arguments and keyword arguments, look parent field docs.
11111222
All arguments should be passed as keyword arguments, to exclude unexpected behaviour.
1223+
1224+
.. versionchanged:: 2.0.0
1225+
Default form field output changed from :class:`.NoneStringField` to
1226+
:class:`~flask_mongoengine.wtf.fields.MongoURLField`
1227+
1228+
.. versionchanged:: 2.0.0
1229+
Now appends :class:`~wtforms.validators.Regexp` and use regexp provided to
1230+
__init__ :attr:`url_regex`, instead of using non-configurable regexp from
1231+
:class:`~wtforms.validators.URL`. This includes configuration conflicts, between
1232+
modules.
11121233
"""
11131234

1114-
DEFAULT_WTF_FIELD = custom_fields.NoneStringField if custom_fields else None
1235+
DEFAULT_WTF_FIELD = custom_fields.MongoURLField if custom_fields else None
11151236

1116-
def to_wtf_field(
1117-
self,
1118-
*,
1119-
model: Optional[Type] = None,
1120-
field_kwargs: Optional[dict] = None,
1121-
):
1122-
"""
1123-
Protection from execution of :func:`to_wtf_field` in form generation.
1237+
@property
1238+
@wtf_required
1239+
def wtf_generated_options(self) -> dict:
1240+
"""Extend form validators with :class:`wtforms.validators.Regexp`"""
1241+
options = super().wtf_generated_options
1242+
options = _setup_strings_common_validators(options, self)
1243+
options["validators"].insert(
1244+
0, wtf_validators_.Regexp(regex=self.url_regex, message="Invalid URL.")
1245+
)
11241246

1125-
:raises NotImplementedError: Field converter to WTForm Field not implemented.
1126-
"""
1127-
raise NotImplementedError("Field converter to WTForm Field not implemented.")
1247+
return options
11281248

11291249

11301250
class UUIDField(WtfFieldMixin, fields.UUIDField):

tests/test_db_fields.py

Lines changed: 109 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Tests for db_fields overwrite and WTForms integration."""
2+
import re
23
from enum import Enum
34
from unittest.mock import Mock
45

@@ -9,11 +10,16 @@
910
from flask_mongoengine import db_fields, documents
1011

1112
try:
13+
from wtforms import fields as wtf_fields
1214
from wtforms import validators as wtf_validators_
1315

16+
from flask_mongoengine.wtf import fields as mongo_fields
17+
1418
wtforms_not_installed = False
1519
except ImportError:
1620
wtf_validators_ = None
21+
wtf_fields = None
22+
mongo_fields = None
1723
wtforms_not_installed = True
1824

1925

@@ -151,7 +157,6 @@ def test__ensure_callable_or_list__raise_error_if_argument_not_callable_and_not_
151157
db_fields.DecimalField,
152158
db_fields.DictField,
153159
db_fields.DynamicField,
154-
db_fields.EmailField,
155160
db_fields.EmbeddedDocumentField,
156161
db_fields.EmbeddedDocumentListField,
157162
db_fields.EnumField,
@@ -178,8 +183,6 @@ def test__ensure_callable_or_list__raise_error_if_argument_not_callable_and_not_
178183
db_fields.ReferenceField,
179184
db_fields.SequenceField,
180185
db_fields.SortedListField,
181-
db_fields.StringField,
182-
db_fields.URLField,
183186
db_fields.UUIDField,
184187
],
185188
)
@@ -874,6 +877,53 @@ def test__parent__init__method_included_in_init_chain(self, db, mocker):
874877
mixin_init_spy.assert_called_once()
875878

876879

880+
@pytest.mark.skipif(condition=wtforms_not_installed, reason="No WTF CI/CD chain")
881+
@pytest.mark.parametrize(
882+
"StringClass",
883+
[
884+
db_fields.StringField,
885+
db_fields.URLField,
886+
db_fields.EmailField,
887+
],
888+
)
889+
class TestStringFieldCommons:
890+
"""
891+
Ensure all string based classes call :func:`~._setup_strings_common_validators`
892+
and get expected results.
893+
"""
894+
895+
@pytest.mark.parametrize(
896+
["min_", "max_", "validator_min", "validator_max"],
897+
[
898+
[None, 3, -1, 3],
899+
[3, None, 3, -1],
900+
[3, 5, 3, 5],
901+
],
902+
)
903+
def test__init__method__set_length_validator__if_size_given(
904+
self, StringClass, min_, max_, validator_min, validator_max
905+
):
906+
field = StringClass(min_length=min_, max_length=max_)
907+
validator = [
908+
val
909+
for val in field.wtf_field_options["validators"]
910+
if val.__class__ is wtf_validators_.Length
911+
][0]
912+
assert validator is not None
913+
assert validator.min == validator_min
914+
assert validator.max == validator_max
915+
916+
def test__init__method__set_regex_validator__if_option(self, StringClass):
917+
field = StringClass(regex="something")
918+
validator = [
919+
val
920+
for val in field.wtf_field_options["validators"]
921+
if val.__class__ is wtf_validators_.Regexp
922+
][-1]
923+
assert validator is not None
924+
assert validator.regex == re.compile("something")
925+
926+
877927
class TestStringField:
878928
"""Custom test set for :class:`~flask_mongoengine.wtf.db_fields.StringField`"""
879929

@@ -887,6 +937,62 @@ def test__parent__init__method_included_in_init_chain(self, db, mocker):
887937
field_init_spy.assert_called_once()
888938
mixin_init_spy.assert_called_once()
889939

940+
@pytest.mark.skipif(condition=wtforms_not_installed, reason="No WTF CI/CD chain")
941+
def test__init__method_report_warning__if_password_keyword_setting_set(
942+
self, recwarn
943+
):
944+
field = db_fields.StringField(password=True)
945+
assert str(recwarn.list[0].message) == (
946+
"Passing 'password' keyword argument to field definition is "
947+
"deprecated and will be removed in version 3.0.0. "
948+
"Please use 'wtf_field_class' parameter to specify correct field "
949+
"class. If both values set, 'wtf_field_class' is used."
950+
)
951+
assert issubclass(field.wtf_field_class, wtf_fields.PasswordField)
952+
953+
@pytest.mark.skipif(condition=wtforms_not_installed, reason="No WTF CI/CD chain")
954+
def test__init__method_report_warning__if_textarea_keyword_setting_set(
955+
self, recwarn
956+
):
957+
field = db_fields.StringField(textarea=True)
958+
assert str(recwarn.list[0].message) == (
959+
"Passing 'textarea' keyword argument to field definition is "
960+
"deprecated and will be removed in version 3.0.0. "
961+
"Please use 'wtf_field_class' parameter to specify correct field "
962+
"class. If both values set, 'wtf_field_class' is used."
963+
)
964+
assert issubclass(field.wtf_field_class, wtf_fields.TextAreaField)
965+
966+
def test__init__method_raise_error__if_password_and_keyword_setting_both_set(self):
967+
with pytest.raises(ValueError) as error:
968+
db_fields.StringField(textarea=True, password=True)
969+
970+
assert str(error.value) == "Password field cannot use TextAreaField class."
971+
972+
@pytest.mark.skipif(condition=wtforms_not_installed, reason="No WTF CI/CD chain")
973+
def test__init__method__set_textarea__in__wtf_field_class__even_if_size_given(self):
974+
field = db_fields.StringField(textarea=True, min_length=3)
975+
assert field.wtf_field_class is mongo_fields.MongoTextAreaField
976+
977+
@pytest.mark.skipif(condition=wtforms_not_installed, reason="No WTF CI/CD chain")
978+
@pytest.mark.parametrize(
979+
["min_", "max_"],
980+
[
981+
[None, 3],
982+
[3, None],
983+
[3, 5],
984+
],
985+
)
986+
def test__init__method__set_string_field_class__if_size_given(self, min_, max_):
987+
field = db_fields.StringField(min_length=min_, max_length=max_)
988+
validator = [
989+
val
990+
for val in field.wtf_field_options["validators"]
991+
if val.__class__ is wtf_validators_.Length
992+
]
993+
assert field.wtf_field_class is mongo_fields.MongoStringField
994+
assert validator is not None
995+
890996

891997
class TestURLField:
892998
"""Custom test set for :class:`~flask_mongoengine.wtf.db_fields.URLField`"""

0 commit comments

Comments
 (0)