diff --git a/itools/database/fields.py b/itools/database/fields.py index 54d66a26d..642112c97 100644 --- a/itools/database/fields.py +++ b/itools/database/fields.py @@ -15,8 +15,9 @@ # along with this program. If not, see . # Import from itools -from itools.core import is_prototype, prototype +from itools.core import is_prototype, merge_dicts, prototype from itools.gettext import MSG +from itools.validators import validator class Field(prototype): @@ -27,10 +28,14 @@ class Field(prototype): indexed = False stored = False multiple = False - error_messages = { + empty_values = (None, '', [], (), {}) + base_error_messages = { 'invalid': MSG(u'Invalid value.'), 'required': MSG(u'This field is required.'), } + error_messages = {} + validators = [] + def get_datatype(self): return self.datatype @@ -41,6 +46,22 @@ def access(self, mode, resource): return True + def get_validators(self): + validators = [] + for v in self.validators: + if type(v) is str: + v = validator(v)() + validators.append(v) + return validators + + + def get_error_message(self, code): + messages = merge_dicts( + self.base_error_messages, + self.error_messages) + return messages.get(code) + + def get_field_and_datatype(elt): """ Now schema can be Datatype or Field. diff --git a/itools/validators/__init__.py b/itools/validators/__init__.py new file mode 100644 index 000000000..de196d0fc --- /dev/null +++ b/itools/validators/__init__.py @@ -0,0 +1,29 @@ +# -*- coding: UTF-8 -*- +# Copyright (C) 2016 Sylvain Taverne +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Import from itools +from base import BaseValidator +from exceptions import ValidationError +from registry import register_validator, validator +import database +import files +import password + +__all__ = [ + 'BaseValidator', + 'ValidationError', + 'register_validator', + 'validator'] diff --git a/itools/validators/base.py b/itools/validators/base.py new file mode 100644 index 000000000..a65376ec1 --- /dev/null +++ b/itools/validators/base.py @@ -0,0 +1,211 @@ +# -*- coding: UTF-8 -*- +# Copyright (C) 2016 Sylvain Taverne +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Import from standard library +import re + +# Import from itools +from itools.core import prototype, prototype_type +from itools.gettext import MSG + +# Import from here +from exceptions import ValidationError +from registry import register_validator + + +class BaseValidatorMetaclass(prototype_type): + + def __new__(mcs, name, bases, dict): + cls = prototype_type.__new__(mcs, name, bases, dict) + if 'validator_id' in dict: + register_validator(cls) + return cls + + +class validator_prototype(prototype): + + __metaclass__ = BaseValidatorMetaclass + + +class BaseValidator(validator_prototype): + + validator_id = None + errors = {'invalid': MSG(u'Enter a valid value')} + + def is_valid(self, value): + try: + self.check(value) + except ValidationError: + return False + return True + + + def check(self, value): + raise NotImplementedError('Validator is not configured') + + + def get_error_msg(self): + return self.msg + + + def raise_default_error(self, kw={}): + code, msg = self.errors.items()[0] + raise ValidationError(msg, code, kw) + + + def raise_errors(self, errors, kw={}): + l = [] + for code in errors: + msg = self.errors[code] + l.append((msg, code, kw)) + raise ValidationError(l) + + + def __call__(self, value): + return self.check(value) + + + +class EqualsValidator(BaseValidator): + + validator_id = 'equals-to' + base_value = None + errors = {'not_equals': MSG(u'The value should be equals to {base_value}')} + + def check(self, value): + if value != self.base_value: + kw = {'base_value': self.base_value} + self.raise_default_error(kw) + + + +class RegexValidator(BaseValidator): + + regex = None + inverse_match = False + + def check(self, value): + value = str(value) + r = re.compile(self.regex, 0) + if bool(r.search(value)) != (not self.inverse_match): + self.raise_default_error() + + + + +class HexadecimalValidator(RegexValidator): + + validator_id = 'hexadecimal' + regex = '^#[A-Fa-f0-9]+$' + errors = {'invalid': MSG(u'Enter a valid value.')} + + + +class PositiveIntegerValidator(BaseValidator): + + validator_id = 'integer-positive' + errors = {'integer_positive': MSG(u'Ensure this value is positive.')} + + def check(self, value): + if value < 0: + kw = {'value': value} + self.raise_default_error(kw) + + + +class PositiveIntegerNotNullValidator(BaseValidator): + + validator_id = 'integer-positive-not-null' + errors = {'integer_positive_not_null': MSG(u'Ensure this value is greater than 0.')} + + def check(self, value): + if value <= 0: + kw = {'value': value} + self.raise_default_error(kw) + + + +class MaxValueValidator(BaseValidator): + + validator_id = 'max-value' + errors = {'max_value': MSG(u'Ensure this value is less than or equal to {max_value}.')} + max_value = None + + def check(self, value): + if value and value > self.max_value: + kw = {'max_value': self.max_value} + self.raise_default_error(kw) + + + +class MinValueValidator(BaseValidator): + + validator_id = 'min-value' + errors = {'min_value': MSG(u'Ensure this value is greater than or equal to {min_value}.')} + min_value = None + + def check(self, value): + if value < self.min_value: + kw = {'min_value': self.min_value} + self.raise_default_error(kw) + + + +class MinMaxValueValidator(BaseValidator): + + validator_id = 'min-max-value' + errors = {'min_max_value': MSG( + u'Ensure this value is greater than or equal to {min_value} ' + u'and value is less than or equal to {max_value}.')} + min_value = None + max_value = None + + def check(self, value): + if value < self.min_value or value > self.max_value: + kw = {'max_value': self.max_value, + 'min_value': self.min_value} + self.raise_default_error(kw) + + + + +class MinLengthValidator(BaseValidator): + + validator_id = 'min-length' + min_length = 0 + errors = {'min_length': MSG(u'Ensure this value has at least {min_length} characters.')} + + def check(self, value): + if len(value) < self.min_length: + kw = {'value': value, + 'size': len(value), + 'min_length': self.min_length} + self.raise_default_error(kw) + + + +class MaxLengthValidator(BaseValidator): + + validator_id = 'max-length' + max_length = 0 + errors = {'max_length': MSG(u'Ensure this value has at most {max_length} characters.')} + + def check(self, value): + if len(value) > self.max_length: + kw = {'value': value, + 'size': len(value), + 'max_length': self.max_length} + self.raise_default_error(kw) diff --git a/itools/validators/database.py b/itools/validators/database.py new file mode 100644 index 000000000..05222bc3e --- /dev/null +++ b/itools/validators/database.py @@ -0,0 +1,47 @@ +# -*- coding: UTF-8 -*- +# Copyright (C) 2016 Sylvain Taverne +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Import from itools +from itools.gettext import MSG + +# Import from here +from base import BaseValidator + + +class UniqueValidator(BaseValidator): + + validator_id = 'unique' + errors = {'unique': MSG(u'The field should be unique.')} + field_name = None + base_query = None + + def check(self, value): + from itools.database import AndQuery, NotQuery + from itools.database import PhraseQuery + if not value: + return + context = self.context + here = context.resource + query = AndQuery( + NotQuery(PhraseQuery('abspath', str(here.abspath))), + PhraseQuery(self.field_name, value)) + if self.base_query: + query.append(self.base_query) + search = context.database.search(query) + nb_results = len(search) + if nb_results > 0: + kw = {'nb_results': nb_results} + self.raise_default_error(kw) diff --git a/itools/validators/exceptions.py b/itools/validators/exceptions.py new file mode 100644 index 000000000..96c319702 --- /dev/null +++ b/itools/validators/exceptions.py @@ -0,0 +1,52 @@ +# -*- coding: UTF-8 -*- +# Copyright (C) 2016 Sylvain Taverne +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Import from itools +from itools.gettext import MSG + + +class ValidationError(Exception): + + errors = [] + + def __init__(self, msg=None, code=None, msg_params=None): + errors = [] + if type(msg) is list: + errors.extend(msg) + else: + errors.append((msg, code, msg_params)) + self.errors = errors + + + def get_messages(self, field): + l = [] + for msg, code, msg_params in self.errors: + field_msg = field.get_error_message(code) if field else None + msg = field_msg or msg + l.append(msg.gettext(**msg_params)) + return l + + + def get_message(self, field=None, mode='html'): + messages = self.get_messages(field) + if mode == 'html': + msg = '
'.join(messages) + return MSG(msg, format='html') + return '\n'.join(messages) + + + def __str__(self): + return self.get_message() diff --git a/itools/validators/files.py b/itools/validators/files.py new file mode 100644 index 000000000..7f0b0c0d9 --- /dev/null +++ b/itools/validators/files.py @@ -0,0 +1,136 @@ +# -*- coding: UTF-8 -*- +# Copyright (C) 2016 Sylvain Taverne +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Import from standard library +from cStringIO import StringIO + +# Import from PIL +from PIL import Image as PILImage + +# Import from itools +from itools.gettext import MSG + +# Import from here +from base import BaseValidator +from exceptions import ValidationError + + + +class FileExtensionValidator(BaseValidator): + + validator_id = 'file-extension' + allowed_extensions = [] + errors = {'invalid_extension': MSG( + u"File extension '{extension}' is not allowed. " + u"Allowed extensions are: '{allowed_extensions}'.")} + + + def check(self, value): + extension = self.get_extension(value) + if extension not in self.allowed_extensions: + kw = {'extension': extension, + 'allowed_extensions': ','.join(self.allowed_extensions)} + self.raise_default_error(kw) + + + def get_extension(self, value): + filename, mimetype, body = value + return filename.split('.')[-1] + + + +class ImageExtensionValidator(FileExtensionValidator): + + validator_id = 'image-extension' + allowed_extensions = ['jpeg', 'png', 'gif'] + + + +class MimetypesValidator(BaseValidator): + + validator_id = 'file-mimetypes' + allowed_mimetypes = [] + errors = {'bad_mimetype': MSG( + u"File mimetype '{mimetype}' is not allowed. " + u"Allowed mimetypes are: '{allowed_mimetypes}'.")} + + + def check(self, value): + filename, mimetype, body = value + if mimetype not in self.allowed_mimetypes: + kw = {'mimetype': mimetype, + 'allowed_mimetypes': ','.join(self.allowed_mimetypes)} + self.raise_default_error(kw) + + + +class ImageMimetypesValidator(MimetypesValidator): + + validator_id = 'image-mimetypes' + allowed_mimetypes = ['image/jpeg', 'image/png', 'image/gif'] + + + +class FileSizeValidator(BaseValidator): + + validator_id = 'file-size' + max_size = 1024*1024*10 + errors = {'too_big': MSG(u'Your file is too big. ({size})')} + + def check(self, value): + filename, mimetype, body = value + size = len(body) + if size > self.max_size: + kw = {'size': self.pretty_bytes(size), + 'max_size': self.pretty_bytes(self.max_size)} + self.raise_default_error(kw) + + + def pretty_bytes(self, b): + # 1 Byte = 8 Bits + # 1 Kilobyte = 1024 Bytes + # 1 Megabyte = 1048576 Bytes + # 1 Gigabyte = 1073741824 Bytes + if b < 1024: + return u'%.01f Bytes' % b + elif b < 1048576: + return u'%.01f KB' % (b / 1024) + elif b < 1073741824: + return u'%.01f MB' % (b / 1048576) + return u'%.01f GB' % (b / 1073741824) + + + +class ImagePixelsValidator(BaseValidator): + + validator_id = 'image-pixels' + max_pixels = 2000*2000 + + errors = {'too_much_pixels': MSG(u"Image is too big."), + 'image_has_errors': MSG(u"Image contains errors.")} + + def check(self, value): + filename, mimetype, body = value + data = StringIO(body) + try: + im = PILImage.open(data) + im.verify() + except Exception: + code = 'image_has_errors' + raise ValidationError(self.errors[code], code, {}) + if im.width * im.height > self.max_pixels: + code = 'too_much_pixels' + raise ValidationError(self.errors[code], code, {}) diff --git a/itools/validators/password.py b/itools/validators/password.py new file mode 100644 index 000000000..e655a959e --- /dev/null +++ b/itools/validators/password.py @@ -0,0 +1,65 @@ +# -*- coding: UTF-8 -*- +# Copyright (C) 2016 Sylvain Taverne +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Import from standard library +from string import ascii_letters, digits + +# Import from itools +from itools.gettext import MSG + +# Import from here +from base import BaseValidator + + +class StrongPasswordValidator(BaseValidator): + """ + at least 5 characters + at least one character (a,b,c...) + at least one special character ( *?./+#!,;:=) + at least a number (1, 2, 3, ...)" + """ + + validator_id = 'strong-password' + min_length = 5 + + errors = { + 'too_short': MSG(u"This password is too short. It must contain at least {min_length} characters."), + 'need_character': MSG(u"This password should contains at least one character."), + 'need_number': MSG(u"This password should contains at least one number."), + 'need_special_character': MSG(u"This password should contains at least one special character."), + } + + def check(self, value): + errors = [] + if len(value) < self.min_length: + errors.append('too_short') + has_letter = has_digit = has_special = False + for c in value: + if c in ascii_letters: + has_letter = True + elif c in digits: + has_digit = True + else: + has_special = True + if not has_letter: + errors.append('need_character') + if not has_digit: + errors.append('need_number') + if not has_special: + errors.append('need_special_character') + if errors: + kw = {'min_length': self.min_length} + self.raise_errors(errors, kw) diff --git a/itools/validators/registry.py b/itools/validators/registry.py new file mode 100644 index 000000000..c6b6421af --- /dev/null +++ b/itools/validators/registry.py @@ -0,0 +1,25 @@ +# -*- coding: UTF-8 -*- +# Copyright (C) 2016 Sylvain Taverne +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +validators_registry = {} + +def register_validator(cls): + validators_registry[cls.validator_id] = cls + + +def validator(name, **kw): + return validators_registry[name](**kw)() diff --git a/itools/validators/test_rest.py b/itools/validators/test_rest.py new file mode 100644 index 000000000..ec3031e2c --- /dev/null +++ b/itools/validators/test_rest.py @@ -0,0 +1,14 @@ + +from httplib2 import Http +import json +from pprint import pprint + +uri = 'http://ikaaro.agicia.net/;test_validators' +h = Http() +headers = {'Content-type': 'application/json'} +body = {'field_1': 5} +body = json.dumps(body) +resp, content = h.request(uri, "POST", headers=headers, body=body) +data = json.loads(content) +pprint(data) + diff --git a/itools/validators/test_view.py b/itools/validators/test_view.py new file mode 100644 index 000000000..4f09d961a --- /dev/null +++ b/itools/validators/test_view.py @@ -0,0 +1,94 @@ +# -*- coding: UTF-8 -*- +# Copyright (C) 2016 Sylvain Taverne +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Import from itools +from itools.gettext import MSG +from itools.validators import validator + +# Import from ikaaro +from ikaaro.autoedit import AutoEdit +from ikaaro.fields import Char_Field, Integer_Field, Email_Field, File_Field + + +class TestValidators(AutoEdit): + + access = True + title = MSG(u"Test validators") + + fields = ['field_1', 'field_2', 'field_3', 'field_4', 'field_5', 'field_6', + 'field_7', 'field_8', 'field_9', 'field_10', 'field_11', 'field_12', + 'field_13', 'field_14', 'field_15'] + + field_1 = Integer_Field( + title=MSG(u'5+5 equals to ?'), + validators=[validator('equals-to', base_value=10)], + error_messages={'not_equals': MSG(u'Give me a 10 ;)')} + ) + field_2 = Char_Field( + title=MSG(u'Hexadecimal color'), + validators=[validator('hexadecimal')]) + field_3 = Integer_Field( + title=MSG(u'Give a positive number'), + validators=[validator('integer-positive')]) + field_4 = Integer_Field( + title=MSG(u'Give a strict positive number'), + validators=[validator('integer-positive-not-null')]) + field_5 = Integer_Field( + title=MSG(u'Give a number (max value 10)'), + validators=[validator('max-value', max_value=10)]) + field_6 = Integer_Field( + title=MSG(u'Give a number (min value 10)'), + validators=[validator('min-value', min_value=10)]) + field_7 = Integer_Field( + title=MSG(u'Give a number (>=10 and <=20)'), + validators=[validator('min-max-value', min_value=10, max_value=20)]) + field_8 = Char_Field( + title=MSG(u'Give text (min length: 3 characters)'), + validators=[validator('min-length', min_length=3)]) + field_9 = Char_Field( + title=MSG(u'Give text (max length: 5 characters)'), + validators=[validator('max-length', max_length=5)]) + field_10 = Email_Field( + title=MSG(u'Give an email (unique in DB)'), + validators=[validator('unique', field_name='email')], + error_messages={'invalid': MSG(u'Give be an email address !!!'), + 'unique': MSG(u'This address is already used')}) + field_11 = File_Field( + title=MSG(u'File extension (png)'), + validators=[validator('file-extension', allowed_extensions=['png'])]) + field_12 = File_Field( + title=MSG(u'File mimetypes (image/png)'), + validators=[validator('file-mimetypes', allowed_mimetypes=['image/png'])]) + field_13 = File_Field( + title=MSG(u'Image max pixels'), + validators=[validator('image-pixels', max_pixels=10*10)]) + field_14 = Char_Field( + title=MSG(u'Strong password'), + validators=[validator('strong-password')]) + field_15 = Integer_Field( + title=MSG(u'Number >=5 and equals to 10'), + validators=[ + validator('min-value', min_value=5), + validator('equals-to', base_value=10), + ]) + + + def _get_datatype(self, resource, context, name): + field = self.get_field(resource, name) + return field(resource=resource) + + def action(self, resource, context, form): + print form diff --git a/itools/web/context.py b/itools/web/context.py index 784671cda..712fe8fd5 100644 --- a/itools/web/context.py +++ b/itools/web/context.py @@ -42,6 +42,7 @@ from itools.i18n import format_datetime, format_date, format_time from itools.log import Logger, log_error, log_warning from itools.uri import decode_query, get_reference, Path, Reference +from itools.validators import ValidationError # Local imports from entities import Entity @@ -1108,8 +1109,8 @@ def _get_form_value(form, name, type=String, default=None): default = datatype.get_default() # Errors - required_msg = field.error_messages['required'] - invalid_msg = field.error_messages['invalid'] + required_msg = field.get_error_message('required') + invalid_msg = field.get_error_message('invalid') # Missing is_mandatory = getattr(datatype, 'mandatory', False) @@ -1162,19 +1163,36 @@ def _get_form_value(form, name, type=String, default=None): return value +def check_form_value(field, value): + if value in field.empty_values: + return + errors = [] + for validator in field.get_validators(): + validator = validator(title=field.title, context=context) + try: + validator.check(value) + except ValidationError, e: + errors.extend(e.get_messages(field)) + if errors: + raise FormError(messages=errors, invalid=True) + + def get_form_value(form, name, type=String, default=None): + field, datatype = get_field_and_datatype(type) # Not multilingual is_multilingual = getattr(type, 'multilingual', False) if is_multilingual is False: - return _get_form_value(form, name, type, default) - + value = _get_form_value(form, name, type, default) + check_form_value(field, value) + return value # Multilingual values = {} for key, value in form.iteritems(): if key.startswith('%s:' % name): x, lang = key.split(':', 1) - values[lang] = _get_form_value(form, key, type, default) - + value =_get_form_value(form, key, type, default) + values[lang] = value + check_form_value(field, values) return values diff --git a/itools/web/exceptions.py b/itools/web/exceptions.py index 601edfa7a..121d54608 100644 --- a/itools/web/exceptions.py +++ b/itools/web/exceptions.py @@ -105,21 +105,33 @@ def __init__(self, message=None, missing=False, invalid=False, self.invalids = invalids self.messages = messages - - def get_message(self): + def get_messages(self): # Custom message - value = self.msg - if value is not None: - if is_prototype(value, MSG): - return value - return ERROR(value) - # Default message - msg = u'There are errors... XXX' - return ERROR(msg) + final_messages = [] + messages = [] + if self.messages: + messages = self.messages + elif self.msg: + messages = [self.msg] + else: + messages = MSG(u'There are errors... XXX') + for value in messages: + if not is_prototype(value, MSG): + value = ERROR(value) + final_messages.append(value(format='replace').gettext()) + return final_messages + + + def get_message(self, mode='html'): + messages = self.get_messages() + if mode == 'html': + msg = '
'.join(messages) + return ERROR(msg, format='html') + return '\n'.join(messages) def __str__(self): - return self.get_message().gettext() + return self.get_message(mode='text').gettext() def to_dict(self): diff --git a/itools/web/views.py b/itools/web/views.py index 37768e4cf..246dad8e2 100644 --- a/itools/web/views.py +++ b/itools/web/views.py @@ -40,9 +40,10 @@ -def process_form(get_value, schema): +def process_form(get_value, schema, error_msg=None): missings = [] invalids = [] + unknow = [] values = {} for name in schema: datatype = schema[name] @@ -53,9 +54,12 @@ def process_form(get_value, schema): missings.append(name) elif e.invalid: invalids.append(name) - if missings or invalids: + else: + unknow.append(name) + if missings or invalids or unknow: + error_msg = error_msg or ERROR(u'Form values are invalid') raise FormError( - message=ERROR(u'There are errors, check below.'), + message=error_msg, missing=len(missings)>0, invalid=len(invalids)>0, missings=missings, @@ -168,6 +172,7 @@ def get_schema(self, resource, context): return self.schema + form_error_message = ERROR(u'There are errors, check below') def _get_form(self, resource, context): """Form checks the request form and collect inputs consider the schema. This method also checks the request form and raise an @@ -180,7 +185,7 @@ def _get_form(self, resource, context): """ get_value = context.get_form_value schema = self.get_schema(resource, context) - return process_form(get_value, schema) + return process_form(get_value, schema, self.form_error_message) def get_value(self, resource, context, name, datatype): diff --git a/setup.conf b/setup.conf index c6d66844e..7fc2b1162 100644 --- a/setup.conf +++ b/setup.conf @@ -35,8 +35,8 @@ classifiers = " # Packages package_root = itools packages = "abnf core csv database datatypes fs gettext handlers html i18n ical - log loop odf office pdf pkg python relaxng rss srx stl tmx uri web workflow - xliff xml xmlfile" + log loop odf office pdf pkg python relaxng rss srx stl tmx uri validators web + workflow xliff xml xmlfile" # Requires requires = "reportlab(>=2.3)" diff --git a/test/test.py b/test/test.py index e22baf711..7dcab89c8 100644 --- a/test/test.py +++ b/test/test.py @@ -38,6 +38,7 @@ import test_tmx import test_uri import test_fs +import test_validators import test_web import test_workflow import test_xliff @@ -47,7 +48,8 @@ test_modules = [test_abnf, test_core, test_csv, test_database, test_datatypes, test_gettext, test_handlers, test_html, test_i18n, test_ical, test_odf, test_pdf, test_rss, test_srx, test_stl, test_tmx, test_uri, test_fs, - test_web, test_workflow, test_xliff, test_xml, test_xmlfile] + test_validators, test_web, test_workflow, test_xliff, test_xml, + test_xmlfile] loader = TestLoader() diff --git a/test/test_validators.py b/test/test_validators.py new file mode 100644 index 000000000..29e721e98 --- /dev/null +++ b/test/test_validators.py @@ -0,0 +1,67 @@ +# -*- coding: UTF-8 -*- +# Copyright (C) 2016 Sylvain Taverne +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Import from the Standard Library +from unittest import TestCase, main + +# Import from itools +from itools.validators import validator + + +class ValidatorsTestCase(TestCase): + + + def test_hexadecimal(self): + v = validator('hexadecimal') + self.assertEqual(True, v.is_valid('#000000')) + + + def test_equals(self): + v = validator('equals-to', base_value=2) + self.assertEqual(True, v.is_valid(2)) + self.assertEqual(False, v.is_valid(3)) + + + def test_integer(self): + v = validator('integer') + self.assertEqual(True, v.is_valid(2)) + self.assertEqual(False, v.is_valid("a")) + + + def test_integer_positive(self): + v = validator('integer-positive') + self.assertEqual(True, v.is_valid(0)) + self.assertEqual(True, v.is_valid(2)) + self.assertEqual(False, v.is_valid(-1)) + + + def test_integer_positive_not_null(self): + v = validator('integer-positive-not-null') + self.assertEqual(True, v.is_valid(2)) + self.assertEqual(False, v.is_valid(-1)) + self.assertEqual(False, v.is_valid(0)) + + + def test_image_mimetypes(self): + v = validator('image-mimetypes') + image1 = 'image.png', 'image/png', None + image2 = 'image.png', 'application/xml', None + self.assertEqual(True, v.is_valid(image1)) + self.assertEqual(False, v.is_valid(image2)) + + +if __name__ == '__main__': + main()