123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329 |
- import itertools
- from collections import OrderedDict
- from wtforms.meta import DefaultMeta
- from wtforms.utils import unset_value
- __all__ = ("BaseForm", "Form")
- _default_meta = DefaultMeta()
- class BaseForm:
- """
- Base Form Class. Provides core behaviour like field construction,
- validation, and data and error proxying.
- """
- def __init__(self, fields, prefix="", meta=_default_meta):
- """
- :param fields:
- A dict or sequence of 2-tuples of partially-constructed fields.
- :param prefix:
- If provided, all fields will have their name prefixed with the
- value.
- :param meta:
- A meta instance which is used for configuration and customization
- of WTForms behaviors.
- """
- if prefix and prefix[-1] not in "-_;:/.":
- prefix += "-"
- self.meta = meta
- self._prefix = prefix
- self._fields = OrderedDict()
- if hasattr(fields, "items"):
- fields = fields.items()
- translations = self.meta.get_translations(self)
- extra_fields = []
- if meta.csrf:
- self._csrf = meta.build_csrf(self)
- extra_fields.extend(self._csrf.setup_form(self))
- for name, unbound_field in itertools.chain(fields, extra_fields):
- field_name = unbound_field.name or name
- options = dict(name=field_name, prefix=prefix, translations=translations)
- field = meta.bind_field(self, unbound_field, options)
- self._fields[name] = field
- self.form_errors = []
- def __iter__(self):
- """Iterate form fields in creation order."""
- return iter(self._fields.values())
- def __contains__(self, name):
- """Returns `True` if the named field is a member of this form."""
- return name in self._fields
- def __getitem__(self, name):
- """Dict-style access to this form's fields."""
- return self._fields[name]
- def __setitem__(self, name, value):
- """Bind a field to this form."""
- self._fields[name] = value.bind(form=self, name=name, prefix=self._prefix)
- def __delitem__(self, name):
- """Remove a field from this form."""
- del self._fields[name]
- def populate_obj(self, obj):
- """
- Populates the attributes of the passed `obj` with data from the form's
- fields.
- :note: This is a destructive operation; Any attribute with the same name
- as a field will be overridden. Use with caution.
- """
- for name, field in self._fields.items():
- field.populate_obj(obj, name)
- def process(self, formdata=None, obj=None, data=None, extra_filters=None, **kwargs):
- """Process default and input data with each field.
- :param formdata: Input data coming from the client, usually
- ``request.form`` or equivalent. Should provide a "multi
- dict" interface to get a list of values for a given key,
- such as what Werkzeug, Django, and WebOb provide.
- :param obj: Take existing data from attributes on this object
- matching form field attributes. Only used if ``formdata`` is
- not passed.
- :param data: Take existing data from keys in this dict matching
- form field attributes. ``obj`` takes precedence if it also
- has a matching attribute. Only used if ``formdata`` is not
- passed.
- :param extra_filters: A dict mapping field attribute names to
- lists of extra filter functions to run. Extra filters run
- after filters passed when creating the field. If the form
- has ``filter_<fieldname>``, it is the last extra filter.
- :param kwargs: Merged with ``data`` to allow passing existing
- data as parameters. Overwrites any duplicate keys in
- ``data``. Only used if ``formdata`` is not passed.
- """
- formdata = self.meta.wrap_formdata(self, formdata)
- if data is not None:
- kwargs = dict(data, **kwargs)
- filters = extra_filters.copy() if extra_filters is not None else {}
- for name, field in self._fields.items():
- field_extra_filters = filters.get(name, [])
- inline_filter = getattr(self, "filter_%s" % name, None)
- if inline_filter is not None:
- field_extra_filters.append(inline_filter)
- if obj is not None and hasattr(obj, name):
- data = getattr(obj, name)
- elif name in kwargs:
- data = kwargs[name]
- else:
- data = unset_value
- field.process(formdata, data, extra_filters=field_extra_filters)
- def validate(self, extra_validators=None):
- """
- Validates the form by calling `validate` on each field.
- :param extra_validators:
- If provided, is a dict mapping field names to a sequence of
- callables which will be passed as extra validators to the field's
- `validate` method.
- Returns `True` if no errors occur.
- """
- success = True
- for name, field in self._fields.items():
- if extra_validators is not None and name in extra_validators:
- extra = extra_validators[name]
- else:
- extra = tuple()
- if not field.validate(self, extra):
- success = False
- return success
- @property
- def data(self):
- return {name: f.data for name, f in self._fields.items()}
- @property
- def errors(self):
- errors = {name: f.errors for name, f in self._fields.items() if f.errors}
- if self.form_errors:
- errors[None] = self.form_errors
- return errors
- class FormMeta(type):
- """
- The metaclass for `Form` and any subclasses of `Form`.
- `FormMeta`'s responsibility is to create the `_unbound_fields` list, which
- is a list of `UnboundField` instances sorted by their order of
- instantiation. The list is created at the first instantiation of the form.
- If any fields are added/removed from the form, the list is cleared to be
- re-generated on the next instantiation.
- Any properties which begin with an underscore or are not `UnboundField`
- instances are ignored by the metaclass.
- """
- def __init__(cls, name, bases, attrs):
- type.__init__(cls, name, bases, attrs)
- cls._unbound_fields = None
- cls._wtforms_meta = None
- def __call__(cls, *args, **kwargs):
- """
- Construct a new `Form` instance.
- Creates the `_unbound_fields` list and the internal `_wtforms_meta`
- subclass of the class Meta in order to allow a proper inheritance
- hierarchy.
- """
- if cls._unbound_fields is None:
- fields = []
- for name in dir(cls):
- if not name.startswith("_"):
- unbound_field = getattr(cls, name)
- if hasattr(unbound_field, "_formfield"):
- fields.append((name, unbound_field))
- # We keep the name as the second element of the sort
- # to ensure a stable sort.
- fields.sort(key=lambda x: (x[1].creation_counter, x[0]))
- cls._unbound_fields = fields
- # Create a subclass of the 'class Meta' using all the ancestors.
- if cls._wtforms_meta is None:
- bases = []
- for mro_class in cls.__mro__:
- if "Meta" in mro_class.__dict__:
- bases.append(mro_class.Meta)
- cls._wtforms_meta = type("Meta", tuple(bases), {})
- return type.__call__(cls, *args, **kwargs)
- def __setattr__(cls, name, value):
- """
- Add an attribute to the class, clearing `_unbound_fields` if needed.
- """
- if name == "Meta":
- cls._wtforms_meta = None
- elif not name.startswith("_") and hasattr(value, "_formfield"):
- cls._unbound_fields = None
- type.__setattr__(cls, name, value)
- def __delattr__(cls, name):
- """
- Remove an attribute from the class, clearing `_unbound_fields` if
- needed.
- """
- if not name.startswith("_"):
- cls._unbound_fields = None
- type.__delattr__(cls, name)
- class Form(BaseForm, metaclass=FormMeta):
- """
- Declarative Form base class. Extends BaseForm's core behaviour allowing
- fields to be defined on Form subclasses as class attributes.
- In addition, form and instance input data are taken at construction time
- and passed to `process()`.
- """
- Meta = DefaultMeta
- def __init__(
- self,
- formdata=None,
- obj=None,
- prefix="",
- data=None,
- meta=None,
- **kwargs,
- ):
- """
- :param formdata: Input data coming from the client, usually
- ``request.form`` or equivalent. Should provide a "multi
- dict" interface to get a list of values for a given key,
- such as what Werkzeug, Django, and WebOb provide.
- :param obj: Take existing data from attributes on this object
- matching form field attributes. Only used if ``formdata`` is
- not passed.
- :param prefix: If provided, all fields will have their name
- prefixed with the value. This is for distinguishing multiple
- forms on a single page. This only affects the HTML name for
- matching input data, not the Python name for matching
- existing data.
- :param data: Take existing data from keys in this dict matching
- form field attributes. ``obj`` takes precedence if it also
- has a matching attribute. Only used if ``formdata`` is not
- passed.
- :param meta: A dict of attributes to override on this form's
- :attr:`meta` instance.
- :param extra_filters: A dict mapping field attribute names to
- lists of extra filter functions to run. Extra filters run
- after filters passed when creating the field. If the form
- has ``filter_<fieldname>``, it is the last extra filter.
- :param kwargs: Merged with ``data`` to allow passing existing
- data as parameters. Overwrites any duplicate keys in
- ``data``. Only used if ``formdata`` is not passed.
- """
- meta_obj = self._wtforms_meta()
- if meta is not None and isinstance(meta, dict):
- meta_obj.update_values(meta)
- super().__init__(self._unbound_fields, meta=meta_obj, prefix=prefix)
- for name, field in self._fields.items():
- # Set all the fields to attributes so that they obscure the class
- # attributes with the same names.
- setattr(self, name, field)
- self.process(formdata, obj, data=data, **kwargs)
- def __setitem__(self, name, value):
- raise TypeError("Fields may not be added to Form instances, only classes.")
- def __delitem__(self, name):
- del self._fields[name]
- setattr(self, name, None)
- def __delattr__(self, name):
- if name in self._fields:
- self.__delitem__(name)
- else:
- # This is done for idempotency, if we have a name which is a field,
- # we want to mask it by setting the value to None.
- unbound_field = getattr(self.__class__, name, None)
- if unbound_field is not None and hasattr(unbound_field, "_formfield"):
- setattr(self, name, None)
- else:
- super().__delattr__(name)
- def validate(self, extra_validators=None):
- """Validate the form by calling ``validate`` on each field.
- Returns ``True`` if validation passes.
- If the form defines a ``validate_<fieldname>`` method, it is
- appended as an extra validator for the field's ``validate``.
- :param extra_validators: A dict mapping field names to lists of
- extra validator methods to run. Extra validators run after
- validators passed when creating the field. If the form has
- ``validate_<fieldname>``, it is the last extra validator.
- """
- if extra_validators is not None:
- extra = extra_validators.copy()
- else:
- extra = {}
- for name in self._fields:
- inline = getattr(self.__class__, f"validate_{name}", None)
- if inline is not None:
- extra.setdefault(name, []).append(inline)
- return super().validate(extra)
|