- Implement server-side location chaining with HTMX support via `htmx=True` in fields and widgets. - Update location views to handle HTMX requests and return HTML <option> fragments. - Add NepaliDateYearFilter and NepaliDateRangeFilter for robust DRF API filtering. - Expand default `DATE_INPUT_FORMATS` in configuration to support multiple common Nepali date styles. - Update DRF serializers to automatically respect global `NEPKIT` configuration settings. - Add `django-filter` as an optional dependency under the `drf` extra in pyproject.toml. - Correct underlying dependency references from `nepali-datetime` to nepali library. - Complete overhaul of README.md for premium DX, including new sections for storage behavior, timezone awareness, and API usage.
348 lines
12 KiB
Python
348 lines
12 KiB
Python
from datetime import date as python_date
|
|
from datetime import datetime as python_datetime
|
|
|
|
from django.db import models
|
|
from django.utils import timezone
|
|
from django.utils.translation import gettext_lazy as _
|
|
from nepali.datetime import nepalidate, nepalidatetime
|
|
from nepali.locations import districts, municipalities, provinces
|
|
|
|
from django_nepkit.forms import NepaliDateFormField
|
|
from django_nepkit.utils import (
|
|
BS_DATE_FORMAT,
|
|
BS_DATETIME_FORMAT,
|
|
try_parse_nepali_date,
|
|
try_parse_nepali_datetime,
|
|
)
|
|
from django_nepkit.validators import validate_nepali_phone_number
|
|
from django_nepkit.widgets import (
|
|
DistrictSelectWidget,
|
|
MunicipalitySelectWidget,
|
|
NepaliDatePickerWidget,
|
|
ProvinceSelectWidget,
|
|
)
|
|
from django_nepkit.conf import nepkit_settings
|
|
|
|
|
|
class NepaliFieldMixin:
|
|
"""
|
|
Mixin for Nepali model fields to handle 'ne' and 'en' arguments.
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
default_lang = nepkit_settings.DEFAULT_LANGUAGE
|
|
self.ne = kwargs.pop("ne", default_lang == "ne")
|
|
|
|
explicit_en = "en" in kwargs
|
|
en_value = kwargs.pop("en", not self.ne)
|
|
|
|
if self.ne and not explicit_en:
|
|
self.en = False
|
|
else:
|
|
self.en = en_value
|
|
|
|
self.htmx = kwargs.pop("htmx", False)
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def deconstruct(self):
|
|
name, path, args, kwargs = super().deconstruct()
|
|
if self.ne:
|
|
kwargs["ne"] = True
|
|
if not self.en:
|
|
kwargs["en"] = False
|
|
if self.htmx:
|
|
kwargs["htmx"] = True
|
|
return name, path, args, kwargs
|
|
|
|
|
|
class NepaliPhoneNumberField(models.CharField):
|
|
description = _("Nepali Phone Number")
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
kwargs.setdefault("max_length", 10)
|
|
super().__init__(*args, **kwargs)
|
|
self.validators.append(validate_nepali_phone_number)
|
|
|
|
|
|
class NepaliDateField(NepaliFieldMixin, models.CharField):
|
|
"""
|
|
A Django Model Field that stores Nepali (Bikram Sambat) Date.
|
|
Internally it stores the date as BS string (YYYY-MM-DD) in the database.
|
|
"""
|
|
|
|
description = _("Nepali Date (Bikram Sambat)")
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.auto_now = kwargs.pop("auto_now", False)
|
|
self.auto_now_add = kwargs.pop("auto_now_add", False)
|
|
|
|
# Match Django's DateField behavior
|
|
if self.auto_now or self.auto_now_add:
|
|
kwargs.setdefault("editable", False)
|
|
kwargs.setdefault("blank", True)
|
|
|
|
kwargs.setdefault("max_length", 10)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def pre_save(self, model_instance, add):
|
|
if self.auto_now or (self.auto_now_add and add):
|
|
# Using timezone.now() to get current date, then converting to BS.
|
|
now = timezone.now()
|
|
if timezone.is_aware(now):
|
|
now = timezone.localtime(now)
|
|
value = nepalidate.from_date(now).strftime(BS_DATE_FORMAT)
|
|
setattr(model_instance, self.attname, value)
|
|
return value
|
|
return super().pre_save(model_instance, add)
|
|
|
|
def from_db_value(self, value, expression, connection):
|
|
parsed = try_parse_nepali_date(value)
|
|
return parsed if parsed is not None else value
|
|
|
|
def to_python(self, value):
|
|
if value is None or isinstance(value, nepalidate):
|
|
return value
|
|
if isinstance(value, (python_date, python_datetime)):
|
|
try:
|
|
if isinstance(value, python_datetime) and timezone.is_aware(value):
|
|
value = timezone.localtime(value)
|
|
return nepalidate.from_date(value)
|
|
except (ValueError, TypeError):
|
|
return str(value)
|
|
if isinstance(value, str):
|
|
parsed = try_parse_nepali_date(value)
|
|
return parsed if parsed is not None else value
|
|
return super().to_python(value)
|
|
|
|
def validate(self, value, model_instance):
|
|
if isinstance(value, nepalidate):
|
|
value = value.strftime(BS_DATE_FORMAT)
|
|
super().validate(value, model_instance)
|
|
|
|
def run_validators(self, value):
|
|
if isinstance(value, nepalidate):
|
|
value = value.strftime(BS_DATE_FORMAT)
|
|
super().run_validators(value)
|
|
|
|
def get_prep_value(self, value):
|
|
if value is None:
|
|
return value
|
|
if isinstance(value, nepalidate):
|
|
return value.strftime(BS_DATE_FORMAT)
|
|
if isinstance(value, (python_date, python_datetime)):
|
|
try:
|
|
if isinstance(value, python_datetime) and timezone.is_aware(value):
|
|
value = timezone.localtime(value)
|
|
return nepalidate.from_date(value).strftime(BS_DATE_FORMAT)
|
|
except (ValueError, TypeError):
|
|
return str(value)
|
|
if isinstance(value, str):
|
|
return value
|
|
return str(value)
|
|
|
|
def deconstruct(self):
|
|
name, path, args, kwargs = super().deconstruct()
|
|
if self.auto_now:
|
|
kwargs["auto_now"] = True
|
|
if self.auto_now_add:
|
|
kwargs["auto_now_add"] = True
|
|
return name, path, args, kwargs
|
|
|
|
def formfield(self, **kwargs):
|
|
defaults = {
|
|
"form_class": NepaliDateFormField,
|
|
"widget": NepaliDatePickerWidget(ne=self.ne, en=self.en),
|
|
}
|
|
defaults.update(kwargs)
|
|
return super().formfield(**defaults)
|
|
|
|
|
|
class NepaliTimeField(NepaliFieldMixin, models.TimeField):
|
|
"""
|
|
A Django Model Field for Time with Nepali localization support.
|
|
"""
|
|
|
|
description = _("Nepali Time")
|
|
|
|
|
|
class NepaliDateTimeField(NepaliFieldMixin, models.CharField):
|
|
"""
|
|
A Django Model Field that stores Nepali (Bikram Sambat) DateTime.
|
|
Internally it stores the datetime as BS string (YYYY-MM-DD HH:MM:SS) in the database.
|
|
"""
|
|
|
|
description = _("Nepali DateTime (Bikram Sambat)")
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.auto_now = kwargs.pop("auto_now", False)
|
|
self.auto_now_add = kwargs.pop("auto_now_add", False)
|
|
|
|
# Match Django's DateTimeField behavior
|
|
if self.auto_now or self.auto_now_add:
|
|
kwargs.setdefault("editable", False)
|
|
kwargs.setdefault("blank", True)
|
|
|
|
kwargs.setdefault("max_length", 19) # YYYY-MM-DD HH:MM:SS
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def pre_save(self, model_instance, add):
|
|
if self.auto_now or (self.auto_now_add and add):
|
|
now = timezone.now()
|
|
if timezone.is_aware(now):
|
|
now = timezone.localtime(now)
|
|
value = nepalidatetime.from_datetime(now).strftime(BS_DATETIME_FORMAT)
|
|
setattr(model_instance, self.attname, value)
|
|
return value
|
|
return super().pre_save(model_instance, add)
|
|
|
|
def from_db_value(self, value, expression, connection):
|
|
parsed = try_parse_nepali_datetime(value)
|
|
return parsed if parsed is not None else value
|
|
|
|
def to_python(self, value):
|
|
if value is None or isinstance(value, nepalidatetime):
|
|
return value
|
|
if isinstance(value, python_datetime):
|
|
try:
|
|
if timezone.is_aware(value):
|
|
value = timezone.localtime(value)
|
|
return nepalidatetime.from_datetime(value)
|
|
except (ValueError, TypeError):
|
|
return str(value)
|
|
if isinstance(value, str):
|
|
parsed = try_parse_nepali_datetime(value)
|
|
return parsed if parsed is not None else value
|
|
return super().to_python(value)
|
|
|
|
def validate(self, value, model_instance):
|
|
if isinstance(value, nepalidatetime):
|
|
value = value.strftime(BS_DATETIME_FORMAT)
|
|
super().validate(value, model_instance)
|
|
|
|
def run_validators(self, value):
|
|
if isinstance(value, nepalidatetime):
|
|
value = value.strftime(BS_DATETIME_FORMAT)
|
|
super().run_validators(value)
|
|
|
|
def get_prep_value(self, value):
|
|
if value is None:
|
|
return value
|
|
if isinstance(value, nepalidatetime):
|
|
return value.strftime(BS_DATETIME_FORMAT)
|
|
if isinstance(value, python_datetime):
|
|
try:
|
|
if timezone.is_aware(value):
|
|
value = timezone.localtime(value)
|
|
return nepalidatetime.from_datetime(value).strftime(BS_DATETIME_FORMAT)
|
|
except (ValueError, TypeError):
|
|
return str(value)
|
|
if isinstance(value, str):
|
|
return value
|
|
return str(value)
|
|
|
|
def deconstruct(self):
|
|
name, path, args, kwargs = super().deconstruct()
|
|
if self.auto_now:
|
|
kwargs["auto_now"] = True
|
|
if self.auto_now_add:
|
|
kwargs["auto_now_add"] = True
|
|
return name, path, args, kwargs
|
|
|
|
def formfield(self, **kwargs):
|
|
defaults = {
|
|
"widget": NepaliDatePickerWidget(ne=self.ne, en=self.en),
|
|
}
|
|
defaults.update(kwargs)
|
|
return super().formfield(**defaults)
|
|
|
|
|
|
class ProvinceField(NepaliFieldMixin, models.CharField):
|
|
description = _("Nepali Province")
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
kwargs.setdefault("max_length", 100)
|
|
|
|
# We need self.ne set by Mixin, but Mixin init runs last in normal MRO if we just use super().__init__.
|
|
# So we manually handle pop before calling super, OR we rely on kwargs.
|
|
|
|
# Actually, if we use NepaliFieldMixin as first parent, its __init__ will run.
|
|
# But we need to define choices based on ne.
|
|
# So we should pop 'ne' here, set it, then pass to super?
|
|
# But Mixin expects to pop it.
|
|
|
|
# Better approach: Extract ne here without popping (peek), or pop and pass back?
|
|
# Let's just duplicate popping here to set choices, then re-inject for Mixin?
|
|
# Or just handle it manually here and pass remaining to super.
|
|
|
|
# Let's modify behavior: pop ne here, calculate choices, then put it back?
|
|
# Or just manually set self.ne/self.en here and don't rely on Mixin's init for choices, but rely on it for other things?
|
|
|
|
# To avoid complexity, I'll peek at kwargs['ne'] if it exists.
|
|
default_lang = nepkit_settings.DEFAULT_LANGUAGE
|
|
ne = kwargs.get("ne", default_lang == "ne")
|
|
|
|
def get_p_name(p, ne):
|
|
name = getattr(p, "name_nepali", p.name) if ne else p.name
|
|
if name == "Province 1":
|
|
return "Koshi Province"
|
|
if name == "प्रदेश नं. १":
|
|
return "कोशी प्रदेश"
|
|
return name
|
|
|
|
choices = [(get_p_name(p, ne), get_p_name(p, ne)) for p in provinces]
|
|
|
|
kwargs.setdefault("choices", choices)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def formfield(self, **kwargs):
|
|
defaults = {"widget": ProvinceSelectWidget(ne=self.ne, en=self.en, htmx=self.htmx)}
|
|
defaults.update(kwargs)
|
|
return super(models.CharField, self).formfield(**defaults)
|
|
# Wait, super().formfield() calls Django's CharField.formfield.
|
|
# We want that.
|
|
|
|
|
|
class DistrictField(NepaliFieldMixin, models.CharField):
|
|
description = _("Nepali District")
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
default_lang = nepkit_settings.DEFAULT_LANGUAGE
|
|
ne = kwargs.get("ne", default_lang == "ne")
|
|
kwargs.setdefault("max_length", 100)
|
|
|
|
def get_d_name(d, ne):
|
|
return getattr(d, "name_nepali", d.name) if ne else d.name
|
|
|
|
choices = [(get_d_name(d, ne), get_d_name(d, ne)) for d in districts]
|
|
|
|
kwargs.setdefault("choices", choices)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def formfield(self, **kwargs):
|
|
defaults = {"widget": DistrictSelectWidget(ne=self.ne, en=self.en, htmx=self.htmx)}
|
|
defaults.update(kwargs)
|
|
return super(models.CharField, self).formfield(**defaults)
|
|
|
|
|
|
class MunicipalityField(NepaliFieldMixin, models.CharField):
|
|
description = _("Nepali Municipality")
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
default_lang = nepkit_settings.DEFAULT_LANGUAGE
|
|
ne = kwargs.get("ne", default_lang == "ne")
|
|
kwargs.setdefault("max_length", 100)
|
|
|
|
def get_m_name(m, ne):
|
|
return getattr(m, "name_nepali", m.name) if ne else m.name
|
|
|
|
choices = [(get_m_name(m, ne), get_m_name(m, ne)) for m in municipalities]
|
|
|
|
kwargs.setdefault("choices", choices)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def formfield(self, **kwargs):
|
|
defaults = {"widget": MunicipalitySelectWidget(ne=self.ne, en=self.en, htmx=self.htmx)}
|
|
defaults.update(kwargs)
|
|
return super(models.CharField, self).formfield(**defaults)
|