Skip to content

Commit

Permalink
Clean up Dutch local flavor.
Browse files Browse the repository at this point in the history
 - Move validators to validators.py
 - Add model fields for existing form fields
 - Add form fields for existing model fields (except for NLBankAccountField)
 - Move tests to `tests/test_nl/`
 - Increase test coverage to 100%
 - More consistent error messages.
  • Loading branch information
jieter committed Dec 1, 2015
1 parent d00233a commit 0d4ecfb
Show file tree
Hide file tree
Showing 11 changed files with 490 additions and 214 deletions.
1 change: 1 addition & 0 deletions docs/authors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Authors
* Horst Gutmann
* Jaap Roes
* Jacob Kaplan-Moss
* Jan Pieter Waagmeester
* James Bennett
* Jannis Leidel
* Jérémie Ferry
Expand Down
6 changes: 4 additions & 2 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ New flavors:

New fields for existing flavors:

- None
- Added NLZipCodeField, NLProvinceField, NLSoFiNumberField, NLPhoneNumberField model fields.
(`gh-152 <https://github.com/django/django-localflavor/pull/152>`_).

Modifications to existing flavors:

- None
- Moved Dutch validators from localflavor.nl.forms to localflavor.nl.validators
(`gh-152 <https://github.com/django/django-localflavor/pull/152>`_).

Other changes:

Expand Down
96 changes: 23 additions & 73 deletions localflavor/nl/forms.py
Original file line number Diff line number Diff line change
@@ -1,109 +1,59 @@
# -*- coding: utf-8 -*-

"""
NL-specific Form helpers
"""

from __future__ import absolute_import, unicode_literals

import re

from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
from django.forms.fields import Field, Select
from django.utils.encoding import smart_text
from django.utils.translation import ugettext_lazy as _
import six
from django import forms

from .nl_provinces import PROVINCE_CHOICES
from .validators import (NLPhoneNumberFieldValidator,
NLSoFiNumberFieldValidator, NLZipCodeFieldValidator)


pc_re = re.compile('^\d{4}[A-Z]{2}$')
sofi_re = re.compile('^\d{9}$')
numeric_re = re.compile('^\d+$')


class NLZipCodeField(Field):
class NLZipCodeField(forms.CharField):
"""
A Dutch postal code field.
A Dutch zip code field.
"""
default_error_messages = {
'invalid': _('Enter a valid postal code'),
}
default_validators = [NLZipCodeFieldValidator()]

def clean(self, value):
super(NLZipCodeField, self).clean(value)
if value in EMPTY_VALUES:
return ''

value = value.strip().upper().replace(' ', '')
if not pc_re.search(value):
raise ValidationError(self.error_messages['invalid'])
if isinstance(value, six.string_types):
value = value.upper().replace(' ', '')

if int(value[:4]) < 1000:
raise ValidationError(self.error_messages['invalid'])
if len(value) == 6:
value = '%s %s' % (value[:4], value[4:])

return '%s %s' % (value[:4], value[4:])
return super(NLZipCodeField, self).clean(value)


class NLProvinceSelect(Select):
class NLProvinceSelect(forms.Select):
"""
A Select widget that uses a list of provinces of the Netherlands as its
A Select widget that uses a list of provinces of the Netherlands as it's
choices.
"""
def __init__(self, attrs=None):
super(NLProvinceSelect, self).__init__(attrs, choices=PROVINCE_CHOICES)


class NLPhoneNumberField(Field):
class NLPhoneNumberField(forms.CharField):
"""
A Dutch telephone number field.
"""
default_error_messages = {
'invalid': _('Enter a valid phone number'),
}

def clean(self, value):
super(NLPhoneNumberField, self).clean(value)
if value in EMPTY_VALUES:
return ''

phone_nr = re.sub('[\-\s\(\)]', '', smart_text(value))

if len(phone_nr) == 10 and numeric_re.search(phone_nr):
return value
default_validators = [NLPhoneNumberFieldValidator()]

if phone_nr[:3] == '+31' and len(phone_nr) == 12 and \
numeric_re.search(phone_nr[3:]):
return value

raise ValidationError(self.error_messages['invalid'])


class NLSoFiNumberField(Field):
class NLSoFiNumberField(forms.CharField):
"""
A Dutch social security number (SoFi/BSN) field.
http://nl.wikipedia.org/wiki/Sofinummer
"""
default_error_messages = {
'invalid': _('Enter a valid SoFi number'),
}

def clean(self, value):
super(NLSoFiNumberField, self).clean(value)
if value in EMPTY_VALUES:
return ''

if not sofi_re.search(value):
raise ValidationError(self.error_messages['invalid'])

if int(value) == 0:
raise ValidationError(self.error_messages['invalid'])

checksum = 0
for i in range(9, 1, -1):
checksum += int(value[9 - i]) * i
checksum -= int(value[-1])

if checksum % 11 != 0:
raise ValidationError(self.error_messages['invalid'])
default_validators = [NLSoFiNumberFieldValidator()]

return value
def __init__(self, *args, **kwargs):
kwargs['max_length'] = 9
super(NLSoFiNumberField, self).__init__(*args, **kwargs)
124 changes: 86 additions & 38 deletions localflavor/nl/models.py
Original file line number Diff line number Diff line change
@@ -1,62 +1,110 @@
import re
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from __future__ import absolute_import, unicode_literals

from django.db import models
from django.utils.translation import ugettext_lazy as _

from . import forms
from .nl_provinces import PROVINCE_CHOICES
from .validators import (NLBankAccountNumberFieldValidator,
NLPhoneNumberFieldValidator,
NLSoFiNumberFieldValidator, NLZipCodeFieldValidator)


class NLBankAccountNumberFieldValidator(RegexValidator):
class NLZipCodeField(models.CharField):
"""
Validation for Dutch bank accounts.
A Dutch zip code model field.
Validation references:
http://www.mobilefish.com/services/elfproef/elfproef.php
http://www.credit-card.be/BankAccount/ValidationRules.htm#NL_Validation
This model field uses :class:`validators.NLZipCodeFieldValidator` for validation.
.. versionadded:: 1.1
.. versionadded:: 1.3
"""
default_error_messages = {
'invalid': _('Enter a valid bank account number'),
'wrong_length': _('Bank account numbers have 1 - 7, 9 or 10 digits'),
}

def __init__(self, regex=None, message=None, code=None):
super(NLBankAccountNumberFieldValidator, self).__init__(regex='^[0-9]+$',
message=self.default_error_messages['invalid'])
self.no_leading_zeros_regex = re.compile('[1-9]+')
description = _('Dutch zipcode')

validators = [NLZipCodeFieldValidator()]

def __call__(self, value):
super(NLBankAccountNumberFieldValidator, self).__call__(value)
def __init__(self, *args, **kwargs):
kwargs['max_length'] = 7
super(NLZipCodeField, self).__init__(*args, **kwargs)

def to_python(self, value):
value = super(NLZipCodeField, self).to_python(value)
if value is not None:
value = value.upper().replace(' ', '')
return '%s %s' % (value[:4], value[4:])
return value

# Need to check for values over the field's max length before the zero are stripped.
# This check is needed to allow this validator to be used without Django's MaxLengthValidator.
if len(value) > 10:
raise ValidationError(self.default_error_messages['wrong_length'])
def formfield(self, **kwargs):
defaults = {'form_class': forms.NLZipCodeField}
defaults.update(kwargs)
return super(NLZipCodeField, self).formfield(**defaults)

# Strip the leading zeros.
m = re.search(self.no_leading_zeros_regex, value)
if not m:
raise ValidationError(self.default_error_messages['invalid'])
value = value[m.start():]

if len(value) != 9 and len(value) != 10 and not 1 <= len(value) <= 7:
raise ValidationError(self.default_error_messages['wrong_length'])
class NLProvinceField(models.CharField):
"""
A Dutch Provice field.
.. versionadded:: 1.3
"""
description = _('Dutch province')

def __init__(self, *args, **kwargs):
kwargs.update({
'choices': PROVINCE_CHOICES,
'max_length': 3
})
super(NLProvinceField, self).__init__(*args, **kwargs)


class NLSoFiNumberField(models.CharField):
"""
A Dutch social security number (SoFi)
This model field uses :class:`validators.NLSoFiNumberFieldValidator` for validation.
# Perform the eleven test validation on non-PostBank numbers.
if len(value) == 9 or len(value) == 10:
if len(value) == 9:
value = "0" + value
.. versionadded:: 1.3
"""
description = _('Dutch social security number (SoFi)')

validators = [NLSoFiNumberFieldValidator()]

def __init__(self, *args, **kwargs):
kwargs.setdefault('max_length', 12)
super(NLSoFiNumberField, self).__init__(*args, **kwargs)

def formfield(self, **kwargs):
defaults = {'form_class': forms.NLSoFiNumberField}
defaults.update(kwargs)
return super(NLSoFiNumberField, self).formfield(**defaults)


class NLPhoneNumberField(models.CharField):
"""
Dutch phone number model field
This model field uses :class:`validators.NLPhoneNumberFieldValidator` for validation.
.. versionadded:: 1.3
"""
description = _('Dutch phone number')

validator = [NLPhoneNumberFieldValidator()]

def __init__(self, *args, **kwargs):
kwargs.setdefault('max_length', 12)
super(NLPhoneNumberField, self).__init__(*args, **kwargs)

eleven_test_sum = sum([int(a) * b for a, b in zip(value, range(1, 11))])
if eleven_test_sum % 11 != 0:
raise ValidationError(self.default_error_messages['invalid'])
def formfield(self, **kwargs):
defaults = {'form_class': forms.NLPhoneNumberField}
defaults.update(kwargs)
return super(NLPhoneNumberField, self).formfield(**defaults)


class NLBankAccountNumberField(models.CharField):
"""
A Dutch bank account model field.
This model field uses :class:`.NLBankAccountNumberFieldValidator` for validation.
This model field uses :class:`validators.NLBankAccountNumberFieldValidator` for validation.
.. versionadded:: 1.1
"""
Expand Down
2 changes: 1 addition & 1 deletion localflavor/nl/nl_provinces.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from __future__ import absolute_import, unicode_literals

from django.utils.translation import ugettext_lazy as _

Expand Down
Loading

0 comments on commit 0d4ecfb

Please sign in to comment.