Files
sankalp 37cd71f896 feat(django_nepkit): improve nil handling for ne flag in admin mixin
Clarify that None means “no opinion” versus False, and adjust fallback logic to use the project default when a field lacks the ne attribute.
2026-06-06 21:35:29 +05:45

386 lines
14 KiB
Python

import logging
from django.contrib import admin
from django.core.exceptions import FieldDoesNotExist
from django.utils.translation import gettext_lazy as _
from nepali.datetime import nepalidate, nepalidatetime
from nepali.number import english_to_nepali
from django_nepkit.conf import nepkit_settings
from django_nepkit.models import (
NepaliCurrencyField,
NepaliDateField,
NepaliDateTimeField,
NepaliTimeField,
)
from django_nepkit.utils import (
format_nepali_currency,
format_nepali_time,
try_parse_nepali_date,
try_parse_nepali_datetime,
)
logger = logging.getLogger(__name__)
def _format_nepali_common(value, try_parse_func, format_string, ne, cls_type):
"""Format a Nepali date/time for admin display, falling back gracefully."""
if value is None:
return ""
try:
if isinstance(value, str):
parsed = try_parse_func(value)
if parsed is not None:
if ne and hasattr(parsed, "strftime_ne"):
return parsed.strftime_ne(format_string)
return parsed.strftime(format_string)
if isinstance(value, cls_type):
if ne and hasattr(value, "strftime_ne"):
return value.strftime_ne(format_string)
return value.strftime(format_string)
except (ValueError, TypeError, AttributeError) as exc:
logger.warning("nepkit admin formatter: cannot format %r: %s", value, exc)
return ""
def format_nepali_date(date_value, format_string="%B %d, %Y", ne=False):
"""Format a nepalidate object with Nepali month names."""
return _format_nepali_common(
date_value, try_parse_nepali_date, format_string, ne, nepalidate
)
def format_nepali_datetime(datetime_value, format_string=None, ne=False):
"""Format a nepalidatetime object with Nepali month names."""
if format_string is None:
if nepkit_settings.TIME_FORMAT == 24:
format_string = "%B %d, %Y %H:%M"
else:
format_string = "%B %d, %Y %I:%M %p"
return _format_nepali_common(
datetime_value, try_parse_nepali_datetime, format_string, ne, nepalidatetime
)
def format_nepali_time_admin(time_value, format_string=None, ne=False):
"""Format a ``time``/``datetime`` for the admin with Devanagari support."""
if format_string is None:
format_string = nepkit_settings.BS_TIME_FORMAT
formatted = format_nepali_time(time_value, format_string)
if ne and formatted:
formatted = english_to_nepali(formatted)
return formatted
# ---------------------------------------------------------------------------
# Admin list filters
# ---------------------------------------------------------------------------
def _separator_from_format(fmt):
"""Pick the first non-% character from a strftime format string.
Returns the separator for ``%Y/%m/%d``-style formats, falling back to
``"-"`` when the format has no separator (e.g. ``%Y%m%d``).
"""
if not fmt or len(fmt) < 3:
return "-"
return fmt[2] if not fmt[2].startswith("%") else "-"
class BaseNepaliDateFilter(admin.FieldListFilter):
"""Base class for date filters (Year/Month)."""
def __init__(self, field, request, params, model, model_admin, field_path):
self.parameter_name = f"{field_path}_{self.suffix}"
super().__init__(field, request, params, model, model_admin, field_path)
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"),
}
for value, display in self.get_filter_options():
yield {
"selected": self.used_parameters.get(self.parameter_name) == str(value),
"query_string": changelist.get_query_string(
{self.parameter_name: str(value)}
),
"display": display,
}
def queryset(self, request, queryset):
value = self.used_parameters.get(self.parameter_name)
if value:
return self.apply_filter(queryset, value)
return queryset
def get_filter_options(self):
raise NotImplementedError
def apply_filter(self, queryset, value):
raise NotImplementedError
class NepaliDateFilter(BaseNepaliDateFilter):
"""Filter by Nepali Year (e.g. 2080)."""
suffix = "bs_year"
title = _("Nepali Date (Year)")
def get_filter_options(self):
current_year = nepalidate.today().year
return [(y, str(y)) for y in range(current_year - 10, current_year + 2)]
def apply_filter(self, queryset, value):
fmt = nepkit_settings.BS_DATE_FORMAT
if fmt.startswith("%Y"):
separator = _separator_from_format(fmt)
return queryset.filter(
**{f"{self.field_path}__startswith": f"{value}{separator}"}
)
return queryset.filter(**{f"{self.field_path}__icontains": f"{value}"})
class NepaliMonthFilter(BaseNepaliDateFilter):
"""Filter by Nepali Month (e.g. Baisakh)."""
suffix = "bs_month"
title = _("Nepali Date (Month)")
def get_filter_options(self):
ne = nepkit_settings.DEFAULT_LANGUAGE == "ne"
names = [
("बैशाख", "Baisakh"),
("जेठ", "Jestha"),
("असार", "Ashad"),
("साउन", "Shrawan"),
("भदौ", "Bhadra"),
("असोज", "Ashwin"),
("कात्तिक", "Kartik"),
("मंसिर", "Mangsir"),
("पुष", "Poush"),
("माघ", "Magh"),
("फागुन", "Falgun"),
("चैत", "Chaitra"),
]
return [(f"{i:02d}", n[0] if ne else n[1]) for i, n in enumerate(names, 1)]
def apply_filter(self, queryset, value):
fmt = nepkit_settings.BS_DATE_FORMAT
if fmt == "%Y-%m-%d":
return queryset.filter(**{f"{self.field_path}__contains": f"-{value}-"})
separator = _separator_from_format(fmt)
return queryset.filter(
**{f"{self.field_path}__contains": f"{separator}{value}{separator}"}
)
# Register the year filter as the default for NepaliDateField.
admin.FieldListFilter.register(
lambda f: isinstance(f, NepaliDateField),
NepaliDateFilter,
take_priority=True,
)
# ---------------------------------------------------------------------------
# ModelAdmin mixin
# ---------------------------------------------------------------------------
class NepaliAdminMixin:
"""Provides date formatting helpers for Admin classes."""
def _get_field_ne_setting(self, field_name):
"""Return the field's ``ne`` flag, or ``None`` if the field has none.
``None`` distinguishes "field has no opinion" from an explicit
``ne=False`` (English), which the caller can use to fall back to
the project default without overwriting a field-level override.
"""
if not hasattr(self, "model"):
return None
try:
field = self.model._meta.get_field(field_name)
if hasattr(field, "ne"):
return field.ne
except (FieldDoesNotExist, AttributeError):
return None
return None
def format_nepali_date(
self, date_value, format_string="%B %d, %Y", ne=None, field_name=None
):
if ne is None and field_name:
ne = self._get_field_ne_setting(field_name)
if ne is None:
ne = nepkit_settings.DEFAULT_LANGUAGE == "ne"
return format_nepali_date(date_value, format_string, ne=ne)
def format_nepali_datetime(
self,
datetime_value,
format_string=None,
ne=None,
field_name=None,
):
if ne is None and field_name:
ne = self._get_field_ne_setting(field_name)
if ne is None:
ne = nepkit_settings.DEFAULT_LANGUAGE == "ne"
return format_nepali_datetime(datetime_value, format_string, ne=ne)
def format_nepali_currency(self, value, currency_symbol="Rs.", ne=False, **kwargs):
return format_nepali_currency(value, currency_symbol=currency_symbol, ne=ne)
class NepaliModelAdmin(NepaliAdminMixin, admin.ModelAdmin):
"""
Standard Admin class that automatically formats Nepali dates in lists.
Example::
@admin.register(MyModel)
class MyModelAdmin(NepaliModelAdmin):
list_display = ("name", "birth_date", "created_at")
list_filter = (("birth_date", NepaliDateFilter),)
"""
NepaliDateFilter = NepaliDateFilter
NepaliMonthFilter = NepaliMonthFilter
def _make_nepali_display(self, field_name, formatter_method):
admin_instance = self
try:
field = self.model._meta.get_field(field_name)
short_description = getattr(
field, "verbose_name", field_name.replace("_", " ").title()
)
except (FieldDoesNotExist, AttributeError):
short_description = field_name.replace("_", " ").title()
def display(obj):
val = getattr(obj, field_name, None)
if val is None:
return admin_instance.get_empty_value_display()
return formatter_method(val, field_name=field_name)
display.short_description = short_description
display.admin_order_field = field_name
return display
def _make_nepali_date_display(self, field_name):
return self._make_nepali_display(field_name, self.format_nepali_date)
def _make_nepali_datetime_display(self, field_name):
return self._make_nepali_display(field_name, self.format_nepali_datetime)
def _make_nepali_currency_display(self, field_name):
return self._make_nepali_display(field_name, self.format_nepali_currency)
def get_list_display(self, request):
list_display = super().get_list_display(request)
result = []
for item in list_display:
if not isinstance(item, str):
result.append(item)
continue
try:
field = self.model._meta.get_field(item)
except (FieldDoesNotExist, AttributeError):
result.append(item)
continue
if isinstance(field, NepaliDateField):
result.append(self._make_nepali_date_display(item))
elif isinstance(field, NepaliDateTimeField):
result.append(self._make_nepali_datetime_display(item))
elif isinstance(field, NepaliTimeField):
result.append(self._make_nepali_time_display(item))
elif isinstance(field, NepaliCurrencyField):
result.append(self._make_nepali_currency_display(item))
else:
result.append(item)
return result
def _make_nepali_time_display(self, field_name):
def display(obj):
val = getattr(obj, field_name, None)
if val is None:
return self.get_empty_value_display()
ne = self._get_field_ne_setting(field_name)
if ne is None:
# Field has no ``ne`` attribute (e.g. plain ``TimeField``)
# — fall back to the project default.
ne = nepkit_settings.DEFAULT_LANGUAGE == "ne"
return format_nepali_time_admin(val, ne=ne)
try:
field = self.model._meta.get_field(field_name)
display.short_description = getattr(
field, "verbose_name", field_name.replace("_", " ").title()
)
except (FieldDoesNotExist, AttributeError):
display.short_description = field_name.replace("_", " ").title()
display.admin_order_field = field_name
return display
def formfield_for_dbfield(self, db_field, request, **kwargs):
"""Auto-wire NepaliDatePicker / NepaliTimeWidget in the admin form."""
from django_nepkit.widgets import NepaliDatePickerWidget, NepaliTimeWidget
if (
isinstance(db_field, (NepaliDateField, NepaliDateTimeField))
and nepkit_settings.ADMIN_DATEPICKER
):
widget_kwargs = {}
if hasattr(db_field, "ne"):
widget_kwargs["ne"] = db_field.ne
if hasattr(db_field, "en"):
widget_kwargs["en"] = db_field.en
kwargs.setdefault("widget", NepaliDatePickerWidget(**widget_kwargs))
if isinstance(db_field, NepaliTimeField):
widget_kwargs = {}
if hasattr(db_field, "ne"):
widget_kwargs["ne"] = db_field.ne
if hasattr(db_field, "en"):
widget_kwargs["en"] = db_field.en
if hasattr(db_field, "htmx"):
widget_kwargs["htmx"] = db_field.htmx
kwargs.setdefault("widget", NepaliTimeWidget(**widget_kwargs))
return super().formfield_for_dbfield(db_field, request, **kwargs)
class Media:
"""Loads the Nepali Datepicker and bridging scripts."""
css = {
"all": (
"https://nepalidatepicker.sajanmaharjan.com.np/v5/nepali.datepicker/css/nepali.datepicker.v5.0.6.min.css",
"django_nepkit/css/admin-nepali-datepicker.css",
)
}
js = (
"django_nepkit/js/admin-jquery-bridge.js",
"https://nepalidatepicker.sajanmaharjan.com.np/v5/nepali.datepicker/js/nepali.datepicker.v5.0.6.min.js",
"django_nepkit/js/nepali-datepicker-init.js",
"django_nepkit/js/nepal-data.js",
"django_nepkit/js/address-chaining.js",
)
__all__ = [
"NepaliDateFilter",
"NepaliMonthFilter",
"format_nepali_date",
"format_nepali_datetime",
"NepaliAdminMixin",
"NepaliModelAdmin",
]