diff --git a/localflavor.prospector.yaml b/localflavor.prospector.yaml index c2614f919..53398625c 100644 --- a/localflavor.prospector.yaml +++ b/localflavor.prospector.yaml @@ -11,3 +11,7 @@ pep8: max-line-length: 120 disable: - N802 + +mccabe: + options: + max-complexity: 9 diff --git a/localflavor/bg/models.py b/localflavor/bg/models.py index 4b8b9fb09..4936fb9f1 100644 --- a/localflavor/bg/models.py +++ b/localflavor/bg/models.py @@ -1,6 +1,6 @@ from django.db import models -from .validators import egn_validator, eik_validator +from .validators import EGNValidator, EIKValidator class BGEGNField(models.CharField): @@ -12,7 +12,7 @@ class BGEGNField(models.CharField): models.CharField(max_length=10, validators=[localflavor.bg.validators.egn_validator]) """ - default_validators = models.CharField.default_validators + [egn_validator] + default_validators = models.CharField.default_validators + [EGNValidator()] def __init__(self, *args, **kwargs): kwargs['max_length'] = 10 @@ -28,7 +28,7 @@ class BGEIKField(models.CharField): models.CharField(max_length=13, validators=[localflavor.bg.validators.eik_validator]) """ - default_validators = models.CharField.default_validators + [eik_validator] + default_validators = models.CharField.default_validators + [EIKValidator()] def __init__(self, *args, **kwargs): kwargs['max_length'] = 13 diff --git a/localflavor/bg/validators.py b/localflavor/bg/validators.py index 48c0d43b2..161e73909 100644 --- a/localflavor/bg/validators.py +++ b/localflavor/bg/validators.py @@ -1,10 +1,12 @@ from django.core.exceptions import ValidationError +from django.utils.deconstruct import deconstructible from django.utils.translation import ugettext_lazy as _ from .utils import get_egn_birth_date -def egn_validator(egn): +@deconstructible +class EGNValidator(object): """ Check Bulgarian unique citizenship number (EGN) for validity. @@ -12,7 +14,8 @@ def egn_validator(egn): Full information in Bulgarian about algorithm is available here http://www.grao.bg/esgraon.html#section2 """ - def check_checksum(egn): + + def _check_checksum(self, egn): weights = (2, 4, 8, 5, 10, 9, 7, 3, 6) try: checksum = sum(weight * int(digit) for weight, digit in zip(weights, egn)) @@ -20,17 +23,19 @@ def check_checksum(egn): except ValueError: return False - def check_valid_date(egn): + def _check_valid_date(self, egn): try: return get_egn_birth_date(egn) except ValueError: return None - if not (len(egn) == 10 and check_checksum(egn) and check_valid_date(egn)): - raise ValidationError(_("The EGN is not valid")) + def __call__(self, egn): + if not (len(egn) == 10 and self._check_checksum(egn) and self._check_valid_date(egn)): + raise ValidationError(_("The EGN is not valid")) -def eik_validator(eik): +@deconstructible +class EIKValidator(object): """ Check Bulgarian EIK/BULSTAT codes for validity. @@ -39,30 +44,35 @@ def eik_validator(eik): """ error_message = _('EIK/BULSTAT is not valid') - def get_checksum(weights, digits): + def __call__(self, value): + try: + value = list(map(int, value)) + except ValueError: + raise ValidationError(self.error_message) + + if not (len(value) in [9, 13] and self._check_eik_base(value)): + raise ValidationError(self.error_message) + + if len(value) == 13 and not self._check_eik_extra(value): + raise ValidationError(self.error_message) + + def _get_checksum(self, weights, digits): checksum = sum(weight * digit for weight, digit in zip(weights, digits)) return checksum % 11 - def check_eik_base(eik): - checksum = get_checksum(range(1, 9), eik) + def _check_eik_base(self, eik): + checksum = self._get_checksum(range(1, 9), eik) if checksum == 10: - checksum = get_checksum(range(3, 11), eik) + checksum = self._get_checksum(range(3, 11), eik) return eik[8] == checksum % 10 - def check_eik_extra(eik): + def _check_eik_extra(self, eik): digits = eik[8:12] - checksum = get_checksum((2, 7, 3, 5), digits) + checksum = self._get_checksum((2, 7, 3, 5), digits) if checksum == 10: - checksum = get_checksum((4, 9, 5, 7), digits) + checksum = self._get_checksum((4, 9, 5, 7), digits) return eik[-1] == checksum % 10 - try: - eik = list(map(int, eik)) - except ValueError: - raise ValidationError(error_message) - - if not (len(eik) in [9, 13] and check_eik_base(eik)): - raise ValidationError(error_message) - if len(eik) == 13 and not check_eik_extra(eik): - raise ValidationError(error_message) +eik_validator = EIKValidator() +egn_validator = EGNValidator() diff --git a/localflavor/id_/forms.py b/localflavor/id_/forms.py index 9f7e7d8f7..a8f6474fd 100644 --- a/localflavor/id_/forms.py +++ b/localflavor/id_/forms.py @@ -107,57 +107,75 @@ class IDLicensePlateField(Field): default_error_messages = { 'invalid': _('Enter a valid vehicle license plate number'), } + foreign_vehicles_prefixes = ('CD', 'CC') - def clean(self, value): # noqa - # Load data in memory only when it is required, see also #17275 - from .id_choices import LICENSE_PLATE_PREFIX_CHOICES + def clean(self, value): super(IDLicensePlateField, self).clean(value) if value in EMPTY_VALUES: return '' + plate_number = re.sub(r'\s+', ' ', force_text(value.strip())).upper() - plate_number = re.sub(r'\s+', ' ', - force_text(value.strip())).upper() + number, prefix, suffix = self._validate_regex_match(plate_number) + self._validate_prefix(prefix) + self._validate_jakarta(prefix, suffix) + self._validate_ri(prefix, suffix) + self._validate_number(number) + # CD, CC and B 12345 12 + if len(number) == 5 or prefix in self.foreign_vehicles_prefixes: + self._validate_numeric_suffix(suffix) + self._validate_known_codes_range(number, prefix, suffix) + else: + self._validate_non_numeric_suffix(suffix) + return plate_number + + def _validate_regex_match(self, plate_number): matches = plate_re.search(plate_number) if matches is None: raise ValidationError(self.error_messages['invalid']) - - # Make sure prefix is in the list of known codes. prefix = matches.group('prefix') - if prefix not in [choice[0] for choice in LICENSE_PLATE_PREFIX_CHOICES]: + suffix = matches.group('suffix') + number = matches.group('number') + return number, prefix, suffix + + def _validate_number(self, number): + # Number can't be zero. + if number == '0': raise ValidationError(self.error_messages['invalid']) - # Only Jakarta (prefix B) can have 3 letter suffix. - suffix = matches.group('suffix') - if suffix is not None and len(suffix) == 3 and prefix != 'B': + def _validate_known_codes_range(self, number, prefix, suffix): + # Known codes range is 12-124 + if prefix in self.foreign_vehicles_prefixes and not (12 <= int(number) <= 124): + raise ValidationError(self.error_messages['invalid']) + if len(number) == 5 and not (12 <= int(suffix) <= 124): raise ValidationError(self.error_messages['invalid']) - # RI plates don't have suffix. - if prefix == 'RI' and suffix is not None and suffix != '': + def _validate_numeric_suffix(self, suffix): + # suffix must be numeric and non-empty + if re.match(r'^\d+$', suffix) is None: raise ValidationError(self.error_messages['invalid']) - # Number can't be zero. - number = matches.group('number') - if number == '0': + def _validate_non_numeric_suffix(self, suffix): + # suffix must be non-numeric + if suffix is not None and re.match(r'^[A-Z]{,3}$', suffix) is None: raise ValidationError(self.error_messages['invalid']) - # CD, CC and B 12345 12 - if len(number) == 5 or prefix in ('CD', 'CC'): - # suffix must be numeric and non-empty - if re.match(r'^\d+$', suffix) is None: - raise ValidationError(self.error_messages['invalid']) + def _validate_prefix(self, prefix): + # Load data in memory only when it is required, see also #17275 + from .id_choices import LICENSE_PLATE_PREFIX_CHOICES + # Make sure prefix is in the list of known codes. + if prefix not in [choice[0] for choice in LICENSE_PLATE_PREFIX_CHOICES]: + raise ValidationError(self.error_messages['invalid']) - # Known codes range is 12-124 - if prefix in ('CD', 'CC') and not (12 <= int(number) <= 124): - raise ValidationError(self.error_messages['invalid']) - if len(number) == 5 and not (12 <= int(suffix) <= 124): - raise ValidationError(self.error_messages['invalid']) - else: - # suffix must be non-numeric - if suffix is not None and re.match(r'^[A-Z]{,3}$', suffix) is None: - raise ValidationError(self.error_messages['invalid']) + def _validate_ri(self, prefix, suffix): + # RI plates don't have suffix. + if prefix == 'RI' and suffix is not None and suffix != '': + raise ValidationError(self.error_messages['invalid']) - return plate_number + def _validate_jakarta(self, prefix, suffix): + # Only Jakarta (prefix B) can have 3 letter suffix. + if suffix is not None and len(suffix) == 3 and prefix != 'B': + raise ValidationError(self.error_messages['invalid']) class IDNationalIdentityNumberField(Field): diff --git a/localflavor/no/forms.py b/localflavor/no/forms.py index 426b647f0..8b38a4ec5 100644 --- a/localflavor/no/forms.py +++ b/localflavor/no/forms.py @@ -43,7 +43,7 @@ class NOSocialSecurityNumber(Field): 'invalid': _('Enter a valid Norwegian social security number.'), } - def clean(self, value): # noqa + def clean(self, value): super(NOSocialSecurityNumber, self).clean(value) if value in EMPTY_VALUES: return '' @@ -51,29 +51,8 @@ def clean(self, value): # noqa if not re.match(r'^\d{11}$', value): raise ValidationError(self.error_messages['invalid']) - day = int(value[:2]) - month = int(value[2:4]) - year2 = int(value[4:6]) - - inum = int(value[6:9]) - self.birthday = None - try: - if 000 <= inum < 500: - self.birthday = datetime.date(1900 + year2, month, day) - if 500 <= inum < 750 and year2 > 54: - self.birthday = datetime.date(1800 + year2, month, day) - if 500 <= inum < 1000 and year2 < 40: - self.birthday = datetime.date(2000 + year2, month, day) - if 900 <= inum < 1000 and year2 > 39: - self.birthday = datetime.date(1900 + year2, month, day) - except ValueError: - raise ValidationError(self.error_messages['invalid']) - - sexnum = int(value[8]) - if sexnum % 2 == 0: - self.gender = 'F' - else: - self.gender = 'M' + self.birthday = self._get_birthday(value) + self.gender = self._get_gender(value) digits = map(int, list(value)) weight_1 = [3, 7, 6, 1, 8, 9, 4, 5, 2, 1, 0] @@ -89,6 +68,33 @@ def multiply_reduce(aval, bval): return value + def _get_gender(self, value): + sexnum = int(value[8]) + if sexnum % 2 == 0: + gender = 'F' + else: + gender = 'M' + return gender + + def _get_birthday(self, value): + birthday = None + day = int(value[:2]) + month = int(value[2:4]) + year2 = int(value[4:6]) + inum = int(value[6:9]) + try: + if 000 <= inum < 500: + birthday = datetime.date(1900 + year2, month, day) + if 500 <= inum < 750 and year2 > 54: + birthday = datetime.date(1800 + year2, month, day) + if 500 <= inum < 1000 and year2 < 40: + birthday = datetime.date(2000 + year2, month, day) + if 900 <= inum < 1000 and year2 > 39: + birthday = datetime.date(1900 + year2, month, day) + except ValueError: + raise ValidationError(self.error_messages['invalid']) + return birthday + class NOPhoneNumberField(RegexField): """ @@ -102,5 +108,6 @@ class NOPhoneNumberField(RegexField): } def __init__(self, max_length=None, min_length=None, *args, **kwargs): - super(NOPhoneNumberField, self).__init__(r'^(?:\+47)? ?(\d{3}\s?\d{2}\s?\d{3}|\d{2}\s?\d{2}\s?\d{2}\s?\d{2})$', - max_length, min_length, *args, **kwargs) + super(NOPhoneNumberField, self).__init__( + r'^(?:\+47)? ?(\d{3}\s?\d{2}\s?\d{3}|\d{2}\s?\d{2}\s?\d{2}\s?\d{2})$', + max_length, min_length, *args, **kwargs) diff --git a/localflavor/si/forms.py b/localflavor/si/forms.py index b74a6a706..8875361eb 100644 --- a/localflavor/si/forms.py +++ b/localflavor/si/forms.py @@ -34,14 +34,39 @@ def clean(self, value): value = value.strip() + m = self._regex_match(value) + day, month, year, nationality, gender, checksum = [int(i) for i in m.groups()] + + self._validate_emso(checksum, value) + birthday = self._validate_birthday(day, month, year) + + self.info = { + 'gender': gender < 500 and 'male' or 'female', + 'birthdate': birthday, + 'nationality': nationality, + } + return value + + def _regex_match(self, value): m = self.emso_regex.match(value) if m is None: raise ValidationError(self.error_messages['invalid']) + return m - # Extract information in the identification number. - day, month, year, nationality, gender, checksum = [int(i) for i in m.groups()] + def _validate_birthday(self, day, month, year): + if year < 890: + year += 2000 + else: + year += 1000 + try: + birthday = datetime.date(year, month, day) + except ValueError: + raise ValidationError(self.error_messages['date']) + if datetime.date.today() < birthday: + raise ValidationError(self.error_messages['date']) + return birthday - # Validate EMSO + def _validate_emso(self, checksum, value): s = 0 int_values = [int(i) for i in value] for a, b in zip(int_values, list(range(7, 1, -1)) * 2): @@ -51,30 +76,9 @@ def clean(self, value): k = 0 else: k = 11 - chk - if k == 10 or checksum != k: raise ValidationError(self.error_messages['checksum']) - # Validate birth date. - if year < 890: - year += 2000 - else: - year += 1000 - - try: - birthday = datetime.date(year, month, day) - except ValueError: - raise ValidationError(self.error_messages['date']) - if datetime.date.today() < birthday: - raise ValidationError(self.error_messages['date']) - - self.info = { - 'gender': gender < 500 and 'male' or 'female', - 'birthdate': birthday, - 'nationality': nationality, - } - return value - class SITaxNumberField(CharField): """ diff --git a/tests/test_bg.py b/tests/test_bg.py index 6ed5a31d3..32fda5ab2 100644 --- a/tests/test_bg.py +++ b/tests/test_bg.py @@ -10,7 +10,7 @@ from localflavor.bg.models import BGEGNField, BGEIKField from localflavor.bg.utils import get_egn_birth_date -from localflavor.bg.validators import egn_validator, eik_validator +from localflavor.bg.validators import EGNValidator, EIKValidator VALID_EGNS = ( '7523169263', @@ -60,6 +60,9 @@ 'aaaaaaaaaa', ) +eik_validator = EIKValidator() +egn_validator = EGNValidator() + class BGLocalFlavorValidatorsTests(TestCase):