diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/Untitled b/Untitled new file mode 100644 index 0000000..0036a18 --- /dev/null +++ b/Untitled @@ -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 diff --git a/django_nepkit/admin.py b/django_nepkit/admin.py index 0036a18..6997967 100644 --- a/django_nepkit/admin.py +++ b/django_nepkit/admin.py @@ -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 diff --git a/django_nepkit/forms.py b/django_nepkit/forms.py index 5cf206b..2fc0639 100644 --- a/django_nepkit/forms.py +++ b/django_nepkit/forms.py @@ -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") diff --git a/django_nepkit/models.py b/django_nepkit/models.py index a37c075..51369a3 100644 --- a/django_nepkit/models.py +++ b/django_nepkit/models.py @@ -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) diff --git a/django_nepkit/serializers.py b/django_nepkit/serializers.py new file mode 100644 index 0000000..ea332a2 --- /dev/null +++ b/django_nepkit/serializers.py @@ -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 diff --git a/django_nepkit/templatetag/nepali.py b/django_nepkit/templatetag/nepali.py index 10f804f..bcab9b3 100644 --- a/django_nepkit/templatetag/nepali.py +++ b/django_nepkit/templatetag/nepali.py @@ -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 diff --git a/django_nepkit/widgets.py b/django_nepkit/widgets.py index 477c5da..feafe77 100644 --- a/django_nepkit/widgets.py +++ b/django_nepkit/widgets.py @@ -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)