Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions docs/authors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Authors
* James Bennett
* Jannis Leidel
* Jan Pieter Waagmeester
* Jarmo van Lenthe
* Jérémie Ferry
* Jocelyn Delalande
* Johannes Hoppe
Expand Down
4 changes: 2 additions & 2 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ New flavors:

New fields for existing flavors:

- None
- `NLLicensePlateField` in NL flavor.

Modifications to existing flavors:

- None
- Added `NLLicensePlateField` to NL flavor.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The entry is only needed once in the 'New fields for existing flavors' section.


Other changes:

Expand Down
53 changes: 52 additions & 1 deletion localflavor/nl/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@

from __future__ import unicode_literals

import re

from django import forms
from django.utils import six

from .nl_provinces import PROVINCE_CHOICES
from .validators import NLBSNFieldValidator, NLZipCodeFieldValidator
from .validators import NLBSNFieldValidator, NLLicensePlateFieldValidator, NLZipCodeFieldValidator


class NLZipCodeField(forms.CharField):
Expand Down Expand Up @@ -48,3 +50,52 @@ class NLBSNFormField(forms.CharField):
def __init__(self, *args, **kwargs):
kwargs['max_length'] = 9
super(NLBSNFormField, self).__init__(*args, **kwargs)


class NLLicensePlateFormField(forms.CharField):
"""
A Dutch license plate field.

https://www.rdw.nl/
https://nl.wikipedia.org/wiki/Nederlands_kenteken

.. versionadded:: 2.1
"""

default_validators = [NLLicensePlateFieldValidator()]

SANITIZE_REGEXS = {
"sidecode1": re.compile(r"^([A-Z]{2})([0-9]{2})([0-9]{2})$"), # AA-99-99
"sidecode2": re.compile(r"^([0-9]{2})([0-9]{2})([A-Z]{2})$"), # 99-99-AA
"sidecode3": re.compile(r"^([0-9]{2})([A-Z]{2})([0-9]{2})$"), # 99-AA-99
"sidecode4": re.compile(r"^([A-Z]{2})([0-9]{2})([A-Z]{2})$"), # AA-99-AA
"sidecode5": re.compile(r"^([A-Z]{2})([A-Z]{2})([0-9]{2})$"), # AA-AA-99
"sidecode6": re.compile(r"^([0-9]{2})([A-Z]{2})([A-Z]{2})$"), # 99-AA-AA
"sidecode7": re.compile(r"^([0-9]{2})([A-Z]{3})([0-9]{1})$"), # 99-AAA-9
"sidecode8": re.compile(r"^([0-9]{1})([A-Z]{3})([0-9]{2})$"), # 9-AAA-99
"sidecode9": re.compile(r"^([A-Z]{2})([0-9]{3})([A-Z]{1})$"), # AA-999-A
"sidecode10": re.compile(r"^([A-Z]{1})([0-9]{3})([A-Z]{2})$"), # A-999-AA
"sidecode11": re.compile(r"^([A-Z]{3})([0-9]{2})([A-Z]{1})$"), # AAA-99-A
"sidecode12": re.compile(r"^([A-Z]{1})([0-9]{2})([A-Z]{3})$"), # A-99-AAA
"sidecode13": re.compile(r"^([0-9]{1})([A-Z]{2})([0-9]{3})$"), # 9-AA-999
"sidecode14": re.compile(r"^([0-9]{3})([A-Z]{2})([0-9]{1})$"), # 999-AA-9
"sidecode_koninklijk_huis": re.compile(r"^(AA)([0-9]{2,3})(([0-9]{2})?)$"), # AA-99(-99)?
"sidecode_internationaal_gerechtshof": re.compile(r"^(CDJ)([0-9]{3})$"), # CDJ-999
"sidecode_bijzondere_toelating": re.compile(r"^(ZZ)([0-9]{2})([0-9]{2})$"), # ZZ-99-99
"sidecode_tijdelijk_voor_een_dag": re.compile(r"^(F)([0-9]{2})([0-9]{2})$"), # F-99-99
"sidecode_voertuig_binnen_of_buiten_nederland_brengen": re.compile(r"^(Z)([0-9]{2})([0-9]{2})$"), # Z-99-99
}

def __init__(self, *args, **kwargs):
kwargs['max_length'] = 8
super(NLLicensePlateFormField, self).__init__(*args, **kwargs)

def clean(self, value):
value = super(NLLicensePlateFormField, self).clean(value)
if value:
value = value.upper().replace('-', '')
for sidecode, regex in self.SANITIZE_REGEXS.items():
match = regex.match(value)
if match:
return '-'.join(match.groups())
return value
27 changes: 26 additions & 1 deletion localflavor/nl/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from . import forms
from .nl_provinces import PROVINCE_CHOICES
from .validators import NLBSNFieldValidator, NLZipCodeFieldValidator
from .validators import NLBSNFieldValidator, NLLicensePlateFieldValidator, NLZipCodeFieldValidator


class NLZipCodeField(models.CharField):
Expand Down Expand Up @@ -82,3 +82,28 @@ def formfield(self, **kwargs):
defaults = {'form_class': forms.NLBSNFormField}
defaults.update(kwargs)
return super(NLBSNField, self).formfield(**defaults)


class NLLicensePlateField(models.CharField):
"""
A Dutch car license plate.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's probably better to remove 'car' as this can be used for all Dutch license plates.


This model field uses :class:`validators.NLLicensePlateFieldValidator` for validation.

.. versionadded:: 2.1
"""

description = _('Dutch license plate')

default_form_field = forms.NLLicensePlateFormField

validators = [NLLicensePlateFieldValidator()]

def __init__(self, *args, **kwargs):
kwargs.setdefault('max_length', 8)
super(NLLicensePlateField, self).__init__(*args, **kwargs)

def formfield(self, **kwargs):
defaults = {'form_class': forms.NLLicensePlateFormField}
defaults.update(kwargs)
return super(NLLicensePlateField, self).formfield(**defaults)
36 changes: 36 additions & 0 deletions localflavor/nl/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,39 @@ def __call__(self, value):

if not self.bsn_checksum_ok(value):
raise ValidationError(self.error_message)


class NLLicensePlateFieldValidator(RegexValidator):
"""
Validation for Dutch license plates.

.. versionadded:: 2.1
"""

error_message = _('Enter a valid license plate')

VALIDATION_REGEXS = {
"sidecode1": r"^[A-Z]{2}-[0-9]{2}-[0-9]{2}$", # AA-99-99
"sidecode2": r"^[0-9]{2}-[0-9]{2}-[A-Z]{2}$", # 99-99-AA
"sidecode3": r"^[0-9]{2}-[A-Z]{2}-[0-9]{2}$", # 99-AA-99
"sidecode4": r"^[A-Z]{2}-[0-9]{2}-[A-Z]{2}$", # AA-99-AA
"sidecode5": r"^[A-Z]{2}-[A-Z]{2}-[0-9]{2}$", # AA-AA-99
"sidecode6": r"^[0-9]{2}-[A-Z]{2}-[A-Z]{2}$", # 99-AA-AA
"sidecode7": r"^[0-9]{2}-[A-Z]{3}-[0-9]{1}$", # 99-AAA-9
"sidecode8": r"^[0-9]{1}-[A-Z]{3}-[0-9]{2}$", # 9-AAA-99
"sidecode9": r"^[A-Z]{2}-[0-9]{3}-[A-Z]{1}$", # AA-999-A
"sidecode10": r"^[A-Z]{1}-[0-9]{3}-[A-Z]{2}$", # A-999-AA
"sidecode11": r"^[A-Z]{3}-[0-9]{2}-[A-Z]{1}$", # AAA-99-A
"sidecode12": r"^[A-Z]{1}-[0-9]{2}-[A-Z]{3}$", # A-99-AAA
"sidecode13": r"^[0-9]{1}-[A-Z]{2}-[0-9]{3}$", # 9-AA-999
"sidecode14": r"^[0-9]{3}-[A-Z]{2}-[0-9]{1}$", # 999-AA-9
"sidecode_koninklijk_huis": r"^AA-[0-9]{2,3}(-[0-9]{2})?$", # AA-99(-99)?
"sidecode_internationaal_gerechtshof": r"^CDJ-[0-9]{3}$", # CDJ-999
"sidecode_bijzondere_toelating": r"^ZZ-[0-9]{2}-[0-9]{2}$", # ZZ-99-99
"sidecode_tijdelijk_voor_een_dag": r"^F-[0-9]{2}-[0-9]{2}$", # F-99-99
"sidecode_voertuig_binnen_of_buiten_nederland_brengen": r"^Z-[0-9]{2}-[0-9]{2}$", # Z-99-99
}

def __init__(self):
regex = r'(' + r'|'.join(self.VALIDATION_REGEXS.values()) + r')'
super(NLLicensePlateFieldValidator, self).__init__(regex=regex, message=self.error_message)
9 changes: 8 additions & 1 deletion tests/test_nl/forms.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
from django.forms import ModelForm

from .models import NLPlace
from .models import NLCar, NLPlace


class NLPlaceForm(ModelForm):

class Meta:
model = NLPlace
fields = ('zipcode', 'province', 'bsn')


class NLCarForm(ModelForm):

class Meta:
model = NLCar
fields = ('license_plate', )
10 changes: 9 additions & 1 deletion tests/test_nl/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.db import models

from localflavor.nl.models import NLBSNField, NLProvinceField, NLZipCodeField
from localflavor.nl.models import NLBSNField, NLLicensePlateField, NLProvinceField, NLZipCodeField


class NLPlace(models.Model):
Expand All @@ -11,3 +11,11 @@ class NLPlace(models.Model):

class Meta:
app_label = 'test_nl'


class NLCar(models.Model):

license_plate = NLLicensePlateField()

class Meta:
app_label = 'test_nl'
66 changes: 64 additions & 2 deletions tests/test_nl/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

from localflavor.nl import forms, models, validators

from .forms import NLPlaceForm
from .models import NLPlace
from .forms import NLCarForm, NLPlaceForm
from .models import NLCar, NLPlace


class NLLocalFlavorValidatorTests(SimpleTestCase):
Expand Down Expand Up @@ -46,6 +46,19 @@ def test_NLBSNValidator(self):
]
self.assert_validator(validators.NLBSNFieldValidator(), valid, invalid)

def test_NLLicensePlateValidator(self):
valid = [
'12-AA-13',
'CDJ-123',
'AA-01',
]
invalid = [
'ZZZ-123',
'AA-AAA-1',
'11-123-A',
]
self.assert_validator(validators.NLLicensePlateFieldValidator(), valid, invalid)


class NLLocalFlavorModelTests(SimpleTestCase):
def test_NLZipCodeField(self):
Expand Down Expand Up @@ -85,6 +98,27 @@ def test_NL_model_cleanup(self):
m.clean_fields()
self.assertEquals(str(m.zipcode), '2403 BW')

def test_NL_car(self):
m = NLCar(**{
'license_plate': 'AB-12-CD'
})

m.clean_fields()
self.assertEqual(str(m.license_plate), 'AB-12-CD')

def test_NL_car_cleanup(self):
m = NLCar(**{
'license_plate': 'AA11AA'
})

# incorrect license plate number, should raise an error
self.assertRaises(ValidationError, lambda: m.clean_fields())

# correct license plate number, should be clean now
m.license_plate = 'AA-11-AA'
m.clean_fields()
self.assertEquals(str(m.license_plate), 'AA-11-AA')


class NLLocalFlavorFormTests(SimpleTestCase):
def test_NLZipCodeField(self):
Expand Down Expand Up @@ -156,3 +190,31 @@ def test_NL_ModelForm_valid(self):
'bsn': '123456782',
})
self.assertTrue(form.is_valid())

def test_NLLicensePlateFormField(self):
error_invalid = ['Enter a valid license plate']
valid = {
'12-AAA-1': '12-AAA-1',
'12-AAA-1': '12-AAA-1',
'CDJ-123': 'CDJ-123',
}
invalid = {
'AAA-AA-1': error_invalid,
'CDA-111': error_invalid,
'a1s2d3': error_invalid,
}
self.assertFieldOutput(forms.NLLicensePlateFormField, valid, invalid)

def test_NL_car_ModelForm_valid(self):
form = NLCarForm({
'license_plate': 'AA-11-AA',
})

self.assertTrue(form.is_valid())

def test_NL_car_ModelForm_invalid(self):
form = NLCarForm({
'license_plate': 'AA-AAA-1',
})

self.assertFalse(form.is_valid())