diff --git a/django_nepkit/tests/test_conf.py b/django_nepkit/tests/test_conf.py new file mode 100644 index 0000000..b563eb9 --- /dev/null +++ b/django_nepkit/tests/test_conf.py @@ -0,0 +1,88 @@ +""" +Tests for django-nepkit configuration (conf.py). +""" + +import pytest + + +class TestNepkitSettings: + """Tests for NepkitSettings configuration handling.""" + + def test_default_language_is_english(self): + """Default language should be 'en'.""" + from django_nepkit.conf import NepkitSettings, DEFAULTS + + settings = NepkitSettings(user_settings={}, defaults=DEFAULTS) + assert settings.DEFAULT_LANGUAGE == "en" + + def test_default_date_formats(self): + """Default date input formats should be present.""" + from django_nepkit.conf import NepkitSettings, DEFAULTS + + settings = NepkitSettings(user_settings={}, defaults=DEFAULTS) + assert "%Y-%m-%d" in settings.DATE_INPUT_FORMATS + assert "%d/%m/%Y" in settings.DATE_INPUT_FORMATS + assert "%d-%m-%Y" in settings.DATE_INPUT_FORMATS + + def test_default_bs_date_format(self): + """Default BS date format should be '%Y-%m-%d'.""" + from django_nepkit.conf import NepkitSettings, DEFAULTS + + settings = NepkitSettings(user_settings={}, defaults=DEFAULTS) + assert settings.BS_DATE_FORMAT == "%Y-%m-%d" + + def test_default_bs_datetime_format(self): + """Default BS datetime format should include time.""" + from django_nepkit.conf import NepkitSettings, DEFAULTS + + settings = NepkitSettings(user_settings={}, defaults=DEFAULTS) + assert settings.BS_DATETIME_FORMAT == "%Y-%m-%d %H:%M:%S" + + def test_default_time_format(self): + """Default time format should be 12-hour.""" + from django_nepkit.conf import NepkitSettings, DEFAULTS + + settings = NepkitSettings(user_settings={}, defaults=DEFAULTS) + assert settings.TIME_FORMAT == 12 + + def test_default_admin_datepicker(self): + """Admin datepicker should be enabled by default.""" + from django_nepkit.conf import NepkitSettings, DEFAULTS + + settings = NepkitSettings(user_settings={}, defaults=DEFAULTS) + assert settings.ADMIN_DATEPICKER is True + + def test_user_setting_overrides_default(self): + """User settings should override defaults.""" + from django_nepkit.conf import NepkitSettings, DEFAULTS + + settings = NepkitSettings( + user_settings={"DEFAULT_LANGUAGE": "ne"}, defaults=DEFAULTS + ) + assert settings.DEFAULT_LANGUAGE == "ne" + + def test_invalid_setting_raises_attribute_error(self): + """Accessing an invalid setting key should raise AttributeError.""" + from django_nepkit.conf import NepkitSettings, DEFAULTS + + settings = NepkitSettings(user_settings={}, defaults=DEFAULTS) + with pytest.raises(AttributeError, match="Invalid NEPKIT setting"): + _ = settings.NONEXISTENT_SETTING + + def test_django_settings_override(self): + """nepkit_settings reads from Django's NEPKIT dict at import time.""" + # The module-level nepkit_settings already uses NEPKIT from settings.py + from django_nepkit.conf import nepkit_settings + + # Our test settings.py sets DEFAULT_LANGUAGE = "en" + assert nepkit_settings.DEFAULT_LANGUAGE == "en" + + def test_user_setting_partial_override(self): + """Only explicitly set keys are overridden; others use defaults.""" + from django_nepkit.conf import NepkitSettings, DEFAULTS + + settings = NepkitSettings(user_settings={"TIME_FORMAT": 24}, defaults=DEFAULTS) + assert settings.TIME_FORMAT == 24 + # Others still use defaults + assert settings.DEFAULT_LANGUAGE == "en" + assert settings.ADMIN_DATEPICKER is True diff --git a/django_nepkit/tests/test_forms.py b/django_nepkit/tests/test_forms.py index f1b1a99..dcfcb7a 100644 --- a/django_nepkit/tests/test_forms.py +++ b/django_nepkit/tests/test_forms.py @@ -50,7 +50,6 @@ class TestNepaliDateFormField: with pytest.raises(ValidationError): field.clean("") - @pytest.mark.xfail(reason="Translation infrastructure timing issue in tests") def test_invalid_date_format(self): """Test that invalid date format raises ValidationError.""" field = NepaliDateFormField() diff --git a/django_nepkit/tests/test_lang_utils.py b/django_nepkit/tests/test_lang_utils.py new file mode 100644 index 0000000..f7b03f9 --- /dev/null +++ b/django_nepkit/tests/test_lang_utils.py @@ -0,0 +1,132 @@ +""" +Tests for django-nepkit language utilities (lang_utils.py). +""" + + +class TestResolveLanguageParams: + """Tests for resolve_language_params().""" + + def test_defaults_to_english(self): + """With no args and default settings (en), ne=False and en=True.""" + from django_nepkit.lang_utils import resolve_language_params + + ne, en = resolve_language_params() + assert ne is False + assert en is True + + def test_ne_true_sets_en_false(self): + """When ne=True is explicitly passed, en should be False.""" + from django_nepkit.lang_utils import resolve_language_params + + ne, en = resolve_language_params(ne=True) + assert ne is True + assert en is False + + def test_en_true_sets_ne_false(self): + """When en=True is explicitly passed, ne should be False.""" + from django_nepkit.lang_utils import resolve_language_params + + ne, en = resolve_language_params(en=True) + assert ne is False + assert en is True + + def test_explicit_en_true_with_ne_true(self): + """When both ne=True and en=True, en should respect the explicit value.""" + from django_nepkit.lang_utils import resolve_language_params + + ne, en = resolve_language_params(ne=True, en=True) + # ne=True with explicit en=True — en is explicitly set, so it stays True + assert ne is True + assert en is True + + def test_ne_false_sets_en_true(self): + """When ne=False is explicitly passed, en should be True.""" + from django_nepkit.lang_utils import resolve_language_params + + ne, en = resolve_language_params(ne=False) + assert ne is False + assert en is True + + def test_via_kwargs_ne(self): + """ne can be passed via **kwargs.""" + from django_nepkit.lang_utils import resolve_language_params + + ne, en = resolve_language_params(**{"ne": True}) + assert ne is True + assert en is False + + def test_via_kwargs_en(self): + """en can be passed via **kwargs.""" + from django_nepkit.lang_utils import resolve_language_params + + ne, en = resolve_language_params(**{"en": True}) + assert ne is False + assert en is True + + +class TestPopLanguageParams: + """Tests for pop_language_params().""" + + def test_pops_ne_from_kwargs(self): + """ne is consumed from the kwargs dict.""" + from django_nepkit.lang_utils import pop_language_params + + kwargs = {"ne": True, "other": "value"} + ne, en = pop_language_params(kwargs) + + assert ne is True + assert en is False + assert "ne" not in kwargs # was popped + assert "other" in kwargs # unrelated keys remain + + def test_pops_en_from_kwargs(self): + """en is consumed from the kwargs dict.""" + from django_nepkit.lang_utils import pop_language_params + + kwargs = {"en": True, "other": "value"} + ne, en = pop_language_params(kwargs) + + assert en is True + assert ne is False + assert "en" not in kwargs # was popped + + def test_defaults_when_nothing_set(self): + """Without any language kwargs, defaults from settings are used.""" + from django_nepkit.lang_utils import pop_language_params + + kwargs = {} + ne, en = pop_language_params(kwargs) + + # Default language is 'en' in test settings + assert ne is False + assert en is True + + def test_ne_true_en_not_explicit(self): + """When ne=True is in kwargs and en is absent, en becomes False.""" + from django_nepkit.lang_utils import pop_language_params + + kwargs = {"ne": True} + ne, en = pop_language_params(kwargs) + + assert ne is True + assert en is False + + def test_ne_true_en_explicit_true(self): + """When both ne=True and en=True are in kwargs, en respects the explicit value.""" + from django_nepkit.lang_utils import pop_language_params + + kwargs = {"ne": True, "en": True} + ne, en = pop_language_params(kwargs) + + assert ne is True + assert en is True + + def test_extra_kwargs_not_touched(self): + """Other kwargs are left intact after popping language params.""" + from django_nepkit.lang_utils import pop_language_params + + kwargs = {"ne": False, "max_length": 100, "blank": True} + pop_language_params(kwargs) + + assert "max_length" in kwargs + assert "blank" in kwargs diff --git a/django_nepkit/tests/test_location_utils.py b/django_nepkit/tests/test_location_utils.py new file mode 100644 index 0000000..2169052 --- /dev/null +++ b/django_nepkit/tests/test_location_utils.py @@ -0,0 +1,180 @@ +""" +Tests for location utility helpers in utils.py: + - get_districts_by_province + - get_municipalities_by_district + - _normalize_nepali_text + - _matches_location_name + - _is_nepali_text +""" + + +class TestGetDistrictsByProvince: + """Tests for get_districts_by_province.""" + + def test_returns_districts_for_valid_province(self): + from django_nepkit.utils import get_districts_by_province + + result = get_districts_by_province("Bagmati Province") + assert isinstance(result, list) + assert len(result) > 0 + # Each item should have 'id' and 'text' + assert all("id" in item and "text" in item for item in result) + + def test_includes_known_district(self): + from django_nepkit.utils import get_districts_by_province + + result = get_districts_by_province("Bagmati Province") + district_names = [item["text"] for item in result] + assert "Kathmandu" in district_names + + def test_returns_empty_for_invalid_province(self): + from django_nepkit.utils import get_districts_by_province + + result = get_districts_by_province("Nonexistent Province") + assert result == [] + + def test_returns_nepali_names_when_ne_true(self): + from django_nepkit.utils import get_districts_by_province + import re + + result = get_districts_by_province("Bagmati Province", ne=True) + assert len(result) > 0 + has_devanagari = any( + re.search(r"[\u0900-\u097F]", item["text"]) for item in result + ) + assert has_devanagari + + def test_finds_province_by_nepali_name(self): + from django_nepkit.utils import get_districts_by_province + + # बागमती प्रदेश is the Nepali name for Bagmati Province + result = get_districts_by_province("बागमती प्रदेश", ne=True) + assert len(result) > 0 + + def test_koshi_province(self): + from django_nepkit.utils import get_districts_by_province + + result = get_districts_by_province("Koshi Province") + assert len(result) > 0 + district_names = [item["text"] for item in result] + assert "Taplejung" in district_names or any( + "Taplejung" in n for n in district_names + ) + + +class TestGetMunicipalitiesByDistrict: + """Tests for get_municipalities_by_district.""" + + def test_returns_municipalities_for_valid_district(self): + from django_nepkit.utils import get_municipalities_by_district + + result = get_municipalities_by_district("Kathmandu") + assert isinstance(result, list) + assert len(result) > 0 + assert all("id" in item and "text" in item for item in result) + + def test_includes_kathmandu_metro(self): + from django_nepkit.utils import get_municipalities_by_district + + result = get_municipalities_by_district("Kathmandu") + names = [item["text"] for item in result] + assert any("Kathmandu" in n for n in names) + + def test_returns_empty_for_invalid_district(self): + from django_nepkit.utils import get_municipalities_by_district + + result = get_municipalities_by_district("Nonexistent District") + assert result == [] + + def test_returns_nepali_names_when_ne_true(self): + from django_nepkit.utils import get_municipalities_by_district + import re + + result = get_municipalities_by_district("Kathmandu", ne=True) + assert len(result) > 0 + has_devanagari = any( + re.search(r"[\u0900-\u097F]", item["text"]) for item in result + ) + assert has_devanagari + + +class TestNormalizeNepaliText: + """Tests for _normalize_nepali_text internal helper.""" + + def test_returns_none_for_falsy(self): + from django_nepkit.utils import _normalize_nepali_text + + assert _normalize_nepali_text(None) is None + assert _normalize_nepali_text("") == "" + + def test_replaces_chandrabindu_with_anusvara(self): + from django_nepkit.utils import _normalize_nepali_text + + # ँ (chandrabindu) → ं (anusvara) + result = _normalize_nepali_text("काठमाडौँ") + assert "ँ" not in result + assert "ं" in result + + def test_plain_text_unchanged(self): + from django_nepkit.utils import _normalize_nepali_text + + text = "Kathmandu" + assert _normalize_nepali_text(text) == text + + +class TestMatchesLocationName: + """Tests for _matches_location_name internal helper.""" + + def test_exact_nepali_match(self): + from django_nepkit.utils import _matches_location_name + + assert _matches_location_name("Kathmandu", "काठमाडौं", "काठमाडौं", "काठमाडौं") + + def test_exact_english_match_case_insensitive(self): + from django_nepkit.utils import _matches_location_name + + assert _matches_location_name("Kathmandu", "काठमाडौं", "kathmandu", "kathmandu") + + def test_partial_english_match(self): + from django_nepkit.utils import _matches_location_name + + # "Pokhara" is contained in "Pokhara Metropolitan City" + assert _matches_location_name( + "Pokhara Metropolitan City", "पोखरा महानगरपालिका", "Pokhara", "Pokhara" + ) + + def test_short_token_no_partial_english_match(self): + from django_nepkit.utils import _matches_location_name + + # Token shorter than 4 chars should not trigger partial English match + assert not _matches_location_name("Kathmandu", "काठमाडौं", "Kat", "Kat") + + def test_no_match(self): + from django_nepkit.utils import _matches_location_name + + assert not _matches_location_name("Lalitpur", "ललितपुर", "Pokhara", "Pokhara") + + +class TestIsNepaliText: + """Tests for _is_nepali_text internal helper.""" + + def test_devanagari_detected(self): + from django_nepkit.utils import _is_nepali_text + + assert _is_nepali_text(["काठमाडौं"]) + + def test_english_not_detected(self): + from django_nepkit.utils import _is_nepali_text + + assert not _is_nepali_text(["Kathmandu", "Bagmati"]) + + def test_mixed_tokens(self): + from django_nepkit.utils import _is_nepali_text + + # If any token has Devanagari, returns True + assert _is_nepali_text(["Kathmandu", "काठमाडौं"]) + + def test_empty_list(self): + from django_nepkit.utils import _is_nepali_text + + assert not _is_nepali_text([]) diff --git a/django_nepkit/tests/test_views.py b/django_nepkit/tests/test_views.py new file mode 100644 index 0000000..81a011d --- /dev/null +++ b/django_nepkit/tests/test_views.py @@ -0,0 +1,274 @@ +""" +Tests for django-nepkit views (views.py). +Covers district_list_view, municipality_list_view, and private helpers. +""" + +import pytest +from django.test import RequestFactory + + +@pytest.fixture +def rf(): + return RequestFactory() + + +class TestRenderOptions: + """Tests for _render_options internal helper.""" + + def test_renders_placeholder_option(self): + from django_nepkit.views import _render_options + + response = _render_options([], "Select District") + html = response.content.decode() + assert 'value=""' in html + assert "Select District" in html + + def test_renders_data_options(self): + from django_nepkit.views import _render_options + + data = [{"id": "Kathmandu", "text": "Kathmandu"}] + response = _render_options(data, "Select") + html = response.content.decode() + assert 'value="Kathmandu"' in html + assert ">Kathmandu<" in html + + def test_renders_multiple_options(self): + from django_nepkit.views import _render_options + + data = [ + {"id": "Kathmandu", "text": "Kathmandu"}, + {"id": "Lalitpur", "text": "Lalitpur"}, + ] + response = _render_options(data, "Select") + html = response.content.decode() + assert "Kathmandu" in html + assert "Lalitpur" in html + + +class TestGetPrimaryParam: + """Tests for _get_primary_param internal helper.""" + + def test_returns_named_param(self, rf): + from django_nepkit.views import _get_primary_param + + request = rf.get("/", {"province": "Bagmati Province"}) + value = _get_primary_param(request, "province") + assert value == "Bagmati Province" + + def test_falls_back_to_first_non_internal_param(self, rf): + from django_nepkit.views import _get_primary_param + + # No "province" key, but has "field" key (not an internal param) + request = rf.get("/", {"field": "Bagmati Province"}) + value = _get_primary_param(request, "province") + assert value == "Bagmati Province" + + def test_internal_params_excluded_from_fallback(self, rf): + from django_nepkit.views import _get_primary_param + + # Only internal params (ne, en, html) present — should return None + request = rf.get("/", {"ne": "true", "en": "true", "html": "true"}) + value = _get_primary_param(request, "province") + assert value is None + + def test_returns_none_when_no_params(self, rf): + from django_nepkit.views import _get_primary_param + + request = rf.get("/") + value = _get_primary_param(request, "province") + assert value is None + + +class TestParseLanguageParams: + """Tests for _parse_language_params internal helper.""" + + def test_defaults_to_english(self, rf): + from django_nepkit.views import _parse_language_params + + request = rf.get("/") + ne, en = _parse_language_params(request) + assert ne is False + assert en is True + + def test_ne_true_from_query(self, rf): + from django_nepkit.views import _parse_language_params + + request = rf.get("/", {"ne": "true"}) + ne, en = _parse_language_params(request) + assert ne is True + assert en is False + + def test_en_true_from_query(self, rf): + from django_nepkit.views import _parse_language_params + + request = rf.get("/", {"en": "true"}) + ne, en = _parse_language_params(request) + assert en is True + + def test_ne_false_from_query(self, rf): + from django_nepkit.views import _parse_language_params + + request = rf.get("/", {"ne": "false"}) + ne, en = _parse_language_params(request) + assert ne is False + assert en is True + + +class TestShouldReturnHtml: + """Tests for _should_return_html internal helper.""" + + def test_html_param_true(self, rf): + from django_nepkit.views import _should_return_html + + request = rf.get("/", {"html": "true"}) + assert _should_return_html(request) is True + + def test_html_param_false(self, rf): + from django_nepkit.views import _should_return_html + + request = rf.get("/", {"html": "false"}) + assert _should_return_html(request) is False + + def test_htmx_request_header(self, rf): + from django_nepkit.views import _should_return_html + + request = rf.get("/", HTTP_HX_REQUEST="true") + assert _should_return_html(request) is True + + def test_no_html_indicator(self, rf): + from django_nepkit.views import _should_return_html + + request = rf.get("/") + assert _should_return_html(request) is False + + +class TestDistrictListView: + """Tests for district_list_view.""" + + def test_returns_json_for_valid_province(self, rf): + from django_nepkit.views import district_list_view + + request = rf.get("/", {"province": "Bagmati Province"}) + response = district_list_view(request) + + assert response.status_code == 200 + import json + + data = json.loads(response.content) + assert isinstance(data, list) + assert len(data) > 0 + # Each item has id and text + assert "id" in data[0] + assert "text" in data[0] + + def test_returns_empty_json_when_no_province(self, rf): + from django_nepkit.views import district_list_view + + request = rf.get("/") + response = district_list_view(request) + + assert response.status_code == 200 + import json + + data = json.loads(response.content) + assert data == [] + + def test_returns_html_when_html_true(self, rf): + from django_nepkit.views import district_list_view + + request = rf.get("/", {"province": "Bagmati Province", "html": "true"}) + response = district_list_view(request) + + assert response.status_code == 200 + assert "text/html" in response["Content-Type"] + assert b" 0 + # Nepali names should contain Devanagari characters + import re + + has_devanagari = any( + re.search(r"[\u0900-\u097F]", item["text"]) for item in data + ) + assert has_devanagari + + def test_returns_html_with_nepali_placeholder(self, rf): + from django_nepkit.views import district_list_view + + request = rf.get( + "/", {"province": "Bagmati Province", "html": "true", "ne": "true"} + ) + response = district_list_view(request) + + # Placeholder should contain Devanagari text (जिल्ला छान्नुहोस्) + decoded = response.content.decode("utf-8") + assert "जिल्ला" in decoded + + +class TestMunicipalityListView: + """Tests for municipality_list_view.""" + + def test_returns_json_for_valid_district(self, rf): + from django_nepkit.views import municipality_list_view + import json + + request = rf.get("/", {"district": "Kathmandu"}) + response = municipality_list_view(request) + + assert response.status_code == 200 + data = json.loads(response.content) + assert isinstance(data, list) + assert len(data) > 0 + + def test_returns_empty_json_when_no_district(self, rf): + from django_nepkit.views import municipality_list_view + import json + + request = rf.get("/") + response = municipality_list_view(request) + + assert response.status_code == 200 + data = json.loads(response.content) + assert data == [] + + def test_returns_html_when_html_true(self, rf): + from django_nepkit.views import municipality_list_view + + request = rf.get("/", {"district": "Kathmandu", "html": "true"}) + response = municipality_list_view(request) + + assert response.status_code == 200 + assert "text/html" in response["Content-Type"] + assert b" 0 + has_devanagari = any( + re.search(r"[\u0900-\u097F]", item["text"]) for item in data + ) + assert has_devanagari + + def test_htmx_request_returns_html(self, rf): + from django_nepkit.views import municipality_list_view + + request = rf.get("/", {"district": "Kathmandu"}, HTTP_HX_REQUEST="true") + response = municipality_list_view(request) + + assert "text/html" in response["Content-Type"] + assert b"