From 2c532a1484a091f646d0ca706f64ca019a3158bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C4=83lina=20Cenan?= Date: Fri, 22 Jan 2021 01:45:57 +0200 Subject: [PATCH] "Sometimes" validation and some other fixes (#28) Co-authored-by: Calina Cenan --- docs/source/index.md | 4 +++ flask_sieve/lang/en.py | 4 +++ flask_sieve/rules_processor.py | 23 ++++++++++++++--- tests/test_validator.py | 47 +++++++++++++++++++++++++++++++++- 4 files changed, 74 insertions(+), 4 deletions(-) diff --git a/docs/source/index.md b/docs/source/index.md index 8a4e9ce..cad4271 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -580,6 +580,10 @@ The given _field_ must match the field under validation. The field under validation must have a size matching the given _value_. For string data, _value_ corresponds to the number of characters. For numeric data, _value_ corresponds to a given integer value. For an array, _size_ corresponds to the `count` of the array. For files, _size_ corresponds to the file size in kilobytes. +#### sometimes + +The other validations will only apply if this field is present and non-empty. Incompatible with _required_ and _nullable_. + #### starts_with:_foo_,_bar_,... The field under validation must start with one of the given values. diff --git a/flask_sieve/lang/en.py b/flask_sieve/lang/en.py index 5ba658f..ac34fb2 100644 --- a/flask_sieve/lang/en.py +++ b/flask_sieve/lang/en.py @@ -54,18 +54,21 @@ 'file': 'The :attribute must be less than :value_0 kilobytes.', 'string': 'The :attribute must be less than :value_0 characters.', 'array': 'The :attribute must have less than :value_0 items.', + 'empty': 'The :attribute could not be validated since it is empty.' }, 'lte': { 'numeric': 'The :attribute must be less than or equal :value_0.', 'file': 'The :attribute must be less than or equal :value_0 kilobytes.', 'string': 'The :attribute must be less than or equal :value_0 characters.', 'array': 'The :attribute must not have more than :value_0 items.', + 'empty': 'The :attribute could not be validated since it is empty.' }, 'max': { 'numeric': 'The :attribute may not be greater than :max_0.', 'file': 'The :attribute may not be greater than :max_0 kilobytes.', 'string': 'The :attribute may not be greater than :max_0 characters.', 'array': 'The :attribute may not have more than :max_0 items.', + 'empty': 'The :attribute could not be validated since it is empty.' }, 'mime_types': 'The :attribute must be a file of type: :values_0.', 'min': { @@ -73,6 +76,7 @@ 'file': 'The :attribute must be at least :min_0 kilobytes.', 'string': 'The :attribute must be at least :min_0 characters.', 'array': 'The :attribute must have at least :min_0 items.', + 'empty': 'The :attribute could not be validated since it is empty.' }, 'not_in': 'The selected :attribute is invalid.', 'not_regex': 'The :attribute format is invalid.', diff --git a/flask_sieve/rules_processor.py b/flask_sieve/rules_processor.py index d6f58b2..1fee1ba 100644 --- a/flask_sieve/rules_processor.py +++ b/flask_sieve/rules_processor.py @@ -246,6 +246,10 @@ def validate_extension(self, value, params, **kwargs): def validate_file(value, **kwargs): return isinstance(value, FileStorage) + @staticmethod + def validate_empty(value, **kwargs): + return value == '' + def validate_filled(self, value, attribute, nullable, **kwargs): if self.validate_present(attribute): return self.validate_required(value, attribute, nullable) @@ -364,18 +368,24 @@ def validate_json(self, value, **kwargs): def validate_lt(self, value, params, **kwargs): self._assert_params_size(size=1, params=params, rule='lt') + if value == '': + return False value = self._get_size(value) lower = self._get_size(self._attribute_value(params[0])) return value < lower def validate_lte(self, value, params, **kwargs): self._assert_params_size(size=1, params=params, rule='lte') + if value == '': + return False value = self._get_size(value) lower = self._get_size(self._attribute_value(params[0])) return value <= lower def validate_max(self, value, params, **kwargs): self._assert_params_size(size=1, params=params, rule='max') + if value == '': + return False value = self._get_size(value) upper = self._get_size(params[0]) return value <= upper @@ -406,6 +416,9 @@ def validate_not_regex(self, value, params, **kwargs): def validate_nullable(value, **kwargs): return True + def validate_sometimes(self, value, **kwargs): + return True + def validate_numeric(self, value, **kwargs): return self._can_call_with_method(float, value) @@ -527,14 +540,16 @@ def validate_uuid(value, **kwargs): r'^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$', str(value).lower() ) is not None - + def _is_attribute_nullable(self, attribute, params, rules, **kwargs): is_explicitly_nullable = self._has_rule(rules, 'nullable') if is_explicitly_nullable: return True value = self._attribute_value(attribute) - if value is not None: - return False + is_optional = self._has_rule(rules, 'sometimes') + if is_optional and value is not None: + return True + attribute_conditional_rules = list(filter(lambda rule: rule['name'] in conditional_inclusion_rules, rules)) if len(attribute_conditional_rules) == 0: return False @@ -621,6 +636,8 @@ def _get_type_from_value(self, value): return 'array' elif self.validate_file(value): return 'file' + elif self.validate_empty(value): + return 'empty' return 'string' def _has_any_of_rules(self, subset, rules): diff --git a/tests/test_validator.py b/tests/test_validator.py index d41ebd9..fecfe45 100644 --- a/tests/test_validator.py +++ b/tests/test_validator.py @@ -163,6 +163,17 @@ def validate_odd(value, **kwargs): ) self.assertTrue(self._validator.passes()) + def test_translates_validations_set_through_custom_handlers(self): + def validate_odd(value, **kwargs): + return int(value) % 2 + self._validator.set_custom_handlers([ + { + 'handler': validate_odd, + 'message':'This number must be odd.', + 'params_count':0 + } + ]) + def test_cannot_set_custom_handler_without_validate_keyword(self): def method_odd(value, **kwargs): return int(value) % 2 @@ -173,9 +184,43 @@ def method_odd(value, **kwargs): params_count=0 ) + def test_sometimes_request(self): + self.set_validator_params( + rules={'number': ['sometimes', 'max:5']}, + request={} + ) + self.assertTrue(self._validator.passes()) + + self.set_validator_params( + rules={'number': ['sometimes', 'max:5']}, + request={'number': ''} + ) + self.assertTrue(self._validator.fails()) + self.assertDictEqual({ + 'number': [ + 'The number could not be validated since it is empty.' + ] + }, self._validator.messages()) + + self.set_validator_params( + rules={'number': ['sometimes', 'max:5']}, + request={'number': 2} + ) + self.assertTrue(self._validator.passes()) + + self.set_validator_params( + rules={'number': ['sometimes', 'max:5']}, + request={'number': 10} + ) + self.assertTrue(self._validator.fails()) + self.assertDictEqual({ + 'number': [ + 'The number may not be greater than 5.' + ] + }, self._validator.messages()) + def set_validator_params(self, rules=None, request=None): rules = rules or {} request = request or {} self._validator.set_rules(rules) self._validator.set_request(request) -