- 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
302 lines
8.9 KiB
Python
302 lines
8.9 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any, Optional
|
|
|
|
from nepali.datetime import nepalidate, nepalidatetime
|
|
from nepali.locations import districts, municipalities, provinces
|
|
|
|
from django_nepkit.conf import nepkit_settings
|
|
|
|
BS_DATE_FORMAT = nepkit_settings.BS_DATE_FORMAT
|
|
BS_DATETIME_FORMAT = nepkit_settings.BS_DATETIME_FORMAT
|
|
|
|
|
|
def _try_parse_nepali(value: Any, cls: Any, fallback_fmt: str) -> Any:
|
|
"""Helper to turn a string into a Nepali date object."""
|
|
if value in (None, ""):
|
|
return None
|
|
if isinstance(value, cls):
|
|
return value
|
|
if isinstance(value, str):
|
|
formats = nepkit_settings.DATE_INPUT_FORMATS
|
|
if fallback_fmt not in formats:
|
|
formats = list(formats) + [fallback_fmt]
|
|
|
|
for fmt in formats:
|
|
try:
|
|
return cls.strptime(value.strip(), fmt)
|
|
except Exception:
|
|
continue
|
|
return None
|
|
|
|
|
|
def try_parse_nepali_date(value: Any) -> Optional[nepalidate]:
|
|
"""Convert any value to a Nepali Date."""
|
|
return _try_parse_nepali(value, nepalidate, BS_DATE_FORMAT)
|
|
|
|
|
|
def try_parse_nepali_datetime(value: Any) -> Optional[nepalidatetime]:
|
|
"""Convert any value to a Nepali Date and Time."""
|
|
return _try_parse_nepali(value, nepalidatetime, BS_DATETIME_FORMAT)
|
|
|
|
|
|
def _get_location_children(parent_list, parent_name, child_attr, ne=False):
|
|
"""Find children (like districts) of a parent (like a province)."""
|
|
selected_parent = None
|
|
for p in parent_list:
|
|
p_name = p.name
|
|
p_name_ne = getattr(p, "name_nepali", None)
|
|
|
|
if p_name == parent_name or p_name_ne == parent_name:
|
|
selected_parent = p
|
|
break
|
|
|
|
if not selected_parent:
|
|
return []
|
|
|
|
children = getattr(selected_parent, child_attr, [])
|
|
|
|
if ne:
|
|
return [
|
|
{
|
|
"id": getattr(child, "name_nepali", child.name),
|
|
"text": getattr(child, "name_nepali", child.name),
|
|
}
|
|
for child in children
|
|
]
|
|
else:
|
|
return [{"id": child.name, "text": child.name} for child in children]
|
|
|
|
|
|
def get_districts_by_province(province_name, ne=False, en=True):
|
|
"""Get all districts for a province."""
|
|
# Logic note: if ne=True is passed, we shouldn't care about en=True (handled by caller typically)
|
|
return _get_location_children(provinces, province_name, "districts", ne=ne)
|
|
|
|
|
|
def get_municipalities_by_district(district_name, ne=False, en=True):
|
|
"""Get all municipalities for a district."""
|
|
return _get_location_children(districts, district_name, "municipalities", ne=ne)
|
|
|
|
|
|
def format_nepali_currency(
|
|
number: Any, currency_symbol: str = "Rs.", ne: bool = False
|
|
) -> str:
|
|
"""
|
|
Formats a number with Nepali-style commas and optional currency symbol.
|
|
Eg. 1234567 -> Rs. 12,34,567
|
|
"""
|
|
from nepali.number import add_comma, english_to_nepali
|
|
|
|
if number is None:
|
|
return ""
|
|
|
|
try:
|
|
# Convert to string and split by decimal point
|
|
num_str = f"{float(number):.2f}"
|
|
if "." in num_str:
|
|
integer_part, decimal_part = num_str.split(".")
|
|
else:
|
|
integer_part, decimal_part = num_str, ""
|
|
|
|
# Format integer part with commas
|
|
formatted_integer = add_comma(int(integer_part))
|
|
|
|
# Join back
|
|
res = formatted_integer
|
|
if decimal_part:
|
|
res = f"{res}.{decimal_part}"
|
|
|
|
if ne:
|
|
res = english_to_nepali(res)
|
|
|
|
if currency_symbol:
|
|
return f"{currency_symbol} {res}"
|
|
return res
|
|
except Exception:
|
|
return str(number)
|
|
|
|
|
|
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 ""
|
|
|
|
try:
|
|
num = int(float(number))
|
|
except (ValueError, TypeError):
|
|
return str(number)
|
|
|
|
if num == 0:
|
|
return "शून्य"
|
|
|
|
def _convert(n):
|
|
if n == 0:
|
|
return ""
|
|
if n < 100:
|
|
return NEPALI_ONES[n]
|
|
|
|
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 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}"
|
|
if remainder > 0:
|
|
res += f" {_convert(remainder)}"
|
|
return res.strip()
|
|
return ""
|
|
|
|
return _convert(num)
|
|
|
|
|
|
def english_to_nepali_unicode(text: Any) -> str:
|
|
"""
|
|
Converts English text/numbers to Nepali Unicode.
|
|
Currently focuses on numbers.
|
|
"""
|
|
from nepali.number import english_to_nepali
|
|
|
|
if text is None:
|
|
return ""
|
|
|
|
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.
|
|
Returns a dictionary with 'province', 'district', and 'municipality'.
|
|
"""
|
|
if not address_string:
|
|
return {"province": None, "district": None, "municipality": None}
|
|
|
|
result: dict[str, Optional[str]] = {
|
|
"province": None,
|
|
"district": None,
|
|
"municipality": None,
|
|
}
|
|
|
|
# Prepare tokens
|
|
content = address_string.replace(",", " ").replace("-", " ")
|
|
tokens = [t.strip() for t in content.split() if t.strip()]
|
|
normalized_tokens = [_normalize_nepali_text(t) for t in tokens]
|
|
|
|
# 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:
|
|
result["municipality"] = found_municipality.name
|
|
if not found_district:
|
|
found_district = found_municipality.district
|
|
if not found_province:
|
|
found_province = found_municipality.province
|
|
|
|
if found_district:
|
|
result["district"] = found_district.name
|
|
if not found_province:
|
|
found_province = found_district.province
|
|
|
|
if found_province:
|
|
result["province"] = found_province.name
|
|
|
|
# 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:
|
|
result["district"] = found_district.name_nepali
|
|
if found_province:
|
|
result["province"] = found_province.name_nepali
|
|
|
|
return result
|