Source code for flask_mongoengine.wtf.fields

"""
Useful form fields for use with the mongoengine.
"""
__all__ = [
    "ModelSelectField",
    "QuerySetSelectField",
]
from typing import Callable, Optional

from flask import json
from mongoengine.queryset import DoesNotExist
from wtforms import fields as wtf_fields
from wtforms import validators as wtf_validators
from wtforms import widgets as wtf_widgets


[docs]def coerce_boolean(value: Optional[str]) -> Optional[bool]: """Transform SelectField boolean value from string and in reverse direction.""" try: value = value.lower() except AttributeError: pass if value is None or value in {"", "none", "null"}: return None elif value is False or value in {"no", "n", "false"}: return False elif value is True or value in {"yes", "y", "true"}: return True else: raise ValueError("Unexpected string value.")
# noinspection PyAttributeOutsideInit,PyAbstractClass
[docs]class QuerySetSelectField(wtf_fields.SelectFieldBase): """ Given a QuerySet either at initialization or inside a view, will display a select drop-down field of choices. The `data` property actually will store/keep an ORM model instance, not the ID. Submitting a choice which is not in the queryset will result in a validation error. Specifying `label_attr` in the constructor will use that property of the model instance for display in the list, else the model object's `__str__` or `__unicode__` will be used. If `allow_blank` is set to `True`, then a blank choice will be added to the top of the list. Selecting this choice will result in the `data` property being `None`. The label for the blank choice can be set by specifying the `blank_text` parameter. """ widget = wtf_widgets.Select()
[docs] def __init__( self, label="", validators=None, queryset=None, label_attr="", allow_blank=False, blank_text="---", label_modifier=None, **kwargs, ): """Init docstring placeholder.""" super(QuerySetSelectField, self).__init__(label, validators, **kwargs) self.label_attr = label_attr self.allow_blank = allow_blank self.blank_text = blank_text self.label_modifier = label_modifier self.queryset = queryset
[docs] def iter_choices(self): """ Provides data for choice widget rendering. Must return a sequence or iterable of (value, label, selected) tuples. """ if self.allow_blank: yield "__None", self.blank_text, self.data is None if self.queryset is None: return self.queryset.rewind() for obj in self.queryset: label = ( self.label_modifier(obj) if self.label_modifier else (self.label_attr and getattr(obj, self.label_attr) or obj) ) if isinstance(self.data, list): selected = obj in self.data else: selected = self._is_selected(obj) yield obj.id, label, selected
[docs] def process_formdata(self, valuelist): """ Process data received over the wire from a form. This will be called during form construction with data supplied through the `formdata` argument. :param valuelist: A list of strings to process. """ if not valuelist or valuelist[0] == "__None" or self.queryset is None: self.data = None return try: obj = self.queryset.get(pk=valuelist[0]) self.data = obj except DoesNotExist: self.data = None
[docs] def pre_validate(self, form): """ Field-level validation. Runs before any other validators. :param form: The form the field belongs to. """ if (not self.allow_blank or self.data is not None) and not self.data: raise wtf_validators.ValidationError(self.gettext("Not a valid choice"))
[docs] def _is_selected(self, item): return item == self.data
# noinspection PyAttributeOutsideInit,PyAbstractClass
[docs]class QuerySetSelectMultipleField(QuerySetSelectField): """Same as :class:`QuerySetSelectField` but with multiselect options.""" widget = wtf_widgets.Select(multiple=True)
[docs] def __init__( self, label="", validators=None, queryset=None, label_attr="", allow_blank=False, blank_text="---", **kwargs, ): super(QuerySetSelectMultipleField, self).__init__( label, validators, queryset, label_attr, allow_blank, blank_text, **kwargs )
[docs] def process_formdata(self, valuelist): """ Process data received over the wire from a form. This will be called during form construction with data supplied through the `formdata` argument. :param valuelist: A list of strings to process. """ if not valuelist or valuelist[0] == "__None" or not self.queryset: self.data = None return self.queryset.rewind() self.data = list(self.queryset(pk__in=valuelist)) if not len(self.data): self.data = None
[docs] def _is_selected(self, item): return item in self.data if self.data else False
# noinspection PyAttributeOutsideInit,PyAbstractClass
[docs]class ModelSelectField(QuerySetSelectField): """ Like a QuerySetSelectField, except takes a model class instead of a queryset and lists everything in it. """
[docs] def __init__(self, label="", validators=None, model=None, **kwargs): queryset = kwargs.pop("queryset", model.objects) super(ModelSelectField, self).__init__( label, validators, queryset=queryset, **kwargs )
# noinspection PyAttributeOutsideInit,PyAbstractClass
[docs]class ModelSelectMultipleField(QuerySetSelectMultipleField): """ Allows multiple select """
[docs] def __init__(self, label="", validators=None, model=None, **kwargs): queryset = kwargs.pop("queryset", model.objects) super(ModelSelectMultipleField, self).__init__( label, validators, queryset=queryset, **kwargs )
# noinspection PyAttributeOutsideInit,PyAbstractClass
[docs]class JSONField(wtf_fields.TextAreaField): """Special version fo :class:`wtforms.fields.TextAreaField`."""
[docs] def _value(self): # TODO: Investigate why raw mentioned. if self.raw_data: return self.raw_data[0] else: return self.data and json.dumps(self.data) or ""
[docs] def process_formdata(self, valuelist): """ Process data received over the wire from a form. This will be called during form construction with data supplied through the `formdata` argument. :param valuelist: A list of strings to process. """ if valuelist: try: self.data = json.loads(valuelist[0]) except ValueError as error: raise ValueError(self.gettext("Invalid JSON data.")) from error
[docs]class DictField(JSONField): """ Special version fo :class:`JSONField` to be generated for :class:`flask_mongoengine.db_fields.DictField`. Used in generator before flask_mongoengine version 2.0 """
[docs] def process_formdata(self, valuelist): """ Process data received over the wire from a form. This will be called during form construction with data supplied through the `formdata` argument. :param valuelist: A list of strings to process. """ super(DictField, self).process_formdata(valuelist) if valuelist and not isinstance(self.data, dict): raise ValueError(self.gettext("Not a valid dictionary."))
# noinspection PyAttributeOutsideInit
[docs]class NoneStringField(wtf_fields.StringField): """ Custom StringField that counts "" as None """
[docs] def process_formdata(self, valuelist): """ Process data received over the wire from a form. This will be called during form construction with data supplied through the `formdata` argument. :param valuelist: A list of strings to process. """ if valuelist: self.data = valuelist[0] if self.data == "": self.data = None
# noinspection PyAttributeOutsideInit
[docs]class BinaryField(wtf_fields.TextAreaField): """ Custom TextAreaField that converts its value with binary_type. """
[docs] def process_formdata(self, valuelist): """ Process data received over the wire from a form. This will be called during form construction with data supplied through the `formdata` argument. :param valuelist: A list of strings to process. """ if valuelist: self.data = bytes(valuelist[0], "utf-8")
# noinspection PyUnresolvedReferences,PyAttributeOutsideInit
[docs]class EmptyStringIsNoneMixin: """ Special mixin to ignore empty strings **before** parent class processing. Unlike old :class:`NoneStringField` we do it before parent class call, this allows us to reuse this mixin in many more cases without errors. """
[docs] def process_formdata(self, valuelist): """ Ignores empty string and calls parent :func:`process_formdata` if data present. :param valuelist: A list of strings to process. """ if not valuelist or valuelist[0] == "": self.data = None else: super().process_formdata(valuelist)
[docs]class MongoBooleanField(wtf_fields.SelectField): """Mongo SelectField field for BooleanFields, that correctly coerce values."""
[docs] def __init__( self, label=None, validators=None, coerce=None, choices=None, validate_choice=True, **kwargs, ): """ Replaces defaults of :class:`wtforms.fields.SelectField` with for Boolean values. Fully compatible with :class:`wtforms.fields.SelectField` and have same parameters. """ if coerce is None: coerce = coerce_boolean if choices is None: choices = [("", "---"), ("yes", "yes"), ("no", "no")] super().__init__( label=label, validators=validators, coerce=coerce, choices=choices, validate_choice=validate_choice, **kwargs, )
[docs]class MongoEmailField(EmptyStringIsNoneMixin, wtf_fields.EmailField): """ Regular :class:`wtforms.fields.EmailField`, that transform empty string to `None`. """ pass
[docs]class MongoHiddenField(EmptyStringIsNoneMixin, wtf_fields.HiddenField): """ Regular :class:`wtforms.fields.HiddenField`, that transform empty string to `None`. """ pass
[docs]class MongoPasswordField(EmptyStringIsNoneMixin, wtf_fields.PasswordField): """ Regular :class:`wtforms.fields.PasswordField`, that transform empty string to `None`. """ pass
[docs]class MongoSearchField(EmptyStringIsNoneMixin, wtf_fields.SearchField): """ Regular :class:`wtforms.fields.SearchField`, that transform empty string to `None`. """ pass
[docs]class MongoStringField(EmptyStringIsNoneMixin, wtf_fields.StringField): """ Regular :class:`wtforms.fields.StringField`, that transform empty string to `None`. """ pass
[docs]class MongoTelField(EmptyStringIsNoneMixin, wtf_fields.TelField): """ Regular :class:`wtforms.fields.TelField`, that transform empty string to `None`. """ pass
[docs]class MongoTextAreaField(EmptyStringIsNoneMixin, wtf_fields.TextAreaField): """ Regular :class:`wtforms.fields.TextAreaField`, that transform empty string to `None`. """ pass
[docs]class MongoURLField(EmptyStringIsNoneMixin, wtf_fields.URLField): """ Regular :class:`wtforms.fields.URLField`, that transform empty string to `None`. """ pass
[docs]class MongoFloatField(wtf_fields.FloatField): """ Regular :class:`wtforms.fields.FloatField`, with widget replaced to :class:`wtforms.widgets.NumberInput`. """ widget = wtf_widgets.NumberInput(step="any")
[docs]class MongoDictField(MongoTextAreaField): """Form field to handle JSON in :class:`~flask_mongoengine.db_fields.DictField`."""
[docs] def __init__( self, json_encoder: Optional[Callable] = None, json_encoder_kwargs: Optional[dict] = None, json_decoder: Optional[Callable] = None, json_decoder_kwargs: Optional[dict] = None, null: Optional[bool] = None, *args, **kwargs, ): """ Special WTForms field for :class:`~flask_mongoengine.db_fields.DictField` Configuration available with providing :attr:`wtf_options` on :class:`~flask_mongoengine.db_fields.DictField` initialization. :param json_encoder: Any function, capable to transform dict to string, by default :func:`json.dumps` :param json_encoder_kwargs: Any dictionary with parameters to :func:`json_encoder`, by default: `{"indent":4}` :param json_decoder: Any function, capable to transform string to dict, by default :func:`json.loads` :param json_decoder_kwargs: Any dictionary with parameters to :func:`json_decoder`, by default: `{}` """ self.json_encoder = json_encoder or json.dumps self.json_encoder_kwargs = json_encoder_kwargs or {"indent": 4} self.json_decoder = json_decoder or json.loads self.json_decoder_kwargs = json_decoder_kwargs or {} self.data = None self.null = null super().__init__(*args, **kwargs) try: self._default = self.default() except TypeError: self._default = self.default if isinstance(self._default, dict): self._default = self.json_encoder(self._default, **self.json_encoder_kwargs)
[docs] def _parse_json_data(self): """Tries to load JSON data with python internal JSON library.""" try: self.data = self.json_decoder(self.data, **self.json_decoder_kwargs) except ValueError as error: raise wtf_validators.ValidationError( self.gettext(f"Cannot load data: {error}") ) from error
[docs] def _ensure_data_is_dict(self): """Ensures that saved data is dict, not a list or other valid parsed JSON.""" if not isinstance(self.data, dict): raise wtf_validators.ValidationError( self.gettext("Not a valid dictionary (list input detected).") )
[docs] def process_formdata(self, valuelist): """Process text form data to dictionary or raise JSONDecodeError.""" super().process_formdata(valuelist) if self.data is not None: self._parse_json_data() self._ensure_data_is_dict()
[docs] def _value(self): """Show existing data as pretty-formatted, or show raw data/empty dict.""" if self.data is not None: return ( self.json_encoder(self.data, **self.json_encoder_kwargs) if isinstance(self.data, dict) else self.data ) return self._default if self._default is not None and not self.null else ""