Implemented a comprehensive update to handle Nepali (Bikram Sambat) localization more robustly across models, forms, admin, and API serializers. This includes native support for Devanagari script output and a complete documentation overhaul. Models & Fields: - Added [NepaliFieldMixin](cci:2://file:///home/sankalp/Projects/prod-django-nepkit/django_nepkit/models.py:15:0-36:39) to support [ne](cci:1://file:///home/sankalp/Projects/prod-django-nepkit/django_nepkit/admin.py:146:4-163:67) (Nepali) and [en](cci:1://file:///home/sankalp/Projects/prod-django-nepkit/django_nepkit/widgets.py:4:0-10:16) (English) toggles for all custom fields. - Updated [NepaliDateField](cci:2://file:///home/sankalp/Projects/prod-django-nepkit/django_nepkit/models.py:48:0-134:44), [NepaliDateTimeField](cci:2://file:///home/sankalp/Projects/prod-django-nepkit/django_nepkit/models.py:144:0-217:44), and [NepaliTimeField](cci:2://file:///home/sankalp/Projects/prod-django-nepkit/django_nepkit/models.py:137:0-141:34) with the new mixin and improved deconstruction. - Enhanced address fields ([ProvinceField](cci:2://file:///home/sankalp/Projects/prod-django-nepkit/django_nepkit/models.py:220:0-262:23), [DistrictField](cci:2://file:///home/sankalp/Projects/prod-django-nepkit/django_nepkit/models.py:265:0-286:66), [MunicipalityField](cci:2://file:///home/sankalp/Projects/prod-django-nepkit/django_nepkit/models.py:289:0-310:66)) to support bilingual choices. Admin Enhancements: - Refactored date formatting into a common utility supporting Devanagari via `strftime_ne`. - Updated [NepaliModelAdmin](cci:2://file:///home/sankalp/Projects/prod-django-nepkit/django_nepkit/admin.py:186:0-294:9) and [NepaliAdminMixin](cci:2://file:///home/sankalp/Projects/prod-django-nepkit/django_nepkit/admin.py:118:0-183:75) to auto-detect [ne](cci:1://file:///home/sankalp/Projects/prod-django-nepkit/django_nepkit/admin.py:146:4-163:67) settings from model fields for [list_display](cci:1://file:///home/sankalp/Projects/prod-django-nepkit/django_nepkit/admin.py:230:4-250:21). - Improved widget injection in the admin to respect field-level localization settings. - Added a JS bridge for admin jQuery compatibility with the Nepali datepicker. Forms & Widgets: - Introduced [NepaliWidgetMixin](cci:2://file:///home/sankalp/Projects/prod-django-nepkit/django_nepkit/widgets.py:13:0-38:12) to handle localization data attributes in the frontend. - Updated [NepaliDatePickerWidget](cci:2://file:///home/sankalp/Projects/prod-django-nepkit/django_nepkit/widgets.py:63:0-99:42) with improved asset loading and Devanagari support. - Fixed [NepaliDateFormField](cci:2://file:///home/sankalp/Projects/prod-django-nepkit/django_nepkit/forms.py:10:0-41:13) to correctly handle `empty_value` passed by the Django admin. DRF (Serializers): - Added [BaseNepaliBSField](cci:2://file:///home/sankalp/Projects/prod-django-nepkit/django_nepkit/serializers.py:21:0-106:48) to DRF serializers with support for `ne=True` for Devanagari output. - Implemented specialized [NepaliDateSerializerField](cci:2://file:///home/sankalp/Projects/prod-django-nepkit/django_nepkit/serializers.py:114:0-122:28) and [NepaliDateTimeSerializerField](cci:2://file:///home/sankalp/Projects/prod-django-nepkit/django_nepkit/serializers.py:130:0-138:32). Documentation: - Completely revamped [README.md](cci:7://file:///home/sankalp/Projects/prod-django-nepkit/README.md:0:0-0:0) with professional design, badges, and detailed usage guides. - Added explicit instructions for address chaining URLs and [NepaliAdminMixin](cci:2://file:///home/sankalp/Projects/prod-django-nepkit/django_nepkit/admin.py:118:0-183:75) usage. Misc: - Optimized address chaining JS logic. - Updated example demo app models and migrations to reflect new field capabilities.
306 lines
11 KiB
Python
306 lines
11 KiB
Python
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, NepaliDateTimeField
|
|
from django_nepkit.utils import (
|
|
try_parse_nepali_date,
|
|
try_parse_nepali_datetime,
|
|
)
|
|
|
|
|
|
def _format_nepali_common(value, try_parse_func, format_string, ne, cls_type):
|
|
"""Internal helper for formatting Nepali objects."""
|
|
if value is None:
|
|
return ""
|
|
|
|
try:
|
|
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):
|
|
pass
|
|
|
|
return str(value) if value else ""
|
|
|
|
|
|
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="%B %d, %Y %I:%M %p", ne=False
|
|
):
|
|
"""
|
|
Format a nepalidatetime object with Nepali month names.
|
|
"""
|
|
return _format_nepali_common(
|
|
datetime_value, try_parse_nepali_datetime, format_string, ne, nepalidatetime
|
|
)
|
|
|
|
|
|
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
|
|
|
|
|
|
# Register NepaliDateFilter as the default list filter for NepaliDateField
|
|
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 _get_field_ne_setting(self, field_name):
|
|
"""
|
|
Get the 'ne' setting from a model field.
|
|
|
|
Args:
|
|
field_name: Name of the field in the model
|
|
|
|
Returns:
|
|
True if field has ne=True, False otherwise
|
|
"""
|
|
if not hasattr(self, "model"):
|
|
return False
|
|
|
|
try:
|
|
field = self.model._meta.get_field(field_name)
|
|
if hasattr(field, "ne"):
|
|
return field.ne
|
|
except (AttributeError, LookupError):
|
|
pass
|
|
|
|
return False
|
|
|
|
def format_nepali_date(
|
|
self, date_value, format_string="%B %d, %Y", ne=None, field_name=None
|
|
):
|
|
"""
|
|
Format a nepalidate object with Nepali month names.
|
|
Available as a method on admin classes using this mixin.
|
|
|
|
Args:
|
|
date_value: A nepalidate object or string
|
|
format_string: strftime format string
|
|
ne: If True, format using Devanagari script. If None, auto-detect from field (default: None)
|
|
field_name: Name of the field to auto-detect 'ne' setting from (optional)
|
|
"""
|
|
# Auto-detect 'ne' from field if not explicitly provided
|
|
if ne is None and field_name:
|
|
ne = self._get_field_ne_setting(field_name)
|
|
elif ne is None:
|
|
ne = False
|
|
|
|
return format_nepali_date(date_value, format_string, ne=ne)
|
|
|
|
def format_nepali_datetime(
|
|
self,
|
|
datetime_value,
|
|
format_string="%B %d, %Y %I:%M %p",
|
|
ne=None,
|
|
field_name=None,
|
|
):
|
|
"""
|
|
Format a nepalidatetime object with Nepali month names.
|
|
|
|
Args:
|
|
datetime_value: A nepalidatetime object or string
|
|
format_string: strftime format string
|
|
ne: If True, format using Devanagari script. If None, auto-detect from field (default: None)
|
|
field_name: Name of the field to auto-detect 'ne' setting from (optional)
|
|
"""
|
|
# Auto-detect 'ne' from field if not explicitly provided
|
|
if ne is None and field_name:
|
|
ne = self._get_field_ne_setting(field_name)
|
|
elif ne is None:
|
|
ne = False
|
|
|
|
return format_nepali_datetime(datetime_value, format_string, ne=ne)
|
|
|
|
|
|
class NepaliModelAdmin(NepaliAdminMixin, admin.ModelAdmin):
|
|
"""
|
|
Base ModelAdmin class with Nepali date utilities built-in.
|
|
NepaliDateField and NepaliDateTimeField in list_display are automatically
|
|
formatted (using the field's ne setting). No manual display methods needed.
|
|
|
|
Example:
|
|
from django_nepkit import NepaliModelAdmin, NepaliDateFilter
|
|
|
|
@admin.register(MyModel)
|
|
class MyModelAdmin(NepaliModelAdmin):
|
|
list_display = ("name", "birth_date", "created_at") # auto-formatted
|
|
list_filter = (("birth_date", NepaliDateFilter),)
|
|
"""
|
|
|
|
# Make NepaliDateFilter available as a class attribute
|
|
NepaliDateFilter = NepaliDateFilter
|
|
|
|
def _make_nepali_display(self, field_name, formatter_method):
|
|
"""Generic helper to return a callable that formats a Nepali field for list_display."""
|
|
admin_instance = self
|
|
try:
|
|
field = self.model._meta.get_field(field_name)
|
|
short_description = getattr(
|
|
field, "verbose_name", field_name.replace("_", " ").title()
|
|
)
|
|
except Exception:
|
|
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()
|
|
# Call the passed formatter method (bound to self)
|
|
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 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)
|
|
if isinstance(field, NepaliDateField):
|
|
result.append(self._make_nepali_date_display(item))
|
|
continue
|
|
if isinstance(field, NepaliDateTimeField):
|
|
result.append(self._make_nepali_datetime_display(item))
|
|
continue
|
|
except Exception:
|
|
pass
|
|
result.append(item)
|
|
return result
|
|
|
|
# Ensure admin forms render Nepali fields with the proper widget,
|
|
# even if a project doesn't provide custom ModelForms.
|
|
def formfield_for_dbfield(self, db_field, request, **kwargs):
|
|
"""
|
|
Force Nepali widgets in Django admin without requiring user forms.
|
|
"""
|
|
try:
|
|
from django_nepkit.models import NepaliDateField, NepaliDateTimeField
|
|
from django_nepkit.widgets import NepaliDatePickerWidget
|
|
except Exception:
|
|
return super().formfield_for_dbfield(db_field, request, **kwargs)
|
|
|
|
if isinstance(db_field, (NepaliDateField, NepaliDateTimeField)):
|
|
# Pass ne/en parameters from field to widget if they exist
|
|
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))
|
|
|
|
return super().formfield_for_dbfield(db_field, request, **kwargs)
|
|
|
|
class Media:
|
|
"""
|
|
Django admin ships jQuery as `django.jQuery` (not `window.jQuery`).
|
|
The Nepali date picker library expects a global `jQuery`, so we bridge it.
|
|
"""
|
|
|
|
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 = (
|
|
# Bridge admin's `django.jQuery` -> `window.jQuery`
|
|
"django_nepkit/js/admin-jquery-bridge.js",
|
|
# Date picker lib
|
|
"https://nepalidatepicker.sajanmaharjan.com.np/v5/nepali.datepicker/js/nepali.datepicker.v5.0.6.min.js",
|
|
# Init
|
|
"django_nepkit/js/nepali-datepicker-init.js",
|
|
)
|
|
|
|
|
|
# Exporting for easy usage
|
|
__all__ = [
|
|
"NepaliDateFilter",
|
|
"format_nepali_date",
|
|
"format_nepali_datetime",
|
|
"NepaliAdminMixin",
|
|
"NepaliModelAdmin",
|
|
]
|