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.
Files changed (39) hide show
  1. django_nepkit/__init__.py +31 -0
  2. django_nepkit/admin.py +211 -0
  3. django_nepkit/forms.py +50 -0
  4. django_nepkit/models.py +269 -0
  5. django_nepkit/serializers.py +113 -0
  6. django_nepkit/static/django_nepkit/css/admin-nepali-datepicker.css +37 -0
  7. django_nepkit/static/django_nepkit/js/address-chaining.js +64 -0
  8. django_nepkit/static/django_nepkit/js/admin-jquery-bridge.js +10 -0
  9. django_nepkit/static/django_nepkit/js/nepali-datepicker-init.js +108 -0
  10. django_nepkit/templatetags/__init__.py +0 -0
  11. django_nepkit/templatetags/nepali.py +74 -0
  12. django_nepkit/urls.py +10 -0
  13. django_nepkit/utils.py +77 -0
  14. django_nepkit/validators.py +12 -0
  15. django_nepkit/views.py +22 -0
  16. django_nepkit/widgets.py +72 -0
  17. django_nepkit-0.1.0.dist-info/METADATA +377 -0
  18. django_nepkit-0.1.0.dist-info/RECORD +39 -0
  19. django_nepkit-0.1.0.dist-info/WHEEL +5 -0
  20. django_nepkit-0.1.0.dist-info/licenses/LICENSE +21 -0
  21. django_nepkit-0.1.0.dist-info/top_level.txt +2 -0
  22. example/demo/__init__.py +0 -0
  23. example/demo/admin.py +42 -0
  24. example/demo/apps.py +5 -0
  25. example/demo/migrations/0001_initial.py +2113 -0
  26. example/demo/migrations/0002_alter_person_phone_number.py +18 -0
  27. example/demo/migrations/0003_person_created_at_person_updated_at.py +27 -0
  28. example/demo/migrations/0004_alter_person_created_at_alter_person_updated_at.py +23 -0
  29. example/demo/migrations/0005_alter_person_created_at_alter_person_updated_at.py +27 -0
  30. example/demo/migrations/__init__.py +0 -0
  31. example/demo/models.py +27 -0
  32. example/demo/tests.py +1 -0
  33. example/demo/urls.py +9 -0
  34. example/demo/views.py +32 -0
  35. example/example_project/__init__.py +0 -0
  36. example/example_project/settings.py +76 -0
  37. example/example_project/urls.py +8 -0
  38. example/example_project/wsgi.py +7 -0
  39. 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)
@@ -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