Skip to content
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 @@ -138,3 +138,4 @@ Authors
* Abhineet Tamrakar
* Tudor Amariei
* Dishan Sachin
* Sami El Achi
3 changes: 2 additions & 1 deletion docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ Changelog

New flavors:

- None
- United Arab Emirates LocalFlavor
(`gh-527 <https://github.com/django/django-localflavor/pull/527>`_).

New fields for existing flavors:

Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ validate Finnish social security numbers.
:columns: 4

* :doc:`localflavor/ar`
* :doc:`localflavor/ae`
* :doc:`localflavor/at`
* :doc:`localflavor/au`
* :doc:`localflavor/be`
Expand Down
21 changes: 21 additions & 0 deletions docs/localflavor/ae.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
United Arab Emirates (``ae``)
=============================

Forms
-----

.. automodule:: localflavor.ae.forms
:members:

Models
------

.. automodule:: localflavor.ae.models
:members:

Data
----

.. autodata:: localflavor.ae.ae_emirates.EMIRATE_CHOICES

.. autodata:: localflavor.ae.ae_emirates.EMIRATES_NORMALIZED
Empty file added localflavor/ae/__init__.py
Empty file.
44 changes: 44 additions & 0 deletions localflavor/ae/ae_emirates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""UAE emirates."""

from django.utils.translation import gettext_lazy as _

#: A list of UAE emirates as `(abbreviation, name)` tuples.
EMIRATE_CHOICES = (
('AZ', _('Abu Dhabi')),
('AJ', _('Ajman')),
('DU', _('Dubai')),
('FU', _('Fujairah')),
('RA', _('Ras Al Khaimah')),
('SH', _('Sharjah')),
('UQ', _('Umm Al Quwain')),
)

#: Dictionary that maps emirate names and abbreviations to the
#: canonical abbreviation.
EMIRATES_NORMALIZED = {}

# Abbreviations
for abbr, name in EMIRATE_CHOICES:
EMIRATES_NORMALIZED[abbr.lower()] = abbr
EMIRATES_NORMALIZED[abbr.upper()] = abbr

# Names
EMIRATES_NORMALIZED.update({
'abu dhabi': 'AZ',
'ajman': 'AJ',
'dubai': 'DU',
'fujairah': 'FU',
'ras al khaimah': 'RA',
'ras al-khaimah': 'RA',
'sharjah': 'SH',
'umm al quwain': 'UQ',
'umm al-quwain': 'UQ',
# Arabic names
'أبو ظبي': 'AZ',
'عجمان': 'AJ',
'دبي': 'DU',
'الفجيرة': 'FU',
'رأس الخيمة': 'RA',
'الشارقة': 'SH',
'أم القيوين': 'UQ',
})
163 changes: 163 additions & 0 deletions localflavor/ae/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
"""UAE-specific Form helpers."""

import re

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

from .ae_emirates import EMIRATE_CHOICES
from .validators import UAEEmiratesIDValidator, UAEPostalCodeValidator, UAEPOBoxValidator


class UAEEmiratesIDField(CharField):
"""
A field for validating UAE Emirates ID numbers.

UAE Emirates ID format: 784-YYYY-NNNNNNN-N
Where:
- 784 is the UAE country code
- YYYY is the year of birth (1900-current year)
- NNNNNNN is a 7-digit sequence number
- N is a single check digit

.. versionadded:: 5.1
"""

default_error_messages = {
'invalid': _('Enter a valid UAE Emirates ID number in format 784-YYYY-NNNNNNN-N.'),
}

def __init__(self, **kwargs):
super().__init__(**kwargs)
self.validators.append(UAEEmiratesIDValidator())

def clean(self, value):
value = super().clean(value)
if value in self.empty_values:
return value

# Remove any dashes or spaces and return clean 15-digit format
clean_value = re.sub(r'[\s\-]', '', str(value))

# Format as 784-YYYY-NNNNNNN-N for consistency
if len(clean_value) == 15:
formatted = f"{clean_value[:3]}-{clean_value[3:7]}-{clean_value[7:14]}-{clean_value[14]}"
return formatted

return clean_value


class UAEEmirateField(ChoiceField):
"""
A choice field that uses a list of UAE Emirates as its choices.

.. versionadded:: 5.1
"""

def __init__(self, **kwargs):
kwargs.setdefault('choices', EMIRATE_CHOICES)
super().__init__(**kwargs)


class UAEEmirateSelect(Select):
"""
A Select widget that uses a list of UAE Emirates as its choices.

.. versionadded:: 5.1
"""

def __init__(self, attrs=None):
super().__init__(attrs, choices=EMIRATE_CHOICES)


class UAEPostalCodeField(CharField):
"""
A field for validating UAE postal codes.

UAE doesn't use postal codes, but some systems require "00000".
This field accepts "00000" or empty values.

.. versionadded:: 5.1
"""

default_error_messages = {
'invalid': _('Enter a valid UAE postal code. Use 00000 if postal code is required.'),
}

def __init__(self, **kwargs):
kwargs.setdefault('required', False)
super().__init__(**kwargs)
self.validators.append(UAEPostalCodeValidator())

def clean(self, value):
value = super().clean(value)
if value in self.empty_values:
return self.empty_value

# Normalize to 00000 if needed
clean_value = str(value).strip()
if clean_value in ('00000', ''):
return clean_value

return value


class UAEPOBoxField(CharField):
"""
A field for validating UAE P.O. Box numbers.

Accepts formats: "P.O. Box XXXXX", "PO Box XXXXX", "POB XXXXX", or just "XXXXX"

.. versionadded:: 5.1
"""

default_error_messages = {
'invalid': _('Enter a valid P.O. Box number.'),
}

def __init__(self, **kwargs):
super().__init__(**kwargs)
self.validators.append(UAEPOBoxValidator())

def clean(self, value):
value = super().clean(value)
if value in self.empty_values:
return value

# Clean up the value
clean_value = str(value).strip().upper()

# Remove "P.O. BOX" or "PO BOX" prefix if present
clean_value = re.sub(r'^(P\.?O\.?\s*BOX\s*)', '', clean_value)

# Return just the number part
if re.match(r'^\d{1,10}$', clean_value):
return clean_value

return value


class UAETaxRegistrationNumberField(RegexField):
"""
A field for validating UAE Tax Registration Numbers (TRN).

UAE TRN is a 15-digit number used for VAT registration.

.. versionadded:: 5.1
"""

default_error_messages = {
'invalid': _('Enter a valid UAE Tax Registration Number (15 digits).'),
}

def __init__(self, **kwargs):
super().__init__(r'^\d{15}$', **kwargs)

def clean(self, value):
value = super().clean(value)
if value in self.empty_values:
return value

# Remove any spaces or formatting
clean_value = re.sub(r'\s', '', str(value))
return clean_value
127 changes: 127 additions & 0 deletions localflavor/ae/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"""UAE-specific Model helpers."""

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

from .ae_emirates import EMIRATE_CHOICES
from .validators import UAEEmiratesIDValidator, UAEPostalCodeValidator, UAEPOBoxValidator


class UAEEmiratesIDField(models.CharField):
"""
A model field for UAE Emirates ID numbers.

Stores Emirates ID as a 15-digit string with format: 784-YYYY-NNNNNNN-N

.. versionadded:: 5.1
"""

description = _("UAE Emirates ID number")

def __init__(self, *args, **kwargs):
kwargs.setdefault('max_length', 18) # 15 digits + 3 dashes = 18 characters
kwargs.setdefault('validators', []).append(UAEEmiratesIDValidator())
super().__init__(*args, **kwargs)

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


class UAEEmirateField(models.CharField):
"""
A model field for UAE Emirates.

Stores emirate as a 2-character abbreviation (e.g., 'DU' for Dubai).

.. versionadded:: 5.1
"""

description = _("UAE Emirate")

def __init__(self, *args, **kwargs):
kwargs.setdefault('max_length', 2)
kwargs.setdefault('choices', EMIRATE_CHOICES)
super().__init__(*args, **kwargs)

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

def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
del kwargs['choices']
return name, path, args, kwargs


class UAEPostalCodeField(models.CharField):
"""
A model field for UAE postal codes.

UAE doesn't use postal codes, but stores "00000" if required.

.. versionadded:: 5.1
"""

description = _("UAE postal code")

def __init__(self, *args, **kwargs):
kwargs.setdefault('max_length', 5)
kwargs.setdefault('validators', []).append(UAEPostalCodeValidator())
kwargs.setdefault('blank', True)
super().__init__(*args, **kwargs)

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


class UAEPOBoxField(models.CharField):
"""
A model field for UAE P.O. Box numbers.

Stores P.O. Box numbers as strings (numeric only).

.. versionadded:: 5.1
"""

description = _("UAE P.O. Box number")

def __init__(self, *args, **kwargs):
kwargs.setdefault('max_length', 10)
kwargs.setdefault('validators', []).append(UAEPOBoxValidator())
super().__init__(*args, **kwargs)

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


class UAETaxRegistrationNumberField(models.CharField):
"""
A model field for UAE Tax Registration Numbers (TRN).

Stores TRN as a 15-digit string for VAT purposes.

.. versionadded:: 5.1
"""

description = _("UAE Tax Registration Number")

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

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