diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0dc9aa1..f67e24c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,21 @@
+## V.2.1.0 — 2027-02-05
+
+### ✨ Features
+
+- upgrade nepali package to 1.2.0 and remove hardcoded Koshi Province mapping
+
+### ♻️ Refactoring
+
+- remove code redundancy and improve maintainability
+- extract duplicate view logic into reusable helper functions in views.py
+- create constants.py for centralized hardcoded values (Nepali words, placeholders)
+- simplify number_to_nepali_words function from 157 to 42 lines
+- refactor normalize_address with extracted helper functions
+- add lang_utils.py for consistent language parameter handling
+- reduce JavaScript duplication in address-chaining.js with helper functions
+- move circular import from module level to method scope in models.py
+- remove ~160+ lines of redundant code while preserving all logic
+
## V.2.0.0 — 2026-01-31
### ✨ Features
diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
new file mode 100644
index 0000000..45748de
--- /dev/null
+++ b/RELEASE-NOTES.md
@@ -0,0 +1,33 @@
+## 🚀 Release Unreleased — 2026-02-05
+
+### ✨ Features
+- implement address normalization and demo
+- add Nepali currency support, number-to-words conversion, and REST API enhancements
+- add Nepali currency field, number to words, and unicode helpers
+- add Nepali currency field, number to words, and unicode helpers
+- add English datepicker support and comprehensive documentation improvements
+- implement HTMX support, DRF filters, and professionalized documentation
+- implement zero-config address chaining and revamp documentation
+- revamp localization support with Devanagari (BS) integration and documentation
+
+### 📚 Documentation
+- update
+- added showcase
+- updated readme
+
+### 🧹 Other Changes
+- feat:upgrade nepali package to 1.2.0 and remove hardcoded Koshi Province mapping
+- feat:upgrade nepali package to 1.2.0 and remove hardcoded Koshi Province mapping
+- bumped to v.0.2
+- bumped to v.0.2
+- fix
+- update
+- update todo
+- added todo
+- update
+- updated workflow
+- added workflow
+- add comprehensive pytest test suite with 67% coverage
+- update gitignore
+- global project refinement, DRY consolidation, and documentation overhaul
+- unwanted files
diff --git a/django_nepkit/constants.py b/django_nepkit/constants.py
new file mode 100644
index 0000000..8c750b3
--- /dev/null
+++ b/django_nepkit/constants.py
@@ -0,0 +1,129 @@
+"""
+Constants for django-nepkit package.
+Contains hardcoded values, word mappings, and configuration data.
+"""
+
+# Nepali number words mapping (0-99)
+NEPALI_ONES = [
+ "",
+ "एक",
+ "दुई",
+ "तीन",
+ "चार",
+ "पाँच",
+ "छ",
+ "सात",
+ "आठ",
+ "नौ",
+ "दश",
+ "एघार",
+ "बाह्र",
+ "तेह्र",
+ "चौध",
+ "पन्ध्र",
+ "सोह्र",
+ "सत्र",
+ "अठार",
+ "उन्नाइस",
+ "बीस",
+ "एकाइस",
+ "बाइस",
+ "तेईस",
+ "चौबीस",
+ "पच्चीस",
+ "छब्बीस",
+ "सत्ताइस",
+ "अठाइस",
+ "उनन्तीस",
+ "तीस",
+ "एकतीस",
+ "बत्तीस",
+ "तेत्तीस",
+ "चौंतीस",
+ "पैंतीस",
+ "छत्तीस",
+ "सैंतीस",
+ "अठतीस",
+ "उनन्चालीस",
+ "चालीस",
+ "एकचालीस",
+ "बयालीस",
+ "त्रिचालीस",
+ "चवालीस",
+ "पैंतालीस",
+ "छयालीस",
+ "सत्तालीस",
+ "अठचालीस",
+ "उनन्पचास",
+ "पचास",
+ "एकाउन्न",
+ "बाउन्न",
+ "त्रिपन्न",
+ "चउन्न",
+ "पचपन्न",
+ "छपन्न",
+ "सन्ताउन्न",
+ "अन्ठाउन्न",
+ "उनन्साठी",
+ "साठी",
+ "एकसाठी",
+ "बासट्ठी",
+ "त्रिसट्ठी",
+ "चौसट्ठी",
+ "पैंसट्ठी",
+ "छयसट्ठी",
+ "सतसट्ठी",
+ "अठसट्ठी",
+ "उनन्सत्तरी",
+ "सत्तरी",
+ "एकहत्तर",
+ "बाहत्तर",
+ "त्रिहत्तर",
+ "चौरहत्तर",
+ "पचहत्तर",
+ "छयहत्तर",
+ "सतहत्तर",
+ "अठहत्तर",
+ "उनन्असी",
+ "असी",
+ "एकासी",
+ "बयासी",
+ "त्रियासी",
+ "चौरासी",
+ "पचासी",
+ "छयासी",
+ "सतासी",
+ "अठासी",
+ "उनन्नब्बे",
+ "नब्बे",
+ "एकानब्बे",
+ "बयानब्बे",
+ "त्रियानब्बे",
+ "चौरानब्बे",
+ "पञ्चानब्बे",
+ "छ्यानब्बे",
+ "सन्तानब्बे",
+ "अन्ठानब्बे",
+ "उनन्सय",
+]
+
+# Nepali number units and their corresponding names
+NEPALI_UNITS = [
+ ("", ""),
+ (100, "सय"),
+ (1000, "हजार"),
+ (100000, "लाख"),
+ (10000000, "करोड"),
+ (1000000000, "अरब"),
+ (100000000000, "खरब"),
+]
+
+# Placeholder text for location selects
+PLACEHOLDERS = {
+ "province": {"ne": "प्रदेश छान्नुहोस्", "en": "Select Province"},
+ "district": {"ne": "जिल्ला छान्नुहोस्", "en": "Select District"},
+ "municipality": {"ne": "नगरपालिका छान्नुहोस्", "en": "Select Municipality"},
+}
+
+# Internal parameters to exclude from fallback logic
+INTERNAL_PARAMS = ["ne", "en", "html"]
diff --git a/django_nepkit/lang_utils.py b/django_nepkit/lang_utils.py
new file mode 100644
index 0000000..f9a3e40
--- /dev/null
+++ b/django_nepkit/lang_utils.py
@@ -0,0 +1,52 @@
+"""
+Language parameter utilities for django-nepkit.
+Helper functions to resolve Nepali/English language parameters consistently.
+"""
+
+from django_nepkit.conf import nepkit_settings
+
+
+def resolve_language_params(ne=None, en=None, **kwargs):
+ """
+ Resolve language parameters with fallback to default settings.
+ """
+ default_lang = nepkit_settings.DEFAULT_LANGUAGE
+
+ # Extract from kwargs if provided
+ if ne is None and "ne" in kwargs:
+ ne = kwargs.get("ne")
+ if en is None and "en" in kwargs:
+ en = kwargs.get("en")
+
+ # Set defaults
+ if ne is None:
+ ne = default_lang == "ne"
+
+ # Handle en parameter logic
+ explicit_en = en is not None
+ if en is None:
+ en = not ne
+
+ # If ne is True and en wasn't explicitly set, en should be False
+ if ne and not explicit_en:
+ en = False
+
+ return ne, en
+
+
+def pop_language_params(kwargs):
+ """
+ Pop ne/en parameters from kwargs dict and return resolved values.
+ """
+ default_lang = nepkit_settings.DEFAULT_LANGUAGE
+ ne = kwargs.pop("ne", default_lang == "ne")
+
+ explicit_en = "en" in kwargs
+ en_value = kwargs.pop("en", not ne)
+
+ if ne and not explicit_en:
+ en = False
+ else:
+ en = en_value
+
+ return ne, en
diff --git a/django_nepkit/models.py b/django_nepkit/models.py
index f1aedcf..cfadede 100644
--- a/django_nepkit/models.py
+++ b/django_nepkit/models.py
@@ -7,7 +7,6 @@ from django.utils.translation import gettext_lazy as _
from nepali.datetime import nepalidate, nepalidatetime
from nepali.locations import districts, municipalities, provinces
-from django_nepkit.forms import NepaliDateFormField
from django_nepkit.utils import (
BS_DATE_FORMAT,
BS_DATETIME_FORMAT,
@@ -172,6 +171,9 @@ class NepaliDateField(BaseNepaliBSField):
parse_func = staticmethod(try_parse_nepali_date)
def formfield(self, **kwargs):
+ # Import here to avoid circular dependency
+ from django_nepkit.forms import NepaliDateFormField
+
kwargs.setdefault("form_class", NepaliDateFormField)
return super().formfield(**kwargs)
diff --git a/django_nepkit/static/django_nepkit/js/address-chaining.js b/django_nepkit/static/django_nepkit/js/address-chaining.js
index 8004d87..e6fda4c 100644
--- a/django_nepkit/static/django_nepkit/js/address-chaining.js
+++ b/django_nepkit/static/django_nepkit/js/address-chaining.js
@@ -1,6 +1,20 @@
(function() {
'use strict';
+ // Placeholder text constants
+ const PLACEHOLDERS = {
+ province: { ne: 'प्रदेश छान्नुहोस्', en: 'Select Province' },
+ district: { ne: 'जिल्ला छान्नुहोस्', en: 'Select District' },
+ municipality: { ne: 'नगरपालिका छान्नुहोस्', en: 'Select Municipality' }
+ };
+
+ /**
+ * Get placeholder text for a specific location type
+ */
+ function getPlaceholder(type, isNepali) {
+ return PLACEHOLDERS[type][isNepali ? 'ne' : 'en'];
+ }
+
function updateOptions(selectElement, data, placeholder) {
selectElement.innerHTML = '';
if (placeholder) {
@@ -43,7 +57,45 @@
return null;
}
+ /**
+ * Fetch data from server endpoint
+ */
+ function fetchData(selectElement, paramName, paramValue, placeholderType, isNepali) {
+ if (!selectElement.dataset.url) return;
+
+ let url = selectElement.dataset.url + '?' + paramName + '=' + encodeURIComponent(paramValue);
+ if (isNepali) url += '&ne=true';
+ if (selectElement.dataset.en === 'true') url += '&en=true';
+
+ fetch(url)
+ .then(response => response.json())
+ .then(data => {
+ updateOptions(selectElement, data, getPlaceholder(placeholderType, isNepali));
+ });
+ }
+
+ /**
+ * Update dependent select element based on parent value
+ */
+ function updateDependentSelect(childSelect, dataType, parentValue, placeholderType, isNepali, paramName) {
+ if (!childSelect) return;
+
+ if (!parentValue) {
+ updateOptions(childSelect, [], getPlaceholder(placeholderType, isNepali));
+ return;
+ }
+
+ const localData = getLocalData(dataType, parentValue, isNepali);
+
+ if (localData) {
+ updateOptions(childSelect, localData, getPlaceholder(placeholderType, isNepali));
+ } else {
+ fetchData(childSelect, paramName, parentValue, placeholderType, isNepali);
+ }
+ }
+
function init() {
+ // Initialize province selects
document.querySelectorAll('.nepkit-province-select').forEach(provinceSelect => {
const isNepali = provinceSelect.dataset.ne === 'true';
const container = provinceSelect.closest('form') || document;
@@ -52,14 +104,15 @@
if (!provinceSelect.value) {
if (districtSelect && !districtSelect.value) {
- updateOptions(districtSelect, [], isNepali ? 'जिल्ला छान्नुहोस्' : 'Select District');
+ updateOptions(districtSelect, [], getPlaceholder('district', isNepali));
}
if (municipalitySelect && !municipalitySelect.value) {
- updateOptions(municipalitySelect, [], isNepali ? 'नगरपालिका छान्नुहोस्' : 'Select Municipality');
+ updateOptions(municipalitySelect, [], getPlaceholder('municipality', isNepali));
}
}
});
+ // Initialize district selects
document.querySelectorAll('.nepkit-district-select').forEach(districtSelect => {
const isNepali = districtSelect.dataset.ne === 'true';
const container = districtSelect.closest('form') || document;
@@ -67,13 +120,14 @@
if (!districtSelect.value) {
if (municipalitySelect && !municipalitySelect.value) {
- updateOptions(municipalitySelect, [], isNepali ? 'नगरपालिका छान्नुहोस्' : 'Select Municipality');
+ updateOptions(municipalitySelect, [], getPlaceholder('municipality', isNepali));
}
}
});
}
document.addEventListener('change', function(e) {
+ // Handle province select change
if (e.target.matches('.nepkit-province-select')) {
const province = e.target.value;
const isNepali = e.target.dataset.ne === 'true';
@@ -83,63 +137,22 @@
// Always clear municipality if province changes
if (municipalitySelect) {
- updateOptions(municipalitySelect, [], isNepali ? 'नगरपालिका छान्नुहोस्' : 'Select Municipality');
+ updateOptions(municipalitySelect, [], getPlaceholder('municipality', isNepali));
}
- if (districtSelect) {
- if (!province) {
- updateOptions(districtSelect, [], isNepali ? 'जिल्ला छान्नुहोस्' : 'Select District');
- return;
- }
-
- 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');
- });
- }
- }
+ // Update district options
+ updateDependentSelect(districtSelect, 'districts', province, 'district', isNepali, 'province');
}
+ // Handle district select change
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 = getMatchingSelect(container, '.nepkit-municipality-select', isNepali);
- if (municipalitySelect) {
- if (!district) {
- updateOptions(municipalitySelect, [], isNepali ? 'नगरपालिका छान्नुहोस्' : 'Select Municipality');
- return;
- }
-
- 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');
- });
- }
- }
+ // Update municipality options
+ updateDependentSelect(municipalitySelect, 'municipalities', district, 'municipality', isNepali, 'district');
}
});
diff --git a/django_nepkit/utils.py b/django_nepkit/utils.py
index 44ba9f6..acd5943 100644
--- a/django_nepkit/utils.py
+++ b/django_nepkit/utils.py
@@ -122,14 +122,11 @@ def number_to_nepali_words(number: Any) -> str:
Converts a number to Nepali words.
Eg. 123 -> एक सय तेईस
"""
+ from django_nepkit.constants import NEPALI_ONES, NEPALI_UNITS
+
if number is None:
return ""
- # Basic implementation for now, can be expanded
- # Mapping for numbers to words (simplified)
- # This is a complex task for a full implementation,
- # but I'll provide a robust enough version for common usage.
-
try:
num = int(float(number))
except (ValueError, TypeError):
@@ -138,132 +135,19 @@ def number_to_nepali_words(number: Any) -> str:
if num == 0:
return "शून्य"
- ones = [
- "",
- "एक",
- "दुई",
- "तीन",
- "चार",
- "पाँच",
- "छ",
- "सात",
- "आठ",
- "नौ",
- "दश",
- "एघार",
- "बाह्र",
- "तेह्र",
- "चौध",
- "पन्ध्र",
- "सोह्र",
- "सत्र",
- "अठार",
- "उन्नाइस",
- "बीस",
- "एकाइस",
- "बाइस",
- "तेईस",
- "चौबीस",
- "पच्चीस",
- "छब्बीस",
- "सत्ताइस",
- "अठाइस",
- "उनन्तीस",
- "तीस",
- "एकतीस",
- "बत्तीस",
- "तेत्तीस",
- "चौंतीस",
- "पैंतीस",
- "छत्तीस",
- "सैंतीस",
- "अठतीस",
- "उनन्चालीस",
- "चालीस",
- "एकचालीस",
- "बयालीस",
- "त्रिचालीस",
- "चवालीस",
- "पैंतालीस",
- "छयालीस",
- "सत्तालीस",
- "अठचालीस",
- "उनन्पचास",
- "पचास",
- "एकाउन्न",
- "बाउन्न",
- "त्रिपन्न",
- "चउन्न",
- "पचपन्न",
- "छपन्न",
- "सन्ताउन्न",
- "अन्ठाउन्न",
- "उनन्साठी",
- "साठी",
- "एकसाठी",
- "बासट्ठी",
- "त्रिसट्ठी",
- "चौसट्ठी",
- "पैंसट्ठी",
- "छयसट्ठी",
- "सतसट्ठी",
- "अठसट्ठी",
- "उनन्सत्तरी",
- "सत्तरी",
- "एकहत्तर",
- "बाहत्तर",
- "त्रिहत्तर",
- "चौरहत्तर",
- "पचहत्तर",
- "छयहत्तर",
- "सतहत्तर",
- "अठहत्तर",
- "उनन्असी",
- "असी",
- "एकासी",
- "बयासी",
- "त्रियासी",
- "चौरासी",
- "पचासी",
- "छयासी",
- "सतासी",
- "अठासी",
- "उनन्नब्बे",
- "नब्बे",
- "एकानब्बे",
- "बयानब्बे",
- "त्रियानब्बे",
- "चौरानब्बे",
- "पञ्चानब्बे",
- "छ्यानब्बे",
- "सन्तानब्बे",
- "अन्ठानब्बे",
- "उनन्सय",
- ]
-
- units = [
- ("", ""),
- (100, "सय"),
- (1000, "हजार"),
- (100000, "लाख"),
- (10000000, "करोड"),
- (1000000000, "अरब"),
- (100000000000, "खरब"),
- ]
-
def _convert(n):
if n == 0:
return ""
if n < 100:
- return ones[n]
+ return NEPALI_ONES[n]
- for i in range(len(units) - 1, 0, -1):
- div, unit_name = units[i]
+ for i in range(len(NEPALI_UNITS) - 1, 0, -1):
+ div, unit_name = NEPALI_UNITS[i]
if n >= div:
prefix_val = n // div
remainder = n % div
- # For 'सय' (100), we use ones[prefix_val]
+ # For 'सय' (100), we use NEPALI_ONES[prefix_val]
# For others, we might need recursive calls if prefix_val >= 100
prefix_words = _convert(prefix_val)
res = f"{prefix_words} {unit_name}"
@@ -288,6 +172,81 @@ def english_to_nepali_unicode(text: Any) -> str:
return english_to_nepali(text)
+def _normalize_nepali_text(text):
+ """
+ Normalize Nepali text for easier matching.
+ Replaces Chandrabindu with Anusvara.
+ """
+ if not text:
+ return text
+ return text.replace("ँ", "ं").replace("ाँ", "ां")
+
+
+def _matches_location_name(name_eng, name_nep, token, normalized_token):
+ """
+ Check if a token matches a location name (English or Nepali).
+
+ Args:
+ name_eng: English name of location
+ name_nep: Nepali name of location
+ token: Original token to match
+ normalized_token: Normalized version of token
+
+ Returns:
+ True if token matches the location name
+ """
+ token_lower = token.lower()
+ name_nep_norm = _normalize_nepali_text(name_nep)
+
+ # Exact matches
+ if (
+ token == name_nep
+ or normalized_token == name_nep_norm
+ or token_lower == name_eng.lower()
+ ):
+ return True
+
+ # Partial matches for English (e.g., "Pokhara" in "Pokhara Metropolitan City")
+ # Only if token is at least 4 characters to avoid too many false positives
+ if len(token) >= 4:
+ if token_lower in name_eng.lower():
+ return True
+
+ # Partial matches for Nepali
+ if len(normalized_token) >= 2:
+ if normalized_token in name_nep_norm:
+ return True
+
+ return False
+
+
+def _find_location_in_tokens(location_list, tokens, normalized_tokens):
+ """
+ Find a location from a list by matching against tokens.
+
+ Args:
+ location_list: List of location objects to search
+ tokens: List of original tokens
+ normalized_tokens: List of normalized tokens
+
+ Returns:
+ Matching location object or None
+ """
+ for i, token in enumerate(tokens):
+ nt = normalized_tokens[i]
+ for location in location_list:
+ if _matches_location_name(location.name, location.name_nepali, token, nt):
+ return location
+ return None
+
+
+def _is_nepali_text(tokens):
+ """Check if any token contains Devanagari characters."""
+ import re
+
+ return any(re.search(r"[\u0900-\u097F]", t) for t in tokens)
+
+
def normalize_address(address_string: str) -> dict[str, Optional[str]]:
"""
Attempts to normalize a Nepali address string into Province, District, and Municipality.
@@ -296,81 +255,23 @@ def normalize_address(address_string: str) -> dict[str, Optional[str]]:
if not address_string:
return {"province": None, "district": None, "municipality": None}
- result = {"province": None, "district": None, "municipality": None}
-
- import re
+ result: dict[str, Optional[str]] = {
+ "province": None,
+ "district": None,
+ "municipality": None,
+ }
+ # Prepare tokens
content = address_string.replace(",", " ").replace("-", " ")
-
- def normalize_nepali(text):
- if not text:
- return text
- # Replace Chandrabindu with Anusvara for easier matching
- return text.replace("ँ", "ं").replace("ाँ", "ां")
-
tokens = [t.strip() for t in content.split() if t.strip()]
- normalized_tokens = [normalize_nepali(t) for t in tokens]
+ normalized_tokens = [_normalize_nepali_text(t) for t in tokens]
- # We try to match from most specific to least specific
- found_municipality = None
- found_district = None
- found_province = None
-
- # Helper for name matching
- def matches(name_eng, name_nep, token, normalized_token):
- token_lower = token.lower()
- name_nep_norm = normalize_nepali(name_nep)
-
- # Exact matches
- if (
- token == name_nep
- or normalized_token == name_nep_norm
- or token_lower == name_eng.lower()
- ):
- return True
-
- # Partial matches for English (e.g., "Pokhara" in "Pokhara Metropolitan City")
- # Only if token is at least 4 characters to avoid too many false positives
- if len(token) >= 4:
- if token_lower in name_eng.lower():
- return True
-
- # Partial matches for Nepali
- if len(normalized_token) >= 2:
- if normalized_token in name_nep_norm:
- return True
-
- return False
-
- # Check for municipality first
- for i, token in enumerate(tokens):
- nt = normalized_tokens[i]
- for m in municipalities:
- if matches(m.name, m.name_nepali, token, nt):
- found_municipality = m
- break
- if found_municipality:
- break
-
- # Check for district
- for i, token in enumerate(tokens):
- nt = normalized_tokens[i]
- for d in districts:
- if matches(d.name, d.name_nepali, token, nt):
- found_district = d
- break
- if found_district:
- break
-
- # Check for province
- for i, token in enumerate(tokens):
- nt = normalized_tokens[i]
- for p in provinces:
- if matches(p.name, p.name_nepali, token, nt):
- found_province = p
- break
- if found_province:
- break
+ # Find locations - most specific to least specific
+ found_municipality = _find_location_in_tokens(
+ municipalities, tokens, normalized_tokens
+ )
+ found_district = _find_location_in_tokens(districts, tokens, normalized_tokens)
+ found_province = _find_location_in_tokens(provinces, tokens, normalized_tokens)
# Fill in the gaps using hierarchy
if found_municipality:
@@ -385,13 +286,11 @@ def normalize_address(address_string: str) -> dict[str, Optional[str]]:
if not found_province:
found_province = found_district.province
+ if found_province:
result["province"] = found_province.name
- # Check for Nepali context
- is_nepali = any(
- re.search(r"[\u0900-\u097F]", t) for t in tokens
- ) # Basic check for Devanagari characters
- if is_nepali:
+ # Use Nepali names if input contains Nepali text
+ if _is_nepali_text(tokens):
if found_municipality:
result["municipality"] = found_municipality.name_nepali
if found_district:
diff --git a/django_nepkit/views.py b/django_nepkit/views.py
index b3385fd..90611e5 100644
--- a/django_nepkit/views.py
+++ b/django_nepkit/views.py
@@ -5,7 +5,6 @@ from django_nepkit.utils import (
get_municipalities_by_district,
)
-
from django_nepkit.conf import nepkit_settings
@@ -14,72 +13,127 @@ def _render_options(data, placeholder):
options = [f'']
for item in data:
options.append(f'')
- return HttpResponse("\n".join(options))
+ return HttpResponse("\n".join(options), content_type="text/html")
+
+
+def _get_primary_param(request, param_name, exclude_params=None):
+ """
+ Extract a parameter from request.GET with fallback logic.
+
+ Args:
+ request: Django request object
+ param_name: Primary parameter name to look for
+ exclude_params: List of parameter names to exclude from fallback
+
+ Returns:
+ Parameter value or None
+ """
+ from django_nepkit.constants import INTERNAL_PARAMS
+
+ if exclude_params is None:
+ exclude_params = INTERNAL_PARAMS
+
+ # Try primary parameter first
+ value = request.GET.get(param_name)
+
+ # Fallback: take the first non-internal parameter
+ if not value:
+ for key, val in request.GET.items():
+ if key not in exclude_params and val:
+ value = val
+ break
+
+ return value
+
+
+def _parse_language_params(request):
+ """
+ Parse language parameters from request.
+
+ Args:
+ request: Django request object
+
+ Returns:
+ Tuple of (ne, en) boolean values
+ """
+ default_lang = nepkit_settings.DEFAULT_LANGUAGE
+ ne_param = request.GET.get("ne")
+ ne = ne_param.lower() == "true" if ne_param else default_lang == "ne"
+
+ en_param = request.GET.get("en")
+ en = en_param.lower() == "true" if en_param else not ne
+
+ return ne, en
+
+
+def _should_return_html(request):
+ """
+ Check if response should be HTML format.
+
+ Args:
+ request: Django request object
+
+ Returns:
+ Boolean indicating if HTML format is requested
+ """
+ return (
+ request.GET.get("html", "false").lower() == "true"
+ or request.headers.get("HX-Request") == "true"
+ )
+
+
+def _location_list_view(request, param_name, data_func, placeholders):
+ """
+ Generic view handler for location hierarchy endpoints.
+
+ Args:
+ request: Django request object
+ param_name: Name of the primary parameter to extract
+ data_func: Function to call to get location data
+ placeholders: Tuple of (nepali_placeholder, english_placeholder)
+
+ Returns:
+ JsonResponse or HttpResponse with location data
+ """
+ param_value = _get_primary_param(request, param_name)
+
+ if not param_value:
+ return JsonResponse([], safe=False)
+
+ ne, en = _parse_language_params(request)
+ as_html = _should_return_html(request)
+
+ data = data_func(param_value, ne=ne, en=en)
+
+ if as_html:
+ placeholder = placeholders[0] if ne else placeholders[1]
+ return _render_options(data, placeholder)
+
+ return JsonResponse(data, safe=False)
def district_list_view(request):
- province = request.GET.get("province")
+ """Return list of districts for a given province."""
+ from django_nepkit.constants import PLACEHOLDERS
- # Fallback: take the first non-internal parameter as province
- if not province:
- for key, value in request.GET.items():
- if key not in ["ne", "en", "html"] and value:
- province = value
- break
-
- if not province:
- return JsonResponse([], safe=False)
-
- default_lang = nepkit_settings.DEFAULT_LANGUAGE
- ne_param = request.GET.get("ne")
- ne = ne_param.lower() == "true" if ne_param else default_lang == "ne"
-
- en_param = request.GET.get("en")
- en = en_param.lower() == "true" if en_param else not ne
-
- as_html = (
- request.GET.get("html", "false").lower() == "true"
- or request.headers.get("HX-Request") == "true"
+ return _location_list_view(
+ request,
+ param_name="province",
+ data_func=get_districts_by_province,
+ placeholders=(PLACEHOLDERS["district"]["ne"], PLACEHOLDERS["district"]["en"]),
)
- data = get_districts_by_province(province, ne=ne, en=en)
-
- if as_html:
- placeholder = "जिल्ला छान्नुहोस्" if ne else "Select District"
- return _render_options(data, placeholder)
-
- return JsonResponse(data, safe=False)
-
def municipality_list_view(request):
- district = request.GET.get("district")
+ """Return list of municipalities for a given district."""
+ from django_nepkit.constants import PLACEHOLDERS
- # Fallback: take the first non-internal parameter as district
- if not district:
- for key, value in request.GET.items():
- if key not in ["ne", "en", "html"] and value:
- district = value
- break
-
- if not district:
- return JsonResponse([], safe=False)
-
- default_lang = nepkit_settings.DEFAULT_LANGUAGE
- ne_param = request.GET.get("ne")
- ne = ne_param.lower() == "true" if ne_param else default_lang == "ne"
-
- en_param = request.GET.get("en")
- en = en_param.lower() == "true" if en_param else not ne
-
- as_html = (
- request.GET.get("html", "false").lower() == "true"
- or request.headers.get("HX-Request") == "true"
+ return _location_list_view(
+ request,
+ param_name="district",
+ data_func=get_municipalities_by_district,
+ placeholders=(
+ PLACEHOLDERS["municipality"]["ne"],
+ PLACEHOLDERS["municipality"]["en"],
+ ),
)
-
- data = get_municipalities_by_district(district, ne=ne, en=en)
-
- if as_html:
- placeholder = "नगरपालिका छान्नुहोस्" if ne else "Select Municipality"
- return _render_options(data, placeholder)
-
- return JsonResponse(data, safe=False)