django-nepkit 0.1.0__py3-none-any.whl
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.
- django_nepkit/__init__.py +31 -0
- django_nepkit/admin.py +211 -0
- django_nepkit/forms.py +50 -0
- django_nepkit/models.py +269 -0
- django_nepkit/serializers.py +113 -0
- django_nepkit/static/django_nepkit/css/admin-nepali-datepicker.css +37 -0
- django_nepkit/static/django_nepkit/js/address-chaining.js +64 -0
- django_nepkit/static/django_nepkit/js/admin-jquery-bridge.js +10 -0
- django_nepkit/static/django_nepkit/js/nepali-datepicker-init.js +108 -0
- django_nepkit/templatetags/__init__.py +0 -0
- django_nepkit/templatetags/nepali.py +74 -0
- django_nepkit/urls.py +10 -0
- django_nepkit/utils.py +77 -0
- django_nepkit/validators.py +12 -0
- django_nepkit/views.py +22 -0
- django_nepkit/widgets.py +72 -0
- django_nepkit-0.1.0.dist-info/METADATA +377 -0
- django_nepkit-0.1.0.dist-info/RECORD +39 -0
- django_nepkit-0.1.0.dist-info/WHEEL +5 -0
- django_nepkit-0.1.0.dist-info/licenses/LICENSE +21 -0
- django_nepkit-0.1.0.dist-info/top_level.txt +2 -0
- example/demo/__init__.py +0 -0
- example/demo/admin.py +42 -0
- example/demo/apps.py +5 -0
- example/demo/migrations/0001_initial.py +2113 -0
- example/demo/migrations/0002_alter_person_phone_number.py +18 -0
- example/demo/migrations/0003_person_created_at_person_updated_at.py +27 -0
- example/demo/migrations/0004_alter_person_created_at_alter_person_updated_at.py +23 -0
- example/demo/migrations/0005_alter_person_created_at_alter_person_updated_at.py +27 -0
- example/demo/migrations/__init__.py +0 -0
- example/demo/models.py +27 -0
- example/demo/tests.py +1 -0
- example/demo/urls.py +9 -0
- example/demo/views.py +32 -0
- example/example_project/__init__.py +0 -0
- example/example_project/settings.py +76 -0
- example/example_project/urls.py +8 -0
- example/example_project/wsgi.py +7 -0
- example/manage.py +24 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from .models import (
|
|
2
|
+
NepaliDateField,
|
|
3
|
+
NepaliTimeField,
|
|
4
|
+
NepaliDateTimeField,
|
|
5
|
+
NepaliPhoneNumberField,
|
|
6
|
+
ProvinceField,
|
|
7
|
+
DistrictField,
|
|
8
|
+
MunicipalityField,
|
|
9
|
+
)
|
|
10
|
+
from .admin import (
|
|
11
|
+
NepaliDateFilter,
|
|
12
|
+
format_nepali_date,
|
|
13
|
+
format_nepali_datetime,
|
|
14
|
+
NepaliModelAdmin,
|
|
15
|
+
NepaliAdminMixin,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"NepaliDateField",
|
|
20
|
+
"NepaliTimeField",
|
|
21
|
+
"NepaliDateTimeField",
|
|
22
|
+
"NepaliPhoneNumberField",
|
|
23
|
+
"ProvinceField",
|
|
24
|
+
"DistrictField",
|
|
25
|
+
"MunicipalityField",
|
|
26
|
+
"NepaliDateFilter",
|
|
27
|
+
"format_nepali_date",
|
|
28
|
+
"format_nepali_datetime",
|
|
29
|
+
"NepaliModelAdmin",
|
|
30
|
+
"NepaliAdminMixin",
|
|
31
|
+
]
|
django_nepkit/admin.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
from django.utils.translation import gettext_lazy as _
|
|
3
|
+
from nepali.datetime import nepalidate, nepalidatetime
|
|
4
|
+
|
|
5
|
+
from django_nepkit.models import NepaliDateField
|
|
6
|
+
from django_nepkit.utils import (
|
|
7
|
+
try_parse_nepali_date,
|
|
8
|
+
try_parse_nepali_datetime,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def format_nepali_date(date_value, format_string="%B %d, %Y"):
|
|
13
|
+
"""
|
|
14
|
+
Format a nepalidate object with Nepali month names.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
date_value: A nepalidate object or string in YYYY-MM-DD format
|
|
18
|
+
format_string: strftime format string (default: '%B %d, %Y')
|
|
19
|
+
%B = Full month name (Baishak, Jestha, etc.)
|
|
20
|
+
%b = Short month name
|
|
21
|
+
%d = Day of month
|
|
22
|
+
%Y = Year
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Formatted date string with Nepali month names, or empty string if invalid
|
|
26
|
+
"""
|
|
27
|
+
if date_value is None:
|
|
28
|
+
return ""
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
parsed = try_parse_nepali_date(date_value)
|
|
32
|
+
if parsed is not None:
|
|
33
|
+
return parsed.strftime(format_string)
|
|
34
|
+
if isinstance(date_value, nepalidate):
|
|
35
|
+
return date_value.strftime(
|
|
36
|
+
format_string
|
|
37
|
+
) # defensive; should be covered above
|
|
38
|
+
except (ValueError, TypeError, AttributeError):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
return str(date_value) if date_value else ""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def format_nepali_datetime(datetime_value, format_string="%B %d, %Y %I:%M %p"):
|
|
45
|
+
"""
|
|
46
|
+
Format a nepalidatetime object with Nepali month names.
|
|
47
|
+
|
|
48
|
+
Default output uses **12-hour time with AM/PM**.
|
|
49
|
+
"""
|
|
50
|
+
if datetime_value is None:
|
|
51
|
+
return ""
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
parsed = try_parse_nepali_datetime(datetime_value)
|
|
55
|
+
if parsed is not None:
|
|
56
|
+
return parsed.strftime(format_string)
|
|
57
|
+
if isinstance(datetime_value, nepalidatetime):
|
|
58
|
+
return datetime_value.strftime(
|
|
59
|
+
format_string
|
|
60
|
+
) # defensive; should be covered above
|
|
61
|
+
except (ValueError, TypeError, AttributeError):
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
return str(datetime_value) if datetime_value else ""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class NepaliDateFilter(admin.FieldListFilter):
|
|
68
|
+
"""
|
|
69
|
+
A list filter for NepaliDateField to filter by BS Year.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(self, field, request, params, model, model_admin, field_path):
|
|
73
|
+
self.parameter_name = f"{field_path}_bs_year"
|
|
74
|
+
super().__init__(field, request, params, model, model_admin, field_path)
|
|
75
|
+
self.title = _("Nepali Date (Year)")
|
|
76
|
+
|
|
77
|
+
def expected_parameters(self):
|
|
78
|
+
return [self.parameter_name]
|
|
79
|
+
|
|
80
|
+
def choices(self, changelist):
|
|
81
|
+
yield {
|
|
82
|
+
"selected": self.used_parameters.get(self.parameter_name) is None,
|
|
83
|
+
"query_string": changelist.get_query_string(remove=[self.parameter_name]),
|
|
84
|
+
"display": _("All"),
|
|
85
|
+
}
|
|
86
|
+
current_year = nepalidate.today().year
|
|
87
|
+
for year in range(current_year - 10, current_year + 2):
|
|
88
|
+
yield {
|
|
89
|
+
"selected": self.used_parameters.get(self.parameter_name) == str(year),
|
|
90
|
+
"query_string": changelist.get_query_string(
|
|
91
|
+
{self.parameter_name: str(year)}
|
|
92
|
+
),
|
|
93
|
+
"display": str(year),
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
def queryset(self, request, queryset):
|
|
97
|
+
value = self.used_parameters.get(self.parameter_name)
|
|
98
|
+
if value:
|
|
99
|
+
year = int(value)
|
|
100
|
+
# Convert BS year range to AD date range
|
|
101
|
+
start_date_bs = nepalidate(year, 1, 1)
|
|
102
|
+
# Find last day of the year. Some BS years end on the 30th,
|
|
103
|
+
# so we try 31 first and then fall back to 30 if that date
|
|
104
|
+
# is not valid.
|
|
105
|
+
try:
|
|
106
|
+
end_date_bs = nepalidate(year, 12, 31)
|
|
107
|
+
except ValueError:
|
|
108
|
+
end_date_bs = nepalidate(year, 12, 30)
|
|
109
|
+
|
|
110
|
+
start_date_ad = start_date_bs.to_date()
|
|
111
|
+
end_date_ad = end_date_bs.to_date()
|
|
112
|
+
|
|
113
|
+
return queryset.filter(
|
|
114
|
+
**{f"{self.field_path}__range": (start_date_ad, end_date_ad)}
|
|
115
|
+
)
|
|
116
|
+
return queryset
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# Register NepaliDateFilter as the default list filter for NepaliDateField
|
|
120
|
+
admin.FieldListFilter.register(
|
|
121
|
+
lambda f: isinstance(f, NepaliDateField),
|
|
122
|
+
NepaliDateFilter,
|
|
123
|
+
take_priority=True,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class NepaliAdminMixin:
|
|
128
|
+
"""
|
|
129
|
+
Mixin for Django admin classes that provides Nepali date utilities.
|
|
130
|
+
Makes format_nepali_date and NepaliDateFilter available without explicit imports.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
def format_nepali_date(self, date_value, format_string="%B %d, %Y"):
|
|
134
|
+
"""
|
|
135
|
+
Format a nepalidate object with Nepali month names.
|
|
136
|
+
Available as a method on admin classes using this mixin.
|
|
137
|
+
"""
|
|
138
|
+
return format_nepali_date(date_value, format_string)
|
|
139
|
+
|
|
140
|
+
def format_nepali_datetime(
|
|
141
|
+
self, datetime_value, format_string="%B %d, %Y %I:%M %p"
|
|
142
|
+
):
|
|
143
|
+
return format_nepali_datetime(datetime_value, format_string)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class NepaliModelAdmin(NepaliAdminMixin, admin.ModelAdmin):
|
|
147
|
+
"""
|
|
148
|
+
Base ModelAdmin class with Nepali date utilities built-in.
|
|
149
|
+
Use this instead of admin.ModelAdmin to have format_nepali_date available.
|
|
150
|
+
|
|
151
|
+
Example:
|
|
152
|
+
from django_nepkit import NepaliModelAdmin, NepaliDateFilter
|
|
153
|
+
|
|
154
|
+
@admin.register(MyModel)
|
|
155
|
+
class MyModelAdmin(NepaliModelAdmin):
|
|
156
|
+
list_filter = (('date_field', NepaliDateFilter),)
|
|
157
|
+
|
|
158
|
+
def display_date(self, obj):
|
|
159
|
+
return self.format_nepali_date(obj.date_field)
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
# Make NepaliDateFilter available as a class attribute
|
|
163
|
+
NepaliDateFilter = NepaliDateFilter
|
|
164
|
+
|
|
165
|
+
# Ensure admin forms render Nepali fields with the proper widget,
|
|
166
|
+
# even if a project doesn't provide custom ModelForms.
|
|
167
|
+
def formfield_for_dbfield(self, db_field, request, **kwargs):
|
|
168
|
+
"""
|
|
169
|
+
Force Nepali widgets in Django admin without requiring user forms.
|
|
170
|
+
"""
|
|
171
|
+
try:
|
|
172
|
+
from django_nepkit.models import NepaliDateField, NepaliDateTimeField
|
|
173
|
+
from django_nepkit.widgets import NepaliDatePickerWidget
|
|
174
|
+
except Exception:
|
|
175
|
+
return super().formfield_for_dbfield(db_field, request, **kwargs)
|
|
176
|
+
|
|
177
|
+
if isinstance(db_field, (NepaliDateField, NepaliDateTimeField)):
|
|
178
|
+
kwargs.setdefault("widget", NepaliDatePickerWidget)
|
|
179
|
+
|
|
180
|
+
return super().formfield_for_dbfield(db_field, request, **kwargs)
|
|
181
|
+
|
|
182
|
+
class Media:
|
|
183
|
+
"""
|
|
184
|
+
Django admin ships jQuery as `django.jQuery` (not `window.jQuery`).
|
|
185
|
+
The Nepali date picker library expects a global `jQuery`, so we bridge it.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
css = {
|
|
189
|
+
"all": (
|
|
190
|
+
"https://nepalidatepicker.sajanmaharjan.com.np/v5/nepali.datepicker/css/nepali.datepicker.v5.0.6.min.css",
|
|
191
|
+
"django_nepkit/css/admin-nepali-datepicker.css",
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
js = (
|
|
195
|
+
# Bridge admin's `django.jQuery` -> `window.jQuery`
|
|
196
|
+
"django_nepkit/js/admin-jquery-bridge.js",
|
|
197
|
+
# Date picker lib
|
|
198
|
+
"https://nepalidatepicker.sajanmaharjan.com.np/v5/nepali.datepicker/js/nepali.datepicker.v5.0.6.min.js",
|
|
199
|
+
# Init
|
|
200
|
+
"django_nepkit/js/nepali-datepicker-init.js",
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# Exporting for easy usage
|
|
205
|
+
__all__ = [
|
|
206
|
+
"NepaliDateFilter",
|
|
207
|
+
"format_nepali_date",
|
|
208
|
+
"format_nepali_datetime",
|
|
209
|
+
"NepaliAdminMixin",
|
|
210
|
+
"NepaliModelAdmin",
|
|
211
|
+
]
|
django_nepkit/forms.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from datetime import date as python_date
|
|
2
|
+
|
|
3
|
+
from django import forms
|
|
4
|
+
from django.utils.translation import gettext_lazy as _
|
|
5
|
+
from nepali.datetime import nepalidate
|
|
6
|
+
|
|
7
|
+
from django_nepkit.utils import try_parse_nepali_date
|
|
8
|
+
from django_nepkit.validators import validate_nepali_phone_number
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class NepaliDateFormField(forms.DateField):
|
|
12
|
+
"""
|
|
13
|
+
A Django Form Field for Nepali Date (Bikram Sambat).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .widgets import NepaliDatePickerWidget
|
|
17
|
+
|
|
18
|
+
widget = NepaliDatePickerWidget
|
|
19
|
+
|
|
20
|
+
def __init__(self, *args, **kwargs):
|
|
21
|
+
kwargs.pop("max_length", None)
|
|
22
|
+
super().__init__(*args, **kwargs)
|
|
23
|
+
|
|
24
|
+
def to_python(self, value):
|
|
25
|
+
if value in self.empty_values:
|
|
26
|
+
return None
|
|
27
|
+
if isinstance(value, nepalidate):
|
|
28
|
+
return value
|
|
29
|
+
if isinstance(value, python_date):
|
|
30
|
+
return nepalidate.from_date(value)
|
|
31
|
+
try:
|
|
32
|
+
parsed = try_parse_nepali_date(str(value))
|
|
33
|
+
if parsed is not None:
|
|
34
|
+
return parsed
|
|
35
|
+
raise ValueError("Invalid BS date format")
|
|
36
|
+
except Exception:
|
|
37
|
+
raise forms.ValidationError(
|
|
38
|
+
_("Enter a valid Nepali date in YYYY-MM-DD format."),
|
|
39
|
+
code="invalid",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class NepaliPhoneNumberFormField(forms.CharField):
|
|
44
|
+
"""
|
|
45
|
+
A Django Form Field for Nepali Phone Numbers.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, *args, **kwargs):
|
|
49
|
+
super().__init__(*args, **kwargs)
|
|
50
|
+
self.validators.append(validate_nepali_phone_number)
|
django_nepkit/models.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
from datetime import date as python_date
|
|
2
|
+
|
|
3
|
+
from django.db import models
|
|
4
|
+
from django.utils.translation import gettext_lazy as _
|
|
5
|
+
from nepali.datetime import nepalidate, nepalidatetime
|
|
6
|
+
|
|
7
|
+
from django_nepkit.utils import (
|
|
8
|
+
BS_DATE_FORMAT,
|
|
9
|
+
BS_DATETIME_FORMAT,
|
|
10
|
+
try_parse_nepali_date,
|
|
11
|
+
try_parse_nepali_datetime,
|
|
12
|
+
)
|
|
13
|
+
from django_nepkit.validators import validate_nepali_phone_number
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NepaliPhoneNumberField(models.CharField):
|
|
17
|
+
description = _("Nepali Phone Number")
|
|
18
|
+
|
|
19
|
+
def __init__(self, *args, **kwargs):
|
|
20
|
+
kwargs.setdefault("max_length", 10)
|
|
21
|
+
super().__init__(*args, **kwargs)
|
|
22
|
+
self.validators.append(validate_nepali_phone_number)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class NepaliDateField(models.CharField):
|
|
26
|
+
"""
|
|
27
|
+
A Django Model Field that stores Nepali (Bikram Sambat) Date.
|
|
28
|
+
Internally it stores the date as BS string (YYYY-MM-DD) in the database.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
description = _("Nepali Date (Bikram Sambat)")
|
|
32
|
+
|
|
33
|
+
def __init__(self, *args, **kwargs):
|
|
34
|
+
self.auto_now = kwargs.pop("auto_now", False)
|
|
35
|
+
self.auto_now_add = kwargs.pop("auto_now_add", False)
|
|
36
|
+
|
|
37
|
+
# Match Django's DateField behavior
|
|
38
|
+
if self.auto_now or self.auto_now_add:
|
|
39
|
+
kwargs.setdefault("editable", False)
|
|
40
|
+
kwargs.setdefault("blank", True)
|
|
41
|
+
|
|
42
|
+
kwargs.setdefault("max_length", 10)
|
|
43
|
+
super().__init__(*args, **kwargs)
|
|
44
|
+
|
|
45
|
+
def pre_save(self, model_instance, add):
|
|
46
|
+
if self.auto_now or (self.auto_now_add and add):
|
|
47
|
+
# Using nepalidate.today() to get current BS date.
|
|
48
|
+
# This is BS-native as much as possible.
|
|
49
|
+
value = nepalidate.today().strftime(BS_DATE_FORMAT)
|
|
50
|
+
setattr(model_instance, self.attname, value)
|
|
51
|
+
return value
|
|
52
|
+
return super().pre_save(model_instance, add)
|
|
53
|
+
|
|
54
|
+
def from_db_value(self, value, expression, connection):
|
|
55
|
+
parsed = try_parse_nepali_date(value)
|
|
56
|
+
return parsed if parsed is not None else value
|
|
57
|
+
|
|
58
|
+
def to_python(self, value):
|
|
59
|
+
if value is None or isinstance(value, nepalidate):
|
|
60
|
+
return value
|
|
61
|
+
if isinstance(value, python_date):
|
|
62
|
+
# If we MUST handle AD date object, we convert it to BS.
|
|
63
|
+
# But we should prefer strings or nepalidate.
|
|
64
|
+
try:
|
|
65
|
+
return nepalidate.from_date(value)
|
|
66
|
+
except (ValueError, TypeError):
|
|
67
|
+
return str(value)
|
|
68
|
+
if isinstance(value, str):
|
|
69
|
+
parsed = try_parse_nepali_date(value)
|
|
70
|
+
return parsed if parsed is not None else value
|
|
71
|
+
return super().to_python(value)
|
|
72
|
+
|
|
73
|
+
def validate(self, value, model_instance):
|
|
74
|
+
if isinstance(value, nepalidate):
|
|
75
|
+
value = value.strftime(BS_DATE_FORMAT)
|
|
76
|
+
super().validate(value, model_instance)
|
|
77
|
+
|
|
78
|
+
def run_validators(self, value):
|
|
79
|
+
if isinstance(value, nepalidate):
|
|
80
|
+
value = value.strftime(BS_DATE_FORMAT)
|
|
81
|
+
super().run_validators(value)
|
|
82
|
+
|
|
83
|
+
def get_prep_value(self, value):
|
|
84
|
+
if value is None:
|
|
85
|
+
return value
|
|
86
|
+
if isinstance(value, nepalidate):
|
|
87
|
+
return value.strftime(BS_DATE_FORMAT)
|
|
88
|
+
if isinstance(value, python_date):
|
|
89
|
+
try:
|
|
90
|
+
return nepalidate.from_date(value).strftime(BS_DATE_FORMAT)
|
|
91
|
+
except (ValueError, TypeError):
|
|
92
|
+
return str(value)
|
|
93
|
+
if isinstance(value, str):
|
|
94
|
+
return value
|
|
95
|
+
# Fallback: convert to string
|
|
96
|
+
return str(value)
|
|
97
|
+
|
|
98
|
+
def deconstruct(self):
|
|
99
|
+
name, path, args, kwargs = super().deconstruct()
|
|
100
|
+
if self.auto_now:
|
|
101
|
+
kwargs["auto_now"] = True
|
|
102
|
+
if self.auto_now_add:
|
|
103
|
+
kwargs["auto_now_add"] = True
|
|
104
|
+
return name, path, args, kwargs
|
|
105
|
+
|
|
106
|
+
def formfield(self, **kwargs):
|
|
107
|
+
from .forms import NepaliDateFormField
|
|
108
|
+
from .widgets import NepaliDatePickerWidget
|
|
109
|
+
|
|
110
|
+
defaults = {
|
|
111
|
+
"form_class": NepaliDateFormField,
|
|
112
|
+
"widget": NepaliDatePickerWidget,
|
|
113
|
+
}
|
|
114
|
+
defaults.update(kwargs)
|
|
115
|
+
return super().formfield(**defaults)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class NepaliTimeField(models.TimeField):
|
|
119
|
+
"""
|
|
120
|
+
A Django Model Field for Time with Nepali localization support.
|
|
121
|
+
Supports auto_now and auto_now_add like standard Django TimeField.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
description = _("Nepali Time")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class NepaliDateTimeField(models.CharField):
|
|
128
|
+
"""
|
|
129
|
+
A Django Model Field that stores Nepali (Bikram Sambat) DateTime.
|
|
130
|
+
Internally it stores the datetime as BS string (YYYY-MM-DD HH:MM:SS) in the database.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
description = _("Nepali DateTime (Bikram Sambat)")
|
|
134
|
+
|
|
135
|
+
def __init__(self, *args, **kwargs):
|
|
136
|
+
self.auto_now = kwargs.pop("auto_now", False)
|
|
137
|
+
self.auto_now_add = kwargs.pop("auto_now_add", False)
|
|
138
|
+
|
|
139
|
+
# Match Django's DateTimeField behavior
|
|
140
|
+
if self.auto_now or self.auto_now_add:
|
|
141
|
+
kwargs.setdefault("editable", False)
|
|
142
|
+
kwargs.setdefault("blank", True)
|
|
143
|
+
|
|
144
|
+
kwargs.setdefault("max_length", 19) # YYYY-MM-DD HH:MM:SS
|
|
145
|
+
super().__init__(*args, **kwargs)
|
|
146
|
+
|
|
147
|
+
def pre_save(self, model_instance, add):
|
|
148
|
+
if self.auto_now or (self.auto_now_add and add):
|
|
149
|
+
# Using nepalidatetime.now() to get current BS datetime
|
|
150
|
+
value = nepalidatetime.now().strftime(BS_DATETIME_FORMAT)
|
|
151
|
+
setattr(model_instance, self.attname, value)
|
|
152
|
+
return value
|
|
153
|
+
return super().pre_save(model_instance, add)
|
|
154
|
+
|
|
155
|
+
def from_db_value(self, value, expression, connection):
|
|
156
|
+
parsed = try_parse_nepali_datetime(value)
|
|
157
|
+
return parsed if parsed is not None else value
|
|
158
|
+
|
|
159
|
+
def to_python(self, value):
|
|
160
|
+
if value is None or isinstance(value, nepalidatetime):
|
|
161
|
+
return value
|
|
162
|
+
if isinstance(value, str):
|
|
163
|
+
parsed = try_parse_nepali_datetime(value)
|
|
164
|
+
return parsed if parsed is not None else value
|
|
165
|
+
return super().to_python(value)
|
|
166
|
+
|
|
167
|
+
def validate(self, value, model_instance):
|
|
168
|
+
if isinstance(value, nepalidatetime):
|
|
169
|
+
value = value.strftime(BS_DATETIME_FORMAT)
|
|
170
|
+
super().validate(value, model_instance)
|
|
171
|
+
|
|
172
|
+
def run_validators(self, value):
|
|
173
|
+
if isinstance(value, nepalidatetime):
|
|
174
|
+
value = value.strftime(BS_DATETIME_FORMAT)
|
|
175
|
+
super().run_validators(value)
|
|
176
|
+
|
|
177
|
+
def get_prep_value(self, value):
|
|
178
|
+
if value is None:
|
|
179
|
+
return value
|
|
180
|
+
if isinstance(value, nepalidatetime):
|
|
181
|
+
return value.strftime(BS_DATETIME_FORMAT)
|
|
182
|
+
if isinstance(value, str):
|
|
183
|
+
return value
|
|
184
|
+
# Fallback: convert to string
|
|
185
|
+
return str(value)
|
|
186
|
+
|
|
187
|
+
def deconstruct(self):
|
|
188
|
+
name, path, args, kwargs = super().deconstruct()
|
|
189
|
+
if self.auto_now:
|
|
190
|
+
kwargs["auto_now"] = True
|
|
191
|
+
if self.auto_now_add:
|
|
192
|
+
kwargs["auto_now_add"] = True
|
|
193
|
+
return name, path, args, kwargs
|
|
194
|
+
|
|
195
|
+
def formfield(self, **kwargs):
|
|
196
|
+
from .widgets import NepaliDatePickerWidget
|
|
197
|
+
|
|
198
|
+
defaults = {
|
|
199
|
+
"widget": NepaliDatePickerWidget,
|
|
200
|
+
}
|
|
201
|
+
defaults.update(kwargs)
|
|
202
|
+
return super().formfield(**defaults)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class ProvinceField(models.CharField):
|
|
206
|
+
"""
|
|
207
|
+
A Django Model Field for Nepali Provinces.
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
description = _("Nepali Province")
|
|
211
|
+
|
|
212
|
+
def __init__(self, *args, **kwargs):
|
|
213
|
+
from nepali.locations import provinces
|
|
214
|
+
|
|
215
|
+
kwargs.setdefault("max_length", 100)
|
|
216
|
+
kwargs.setdefault("choices", [(p.name, p.name) for p in provinces])
|
|
217
|
+
super().__init__(*args, **kwargs)
|
|
218
|
+
|
|
219
|
+
def formfield(self, **kwargs):
|
|
220
|
+
from .widgets import ProvinceSelectWidget
|
|
221
|
+
|
|
222
|
+
defaults = {"widget": ProvinceSelectWidget}
|
|
223
|
+
defaults.update(kwargs)
|
|
224
|
+
return super().formfield(**defaults)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class DistrictField(models.CharField):
|
|
228
|
+
"""
|
|
229
|
+
A Django Model Field for Nepali Districts.
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
description = _("Nepali District")
|
|
233
|
+
|
|
234
|
+
def __init__(self, *args, **kwargs):
|
|
235
|
+
from nepali.locations import districts
|
|
236
|
+
|
|
237
|
+
kwargs.setdefault("max_length", 100)
|
|
238
|
+
kwargs.setdefault("choices", [(d.name, d.name) for d in districts])
|
|
239
|
+
super().__init__(*args, **kwargs)
|
|
240
|
+
|
|
241
|
+
def formfield(self, **kwargs):
|
|
242
|
+
from .widgets import DistrictSelectWidget
|
|
243
|
+
|
|
244
|
+
defaults = {"widget": DistrictSelectWidget}
|
|
245
|
+
defaults.update(kwargs)
|
|
246
|
+
return super().formfield(**defaults)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class MunicipalityField(models.CharField):
|
|
250
|
+
"""
|
|
251
|
+
A Django Model Field for Nepali Municipalities.
|
|
252
|
+
Includes Metropolitan, Sub-Metropolitan, Municipality, and Rural Municipality.
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
description = _("Nepali Municipality")
|
|
256
|
+
|
|
257
|
+
def __init__(self, *args, **kwargs):
|
|
258
|
+
from nepali.locations import municipalities
|
|
259
|
+
|
|
260
|
+
kwargs.setdefault("max_length", 100)
|
|
261
|
+
kwargs.setdefault("choices", [(m.name, m.name) for m in municipalities])
|
|
262
|
+
super().__init__(*args, **kwargs)
|
|
263
|
+
|
|
264
|
+
def formfield(self, **kwargs):
|
|
265
|
+
from .widgets import MunicipalitySelectWidget
|
|
266
|
+
|
|
267
|
+
defaults = {"widget": MunicipalitySelectWidget}
|
|
268
|
+
defaults.update(kwargs)
|
|
269
|
+
return super().formfield(**defaults)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional, Type
|
|
4
|
+
|
|
5
|
+
from nepali.datetime import nepalidate, nepalidatetime
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from rest_framework import serializers
|
|
9
|
+
except ModuleNotFoundError as e:
|
|
10
|
+
raise ModuleNotFoundError(
|
|
11
|
+
"django-nepkit DRF support is optional. Install with `django-nepkit[drf]` "
|
|
12
|
+
"to use `django_nepkit.serializers`."
|
|
13
|
+
) from e
|
|
14
|
+
|
|
15
|
+
from django_nepkit.utils import try_parse_nepali_date, try_parse_nepali_datetime
|
|
16
|
+
|
|
17
|
+
# --------------------------------------------------
|
|
18
|
+
# Base Serializer Field
|
|
19
|
+
# --------------------------------------------------
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BaseNepaliBSField(serializers.Field):
|
|
23
|
+
"""
|
|
24
|
+
Base DRF field for Nepali (BS) Date / DateTime.
|
|
25
|
+
|
|
26
|
+
- **Input**: BS string, or an already-parsed `nepalidate`/`nepalidatetime`
|
|
27
|
+
- **Output**: formatted BS string (configurable)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
format: str = ""
|
|
31
|
+
nepali_type: Type[object] = object
|
|
32
|
+
|
|
33
|
+
default_error_messages = {
|
|
34
|
+
"invalid": "Invalid Bikram Sambat value. Expected format: {format}.",
|
|
35
|
+
"invalid_type": "Invalid type. Expected a string.",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
def __init__(self, *, format: Optional[str] = None, **kwargs: Any) -> None:
|
|
39
|
+
"""
|
|
40
|
+
Args:
|
|
41
|
+
format: Optional `strftime` format used for representation.
|
|
42
|
+
If not provided, uses the class default.
|
|
43
|
+
"""
|
|
44
|
+
if format is not None:
|
|
45
|
+
self.format = format
|
|
46
|
+
super().__init__(**kwargs)
|
|
47
|
+
|
|
48
|
+
def _parse(self, value: str):
|
|
49
|
+
if self.nepali_type is nepalidate:
|
|
50
|
+
return try_parse_nepali_date(value)
|
|
51
|
+
if self.nepali_type is nepalidatetime:
|
|
52
|
+
return try_parse_nepali_datetime(value)
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
def to_representation(self, value: Any) -> Optional[str]:
|
|
56
|
+
if value is None:
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
if isinstance(value, self.nepali_type):
|
|
60
|
+
return value.strftime(self.format) # type: ignore[attr-defined]
|
|
61
|
+
|
|
62
|
+
# If DB returns string, try to normalize it.
|
|
63
|
+
if isinstance(value, str):
|
|
64
|
+
parsed = self._parse(value)
|
|
65
|
+
if parsed is not None:
|
|
66
|
+
return parsed.strftime(self.format) # type: ignore[attr-defined]
|
|
67
|
+
|
|
68
|
+
# Fallback: best-effort stringify (keeps behavior non-breaking)
|
|
69
|
+
return str(value)
|
|
70
|
+
|
|
71
|
+
def to_internal_value(self, data: Any):
|
|
72
|
+
if data in (None, ""):
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
if isinstance(data, self.nepali_type):
|
|
76
|
+
return data
|
|
77
|
+
|
|
78
|
+
if not isinstance(data, str):
|
|
79
|
+
self.fail("invalid_type")
|
|
80
|
+
|
|
81
|
+
parsed = self._parse(data)
|
|
82
|
+
if parsed is not None:
|
|
83
|
+
return parsed
|
|
84
|
+
|
|
85
|
+
self.fail("invalid", format=self.format)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# --------------------------------------------------
|
|
89
|
+
# Nepali Date (BS)
|
|
90
|
+
# --------------------------------------------------
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class NepaliDateSerializerField(BaseNepaliBSField):
|
|
94
|
+
"""
|
|
95
|
+
DRF field for Nepali BS Date (YYYY-MM-DD)
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
format = "%Y-%m-%d"
|
|
99
|
+
nepali_type = nepalidate
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# --------------------------------------------------
|
|
103
|
+
# Nepali DateTime (BS)
|
|
104
|
+
# --------------------------------------------------
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class NepaliDateTimeSerializerField(BaseNepaliBSField):
|
|
108
|
+
"""
|
|
109
|
+
DRF field for Nepali BS DateTime (YYYY-MM-DD HH:MM:SS)
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
format = "%Y-%m-%d %H:%M:%S"
|
|
113
|
+
nepali_type = nepalidatetime
|