From 0baa10f65eb66c13530b7d07ab4db760e60858b1 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Thu, 3 Aug 2023 19:18:59 +0530 Subject: [PATCH] Adds utility validator: `PhoneNumberValidator` --- .../tests/test_phone_number_validator.py | 29 ---- care/utils/models/validators.py | 54 ++++++++ .../tests/test_phone_number_validator.py | 131 ++++++++++++++++++ 3 files changed, 185 insertions(+), 29 deletions(-) delete mode 100644 care/facility/tests/test_phone_number_validator.py create mode 100644 care/utils/tests/test_phone_number_validator.py diff --git a/care/facility/tests/test_phone_number_validator.py b/care/facility/tests/test_phone_number_validator.py deleted file mode 100644 index ef1f0a5f73..0000000000 --- a/care/facility/tests/test_phone_number_validator.py +++ /dev/null @@ -1,29 +0,0 @@ -from django.core.exceptions import ValidationError -from django.test import TestCase - -from care.users.models import phone_number_regex - - -class PhoneNumberValidatorTests(TestCase): - def test_valid_phone_number(self): - self.assertIsNone(phone_number_regex("+919876543210")) - self.assertIsNone(phone_number_regex("9876543210")) - self.assertIsNone(phone_number_regex("02228820000")) - - def test_invalid_phone_number(self): - with self.assertRaises(ValidationError): - phone_number_regex("987654321") - with self.assertRaises(ValidationError): - phone_number_regex("98765432101") - with self.assertRaises(ValidationError): - phone_number_regex("987654321a") - with self.assertRaises(ValidationError): - phone_number_regex("+9198765432100") - with self.assertRaises(ValidationError): - phone_number_regex("+91 9876543210") - with self.assertRaises(ValidationError): - phone_number_regex("98765 43210") - with self.assertRaises(ValidationError): - phone_number_regex("98765-43210") - with self.assertRaises(ValidationError): - phone_number_regex("022 26543210") diff --git a/care/utils/models/validators.py b/care/utils/models/validators.py index 16fde41188..8bea08e6c6 100644 --- a/care/utils/models/validators.py +++ b/care/utils/models/validators.py @@ -2,6 +2,7 @@ import jsonschema from django.core.exceptions import ValidationError +from django.core.validators import RegexValidator from django.utils.deconstruct import deconstructible @@ -41,3 +42,56 @@ def _extract_errors( message = str(error).replace("\n\n", ": ").replace("\n", "") container.append(ValidationError(message)) + + +@deconstructible +class PhoneNumberValidator(RegexValidator): + """ + Any one of the specified types of phone numbers are considered valid. + + Allowed types: + - `mobile` (Indian XOR International) + - `indian_mobile` (Indian only) + - `international_mobile` (International only) + - `landline` (Indian only) + - `support` (Indian only) + + Example usage: + + ``` + field = models.CharField( + validators=[PhoneNumberValidator(types=("mobile", "landline", "support"))]) + ) + ``` + """ + + indian_mobile_number_regex = r"^(?=^\+91)(^\+91\d{10}$)" + international_mobile_number_regex = r"^(?!^\+91)(^\+\d{1,3}\d{8,14}$)" + landline_number_regex = r"^\+91[2-9]\d{7,9}$" + support_number_regex = r"^(1800|1860)\d{6,7}$" + + regex_map = { + "indian_mobile": indian_mobile_number_regex, + "international_mobile": international_mobile_number_regex, + "mobile": rf"{indian_mobile_number_regex}|{international_mobile_number_regex}", + "landline": landline_number_regex, + "support": support_number_regex, + } + + def __init__(self, types: Iterable[str], *args, **kwargs): + if not isinstance(types, Iterable) or isinstance(types, str) or len(types) == 0: + raise ValueError("The `types` argument must be a non-empty iterable.") + + self.types = types + self.message = f"Invalid phone number. Must be one of the following types: {', '.join(self.types)}. Received: %(value)s" + self.code = "invalid_phone_number" + + self.regex = r"|".join([self.regex_map[type] for type in self.types]) + super().__init__(*args, **kwargs) + + def __eq__(self, other): + return isinstance(other, PhoneNumberValidator) and self.types == other.types + + +mobile_validator = PhoneNumberValidator(types=("mobile",)) +mobile_or_landline_number_validator = PhoneNumberValidator(types=("mobile", "landline")) diff --git a/care/utils/tests/test_phone_number_validator.py b/care/utils/tests/test_phone_number_validator.py new file mode 100644 index 0000000000..4c04d68262 --- /dev/null +++ b/care/utils/tests/test_phone_number_validator.py @@ -0,0 +1,131 @@ +from django.core.exceptions import ValidationError +from django.test import TestCase + +from care.utils.models.validators import PhoneNumberValidator + + +class PhoneNumberValidatorTests(TestCase): + mobile_validator = PhoneNumberValidator(types=("mobile",)) + indian_mobile_validator = PhoneNumberValidator(types=("indian_mobile",)) + international_mobile_validator = PhoneNumberValidator( + types=("international_mobile",) + ) + landline_validator = PhoneNumberValidator(types=("landline",)) + support_validator = PhoneNumberValidator(types=("support",)) + + valid_indian_mobile_numbers = [ + "+919876543210", + ] + + valid_international_mobile_numbers = [ + "+44712345678", + "+447123456789", + "+4471234567890", + "+44712345678901", + "+447123456789012", + "+4471234567890123", + "+44712345678901234", + ] + + valid_landline_numbers = [ + "+914902626488", + ] + + valid_support_numbers = [ + "1800123456", + "18001234567", + "1860123456", + "18601234567", + ] + + invalid_indian_mobile_numbers = [ + *valid_support_numbers, + *valid_international_mobile_numbers, + "9876543210", + "98765432101", + "987654321a", + "+9198765432100", + "+91 9876543210", + "98765 43210", + "98765-43210", + ] + + invalid_international_mobile_numbers = [ + *valid_support_numbers, + *valid_indian_mobile_numbers, + "4471234567", + "447123456789012345", + "+447123456789012345", + "+44 7123456789012345", + "4471234567890123456", + "+4471234567890123456", + "+44 71234567890123456", + ] + + invalid_landline_numbers = [ + *valid_support_numbers, + "4902626488", + "02226543210", + "022 26543210", + "022-26543210", + ] + + invalid_support_numbers = [ + *valid_indian_mobile_numbers, + *valid_international_mobile_numbers, + *valid_landline_numbers, + "180012345", + "180012345678", + "186012345", + "186012345678", + ] + + def test_valid_mobile_numbers(self): + for number in ( + self.valid_indian_mobile_numbers + self.valid_international_mobile_numbers + ): + self.assertIsNone(self.mobile_validator(number), msg=f"Failed for {number}") + + def test_valid_indian_mobile_numbers(self): + for number in self.valid_indian_mobile_numbers: + self.assertIsNone( + self.indian_mobile_validator(number), msg=f"Failed for {number}" + ) + + def test_valid_international_mobile_numbers(self): + for number in self.valid_international_mobile_numbers: + self.assertIsNone( + self.international_mobile_validator(number), msg=f"Failed for {number}" + ) + + def test_valid_landline_numbers(self): + for number in self.valid_landline_numbers: + self.assertIsNone( + self.landline_validator(number), msg=f"Failed for {number}" + ) + + def test_valid_support_numbers(self): + for number in self.valid_support_numbers: + self.assertIsNone( + self.support_validator(number), msg=f"Failed for {number}" + ) + + def test_invalid_indian_mobile_numbers(self): + for number in self.invalid_indian_mobile_numbers: + with self.assertRaises(ValidationError, msg=f"Failed for {number}"): + self.indian_mobile_validator(number) + + def test_invalid_international_mobile_numbers(self): + for number in self.invalid_international_mobile_numbers: + with self.assertRaises(ValidationError, msg=f"Failed for {number}"): + self.international_mobile_validator(number) + + def test_invalid_landline_numbers(self): + for number in self.invalid_landline_numbers: + with self.assertRaises(ValidationError, msg=f"Failed for {number}"): + self.landline_validator(number) + + def test_invalid_support_numbers(self): + for number in self.invalid_support_numbers: + with self.assertRaises(ValidationError, msg=f"Failed for {number}"): + self.support_validator(number)