- Implement server-side location chaining with HTMX support via `htmx=True` in fields and widgets. - Update location views to handle HTMX requests and return HTML <option> fragments. - Add NepaliDateYearFilter and NepaliDateRangeFilter for robust DRF API filtering. - Expand default `DATE_INPUT_FORMATS` in configuration to support multiple common Nepali date styles. - Update DRF serializers to automatically respect global `NEPKIT` configuration settings. - Add `django-filter` as an optional dependency under the `drf` extra in pyproject.toml. - Correct underlying dependency references from `nepali-datetime` to nepali library. - Complete overhaul of README.md for premium DX, including new sections for storage behavior, timezone awareness, and API usage.
313 lines
11 KiB
Python
313 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,
|
|
)
|
|
from django_nepkit.conf import nepkit_settings
|
|
|
|
|
|
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=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
|
|
)
|
|
|
|
|
|
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 or global settings (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 = 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,
|
|
):
|
|
"""
|
|
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 or global settings (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 = nepkit_settings.DEFAULT_LANGUAGE == "ne"
|
|
|
|
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)) and nepkit_settings.ADMIN_DATEPICKER:
|
|
# 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",
|
|
]
|