class EnumChoice(Converter): ''' In addition to Converter interface it must provide :meth:`options` and :meth:`get_label` methods. ''' #: converter for value, before it is tested to be in a set of acceptable #: values conv = Char() #: acceptable choices list:: #: #: EnumChoice(choices=[(python_value, label), ...]) choices = () error_required = N_('you must select a value') def from_python(self, value): conv = self.conv return conv.from_python(value) def to_python(self, value): value = self.conv.accept(value, silent=True) if value not in dict(self.choices): return None return value def options(self): ''' Yields `(raw_value, label)` pairs for all acceptable choices. ''' conv = self.conv for python_value, label in self.choices: yield conv.from_python(python_value), label
class PasswordConv(convs.Char): error_mismatch = N_('password and confirm mismatch') error_required = N_('password required') def from_python(self, value): return dict([(field.name, None) for field in self.field.fields]) def get_initial(self): return '' def to_python(self, value): etalon = value[list(value)[0]] for field in self.field.fields: self.assert_(value[field.name] == etalon, self.error_mismatch) if self.required: self.assert_(etalon not in (None, ''), self.error_required) elif etalon in (None, ''): return None return etalon
def clean_value(self, value): value = Char.clean_value(self, value) try: doc = html.fragment_fromstring(value, create_parent=True) except XMLSyntaxError: # pragma: no cover. XXX: seems like this exception is # unreachable with create_parent=True, # maybe it should be removed? raise ValidationError(N_(u'Error parsing HTML')) self.cleaner(doc) clean = html.tostring(doc, encoding='utf-8').decode('utf-8') clean = clean.split('>', 1)[1].rsplit('<', 1)[0] return self.Markup(clean)
def __init__(self, min_length, max_length): self.min_length = min_length self.max_length = max_length self.format_args = dict(min=min_length, max=max_length) if min_length == max_length: self.message = M_(u'length of value must be exactly %(max)d symbol', u'length of value must be exactly %(max)d symbols', count_field="max", format_args=self.format_args) else: self.message = N_('length should be between %(min)d and %(max)d symbols')
class ValidationError(Exception): ''' Error raised from inside of `Converter.to_python` or validator function. :param unicode message: error message for current validating field, for most cases. :param dict by_field: contains {field-name: error message} pairs. :param dict format_args: used to format error message. ''' default_message = N_('Something is wrong') def __init__(self, message=None, by_field=None, format_args=None): if not (message or by_field): message = self.default_message self.message = message self.by_field = by_field or {} self.format_args = format_args or {} def translate(self, env, message): format_args = self.format_args if isinstance(message, M_): trans = env.ngettext(message.single, message.plural, message.count) format_args = dict(format_args, **message.format_args) else: trans = env.gettext(message) return trans % format_args def fill_errors(self, field): form = field.form if self.message is not None: form.errors[field.input_name] = self.translate(form.env, self.message) for name, message in self.by_field.items(): if name.startswith('.'): nm, f = name.lstrip('.'), field for i in range(len(name) - len(nm) - 1): f = f.parent rel_field = f.get_field(nm) name = rel_field.input_name else: rel_field = form.get_field(name) form.errors[name] = self.translate(form.env, message) def __repr__(self): return "{}({!r}, {!r})".format(self.__class__, self.message, self.by_field)
def between(min_value, max_value): 'Numerical values limit' message = N_('value should be between %(min)d and %(max)d') % \ dict(min=min_value, max=max_value) @validator(message) def wrapper(conv, value): if value is None: # it meens that this value is not required return True if value < min_value: return False if value > max_value: return False return True return wrapper
class Email(Char): regex = re.compile( # dot-atom r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*" # Although quoted variant is allowed by spec it's actually not used # except by geeks that are looking for problems. But the characters # allowed in quoted string are not safe for HTML and XML, so quoted # e-mail can't be expressed in such formats directly which is quite # common. We prefer to forbid such valid but unsafe e-mails to avoid # security problems. To allow quoted names disable non-text characters # replacement and uncomment the following lines of regexp: ## quoted-string #r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|' # r'\\[\001-011\013\014\016-\177])*"' r')@(?:[A-Z0-9-]+\.)+[A-Z]{2,6}$', re.IGNORECASE) error_regex = N_('incorrect e-mail address')
class BaseDatetime(CharBased): ''' A base class for `Datetime`, `Date` and `Time` converters. ''' #: format used to convert from string by `strptime` #: and to convert to string by `strftime`. format = None #: format used in error message. By default, generated automatically #: based `format` and `replacements` attributes readable_format = None replacements = (('%H', 'HH'), ('%M', 'MM'), ('%d', 'DD'), ('%m', 'MM'), ('%Y', 'YYYY')) #: error message for wrong format. error_wrong_format = N_('Wrong format (%(readable_format)s)') nontext_replacement = '' def __init__(self, *args, **kwargs): if not 'readable_format' in kwargs or 'format' in kwargs: replacements = self.replacements # XXX make language-dependent fmt = kwargs.get('format', self.format) for repl in replacements: fmt = fmt.replace(*repl) kwargs['readable_format'] = fmt Converter.__init__(self, *args, **kwargs) def from_python(self, value): if value is None: return '' # carefull to years before 1900 return strftime(value, self.format) def to_python(self, value): value = self.clean_value(value) if not value: return None try: return self.convert_datetime(value) except ValueError: format_args = dict(readable_format=self.readable_format) raise ValidationError(self.error_wrong_format, format_args=format_args)
class Int(Converter): """ Converts digital sequences to `int` """ #: Error message for the case value can not be converted to int error_notvalid = N_('it is not valid integer') def to_python(self, value): if value == '': return None try: value = int(value) except ValueError: raise ValidationError(self.error_notvalid) return value def from_python(self, value): if value is None: return '' return six.text_type(value)
class Char(CharBased): '''Converts and validates strings''' #: Regexp to match input string regex = None #: Error message for the case self.regex does not match error_regex = N_('field should match %(regex)s') #: Whether strip value before convertation or not strip = True @property def max_length(self): length_validators = [x for x in self.validators if isinstance(x, length)] if length_validators: return min([x.max_length for x in length_validators]) return None def to_python(self, value): # converting value = self.clean_value(value) if value and self.regex: regex = self.regex if isinstance(regex, six.string_types): regex = re.compile(self.regex, re.U) if not regex.match(value): error = self.error_regex % {'regex': self.regex} raise ValidationError(error) return value def from_python(self, value): if value is None: return '' if six.PY3 and isinstance(value, bytes): raise TypeError() # pragma: no cover, safety check return six.text_type(value)
class Converter(object): ''' :meth:`accept` method takes value from field and converts it to python type. Subclasses must redefine :meth:`to_python` method to make their own convertation and validation. :meth:`from_python` method takes value as python object and converts it to string or something else widget can display. Accepts a list of validators as **\*args**. ''' # obsolete parameters from previous versions _obsolete = frozenset(['max_length', 'min_length', 'null', 'min', 'max', 'multiple', 'initial']) #: Flag of whether perform require check after :meth:`to_python` method or not. #: The resulting value is checked to be non-empty (`[]`, `None`). required = False multiple = False #: An ordered list of validator functions. Are passed as position args #: to the converter:: #: #: Int(validator1, validator2) #: #: When a converter is copied, new validators are added to existing ones. validators = () #: Values are not accepted by Required validator error_required = N_('required field') def __init__(self, *args, **kwargs): if self._obsolete & set(kwargs): raise TypeError( 'Obsolete parameters are used: {}'.format( list(self._obsolete & set(kwargs)))) self.field = weakproxy(kwargs.get('field')) self._init_kwargs = kwargs self.__dict__.update(kwargs) self.validators = tuple(self.validators) + args @property def env(self): '''A shortcut for `form.env`''' return self.field.env def _is_empty(self, value): return value in ('', [], {}, None) def accept(self, value, silent=False): ''' Accepts a value from the form, calls :meth:`to_python` method, checks `required` condition, applies filters and validators, catches ValidationError. :param value: a value to be accepted :param silent=False: write errors to `form.errors` or not ''' try: value = self.to_python(value) for v in self.validators: value = v(self, value) if self.required and self._is_empty(value): raise ValidationError(self.error_required) except ValidationError as e: if not silent: e.fill_errors(self.field) #NOTE: by default value for field is in python_data, # but this is not true for FieldList where data # is dynamic, so we set value to None for absent value. value = self._existing_value return value def to_python(self, value): """ Converts value and validates it. Custom converters should override this """ if value == '': return None # XXX is this right? return value def from_python(self, value): """ Serializes value. Custom converters should override this """ if value is None: value = '' return value def __call__(self, *args, **kwargs): ''' Creates current object's copy with extra constructor arguments (including validators) passed. ''' kwargs = dict(self._init_kwargs, **kwargs) kwargs.setdefault('field', self.field) validators = kwargs.pop('validators', self.validators) validators = tuple(validators) + args return self.__class__(*validators, **kwargs) def assert_(self, expression, msg): 'Shortcut for assertions of certain type' if not expression: raise ValidationError(msg) @property def _existing_value(self): if self.field is not None: return self.field.parent.python_data.get(self.field.name) return [] if self.multiple else None def __repr__(self): args = ', '.join([k+'='+repr(v) for k, v in self._init_kwargs.items() if k!='parent']) return '{}({})'.format(self.__class__.__name__, args)
class LoginForm(Form): fields = (Field('login', convs.Char(), label=N_('Username')), Field('password', convs.Char(), widget=widgets.PasswordInput(), label=N_(u'Password')))