feat(models): added nepalidatetime fields
This commit is contained in:
8 files changed
+628
-212
No files matched your search
@@ -0,0 +1,155 @@
|
||||
from django.contrib import admin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from nepali.datetime import nepalidate, nepalidatetime
|
||||
|
||||
from django_nepkit.models import NepaliDateField
|
||||
|
||||
|
||||
def format_nepali_date(date_value, format_string="%B %d, %Y"):
|
||||
"""
|
||||
Format a nepalidate object with Nepali month names.
|
||||
|
||||
Args:
|
||||
date_value: A nepalidate object or string in YYYY-MM-DD format
|
||||
format_string: strftime format string (default: '%B %d, %Y')
|
||||
%B = Full month name (Baishak, Jestha, etc.)
|
||||
%b = Short month name
|
||||
%d = Day of month
|
||||
%Y = Year
|
||||
|
||||
Returns:
|
||||
Formatted date string with Nepali month names, or empty string if invalid
|
||||
"""
|
||||
|
||||
if date_value is None:
|
||||
return ""
|
||||
|
||||
try:
|
||||
if isinstance(date_value, str):
|
||||
date_value = nepalidate.strptime(date_value, "%Y-%m-%d")
|
||||
|
||||
if isinstance(date_value, nepalidate):
|
||||
return date_value.strftime(format_string)
|
||||
except (ValueError, TypeError, AttributeError):
|
||||
pass
|
||||
|
||||
return str(date_value) if date_value else ""
|
||||
|
||||
|
||||
def format_nepali_datetime(datetime_value, format_string="%B %d, %Y %I:%M %p"):
|
||||
"""
|
||||
Format a nepalidatetime object with Nepali month names.
|
||||
|
||||
Default output uses **12-hour time with AM/PM**.
|
||||
"""
|
||||
if datetime_value is None:
|
||||
return ""
|
||||
|
||||
try:
|
||||
if isinstance(datetime_value, str):
|
||||
datetime_value = nepalidatetime.strptime(
|
||||
datetime_value, "%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
|
||||
if isinstance(datetime_value, nepalidatetime):
|
||||
return datetime_value.strftime(format_string)
|
||||
except (ValueError, TypeError, AttributeError):
|
||||
pass
|
||||
|
||||
return str(datetime_value) if datetime_value else ""
|
||||
|
||||
|
||||
class NepaliDateFilter(admin.FieldListFilter):
|
||||
"""
|
||||
A list filter for NepaliDateField to filter by BS Year.
|
||||
"""
|
||||
|
||||
def __init__(self, field, request, params, model, model_admin, field_path):
|
||||
self.parameter_name = f"{field_path}_bs_year"
|
||||
super().__init__(field, request, params, model, model_admin, field_path)
|
||||
self.title = _("Nepali Date (Year)")
|
||||
|
||||
def expected_parameters(self):
|
||||
return [self.parameter_name]
|
||||
|
||||
def choices(self, changelist):
|
||||
yield {
|
||||
"selected": self.used_parameters.get(self.parameter_name) is None,
|
||||
"query_string": changelist.get_query_string(remove=[self.parameter_name]),
|
||||
"display": _("All"),
|
||||
}
|
||||
current_year = nepalidate.today().year
|
||||
for year in range(current_year - 10, current_year + 2):
|
||||
yield {
|
||||
"selected": self.used_parameters.get(self.parameter_name) == str(year),
|
||||
"query_string": changelist.get_query_string(
|
||||
{self.parameter_name: str(year)}
|
||||
),
|
||||
"display": str(year),
|
||||
}
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
value = self.used_parameters.get(self.parameter_name)
|
||||
if value:
|
||||
year = int(value)
|
||||
# Convert BS year range to AD date range
|
||||
start_date_bs = nepalidate(year, 1, 1)
|
||||
# Find last day of the year. Some BS years end on the 30th,
|
||||
# so we try 31 first and then fall back to 30 if that date
|
||||
# is not valid.
|
||||
try:
|
||||
end_date_bs = nepalidate(year, 12, 31)
|
||||
except ValueError:
|
||||
end_date_bs = nepalidate(year, 12, 30)
|
||||
|
||||
start_date_ad = start_date_bs.to_date()
|
||||
end_date_ad = end_date_bs.to_date()
|
||||
|
||||
return queryset.filter(
|
||||
**{f"{self.field_path}__range": (start_date_ad, end_date_ad)}
|
||||
)
|
||||
return queryset
|
||||
|
||||
|
||||
admin.FieldListFilter.register(
|
||||
lambda f: isinstance(f, NepaliDateField), NepaliDateFilter, take_priority=True
|
||||
)
|
||||
|
||||
|
||||
class NepaliAdminMixin:
|
||||
"""
|
||||
Mixin for Django admin classes that provides Nepali date utilities.
|
||||
Makes format_nepali_date and NepaliDateFilter available without explicit imports.
|
||||
"""
|
||||
|
||||
def format_nepali_date(self, date_value, format_string="%B %d, %Y"):
|
||||
"""
|
||||
Format a nepalidate object with Nepali month names.
|
||||
Available as a method on admin classes using this mixin.
|
||||
"""
|
||||
return format_nepali_date(date_value, format_string)
|
||||
|
||||
def format_nepali_datetime(
|
||||
self, datetime_value, format_string="%B %d, %Y %I:%M %p"
|
||||
):
|
||||
return format_nepali_datetime(datetime_value, format_string)
|
||||
|
||||
|
||||
class NepaliModelAdmin(NepaliAdminMixin, admin.ModelAdmin):
|
||||
"""
|
||||
Base ModelAdmin class with Nepali date utilities built-in.
|
||||
Use this instead of admin.ModelAdmin to have format_nepali_date available.
|
||||
|
||||
Example:
|
||||
from django_nepkit import NepaliModelAdmin, NepaliDateFilter
|
||||
|
||||
@admin.register(MyModel)
|
||||
class MyModelAdmin(NepaliModelAdmin):
|
||||
list_filter = (('date_field', NepaliDateFilter),)
|
||||
|
||||
def display_date(self, obj):
|
||||
return self.format_nepali_date(obj.date_field)
|
||||
"""
|
||||
|
||||
# Make NepaliDateFilter available as a class attribute
|
||||
NepaliDateFilter = NepaliDateFilter
|
||||
+103
-84
@@ -1,155 +1,174 @@
|
||||
from django.contrib import admin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from nepali.datetime import nepalidate, nepalidatetime
|
||||
|
||||
from django_nepkit.models import NepaliDateField
|
||||
|
||||
|
||||
def format_nepali_date(date_value, format_string="%B %d, %Y"):
|
||||
# --------------------------------------------------
|
||||
# Internal helpers
|
||||
# --------------------------------------------------
|
||||
|
||||
def _parse_nepali_date(value):
|
||||
"""
|
||||
Format a nepalidate object with Nepali month names.
|
||||
|
||||
Args:
|
||||
date_value: A nepalidate object or string in YYYY-MM-DD format
|
||||
format_string: strftime format string (default: '%B %d, %Y')
|
||||
%B = Full month name (Baishak, Jestha, etc.)
|
||||
%b = Short month name
|
||||
%d = Day of month
|
||||
%Y = Year
|
||||
|
||||
Returns:
|
||||
Formatted date string with Nepali month names, or empty string if invalid
|
||||
Normalize string / nepalidate to nepalidate.
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
if date_value is None:
|
||||
return ""
|
||||
if isinstance(value, nepalidate):
|
||||
return value
|
||||
|
||||
try:
|
||||
if isinstance(date_value, str):
|
||||
date_value = nepalidate.strptime(date_value, "%Y-%m-%d")
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return nepalidate.strptime(value, "%Y-%m-%d")
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
if isinstance(date_value, nepalidate):
|
||||
return date_value.strftime(format_string)
|
||||
except (ValueError, TypeError, AttributeError):
|
||||
pass
|
||||
|
||||
return str(date_value) if date_value else ""
|
||||
return None
|
||||
|
||||
|
||||
def format_nepali_datetime(datetime_value, format_string="%B %d, %Y %I:%M %p"):
|
||||
def _parse_nepali_datetime(value):
|
||||
"""
|
||||
Format a nepalidatetime object with Nepali month names.
|
||||
|
||||
Default output uses **12-hour time with AM/PM**.
|
||||
Normalize string / nepalidatetime to nepalidatetime.
|
||||
"""
|
||||
if datetime_value is None:
|
||||
return ""
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
if isinstance(datetime_value, str):
|
||||
datetime_value = nepalidatetime.strptime(
|
||||
datetime_value, "%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
if isinstance(value, nepalidatetime):
|
||||
return value
|
||||
|
||||
if isinstance(datetime_value, nepalidatetime):
|
||||
return datetime_value.strftime(format_string)
|
||||
except (ValueError, TypeError, AttributeError):
|
||||
pass
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return nepalidatetime.strptime(value, "%Y-%m-%d %H:%M:%S")
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
return str(datetime_value) if datetime_value else ""
|
||||
return None
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# Formatting helpers
|
||||
# --------------------------------------------------
|
||||
|
||||
def format_nepali_date(value, format_string="%B %d, %Y"):
|
||||
"""
|
||||
Format a Nepali (BS) date with Nepali month names.
|
||||
"""
|
||||
nd = _parse_nepali_date(value)
|
||||
return nd.strftime(format_string) if nd else ""
|
||||
|
||||
|
||||
def format_nepali_datetime(value, format_string="%B %d, %Y %I:%M %p"):
|
||||
"""
|
||||
Format a Nepali (BS) datetime (12-hour format by default).
|
||||
"""
|
||||
ndt = _parse_nepali_datetime(value)
|
||||
return ndt.strftime(format_string) if ndt else ""
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# Admin List Filter (BS Year)
|
||||
# --------------------------------------------------
|
||||
|
||||
class NepaliDateFilter(admin.FieldListFilter):
|
||||
"""
|
||||
A list filter for NepaliDateField to filter by BS Year.
|
||||
Admin list filter for NepaliDateField by BS year.
|
||||
"""
|
||||
|
||||
title = _("Nepali Date (Year)")
|
||||
|
||||
def __init__(self, field, request, params, model, model_admin, field_path):
|
||||
self.field_path = field_path
|
||||
self.parameter_name = f"{field_path}_bs_year"
|
||||
super().__init__(field, request, params, model, model_admin, field_path)
|
||||
self.title = _("Nepali Date (Year)")
|
||||
|
||||
def expected_parameters(self):
|
||||
return [self.parameter_name]
|
||||
|
||||
def _year_choices(self):
|
||||
current_year = nepalidate.today().year
|
||||
return range(current_year - 10, current_year + 2)
|
||||
|
||||
def choices(self, changelist):
|
||||
yield {
|
||||
"selected": self.used_parameters.get(self.parameter_name) is None,
|
||||
"selected": self.parameter_name not in self.used_parameters,
|
||||
"query_string": changelist.get_query_string(remove=[self.parameter_name]),
|
||||
"display": _("All"),
|
||||
}
|
||||
current_year = nepalidate.today().year
|
||||
for year in range(current_year - 10, current_year + 2):
|
||||
|
||||
for year in self._year_choices():
|
||||
yield {
|
||||
"selected": self.used_parameters.get(self.parameter_name) == str(year),
|
||||
"query_string": changelist.get_query_string(
|
||||
{self.parameter_name: str(year)}
|
||||
{self.parameter_name: year}
|
||||
),
|
||||
"display": str(year),
|
||||
"display": year,
|
||||
}
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
value = self.used_parameters.get(self.parameter_name)
|
||||
if value:
|
||||
if not value:
|
||||
return queryset
|
||||
|
||||
try:
|
||||
year = int(value)
|
||||
# Convert BS year range to AD date range
|
||||
start_date_bs = nepalidate(year, 1, 1)
|
||||
# Find last day of the year. Some BS years end on the 30th,
|
||||
# so we try 31 first and then fall back to 30 if that date
|
||||
# is not valid.
|
||||
except (TypeError, ValueError):
|
||||
return queryset
|
||||
|
||||
start_bs = nepalidate(year, 1, 1)
|
||||
|
||||
# BS year length varies (30/31 days)
|
||||
for last_day in (31, 30):
|
||||
try:
|
||||
end_date_bs = nepalidate(year, 12, 31)
|
||||
end_bs = nepalidate(year, 12, last_day)
|
||||
break
|
||||
except ValueError:
|
||||
end_date_bs = nepalidate(year, 12, 30)
|
||||
continue
|
||||
|
||||
start_date_ad = start_date_bs.to_date()
|
||||
end_date_ad = end_date_bs.to_date()
|
||||
|
||||
return queryset.filter(
|
||||
**{f"{self.field_path}__range": (start_date_ad, end_date_ad)}
|
||||
)
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
**{
|
||||
f"{self.field_path}__range": (
|
||||
start_bs.to_date(),
|
||||
end_bs.to_date(),
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# Register filter automatically for NepaliDateField
|
||||
admin.FieldListFilter.register(
|
||||
lambda f: isinstance(f, NepaliDateField), NepaliDateFilter, take_priority=True
|
||||
lambda f: isinstance(f, NepaliDateField),
|
||||
NepaliDateFilter,
|
||||
take_priority=True,
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# Admin mixins
|
||||
# --------------------------------------------------
|
||||
|
||||
class NepaliAdminMixin:
|
||||
"""
|
||||
Mixin for Django admin classes that provides Nepali date utilities.
|
||||
Makes format_nepali_date and NepaliDateFilter available without explicit imports.
|
||||
Adds Nepali date formatting helpers to admin classes.
|
||||
"""
|
||||
|
||||
def format_nepali_date(self, date_value, format_string="%B %d, %Y"):
|
||||
"""
|
||||
Format a nepalidate object with Nepali month names.
|
||||
Available as a method on admin classes using this mixin.
|
||||
"""
|
||||
return format_nepali_date(date_value, format_string)
|
||||
|
||||
def format_nepali_datetime(
|
||||
self, datetime_value, format_string="%B %d, %Y %I:%M %p"
|
||||
):
|
||||
return format_nepali_datetime(datetime_value, format_string)
|
||||
format_nepali_date = staticmethod(format_nepali_date)
|
||||
format_nepali_datetime = staticmethod(format_nepali_datetime)
|
||||
|
||||
|
||||
class NepaliModelAdmin(NepaliAdminMixin, admin.ModelAdmin):
|
||||
"""
|
||||
Base ModelAdmin class with Nepali date utilities built-in.
|
||||
Use this instead of admin.ModelAdmin to have format_nepali_date available.
|
||||
Base ModelAdmin with Nepali utilities.
|
||||
|
||||
Example:
|
||||
from django_nepkit import NepaliModelAdmin, NepaliDateFilter
|
||||
|
||||
@admin.register(MyModel)
|
||||
class MyModelAdmin(NepaliModelAdmin):
|
||||
list_filter = (('date_field', NepaliDateFilter),)
|
||||
list_filter = (('date_bs', NepaliDateFilter),)
|
||||
|
||||
def display_date(self, obj):
|
||||
return self.format_nepali_date(obj.date_field)
|
||||
def date_display(self, obj):
|
||||
return self.format_nepali_date(obj.date_bs)
|
||||
"""
|
||||
|
||||
# Make NepaliDateFilter available as a class attribute
|
||||
NepaliDateFilter = NepaliDateFilter
|
||||
+52
-12
@@ -2,36 +2,76 @@ from datetime import date as python_date
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from nepali.datetime import nepalidate
|
||||
|
||||
from nepali.datetime import nepalidate
|
||||
from django_nepkit.validators import validate_nepali_phone_number
|
||||
from django_nepkit.widgets import NepaliDatePickerWidget
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# Nepali Phone Number Form Field
|
||||
# --------------------------------------------------
|
||||
|
||||
class NepaliPhoneNumberField(forms.CharField):
|
||||
"""
|
||||
Form field for Nepali phone numbers.
|
||||
"""
|
||||
|
||||
default_error_messages = {
|
||||
"invalid": _("Enter a valid Nepali phone number."),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.validators.append(validate_nepali_phone_number)
|
||||
|
||||
|
||||
class NepaliDateFormField(forms.DateField):
|
||||
# --------------------------------------------------
|
||||
# Nepali Date (BS) Form Field
|
||||
# --------------------------------------------------
|
||||
|
||||
class NepaliDateFormField(forms.Field):
|
||||
"""
|
||||
Form field for Nepali (Bikram Sambat) date.
|
||||
|
||||
Accepts:
|
||||
- YYYY-MM-DD (string)
|
||||
- python date
|
||||
- nepalidate instance
|
||||
|
||||
Returns:
|
||||
- nepalidate instance
|
||||
"""
|
||||
|
||||
widget = NepaliDatePickerWidget
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.pop("max_length", None)
|
||||
super().__init__(*args, **kwargs)
|
||||
default_error_messages = {
|
||||
"invalid": _("Enter a valid Nepali date in YYYY-MM-DD format."),
|
||||
}
|
||||
|
||||
def to_python(self, value):
|
||||
if value in self.empty_values:
|
||||
return None
|
||||
|
||||
if isinstance(value, nepalidate):
|
||||
return value
|
||||
|
||||
if isinstance(value, python_date):
|
||||
return nepalidate.from_date(value)
|
||||
try:
|
||||
return nepalidate.strptime(str(value).strip(), "%Y-%m-%d")
|
||||
except Exception:
|
||||
raise forms.ValidationError(
|
||||
_("Enter a valid Nepali date in YYYY-MM-DD format."),
|
||||
code="invalid",
|
||||
)
|
||||
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return nepalidate.strptime(value.strip(), "%Y-%m-%d")
|
||||
except (ValueError, TypeError):
|
||||
self.fail("invalid")
|
||||
|
||||
self.fail("invalid")
|
||||
|
||||
def validate(self, value):
|
||||
"""
|
||||
Override default validation to allow nepalidate objects.
|
||||
"""
|
||||
if value is None:
|
||||
return
|
||||
if not isinstance(value, nepalidate):
|
||||
self.fail("invalid")
|
||||
+126
-71
@@ -2,8 +2,8 @@ from datetime import date as python_date
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from nepali.datetime import nepalidate
|
||||
from nepali.locations import districts, municipalities, provices
|
||||
from nepali.datetime import nepalidate, nepalidatetime
|
||||
from nepali.locations import districts, municipalities, provinces
|
||||
|
||||
from django_nepkit.forms import NepaliDateFormField
|
||||
from django_nepkit.validators import validate_nepali_phone_number
|
||||
@@ -14,6 +14,10 @@ from django_nepkit.widgets import (
|
||||
ProvinceSelectWidget,
|
||||
)
|
||||
|
||||
# --------------------------------------------------
|
||||
# Phone Number
|
||||
# --------------------------------------------------
|
||||
|
||||
|
||||
class NepaliPhoneNumberField(models.CharField):
|
||||
description = _("Nepali phone number")
|
||||
@@ -24,119 +28,133 @@ class NepaliPhoneNumberField(models.CharField):
|
||||
self.validators.append(validate_nepali_phone_number)
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# Location Fields
|
||||
# --------------------------------------------------
|
||||
|
||||
|
||||
class ProvinceField(models.CharField):
|
||||
description = _("Nepal's Province")
|
||||
description = _("Nepal Province")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault("max_length", 100)
|
||||
kwargs.setdefault("choices", [(p.name, p.name) for p in provices])
|
||||
kwargs.setdefault(
|
||||
"choices",
|
||||
[(p.name, p.name) for p in provinces],
|
||||
)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {"widget": ProvinceSelectWidget}
|
||||
defaults.update(kwargs)
|
||||
return super().formfield(**defaults)
|
||||
kwargs.setdefault("widget", ProvinceSelectWidget)
|
||||
return super().formfield(**kwargs)
|
||||
|
||||
|
||||
class DistrictField(models.CharField):
|
||||
description = _("Nepal's District")
|
||||
description = _("Nepal District")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault("max_length", 100)
|
||||
kwargs.setdefault("choices", [(d.name, d.name) for d in districts])
|
||||
kwargs.setdefault(
|
||||
"choices",
|
||||
[(d.name, d.name) for d in districts],
|
||||
)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {"widget": DistrictSelectWidget}
|
||||
defaults.update(kwargs)
|
||||
return super().formfield(**defaults)
|
||||
kwargs.setdefault("widget", DistrictSelectWidget)
|
||||
return super().formfield(**kwargs)
|
||||
|
||||
|
||||
class MunicipalityField(models.CharField):
|
||||
description = _("Nepal's Municipality")
|
||||
description = _("Nepal Municipality")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault("max_length", 100)
|
||||
kwargs.setdefault("choices", [(m.name, m.name) for m in municipalities])
|
||||
kwargs.setdefault(
|
||||
"choices",
|
||||
[(m.name, m.name) for m in municipalities],
|
||||
)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {"widget": MunicipalitySelectWidget}
|
||||
defaults.update(kwargs)
|
||||
return super().formfield(**defaults)
|
||||
kwargs.setdefault("widget", MunicipalitySelectWidget)
|
||||
return super().formfield(**kwargs)
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# Time Field
|
||||
# --------------------------------------------------
|
||||
class NepaliTimeField(models.TimeField):
|
||||
"""
|
||||
Time field for Nepal (standard time).
|
||||
This is essentially Django's TimeField,
|
||||
kept for semantic clarity and future extensions.
|
||||
"""
|
||||
|
||||
description = _("Nepali Time")
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
return super().formfield(**kwargs)
|
||||
|
||||
class NepaliDateField(models.CharField):
|
||||
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)
|
||||
# --------------------------------------------------
|
||||
# Base Nepali BS Date / DateTime Field
|
||||
# --------------------------------------------------
|
||||
|
||||
if self.auto_now or self.auto_now_add:
|
||||
|
||||
class BaseNepaliDateCharField(models.CharField):
|
||||
"""
|
||||
Base class for Nepali (Bikram Sambat) Date / DateTime fields.
|
||||
Stored as string in DB, returned as nepali date objects in Python.
|
||||
"""
|
||||
|
||||
format = None
|
||||
nepali_type = None
|
||||
|
||||
def __init__(self, *args, auto_now=False, auto_now_add=False, **kwargs):
|
||||
self.auto_now = auto_now
|
||||
self.auto_now_add = auto_now_add
|
||||
|
||||
if auto_now or auto_now_add:
|
||||
kwargs.setdefault("editable", False)
|
||||
kwargs.setdefault("blank", True)
|
||||
|
||||
kwargs.setdefault("max_length", 10)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _now(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def pre_save(self, model_instance, add):
|
||||
if self.auto_now or (self.auto_now_add and add):
|
||||
value = nepalidate.today().strftime("%Y-%m-%d")
|
||||
value = self._now().strftime(self.format)
|
||||
setattr(model_instance, self.attname, value)
|
||||
return value
|
||||
return super().pre_save(model_instance, add)
|
||||
|
||||
def to_python(self, value):
|
||||
if value is None or isinstance(value, self.nepali_type):
|
||||
return value
|
||||
|
||||
if isinstance(value, python_date):
|
||||
return self.nepali_type.from_date(value)
|
||||
|
||||
if isinstance(value, str):
|
||||
return self.nepali_type.strptime(value.strip(), self.format)
|
||||
|
||||
return value
|
||||
|
||||
def from_db_value(self, value, expression, connection):
|
||||
if value is None:
|
||||
return value
|
||||
try:
|
||||
return nepalidate.strptime(value, "%Y-%m-%d")
|
||||
except (ValueError, TypeError):
|
||||
return value
|
||||
|
||||
def to_python(self, value):
|
||||
if value is None or isinstance(value, nepalidate):
|
||||
return value
|
||||
if isinstance(value, python_date):
|
||||
try:
|
||||
return nepalidate.from_date(value)
|
||||
except (ValueError, TypeError):
|
||||
return str(value)
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return nepalidate.strptime(value.strip(), "%Y-%m-%d")
|
||||
except (ValueError, TypeError):
|
||||
return value
|
||||
return super().to_python(value)
|
||||
|
||||
def validate(self, value, model_instance):
|
||||
if isinstance(value, nepalidate):
|
||||
value = value.strftime("%Y-%m-%d")
|
||||
super().validate(value, model_instance)
|
||||
|
||||
def run_validators(self, value):
|
||||
if isinstance(value, nepalidate):
|
||||
value = value.strftime("%Y-%m-%d")
|
||||
super().run_validators(value)
|
||||
return self.nepali_type.strptime(value, self.format)
|
||||
|
||||
def get_prep_value(self, value):
|
||||
if value is None:
|
||||
return value
|
||||
if isinstance(value, nepalidate):
|
||||
return value.strftime("%Y-%m-%d")
|
||||
if isinstance(value, python_date):
|
||||
try:
|
||||
return nepalidate.from_date(value).strftime("%Y-%m-%d")
|
||||
except (ValueError, TypeError):
|
||||
return str(value)
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
# Fallback: convert to string
|
||||
return None
|
||||
|
||||
if isinstance(value, self.nepali_type):
|
||||
return value.strftime(self.format)
|
||||
|
||||
return str(value)
|
||||
|
||||
def deconstruct(self):
|
||||
@@ -147,10 +165,47 @@ class NepaliDateField(models.CharField):
|
||||
kwargs["auto_now_add"] = True
|
||||
return name, path, args, kwargs
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# Nepali Date Field (BS)
|
||||
# --------------------------------------------------
|
||||
|
||||
|
||||
class NepaliDateField(BaseNepaliDateCharField):
|
||||
description = _("Nepali Date (Bikram Sambat)")
|
||||
format = "%Y-%m-%d"
|
||||
nepali_type = nepalidate
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault("max_length", 10)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _now(self):
|
||||
return nepalidate.today()
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {
|
||||
"form_class": NepaliDateFormField,
|
||||
"widget": NepaliDatePickerWidget,
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
return super().formfield(**defaults)
|
||||
kwargs.setdefault("form_class", NepaliDateFormField)
|
||||
kwargs.setdefault("widget", NepaliDatePickerWidget)
|
||||
return super().formfield(**kwargs)
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# Nepali DateTime Field (BS)
|
||||
# --------------------------------------------------
|
||||
|
||||
|
||||
class NepaliDateTimeField(BaseNepaliDateCharField):
|
||||
description = _("Nepali DateTime (Bikram Sambat)")
|
||||
format = "%Y-%m-%d %H:%M:%S"
|
||||
nepali_type = nepalidatetime
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault("max_length", 19)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _now(self):
|
||||
return nepalidatetime.now()
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
kwargs.setdefault("widget", NepaliDatePickerWidget)
|
||||
return super().formfield(**kwargs)
|
||||
@@ -0,0 +1,69 @@
|
||||
from nepali.datetime import nepalidate, nepalidatetime
|
||||
from rest_framework import serializers
|
||||
|
||||
# --------------------------------------------------
|
||||
# Base Serializer Field
|
||||
# --------------------------------------------------
|
||||
|
||||
|
||||
class BaseNepaliBSField(serializers.Field):
|
||||
"""
|
||||
Base DRF field for Nepali (BS) Date / DateTime.
|
||||
"""
|
||||
|
||||
format = None
|
||||
nepali_type = None
|
||||
error_messages = {"invalid": "Invalid Bikram Sambat format. Expected {format}."}
|
||||
|
||||
def to_representation(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
if isinstance(value, self.nepali_type):
|
||||
return value.strftime(self.format)
|
||||
|
||||
# Fallback: DB string
|
||||
return str(value)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if data in (None, ""):
|
||||
return None
|
||||
|
||||
if isinstance(data, self.nepali_type):
|
||||
return data
|
||||
|
||||
if isinstance(data, str):
|
||||
try:
|
||||
return self.nepali_type.strptime(data.strip(), self.format)
|
||||
except Exception:
|
||||
self.fail("invalid", format=self.format)
|
||||
|
||||
self.fail("invalid", format=self.format)
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# Nepali Date (BS)
|
||||
# --------------------------------------------------
|
||||
|
||||
|
||||
class NepaliDateSerializerField(BaseNepaliBSField):
|
||||
"""
|
||||
DRF field for Nepali BS Date (YYYY-MM-DD)
|
||||
"""
|
||||
|
||||
format = "%Y-%m-%d"
|
||||
nepali_type = nepalidate
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# Nepali DateTime (BS)
|
||||
# --------------------------------------------------
|
||||
|
||||
|
||||
class NepaliDateTimeSerializerField(BaseNepaliBSField):
|
||||
"""
|
||||
DRF field for Nepali BS DateTime (YYYY-MM-DD HH:MM:SS)
|
||||
"""
|
||||
|
||||
format = "%Y-%m-%d %H:%M:%S"
|
||||
nepali_type = nepalidatetime
|
||||
@@ -1,67 +1,100 @@
|
||||
import datetime
|
||||
|
||||
from django import template
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from nepali.datetime import nepalidate, nepalihumanize
|
||||
from nepali.number import nepalinumber
|
||||
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter
|
||||
# --------------------------------------------------
|
||||
# Internal helpers
|
||||
# --------------------------------------------------
|
||||
|
||||
def _to_nepali_date(value):
|
||||
"""
|
||||
Normalize AD date/datetime or BS date to nepalidate.
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
if isinstance(value, nepalidate):
|
||||
return value
|
||||
|
||||
if isinstance(value, (datetime.date, datetime.datetime)):
|
||||
return nepalidate.from_date(value)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# Date filters
|
||||
# --------------------------------------------------
|
||||
|
||||
@register.filter(is_safe=True)
|
||||
def nepali_date(value, format_str="%Y-%m-%d"):
|
||||
"""
|
||||
Formats a date or datetime object into a Nepali date string.
|
||||
Format a date/datetime as Nepali (BS) date.
|
||||
"""
|
||||
if value is None:
|
||||
nd = _to_nepali_date(value)
|
||||
if not nd:
|
||||
return ""
|
||||
|
||||
if isinstance(value, (datetime.datetime, datetime.date)):
|
||||
value = nepalidate.from_date(value)
|
||||
|
||||
if hasattr(value, "strftime"):
|
||||
return value.strftime(format_str)
|
||||
return value
|
||||
return nd.strftime(format_str)
|
||||
|
||||
|
||||
@register.filter
|
||||
@register.filter(is_safe=True)
|
||||
def nepali_date_ne(value, format_str="%Y-%m-%d"):
|
||||
"""
|
||||
Formats a date or datetime object into a Nepali date string (Devanagari).
|
||||
Format a date/datetime as Nepali (BS) date in Devanagari.
|
||||
"""
|
||||
if value is None:
|
||||
nd = _to_nepali_date(value)
|
||||
if not nd:
|
||||
return ""
|
||||
|
||||
if isinstance(value, (datetime.datetime, datetime.date)):
|
||||
value = nepalidate.from_date(value)
|
||||
|
||||
if hasattr(value, "strftime_ne"):
|
||||
return value.strftime_ne(format_str)
|
||||
return value
|
||||
return nd.strftime_ne(format_str)
|
||||
|
||||
|
||||
@register.filter
|
||||
# --------------------------------------------------
|
||||
# Number filter
|
||||
# --------------------------------------------------
|
||||
|
||||
@register.filter(is_safe=True)
|
||||
def nepali_number(value):
|
||||
"""
|
||||
Converts a number to Devanagari.
|
||||
Convert a number to Nepali (Devanagari) digits.
|
||||
"""
|
||||
if value is None:
|
||||
if value in (None, ""):
|
||||
return ""
|
||||
return nepalinumber(value).str_ne()
|
||||
|
||||
try:
|
||||
return nepalinumber(value).str_ne()
|
||||
except Exception:
|
||||
return value
|
||||
|
||||
|
||||
@register.filter
|
||||
# --------------------------------------------------
|
||||
# Humanize filter
|
||||
# --------------------------------------------------
|
||||
|
||||
@register.filter(is_safe=True)
|
||||
def nepali_humanize(value, threshold=None, format_str=None):
|
||||
"""
|
||||
Returns a human-readable "time ago" string in Nepali.
|
||||
Human-readable Nepali time difference.
|
||||
"""
|
||||
if value is None:
|
||||
return ""
|
||||
|
||||
# Threshold and format_str are optional
|
||||
kwargs = {}
|
||||
if threshold:
|
||||
if threshold is not None:
|
||||
kwargs["threshold"] = threshold
|
||||
if format_str:
|
||||
if format_str is not None:
|
||||
kwargs["format"] = format_str
|
||||
|
||||
return nepalihumanize(value, **kwargs)
|
||||
try:
|
||||
return nepalihumanize(value, **kwargs)
|
||||
except Exception:
|
||||
return value
|
||||
+62
-17
@@ -2,39 +2,82 @@ from django import forms
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
|
||||
class ChainedSelectWidget(forms.Select):
|
||||
# --------------------------------------------------
|
||||
# Utility mixin
|
||||
# --------------------------------------------------
|
||||
|
||||
class ClassAttrMixin:
|
||||
"""
|
||||
Safely append CSS classes to widget attrs.
|
||||
"""
|
||||
|
||||
css_class = ""
|
||||
|
||||
def _build_attrs(self, attrs):
|
||||
attrs = attrs or {}
|
||||
classes = attrs.get("class", "").split()
|
||||
if self.css_class:
|
||||
classes.append(self.css_class)
|
||||
attrs["class"] = " ".join(dict.fromkeys(classes))
|
||||
return attrs
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# Chained Select Base
|
||||
# --------------------------------------------------
|
||||
|
||||
class ChainedSelectWidget(ClassAttrMixin, forms.Select):
|
||||
"""
|
||||
Base widget for chained location selects.
|
||||
"""
|
||||
|
||||
class Media:
|
||||
js = ("django_nepkit/js/address-chaining.js",)
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# Province / District / Municipality Widgets
|
||||
# --------------------------------------------------
|
||||
|
||||
class ProvinceSelectWidget(ChainedSelectWidget):
|
||||
css_class = "nepkit-province-select"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
attrs = kwargs.get("attrs", {})
|
||||
attrs["class"] = attrs.get("class", "") + " nepkit-province-select"
|
||||
kwargs["attrs"] = attrs
|
||||
kwargs["attrs"] = self._build_attrs(kwargs.get("attrs"))
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class DistrictSelectWidget(ChainedSelectWidget):
|
||||
css_class = "nepkit-district-select"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
attrs = kwargs.get("attrs", {})
|
||||
attrs["class"] = attrs.get("class", "") + " nepkit-district-select"
|
||||
attrs["data-url"] = reverse_lazy("django_nepkit:district-list")
|
||||
attrs = self._build_attrs(kwargs.get("attrs"))
|
||||
attrs.setdefault("data-url", reverse_lazy("django_nepkit:district-list"))
|
||||
kwargs["attrs"] = attrs
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class MunicipalitySelectWidget(ChainedSelectWidget):
|
||||
css_class = "nepkit-municipality-select"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
attrs = kwargs.get("attrs", {})
|
||||
attrs["class"] = attrs.get("class", "") + " nepkit-municipality-select"
|
||||
attrs["data-url"] = reverse_lazy("django_nepkit:municipality-list")
|
||||
attrs = self._build_attrs(kwargs.get("attrs"))
|
||||
attrs.setdefault("data-url", reverse_lazy("django_nepkit:municipality-list"))
|
||||
kwargs["attrs"] = attrs
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class NepaliDatePickerWidget(forms.TextInput):
|
||||
# --------------------------------------------------
|
||||
# Nepali Date Picker
|
||||
# --------------------------------------------------
|
||||
|
||||
class NepaliDatePickerWidget(ClassAttrMixin, forms.TextInput):
|
||||
"""
|
||||
Nepali BS date picker widget.
|
||||
"""
|
||||
|
||||
input_type = "text"
|
||||
css_class = "nepkit-datepicker"
|
||||
|
||||
class Media:
|
||||
css = {
|
||||
@@ -50,12 +93,14 @@ class NepaliDatePickerWidget(forms.TextInput):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
attrs = kwargs.get("attrs", {})
|
||||
classes = attrs.get("class", "")
|
||||
if "vDateField" in classes:
|
||||
classes = classes.replace("vDateField", "")
|
||||
|
||||
attrs["class"] = (classes + " nepkit-datepicker").strip()
|
||||
attrs["autocomplete"] = "off"
|
||||
attrs["placeholder"] = "YYYY-MM-DD"
|
||||
# Remove Django admin default class
|
||||
classes = attrs.get("class", "").replace("vDateField", "").strip()
|
||||
attrs["class"] = classes
|
||||
|
||||
attrs = self._build_attrs(attrs)
|
||||
attrs.setdefault("autocomplete", "off")
|
||||
attrs.setdefault("placeholder", "YYYY-MM-DD")
|
||||
|
||||
kwargs["attrs"] = attrs
|
||||
super().__init__(*args, **kwargs)
|
||||
Reference in new issue
Block a user