feat(django_nepkit): cache parsed Nepali dates/times in admin list views and improve HTML escaping security

Cache parsed values with LRU and use per‑option format_html to prevent XSS attacks.
This commit is contained in:
sankalp committed 2026-06-06 21:51:51 +05:45
1 parent 8a56be0900
commit 589fe8c104
3 files changed
+51 -16

No files matched your search

+19 -4
View File
@@ -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):
+16
View File
@@ -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
+16 -12
View File
@@ -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 ``<option>`` is built with an explicit ``format_html`` call so
that the attribute (``value="..."``) and the text node are
individually escaped — a user-supplied data row that contains a
``"><script>`` payload can never smuggle markup into the rendered
page, regardless of which slot it ends up in. ``format_html_join``
is intentionally avoided: the per-option ``format_html`` makes the
escaping contract obvious at the call site and avoids the
placeholder/value/template-coupling that ``format_html_join``
forces when the template changes.
"""
rendered_options = format_html_join(
"\n",
'<option value="{}">{}</option>',
((item.get("id", ""), item.get("text", "")) for item in data),
)
html = format_html(
'<option value="">{0}</option>\n{1}', placeholder, rendered_options
)
return HttpResponse(str(html), content_type="text/html")
parts: list[str] = [format_html('<option value="">{0}</option>', placeholder or "")]
for item in data or []:
opt_id = (item.get("id") if isinstance(item, dict) else "") or ""
opt_text = (item.get("text") if isinstance(item, dict) else "") or ""
parts.append(format_html('<option value="{0}">{1}</option>', opt_id, opt_text))
return HttpResponse("\n".join(parts), content_type="text/html")
def _get_primary_param(request, param_name, exclude_params=None):