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()