Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactored complex methods #263

Merged
merged 1 commit into from
Nov 16, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about deprecating the *_validator versions? I suppose there's no real harm in keeping them around but it would be nice to provide just the class version. Users can always make their own version of the callable if they want to use it that way. Consider this optional and only do it if you agree.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I was thinking to deprecate them. There is no point in callable, because usually logic is complex so you have to split into several methods. And even for simple cases think is better to stick to class validators conventionally.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm ok with merging this before these are depreciated because the deprecation work in #262 hasn't been merged yet. Just file a new issue so we don't forget to do the deprecation once #262 has been merged.

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