Source code for bottle_utils.form.forms

from collections import OrderedDict

try:
    from bottle_utils.i18n import lazy_gettext as _
except ImportError:
    _ = lambda x: x

from .fields import DormantField, Field, ErrorMixin
from .exceptions import ValidationError


[docs]class Form(ErrorMixin): """ Base form class to be subclassed. To define a new form subclass this class:: class NewForm(Form): field1 = Field('Field 1') field2 = Field('Field 2', [Required]) Forms support field pre- and post-procesors. These methods are named after the field names by prepending ``preprocess_`` and ``postprocess_`` respectively. For example:: class NewForm(Form): field1 = Field('Field 1') field2 = Field('Field 2', [Required]) def preprocess_field1(self, value): return value.replace('this', 'that') def postprocess_field1(self, value): return value + 'done' Preprocessors can be defined for individual fields, and are ran before any validation happens over the field's data. Preprocessors are also allowed to raise ``ValidationError``, though their actual purpose is to perform some manipulation over the incoming data, before it is passed over to the validators. The return value of the preprocessor is the value that is going to be validated further. Postprocessors perform a similar purpose as preprocessors, except that they are invoked after field-level validation passes. Their return value is the value that is going to be the stored as cleaned / validated data. """ ValidationError = ValidationError #: Prefix to use for looking up preprocessors _pre_processor_prefix = 'preprocess_' #: Prefix to use for looking up postprocessors _post_processor_prefix = 'postprocess_' # Translators, used as generic error message in forms generic_error = _('Form contains invalid data.') messages = {} def __init__(self, data=None, messages={}): """Initialize forms. :param data: Dict-like object containing the form data to be validated, or the initial values of a new form """ self._has_error = False self._error = None self.processed_data = {} self.messages = self.messages.copy() self.messages.update(messages) self._bind(data) def _bind(self, data): """Binds field names and values to the field instances.""" for field_name, dormant_field in self.fields.items(): field_instance = dormant_field.bind(field_name) setattr(self, field_name, field_instance) if data is not None: field_instance.bind_value(data.get(field_name)) @property def field_errors(self): """ Dictionary of all field error messages. This property maps the field names to error message maps. Field names are mapped to fields' messages property, which maps error type to actual message. This dictionary can also be used to modify the messages because message mappings are not copied. """ messages = {} for field_name, field in self.fields.items(): messages[field_name] = field.messages return messages #: Alias for :py:attr:`~field_errors` retained for # backwards-compatibility field_messages = field_errors @property def fields(self): """ Dictionary of all the fields found on the form instance. The return value is never cached so dynamically adding new fields to the form is allowed. """ types = (Field, DormantField) is_form_field = lambda name: isinstance(getattr(self, name), types) ignored_attrs = ['fields', 'field_messages', 'field_errors'] pairs = [(name, getattr(self, name)) for name in dir(self) if name not in ignored_attrs and is_form_field(name)] return OrderedDict(sorted(pairs, key=lambda x: x[1]._order)) def _add_error(self, field, error): # if the error is from one of the processors, bind it to the field too field._error = error self._has_error = True def _run_processor(self, prefix, field_name, value): processor_name = prefix + field_name processor = getattr(self, processor_name, None) if callable(processor): return processor(value) return value
[docs] def is_valid(self): """ Perform full form validation over the initialized form. The method has the following side-effects: - in case errors are found, the form's `errors` container is going to be populated accordingly. - validated and processed values are going to be put into the `processed_data` dictionary. Return value is a boolean, and is ``True`` is form data is valid. """ for field_name, field in self.fields.items(): # run pre-processor on value, if defined try: field.processed_value = self._run_processor( self._pre_processor_prefix, field_name, field.value ) except ValidationError as exc: self._add_error(field, exc) continue # perform individual field validation if not field.is_valid(): self._has_error = True continue # run post-processor on processed value, if defined try: field.processed_value = self._run_processor( self._post_processor_prefix, field_name, field.processed_value ) except ValidationError as exc: self._add_error(field, exc) continue # if field level validations passed, add the value to a dictionary # holding validated / processed data self.processed_data[field_name] = field.processed_value # run form level validation only if there were no field errors detected if not self._has_error: try: self.validate() except ValidationError as exc: exc.is_form = True self._add_error(self, exc) return not self._has_error
[docs] def validate(self): """Perform form-level validation, which can check fields dependent on each other. The function is expected to be overridden by implementors in case form-level validation is needed, but it's optional. In case an error is found, a `ValidationError` exception should be raised by the function.""" pass