nilva-django-utils 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nilva_django_utils-0.1.0/LICENSE +21 -0
- nilva_django_utils-0.1.0/PKG-INFO +168 -0
- nilva_django_utils-0.1.0/README.md +139 -0
- nilva_django_utils-0.1.0/nilva_django_utils/__init__.py +46 -0
- nilva_django_utils-0.1.0/nilva_django_utils/admin.py +81 -0
- nilva_django_utils-0.1.0/nilva_django_utils/bank.py +43 -0
- nilva_django_utils-0.1.0/nilva_django_utils/conf.py +27 -0
- nilva_django_utils-0.1.0/nilva_django_utils/digits.py +44 -0
- nilva_django_utils-0.1.0/nilva_django_utils/jalali.py +71 -0
- nilva_django_utils-0.1.0/nilva_django_utils/national_id.py +35 -0
- nilva_django_utils-0.1.0/nilva_django_utils/numbers.py +41 -0
- nilva_django_utils-0.1.0/nilva_django_utils/patterns.py +38 -0
- nilva_django_utils-0.1.0/nilva_django_utils/phone.py +92 -0
- nilva_django_utils-0.1.0/nilva_django_utils/validators.py +56 -0
- nilva_django_utils-0.1.0/nilva_django_utils.egg-info/PKG-INFO +168 -0
- nilva_django_utils-0.1.0/nilva_django_utils.egg-info/SOURCES.txt +29 -0
- nilva_django_utils-0.1.0/nilva_django_utils.egg-info/dependency_links.txt +1 -0
- nilva_django_utils-0.1.0/nilva_django_utils.egg-info/requires.txt +14 -0
- nilva_django_utils-0.1.0/nilva_django_utils.egg-info/top_level.txt +1 -0
- nilva_django_utils-0.1.0/pyproject.toml +65 -0
- nilva_django_utils-0.1.0/setup.cfg +4 -0
- nilva_django_utils-0.1.0/tests/test_admin.py +52 -0
- nilva_django_utils-0.1.0/tests/test_bank.py +47 -0
- nilva_django_utils-0.1.0/tests/test_digits.py +39 -0
- nilva_django_utils-0.1.0/tests/test_jalali.py +56 -0
- nilva_django_utils-0.1.0/tests/test_national_id.py +43 -0
- nilva_django_utils-0.1.0/tests/test_numbers.py +46 -0
- nilva_django_utils-0.1.0/tests/test_package.py +27 -0
- nilva_django_utils-0.1.0/tests/test_patterns.py +45 -0
- nilva_django_utils-0.1.0/tests/test_phone.py +62 -0
- nilva_django_utils-0.1.0/tests/test_validators.py +52 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Nilva
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nilva-django-utils
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Nilva house Iranian-locale toolkit — Persian digit normalization, Jalali dates and admin widgets, national-code / phone / IBAN / card validation, shared regex patterns
|
|
5
|
+
Author-email: Nilva <dev@nilva.ai>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: homepage, https://gitlab.nilva.ai/nilva/dev-assets/django/nilva-django-utils
|
|
8
|
+
Project-URL: repository, https://gitlab.nilva.ai/nilva/dev-assets/django/nilva-django-utils
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Framework :: Django
|
|
13
|
+
Classifier: Framework :: Django :: 6.0
|
|
14
|
+
Classifier: Natural Language :: Persian
|
|
15
|
+
Requires-Python: >=3.14
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Provides-Extra: django
|
|
19
|
+
Requires-Dist: django>=6.0; extra == "django"
|
|
20
|
+
Provides-Extra: jalali
|
|
21
|
+
Requires-Dist: jdatetime>=4.1; extra == "jalali"
|
|
22
|
+
Provides-Extra: jalali-admin
|
|
23
|
+
Requires-Dist: django>=6.0; extra == "jalali-admin"
|
|
24
|
+
Requires-Dist: jdatetime>=4.1; extra == "jalali-admin"
|
|
25
|
+
Requires-Dist: django-jalali>=7.0; extra == "jalali-admin"
|
|
26
|
+
Provides-Extra: phonenumbers
|
|
27
|
+
Requires-Dist: phonenumbers>=8.13; extra == "phonenumbers"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# nilva-django-utils
|
|
31
|
+
|
|
32
|
+
Nilva's house Iranian-locale toolkit for Django and plain-Python services.
|
|
33
|
+
Consolidates the digit-normalization, Jalali-date, national-code, phone,
|
|
34
|
+
bank-identifier and regex-constant code that was previously copy-pasted
|
|
35
|
+
(and drifting) across the Nilva backends.
|
|
36
|
+
|
|
37
|
+
The **core is dependency-free** — `digits`, `numbers`, `national_id`,
|
|
38
|
+
`phone`, `bank`, `patterns` and `conf` are pure standard library. Jalali,
|
|
39
|
+
Django and libphonenumber layers are opt-in extras and are never imported by
|
|
40
|
+
the top-level package.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uv add nilva-django-utils # stdlib core only
|
|
46
|
+
uv add "nilva-django-utils[django]" # + Django validators
|
|
47
|
+
uv add "nilva-django-utils[jalali]" # + jdatetime formatting helpers
|
|
48
|
+
uv add "nilva-django-utils[jalali-admin]" # + admin datepicker widgets (django-jalali)
|
|
49
|
+
uv add "nilva-django-utils[phonenumbers]" # + international E.164 normalization
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Digits — `nilva_django_utils.digits`
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from nilva_django_utils import to_english_digits, to_persian_digits
|
|
56
|
+
|
|
57
|
+
to_english_digits("۱۰۰۰٠٠٠۰۰۷") # "1000000007" (Persian AND Arabic-Indic)
|
|
58
|
+
to_persian_digits(1234567, separate=True) # "۱,۲۳۴,۵۶۷"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`translate_numbers(text, is_separate=False)` is kept as a drop-in for the
|
|
62
|
+
legacy `utils.general.number_utils` signatures.
|
|
63
|
+
|
|
64
|
+
## Numbers — `nilva_django_utils.numbers`
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
convert_to_seconds("1h30m") # 5400 — bare numbers count as seconds
|
|
68
|
+
separate_thousands(1234567) # "1,234,567"
|
|
69
|
+
truncate(1.23456789, 4) # 1.2345 (truncates, never rounds)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## National code (کد ملی) — `nilva_django_utils.national_id`
|
|
73
|
+
|
|
74
|
+
Real mod-11 checksum (not `isdigit()`), digit-normalized first:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
is_valid_national_id("۱۲۳۴۵۶۷۸۹۱") # True
|
|
78
|
+
is_valid_national_id("1111111111") # False (all-same rejected)
|
|
79
|
+
normalize_national_id("11111119", pad=True) # "0011111119" — Excel ate the zeros
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Phone — `nilva_django_utils.phone`
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
normalize_iranian_mobile("+98 912 345 6789") # "09123456789" (canonical national)
|
|
86
|
+
to_e164_iranian("۰۹۱۲۳۴۵۶۷۸۹") # "+989123456789"
|
|
87
|
+
is_valid_iranian_mobile("9123456789") # True — all house spellings accepted
|
|
88
|
+
|
|
89
|
+
# international numbers, libphonenumber-backed ([phonenumbers] extra):
|
|
90
|
+
normalize_phone("00442071838750") # "+442071838750"
|
|
91
|
+
normalize_phone("2071838750", country_code="GB")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Bank — `nilva_django_utils.bank`
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
is_valid_iban("IR49 0000 0000 0000 0000 0000 00") # ISO 13616 mod-97
|
|
98
|
+
is_valid_card_number("6104-3377-1234-5675") # Luhn
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Patterns — `nilva_django_utils.patterns`
|
|
102
|
+
|
|
103
|
+
One canonical constant per identifier (mobile, MCI-only ranges, landline,
|
|
104
|
+
IBAN, postal code, card, account, national id) replacing the divergent
|
|
105
|
+
per-project `PHONE_REGEX_PATTERN`s. Structure-only — pair with the checksum
|
|
106
|
+
helpers above where correctness matters.
|
|
107
|
+
|
|
108
|
+
## Django validators — `nilva_django_utils.validators`
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from nilva_django_utils.validators import (
|
|
112
|
+
validate_national_id, validate_iranian_mobile, validate_iban, validate_card_number,
|
|
113
|
+
iranian_mobile_regex_validator, iban_regex_validator, postal_code_regex_validator,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
class Profile(models.Model):
|
|
117
|
+
national_id = models.CharField(max_length=10, validators=[validate_national_id])
|
|
118
|
+
phone = models.CharField(max_length=13, validators=[validate_iranian_mobile])
|
|
119
|
+
sheba = models.CharField(max_length=26, validators=[validate_iban])
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
They work unchanged in DRF serializers (DRF converts Django `ValidationError`s).
|
|
123
|
+
|
|
124
|
+
## Jalali — `nilva_django_utils.jalali` / `.admin`
|
|
125
|
+
|
|
126
|
+
House convention: the database and the API stay **gregorian**; Jalali only
|
|
127
|
+
appears at the display/input boundary.
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from nilva_django_utils.jalali import to_jalali, to_jalali_datetime, parse_jalali
|
|
131
|
+
|
|
132
|
+
to_jalali(obj.created_at) # "1403-01-01"
|
|
133
|
+
to_jalali_datetime(obj.created_at) # "1403-01-01 08:30" (active TZ when Django is up)
|
|
134
|
+
parse_jalali("۱۴۰۳/۰۱/۰۱") # datetime.date(2024, 3, 20)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Admin datepickers over gregorian `DateField`s (`[jalali-admin]` extra):
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from nilva_django_utils.admin import JalaliDateAdminMixin, jalali_datetime_display
|
|
141
|
+
|
|
142
|
+
@admin.register(Delivery)
|
|
143
|
+
class DeliveryAdmin(JalaliDateAdminMixin, admin.ModelAdmin):
|
|
144
|
+
readonly_fields = ("created_jalali",)
|
|
145
|
+
|
|
146
|
+
@admin.display(description="ایجاد")
|
|
147
|
+
def created_jalali(self, obj):
|
|
148
|
+
return jalali_datetime_display(obj.created_at)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Add the datepicker static files exactly as Shahab does:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
# settings.py
|
|
155
|
+
from nilva_django_utils.conf import JALALI_SETTINGS # noqa: F401
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Projects that store `jmodels.jDateField` natively (Shahab) or use
|
|
159
|
+
`django-jalali-date` mixins (Etka/Bazargah/Xpress) keep working as-is — this
|
|
160
|
+
module is for the gregorian-DB + Jalali-admin convention (Depository).
|
|
161
|
+
|
|
162
|
+
## Development
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
uv sync --group dev
|
|
166
|
+
./run_tests.sh # ./run_tests.sh -c for coverage
|
|
167
|
+
uv run ruff check .
|
|
168
|
+
```
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# nilva-django-utils
|
|
2
|
+
|
|
3
|
+
Nilva's house Iranian-locale toolkit for Django and plain-Python services.
|
|
4
|
+
Consolidates the digit-normalization, Jalali-date, national-code, phone,
|
|
5
|
+
bank-identifier and regex-constant code that was previously copy-pasted
|
|
6
|
+
(and drifting) across the Nilva backends.
|
|
7
|
+
|
|
8
|
+
The **core is dependency-free** — `digits`, `numbers`, `national_id`,
|
|
9
|
+
`phone`, `bank`, `patterns` and `conf` are pure standard library. Jalali,
|
|
10
|
+
Django and libphonenumber layers are opt-in extras and are never imported by
|
|
11
|
+
the top-level package.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv add nilva-django-utils # stdlib core only
|
|
17
|
+
uv add "nilva-django-utils[django]" # + Django validators
|
|
18
|
+
uv add "nilva-django-utils[jalali]" # + jdatetime formatting helpers
|
|
19
|
+
uv add "nilva-django-utils[jalali-admin]" # + admin datepicker widgets (django-jalali)
|
|
20
|
+
uv add "nilva-django-utils[phonenumbers]" # + international E.164 normalization
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Digits — `nilva_django_utils.digits`
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from nilva_django_utils import to_english_digits, to_persian_digits
|
|
27
|
+
|
|
28
|
+
to_english_digits("۱۰۰۰٠٠٠۰۰۷") # "1000000007" (Persian AND Arabic-Indic)
|
|
29
|
+
to_persian_digits(1234567, separate=True) # "۱,۲۳۴,۵۶۷"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`translate_numbers(text, is_separate=False)` is kept as a drop-in for the
|
|
33
|
+
legacy `utils.general.number_utils` signatures.
|
|
34
|
+
|
|
35
|
+
## Numbers — `nilva_django_utils.numbers`
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
convert_to_seconds("1h30m") # 5400 — bare numbers count as seconds
|
|
39
|
+
separate_thousands(1234567) # "1,234,567"
|
|
40
|
+
truncate(1.23456789, 4) # 1.2345 (truncates, never rounds)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## National code (کد ملی) — `nilva_django_utils.national_id`
|
|
44
|
+
|
|
45
|
+
Real mod-11 checksum (not `isdigit()`), digit-normalized first:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
is_valid_national_id("۱۲۳۴۵۶۷۸۹۱") # True
|
|
49
|
+
is_valid_national_id("1111111111") # False (all-same rejected)
|
|
50
|
+
normalize_national_id("11111119", pad=True) # "0011111119" — Excel ate the zeros
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Phone — `nilva_django_utils.phone`
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
normalize_iranian_mobile("+98 912 345 6789") # "09123456789" (canonical national)
|
|
57
|
+
to_e164_iranian("۰۹۱۲۳۴۵۶۷۸۹") # "+989123456789"
|
|
58
|
+
is_valid_iranian_mobile("9123456789") # True — all house spellings accepted
|
|
59
|
+
|
|
60
|
+
# international numbers, libphonenumber-backed ([phonenumbers] extra):
|
|
61
|
+
normalize_phone("00442071838750") # "+442071838750"
|
|
62
|
+
normalize_phone("2071838750", country_code="GB")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Bank — `nilva_django_utils.bank`
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
is_valid_iban("IR49 0000 0000 0000 0000 0000 00") # ISO 13616 mod-97
|
|
69
|
+
is_valid_card_number("6104-3377-1234-5675") # Luhn
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Patterns — `nilva_django_utils.patterns`
|
|
73
|
+
|
|
74
|
+
One canonical constant per identifier (mobile, MCI-only ranges, landline,
|
|
75
|
+
IBAN, postal code, card, account, national id) replacing the divergent
|
|
76
|
+
per-project `PHONE_REGEX_PATTERN`s. Structure-only — pair with the checksum
|
|
77
|
+
helpers above where correctness matters.
|
|
78
|
+
|
|
79
|
+
## Django validators — `nilva_django_utils.validators`
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from nilva_django_utils.validators import (
|
|
83
|
+
validate_national_id, validate_iranian_mobile, validate_iban, validate_card_number,
|
|
84
|
+
iranian_mobile_regex_validator, iban_regex_validator, postal_code_regex_validator,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
class Profile(models.Model):
|
|
88
|
+
national_id = models.CharField(max_length=10, validators=[validate_national_id])
|
|
89
|
+
phone = models.CharField(max_length=13, validators=[validate_iranian_mobile])
|
|
90
|
+
sheba = models.CharField(max_length=26, validators=[validate_iban])
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
They work unchanged in DRF serializers (DRF converts Django `ValidationError`s).
|
|
94
|
+
|
|
95
|
+
## Jalali — `nilva_django_utils.jalali` / `.admin`
|
|
96
|
+
|
|
97
|
+
House convention: the database and the API stay **gregorian**; Jalali only
|
|
98
|
+
appears at the display/input boundary.
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from nilva_django_utils.jalali import to_jalali, to_jalali_datetime, parse_jalali
|
|
102
|
+
|
|
103
|
+
to_jalali(obj.created_at) # "1403-01-01"
|
|
104
|
+
to_jalali_datetime(obj.created_at) # "1403-01-01 08:30" (active TZ when Django is up)
|
|
105
|
+
parse_jalali("۱۴۰۳/۰۱/۰۱") # datetime.date(2024, 3, 20)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Admin datepickers over gregorian `DateField`s (`[jalali-admin]` extra):
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from nilva_django_utils.admin import JalaliDateAdminMixin, jalali_datetime_display
|
|
112
|
+
|
|
113
|
+
@admin.register(Delivery)
|
|
114
|
+
class DeliveryAdmin(JalaliDateAdminMixin, admin.ModelAdmin):
|
|
115
|
+
readonly_fields = ("created_jalali",)
|
|
116
|
+
|
|
117
|
+
@admin.display(description="ایجاد")
|
|
118
|
+
def created_jalali(self, obj):
|
|
119
|
+
return jalali_datetime_display(obj.created_at)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Add the datepicker static files exactly as Shahab does:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
# settings.py
|
|
126
|
+
from nilva_django_utils.conf import JALALI_SETTINGS # noqa: F401
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Projects that store `jmodels.jDateField` natively (Shahab) or use
|
|
130
|
+
`django-jalali-date` mixins (Etka/Bazargah/Xpress) keep working as-is — this
|
|
131
|
+
module is for the gregorian-DB + Jalali-admin convention (Depository).
|
|
132
|
+
|
|
133
|
+
## Development
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
uv sync --group dev
|
|
137
|
+
./run_tests.sh # ./run_tests.sh -c for coverage
|
|
138
|
+
uv run ruff check .
|
|
139
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Nilva Django Utils — the Nilva house Iranian-locale toolkit.
|
|
2
|
+
|
|
3
|
+
Persian / Arabic-Indic digit normalization, Jalali calendar formatting and
|
|
4
|
+
admin datepickers, national-code (کد ملی) validation, Iranian phone-number
|
|
5
|
+
normalization, bank identifier checksums (IBAN, card number) and the shared
|
|
6
|
+
regex patterns that were previously copy-pasted across the Nilva backends.
|
|
7
|
+
|
|
8
|
+
The core (``digits``, ``numbers``, ``national_id``, ``phone``, ``bank``,
|
|
9
|
+
``patterns``, ``conf``) is pure standard library and works in any Python
|
|
10
|
+
service. The optional layers are imported explicitly and never load from
|
|
11
|
+
here:
|
|
12
|
+
|
|
13
|
+
* ``nilva_django_utils.jalali`` — requires ``jdatetime`` (``[jalali]`` extra)
|
|
14
|
+
* ``nilva_django_utils.validators`` — requires Django (``[django]`` extra)
|
|
15
|
+
* ``nilva_django_utils.admin`` — requires Django + ``django-jalali``
|
|
16
|
+
(``[jalali-admin]`` extra)
|
|
17
|
+
* ``phone.normalize_phone`` — requires ``phonenumbers``
|
|
18
|
+
(``[phonenumbers]`` extra)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
__version__ = "0.1.0"
|
|
22
|
+
|
|
23
|
+
from . import patterns
|
|
24
|
+
from .bank import is_valid_card_number, is_valid_iban
|
|
25
|
+
from .digits import to_english_digits, to_persian_digits, translate_numbers
|
|
26
|
+
from .national_id import is_valid_national_id, normalize_national_id
|
|
27
|
+
from .numbers import convert_to_seconds, separate_thousands, truncate
|
|
28
|
+
from .phone import is_valid_iranian_mobile, normalize_iranian_mobile, to_e164_iranian
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"__version__",
|
|
32
|
+
"patterns",
|
|
33
|
+
"is_valid_card_number",
|
|
34
|
+
"is_valid_iban",
|
|
35
|
+
"to_english_digits",
|
|
36
|
+
"to_persian_digits",
|
|
37
|
+
"translate_numbers",
|
|
38
|
+
"is_valid_national_id",
|
|
39
|
+
"normalize_national_id",
|
|
40
|
+
"convert_to_seconds",
|
|
41
|
+
"separate_thousands",
|
|
42
|
+
"truncate",
|
|
43
|
+
"is_valid_iranian_mobile",
|
|
44
|
+
"normalize_iranian_mobile",
|
|
45
|
+
"to_e164_iranian",
|
|
46
|
+
]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Jalali datepickers and display helpers for the Django admin
|
|
2
|
+
(the ``[jalali-admin]`` extra: Django + ``django-jalali``).
|
|
3
|
+
|
|
4
|
+
Ported from the Depository backend: dates stay *gregorian* in the database
|
|
5
|
+
(the API contract is gregorian ISO-8601) while the admin renders and accepts
|
|
6
|
+
Jalali. ``JalaliDateAdminMixin`` wires the widget onto every ``DateField`` of
|
|
7
|
+
a ``ModelAdmin``; ``jalali_display`` / ``jalali_datetime_display`` are for
|
|
8
|
+
read-only ``list_display`` / ``readonly_fields`` cells.
|
|
9
|
+
|
|
10
|
+
Projects that instead store ``jmodels.jDateField`` natively (Shahab) or use
|
|
11
|
+
``django-jalali-date`` mixins don't need this module.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import datetime
|
|
15
|
+
|
|
16
|
+
import jdatetime
|
|
17
|
+
from django import forms
|
|
18
|
+
from django.db import models
|
|
19
|
+
from django.utils.safestring import mark_safe
|
|
20
|
+
from django_jalali.admin.widgets import AdminjDateWidget
|
|
21
|
+
|
|
22
|
+
from .jalali import JALALI_DATE_FORMAT, JALALI_DATETIME_FORMAT, parse_jalali, to_jalali, to_jalali_datetime
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def jalali_display(value, fmt: str = JALALI_DATE_FORMAT) -> str:
|
|
26
|
+
"""Read-only admin cell: Jalali date wrapped LTR so RTL renders cleanly."""
|
|
27
|
+
text = to_jalali(value, fmt)
|
|
28
|
+
if not text:
|
|
29
|
+
return "-"
|
|
30
|
+
return mark_safe(f'<span dir="ltr">{text}</span>')
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def jalali_datetime_display(value, fmt: str = JALALI_DATETIME_FORMAT) -> str:
|
|
34
|
+
"""Read-only admin cell: Jalali date+time wrapped LTR."""
|
|
35
|
+
text = to_jalali_datetime(value, fmt)
|
|
36
|
+
if not text:
|
|
37
|
+
return "-"
|
|
38
|
+
return mark_safe(f'<span dir="ltr">{text}</span>')
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class JalaliAdminDateWidget(AdminjDateWidget):
|
|
42
|
+
"""Persian datepicker over a *gregorian* ``DateField``.
|
|
43
|
+
|
|
44
|
+
Inherits the django-jalali admin datepicker media (jQuery UI calendar) and
|
|
45
|
+
the ``vjDateField`` class the JS binds to, but converts the incoming
|
|
46
|
+
gregorian value to a Jalali display string.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, attrs=None, format=None):
|
|
50
|
+
super().__init__(attrs=attrs, format=format or JALALI_DATE_FORMAT)
|
|
51
|
+
|
|
52
|
+
def _format_value(self, value):
|
|
53
|
+
if isinstance(value, datetime.datetime):
|
|
54
|
+
value = value.date()
|
|
55
|
+
if isinstance(value, datetime.date) and not isinstance(value, jdatetime.date):
|
|
56
|
+
value = jdatetime.date.fromgregorian(date=value)
|
|
57
|
+
return super()._format_value(value)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class JalaliFormDateField(forms.DateField):
|
|
61
|
+
"""Form field that reads a Jalali string and stores a gregorian date."""
|
|
62
|
+
|
|
63
|
+
widget = JalaliAdminDateWidget
|
|
64
|
+
|
|
65
|
+
def to_python(self, value):
|
|
66
|
+
if value in self.empty_values:
|
|
67
|
+
return None
|
|
68
|
+
if isinstance(value, str):
|
|
69
|
+
try:
|
|
70
|
+
return parse_jalali(value)
|
|
71
|
+
except ValueError:
|
|
72
|
+
pass
|
|
73
|
+
return super().to_python(value)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class JalaliDateAdminMixin:
|
|
77
|
+
"""Mix into a ModelAdmin to give its ``DateField``\\ s a Persian datepicker."""
|
|
78
|
+
|
|
79
|
+
formfield_overrides = {
|
|
80
|
+
models.DateField: {"form_class": JalaliFormDateField, "widget": JalaliAdminDateWidget},
|
|
81
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Bank identifier checksums.
|
|
2
|
+
|
|
3
|
+
The backends only regex-check IBANs (``^IR\\d{24}$``) and card numbers
|
|
4
|
+
(``^\\d{16}$``); these helpers add the real checksums (ISO 13616 mod-97 and
|
|
5
|
+
Luhn) on top. Input digits may be Persian or Arabic-Indic.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
from .digits import to_english_digits
|
|
11
|
+
|
|
12
|
+
_IBAN_STRUCTURE_RE = re.compile(r"^[A-Z]{2}\d{2}[A-Z0-9]{1,30}$")
|
|
13
|
+
|
|
14
|
+
_STRIP_RE = re.compile(r"[\s\-]")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def is_valid_iban(value) -> bool:
|
|
18
|
+
"""ISO 13616 mod-97 IBAN check. Iranian IBANs (شبا) are ``IR`` + 24
|
|
19
|
+
digits; other countries validate too. Accepts spaces/dashes and a
|
|
20
|
+
leading ``شبا``-less bare form is NOT accepted — the ``IR`` prefix is
|
|
21
|
+
required."""
|
|
22
|
+
iban = _STRIP_RE.sub("", to_english_digits(value).strip()).upper()
|
|
23
|
+
if not _IBAN_STRUCTURE_RE.match(iban):
|
|
24
|
+
return False
|
|
25
|
+
rearranged = iban[4:] + iban[:4]
|
|
26
|
+
numeric = "".join(str(int(char, 36)) for char in rearranged)
|
|
27
|
+
return int(numeric) % 97 == 1
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def is_valid_card_number(value) -> bool:
|
|
31
|
+
"""16-digit bank card Luhn check (Iranian cards are Luhn-valid)."""
|
|
32
|
+
card = _STRIP_RE.sub("", to_english_digits(value).strip())
|
|
33
|
+
if len(card) != 16 or not card.isdigit() or card == card[0] * 16:
|
|
34
|
+
return False
|
|
35
|
+
total = 0
|
|
36
|
+
for index, char in enumerate(card):
|
|
37
|
+
digit = int(char)
|
|
38
|
+
if index % 2 == 0:
|
|
39
|
+
digit *= 2
|
|
40
|
+
if digit > 9:
|
|
41
|
+
digit -= 9
|
|
42
|
+
total += digit
|
|
43
|
+
return total % 10 == 0
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Settings snippets (pure data — safe to import from a settings module).
|
|
2
|
+
|
|
3
|
+
``JALALI_SETTINGS`` registers the admin datepicker JS/CSS that
|
|
4
|
+
``django-jalali`` ships, exactly as the Shahab backend does::
|
|
5
|
+
|
|
6
|
+
# settings.py
|
|
7
|
+
from nilva_django_utils.conf import JALALI_SETTINGS # noqa: F401
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
JALALI_SETTINGS = {
|
|
11
|
+
# JavaScript static files for the admin Jalali date widget
|
|
12
|
+
"ADMIN_JS_STATIC_FILES": [
|
|
13
|
+
"admin/jquery.ui.datepicker.jalali/scripts/jquery-1.10.2.min.js",
|
|
14
|
+
"admin/jquery.ui.datepicker.jalali/scripts/jquery.ui.core.js",
|
|
15
|
+
"admin/jquery.ui.datepicker.jalali/scripts/jquery.ui.datepicker-cc.js",
|
|
16
|
+
"admin/jquery.ui.datepicker.jalali/scripts/calendar.js",
|
|
17
|
+
"admin/jquery.ui.datepicker.jalali/scripts/jquery.ui.datepicker-cc-fa.js",
|
|
18
|
+
"admin/main.js",
|
|
19
|
+
],
|
|
20
|
+
# CSS static files for the admin Jalali date widget
|
|
21
|
+
"ADMIN_CSS_STATIC_FILES": {
|
|
22
|
+
"all": [
|
|
23
|
+
"admin/jquery.ui.datepicker.jalali/themes/base/jquery-ui.min.css",
|
|
24
|
+
"admin/css/main.css",
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Persian / Arabic-Indic digit normalization.
|
|
2
|
+
|
|
3
|
+
Every Nilva backend accepts user input typed with Persian (۰-۹) or
|
|
4
|
+
Arabic-Indic (٠-٩) digits and renders numbers back in Persian. These tables
|
|
5
|
+
are the union of the per-project ``str.maketrans`` maps.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .numbers import separate_thousands
|
|
9
|
+
|
|
10
|
+
ASCII_DIGITS = "0123456789"
|
|
11
|
+
PERSIAN_DIGITS = "۰۱۲۳۴۵۶۷۸۹"
|
|
12
|
+
ARABIC_INDIC_DIGITS = "٠١٢٣٤٥٦٧٨٩"
|
|
13
|
+
|
|
14
|
+
#: Persian and Arabic-Indic digits -> ASCII ("۱۲۳" or "١٢٣" -> "123").
|
|
15
|
+
TO_ENGLISH_TABLE = str.maketrans(PERSIAN_DIGITS + ARABIC_INDIC_DIGITS, ASCII_DIGITS * 2)
|
|
16
|
+
|
|
17
|
+
#: ASCII and Arabic-Indic digits -> Persian ("123" or "١٢٣" -> "۱۲۳").
|
|
18
|
+
TO_PERSIAN_TABLE = str.maketrans(ASCII_DIGITS + ARABIC_INDIC_DIGITS, PERSIAN_DIGITS * 2)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def to_english_digits(text) -> str:
|
|
22
|
+
"""Localized digits (Persian and Arabic-Indic) to ASCII. ``None`` -> ``""``."""
|
|
23
|
+
if text is None:
|
|
24
|
+
return ""
|
|
25
|
+
return str(text).translate(TO_ENGLISH_TABLE)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def to_persian_digits(text, separate: bool = False) -> str:
|
|
29
|
+
"""ASCII (and Arabic-Indic) digits to Persian. ``None`` -> ``""``.
|
|
30
|
+
|
|
31
|
+
With ``separate=True`` numeric values get thousands separators first
|
|
32
|
+
(``1234567`` -> ``"۱,۲۳۴,۵۶۷"``).
|
|
33
|
+
"""
|
|
34
|
+
if text is None:
|
|
35
|
+
return ""
|
|
36
|
+
if separate:
|
|
37
|
+
text = separate_thousands(text)
|
|
38
|
+
return str(text).translate(TO_PERSIAN_TABLE)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def translate_numbers(text, is_separate: bool = False) -> str:
|
|
42
|
+
"""Backwards-compatible alias covering both legacy signatures
|
|
43
|
+
(``utils.general.number_utils.translate_numbers``)."""
|
|
44
|
+
return to_persian_digits(text, separate=is_separate)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Jalali (Persian) calendar formatting and parsing.
|
|
2
|
+
|
|
3
|
+
Requires ``jdatetime`` (the ``[jalali]`` extra). Dates stay *gregorian* in
|
|
4
|
+
the database and over the API — these helpers only convert at the display /
|
|
5
|
+
input boundary, the convention shared by the Depository and Cheshmeh
|
|
6
|
+
backends. Django is optional: timezone-aware datetimes are localized through
|
|
7
|
+
``django.utils.timezone`` only when Django is installed and configured.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import datetime
|
|
11
|
+
|
|
12
|
+
import jdatetime
|
|
13
|
+
|
|
14
|
+
from .digits import to_english_digits
|
|
15
|
+
|
|
16
|
+
JALALI_DATE_FORMAT = "%Y-%m-%d"
|
|
17
|
+
JALALI_DATETIME_FORMAT = "%Y-%m-%d %H:%M"
|
|
18
|
+
|
|
19
|
+
#: Input formats accepted when parsing a Jalali string back to gregorian.
|
|
20
|
+
JALALI_INPUT_FORMATS = (JALALI_DATE_FORMAT, "%Y/%m/%d")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _localtime(value: datetime.datetime) -> datetime.datetime:
|
|
24
|
+
try:
|
|
25
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
26
|
+
from django.utils import timezone
|
|
27
|
+
except ImportError:
|
|
28
|
+
return value
|
|
29
|
+
try:
|
|
30
|
+
return timezone.localtime(value)
|
|
31
|
+
except ImproperlyConfigured:
|
|
32
|
+
return value
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def to_jalali(value, fmt: str = JALALI_DATE_FORMAT) -> str:
|
|
36
|
+
"""Gregorian date/datetime -> Jalali string (``""`` when empty)."""
|
|
37
|
+
if value is None:
|
|
38
|
+
return ""
|
|
39
|
+
if isinstance(value, datetime.datetime):
|
|
40
|
+
value = value.date()
|
|
41
|
+
if isinstance(value, datetime.date) and not isinstance(value, jdatetime.date):
|
|
42
|
+
value = jdatetime.date.fromgregorian(date=value)
|
|
43
|
+
if hasattr(value, "strftime"):
|
|
44
|
+
return value.strftime(fmt)
|
|
45
|
+
return str(value)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def to_jalali_datetime(value, fmt: str = JALALI_DATETIME_FORMAT) -> str:
|
|
49
|
+
"""Gregorian datetime -> Jalali date+time string, localized to the active
|
|
50
|
+
Django timezone when one is configured (``""`` when empty)."""
|
|
51
|
+
if value is None:
|
|
52
|
+
return ""
|
|
53
|
+
if isinstance(value, datetime.datetime):
|
|
54
|
+
if value.tzinfo is not None:
|
|
55
|
+
value = _localtime(value)
|
|
56
|
+
return jdatetime.datetime.fromgregorian(datetime=value).strftime(fmt)
|
|
57
|
+
return to_jalali(value, fmt)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def parse_jalali(value, formats: tuple = JALALI_INPUT_FORMATS) -> datetime.date:
|
|
61
|
+
"""Jalali date string (any digit script) -> gregorian ``datetime.date``.
|
|
62
|
+
|
|
63
|
+
Raises ``ValueError`` when no format matches.
|
|
64
|
+
"""
|
|
65
|
+
normalized = to_english_digits(value).strip()
|
|
66
|
+
for fmt in formats:
|
|
67
|
+
try:
|
|
68
|
+
return jdatetime.datetime.strptime(normalized, fmt).togregorian().date()
|
|
69
|
+
except ValueError:
|
|
70
|
+
continue
|
|
71
|
+
raise ValueError(f"Not a valid Jalali date: {value!r}")
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Iranian national code (کد ملی) validation.
|
|
2
|
+
|
|
3
|
+
Implements the real mod-11 weighted checksum (the algorithm behind the
|
|
4
|
+
``codemelli`` package) instead of the ``isdigit()`` / 10-digit-regex checks
|
|
5
|
+
scattered across the backends. Input is digit-normalized first, so a code
|
|
6
|
+
typed as ``۱۰۰۰۰۰۰۰۰۷`` validates the same as ``1000000007``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .digits import to_english_digits
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def normalize_national_id(value, pad: bool = False) -> str:
|
|
13
|
+
"""Clean ``value`` to an ASCII digit string (localized digits translated,
|
|
14
|
+
spaces/dashes stripped).
|
|
15
|
+
|
|
16
|
+
``pad=True`` zero-fills 8/9-digit codes back to 10 digits — Excel imports
|
|
17
|
+
routinely strip leading zeros.
|
|
18
|
+
"""
|
|
19
|
+
code = to_english_digits(value).strip().replace(" ", "").replace("-", "")
|
|
20
|
+
if pad and code.isdigit() and 8 <= len(code) < 10:
|
|
21
|
+
code = code.zfill(10)
|
|
22
|
+
return code
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def is_valid_national_id(value, pad: bool = False) -> bool:
|
|
26
|
+
"""True when ``value`` is a structurally valid کد ملی (10 digits, not all
|
|
27
|
+
identical, mod-11 checksum passes)."""
|
|
28
|
+
code = normalize_national_id(value, pad=pad)
|
|
29
|
+
if len(code) != 10 or not code.isdigit():
|
|
30
|
+
return False
|
|
31
|
+
if code == code[0] * 10:
|
|
32
|
+
return False
|
|
33
|
+
check = int(code[9])
|
|
34
|
+
remainder = sum(int(code[i]) * (10 - i) for i in range(9)) % 11
|
|
35
|
+
return check == remainder if remainder < 2 else check == 11 - remainder
|