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
This commit is contained in:
8 files changed
+509
-309
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+97
-198
@@ -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:
|
||||
|
||||
+114
-60
@@ -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'<option value="">{placeholder}</option>']
|
||||
for item in data:
|
||||
options.append(f'<option value="{item["id"]}">{item["text"]}</option>')
|
||||
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)
|
||||
Reference in new issue
Block a user