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.
This commit is contained in:
40 files changed
+3286
-1406
No files matched your search
@@ -1,39 +0,0 @@
|
||||
from rest_framework import serializers
|
||||
from django_nepkit.serializers import (
|
||||
NepaliDateSerializerField,
|
||||
NepaliCurrencySerializerField,
|
||||
NepaliLocalizedSerializerMixin,
|
||||
)
|
||||
from .models import Person, Citizen, AuditedPerson, Transaction
|
||||
|
||||
|
||||
class PersonSerializer(NepaliLocalizedSerializerMixin, serializers.ModelSerializer):
|
||||
birth_date = NepaliDateSerializerField()
|
||||
|
||||
class Meta:
|
||||
model = Person
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class TransactionSerializer(
|
||||
NepaliLocalizedSerializerMixin, serializers.ModelSerializer
|
||||
):
|
||||
amount_formatted = NepaliCurrencySerializerField(source="amount")
|
||||
|
||||
class Meta:
|
||||
model = Transaction
|
||||
fields = ["id", "title", "amount", "amount_formatted", "date"]
|
||||
|
||||
|
||||
class CitizenSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Citizen
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class AuditedPersonSerializer(serializers.ModelSerializer):
|
||||
birth_date = NepaliDateSerializerField()
|
||||
|
||||
class Meta:
|
||||
model = AuditedPerson
|
||||
fields = "__all__"
|
||||
+8
-11
@@ -1,20 +1,17 @@
|
||||
from . import views
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from django.urls import path
|
||||
|
||||
app_name = "demo"
|
||||
from . import views
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register("api/persons", views.PersonViewSet, basename="person-api")
|
||||
router.register("api/citizens", views.CitizenViewSet, basename="citizen-api")
|
||||
router.register("api/audited", views.AuditedPersonViewSet, basename="audited-api")
|
||||
router.register(
|
||||
"api/transactions", views.TransactionViewSet, basename="transaction-api"
|
||||
)
|
||||
app_name = "demo"
|
||||
|
||||
urlpatterns = [
|
||||
path("", views.person_list, name="person-list"),
|
||||
path("add/", views.person_create, name="person-create"),
|
||||
path("transactions/add/", views.transaction_create, name="transaction-create"),
|
||||
path("normalize/", views.address_normalize_demo, name="address-normalize"),
|
||||
] + router.urls
|
||||
# JSON API (plain Django — no DRF, no django-filter)
|
||||
path("api/persons.json", views.person_api, name="person-api"),
|
||||
path("api/citizens.json", views.citizen_api, name="citizen-api"),
|
||||
path("api/audited.json", views.audited_api, name="audited-api"),
|
||||
path("api/transactions.json", views.transaction_api, name="transaction-api"),
|
||||
]
|
||||
+396
-90
@@ -1,21 +1,39 @@
|
||||
"""
|
||||
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.shortcuts import render, redirect
|
||||
from rest_framework import viewsets, filters as drf_filters
|
||||
from django_filters import rest_framework as django_filters
|
||||
from django_nepkit.filters import NepaliDateYearFilter, NepaliDateMonthFilter
|
||||
from .models import Person, Citizen, AuditedPerson, Transaction
|
||||
from .serializers import (
|
||||
PersonSerializer,
|
||||
CitizenSerializer,
|
||||
AuditedPersonSerializer,
|
||||
TransactionSerializer,
|
||||
)
|
||||
from django_nepkit.filters import (
|
||||
NepaliCurrencyRangeFilter,
|
||||
NepaliDateRangeFilter,
|
||||
)
|
||||
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:
|
||||
@@ -34,7 +52,13 @@ class PersonForm(forms.ModelForm):
|
||||
]
|
||||
|
||||
|
||||
def person_list(request):
|
||||
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(
|
||||
@@ -44,7 +68,7 @@ def person_list(request):
|
||||
)
|
||||
|
||||
|
||||
def person_create(request):
|
||||
def person_create(request: HttpRequest):
|
||||
if request.method == "POST":
|
||||
form = PersonForm(request.POST)
|
||||
if form.is_valid():
|
||||
@@ -55,13 +79,7 @@ def person_create(request):
|
||||
return render(request, "demo/person_form.html", {"form": form})
|
||||
|
||||
|
||||
class TransactionForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Transaction
|
||||
fields = ["title", "amount"]
|
||||
|
||||
|
||||
def transaction_create(request):
|
||||
def transaction_create(request: HttpRequest):
|
||||
if request.method == "POST":
|
||||
form = TransactionForm(request.POST)
|
||||
if form.is_valid():
|
||||
@@ -74,71 +92,7 @@ def transaction_create(request):
|
||||
)
|
||||
|
||||
|
||||
# --- API Support (DRF) ---
|
||||
|
||||
|
||||
class PersonViewSet(viewsets.ModelViewSet):
|
||||
queryset = Person.objects.all()
|
||||
serializer_class = PersonSerializer
|
||||
|
||||
# Sort and Search work automatically with BS date strings
|
||||
filter_backends = [
|
||||
django_filters.DjangoFilterBackend,
|
||||
drf_filters.SearchFilter,
|
||||
drf_filters.OrderingFilter,
|
||||
]
|
||||
|
||||
ordering_fields = ["birth_date", "created_at"]
|
||||
search_fields = ["name", "birth_date", "phone_number"]
|
||||
|
||||
# Simple exact date filter
|
||||
filterset_fields = {
|
||||
"birth_date": ["exact"],
|
||||
}
|
||||
|
||||
class PersonFilter(django_filters.FilterSet):
|
||||
year = NepaliDateYearFilter(field_name="birth_date")
|
||||
month = NepaliDateMonthFilter(field_name="birth_date")
|
||||
|
||||
class Meta:
|
||||
model = Person
|
||||
fields = ["province", "district"]
|
||||
|
||||
filterset_class = PersonFilter
|
||||
|
||||
|
||||
class CitizenViewSet(viewsets.ModelViewSet):
|
||||
queryset = Citizen.objects.all()
|
||||
serializer_class = CitizenSerializer
|
||||
filter_backends = [drf_filters.OrderingFilter]
|
||||
ordering_fields = ["province", "district"]
|
||||
|
||||
|
||||
class AuditedPersonViewSet(viewsets.ModelViewSet):
|
||||
queryset = AuditedPerson.objects.all()
|
||||
serializer_class = AuditedPersonSerializer
|
||||
filter_backends = [drf_filters.SearchFilter, drf_filters.OrderingFilter]
|
||||
search_fields = ["name", "birth_date"]
|
||||
ordering_fields = ["created_at"]
|
||||
|
||||
|
||||
class TransactionViewSet(viewsets.ModelViewSet):
|
||||
queryset = Transaction.objects.all()
|
||||
serializer_class = TransactionSerializer
|
||||
filter_backends = [django_filters.DjangoFilterBackend]
|
||||
|
||||
class TransactionFilter(django_filters.FilterSet):
|
||||
amount_range = NepaliCurrencyRangeFilter(field_name="amount")
|
||||
date_range = NepaliDateRangeFilter(field_name="date")
|
||||
|
||||
class Meta:
|
||||
model = Transaction
|
||||
fields = ["title", "amount", "date"]
|
||||
|
||||
filterset_class = TransactionFilter
|
||||
|
||||
|
||||
def address_normalize_demo(request):
|
||||
def address_normalize_demo(request: HttpRequest):
|
||||
address = request.GET.get("address", "")
|
||||
result = None
|
||||
if address:
|
||||
@@ -150,5 +104,357 @@ def address_normalize_demo(request):
|
||||
)
|
||||
|
||||
return render(
|
||||
request, "demo/address_normalize.html", {"address": address, "result": result}
|
||||
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,
|
||||
}
|
||||
)
|
||||
@@ -14,8 +14,6 @@ INSTALLED_APPS = [
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"django_nepkit",
|
||||
"rest_framework",
|
||||
"django_filters",
|
||||
"demo",
|
||||
]
|
||||
|
||||
|
||||
@@ -3,6 +3,6 @@ from django.urls import include, path
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
# path("nepkit/", include("django_nepkit.urls")),
|
||||
path("nepkit/", include("django_nepkit.urls")),
|
||||
path("", include("demo.urls")),
|
||||
]
|
||||
Reference in new issue
Block a user