Skip to content

Commit 8cd2d36

Browse files
committed
Add UAE (United Arab Emirates) localflavor support
- Emirates ID validation (784-YYYY-NNNNNNN-N format) - 7 UAE Emirates with Arabic translations - P.O. Box and VAT registration number validation - 22 tests with 100% coverage - Complete documentation - Arabic i18n support
1 parent e3c1928 commit 8cd2d36

File tree

15 files changed

+859
-4
lines changed

15 files changed

+859
-4
lines changed

docs/authors.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,4 @@ Authors
138138
* Abhineet Tamrakar
139139
* Tudor Amariei
140140
* Dishan Sachin
141+
* Sami El Achi

docs/changelog.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ Changelog
66

77
New flavors:
88

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

1112
New fields for existing flavors:
1213

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ validate Finnish social security numbers.
2424
:columns: 4
2525

2626
* :doc:`localflavor/ar`
27+
* :doc:`localflavor/ae`
2728
* :doc:`localflavor/at`
2829
* :doc:`localflavor/au`
2930
* :doc:`localflavor/be`

docs/localflavor/ae.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
United Arab Emirates (``ae``)
2+
=============================
3+
4+
Forms
5+
-----
6+
7+
.. automodule:: localflavor.ae.forms
8+
:members:
9+
10+
Models
11+
------
12+
13+
.. automodule:: localflavor.ae.models
14+
:members:
15+
16+
Data
17+
----
18+
19+
.. autodata:: localflavor.ae.ae_emirates.EMIRATE_CHOICES
20+
21+
.. autodata:: localflavor.ae.ae_emirates.EMIRATES_NORMALIZED

localflavor/ae/__init__.py

Whitespace-only changes.

localflavor/ae/ae_emirates.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""UAE emirates."""
2+
3+
from django.utils.translation import gettext_lazy as _
4+
5+
#: A list of UAE emirates as `(abbreviation, name)` tuples.
6+
EMIRATE_CHOICES = (
7+
('AZ', _('Abu Dhabi')),
8+
('AJ', _('Ajman')),
9+
('DU', _('Dubai')),
10+
('FU', _('Fujairah')),
11+
('RA', _('Ras Al Khaimah')),
12+
('SH', _('Sharjah')),
13+
('UQ', _('Umm Al Quwain')),
14+
)
15+
16+
#: Dictionary that maps emirate names and abbreviations to the
17+
#: canonical abbreviation.
18+
EMIRATES_NORMALIZED = {}
19+
20+
# Abbreviations
21+
for abbr, name in EMIRATE_CHOICES:
22+
EMIRATES_NORMALIZED[abbr.lower()] = abbr
23+
EMIRATES_NORMALIZED[abbr.upper()] = abbr
24+
25+
# Names
26+
EMIRATES_NORMALIZED.update({
27+
'abu dhabi': 'AZ',
28+
'ajman': 'AJ',
29+
'dubai': 'DU',
30+
'fujairah': 'FU',
31+
'ras al khaimah': 'RA',
32+
'ras al-khaimah': 'RA',
33+
'sharjah': 'SH',
34+
'umm al quwain': 'UQ',
35+
'umm al-quwain': 'UQ',
36+
# Arabic names
37+
'أبو ظبي': 'AZ',
38+
'عجمان': 'AJ',
39+
'دبي': 'DU',
40+
'الفجيرة': 'FU',
41+
'رأس الخيمة': 'RA',
42+
'الشارقة': 'SH',
43+
'أم القيوين': 'UQ',
44+
})

localflavor/ae/forms.py

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
"""UAE-specific Form helpers."""
2+
3+
import re
4+
5+
from django.forms.fields import CharField, RegexField, Select, ChoiceField
6+
from django.utils.translation import gettext_lazy as _
7+
8+
from .ae_emirates import EMIRATE_CHOICES
9+
from .validators import UAEEmiratesIDValidator, UAEPostalCodeValidator, UAEPOBoxValidator
10+
11+
12+
class UAEEmiratesIDField(CharField):
13+
"""
14+
A field for validating UAE Emirates ID numbers.
15+
16+
UAE Emirates ID format: 784-YYYY-NNNNNNN-N
17+
Where:
18+
- 784 is the UAE country code
19+
- YYYY is the year of birth (1900-current year)
20+
- NNNNNNN is a 7-digit sequence number
21+
- N is a single check digit
22+
23+
.. versionadded:: 5.1
24+
"""
25+
26+
default_error_messages = {
27+
'invalid': _('Enter a valid UAE Emirates ID number in format 784-YYYY-NNNNNNN-N.'),
28+
}
29+
30+
def __init__(self, **kwargs):
31+
super().__init__(**kwargs)
32+
self.validators.append(UAEEmiratesIDValidator())
33+
34+
def clean(self, value):
35+
value = super().clean(value)
36+
if value in self.empty_values:
37+
return value
38+
39+
# Remove any dashes or spaces and return clean 15-digit format
40+
clean_value = re.sub(r'[\s\-]', '', str(value))
41+
42+
# Format as 784-YYYY-NNNNNNN-N for consistency
43+
if len(clean_value) == 15:
44+
formatted = f"{clean_value[:3]}-{clean_value[3:7]}-{clean_value[7:14]}-{clean_value[14]}"
45+
return formatted
46+
47+
return clean_value
48+
49+
50+
class UAEEmirateField(ChoiceField):
51+
"""
52+
A choice field that uses a list of UAE Emirates as its choices.
53+
54+
.. versionadded:: 5.1
55+
"""
56+
57+
def __init__(self, **kwargs):
58+
kwargs.setdefault('choices', EMIRATE_CHOICES)
59+
super().__init__(**kwargs)
60+
61+
62+
class UAEEmirateSelect(Select):
63+
"""
64+
A Select widget that uses a list of UAE Emirates as its choices.
65+
66+
.. versionadded:: 5.1
67+
"""
68+
69+
def __init__(self, attrs=None):
70+
super().__init__(attrs, choices=EMIRATE_CHOICES)
71+
72+
73+
class UAEPostalCodeField(CharField):
74+
"""
75+
A field for validating UAE postal codes.
76+
77+
UAE doesn't use postal codes, but some systems require "00000".
78+
This field accepts "00000" or empty values.
79+
80+
.. versionadded:: 5.1
81+
"""
82+
83+
default_error_messages = {
84+
'invalid': _('Enter a valid UAE postal code. Use 00000 if postal code is required.'),
85+
}
86+
87+
def __init__(self, **kwargs):
88+
kwargs.setdefault('required', False)
89+
super().__init__(**kwargs)
90+
self.validators.append(UAEPostalCodeValidator())
91+
92+
def clean(self, value):
93+
value = super().clean(value)
94+
if value in self.empty_values:
95+
return ''
96+
97+
# Normalize to 00000 if needed
98+
clean_value = str(value).strip()
99+
if clean_value == '00000' or clean_value == '':
100+
return clean_value
101+
102+
return value
103+
104+
105+
class UAEPOBoxField(CharField):
106+
"""
107+
A field for validating UAE P.O. Box numbers.
108+
109+
Accepts formats: "P.O. Box XXXXX", "PO Box XXXXX", "POB XXXXX", or just "XXXXX"
110+
111+
.. versionadded:: 5.1
112+
"""
113+
114+
default_error_messages = {
115+
'invalid': _('Enter a valid P.O. Box number.'),
116+
}
117+
118+
def __init__(self, **kwargs):
119+
super().__init__(**kwargs)
120+
self.validators.append(UAEPOBoxValidator())
121+
122+
def clean(self, value):
123+
value = super().clean(value)
124+
if value in self.empty_values:
125+
return value
126+
127+
# Clean up the value
128+
clean_value = str(value).strip().upper()
129+
130+
# Remove "P.O. BOX" or "PO BOX" prefix if present
131+
clean_value = re.sub(r'^(P\.?O\.?\s*BOX\s*)', '', clean_value)
132+
133+
# Return just the number part
134+
if re.match(r'^\d{1,10}$', clean_value):
135+
return clean_value
136+
137+
return value
138+
139+
140+
class UAETaxRegistrationNumberField(RegexField):
141+
"""
142+
A field for validating UAE Tax Registration Numbers (TRN).
143+
144+
UAE TRN is a 15-digit number used for VAT registration.
145+
146+
.. versionadded:: 5.1
147+
"""
148+
149+
default_error_messages = {
150+
'invalid': _('Enter a valid UAE Tax Registration Number (15 digits).'),
151+
}
152+
153+
def __init__(self, **kwargs):
154+
super().__init__(r'^\d{15}$', **kwargs)
155+
156+
def clean(self, value):
157+
value = super().clean(value)
158+
if value in self.empty_values:
159+
return value
160+
161+
# Remove any spaces or formatting
162+
clean_value = re.sub(r'\s', '', str(value))
163+
return clean_value

localflavor/ae/models.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
"""UAE-specific Model helpers."""
2+
3+
from django.db import models
4+
from django.utils.translation import gettext_lazy as _
5+
6+
from .ae_emirates import EMIRATE_CHOICES
7+
from .validators import UAEEmiratesIDValidator, UAEPostalCodeValidator, UAEPOBoxValidator
8+
9+
10+
class UAEEmiratesIDField(models.CharField):
11+
"""
12+
A model field for UAE Emirates ID numbers.
13+
14+
Stores Emirates ID as a 15-digit string with format: 784-YYYY-NNNNNNN-N
15+
16+
.. versionadded:: 5.1
17+
"""
18+
19+
description = _("UAE Emirates ID number")
20+
21+
def __init__(self, *args, **kwargs):
22+
kwargs.setdefault('max_length', 18) # 15 digits + 3 dashes = 18 characters
23+
kwargs.setdefault('validators', []).append(UAEEmiratesIDValidator())
24+
super().__init__(*args, **kwargs)
25+
26+
def formfield(self, **kwargs):
27+
from .forms import UAEEmiratesIDField
28+
defaults = {'form_class': UAEEmiratesIDField}
29+
defaults.update(kwargs)
30+
return super().formfield(**defaults)
31+
32+
33+
class UAEEmirateField(models.CharField):
34+
"""
35+
A model field for UAE Emirates.
36+
37+
Stores emirate as a 2-character abbreviation (e.g., 'DU' for Dubai).
38+
39+
.. versionadded:: 5.1
40+
"""
41+
42+
description = _("UAE Emirate")
43+
44+
def __init__(self, *args, **kwargs):
45+
kwargs.setdefault('max_length', 2)
46+
kwargs.setdefault('choices', EMIRATE_CHOICES)
47+
super().__init__(*args, **kwargs)
48+
49+
def formfield(self, **kwargs):
50+
from .forms import UAEEmirateField
51+
defaults = {'form_class': UAEEmirateField}
52+
defaults.update(kwargs)
53+
return super().formfield(**defaults)
54+
55+
56+
class UAEPostalCodeField(models.CharField):
57+
"""
58+
A model field for UAE postal codes.
59+
60+
UAE doesn't use postal codes, but stores "00000" if required.
61+
62+
.. versionadded:: 5.1
63+
"""
64+
65+
description = _("UAE postal code")
66+
67+
def __init__(self, *args, **kwargs):
68+
kwargs.setdefault('max_length', 5)
69+
kwargs.setdefault('validators', []).append(UAEPostalCodeValidator())
70+
kwargs.setdefault('blank', True)
71+
super().__init__(*args, **kwargs)
72+
73+
def formfield(self, **kwargs):
74+
from .forms import UAEPostalCodeField
75+
defaults = {'form_class': UAEPostalCodeField}
76+
defaults.update(kwargs)
77+
return super().formfield(**defaults)
78+
79+
80+
class UAEPOBoxField(models.CharField):
81+
"""
82+
A model field for UAE P.O. Box numbers.
83+
84+
Stores P.O. Box numbers as strings (numeric only).
85+
86+
.. versionadded:: 5.1
87+
"""
88+
89+
description = _("UAE P.O. Box number")
90+
91+
def __init__(self, *args, **kwargs):
92+
kwargs.setdefault('max_length', 10)
93+
kwargs.setdefault('validators', []).append(UAEPOBoxValidator())
94+
super().__init__(*args, **kwargs)
95+
96+
def formfield(self, **kwargs):
97+
from .forms import UAEPOBoxField
98+
defaults = {'form_class': UAEPOBoxField}
99+
defaults.update(kwargs)
100+
return super().formfield(**defaults)
101+
102+
103+
class UAETaxRegistrationNumberField(models.CharField):
104+
"""
105+
A model field for UAE Tax Registration Numbers (TRN).
106+
107+
Stores TRN as a 15-digit string for VAT purposes.
108+
109+
.. versionadded:: 5.1
110+
"""
111+
112+
description = _("UAE Tax Registration Number")
113+
114+
def __init__(self, *args, **kwargs):
115+
kwargs.setdefault('max_length', 15)
116+
super().__init__(*args, **kwargs)
117+
118+
def formfield(self, **kwargs):
119+
from .forms import UAETaxRegistrationNumberField
120+
defaults = {'form_class': UAETaxRegistrationNumberField}
121+
defaults.update(kwargs)
122+
return super().formfield(**defaults)

0 commit comments

Comments
 (0)