feat: implement zero-config address chaining and revamp documentation
This commit is contained in:
7 files changed
+154
-74
No files matched your search
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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