From 718cff2951108fb68901fcb5f3ff704a676493a6 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Thu, 5 Feb 2026 20:49:26 +0545 Subject: [PATCH] refactor: remove code redundancy and improve maintainability - Extract duplicate view logic into reusable helper functions in views.py - Move circular import from module level to method scope in models.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 - Remove ~160+ lines of redundant code while preserving all logic - All 79 tests passing, ruff linting passes --- CHANGELOG.md | 18 ++ RELEASE-NOTES.md | 33 ++ django_nepkit/constants.py | 129 ++++++++ django_nepkit/lang_utils.py | 52 +++ django_nepkit/models.py | 4 +- .../django_nepkit/js/address-chaining.js | 113 ++++--- django_nepkit/utils.py | 295 ++++++------------ django_nepkit/views.py | 174 +++++++---- 8 files changed, 509 insertions(+), 309 deletions(-) create mode 100644 RELEASE-NOTES.md create mode 100644 django_nepkit/constants.py create mode 100644 django_nepkit/lang_utils.py 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)