Skip to content

Commit

Permalink
Refactored complex methods #261
Browse files Browse the repository at this point in the history
- Complexity 9
- Moved function validators to Validator classes
- Changed Indonesian Numbers
  • Loading branch information
vladimirnani committed Nov 12, 2016
1 parent 2927a30 commit 998cac1
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 107 deletions.
4 changes: 4 additions & 0 deletions localflavor.prospector.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ pep8:
max-line-length: 120
disable:
- N802

mccabe:
options:
max-complexity: 9
6 changes: 3 additions & 3 deletions localflavor/bg/models.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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
Expand All @@ -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
Expand Down
54 changes: 32 additions & 22 deletions localflavor/bg/validators.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,41 @@
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.
More details https://en.wikipedia.org/wiki/Unique_citizenship_number
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))
return int(egn[-1]) == checksum % 11 % 10
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.
Expand All @@ -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()
80 changes: 49 additions & 31 deletions localflavor/id_/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
59 changes: 33 additions & 26 deletions localflavor/no/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,37 +43,16 @@ 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 ''

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]
Expand All @@ -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):
"""
Expand All @@ -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)
52 changes: 28 additions & 24 deletions localflavor/si/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
"""
Expand Down
Loading

0 comments on commit 998cac1

Please sign in to comment.