From 85403a0784f752cde26400353e44121807a93624 Mon Sep 17 00:00:00 2001 From: Taverne Sylvain Date: Fri, 25 Nov 2016 11:28:30 +0100 Subject: [PATCH 01/10] WIP: Works on validators (WIP) --- itools/database/fields.py | 11 ++ itools/validators/__init__.py | 28 +++++ itools/validators/base.py | 205 ++++++++++++++++++++++++++++++++ itools/validators/database.py | 39 ++++++ itools/validators/exceptions.py | 44 +++++++ itools/validators/files.py | 117 ++++++++++++++++++ itools/validators/password.py | 50 ++++++++ itools/validators/registry.py | 27 +++++ itools/web/context.py | 23 +++- itools/web/views.py | 11 +- setup.conf | 4 +- test/test.py | 4 +- test/test_validators.py | 67 +++++++++++ 13 files changed, 620 insertions(+), 10 deletions(-) create mode 100644 itools/validators/__init__.py create mode 100644 itools/validators/base.py create mode 100644 itools/validators/database.py create mode 100644 itools/validators/exceptions.py create mode 100644 itools/validators/files.py create mode 100644 itools/validators/password.py create mode 100644 itools/validators/registry.py create mode 100644 test/test_validators.py diff --git a/itools/database/fields.py b/itools/database/fields.py index 54d66a26d..7850f30cd 100644 --- a/itools/database/fields.py +++ b/itools/database/fields.py @@ -17,6 +17,7 @@ # Import from itools from itools.core import is_prototype, prototype from itools.gettext import MSG +from itools.validators import validator class Field(prototype): @@ -31,6 +32,8 @@ class Field(prototype): 'invalid': MSG(u'Invalid value.'), 'required': MSG(u'This field is required.'), } + validators = [] + def get_datatype(self): return self.datatype @@ -41,6 +44,14 @@ 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_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..556b6e5f8 --- /dev/null +++ b/itools/validators/__init__.py @@ -0,0 +1,28 @@ +# -*- 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 files +import database + +__all__ = [ + 'BaseValidator', + 'ValidationError', + 'register_validator', + 'validator'] diff --git a/itools/validators/base.py b/itools/validators/base.py new file mode 100644 index 000000000..ac5021b2a --- /dev/null +++ b/itools/validators/base.py @@ -0,0 +1,205 @@ +# -*- 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=None): + code, msg = self.errors.items()[0] + raise ValidationError(msg, code, kw) + + + 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 IntegerValidator(RegexValidator): + + validator_id = 'integer' + regex = '^-?\d+\Z' + errors = {'valid-integer': MSG(u'Enter a valid integer.')} + + + +class PositiveIntegerValidator(BaseValidator): + + validator_id = 'integer-positive' + errors = {'integer-positive': MSG(u'Positiver XXX')} + + 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'XXX')} + + 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 > 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} + self.raise_default_error(kw) + + + + +class MinLengthValidator(BaseValidator): + + validator_id = 'min-length' + min_length = 0 + errors = {'min_length': MSG(u'Error')} + + def check(self, value): + if len(value) < self.min_length: + kw = {'value': 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'Error')} + + def check(self, value): + if len(value) > self.max_length: + kw = {'value': value, 'max_length': self.max_length} + self.raise_default_error(kw) + + + +class HexadecimalValidator(RegexValidator): + + validator_id = 'hexadecimal' + regex = '^#[A-Fa-f0-9]+$' + errors = {'invalid': MSG(u'Hexa invalide')} diff --git a/itools/validators/database.py b/itools/validators/database.py new file mode 100644 index 000000000..21a074ded --- /dev/null +++ b/itools/validators/database.py @@ -0,0 +1,39 @@ +# -*- 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 {nb_results} should be unique')} + + def check(self, value): + from itools.database import AndQuery, NotQuery + from itools.database import PhraseQuery + query = AndQuery( + NotQuery(PhraseQuery('abspath', str(self.resource.abspath))), + PhraseQuery(self.field_name, value)) + search = self.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..12b9b1567 --- /dev/null +++ b/itools/validators/exceptions.py @@ -0,0 +1,44 @@ +# -*- 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 . + + +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): + l = [] + for msg, code, msg_params in self.errors: + l.append(msg.gettext(**msg_params)) + return l + + + def get_message(self): + messages = self.get_messages() + 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..afb27f31f --- /dev/null +++ b/itools/validators/files.py @@ -0,0 +1,117 @@ +# -*- 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 = {'value': value} + 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 = 'mimetypes' + authorized_mimetypes = [] + errors = {'bad_mimetype': MSG(u"XXX")} + + + def check(self, value): + filename, mimetype, body = value + if mimetype not in self.authorized_mimetypes: + kw = {'value': value} + self.raise_default_error(kw) + + + +class ImageMimetypesValidator(MimetypesValidator): + + validator_id = 'image-mimetypes' + authorized_mimetypes = ['image/jpeg', 'image/png', 'image/gif'] + + + +class FileSizeValidator(BaseValidator): + + validator_id = 'file-size' + max_size = 1024*1024*10 + errors = {'too_big': MSG(u'XXX')} + + def check(self, value): + filename, mimetype, body = value + size = len(body) + if size > self.max_size: + kw = {'size': size} + self.raise_default_error(kw) + + + +class ImagePixelsValidator(BaseValidator): + + validator_id = 'image-pixels' + max_pixels = 2000*2000 + + errors = {'too_much_pixels': MSG(u"L'image est trop grande."), + 'image_has_errors': MSG(u"L'image contient des erreurs")} + + 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(code, self.errors[code], {}) + if im.width * im.height > self.max_pixels: + code = 'too_much_pixels' + raise ValidationError(code, self.errors[code], {}) diff --git a/itools/validators/password.py b/itools/validators/password.py new file mode 100644 index 000000000..c3cbdafd1 --- /dev/null +++ b/itools/validators/password.py @@ -0,0 +1,50 @@ +# -*- 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): + """ + au minimum un caractère spécial ( *?./+#!,;:=) + at least one special character ( *?./+#!,;:=) + at least a number (1, 2, 3, ...)" + """ + min_length = 8 + + errors = { + 'too_short': MSG(u"This password is too short. It must contain at least {min_length} characters.") + } + help_msg = MSG(u"Your password must contain at least {min_length} characters.") + + def check(self, value): + 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 or not has_digit or not has_special: + self.raise_default_error() diff --git a/itools/validators/registry.py b/itools/validators/registry.py new file mode 100644 index 000000000..73fc9a104 --- /dev/null +++ b/itools/validators/registry.py @@ -0,0 +1,27 @@ +# -*- 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/web/context.py b/itools/web/context.py index 784671cda..50d872b52 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 @@ -1162,19 +1163,33 @@ def _get_form_value(form, name, type=String, default=None): return value +def check_form_value(field, value): + for validator in field.get_validators(): + validator = validator( + title=field.title, context=context) + try: + validator.check(value) + except ValidationError, e: + msg = e.get_message() + raise FormError(msg, 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/views.py b/itools/web/views.py index 37768e4cf..0fdf87c15 100644 --- a/itools/web/views.py +++ b/itools/web/views.py @@ -40,7 +40,8 @@ -def process_form(get_value, schema): +def process_form(get_value, schema, error_msg=None): + messages = [] missings = [] invalids = [] values = {} @@ -49,13 +50,16 @@ def process_form(get_value, schema): try: values[name] = get_value(name, type=datatype) except FormError, e: + messages.append(e.get_message()) if e.missing: missings.append(name) elif e.invalid: invalids.append(name) if missings or invalids: + error_msg = error_msg or ERROR(u'Form values are invalid') raise FormError( - message=ERROR(u'There are errors, check below.'), + message=error_msg, + messages=messages, 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() From 75a89dac0b922213f387a7a8e13c957d4d513bc7 Mon Sep 17 00:00:00 2001 From: Taverne Sylvain Date: Thu, 1 Dec 2016 11:02:22 +0100 Subject: [PATCH 02/10] Works on validators --- itools/database/fields.py | 13 +++++- itools/validators/base.py | 47 +++++++++---------- itools/validators/database.py | 11 +++-- itools/validators/exceptions.py | 8 ++-- itools/validators/files.py | 51 +++++++++++++------- itools/validators/password.py | 2 +- itools/validators/registry.py | 2 - itools/validators/test_view.py | 83 +++++++++++++++++++++++++++++++++ itools/web/context.py | 12 +++-- 9 files changed, 172 insertions(+), 57 deletions(-) create mode 100644 itools/validators/test_view.py diff --git a/itools/database/fields.py b/itools/database/fields.py index 7850f30cd..bac70d3a0 100644 --- a/itools/database/fields.py +++ b/itools/database/fields.py @@ -15,7 +15,7 @@ # 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 @@ -28,10 +28,11 @@ class Field(prototype): indexed = False stored = False multiple = False - error_messages = { + base_error_messages = { 'invalid': MSG(u'Invalid value.'), 'required': MSG(u'This field is required.'), } + error_messages = {} validators = [] @@ -53,6 +54,14 @@ def get_validators(self): 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. To be compatible: diff --git a/itools/validators/base.py b/itools/validators/base.py index ac5021b2a..cdf23a4db 100644 --- a/itools/validators/base.py +++ b/itools/validators/base.py @@ -61,7 +61,7 @@ def get_error_msg(self): return self.msg - def raise_default_error(self, kw=None): + def raise_default_error(self, kw={}): code, msg = self.errors.items()[0] raise ValidationError(msg, code, kw) @@ -97,18 +97,18 @@ def check(self, value): -class IntegerValidator(RegexValidator): - validator_id = 'integer' - regex = '^-?\d+\Z' - errors = {'valid-integer': MSG(u'Enter a valid integer.')} +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'Positiver XXX')} + errors = {'integer-positive': MSG(u'Ensure this value is positive.')} def check(self, value): if value < 0: @@ -120,10 +120,10 @@ def check(self, value): class PositiveIntegerNotNullValidator(BaseValidator): validator_id = 'integer-positive-not-null' - errors = {'integer-positive-not-null': MSG(u'XXX')} + errors = {'integer-positive-not-null': MSG(u'Ensure this value is greater than 0.')} def check(self, value): - if value <= 0: + if value and value <= 0: kw = {'value': value} self.raise_default_error(kw) @@ -132,11 +132,11 @@ def check(self, value): class MaxValueValidator(BaseValidator): validator_id = 'max-value' - errors = {'max-value': MSG(u'Ensure this value is less than or equal to {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 > self.max_value: + if value and value > self.max_value: kw = {'max_value': self.max_value} self.raise_default_error(kw) @@ -158,15 +158,16 @@ def check(self, value): 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}.' + 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} + kw = {'max_value': self.max_value, + 'min_value': self.min_value} self.raise_default_error(kw) @@ -176,11 +177,13 @@ class MinLengthValidator(BaseValidator): validator_id = 'min-length' min_length = 0 - errors = {'min_length': MSG(u'Error')} + 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, 'min_length': self.min_length} + kw = {'value': value, + 'size': len(value), + 'min_length': self.min_length} self.raise_default_error(kw) @@ -189,17 +192,11 @@ class MaxLengthValidator(BaseValidator): validator_id = 'max-length' max_length = 0 - errors = {'max_length': MSG(u'Error')} + 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, 'max_length': self.max_length} + kw = {'value': value, + 'size': len(value), + 'max_length': self.max_length} self.raise_default_error(kw) - - - -class HexadecimalValidator(RegexValidator): - - validator_id = 'hexadecimal' - regex = '^#[A-Fa-f0-9]+$' - errors = {'invalid': MSG(u'Hexa invalide')} diff --git a/itools/validators/database.py b/itools/validators/database.py index 21a074ded..481163637 100644 --- a/itools/validators/database.py +++ b/itools/validators/database.py @@ -24,15 +24,20 @@ class UniqueValidator(BaseValidator): validator_id = 'unique' - errors = {'unique': MSG(u'The field {nb_results} should be unique')} + errors = {'unique': MSG(u'The field should be unique.')} + field_name = 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(self.resource.abspath))), + NotQuery(PhraseQuery('abspath', str(here.abspath))), PhraseQuery(self.field_name, value)) - search = self.context.database.search(query) + search = context.database.search(query) nb_results = len(search) if nb_results > 0: kw = {'nb_results': nb_results} diff --git a/itools/validators/exceptions.py b/itools/validators/exceptions.py index 12b9b1567..3b8b10910 100644 --- a/itools/validators/exceptions.py +++ b/itools/validators/exceptions.py @@ -28,15 +28,17 @@ def __init__(self, msg=None, code=None, msg_params=None): self.errors = errors - def get_messages(self): + 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): - messages = self.get_messages() + def get_message(self, field=None): + messages = self.get_messages(field) return '\n'.join(messages) diff --git a/itools/validators/files.py b/itools/validators/files.py index afb27f31f..a79d65e04 100644 --- a/itools/validators/files.py +++ b/itools/validators/files.py @@ -33,7 +33,7 @@ class FileExtensionValidator(BaseValidator): validator_id = 'file-extension' allowed_extensions = [] - errors = {'invalid_extension': MSG( + errors = {'invalid-extension': MSG( u"File extension '{extension}' is not allowed. " u"Allowed extensions are: '{allowed_extensions}'.")} @@ -41,7 +41,8 @@ class FileExtensionValidator(BaseValidator): def check(self, value): extension = self.get_extension(value) if extension not in self.allowed_extensions: - kw = {'value': value} + kw = {'extension': extension, + 'allowed_extensions': ','.join(self.allowed_extensions)} self.raise_default_error(kw) @@ -60,15 +61,18 @@ class ImageExtensionValidator(FileExtensionValidator): class MimetypesValidator(BaseValidator): - validator_id = 'mimetypes' - authorized_mimetypes = [] - errors = {'bad_mimetype': MSG(u"XXX")} + 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.authorized_mimetypes: - kw = {'value': value} + if mimetype not in self.allowed_mimetypes: + kw = {'mimetype': mimetype, + 'allowed_mimetypes': ','.join(self.allowed_mimetypes)} self.raise_default_error(kw) @@ -76,7 +80,7 @@ def check(self, value): class ImageMimetypesValidator(MimetypesValidator): validator_id = 'image-mimetypes' - authorized_mimetypes = ['image/jpeg', 'image/png', 'image/gif'] + allowed_mimetypes = ['image/jpeg', 'image/png', 'image/gif'] @@ -84,24 +88,39 @@ class FileSizeValidator(BaseValidator): validator_id = 'file-size' max_size = 1024*1024*10 - errors = {'too_big': MSG(u'XXX')} + 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': 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"L'image est trop grande."), - 'image_has_errors': MSG(u"L'image contient des erreurs")} + 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 @@ -110,8 +129,8 @@ def check(self, value): im = PILImage.open(data) im.verify() except Exception: - code = 'image_has_errors' - raise ValidationError(code, self.errors[code], {}) + code = 'image-has-errors' + raise ValidationError(self.errors[code], code, {}) if im.width * im.height > self.max_pixels: - code = 'too_much_pixels' - raise ValidationError(code, self.errors[code], {}) + code = 'too-much-pixels' + raise ValidationError(self.errors[code], code, {}) diff --git a/itools/validators/password.py b/itools/validators/password.py index c3cbdafd1..98fa3cdf8 100644 --- a/itools/validators/password.py +++ b/itools/validators/password.py @@ -33,7 +33,7 @@ class StrongPasswordValidator(BaseValidator): min_length = 8 errors = { - 'too_short': MSG(u"This password is too short. It must contain at least {min_length} characters.") + 'too-short': MSG(u"This password is too short. It must contain at least {min_length} characters.") } help_msg = MSG(u"Your password must contain at least {min_length} characters.") diff --git a/itools/validators/registry.py b/itools/validators/registry.py index 73fc9a104..c6b6421af 100644 --- a/itools/validators/registry.py +++ b/itools/validators/registry.py @@ -15,8 +15,6 @@ # along with this program. If not, see . - - validators_registry = {} def register_validator(cls): diff --git a/itools/validators/test_view.py b/itools/validators/test_view.py new file mode 100644 index 000000000..34806c0c3 --- /dev/null +++ b/itools/validators/test_view.py @@ -0,0 +1,83 @@ +# -*- 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_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 a number (min length: 3 characters)'), + validators=[validator('min-length', min_length=3)]) + field_9 = Char_Field( + title=MSG(u'Give a number (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')]) + 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)]) + + + 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 50d872b52..993395c72 100644 --- a/itools/web/context.py +++ b/itools/web/context.py @@ -1109,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) @@ -1170,7 +1170,7 @@ def check_form_value(field, value): try: validator.check(value) except ValidationError, e: - msg = e.get_message() + msg = e.get_message(field) raise FormError(msg, invalid=True) @@ -1180,7 +1180,8 @@ def get_form_value(form, name, type=String, default=None): is_multilingual = getattr(type, 'multilingual', False) if is_multilingual is False: value = _get_form_value(form, name, type, default) - check_form_value(field, value) + if value is not None: + check_form_value(field, value) return value # Multilingual values = {} @@ -1189,7 +1190,8 @@ def get_form_value(form, name, type=String, default=None): x, lang = key.split(':', 1) value =_get_form_value(form, key, type, default) values[lang] = value - check_form_value(field, values) + if value is not None: + check_form_value(field, values) return values From 7e7384b6b72ef6354b5131201e0e13ccf9d6c452 Mon Sep 17 00:00:00 2001 From: Taverne Sylvain Date: Thu, 1 Dec 2016 14:18:03 +0100 Subject: [PATCH 03/10] Validators: Improve test view --- itools/validators/test_view.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/itools/validators/test_view.py b/itools/validators/test_view.py index 34806c0c3..e1f603447 100644 --- a/itools/validators/test_view.py +++ b/itools/validators/test_view.py @@ -63,7 +63,9 @@ class TestValidators(AutoEdit): 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')]) + 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'])]) From b171a9cd1950df1d5208836ab4adf81a0c774f7b Mon Sep 17 00:00:00 2001 From: Taverne Sylvain Date: Thu, 1 Dec 2016 15:55:22 +0100 Subject: [PATCH 04/10] Validators: Fix various bugs --- itools/database/fields.py | 1 + itools/validators/base.py | 3 ++- itools/validators/test_view.py | 4 ++-- itools/web/context.py | 8 ++++---- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/itools/database/fields.py b/itools/database/fields.py index bac70d3a0..642112c97 100644 --- a/itools/database/fields.py +++ b/itools/database/fields.py @@ -28,6 +28,7 @@ class Field(prototype): indexed = False stored = False multiple = False + empty_values = (None, '', [], (), {}) base_error_messages = { 'invalid': MSG(u'Invalid value.'), 'required': MSG(u'This field is required.'), diff --git a/itools/validators/base.py b/itools/validators/base.py index cdf23a4db..0a43ee996 100644 --- a/itools/validators/base.py +++ b/itools/validators/base.py @@ -105,6 +105,7 @@ class HexadecimalValidator(RegexValidator): errors = {'invalid': MSG(u'Enter a valid value.')} + class PositiveIntegerValidator(BaseValidator): validator_id = 'integer-positive' @@ -123,7 +124,7 @@ class PositiveIntegerNotNullValidator(BaseValidator): errors = {'integer-positive-not-null': MSG(u'Ensure this value is greater than 0.')} def check(self, value): - if value and value <= 0: + if value <= 0: kw = {'value': value} self.raise_default_error(kw) diff --git a/itools/validators/test_view.py b/itools/validators/test_view.py index e1f603447..2306b12eb 100644 --- a/itools/validators/test_view.py +++ b/itools/validators/test_view.py @@ -56,10 +56,10 @@ class TestValidators(AutoEdit): 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 a number (min length: 3 characters)'), + title=MSG(u'Give text (min length: 3 characters)'), validators=[validator('min-length', min_length=3)]) field_9 = Char_Field( - title=MSG(u'Give a number (max length: 5 characters)'), + 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)'), diff --git a/itools/web/context.py b/itools/web/context.py index 993395c72..c7db0cd9f 100644 --- a/itools/web/context.py +++ b/itools/web/context.py @@ -1164,6 +1164,8 @@ def _get_form_value(form, name, type=String, default=None): def check_form_value(field, value): + if value in field.empty_values: + return for validator in field.get_validators(): validator = validator( title=field.title, context=context) @@ -1180,8 +1182,7 @@ def get_form_value(form, name, type=String, default=None): is_multilingual = getattr(type, 'multilingual', False) if is_multilingual is False: value = _get_form_value(form, name, type, default) - if value is not None: - check_form_value(field, value) + check_form_value(field, value) return value # Multilingual values = {} @@ -1190,8 +1191,7 @@ def get_form_value(form, name, type=String, default=None): x, lang = key.split(':', 1) value =_get_form_value(form, key, type, default) values[lang] = value - if value is not None: - check_form_value(field, values) + check_form_value(field, values) return values From 05c26a2b3432e71ea78b2b656bf5ec54861a3012 Mon Sep 17 00:00:00 2001 From: Taverne Sylvain Date: Thu, 1 Dec 2016 16:19:00 +0100 Subject: [PATCH 05/10] Validators: Fix password validators --- itools/validators/__init__.py | 3 ++- itools/validators/base.py | 8 ++++++++ itools/validators/exceptions.py | 8 +++++++- itools/validators/password.py | 27 +++++++++++++++++++++------ itools/validators/test_view.py | 5 ++++- 5 files changed, 42 insertions(+), 9 deletions(-) diff --git a/itools/validators/__init__.py b/itools/validators/__init__.py index 556b6e5f8..de196d0fc 100644 --- a/itools/validators/__init__.py +++ b/itools/validators/__init__.py @@ -18,8 +18,9 @@ from base import BaseValidator from exceptions import ValidationError from registry import register_validator, validator -import files import database +import files +import password __all__ = [ 'BaseValidator', diff --git a/itools/validators/base.py b/itools/validators/base.py index 0a43ee996..2bf6f0bbd 100644 --- a/itools/validators/base.py +++ b/itools/validators/base.py @@ -66,6 +66,14 @@ def raise_default_error(self, kw={}): 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) diff --git a/itools/validators/exceptions.py b/itools/validators/exceptions.py index 3b8b10910..96c319702 100644 --- a/itools/validators/exceptions.py +++ b/itools/validators/exceptions.py @@ -14,6 +14,9 @@ # 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): @@ -37,8 +40,11 @@ def get_messages(self, field): return l - def get_message(self, field=None): + 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) diff --git a/itools/validators/password.py b/itools/validators/password.py index 98fa3cdf8..36e6b4bd6 100644 --- a/itools/validators/password.py +++ b/itools/validators/password.py @@ -26,18 +26,26 @@ class StrongPasswordValidator(BaseValidator): """ - au minimum un caractère spécial ( *?./+#!,;:=) + at least 5 characters + at least one character (a,b,c...) at least one special character ( *?./+#!,;:=) at least a number (1, 2, 3, ...)" """ - min_length = 8 + + 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.") + 'too-short': MSG(u"This password is too short. It must contain at least {min_length} characters."), + 'has-character': MSG(u"This password should contains at least one character."), + 'has-number': MSG(u"This password should contains at least one number."), + 'has-special-character': MSG(u"This password should contains at least one special character."), } - help_msg = MSG(u"Your password must contain at least {min_length} characters.") 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: @@ -46,5 +54,12 @@ def check(self, value): has_digit = True else: has_special = True - if not has_letter or not has_digit or not has_special: - self.raise_default_error() + if not has_letter: + errors.append('has-character') + if not has_digit: + errors.append('has-number') + if not has_special: + errors.append('has-special-character') + if errors: + kw = {'min_length': self.min_length} + self.raise_errors(errors, kw) diff --git a/itools/validators/test_view.py b/itools/validators/test_view.py index 2306b12eb..923164ff0 100644 --- a/itools/validators/test_view.py +++ b/itools/validators/test_view.py @@ -30,7 +30,7 @@ class TestValidators(AutoEdit): 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_13', 'field_14'] field_1 = Integer_Field( title=MSG(u'5+5 equals to ?'), @@ -75,6 +75,9 @@ class TestValidators(AutoEdit): 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')]) def _get_datatype(self, resource, context, name): From f998c7670327870b5ad6521deb4449dddd1fa8bc Mon Sep 17 00:00:00 2001 From: Taverne Sylvain Date: Thu, 1 Dec 2016 18:09:54 +0100 Subject: [PATCH 06/10] Unique validators: Allow to set base query --- itools/validators/database.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/itools/validators/database.py b/itools/validators/database.py index 481163637..05222bc3e 100644 --- a/itools/validators/database.py +++ b/itools/validators/database.py @@ -26,6 +26,7 @@ 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 @@ -37,6 +38,8 @@ def check(self, value): 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: From ec99bf2aeaef74d655a2e2b7afbd736861b1b2f7 Mon Sep 17 00:00:00 2001 From: Taverne Sylvain Date: Thu, 1 Dec 2016 18:14:14 +0100 Subject: [PATCH 07/10] Validators: Replace - by _ (to allow use of merge dicts) --- itools/validators/base.py | 16 ++++++++-------- itools/validators/files.py | 12 ++++++------ itools/validators/password.py | 16 ++++++++-------- itools/validators/test_view.py | 2 +- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/itools/validators/base.py b/itools/validators/base.py index 2bf6f0bbd..a65376ec1 100644 --- a/itools/validators/base.py +++ b/itools/validators/base.py @@ -83,7 +83,7 @@ class EqualsValidator(BaseValidator): validator_id = 'equals-to' base_value = None - errors = {'not-equals': MSG(u'The value should be equals to {base_value}')} + errors = {'not_equals': MSG(u'The value should be equals to {base_value}')} def check(self, value): if value != self.base_value: @@ -117,7 +117,7 @@ class HexadecimalValidator(RegexValidator): class PositiveIntegerValidator(BaseValidator): validator_id = 'integer-positive' - errors = {'integer-positive': MSG(u'Ensure this value is positive.')} + errors = {'integer_positive': MSG(u'Ensure this value is positive.')} def check(self, value): if value < 0: @@ -129,7 +129,7 @@ def check(self, value): class PositiveIntegerNotNullValidator(BaseValidator): validator_id = 'integer-positive-not-null' - errors = {'integer-positive-not-null': MSG(u'Ensure this value is greater than 0.')} + errors = {'integer_positive_not_null': MSG(u'Ensure this value is greater than 0.')} def check(self, value): if value <= 0: @@ -141,7 +141,7 @@ def check(self, value): class MaxValueValidator(BaseValidator): validator_id = 'max-value' - errors = {'max-value': MSG(u'Ensure this value is less than or equal to {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): @@ -154,7 +154,7 @@ def check(self, value): class MinValueValidator(BaseValidator): validator_id = 'min-value' - errors = {'min-value': MSG(u'Ensure this value is greater than or equal to {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): @@ -167,7 +167,7 @@ def check(self, value): class MinMaxValueValidator(BaseValidator): validator_id = 'min-max-value' - errors = {'min-max-value': MSG( + 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 @@ -186,7 +186,7 @@ class MinLengthValidator(BaseValidator): validator_id = 'min-length' min_length = 0 - errors = {'min-length': MSG(u'Ensure this value has at least {min_length} characters.')} + errors = {'min_length': MSG(u'Ensure this value has at least {min_length} characters.')} def check(self, value): if len(value) < self.min_length: @@ -201,7 +201,7 @@ class MaxLengthValidator(BaseValidator): validator_id = 'max-length' max_length = 0 - errors = {'max-length': MSG(u'Ensure this value has at most {max_length} characters.')} + errors = {'max_length': MSG(u'Ensure this value has at most {max_length} characters.')} def check(self, value): if len(value) > self.max_length: diff --git a/itools/validators/files.py b/itools/validators/files.py index a79d65e04..7f0b0c0d9 100644 --- a/itools/validators/files.py +++ b/itools/validators/files.py @@ -33,7 +33,7 @@ class FileExtensionValidator(BaseValidator): validator_id = 'file-extension' allowed_extensions = [] - errors = {'invalid-extension': MSG( + errors = {'invalid_extension': MSG( u"File extension '{extension}' is not allowed. " u"Allowed extensions are: '{allowed_extensions}'.")} @@ -63,7 +63,7 @@ class MimetypesValidator(BaseValidator): validator_id = 'file-mimetypes' allowed_mimetypes = [] - errors = {'bad-mimetype': MSG( + errors = {'bad_mimetype': MSG( u"File mimetype '{mimetype}' is not allowed. " u"Allowed mimetypes are: '{allowed_mimetypes}'.")} @@ -119,8 +119,8 @@ 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.")} + 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 @@ -129,8 +129,8 @@ def check(self, value): im = PILImage.open(data) im.verify() except Exception: - code = 'image-has-errors' + code = 'image_has_errors' raise ValidationError(self.errors[code], code, {}) if im.width * im.height > self.max_pixels: - code = 'too-much-pixels' + code = 'too_much_pixels' raise ValidationError(self.errors[code], code, {}) diff --git a/itools/validators/password.py b/itools/validators/password.py index 36e6b4bd6..e655a959e 100644 --- a/itools/validators/password.py +++ b/itools/validators/password.py @@ -36,16 +36,16 @@ class StrongPasswordValidator(BaseValidator): min_length = 5 errors = { - 'too-short': MSG(u"This password is too short. It must contain at least {min_length} characters."), - 'has-character': MSG(u"This password should contains at least one character."), - 'has-number': MSG(u"This password should contains at least one number."), - 'has-special-character': MSG(u"This password should contains at least one special character."), + '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') + errors.append('too_short') has_letter = has_digit = has_special = False for c in value: if c in ascii_letters: @@ -55,11 +55,11 @@ def check(self, value): else: has_special = True if not has_letter: - errors.append('has-character') + errors.append('need_character') if not has_digit: - errors.append('has-number') + errors.append('need_number') if not has_special: - errors.append('has-special-character') + errors.append('need_special_character') if errors: kw = {'min_length': self.min_length} self.raise_errors(errors, kw) diff --git a/itools/validators/test_view.py b/itools/validators/test_view.py index 923164ff0..fac5d6295 100644 --- a/itools/validators/test_view.py +++ b/itools/validators/test_view.py @@ -35,7 +35,7 @@ class TestValidators(AutoEdit): 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 ;)')} + error_messages={'not_equals': MSG(u'Give me a 10 ;)')} ) field_2 = Char_Field( title=MSG(u'Hexadecimal color'), From 1e46c0b38e45b5730f49fb0d6adeac9c06ba6f93 Mon Sep 17 00:00:00 2001 From: Taverne Sylvain Date: Thu, 1 Dec 2016 18:17:30 +0100 Subject: [PATCH 08/10] Validators: Add test with 2 validators --- itools/validators/test_view.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/itools/validators/test_view.py b/itools/validators/test_view.py index fac5d6295..85686abe1 100644 --- a/itools/validators/test_view.py +++ b/itools/validators/test_view.py @@ -30,7 +30,7 @@ class TestValidators(AutoEdit): 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_13', 'field_14', 'field_15'] field_1 = Integer_Field( title=MSG(u'5+5 equals to ?'), @@ -78,6 +78,12 @@ class TestValidators(AutoEdit): field_14 = Char_Field( title=MSG(u'Strong password'), validators=[validator('strong-password')]) + field_15 = Char_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): From 9d8cc2a61de4cd3ea6fdad2e31d875cd502901b4 Mon Sep 17 00:00:00 2001 From: Taverne Sylvain Date: Thu, 1 Dec 2016 18:56:29 +0100 Subject: [PATCH 09/10] Validators: Handle errors with serveral validators --- itools/validators/test_view.py | 2 +- itools/web/context.py | 9 +++++---- itools/web/exceptions.py | 34 +++++++++++++++++++++++----------- itools/web/views.py | 8 ++++---- 4 files changed, 33 insertions(+), 20 deletions(-) diff --git a/itools/validators/test_view.py b/itools/validators/test_view.py index 85686abe1..4f09d961a 100644 --- a/itools/validators/test_view.py +++ b/itools/validators/test_view.py @@ -78,7 +78,7 @@ class TestValidators(AutoEdit): field_14 = Char_Field( title=MSG(u'Strong password'), validators=[validator('strong-password')]) - field_15 = Char_Field( + field_15 = Integer_Field( title=MSG(u'Number >=5 and equals to 10'), validators=[ validator('min-value', min_value=5), diff --git a/itools/web/context.py b/itools/web/context.py index c7db0cd9f..712fe8fd5 100644 --- a/itools/web/context.py +++ b/itools/web/context.py @@ -1166,14 +1166,15 @@ def _get_form_value(form, name, type=String, default=None): 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) + validator = validator(title=field.title, context=context) try: validator.check(value) except ValidationError, e: - msg = e.get_message(field) - raise FormError(msg, invalid=True) + errors.extend(e.get_messages(field)) + if errors: + raise FormError(messages=errors, invalid=True) def get_form_value(form, name, type=String, default=None): 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 0fdf87c15..246dad8e2 100644 --- a/itools/web/views.py +++ b/itools/web/views.py @@ -41,25 +41,25 @@ def process_form(get_value, schema, error_msg=None): - messages = [] missings = [] invalids = [] + unknow = [] values = {} for name in schema: datatype = schema[name] try: values[name] = get_value(name, type=datatype) except FormError, e: - messages.append(e.get_message()) if e.missing: 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_msg, - messages=messages, missing=len(missings)>0, invalid=len(invalids)>0, missings=missings, From ec3b35d69bb796694c0297fe94bcf7cd2c46de27 Mon Sep 17 00:00:00 2001 From: Taverne Sylvain Date: Fri, 2 Dec 2016 10:49:06 +0100 Subject: [PATCH 10/10] Validators: Add rest test --- itools/validators/test_rest.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 itools/validators/test_rest.py 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) +