diff --git a/django_nepkit/models.py b/django_nepkit/models.py index 93718f9..8e860fe 100644 --- a/django_nepkit/models.py +++ b/django_nepkit/models.py @@ -1,7 +1,13 @@ from django.db import models from django.utils.translation import gettext_lazy as _ +from nepali.locations import districts, municipalities, provices from django_nepkit.validators import validate_nepali_phone_number +from django_nepkit.widgets import ( + DistrictSelectWidget, + MunicipalitySelectWidget, + ProvinceSelectWidget, +) class NepaliPhoneNumberField(models.CharField): @@ -11,3 +17,45 @@ class NepaliPhoneNumberField(models.CharField): kwargs.setdefault("max_length", 10) super().__init__(*args, **kwargs) self.validators.append(validate_nepali_phone_number) + + +class ProvinceField(models.CharField): + description = _("Nepal's Province") + + def __init__(self, *args, **kwargs): + kwargs.setdefault("max_length", 100) + kwargs.setdefault("choices", [(p.name, p.name) for p in provices]) + super().__init__(*args, **kwargs) + + def formfield(self, **kwargs): + defaults = {"widget": ProvinceSelectWidget} + defaults.update(kwargs) + return super().formfield(**defaults) + + +class DistrictField(models.CharField): + description = _("Nepal's District") + + def __init__(self, *args, **kwargs): + kwargs.setdefault("max_length", 100) + kwargs.setdefault("choices", [(d.name, d.name) for d in districts]) + super().__init__(*args, **kwargs) + + def formfield(self, **kwargs): + defaults = {"widget": DistrictSelectWidget} + defaults.update(kwargs) + return super().formfield(**defaults) + + +class MunicipalityField(models.CharField): + description = _("Nepal's Municipality") + + def __init__(self, *args, **kwargs): + kwargs.setdefault("max_length", 100) + kwargs.setdefault("choices", [(m.name, m.name) for m in municipalities]) + super().__init__(*args, **kwargs) + + def formfield(self, **kwargs): + defaults = {"widget": MunicipalitySelectWidget} + defaults.update(kwargs) + return super().formfield(**defaults) diff --git a/django_nepkit/static/js/address-chaining.js b/django_nepkit/static/js/address-chaining.js new file mode 100644 index 0000000..f15befe --- /dev/null +++ b/django_nepkit/static/js/address-chaining.js @@ -0,0 +1,89 @@ +(function() { + "use strict"; + + function updateOptions(selectElement, data, placeholder) { + selectElement.innerHTML = ""; + if (placeholder) { + const opt = document.createElement("option"); + opt.value = ""; + opt.textContent = placeholder; + selectElement.appendChild(opt); + } + data.forEach((item) => { + const opt = document.createElement("option"); + opt.value = item.id; + opt.textContent = item.text; + selectElement.appendChild(opt); + }); + selectElement.dispatchEvent(new Event("change")); + } + + document.addEventListener("change", function(e) { + if (e.target.matches(".nepkit-province-select")) { + const province = e.target.value; + const container = e.target.closest("form") || document; + const districtSelect = container.querySelector( + ".nepkit-district-select", + ); + const municipalitySelect = container.querySelector( + ".nepkit-municipality-select", + ); + + if (districtSelect) { + if (!province) { + updateOptions(districtSelect, [], "Select District"); + if (municipalitySelect) + updateOptions( + municipalitySelect, + [], + "Select Municipality", + ); + return; + } + + const url = + districtSelect.dataset.url + + "?province=" + + encodeURIComponent(province); + fetch(url) + .then((response) => response.json()) + .then((data) => { + updateOptions(districtSelect, data, "Select District"); + }); + } + } + + if (e.target.matches(".nepkit-district-select")) { + const district = e.target.value; + const container = e.target.closest("form") || document; + const municipalitySelect = container.querySelector( + ".nepkit-municipality-select", + ); + + if (municipalitySelect) { + if (!district) { + updateOptions( + municipalitySelect, + [], + "Select Municipality", + ); + return; + } + + const url = + municipalitySelect.dataset.url + + "?district=" + + encodeURIComponent(district); + fetch(url) + .then((response) => response.json()) + .then((data) => { + updateOptions( + municipalitySelect, + data, + "Select Municipality", + ); + }); + } + } + }); +})(); diff --git a/django_nepkit/urls.py b/django_nepkit/urls.py index e69de29..476038a 100644 --- a/django_nepkit/urls.py +++ b/django_nepkit/urls.py @@ -0,0 +1,10 @@ +from django.urls import path + +from django_nepkit.views import district_list_view, municipality_list_view + +app_name = "django_nepkit" + +urlpatterns = [ + path("districts/", district_list_view, name="district-list"), + path("municipalities/", municipality_list_view, name="municipality-list"), +] diff --git a/django_nepkit/utils.py b/django_nepkit/utils.py index e69de29..6de3ca2 100644 --- a/django_nepkit/utils.py +++ b/django_nepkit/utils.py @@ -0,0 +1,15 @@ +from nepali.locations import districts, provices + + +def get_districts_by_province(provice_name): + selected_province = next((p for p in provices if p.name == provice_name), None) + if not selected_province: + return [] + return [{"id": d.name, "text": d.name} for d in selected_province.districts] + + +def get_municipalities_by_district(district_name): + selected_district = next((d for d in districts if d.name == district_name), None) + if not selected_district: + return [] + return [{"id": m.name, "text": m.name} for m in selected_district.municipalities] diff --git a/django_nepkit/views.py b/django_nepkit/views.py index e69de29..6359784 100644 --- a/django_nepkit/views.py +++ b/django_nepkit/views.py @@ -0,0 +1,22 @@ +from django.http import JsonResponse + +from django_nepkit.utils import ( + get_districts_by_province, + get_municipalities_by_district, +) + + +def district_list_view(request): + province = request.GET.get("province") + if not province: + return JsonResponse([], safe=False) + data = get_districts_by_province(province) + return JsonResponse(data, safe=False) + + +def municipality_list_view(request): + district = request.GET.get("district") + if not district: + return JsonResponse([], safe=False) + data = get_municipalities_by_district(district) + return JsonResponse(data, safe=False) diff --git a/django_nepkit/widgets.py b/django_nepkit/widgets.py index e69de29..86bd07e 100644 --- a/django_nepkit/widgets.py +++ b/django_nepkit/widgets.py @@ -0,0 +1,33 @@ +from django import forms +from django.urls import reverse_lazy + + +class ChainedSelectWidget(forms.Select): + class Media: + js = ("django_nepkit/js/address-chaining.js",) + + +class ProvinceSelectWidget(ChainedSelectWidget): + def __init__(self, *args, **kwargs): + attrs = kwargs.get("attrs", {}) + attrs["class"] = attrs.get("class", "") + " nepkit-province-select" + kwargs["attrs"] = attrs + super().__init__(*args, **kwargs) + + +class DistrictSelectWidget(ChainedSelectWidget): + def __init__(self, *args, **kwargs): + attrs = kwargs.get("attrs", {}) + attrs["class"] = attrs.get("class", "") + " nepkit-district-select" + attrs["data-url"] = reverse_lazy("django_nepkit:district-list") + kwargs["attrs"] = attrs + super().__init__(*args, **kwargs) + + +class MunicipalitySelectWidget(ChainedSelectWidget): + def __init__(self, *args, **kwargs): + attrs = kwargs.get("attrs", {}) + attrs["class"] = attrs.get("class", "") + " nepkit-municipality-select" + attrs["data-url"] = reverse_lazy("django_nepkit:municipality-list") + kwargs["attrs"] = attrs + super().__init__(*args, **kwargs)