feat: implement zero-config address chaining and revamp documentation

This commit is contained in:
sankalp committed 2026-01-30 14:32:15 +05:45
1 parent b127d925f1
commit 7b07835e0c
7 files changed
+154 -74

No files matched your search

+2 -18
View File
@@ -122,9 +122,7 @@ class RegistrationForm(forms.Form):
## 🗺️ Address Management (Chained Selects)
Model Nepal's administrative structure with automatic filtering: Province → District → Municipality.
### Step 1: Define Fields
Model Nepal's administrative structure with automatic filtering: Province → District → Municipality. This works out-of-the-box without any extra URL configuration.
```python
from django_nepkit import ProvinceField, DistrictField, MunicipalityField
@@ -135,24 +133,10 @@ class Address(models.Model):
district = DistrictField()
municipality = MunicipalityField()
# For Devanagari (बगती, काठमाडौँ, etc.)
# For Devanagari (बागमती, काठमाडौँ, etc.)
# province = ProvinceField(ne=True)
```
### Step 2: Include URLs (Crucial)
For the chained selects to fetch data dynamically, you **must** include the package URLs in your project's `urls.py`:
```python
# urls.py
from django.urls import include, path
urlpatterns = [
# ...
path("nepkit/", include("django_nepkit.urls")),
]
```
---
## 🔌 API Support (Django REST Framework)
+17 -21
View File
@@ -247,13 +247,15 @@ class ProvinceField(NepaliFieldMixin, models.CharField):
# To avoid complexity, I'll peek at kwargs['ne'] if it exists.
ne = kwargs.get("ne", False)
if ne:
choices = [
(getattr(p, "name_nepali", p.name), getattr(p, "name_nepali", p.name))
for p in provinces
]
else:
choices = [(p.name, p.name) for p in provinces]
def get_p_name(p, ne):
name = getattr(p, "name_nepali", p.name) if ne else p.name
if name == "Province 1":
return "Koshi Province"
if name == "प्रदेश नं. १":
return "कोशी प्रदेश"
return name
choices = [(get_p_name(p, ne), get_p_name(p, ne)) for p in provinces]
kwargs.setdefault("choices", choices)
super().__init__(*args, **kwargs)
@@ -274,13 +276,10 @@ class DistrictField(NepaliFieldMixin, models.CharField):
ne = kwargs.get("ne", False)
kwargs.setdefault("max_length", 100)
if ne:
choices = [
(getattr(d, "name_nepali", d.name), getattr(d, "name_nepali", d.name))
for d in districts
]
else:
choices = [(d.name, d.name) for d in districts]
def get_d_name(d, ne):
return getattr(d, "name_nepali", d.name) if ne else d.name
choices = [(get_d_name(d, ne), get_d_name(d, ne)) for d in districts]
kwargs.setdefault("choices", choices)
super().__init__(*args, **kwargs)
@@ -298,13 +297,10 @@ class MunicipalityField(NepaliFieldMixin, models.CharField):
ne = kwargs.get("ne", False)
kwargs.setdefault("max_length", 100)
if ne:
choices = [
(getattr(m, "name_nepali", m.name), getattr(m, "name_nepali", m.name))
for m in municipalities
]
else:
choices = [(m.name, m.name) for m in municipalities]
def get_m_name(m, ne):
return getattr(m, "name_nepali", m.name) if ne else m.name
choices = [(get_m_name(m, ne), get_m_name(m, ne)) for m in municipalities]
kwargs.setdefault("choices", choices)
super().__init__(*args, **kwargs)
@@ -18,61 +18,134 @@
selectElement.dispatchEvent(new Event('change'));
}
function getLocalData(type, parentId, isNepali) {
if (!window.NEPKIT_DATA) return null;
const lang = isNepali ? 'ne' : 'en';
const data = window.NEPKIT_DATA[lang];
if (!data) return null;
if (type === 'districts') {
return data.districts[parentId] || [];
} else if (type === 'municipalities') {
return data.municipalities[parentId] || [];
}
return null;
}
function getMatchingSelect(container, selector, isNepali) {
const matches = container.querySelectorAll(selector);
for (let el of matches) {
const elIsNepali = el.dataset.ne === 'true';
if (elIsNepali === isNepali) {
return el;
}
}
return null;
}
function init() {
document.querySelectorAll('.nepkit-province-select').forEach(provinceSelect => {
const isNepali = provinceSelect.dataset.ne === 'true';
const container = provinceSelect.closest('form') || document;
const districtSelect = getMatchingSelect(container, '.nepkit-district-select', isNepali);
const municipalitySelect = getMatchingSelect(container, '.nepkit-municipality-select', isNepali);
if (!provinceSelect.value) {
if (districtSelect && !districtSelect.value) {
updateOptions(districtSelect, [], isNepali ? 'जिल्ला छान्नुहोस्' : 'Select District');
}
if (municipalitySelect && !municipalitySelect.value) {
updateOptions(municipalitySelect, [], isNepali ? 'नगरपालिका छान्नुहोस्' : 'Select Municipality');
}
}
});
document.querySelectorAll('.nepkit-district-select').forEach(districtSelect => {
const isNepali = districtSelect.dataset.ne === 'true';
const container = districtSelect.closest('form') || document;
const municipalitySelect = getMatchingSelect(container, '.nepkit-municipality-select', isNepali);
if (!districtSelect.value) {
if (municipalitySelect && !municipalitySelect.value) {
updateOptions(municipalitySelect, [], isNepali ? 'नगरपालिका छान्नुहोस्' : 'Select Municipality');
}
}
});
}
document.addEventListener('change', function(e) {
if (e.target.matches('.nepkit-province-select')) {
const province = e.target.value;
const isNepali = e.target.dataset.ne === 'true';
const container = e.target.closest('form') || document;
const districtSelect = container.querySelector('.nepkit-district-select');
const municipalitySelect = container.querySelector('.nepkit-municipality-select');
const districtSelect = getMatchingSelect(container, '.nepkit-district-select', isNepali);
const municipalitySelect = getMatchingSelect(container, '.nepkit-municipality-select', isNepali);
// Always clear municipality if province changes
if (municipalitySelect) {
updateOptions(municipalitySelect, [], isNepali ? 'नगरपालिका छान्नुहोस्' : 'Select Municipality');
}
if (districtSelect) {
if (!province) {
updateOptions(districtSelect, [], 'Select District');
if (municipalitySelect) updateOptions(municipalitySelect, [], 'Select Municipality');
updateOptions(districtSelect, [], isNepali ? 'जिल्ला छान्नुहोस्' : 'Select District');
return;
}
let url = districtSelect.dataset.url + '?province=' + encodeURIComponent(province);
// Add ne/en parameters if present in data attributes
if (districtSelect.dataset.ne === 'true') {
url += '&ne=true';
const localData = getLocalData('districts', province, isNepali);
if (localData) {
updateOptions(districtSelect, localData, isNepali ? 'जिल्ला छान्नुहोस्' : 'Select District');
} else if (districtSelect.dataset.url) {
// Fallback to AJAX
let url = districtSelect.dataset.url + '?province=' + encodeURIComponent(province);
if (isNepali) url += '&ne=true';
if (districtSelect.dataset.en === 'true') url += '&en=true';
fetch(url)
.then(response => response.json())
.then(data => {
updateOptions(districtSelect, data, isNepali ? 'जिल्ला छान्नुहोस्' : 'Select District');
});
}
if (districtSelect.dataset.en === 'true') {
url += '&en=true';
}
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 isNepali = e.target.dataset.ne === 'true';
const container = e.target.closest('form') || document;
const municipalitySelect = container.querySelector('.nepkit-municipality-select');
const municipalitySelect = getMatchingSelect(container, '.nepkit-municipality-select', isNepali);
if (municipalitySelect) {
if (!district) {
updateOptions(municipalitySelect, [], 'Select Municipality');
updateOptions(municipalitySelect, [], isNepali ? 'नगरपालिका छान्नुहोस्' : 'Select Municipality');
return;
}
let url = municipalitySelect.dataset.url + '?district=' + encodeURIComponent(district);
// Add ne/en parameters if present in data attributes
if (municipalitySelect.dataset.ne === 'true') {
url += '&ne=true';
const localData = getLocalData('municipalities', district, isNepali);
if (localData) {
updateOptions(municipalitySelect, localData, isNepali ? 'नगरपालिका छान्नुहोस्' : 'Select Municipality');
} else if (municipalitySelect.dataset.url) {
// Fallback to AJAX
let url = municipalitySelect.dataset.url + '?district=' + encodeURIComponent(district);
if (isNepali) url += '&ne=true';
if (municipalitySelect.dataset.en === 'true') url += '&en=true';
fetch(url)
.then(response => response.json())
.then(data => {
updateOptions(municipalitySelect, data, isNepali ? 'नगरपालिका छान्नुहोस्' : 'Select Municipality');
});
}
if (municipalitySelect.dataset.en === 'true') {
url += '&en=true';
}
fetch(url)
.then(response => response.json())
.then(data => {
updateOptions(municipalitySelect, data, 'Select Municipality');
});
}
}
});
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
@@ -0,0 +1 @@
window.NEPKIT_DATA = {"en": {"provinces": [{"id": "Sudurpaschim Province", "text": "Sudurpaschim Province"}, {"id": "Madhesh Province", "text": "Madhesh Province"}, {"id": "Gandaki Province", "text": "Gandaki Province"}, {"id": "Bagmati Province", "text": "Bagmati Province"}, {"id": "Lumbini Province", "text": "Lumbini Province"}, {"id": "Karnali Province", "text": "Karnali Province"}, {"id": "Koshi Province", "text": "Koshi Province"}], "districts": {"Sudurpaschim Province": [{"id": "Bajura", "text": "Bajura"}, {"id": "Bajhang", "text": "Bajhang"}, {"id": "Baitadi", "text": "Baitadi"}, {"id": "Dadeldhura", "text": "Dadeldhura"}, {"id": "Doti", "text": "Doti"}, {"id": "Achham", "text": "Achham"}, {"id": "Kailali", "text": "Kailali"}, {"id": "Darchula", "text": "Darchula"}, {"id": "Kanchanpur", "text": "Kanchanpur"}], "Madhesh Province": [{"id": "Saptari", "text": "Saptari"}, {"id": "Siraha", "text": "Siraha"}, {"id": "Dhanusa", "text": "Dhanusa"}, {"id": "Mahottari", "text": "Mahottari"}, {"id": "Sarlahi", "text": "Sarlahi"}, {"id": "Rautahat", "text": "Rautahat"}, {"id": "Bara", "text": "Bara"}, {"id": "Parsa", "text": "Parsa"}], "Gandaki Province": [{"id": "Gorkha", "text": "Gorkha"}, {"id": "Manang", "text": "Manang"}, {"id": "Mustang", "text": "Mustang"}, {"id": "Myagdi", "text": "Myagdi"}, {"id": "Kaski", "text": "Kaski"}, {"id": "Lamjung", "text": "Lamjung"}, {"id": "Tanahu", "text": "Tanahu"}, {"id": "Nawalparasi East", "text": "Nawalparasi East"}, {"id": "Syangja", "text": "Syangja"}, {"id": "Parbat", "text": "Parbat"}, {"id": "Baglung", "text": "Baglung"}], "Bagmati Province": [{"id": "Dolakha", "text": "Dolakha"}, {"id": "Sindhupalchok", "text": "Sindhupalchok"}, {"id": "Rasuwa", "text": "Rasuwa"}, {"id": "Dhading", "text": "Dhading"}, {"id": "Nuwakot", "text": "Nuwakot"}, {"id": "Kathmandu", "text": "Kathmandu"}, {"id": "Bhaktapur", "text": "Bhaktapur"}, {"id": "Lalitpur", "text": "Lalitpur"}, {"id": "Kavrepalanchok", "text": "Kavrepalanchok"}, {"id": "Ramechhap", "text": "Ramechhap"}, {"id": "Sindhuli", "text": "Sindhuli"}, {"id": "Makwanpur", "text": "Makwanpur"}, {"id": "Chitawan", "text": "Chitawan"}], "Lumbini Province": [{"id": "Rukum East", "text": "Rukum East"}, {"id": "Rolpa", "text": "Rolpa"}, {"id": "Pyuthan", "text": "Pyuthan"}, {"id": "Gulmi", "text": "Gulmi"}, {"id": "Arghakhanchi", "text": "Arghakhanchi"}, {"id": "Palpa", "text": "Palpa"}, {"id": "Rupandehi", "text": "Rupandehi"}, {"id": "Kapilvastu", "text": "Kapilvastu"}, {"id": "Dang", "text": "Dang"}, {"id": "Banke", "text": "Banke"}, {"id": "Bardiya", "text": "Bardiya"}, {"id": "Nawalparasi West", "text": "Nawalparasi West"}], "Karnali Province": [{"id": "Dolpa", "text": "Dolpa"}, {"id": "Mugu", "text": "Mugu"}, {"id": "Jumla", "text": "Jumla"}, {"id": "Kalikot", "text": "Kalikot"}, {"id": "Dailekh", "text": "Dailekh"}, {"id": "Jajarkot", "text": "Jajarkot"}, {"id": "Salyan", "text": "Salyan"}, {"id": "Surkhet", "text": "Surkhet"}, {"id": "Humla", "text": "Humla"}, {"id": "Rukum West", "text": "Rukum West"}], "Koshi Province": [{"id": "Taplejung", "text": "Taplejung"}, {"id": "Sankhuwasabha", "text": "Sankhuwasabha"}, {"id": "Solukhumbu", "text": "Solukhumbu"}, {"id": "Okhaldhunga", "text": "Okhaldhunga"}, {"id": "Khotang", "text": "Khotang"}, {"id": "Bhojpur", "text": "Bhojpur"}, {"id": "Dhankuta", "text": "Dhankuta"}, {"id": "Tehrathum", "text": "Tehrathum"}, {"id": "Panchthar", "text": "Panchthar"}, {"id": "Ilam", "text": "Ilam"}, {"id": "Jhapa", "text": "Jhapa"}, {"id": "Morang", "text": "Morang"}, {"id": "Sunsari", "text": "Sunsari"}, {"id": "Udayapur", "text": "Udayapur"}]}, "municipalities": {"Bajura": [{"id": "Himali Rural Municipality", "text": "Himali Rural Municipality"}, {"id": "Gaumul Rural Municipality", "text": "Gaumul Rural Municipality"}, {"id": "Badimalika Municipality", "text": "Badimalika Municipality"}, {"id": "Chhededaha Rural Municipality", "text": "Chhededaha Rural Municipality"}, {"id": "Budhiganga Municipality", "text": "Budhiganga Municipality"}, {"id": "Tribeni Municipality", "text": "Tribeni Municipality"}, {"id": "Jagannath Rural Municipality", "text": "Jagannath Rural Municipality"}, {"id": "Budhinanda Municipality", "text": "Budhinanda Municipality"}, {"id": "Swami Kartik Rural Municipality", "text": "Swami Kartik Rural Municipality"}], "Bajhang": [{"id": "Kanda Rural Municipality", "text": "Kanda Rural Municipality"}, {"id": "Bungal Municipality", "text": "Bungal Municipality"}, {"id": "Talkot Rural Municipality", "text": "Talkot Rural Municipality"}, {"id": "Masta Rural Municipality", "text": "Masta Rural Municipality"}, {"id": "Jayaprithbi Municipality", "text": "Jayaprithbi Municipality"}, {"id": "Chhabis Pathibhara Rural Municipality", "text": "Chhabis Pathibhara Rural Municipality"}, {"id": "Durgathali Rural Municipality", "text": "Durgathali Rural Municipality"}, {"id": "Bitthadchir Rural Municipality", "text": "Bitthadchir Rural Municipality"}, {"id": "Thalara Rural Municipality", "textLine truncated
+14 -1
View File
@@ -43,7 +43,20 @@ def _get_location_children(parent_list, parent_name, child_attr, ne=False):
"""
selected_parent = None
for p in parent_list:
if p.name == parent_name or getattr(p, "name_nepali", None) == parent_name:
p_name = p.name
p_name_ne = getattr(p, "name_nepali", None)
# Mapping fix for lookups
if parent_name == "Koshi Province":
if p_name == "Province 1":
selected_parent = p
break
elif parent_name == "कोशी प्रदेश":
if p_name_ne == "प्रदेश नं. १":
selected_parent = p
break
if p_name == parent_name or p_name_ne == parent_name:
selected_parent = p
break
+16 -3
View File
@@ -42,7 +42,10 @@ class NepaliWidgetMixin:
class ChainedSelectWidget(forms.Select):
class Media:
js = ("django_nepkit/js/address-chaining.js",)
js = (
"django_nepkit/js/nepal-data.js",
"django_nepkit/js/address-chaining.js",
)
class ProvinceSelectWidget(NepaliWidgetMixin, ChainedSelectWidget):
@@ -53,13 +56,23 @@ class ProvinceSelectWidget(NepaliWidgetMixin, ChainedSelectWidget):
class DistrictSelectWidget(NepaliWidgetMixin, ChainedSelectWidget):
def _configure_attrs(self, attrs):
_append_css_class(attrs, "nepkit-district-select")
attrs["data-url"] = reverse_lazy("django_nepkit:district-list")
try:
url = reverse_lazy("django_nepkit:district-list")
str(url) # Force evaluation to catch NoReverseMatch
attrs["data-url"] = url
except Exception:
pass
class MunicipalitySelectWidget(NepaliWidgetMixin, ChainedSelectWidget):
def _configure_attrs(self, attrs):
_append_css_class(attrs, "nepkit-municipality-select")
attrs["data-url"] = reverse_lazy("django_nepkit:municipality-list")
try:
url = reverse_lazy("django_nepkit:municipality-list")
str(url) # Force evaluation to catch NoReverseMatch
attrs["data-url"] = url
except Exception:
pass
class NepaliDatePickerWidget(NepaliWidgetMixin, forms.TextInput):
+1 -1
View File
@@ -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")),
]