Skip to content

Commit 41d475a

Browse files
committed
Add Dict Field database support
1 parent e011ceb commit 41d475a

File tree

4 files changed

+74
-18
lines changed

4 files changed

+74
-18
lines changed

docs/api/wtf.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ flask_mongoengine.wtf.fields module
77
-----------------------------------
88

99
.. automodule:: flask_mongoengine.wtf.fields
10+
:special-members: __init__
1011

1112
flask_mongoengine.wtf.models module
1213
-----------------------------------

flask_mongoengine/db_fields.py

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -457,20 +457,7 @@ class DictField(WtfFieldMixin, fields.DictField):
457457
All arguments should be passed as keyword arguments, to exclude unexpected behaviour.
458458
"""
459459

460-
DEFAULT_WTF_FIELD = custom_fields.DictField if custom_fields else None
461-
462-
def to_wtf_field(
463-
self,
464-
*,
465-
model: Optional[Type] = None,
466-
field_kwargs: Optional[dict] = None,
467-
):
468-
"""
469-
Protection from execution of :func:`to_wtf_field` in form generation.
470-
471-
:raises NotImplementedError: Field converter to WTForm Field not implemented.
472-
"""
473-
raise NotImplementedError("Field converter to WTForm Field not implemented.")
460+
DEFAULT_WTF_FIELD = custom_fields.MongoDictField if custom_fields else None
474461

475462

476463
class DynamicField(WtfFieldMixin, fields.DynamicField):

flask_mongoengine/wtf/fields.py

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
"ModelSelectField",
66
"QuerySetSelectField",
77
]
8-
from gettext import gettext as _
9-
from typing import Optional
8+
from typing import Callable, Optional
109

1110
from flask import json
1211
from mongoengine.queryset import DoesNotExist
@@ -63,6 +62,7 @@ def __init__(
6362
label_modifier=None,
6463
**kwargs,
6564
):
65+
"""Init docstring placeholder."""
6666

6767
super(QuerySetSelectField, self).__init__(label, validators, **kwargs)
6868
self.label_attr = label_attr
@@ -122,7 +122,7 @@ def pre_validate(self, form):
122122
:param form: The form the field belongs to.
123123
"""
124124
if (not self.allow_blank or self.data is not None) and not self.data:
125-
raise wtf_validators.ValidationError(_("Not a valid choice"))
125+
raise wtf_validators.ValidationError(self.gettext("Not a valid choice"))
126126

127127
def _is_selected(self, item):
128128
return item == self.data
@@ -414,3 +414,72 @@ class MongoFloatField(wtf_fields.FloatField):
414414
"""
415415

416416
widget = wtf_widgets.NumberInput(step="any")
417+
418+
419+
class MongoDictField(MongoTextAreaField):
420+
"""Form field to handle JSON in :class:`~flask_mongoengine.db_fields.DictField`."""
421+
422+
def __init__(
423+
self,
424+
json_encoder: Optional[Callable] = None,
425+
json_encoder_kwargs: Optional[dict] = None,
426+
json_decoder: Optional[Callable] = None,
427+
json_decoder_kwargs: Optional[dict] = None,
428+
*args,
429+
**kwargs,
430+
):
431+
"""
432+
Special WTForms field for :class:`~flask_mongoengine.db_fields.DictField`
433+
434+
Configuration available with providing :attr:`wtf_options` on
435+
:class:`~flask_mongoengine.db_fields.DictField` initialization.
436+
437+
:param json_encoder: Any function, capable to transform dict to string, by
438+
default :func:`json.dumps`
439+
:param json_encoder_kwargs: Any dictionary with parameters to
440+
:func:`json_encoder`, by default: `{"indent":4}`
441+
:param json_decoder: Any function, capable to transform string to dict, by
442+
default :func:`json.loads`
443+
:param json_decoder_kwargs: Any dictionary with parameters to
444+
:func:`json_decoder`, by default: `{}`
445+
"""
446+
447+
self.json_encoder = json_encoder or json.dumps
448+
self.json_encoder_kwargs = json_encoder_kwargs or {"indent": 4}
449+
self.json_decoder = json_decoder or json.loads
450+
self.json_decoder_kwargs = json_decoder_kwargs or {}
451+
self.data = None
452+
super().__init__(*args, **kwargs)
453+
454+
def _parse_json_data(self):
455+
"""Tries to load JSON data with python internal JSON library."""
456+
try:
457+
self.data = self.json_decoder(self.data, **self.json_decoder_kwargs)
458+
except ValueError as error:
459+
raise wtf_validators.ValidationError(
460+
self.gettext(f"Cannot load data: {error}")
461+
) from error
462+
463+
def _ensure_data_is_dict(self):
464+
"""Ensures that saved data is dict, not a list or other valid parsed JSON."""
465+
if not isinstance(self.data, dict):
466+
raise wtf_validators.ValidationError(
467+
self.gettext("Not a valid dictionary (list input detected).")
468+
)
469+
470+
def process_formdata(self, valuelist):
471+
"""Process text form data to dictionary or raise JSONDecodeError."""
472+
super().process_formdata(valuelist)
473+
if self.data is not None:
474+
self._parse_json_data()
475+
self._ensure_data_is_dict()
476+
477+
def _value(self):
478+
"""Show existing data as pretty-formatted, or show raw data/empty dict."""
479+
if self.data:
480+
if isinstance(self.data, dict):
481+
return self.json_encoder(self.data, **self.json_encoder_kwargs)
482+
else:
483+
# This allows to fix/see input errors, without escaping.
484+
return self.data
485+
return "{}"

tests/test_db_fields.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,6 @@ def test__ensure_callable_or_list__raise_error_if_argument_not_callable_and_not_
150150
[
151151
db_fields.BinaryField,
152152
db_fields.CachedReferenceField,
153-
db_fields.DictField,
154153
db_fields.DynamicField,
155154
db_fields.EmbeddedDocumentField,
156155
db_fields.EmbeddedDocumentListField,

0 commit comments

Comments
 (0)