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

feat: Add KE localflavor #500

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions docs/authors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,4 @@ Authors
* Vishal Pandey
* Vladimir Nani
* Abhineet Tamrakar
* Shalom Nyende
1 change: 1 addition & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Changelog
------------------

New flavors:
- Kenya localflavor

- Nepal LocalFlavor: Support for Nepal added
(`gh-451 <https://github.com/django/django-localflavor/pull/451>`_).
Expand Down
Empty file added localflavor/ke/__init__.py
Empty file.
2 changes: 2 additions & 0 deletions localflavor/ke/deprecation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class RemovedInLocalflavor30Warning(PendingDeprecationWarning):
...
199 changes: 199 additions & 0 deletions localflavor/ke/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
"""Kenya-specific Form Helpers"""

import re

from django.forms import ValidationError
from django.forms.fields import CharField, RegexField, Select
from django.utils.translation import gettext_lazy as _

from .ke_counties import COUNTY_CHOICES

ke_po_box_re = re.compile(r"\A\d{5,5}\Z")
ke_kra_pin_regex = re.compile(r"^(A|P)\d{9}[A-Z]$")
ke_passport_regex = re.compile(r"^[A-Z]\d{6,7}$")
ke_national_id_regex = re.compile(r"^\d{7,8}$")


class KEPostalCodeField(CharField):
"""
A form field that validates its input as a Kenyan Postal Code.
.. versionadded:: 4.0
"""

default_error_messages = {
"invalid": _("Enter a valid Kenyan Postal code in the format 12345")
}

def clean(self, value: str):
"""Validates KE Postal Code

Args:
value (_type_): _description_

Raises:
ValidationError: _description_

Returns:
_type_: _description_
"""
value = super().clean(value)
if value in self.empty_values:
return self.empty_value

# Strip out spaces and dashes
value = value.replace(" ", "").replace("-", "")
match = re.match(ke_po_box_re, value)
if not match:
raise ValidationError(self.error_messages.get("invalid"))
return value


class KEKRAPINField(CharField):
"""
TODO

A form field that validates input as a Kenya Revenue Authority PIN
(Personal Identification Number) Number.

A Kenyan KRA (Kenya Revenue Authority) PIN (Personal Identification Number)

is typically 11 characters long, consisting of the letter 'A' or 'P' followed

by 9 digits and ending with a letter (e.g., A123456789B or P987654321C).

Validates 2 different formats:

POXXXXXXXX - Company/Institution

AXXXXXXXXX - Individuals

.. versionadded:: 4.0

"""

default_error_messages = {
"invalid": _(
"Enter a valid Kenyan KRA PIN Number in the format A123456789B or P987654321C"
),
}

def clean(self, value):
"""Runs the validation checks

Args:
value (_type_): _description_

Raises:
ValidationError: _description_

Returns:
_type_: _description_
"""
value = super().clean(value)
if value in self.empty_values:
return self.empty_value

# Strip out spaces and dashes
value = value.replace(" ", "").replace("-", "")
match = re.match(ke_kra_pin_regex, value)
if not match:
raise ValidationError(self.error_messages.get("invalid"))
return value.upper()


class KENationalIDNumberField(CharField):
"""
A form field that validates its input as a Kenyan National ID Number.
.. versionadded:: 4.0
"""

default_error_messages = {
"invalid": _(
"Enter a valid Kenyan National ID Number in the format 1234567 or 12345678"
)
}

def clean(self, value):
"""Runs the validation checks for KE National ID Number"""
value = super().clean(value)
if value in self.empty_values:
return self.empty_value

# Strip out spaces and dashes
value = value.replace(" ", "").replace("-", "")
match = re.match(ke_national_id_regex, value)
if not match:
raise ValidationError(self.error_messages.get("invalid"))
return value


class KEPassportNumberField(CharField):
"""
A form field that validates its input as a Kenyan Passport Number.
.. versionadded:: 4.0
"""

default_error_messages = {
"invalid": _(
"Enter a valid Kenyan Passport Number in the format A123456 or B1234567"
)
}

def clean(self, value):
"""Runs the validation checks for KE Passport Number"""
value = super().clean(value)
if value in self.empty_values:
return self.empty_value

# Strip out spaces and dashes
value = value.replace(" ", "").replace("-", "")
match = re.match(ke_passport_regex, value)
if not match:
raise ValidationError(self.error_messages.get("invalid"))
return value.upper()


class KENSSFNumberField(RegexField):
"""
TODO

Kenya National Social Security Fund
"""

...


class KENHIFNumberField(RegexField):
"""
TODO

Kenya National Hospital Insurance Fund
"""

...


class KECompanyRegNumberField(RegexField):
"""
Kenya Companies Reg. Number
"""

...


class KEPayBillNumber(RegexField):
"""
MPESA PayBill
"""

...


class KECountySelectField(Select):
"""
A Select widget listing Kenyan Counties as the choices
.. versionadded:: 4.0
"""

def __init__(self, attrs=None) -> None:
super().__init__(attrs, choices=COUNTY_CHOICES)
56 changes: 56 additions & 0 deletions localflavor/ke/ke_counties.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""
Kenya Counties Data
"""

from django.utils.translation import gettext_lazy as _

# The 47 counties of Kenya
COUNTY_CHOICES = (
("MOMBASA", _("MOMBASA")),
("KWALE", _("KWALE")),
("KILIFI", _("KILIFI")),
("TANA RIVER", _("TANA RIVER")),
("LAMU", _("LAMU")),
("TAITA-TAVETA", _("TAITA-TAVETA")),
("GARISSA", _("GARISSA")),
("WAJIR", _("WAJIR")),
("MANDERA", _("MANDERA")),
("MARSABIT", _("MARSABIT")),
("ISIOLO", _("ISIOLO")),
("MERU", _("MERU")),
("THARAKA-NITHI", _("THARAKA-NITHI")),
("EMBU", _("EMBU")),
("KITUI", _("KITUI")),
("MACHAKOS", _("MACHAKOS")),
("MAKUENI", _("MAKUENI")),
("NYANDARUA", _("NYANDARUA")),
("NYERI", _("NYERI")),
("KIRINYAGA", _("KIRINYAGA")),
("MURANGA", _("MURANGA")),
("KIAMBU", _("KIAMBU")),
("TURKANA", _("TURKANA")),
("WEST POKOT", _("WEST POKOT")),
("SAMBURU", _("SAMBURU")),
("TRANS-NZOIA", _("TRANS-NZOIA")),
("UASIN GISHU", _("UASIN GISHU")),
("ELGEYO-MARAKWET", _("ELGEYO-MARAKWET")),
("NANDI", _("NANDI")),
("BARINGO", _("BARINGO")),
("LAIKIPIA", _("LAIKIPIA")),
("NAKURU", _("NAKURU")),
("NAROK", _("NAROK")),
("KAJIADO", _("KAJIADO")),
("KERICHO", _("KERICHO")),
("BOMET", _("BOMET")),
("KAKAMEGA", _("KAKAMEGA")),
("VIHIGA", _("VIHIGA")),
("BUNGOMA", _("BUNGOMA")),
("BUSIA", _("BUSIA")),
("SIAYA", _("SIAYA")),
("KISUMU", _("KISUMU")),
("HOMA BAY", _("HOMA BAY")),
("MIGORI", _("MIGORI")),
("KISII", _("KISII")),
("NYAMIRA", _("NYAMIRA")),
("NAIROBI", _("NAIROBI")),
)
34 changes: 34 additions & 0 deletions localflavor/ke/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from typing import Any
from django.db.models import CharField
from django.utils.translation import gettext_lazy as _

from .forms import (
KENationalIDNumberField,
KEKRAPINField,
KENHIFNumberField,
KENSSFNumberField,
KEPassportNumberField,
KEPostalCodeField as KEPostalCodeFormField,
)


class KEPostalCodeField(CharField):
"""
A model field that stores the Kenyan Postal Codes
.. versionadded:: 4.0
"""
description = _("Kenya Postal Code")

def __init__(self, *args, **kwargs) -> None:
kwargs.update(max_length=8)
super().__init__(*args, **kwargs)

def formfield(self, **kwargs) -> Any:
defaults = {"form_class": KEPostalCodeFormField}
defaults.update(kwargs)
return super().formfield(**defaults)





59 changes: 59 additions & 0 deletions tests/test_ke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from django.test import SimpleTestCase
from django.forms import ValidationError
from .forms import KEPostalCodeField, KEKRAPINField, KENationalIDNumberField, KEPassportNumberField

class KEPostalCodeFieldTest(SimpleTestCase):
def test_valid_postal_code(self):
field = KEPostalCodeField()
valid_postal_codes = ["12345", "54321"]
for postal_code in valid_postal_codes:
self.assertEqual(field.clean(postal_code), "12345")

def test_invalid_postal_code(self):
field = KEPostalCodeField()
invalid_postal_codes = ["1234", "ABCDE", "12 345", "12-345"]
for postal_code in invalid_postal_codes:
with self.assertRaises(ValidationError):
field.clean(postal_code)

class KEKRAPINFieldTest(SimpleTestCase):
def test_valid_kra_pin(self):
field = KEKRAPINField()
valid_pins = ["A123456789B", "P987654321C"]
for pin in valid_pins:
self.assertEqual(field.clean(pin), pin)

def test_invalid_kra_pin(self):
field = KEKRAPINField()
invalid_pins = ["1234567890", "A123456789", "P987654321", "A12-3456789B"]
for pin in invalid_pins:
with self.assertRaises(ValidationError):
field.clean(pin)

class KENationalIDNumberFieldTest(SimpleTestCase):
def test_valid_national_id(self):
field = KENationalIDNumberField()
valid_ids = ["1234567", "12345678"]
for id_number in valid_ids:
self.assertEqual(field.clean(id_number), id_number)

def test_invalid_national_id(self):
field = KENationalIDNumberField()
invalid_ids = ["12345", "12345A", "12-34567", "123456789"]
for id_number in invalid_ids:
with self.assertRaises(ValidationError):
field.clean(id_number)

class KEPassportNumberFieldTest(SimpleTestCase):
def test_valid_passport_number(self):
field = KEPassportNumberField()
valid_passports = ["A123456", "B1234567"]
for passport in valid_passports:
self.assertEqual(field.clean(passport), passport)

def test_invalid_passport_number(self):
field = KEPassportNumberField()
invalid_passports = ["12345", "A1234567B", "AB-123456"]
for passport in invalid_passports:
with self.assertRaises(ValidationError):
field.clean(passport)