diff --git a/django_nepkit/models.py b/django_nepkit/models.py index cf63b2c..2259bcf 100644 --- a/django_nepkit/models.py +++ b/django_nepkit/models.py @@ -10,6 +10,8 @@ from nepali.datetime import nepalidate, nepalidatetime from nepali.locations import districts, municipalities, provinces from django_nepkit.utils import ( + _cached_parse_nepali_date, + _cached_parse_nepali_datetime, try_parse_nepali_date, try_parse_nepali_datetime, try_parse_nepali_time, @@ -146,10 +148,23 @@ class BaseNepaliBSField(NepaliFieldMixin, models.CharField): return parsed def _parse_str(self, value): - """Parse a stored string into a Nepali object (or ``None``).""" - if isinstance(value, str): - return self.parse_func(value) - return None + """Parse a stored string into a Nepali object (or ``None``). + + Routes through a per-string ``lru_cache`` so admin list views + that load thousands of rows don't re-parse the same stored date + every time. The cache is shared across all field instances + (the parse result for a given string is the same regardless of + which model it's bound to). + """ + if not isinstance(value, str): + return None + if self.nepali_cls is nepalidate: + return _cached_parse_nepali_date(value) + if self.nepali_cls is nepalidatetime: + return _cached_parse_nepali_datetime(value) + # Subclasses with a different ``nepali_cls`` fall back to the + # uncached ``parse_func``. + return self.parse_func(value) def to_python(self, value): if value is None or isinstance(value, self.nepali_cls): diff --git a/django_nepkit/utils.py b/django_nepkit/utils.py index 7bb1aba..a8585f2 100644 --- a/django_nepkit/utils.py +++ b/django_nepkit/utils.py @@ -101,6 +101,22 @@ def try_parse_nepali_datetime(value: Any) -> Optional[nepalidatetime]: return _try_parse_nepali(value, nepalidatetime, nepkit_settings.BS_DATETIME_FORMAT) +# Per-string parse caches used by ``BaseNepaliBSField`` so admin list +# views don't re-parse the same stored string thousands of times per +# request. The cache key is the raw stored string; the value is the +# parsed object (``None`` when the string is unparseable). Capping at +# 2048 covers the worst case (all unique dates in a 5-year window of +# ~30k rows) without unbounded growth. +@lru_cache(maxsize=2048) +def _cached_parse_nepali_date(value: str) -> Optional[nepalidate]: + return try_parse_nepali_date(value) + + +@lru_cache(maxsize=2048) +def _cached_parse_nepali_datetime(value: str) -> Optional[nepalidatetime]: + return try_parse_nepali_datetime(value) + + def try_parse_nepali_time(value: Any) -> Optional[python_time]: """Convert any value to a Python ``time`` (``None`` on failure).""" from datetime import datetime as _dt diff --git a/django_nepkit/views.py b/django_nepkit/views.py index e22e8c8..7382f77 100644 --- a/django_nepkit/views.py +++ b/django_nepkit/views.py @@ -1,7 +1,7 @@ import logging from django.http import HttpResponse, JsonResponse -from django.utils.html import format_html, format_html_join +from django.utils.html import format_html from django_nepkit.conf import nepkit_settings from django_nepkit.utils import ( @@ -37,18 +37,22 @@ def _coerce_bool(raw): def _render_options(data, placeholder): """Render a list of location options as HTML. - All values are HTML-escaped so that user-supplied data injected via - data import can't smuggle markup into the rendered page. + Each ``