"""Norwegian-specific Form helpers."""
import datetime
import re
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.forms.fields import CharField, RegexField, Select
from django.utils.translation import gettext_lazy as _
from .no_municipalities import MUNICIPALITY_CHOICES
[docs]class NOZipCodeField(RegexField):
"""
A form field that validates input as a Norwegian zip code.
Valid codes have four digits.
"""
default_error_messages = {
'invalid': _('Enter a zip code in the format XXXX.'),
}
def __init__(self, **kwargs):
super().__init__(r'^\d{4}$', **kwargs)
[docs]class NOMunicipalitySelect(Select):
"""A Select widget that uses a list of Norwegian municipalities (fylker) as its choices."""
def __init__(self, attrs=None):
super().__init__(attrs, choices=MUNICIPALITY_CHOICES)
[docs]class NOSocialSecurityNumber(CharField):
"""Algorithm is documented at http://no.wikipedia.org/wiki/Personnummer."""
default_error_messages = {
'invalid': _('Enter a valid Norwegian social security number.'),
}
[docs] def clean(self, value):
value = super().clean(value)
if value in self.empty_values:
return value
if not re.match(r'^\d{11}$', value):
raise ValidationError(self.error_messages['invalid'], code='invalid')
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]
weight_2 = [5, 4, 3, 2, 7, 6, 5, 4, 3, 2, 1]
def multiply_reduce(aval, bval):
return sum([(a * b) for (a, b) in zip(aval, bval)])
if multiply_reduce(digits, weight_1) % 11 != 0:
raise ValidationError(self.error_messages['invalid'], code='invalid')
if multiply_reduce(digits, weight_2) % 11 != 0:
raise ValidationError(self.error_messages['invalid'], code='invalid')
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'], code='invalid')
return birthday
[docs]class NOBankAccountNumber(CharField):
"""
A form field for Norwegian bank account numbers.
Performs MOD11 with the custom weights for the Norwegian bank account numbers,
including a check for a remainder of 0, in which event the checksum is also 0.
Usually their string representation is along the lines of ZZZZ.YY.XXXXX, where the last X is the check digit.
They're always a total of 11 digits long, with 10 out of these 11 being the actual account number itself.
* Accepts, and strips, account numbers with extra spaces.
* Accepts, and strips, account numbers provided in form of XXXX.YY.XXXXX.
.. note:: No consideration is taking for banking clearing numbers as of yet, seeing as these are only used between
banks themselves.
.. versionadded:: 1.5
"""
default_error_messages = {
'invalid': _('Enter a valid Norwegian bank account number.'),
'invalid_checksum': _('Invalid control digit. Enter a valid Norwegian bank account number.'),
'invalid_length': _('Invalid length. Norwegian bank account numbers are 11 digits long.'),
}
def validate(self, value):
super().validate(value)
if value in self.empty_values:
# It's alright to be empty.
return
elif not value.isdigit():
# You must only contain decimals.
raise ValidationError(self.error_messages['invalid'], code='invalid')
elif len(value) != 11:
# They only have one length: the number is 10!
# That being said, you always store them with the check digit included, so 11.
raise ValidationError(self.error_messages['invalid_length'], code='invalid_length')
# The control/check digit is the last digit
check_digit = int(value[-1])
bank_number = value[:-1]
# These are the weights by which we multiply to get our checksum digit
weights = [5, 4, 3, 2, 7, 6, 5, 4, 3, 2]
result = sum(w * (int(x)) for w, x in zip(weights, bank_number))
remainder = result % 11
# The checksum is 0 in the event there's no remainder, seeing as we cannot have a checksum of 11
# when 11 is one digit longer than we've got room for
checksum = 0 if remainder == 0 else 11 - remainder
if checksum != check_digit:
raise ValidationError(self.error_messages['invalid_checksum'], code='invalid_checksum')
[docs] def to_python(self, value):
value = super().to_python(value)
if value in self.empty_values:
return value
return value.replace('.', '').replace(' ', '')
def prepare_value(self, value):
value = self.to_python(value)
if value in self.empty_values:
return value
return '{}.{}.{}'.format(value[0:4], value[4:6], value[6:11])