Files
sankalp 0268bcfe98 chore(root): remove DRF and django-filter dependencies
drop DRF and django-filter to make django_nepkit framework‑agnostic, simplify dependencies, and align with v0.2.2 breaking change.
2026-06-06 21:06:03 +05:45

461 lines
14 KiB
Python

"""
Plain Django views for the demo.
The form-based views (``person_list`` / ``person_create`` / ``transaction_create``
/ ``address_normalize_demo``) use Django's built-in machinery. The JSON
endpoints under ``/api/`` use the framework-agnostic helpers in
``django_nepkit.api`` so this demo doesn't need DRF or django-filter.
"""
from __future__ import annotations
from decimal import Decimal, InvalidOperation
from typing import Any
from django import forms
from django.core.paginator import Paginator
from django.db.models import Q
from django.http import HttpRequest, JsonResponse
from django.shortcuts import redirect, render
from django.utils.translation import gettext as _
from django.views.decorators.http import require_GET
from django_nepkit import api as nepkit_api
from django_nepkit.conf import nepkit_settings
from django_nepkit.utils import normalize_address
from .models import AuditedPerson, Citizen, Person, Transaction
_TRUE_VALUES = {"true", "1", "yes", "on"}
# --------------------------------------------------------------------------- #
# Form-based views
# --------------------------------------------------------------------------- #
class PersonForm(forms.ModelForm):
class Meta:
model = Person
fields = [
"name",
"birth_date",
"birth_date_ne",
"phone_number",
"province",
"province_ne",
"district",
"district_ne",
"municipality",
"municipality_ne",
]
class TransactionForm(forms.ModelForm):
class Meta:
model = Transaction
fields = ["title", "amount"]
def person_list(request: HttpRequest):
persons = Person.objects.all()
transactions = Transaction.objects.all()
return render(
request,
"demo/person_list.html",
{"persons": persons, "transactions": transactions},
)
def person_create(request: HttpRequest):
if request.method == "POST":
form = PersonForm(request.POST)
if form.is_valid():
form.save()
return redirect("demo:person-list")
else:
form = PersonForm()
return render(request, "demo/person_form.html", {"form": form})
def transaction_create(request: HttpRequest):
if request.method == "POST":
form = TransactionForm(request.POST)
if form.is_valid():
form.save()
return redirect("demo:person-list")
else:
form = TransactionForm()
return render(
request, "demo/person_form.html", {"form": form, "title": "Add Transaction"}
)
def address_normalize_demo(request: HttpRequest):
address = request.GET.get("address", "")
result = None
if address:
result = normalize_address(address)
if request.headers.get("HX-Request") == "true":
return render(
request, "demo/address_normalize_partial.html", {"result": result}
)
return render(
request,
"demo/address_normalize.html",
{"address": address, "result": result},
)
# --------------------------------------------------------------------------- #
# Plain-Django JSON API (no DRF, no django-filter)
# --------------------------------------------------------------------------- #
def _bad_request(message: str, **extra) -> JsonResponse:
payload = {"error": message}
payload.update(extra)
return JsonResponse(payload, status=400)
def _ne_flag(request: HttpRequest) -> bool:
"""Resolve the ``?ne=true`` flag for JSON endpoints.
Falls back to the ``Accept-Language`` header before defaulting to the
project setting so that consumers that send ``ne`` only on some
requests still get a sensible answer.
"""
raw = request.GET.get("ne")
if raw is not None:
return raw.lower() in _TRUE_VALUES
accept = (request.headers.get("Accept-Language") or "").lower()
if "ne" in accept:
return True
return nepkit_settings.DEFAULT_LANGUAGE == "ne"
def _strict_flag(request: HttpRequest) -> bool:
"""Resolve the ``?strict=1`` flag for JSON endpoints.
When True, the demo's ``serialize_*`` calls raise on unparseable
values instead of falling back to ``str(value)`` — useful for
smoke-testing the strict paths the API exposes.
"""
raw = request.GET.get("strict")
if raw is None:
return False
return raw.lower() in _TRUE_VALUES
def _paginate(request: HttpRequest, queryset, per_page: int = 25):
try:
page_num = max(1, int(request.GET.get("page", 1)))
except (TypeError, ValueError):
page_num = 1
paginator = Paginator(queryset, per_page)
page = paginator.get_page(page_num)
return page.object_list, {
"page": page.number,
"pages": paginator.num_pages,
"per_page": per_page,
"total": paginator.count,
}
def _search(queryset, search_fields: list[str], term: str | None):
if not term:
return queryset
q = Q()
for field in search_fields:
q |= Q(**{f"{field}__icontains": term})
return queryset.filter(q)
def _apply_ordering(queryset, ordering_fields: list[str], order_param: str | None):
"""Apply ``?order=`` whitelist.
Returns ``(queryset, error)``: the queryset is unchanged on error, and
``error`` is an :class:`HttpResponseBadRequest` if the value is
malformed.
"""
if not order_param:
return queryset, None
field = order_param.lstrip("-")
if field not in ordering_fields:
return (
queryset,
_bad_request(
_("Invalid order field: %(field)s"),
field=field,
allowed=sorted(ordering_fields),
),
)
return queryset.order_by(order_param), None
def _year_month_filter(queryset, field_name: str, year: str | None, month: str | None):
"""Year/month substring filter, mirroring the default ``BS_DATE_FORMAT``."""
fmt = nepkit_settings.BS_DATE_FORMAT
if year:
try:
year_int = int(year)
except (TypeError, ValueError):
return queryset, _bad_request(_("Invalid year: %(value)s"), value=year)
if fmt.startswith("%Y"):
sep = fmt[2] if len(fmt) > 2 and not fmt[2].startswith("%") else "-"
queryset = queryset.filter(
**{f"{field_name}__startswith": f"{year_int}{sep}"}
)
if month:
try:
month_int = int(month)
except (TypeError, ValueError):
return queryset, _bad_request(_("Invalid month: %(value)s"), value=month)
if not 1 <= month_int <= 12:
return queryset, _bad_request(
_("Month out of range: %(value)s"), value=month
)
month_str = f"{month_int:02d}"
if fmt == "%Y-%m-%d":
queryset = queryset.filter(**{f"{field_name}__contains": f"-{month_str}-"})
else:
sep = fmt[2] if len(fmt) > 2 and not fmt[2].startswith("%") else "-"
queryset = queryset.filter(
**{f"{field_name}__contains": f"{sep}{month_str}{sep}"}
)
return queryset, None
def _range_filter(queryset, field_name: str, raw: str | None, field_type: str = "auto"):
"""Filter by ``"min,max"`` / ``"min,"`` / ``",max"`` / ``"value"``.
``field_type`` is one of ``"auto"``, ``"decimal"``, ``"date"`` or
``"integer"`` and is used to validate user input before it reaches
the ORM (so a stray string doesn't 500 the request).
"""
if not raw:
return queryset, None
parts = [p.strip() for p in raw.split(",")]
if len(parts) > 2:
return queryset, _bad_request(
_("Range must be one or two comma-separated values: %(value)s"),
value=raw,
)
def _validate(value: str):
if not value:
return None
if field_type == "decimal":
try:
return Decimal(value)
except (InvalidOperation, ValueError):
raise ValueError(value)
if field_type == "integer":
try:
return int(value)
except (TypeError, ValueError):
raise ValueError(value)
if field_type == "date":
# Will be re-validated by the ORM; let it pass.
return value
# Auto: try decimal first, fall back to string.
try:
return Decimal(value)
except (InvalidOperation, ValueError):
return value
try:
if len(parts) == 1:
v = _validate(parts[0])
if v is None:
return queryset, None
return queryset.filter(**{field_name: v}), None
lo, hi = parts
lo_v = _validate(lo) if lo else None
hi_v = _validate(hi) if hi else None
if lo_v is not None and hi_v is not None:
return queryset.filter(**{f"{field_name}__range": (lo_v, hi_v)}), None
if lo_v is not None:
return queryset.filter(**{f"{field_name}__gte": lo_v}), None
if hi_v is not None:
return queryset.filter(**{f"{field_name}__lte": hi_v}), None
return queryset, None
except ValueError as exc:
return queryset, _bad_request(
_("Invalid range value: %(value)s") % {"value": str(exc)}
)
# ---- Person -----------------------------------------------------------------
@require_GET
def person_api(request: HttpRequest):
qs = Person.objects.all()
qs = _search(qs, ["name", "birth_date", "phone_number"], request.GET.get("q"))
qs, err = _apply_ordering(
qs, ["name", "birth_date", "created_at"], request.GET.get("order")
)
if err is not None:
return err
birth_date = request.GET.get("birth_date")
if birth_date:
qs = qs.filter(birth_date=birth_date)
qs, err = _year_month_filter(
qs, "birth_date", request.GET.get("year"), request.GET.get("month")
)
if err is not None:
return err
if request.GET.get("province"):
qs = qs.filter(province=request.GET["province"])
if request.GET.get("district"):
qs = qs.filter(district=request.GET["district"])
items, meta = _paginate(request, qs)
ne = _ne_flag(request)
strict = _strict_flag(request)
payload = [_person_to_payload(p, ne=ne, strict=strict) for p in items]
return JsonResponse({"results": payload, "meta": meta})
def _person_to_payload(
person: Person, *, ne: bool, strict: bool = False
) -> dict[str, Any]:
return nepkit_api.build_localized_payload(
{
"id": person.id,
"name": person.name,
"birth_date": nepkit_api.serialize_nepali_date(
person.birth_date, ne=ne, strict=strict
),
"phone_number": person.phone_number,
"province": person.province,
"district": person.district,
"municipality": person.municipality,
"created_at": nepkit_api.serialize_nepali_datetime(
person.created_at, ne=ne, strict=strict
),
},
ne=ne,
)
# ---- Citizen ----------------------------------------------------------------
@require_GET
def citizen_api(request: HttpRequest):
qs = Citizen.objects.all()
qs, err = _apply_ordering(
qs, ["name", "province", "district"], request.GET.get("order")
)
if err is not None:
return err
items, meta = _paginate(request, qs)
ne = _ne_flag(request)
return JsonResponse(
{
"results": [
nepkit_api.build_localized_payload(
{
"id": c.id,
"name": c.name,
"province": c.province,
"district": c.district,
"municipality": c.municipality,
},
ne=ne,
)
for c in items
],
"meta": meta,
}
)
# ---- Audited person ---------------------------------------------------------
@require_GET
def audited_api(request: HttpRequest):
qs = AuditedPerson.objects.all()
qs = _search(qs, ["name", "birth_date"], request.GET.get("q"))
qs, err = _apply_ordering(qs, ["name", "created_at"], request.GET.get("order"))
if err is not None:
return err
items, meta = _paginate(request, qs)
ne = _ne_flag(request)
strict = _strict_flag(request)
return JsonResponse(
{
"results": [
nepkit_api.build_localized_payload(
{
"id": a.id,
"name": a.name,
"birth_date": nepkit_api.serialize_nepali_date(
a.birth_date, ne=ne, strict=strict
),
"created_at": nepkit_api.serialize_nepali_datetime(
a.created_at, ne=ne, strict=strict
),
},
ne=ne,
)
for a in items
],
"meta": meta,
}
)
# ---- Transaction ------------------------------------------------------------
@require_GET
def transaction_api(request: HttpRequest):
qs = Transaction.objects.all()
qs = _search(qs, ["title", "amount", "date"], request.GET.get("q"))
qs, err = _range_filter(
qs, "amount", request.GET.get("amount_range"), field_type="decimal"
)
if err is not None:
return err
qs, err = _range_filter(
qs, "date", request.GET.get("date_range"), field_type="date"
)
if err is not None:
return err
if request.GET.get("title"):
qs = qs.filter(title=request.GET["title"])
items, meta = _paginate(request, qs)
ne = _ne_flag(request)
strict = _strict_flag(request)
return JsonResponse(
{
"results": [
nepkit_api.build_localized_payload(
{
"id": t.id,
"title": t.title,
"amount": str(t.amount),
"amount_formatted": nepkit_api.serialize_nepali_currency(
t.amount, ne=ne
),
"date": nepkit_api.serialize_nepali_date(
t.date, ne=ne, strict=strict
),
},
ne=ne,
)
for t in items
],
"meta": meta,
}
)