diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..c6fa23d --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,26 @@ +name: pre-commit + +on: [push, pull_request] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + cache-dependency-glob: "uv.lock" + + - name: Set up Python + run: uv python install 3.10 + + - name: Install dependencies + run: uv sync --all-extras --dev + + - name: Run pre-commit + run: uv run pre-commit run --all-files --show-diff-on-failure diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..b662e57 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,27 @@ +name: Publish to PyPI +on: + release: + types: [published] +permissions: + contents: read +jobs: + deploy: + runs-on: ubuntu-latest + environment: release + permissions: + id-token: write # IMPORTANT: this permission is mandatory for trusted publishing + steps: + - uses: actions/checkout@v6 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.10' + cache: 'pip' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build twine + - name: Build package + run: python -m build + - name: Publish package to PyPI + run: python -m twine upload dist/* --verbose diff --git a/.gitignore b/.gitignore index b7faf40..09be0ef 100644 --- a/.gitignore +++ b/.gitignore @@ -182,9 +182,9 @@ cython_debug/ .abstra/ # Visual Studio Code -# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore -# and can be added to the global gitignore or merged into this file. However, if you prefer, +# and can be added to the global gitignore or merged into this file. However, if you prefer, # you could uncomment the following to ignore the entire vscode folder # .vscode/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..4fa1cec --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,17 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-yaml + - id: check-toml + - id: end-of-file-fixer + - id: trailing-whitespace + - id: check-added-large-files + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.14 + hooks: + - id: ruff + args: [--fix] + - id: ruff-format + diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..c4772e1 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include LICENSE +include README.md +recursive-include django_nepkit/static * diff --git a/README.md b/README.md index e69de29..5b60772 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,13 @@ +## Installation + +```bash +pip install django-nepkit +``` + +### Optional: Django REST Framework support + +If you want serializer fields from `django_nepkit.serializers`, install the extra: + +```bash +pip install "django-nepkit[drf]" +``` diff --git a/Untitled b/Untitled deleted file mode 100644 index 0036a18..0000000 --- a/Untitled +++ /dev/null @@ -1,155 +0,0 @@ -from django.contrib import admin -from django.utils.translation import gettext_lazy as _ -from nepali.datetime import nepalidate, nepalidatetime - -from django_nepkit.models import NepaliDateField - - -def format_nepali_date(date_value, format_string="%B %d, %Y"): - """ - Format a nepalidate object with Nepali month names. - - Args: - date_value: A nepalidate object or string in YYYY-MM-DD format - format_string: strftime format string (default: '%B %d, %Y') - %B = Full month name (Baishak, Jestha, etc.) - %b = Short month name - %d = Day of month - %Y = Year - - Returns: - Formatted date string with Nepali month names, or empty string if invalid - """ - - if date_value is None: - return "" - - try: - if isinstance(date_value, str): - date_value = nepalidate.strptime(date_value, "%Y-%m-%d") - - if isinstance(date_value, nepalidate): - return date_value.strftime(format_string) - except (ValueError, TypeError, AttributeError): - pass - - return str(date_value) if date_value else "" - - -def format_nepali_datetime(datetime_value, format_string="%B %d, %Y %I:%M %p"): - """ - Format a nepalidatetime object with Nepali month names. - - Default output uses **12-hour time with AM/PM**. - """ - if datetime_value is None: - return "" - - try: - if isinstance(datetime_value, str): - datetime_value = nepalidatetime.strptime( - datetime_value, "%Y-%m-%d %H:%M:%S" - ) - - if isinstance(datetime_value, nepalidatetime): - return datetime_value.strftime(format_string) - except (ValueError, TypeError, AttributeError): - pass - - return str(datetime_value) if datetime_value else "" - - -class NepaliDateFilter(admin.FieldListFilter): - """ - A list filter for NepaliDateField to filter by BS Year. - """ - - def __init__(self, field, request, params, model, model_admin, field_path): - self.parameter_name = f"{field_path}_bs_year" - super().__init__(field, request, params, model, model_admin, field_path) - self.title = _("Nepali Date (Year)") - - def expected_parameters(self): - return [self.parameter_name] - - def choices(self, changelist): - yield { - "selected": self.used_parameters.get(self.parameter_name) is None, - "query_string": changelist.get_query_string(remove=[self.parameter_name]), - "display": _("All"), - } - current_year = nepalidate.today().year - for year in range(current_year - 10, current_year + 2): - yield { - "selected": self.used_parameters.get(self.parameter_name) == str(year), - "query_string": changelist.get_query_string( - {self.parameter_name: str(year)} - ), - "display": str(year), - } - - def queryset(self, request, queryset): - value = self.used_parameters.get(self.parameter_name) - if value: - year = int(value) - # Convert BS year range to AD date range - start_date_bs = nepalidate(year, 1, 1) - # Find last day of the year. Some BS years end on the 30th, - # so we try 31 first and then fall back to 30 if that date - # is not valid. - try: - end_date_bs = nepalidate(year, 12, 31) - except ValueError: - end_date_bs = nepalidate(year, 12, 30) - - start_date_ad = start_date_bs.to_date() - end_date_ad = end_date_bs.to_date() - - return queryset.filter( - **{f"{self.field_path}__range": (start_date_ad, end_date_ad)} - ) - return queryset - - -admin.FieldListFilter.register( - lambda f: isinstance(f, NepaliDateField), NepaliDateFilter, take_priority=True -) - - -class NepaliAdminMixin: - """ - Mixin for Django admin classes that provides Nepali date utilities. - Makes format_nepali_date and NepaliDateFilter available without explicit imports. - """ - - def format_nepali_date(self, date_value, format_string="%B %d, %Y"): - """ - Format a nepalidate object with Nepali month names. - Available as a method on admin classes using this mixin. - """ - return format_nepali_date(date_value, format_string) - - def format_nepali_datetime( - self, datetime_value, format_string="%B %d, %Y %I:%M %p" - ): - return format_nepali_datetime(datetime_value, format_string) - - -class NepaliModelAdmin(NepaliAdminMixin, admin.ModelAdmin): - """ - Base ModelAdmin class with Nepali date utilities built-in. - Use this instead of admin.ModelAdmin to have format_nepali_date available. - - Example: - from django_nepkit import NepaliModelAdmin, NepaliDateFilter - - @admin.register(MyModel) - class MyModelAdmin(NepaliModelAdmin): - list_filter = (('date_field', NepaliDateFilter),) - - def display_date(self, obj): - return self.format_nepali_date(obj.date_field) - """ - - # Make NepaliDateFilter available as a class attribute - NepaliDateFilter = NepaliDateFilter diff --git a/django_nepkit/__init__.py b/django_nepkit/__init__.py index e69de29..6c24723 100644 --- a/django_nepkit/__init__.py +++ b/django_nepkit/__init__.py @@ -0,0 +1,31 @@ +from .models import ( + NepaliDateField, + NepaliTimeField, + NepaliDateTimeField, + NepaliPhoneNumberField, + ProvinceField, + DistrictField, + MunicipalityField, +) +from .admin import ( + NepaliDateFilter, + format_nepali_date, + format_nepali_datetime, + NepaliModelAdmin, + NepaliAdminMixin, +) + +__all__ = [ + "NepaliDateField", + "NepaliTimeField", + "NepaliDateTimeField", + "NepaliPhoneNumberField", + "ProvinceField", + "DistrictField", + "MunicipalityField", + "NepaliDateFilter", + "format_nepali_date", + "format_nepali_datetime", + "NepaliModelAdmin", + "NepaliAdminMixin", +] diff --git a/django_nepkit/admin.py b/django_nepkit/admin.py index 6997967..a92b4ac 100644 --- a/django_nepkit/admin.py +++ b/django_nepkit/admin.py @@ -1,143 +1,122 @@ from django.contrib import admin from django.utils.translation import gettext_lazy as _ - from nepali.datetime import nepalidate, nepalidatetime from django_nepkit.models import NepaliDateField +from django_nepkit.utils import ( + try_parse_nepali_date, + try_parse_nepali_datetime, +) -# -------------------------------------------------- -# Internal helpers -# -------------------------------------------------- - -def _parse_nepali_date(value): +def format_nepali_date(date_value, format_string="%B %d, %Y"): """ - Normalize string / nepalidate to nepalidate. + Format a nepalidate object with Nepali month names. + + Args: + date_value: A nepalidate object or string in YYYY-MM-DD format + format_string: strftime format string (default: '%B %d, %Y') + %B = Full month name (Baishak, Jestha, etc.) + %b = Short month name + %d = Day of month + %Y = Year + + Returns: + Formatted date string with Nepali month names, or empty string if invalid """ - if value is None: - return None + if date_value is None: + return "" - if isinstance(value, nepalidate): - return value + try: + parsed = try_parse_nepali_date(date_value) + if parsed is not None: + return parsed.strftime(format_string) + if isinstance(date_value, nepalidate): + return date_value.strftime( + format_string + ) # defensive; should be covered above + except (ValueError, TypeError, AttributeError): + pass - if isinstance(value, str): - try: - return nepalidate.strptime(value, "%Y-%m-%d") - except (ValueError, TypeError): - return None - - return None + return str(date_value) if date_value else "" -def _parse_nepali_datetime(value): +def format_nepali_datetime(datetime_value, format_string="%B %d, %Y %I:%M %p"): """ - Normalize string / nepalidatetime to nepalidatetime. + Format a nepalidatetime object with Nepali month names. + + Default output uses **12-hour time with AM/PM**. """ - if value is None: - return None + if datetime_value is None: + return "" - if isinstance(value, nepalidatetime): - return value + try: + parsed = try_parse_nepali_datetime(datetime_value) + if parsed is not None: + return parsed.strftime(format_string) + if isinstance(datetime_value, nepalidatetime): + return datetime_value.strftime( + format_string + ) # defensive; should be covered above + except (ValueError, TypeError, AttributeError): + pass - if isinstance(value, str): - try: - return nepalidatetime.strptime(value, "%Y-%m-%d %H:%M:%S") - except (ValueError, TypeError): - return None + return str(datetime_value) if datetime_value else "" - return None - - -# -------------------------------------------------- -# Formatting helpers -# -------------------------------------------------- - -def format_nepali_date(value, format_string="%B %d, %Y"): - """ - Format a Nepali (BS) date with Nepali month names. - """ - nd = _parse_nepali_date(value) - return nd.strftime(format_string) if nd else "" - - -def format_nepali_datetime(value, format_string="%B %d, %Y %I:%M %p"): - """ - Format a Nepali (BS) datetime (12-hour format by default). - """ - ndt = _parse_nepali_datetime(value) - return ndt.strftime(format_string) if ndt else "" - - -# -------------------------------------------------- -# Admin List Filter (BS Year) -# -------------------------------------------------- class NepaliDateFilter(admin.FieldListFilter): """ - Admin list filter for NepaliDateField by BS year. + A list filter for NepaliDateField to filter by BS Year. """ - title = _("Nepali Date (Year)") - def __init__(self, field, request, params, model, model_admin, field_path): - self.field_path = field_path self.parameter_name = f"{field_path}_bs_year" super().__init__(field, request, params, model, model_admin, field_path) + self.title = _("Nepali Date (Year)") def expected_parameters(self): return [self.parameter_name] - def _year_choices(self): - current_year = nepalidate.today().year - return range(current_year - 10, current_year + 2) - def choices(self, changelist): yield { - "selected": self.parameter_name not in self.used_parameters, + "selected": self.used_parameters.get(self.parameter_name) is None, "query_string": changelist.get_query_string(remove=[self.parameter_name]), "display": _("All"), } - - for year in self._year_choices(): + current_year = nepalidate.today().year + for year in range(current_year - 10, current_year + 2): yield { "selected": self.used_parameters.get(self.parameter_name) == str(year), "query_string": changelist.get_query_string( - {self.parameter_name: year} + {self.parameter_name: str(year)} ), - "display": year, + "display": str(year), } def queryset(self, request, queryset): value = self.used_parameters.get(self.parameter_name) - if not value: - return queryset - - try: + if value: year = int(value) - except (TypeError, ValueError): - return queryset - - start_bs = nepalidate(year, 1, 1) - - # BS year length varies (30/31 days) - for last_day in (31, 30): + # Convert BS year range to AD date range + start_date_bs = nepalidate(year, 1, 1) + # Find last day of the year. Some BS years end on the 30th, + # so we try 31 first and then fall back to 30 if that date + # is not valid. try: - end_bs = nepalidate(year, 12, last_day) - break + end_date_bs = nepalidate(year, 12, 31) except ValueError: - continue + end_date_bs = nepalidate(year, 12, 30) - return queryset.filter( - **{ - f"{self.field_path}__range": ( - start_bs.to_date(), - end_bs.to_date(), - ) - } - ) + start_date_ad = start_date_bs.to_date() + end_date_ad = end_date_bs.to_date() + + return queryset.filter( + **{f"{self.field_path}__range": (start_date_ad, end_date_ad)} + ) + return queryset -# Register filter automatically for NepaliDateField +# Register NepaliDateFilter as the default list filter for NepaliDateField admin.FieldListFilter.register( lambda f: isinstance(f, NepaliDateField), NepaliDateFilter, @@ -145,30 +124,88 @@ admin.FieldListFilter.register( ) -# -------------------------------------------------- -# Admin mixins -# -------------------------------------------------- - class NepaliAdminMixin: """ - Adds Nepali date formatting helpers to admin classes. + Mixin for Django admin classes that provides Nepali date utilities. + Makes format_nepali_date and NepaliDateFilter available without explicit imports. """ - format_nepali_date = staticmethod(format_nepali_date) - format_nepali_datetime = staticmethod(format_nepali_datetime) + def format_nepali_date(self, date_value, format_string="%B %d, %Y"): + """ + Format a nepalidate object with Nepali month names. + Available as a method on admin classes using this mixin. + """ + return format_nepali_date(date_value, format_string) + + def format_nepali_datetime( + self, datetime_value, format_string="%B %d, %Y %I:%M %p" + ): + return format_nepali_datetime(datetime_value, format_string) class NepaliModelAdmin(NepaliAdminMixin, admin.ModelAdmin): """ - Base ModelAdmin with Nepali utilities. + Base ModelAdmin class with Nepali date utilities built-in. + Use this instead of admin.ModelAdmin to have format_nepali_date available. Example: + from django_nepkit import NepaliModelAdmin, NepaliDateFilter + @admin.register(MyModel) class MyModelAdmin(NepaliModelAdmin): - list_filter = (('date_bs', NepaliDateFilter),) + list_filter = (('date_field', NepaliDateFilter),) - def date_display(self, obj): - return self.format_nepali_date(obj.date_bs) + def display_date(self, obj): + return self.format_nepali_date(obj.date_field) """ + # Make NepaliDateFilter available as a class attribute NepaliDateFilter = NepaliDateFilter + + # Ensure admin forms render Nepali fields with the proper widget, + # even if a project doesn't provide custom ModelForms. + def formfield_for_dbfield(self, db_field, request, **kwargs): + """ + Force Nepali widgets in Django admin without requiring user forms. + """ + try: + from django_nepkit.models import NepaliDateField, NepaliDateTimeField + from django_nepkit.widgets import NepaliDatePickerWidget + except Exception: + return super().formfield_for_dbfield(db_field, request, **kwargs) + + if isinstance(db_field, (NepaliDateField, NepaliDateTimeField)): + kwargs.setdefault("widget", NepaliDatePickerWidget) + + return super().formfield_for_dbfield(db_field, request, **kwargs) + + class Media: + """ + Django admin ships jQuery as `django.jQuery` (not `window.jQuery`). + The Nepali date picker library expects a global `jQuery`, so we bridge it. + """ + + css = { + "all": ( + "https://nepalidatepicker.sajanmaharjan.com.np/v5/nepali.datepicker/css/nepali.datepicker.v5.0.6.min.css", + "django_nepkit/css/admin-nepali-datepicker.css", + ) + } + js = ( + # Bridge admin's `django.jQuery` -> `window.jQuery` + "django_nepkit/js/admin-jquery-bridge.js", + # Date picker lib + "https://nepalidatepicker.sajanmaharjan.com.np/v5/nepali.datepicker/js/nepali.datepicker.v5.0.6.min.js", + # Init + "django_nepkit/js/nepali-datepicker-init.js", + ) + + +# Exporting for easy usage +__all__ = [ + "NepaliDateFilter", + "format_nepali_date", + "format_nepali_datetime", + "NepaliAdminMixin", + "NepaliModelAdmin", +] diff --git a/django_nepkit/forms.py b/django_nepkit/forms.py index 2fc0639..823c64c 100644 --- a/django_nepkit/forms.py +++ b/django_nepkit/forms.py @@ -2,76 +2,49 @@ from datetime import date as python_date from django import forms from django.utils.translation import gettext_lazy as _ - from nepali.datetime import nepalidate + +from django_nepkit.utils import try_parse_nepali_date from django_nepkit.validators import validate_nepali_phone_number -from django_nepkit.widgets import NepaliDatePickerWidget -# -------------------------------------------------- -# Nepali Phone Number Form Field -# -------------------------------------------------- - -class NepaliPhoneNumberField(forms.CharField): +class NepaliDateFormField(forms.DateField): """ - Form field for Nepali phone numbers. + A Django Form Field for Nepali Date (Bikram Sambat). """ - default_error_messages = { - "invalid": _("Enter a valid Nepali phone number."), - } - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.validators.append(validate_nepali_phone_number) - - -# -------------------------------------------------- -# Nepali Date (BS) Form Field -# -------------------------------------------------- - -class NepaliDateFormField(forms.Field): - """ - Form field for Nepali (Bikram Sambat) date. - - Accepts: - - YYYY-MM-DD (string) - - python date - - nepalidate instance - - Returns: - - nepalidate instance - """ + from .widgets import NepaliDatePickerWidget widget = NepaliDatePickerWidget - default_error_messages = { - "invalid": _("Enter a valid Nepali date in YYYY-MM-DD format."), - } + def __init__(self, *args, **kwargs): + kwargs.pop("max_length", None) + super().__init__(*args, **kwargs) def to_python(self, value): if value in self.empty_values: return None - if isinstance(value, nepalidate): return value - if isinstance(value, python_date): return nepalidate.from_date(value) + try: + parsed = try_parse_nepali_date(str(value)) + if parsed is not None: + return parsed + raise ValueError("Invalid BS date format") + except Exception: + raise forms.ValidationError( + _("Enter a valid Nepali date in YYYY-MM-DD format."), + code="invalid", + ) - if isinstance(value, str): - try: - return nepalidate.strptime(value.strip(), "%Y-%m-%d") - except (ValueError, TypeError): - self.fail("invalid") - self.fail("invalid") +class NepaliPhoneNumberFormField(forms.CharField): + """ + A Django Form Field for Nepali Phone Numbers. + """ - def validate(self, value): - """ - Override default validation to allow nepalidate objects. - """ - if value is None: - return - if not isinstance(value, nepalidate): - self.fail("invalid") + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.validators.append(validate_nepali_phone_number) diff --git a/django_nepkit/models.py b/django_nepkit/models.py index 51369a3..d3c9baf 100644 --- a/django_nepkit/models.py +++ b/django_nepkit/models.py @@ -3,24 +3,18 @@ from datetime import date as python_date from django.db import models 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.validators import validate_nepali_phone_number -from django_nepkit.widgets import ( - DistrictSelectWidget, - MunicipalitySelectWidget, - NepaliDatePickerWidget, - ProvinceSelectWidget, +from django_nepkit.utils import ( + BS_DATE_FORMAT, + BS_DATETIME_FORMAT, + try_parse_nepali_date, + try_parse_nepali_datetime, ) - -# -------------------------------------------------- -# Phone Number -# -------------------------------------------------- +from django_nepkit.validators import validate_nepali_phone_number class NepaliPhoneNumberField(models.CharField): - description = _("Nepali phone number") + description = _("Nepali Phone Number") def __init__(self, *args, **kwargs): kwargs.setdefault("max_length", 10) @@ -28,133 +22,77 @@ class NepaliPhoneNumberField(models.CharField): self.validators.append(validate_nepali_phone_number) -# -------------------------------------------------- -# Location Fields -# -------------------------------------------------- +class NepaliDateField(models.CharField): + """ + A Django Model Field that stores Nepali (Bikram Sambat) Date. + Internally it stores the date as BS string (YYYY-MM-DD) in the database. + """ - -class ProvinceField(models.CharField): - description = _("Nepal Province") + description = _("Nepali Date (Bikram Sambat)") def __init__(self, *args, **kwargs): - kwargs.setdefault("max_length", 100) - kwargs.setdefault( - "choices", - [(p.name, p.name) for p in provinces], - ) - super().__init__(*args, **kwargs) + self.auto_now = kwargs.pop("auto_now", False) + self.auto_now_add = kwargs.pop("auto_now_add", False) - def formfield(self, **kwargs): - kwargs.setdefault("widget", ProvinceSelectWidget) - return super().formfield(**kwargs) - - -class DistrictField(models.CharField): - description = _("Nepal District") - - def __init__(self, *args, **kwargs): - kwargs.setdefault("max_length", 100) - kwargs.setdefault( - "choices", - [(d.name, d.name) for d in districts], - ) - super().__init__(*args, **kwargs) - - def formfield(self, **kwargs): - kwargs.setdefault("widget", DistrictSelectWidget) - return super().formfield(**kwargs) - - -class MunicipalityField(models.CharField): - description = _("Nepal Municipality") - - def __init__(self, *args, **kwargs): - kwargs.setdefault("max_length", 100) - kwargs.setdefault( - "choices", - [(m.name, m.name) for m in municipalities], - ) - super().__init__(*args, **kwargs) - - def formfield(self, **kwargs): - kwargs.setdefault("widget", MunicipalitySelectWidget) - return super().formfield(**kwargs) - - -# -------------------------------------------------- -# Time Field -# -------------------------------------------------- -class NepaliTimeField(models.TimeField): - """ - Time field for Nepal (standard time). - This is essentially Django's TimeField, - kept for semantic clarity and future extensions. - """ - - description = _("Nepali Time") - - def formfield(self, **kwargs): - return super().formfield(**kwargs) - - -# -------------------------------------------------- -# Base Nepali BS Date / DateTime Field -# -------------------------------------------------- - - -class BaseNepaliDateCharField(models.CharField): - """ - Base class for Nepali (Bikram Sambat) Date / DateTime fields. - Stored as string in DB, returned as nepali date objects in Python. - """ - - format = None - nepali_type = None - - def __init__(self, *args, auto_now=False, auto_now_add=False, **kwargs): - self.auto_now = auto_now - self.auto_now_add = auto_now_add - - if auto_now or auto_now_add: + # Match Django's DateField behavior + if self.auto_now or self.auto_now_add: kwargs.setdefault("editable", False) kwargs.setdefault("blank", True) + kwargs.setdefault("max_length", 10) super().__init__(*args, **kwargs) - def _now(self): - raise NotImplementedError - def pre_save(self, model_instance, add): if self.auto_now or (self.auto_now_add and add): - value = self._now().strftime(self.format) + # Using nepalidate.today() to get current BS date. + # This is BS-native as much as possible. + value = nepalidate.today().strftime(BS_DATE_FORMAT) setattr(model_instance, self.attname, value) return value return super().pre_save(model_instance, add) - def to_python(self, value): - if value is None or isinstance(value, self.nepali_type): - return value - - if isinstance(value, python_date): - return self.nepali_type.from_date(value) - - if isinstance(value, str): - return self.nepali_type.strptime(value.strip(), self.format) - - return value - def from_db_value(self, value, expression, connection): - if value is None: + parsed = try_parse_nepali_date(value) + return parsed if parsed is not None else value + + def to_python(self, value): + if value is None or isinstance(value, nepalidate): return value - return self.nepali_type.strptime(value, self.format) + if isinstance(value, python_date): + # If we MUST handle AD date object, we convert it to BS. + # But we should prefer strings or nepalidate. + try: + return nepalidate.from_date(value) + except (ValueError, TypeError): + return str(value) + if isinstance(value, str): + parsed = try_parse_nepali_date(value) + return parsed if parsed is not None else value + return super().to_python(value) + + def validate(self, value, model_instance): + if isinstance(value, nepalidate): + value = value.strftime(BS_DATE_FORMAT) + super().validate(value, model_instance) + + def run_validators(self, value): + if isinstance(value, nepalidate): + value = value.strftime(BS_DATE_FORMAT) + super().run_validators(value) def get_prep_value(self, value): if value is None: - return None - - if isinstance(value, self.nepali_type): - return value.strftime(self.format) - + return value + if isinstance(value, nepalidate): + return value.strftime(BS_DATE_FORMAT) + if isinstance(value, python_date): + try: + return nepalidate.from_date(value).strftime(BS_DATE_FORMAT) + except (ValueError, TypeError): + return str(value) + if isinstance(value, str): + return value + # Fallback: convert to string return str(value) def deconstruct(self): @@ -165,47 +103,167 @@ class BaseNepaliDateCharField(models.CharField): kwargs["auto_now_add"] = True return name, path, args, kwargs - -# -------------------------------------------------- -# Nepali Date Field (BS) -# -------------------------------------------------- - - -class NepaliDateField(BaseNepaliDateCharField): - description = _("Nepali Date (Bikram Sambat)") - format = "%Y-%m-%d" - nepali_type = nepalidate - - def __init__(self, *args, **kwargs): - kwargs.setdefault("max_length", 10) - super().__init__(*args, **kwargs) - - def _now(self): - return nepalidate.today() - def formfield(self, **kwargs): - kwargs.setdefault("form_class", NepaliDateFormField) - kwargs.setdefault("widget", NepaliDatePickerWidget) - return super().formfield(**kwargs) + from .forms import NepaliDateFormField + from .widgets import NepaliDatePickerWidget + + defaults = { + "form_class": NepaliDateFormField, + "widget": NepaliDatePickerWidget, + } + defaults.update(kwargs) + return super().formfield(**defaults) -# -------------------------------------------------- -# Nepali DateTime Field (BS) -# -------------------------------------------------- +class NepaliTimeField(models.TimeField): + """ + A Django Model Field for Time with Nepali localization support. + Supports auto_now and auto_now_add like standard Django TimeField. + """ + + description = _("Nepali Time") -class NepaliDateTimeField(BaseNepaliDateCharField): +class NepaliDateTimeField(models.CharField): + """ + A Django Model Field that stores Nepali (Bikram Sambat) DateTime. + Internally it stores the datetime as BS string (YYYY-MM-DD HH:MM:SS) in the database. + """ + description = _("Nepali DateTime (Bikram Sambat)") - format = "%Y-%m-%d %H:%M:%S" - nepali_type = nepalidatetime def __init__(self, *args, **kwargs): - kwargs.setdefault("max_length", 19) + self.auto_now = kwargs.pop("auto_now", False) + self.auto_now_add = kwargs.pop("auto_now_add", False) + + # Match Django's DateTimeField behavior + if self.auto_now or self.auto_now_add: + kwargs.setdefault("editable", False) + kwargs.setdefault("blank", True) + + kwargs.setdefault("max_length", 19) # YYYY-MM-DD HH:MM:SS super().__init__(*args, **kwargs) - def _now(self): - return nepalidatetime.now() + def pre_save(self, model_instance, add): + if self.auto_now or (self.auto_now_add and add): + # Using nepalidatetime.now() to get current BS datetime + value = nepalidatetime.now().strftime(BS_DATETIME_FORMAT) + setattr(model_instance, self.attname, value) + return value + return super().pre_save(model_instance, add) + + def from_db_value(self, value, expression, connection): + parsed = try_parse_nepali_datetime(value) + return parsed if parsed is not None else value + + def to_python(self, value): + if value is None or isinstance(value, nepalidatetime): + return value + if isinstance(value, str): + parsed = try_parse_nepali_datetime(value) + return parsed if parsed is not None else value + return super().to_python(value) + + def validate(self, value, model_instance): + if isinstance(value, nepalidatetime): + value = value.strftime(BS_DATETIME_FORMAT) + super().validate(value, model_instance) + + def run_validators(self, value): + if isinstance(value, nepalidatetime): + value = value.strftime(BS_DATETIME_FORMAT) + super().run_validators(value) + + def get_prep_value(self, value): + if value is None: + return value + if isinstance(value, nepalidatetime): + return value.strftime(BS_DATETIME_FORMAT) + if isinstance(value, str): + return value + # Fallback: convert to string + return str(value) + + def deconstruct(self): + name, path, args, kwargs = super().deconstruct() + if self.auto_now: + kwargs["auto_now"] = True + if self.auto_now_add: + kwargs["auto_now_add"] = True + return name, path, args, kwargs def formfield(self, **kwargs): - kwargs.setdefault("widget", NepaliDatePickerWidget) - return super().formfield(**kwargs) + from .widgets import NepaliDatePickerWidget + + defaults = { + "widget": NepaliDatePickerWidget, + } + defaults.update(kwargs) + return super().formfield(**defaults) + + +class ProvinceField(models.CharField): + """ + A Django Model Field for Nepali Provinces. + """ + + description = _("Nepali Province") + + def __init__(self, *args, **kwargs): + from nepali.locations import provinces + + kwargs.setdefault("max_length", 100) + kwargs.setdefault("choices", [(p.name, p.name) for p in provinces]) + super().__init__(*args, **kwargs) + + def formfield(self, **kwargs): + from .widgets import ProvinceSelectWidget + + defaults = {"widget": ProvinceSelectWidget} + defaults.update(kwargs) + return super().formfield(**defaults) + + +class DistrictField(models.CharField): + """ + A Django Model Field for Nepali Districts. + """ + + description = _("Nepali District") + + def __init__(self, *args, **kwargs): + from nepali.locations import districts + + kwargs.setdefault("max_length", 100) + kwargs.setdefault("choices", [(d.name, d.name) for d in districts]) + super().__init__(*args, **kwargs) + + def formfield(self, **kwargs): + from .widgets import DistrictSelectWidget + + defaults = {"widget": DistrictSelectWidget} + defaults.update(kwargs) + return super().formfield(**defaults) + + +class MunicipalityField(models.CharField): + """ + A Django Model Field for Nepali Municipalities. + Includes Metropolitan, Sub-Metropolitan, Municipality, and Rural Municipality. + """ + + description = _("Nepali Municipality") + + def __init__(self, *args, **kwargs): + from nepali.locations import municipalities + + kwargs.setdefault("max_length", 100) + kwargs.setdefault("choices", [(m.name, m.name) for m in municipalities]) + super().__init__(*args, **kwargs) + + def formfield(self, **kwargs): + from .widgets import MunicipalitySelectWidget + + defaults = {"widget": MunicipalitySelectWidget} + defaults.update(kwargs) + return super().formfield(**defaults) diff --git a/django_nepkit/serializers.py b/django_nepkit/serializers.py index ea332a2..d89aaef 100644 --- a/django_nepkit/serializers.py +++ b/django_nepkit/serializers.py @@ -1,5 +1,18 @@ +from __future__ import annotations + +from typing import Any, Optional, Type + from nepali.datetime import nepalidate, nepalidatetime -from rest_framework import serializers + +try: + from rest_framework import serializers +except ModuleNotFoundError as e: + raise ModuleNotFoundError( + "django-nepkit DRF support is optional. Install with `django-nepkit[drf]` " + "to use `django_nepkit.serializers`." + ) from e + +from django_nepkit.utils import try_parse_nepali_date, try_parse_nepali_datetime # -------------------------------------------------- # Base Serializer Field @@ -9,34 +22,65 @@ from rest_framework import serializers class BaseNepaliBSField(serializers.Field): """ Base DRF field for Nepali (BS) Date / DateTime. + + - **Input**: BS string, or an already-parsed `nepalidate`/`nepalidatetime` + - **Output**: formatted BS string (configurable) """ - format = None - nepali_type = None - error_messages = {"invalid": "Invalid Bikram Sambat format. Expected {format}."} + format: str = "" + nepali_type: Type[object] = object - def to_representation(self, value): + default_error_messages = { + "invalid": "Invalid Bikram Sambat value. Expected format: {format}.", + "invalid_type": "Invalid type. Expected a string.", + } + + def __init__(self, *, format: Optional[str] = None, **kwargs: Any) -> None: + """ + Args: + format: Optional `strftime` format used for representation. + If not provided, uses the class default. + """ + if format is not None: + self.format = format + super().__init__(**kwargs) + + def _parse(self, value: str): + if self.nepali_type is nepalidate: + return try_parse_nepali_date(value) + if self.nepali_type is nepalidatetime: + return try_parse_nepali_datetime(value) + return None + + def to_representation(self, value: Any) -> Optional[str]: if value is None: return None if isinstance(value, self.nepali_type): - return value.strftime(self.format) + return value.strftime(self.format) # type: ignore[attr-defined] - # Fallback: DB string + # If DB returns string, try to normalize it. + if isinstance(value, str): + parsed = self._parse(value) + if parsed is not None: + return parsed.strftime(self.format) # type: ignore[attr-defined] + + # Fallback: best-effort stringify (keeps behavior non-breaking) return str(value) - def to_internal_value(self, data): + def to_internal_value(self, data: Any): if data in (None, ""): return None if isinstance(data, self.nepali_type): return data - if isinstance(data, str): - try: - return self.nepali_type.strptime(data.strip(), self.format) - except Exception: - self.fail("invalid", format=self.format) + if not isinstance(data, str): + self.fail("invalid_type") + + parsed = self._parse(data) + if parsed is not None: + return parsed self.fail("invalid", format=self.format) diff --git a/django_nepkit/static/django_nepkit/css/admin-nepali-datepicker.css b/django_nepkit/static/django_nepkit/css/admin-nepali-datepicker.css new file mode 100644 index 0000000..c59de4e --- /dev/null +++ b/django_nepkit/static/django_nepkit/css/admin-nepali-datepicker.css @@ -0,0 +1,38 @@ +.ndp-container.ndp-dark{ + background:#1f2937; /* slate-800-ish */ + color:#e5e7eb; + border:1px solid rgba(255,255,255,.12); + box-shadow:0 10px 25px rgba(0,0,0,.45); +} + +.ndp-container.ndp-dark .ndp-header{ + border-bottom:1px solid rgba(255,255,255,.12); +} + +.ndp-container.ndp-dark .ndp-header .ndp-header-link{ + color:#e5e7eb; +} + +.ndp-container.ndp-dark .ndc-nav-button:hover, +.ndp-container.ndp-dark .ndp-header-link:hover, +.ndp-container.ndp-dark .ndp-table td:hover{ + background:rgba(255,255,255,.08); +} + +.ndp-container.ndp-dark .ndp-table th{ + color:rgba(229,231,235,.75); +} + +.ndp-container.ndp-dark .ndp-table td{ + color:#e5e7eb; +} + +.ndp-container.ndp-dark .ndp-table td.ndp-selected{ + background:#2563eb; /* blue-600 */ + color:#fff; +} + +.ndp-container.ndp-dark .ndp-table td.ndp-today{ + color:#93c5fd; /* blue-300 */ +} + diff --git a/django_nepkit/static/django_nepkit/js/address-chaining.js b/django_nepkit/static/django_nepkit/js/address-chaining.js new file mode 100644 index 0000000..d86cbe9 --- /dev/null +++ b/django_nepkit/static/django_nepkit/js/address-chaining.js @@ -0,0 +1,65 @@ +(function() { + 'use strict'; + + function updateOptions(selectElement, data, placeholder) { + selectElement.innerHTML = ''; + if (placeholder) { + const opt = document.createElement('option'); + opt.value = ''; + opt.textContent = placeholder; + selectElement.appendChild(opt); + } + data.forEach(item => { + const opt = document.createElement('option'); + opt.value = item.id; + opt.textContent = item.text; + selectElement.appendChild(opt); + }); + selectElement.dispatchEvent(new Event('change')); + } + + document.addEventListener('change', function(e) { + if (e.target.matches('.nepkit-province-select')) { + const province = e.target.value; + const container = e.target.closest('form') || document; + const districtSelect = container.querySelector('.nepkit-district-select'); + const municipalitySelect = container.querySelector('.nepkit-municipality-select'); + + if (districtSelect) { + if (!province) { + updateOptions(districtSelect, [], 'Select District'); + if (municipalitySelect) updateOptions(municipalitySelect, [], 'Select Municipality'); + return; + } + + const url = districtSelect.dataset.url + '?province=' + encodeURIComponent(province); + fetch(url) + .then(response => response.json()) + .then(data => { + updateOptions(districtSelect, data, 'Select District'); + }); + } + } + + if (e.target.matches('.nepkit-district-select')) { + const district = e.target.value; + const container = e.target.closest('form') || document; + const municipalitySelect = container.querySelector('.nepkit-municipality-select'); + + if (municipalitySelect) { + if (!district) { + updateOptions(municipalitySelect, [], 'Select Municipality'); + return; + } + + const url = municipalitySelect.dataset.url + '?district=' + encodeURIComponent(district); + fetch(url) + .then(response => response.json()) + .then(data => { + updateOptions(municipalitySelect, data, 'Select Municipality'); + }); + } + } + }); +})(); + diff --git a/django_nepkit/static/django_nepkit/js/admin-jquery-bridge.js b/django_nepkit/static/django_nepkit/js/admin-jquery-bridge.js new file mode 100644 index 0000000..d214ec4 --- /dev/null +++ b/django_nepkit/static/django_nepkit/js/admin-jquery-bridge.js @@ -0,0 +1,11 @@ +(function () { + "use strict"; + + // Django admin exposes jQuery as `django.jQuery`. + // Many third-party plugins expect `window.jQuery`/`window.$`. + if (typeof window !== "undefined" && window.django && window.django.jQuery) { + window.jQuery = window.django.jQuery; + window.$ = window.django.jQuery; + } +})(); + diff --git a/django_nepkit/static/django_nepkit/js/nepali-datepicker-init.js b/django_nepkit/static/django_nepkit/js/nepali-datepicker-init.js new file mode 100644 index 0000000..a5bef33 --- /dev/null +++ b/django_nepkit/static/django_nepkit/js/nepali-datepicker-init.js @@ -0,0 +1,108 @@ +(function () { + 'use strict'; + + function isDarkTheme() { + var docEl = document.documentElement; + var body = document.body; + + // Common theme markers used by Django admin themes / dark-mode packages + var markers = [ + docEl && docEl.getAttribute('data-theme'), + docEl && docEl.getAttribute('data-color-scheme'), + body && body.getAttribute('data-theme'), + body && body.getAttribute('data-color-scheme') + ].filter(Boolean).join(' ').toLowerCase(); + + var classNames = [ + docEl && docEl.className, + body && body.className + ].filter(Boolean).join(' ').toLowerCase(); + + if (markers.indexOf('dark') !== -1 || classNames.indexOf('dark') !== -1) return true; + + // Fallback to OS preference + try { + return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + } catch (e) { + return false; + } + } + + function applyThemeToDatepickerContainers() { + var dark = isDarkTheme(); + var containers = document.querySelectorAll('.ndp-container'); + if (!containers || !containers.length) return; + + containers.forEach(function (c) { + c.classList.remove('ndp-dark'); + c.classList.remove('ndp-light'); + c.classList.add(dark ? 'ndp-dark' : 'ndp-light'); + }); + } + + function initNepaliDatePickers() { + var inputs = document.querySelectorAll('.nepkit-datepicker:not(.nepali-datepicker-initialized)'); + if (!inputs || !inputs.length) return; + + inputs.forEach(function (el) { + // Nepali Datepicker v5 exposes `element.NepaliDatePicker(options)` + if (typeof el.NepaliDatePicker === 'function') { + el.NepaliDatePicker({ + dateFormat: 'YYYY-MM-DD' + }); + el.classList.add('nepali-datepicker-initialized'); + + // Ensure theme is applied when the picker is actually shown. + // (The plugin often creates/inserts DOM on focus.) + var applySoon = function () { + // Run twice: once immediately, once after paint. + applyThemeToDatepickerContainers(); + window.setTimeout(applyThemeToDatepickerContainers, 0); + }; + + el.addEventListener('focus', applySoon); + el.addEventListener('click', applySoon); + } + }); + } + + // Initialize on page load + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initNepaliDatePickers); + } else { + initNepaliDatePickers(); + } + + // Keep in sync if system theme changes (fallback path) + if (window.matchMedia) { + try { + var mql = window.matchMedia('(prefers-color-scheme: dark)'); + if (mql && typeof mql.addEventListener === 'function') { + mql.addEventListener('change', applyThemeToDatepickerContainers); + } else if (mql && typeof mql.addListener === 'function') { + mql.addListener(applyThemeToDatepickerContainers); + } + } catch (e) { + // ignore + } + } + + // Re-initialize when Django admin adds inlines dynamically + if (typeof django !== 'undefined' && django.jQuery) { + django.jQuery(document).on('formset:added', function () { + initNepaliDatePickers(); + }); + } + + // Also listen for DOM changes (admin popups/other dynamic content) + if (typeof MutationObserver !== 'undefined') { + var observer = new MutationObserver(function () { + initNepaliDatePickers(); + applyThemeToDatepickerContainers(); + }); + observer.observe(document.body, { + childList: true, + subtree: true + }); + } +})(); diff --git a/django_nepkit/static/js/address-chaining.js b/django_nepkit/static/js/address-chaining.js deleted file mode 100644 index f15befe..0000000 --- a/django_nepkit/static/js/address-chaining.js +++ /dev/null @@ -1,89 +0,0 @@ -(function() { - "use strict"; - - function updateOptions(selectElement, data, placeholder) { - selectElement.innerHTML = ""; - if (placeholder) { - const opt = document.createElement("option"); - opt.value = ""; - opt.textContent = placeholder; - selectElement.appendChild(opt); - } - data.forEach((item) => { - const opt = document.createElement("option"); - opt.value = item.id; - opt.textContent = item.text; - selectElement.appendChild(opt); - }); - selectElement.dispatchEvent(new Event("change")); - } - - document.addEventListener("change", function(e) { - if (e.target.matches(".nepkit-province-select")) { - const province = e.target.value; - const container = e.target.closest("form") || document; - const districtSelect = container.querySelector( - ".nepkit-district-select", - ); - const municipalitySelect = container.querySelector( - ".nepkit-municipality-select", - ); - - if (districtSelect) { - if (!province) { - updateOptions(districtSelect, [], "Select District"); - if (municipalitySelect) - updateOptions( - municipalitySelect, - [], - "Select Municipality", - ); - return; - } - - const url = - districtSelect.dataset.url + - "?province=" + - encodeURIComponent(province); - fetch(url) - .then((response) => response.json()) - .then((data) => { - updateOptions(districtSelect, data, "Select District"); - }); - } - } - - if (e.target.matches(".nepkit-district-select")) { - const district = e.target.value; - const container = e.target.closest("form") || document; - const municipalitySelect = container.querySelector( - ".nepkit-municipality-select", - ); - - if (municipalitySelect) { - if (!district) { - updateOptions( - municipalitySelect, - [], - "Select Municipality", - ); - return; - } - - const url = - municipalitySelect.dataset.url + - "?district=" + - encodeURIComponent(district); - fetch(url) - .then((response) => response.json()) - .then((data) => { - updateOptions( - municipalitySelect, - data, - "Select Municipality", - ); - }); - } - } - }); -})(); diff --git a/django_nepkit/static/js/nepali-datepicker-init.js b/django_nepkit/static/js/nepali-datepicker-init.js deleted file mode 100644 index 6028e62..0000000 --- a/django_nepkit/static/js/nepali-datepicker-init.js +++ /dev/null @@ -1,45 +0,0 @@ -(function($) { - "use strict"; - - function initNepaliDatePickers() { - if ($.fn.nepaliDatePicker) { - // Initialize any date pickers that aren't already initialized - $(".nepkit-datepicker") - .not(".nepali-datepicker-initialized") - .each(function() { - $(this) - .nepaliDatePicker({ - dateFormat: "%y-%m-%d", - closeOnDateSelect: true, - minDate: "१९७५-१-१", - maxDate: "२१००-१२-३०", - }) - .addClass("nepali-datepicker-initialized"); - }); - } - } - - // Initialize on page load - $(document).ready(function() { - initNepaliDatePickers(); - }); - - // Re-initialize when Django admin adds forms dynamically (e.g., inlines, popups) - // This handles Django admin's dynamic form loading - if (typeof django !== "undefined" && django.jQuery) { - django.jQuery(document).on("formset:added", function() { - initNepaliDatePickers(); - }); - } - - // Also listen for DOM changes (for admin popups and other dynamic content) - if (typeof MutationObserver !== "undefined") { - var observer = new MutationObserver(function(mutations) { - initNepaliDatePickers(); - }); - observer.observe(document.body, { - childList: true, - subtree: true, - }); - } -})(window.jQuery || window.django.jQuery); diff --git a/django_nepkit/templatetag/nepali.py b/django_nepkit/templatetag/nepali.py deleted file mode 100644 index bcab9b3..0000000 --- a/django_nepkit/templatetag/nepali.py +++ /dev/null @@ -1,100 +0,0 @@ -import datetime - -from django import template -from django.utils.safestring import mark_safe - -from nepali.datetime import nepalidate, nepalihumanize -from nepali.number import nepalinumber - - -register = template.Library() - - -# -------------------------------------------------- -# Internal helpers -# -------------------------------------------------- - -def _to_nepali_date(value): - """ - Normalize AD date/datetime or BS date to nepalidate. - """ - if value is None: - return None - - if isinstance(value, nepalidate): - return value - - if isinstance(value, (datetime.date, datetime.datetime)): - return nepalidate.from_date(value) - - return None - - -# -------------------------------------------------- -# Date filters -# -------------------------------------------------- - -@register.filter(is_safe=True) -def nepali_date(value, format_str="%Y-%m-%d"): - """ - Format a date/datetime as Nepali (BS) date. - """ - nd = _to_nepali_date(value) - if not nd: - return "" - - return nd.strftime(format_str) - - -@register.filter(is_safe=True) -def nepali_date_ne(value, format_str="%Y-%m-%d"): - """ - Format a date/datetime as Nepali (BS) date in Devanagari. - """ - nd = _to_nepali_date(value) - if not nd: - return "" - - return nd.strftime_ne(format_str) - - -# -------------------------------------------------- -# Number filter -# -------------------------------------------------- - -@register.filter(is_safe=True) -def nepali_number(value): - """ - Convert a number to Nepali (Devanagari) digits. - """ - if value in (None, ""): - return "" - - try: - return nepalinumber(value).str_ne() - except Exception: - return value - - -# -------------------------------------------------- -# Humanize filter -# -------------------------------------------------- - -@register.filter(is_safe=True) -def nepali_humanize(value, threshold=None, format_str=None): - """ - Human-readable Nepali time difference. - """ - if value is None: - return "" - - kwargs = {} - if threshold is not None: - kwargs["threshold"] = threshold - if format_str is not None: - kwargs["format"] = format_str - - try: - return nepalihumanize(value, **kwargs) - except Exception: - return value diff --git a/django_nepkit/templatetags/__init__.py b/django_nepkit/templatetags/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/django_nepkit/templatetags/__init__.py @@ -0,0 +1 @@ + diff --git a/django_nepkit/templatetags/nepali.py b/django_nepkit/templatetags/nepali.py new file mode 100644 index 0000000..2c9a06c --- /dev/null +++ b/django_nepkit/templatetags/nepali.py @@ -0,0 +1,74 @@ +from django import template +from nepali.datetime import nepalidate, nepalihumanize +from nepali.number import nepalinumber +import datetime + +register = template.Library() + + +def _coerce_ad_date_to_bs(value): + """ + Internal helper: if value is AD date/datetime, convert to `nepalidate`. + Does NOT attempt to parse strings (keeps template behavior minimal). + """ + if isinstance(value, (datetime.datetime, datetime.date)): + return nepalidate.from_date(value) + return value + + +@register.filter +def nepali_date(value, format_str="%Y-%m-%d"): + """ + Formats a date or datetime object into a Nepali date string. + """ + if value is None: + return "" + + value = _coerce_ad_date_to_bs(value) + + if hasattr(value, "strftime"): + return value.strftime(format_str) + return value + + +@register.filter +def nepali_date_ne(value, format_str="%Y-%m-%d"): + """ + Formats a date or datetime object into a Nepali date string (Devanagari). + """ + if value is None: + return "" + + value = _coerce_ad_date_to_bs(value) + + if hasattr(value, "strftime_ne"): + return value.strftime_ne(format_str) + return value + + +@register.filter +def nepali_number(value): + """ + Converts a number to Devanagari. + """ + if value is None: + return "" + return nepalinumber(value).str_ne() + + +@register.filter +def nepali_humanize(value, threshold=None, format_str=None): + """ + Returns a human-readable "time ago" string in Nepali. + """ + if value is None: + return "" + + # Threshold and format_str are optional + kwargs = {} + if threshold: + kwargs["threshold"] = threshold + if format_str: + kwargs["format"] = format_str + + return nepalihumanize(value, **kwargs) diff --git a/django_nepkit/utils.py b/django_nepkit/utils.py index 6de3ca2..bc9526d 100644 --- a/django_nepkit/utils.py +++ b/django_nepkit/utils.py @@ -1,14 +1,76 @@ -from nepali.locations import districts, provices +from __future__ import annotations + +from typing import Any, Optional + +from nepali.datetime import nepalidate, nepalidatetime -def get_districts_by_province(provice_name): - selected_province = next((p for p in provices if p.name == provice_name), None) +BS_DATE_FORMAT = "%Y-%m-%d" +BS_DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S" + + +def try_parse_nepali_date(value: Any) -> Optional[nepalidate]: + """ + Best-effort conversion to `nepalidate`. + + - Returns `None` for empty values. + - Returns `nepalidate` for valid inputs. + - Returns `None` if the value cannot be parsed. + + Callers decide whether to raise, fallback, etc. + """ + if value in (None, ""): + return None + if isinstance(value, nepalidate): + return value + if isinstance(value, str): + try: + return nepalidate.strptime(value.strip(), BS_DATE_FORMAT) + except Exception: + return None + return None + + +def try_parse_nepali_datetime(value: Any) -> Optional[nepalidatetime]: + """ + Best-effort conversion to `nepalidatetime`. + + - Returns `None` for empty values. + - Returns `nepalidatetime` for valid inputs. + - Returns `None` if the value cannot be parsed. + + Callers decide whether to raise, fallback, etc. + """ + if value in (None, ""): + return None + if isinstance(value, nepalidatetime): + return value + if isinstance(value, str): + try: + return nepalidatetime.strptime(value.strip(), BS_DATETIME_FORMAT) + except Exception: + return None + return None + + +def get_districts_by_province(province_name): + """ + Returns a list of districts in the given province. + """ + from nepali.locations import provinces + + selected_province = next((p for p in provinces if p.name == province_name), None) if not selected_province: return [] return [{"id": d.name, "text": d.name} for d in selected_province.districts] def get_municipalities_by_district(district_name): + """ + Returns a list of municipalities in the given district. + """ + from nepali.locations import districts + selected_district = next((d for d in districts if d.name == district_name), None) if not selected_district: return [] diff --git a/django_nepkit/widgets.py b/django_nepkit/widgets.py index feafe77..359e888 100644 --- a/django_nepkit/widgets.py +++ b/django_nepkit/widgets.py @@ -2,82 +2,48 @@ from django import forms from django.urls import reverse_lazy -# -------------------------------------------------- -# Utility mixin -# -------------------------------------------------- - -class ClassAttrMixin: +def _append_css_class(attrs, class_name: str): """ - Safely append CSS classes to widget attrs. + Django-idiomatic helper to append a CSS class without clobbering existing ones. """ - - css_class = "" - - def _build_attrs(self, attrs): - attrs = attrs or {} - classes = attrs.get("class", "").split() - if self.css_class: - classes.append(self.css_class) - attrs["class"] = " ".join(dict.fromkeys(classes)) - return attrs + existing = (attrs.get("class") or "").strip() + attrs["class"] = (f"{existing} {class_name}").strip() if existing else class_name + return attrs -# -------------------------------------------------- -# Chained Select Base -# -------------------------------------------------- - -class ChainedSelectWidget(ClassAttrMixin, forms.Select): - """ - Base widget for chained location selects. - """ - +class ChainedSelectWidget(forms.Select): class Media: js = ("django_nepkit/js/address-chaining.js",) -# -------------------------------------------------- -# Province / District / Municipality Widgets -# -------------------------------------------------- - class ProvinceSelectWidget(ChainedSelectWidget): - css_class = "nepkit-province-select" - def __init__(self, *args, **kwargs): - kwargs["attrs"] = self._build_attrs(kwargs.get("attrs")) + attrs = kwargs.get("attrs", {}) + _append_css_class(attrs, "nepkit-province-select") + kwargs["attrs"] = attrs super().__init__(*args, **kwargs) class DistrictSelectWidget(ChainedSelectWidget): - css_class = "nepkit-district-select" - def __init__(self, *args, **kwargs): - attrs = self._build_attrs(kwargs.get("attrs")) - attrs.setdefault("data-url", reverse_lazy("django_nepkit:district-list")) + attrs = kwargs.get("attrs", {}) + _append_css_class(attrs, "nepkit-district-select") + attrs["data-url"] = reverse_lazy("django_nepkit:district-list") kwargs["attrs"] = attrs super().__init__(*args, **kwargs) class MunicipalitySelectWidget(ChainedSelectWidget): - css_class = "nepkit-municipality-select" - def __init__(self, *args, **kwargs): - attrs = self._build_attrs(kwargs.get("attrs")) - attrs.setdefault("data-url", reverse_lazy("django_nepkit:municipality-list")) + attrs = kwargs.get("attrs", {}) + _append_css_class(attrs, "nepkit-municipality-select") + attrs["data-url"] = reverse_lazy("django_nepkit:municipality-list") kwargs["attrs"] = attrs super().__init__(*args, **kwargs) -# -------------------------------------------------- -# Nepali Date Picker -# -------------------------------------------------- - -class NepaliDatePickerWidget(ClassAttrMixin, forms.TextInput): - """ - Nepali BS date picker widget. - """ - +class NepaliDatePickerWidget(forms.TextInput): input_type = "text" - css_class = "nepkit-datepicker" class Media: css = { @@ -93,14 +59,14 @@ class NepaliDatePickerWidget(ClassAttrMixin, forms.TextInput): def __init__(self, *args, **kwargs): attrs = kwargs.get("attrs", {}) + # Ensure we don't have vDateField class which triggers Django admin calendar + classes = attrs.get("class", "") + if "vDateField" in classes: + classes = classes.replace("vDateField", "") - # Remove Django admin default class - classes = attrs.get("class", "").replace("vDateField", "").strip() - attrs["class"] = classes - - attrs = self._build_attrs(attrs) - attrs.setdefault("autocomplete", "off") - attrs.setdefault("placeholder", "YYYY-MM-DD") - + attrs["class"] = (classes or "").strip() + _append_css_class(attrs, "nepkit-datepicker") + attrs["autocomplete"] = "off" + attrs["placeholder"] = "YYYY-MM-DD" kwargs["attrs"] = attrs super().__init__(*args, **kwargs) diff --git a/example/demo/__init__.py b/example/demo/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/example/demo/admin.py b/example/demo/admin.py new file mode 100644 index 0000000..10bd6c0 --- /dev/null +++ b/example/demo/admin.py @@ -0,0 +1,42 @@ +from django.contrib import admin + +from django_nepkit import NepaliModelAdmin + +from .models import Person + + +@admin.register(Person) +class PersonAdmin(NepaliModelAdmin): + list_display = ( + "name", + "display_birth_date", + "phone_number", + "province", + "district", + "municipality", + "display_created_at", + "display_updated_at", + ) + list_filter = ("birth_date", "province", "district") + search_fields = ("name", "phone_number") + + def display_birth_date(self, obj): + """Display birth date with Nepali month names""" + return self.format_nepali_date(obj.birth_date) + + display_birth_date.short_description = "Birth Date" + display_birth_date.admin_order_field = "birth_date" + + def display_created_at(self, obj): + """Display created date with Nepali month names""" + return self.format_nepali_datetime(obj.created_at) + + display_created_at.short_description = "Created At" + display_created_at.admin_order_field = "created_at" + + def display_updated_at(self, obj): + """Display updated date with Nepali month names""" + return self.format_nepali_datetime(obj.updated_at) + + display_updated_at.short_description = "Updated At" + display_updated_at.admin_order_field = "updated_at" diff --git a/example/demo/apps.py b/example/demo/apps.py new file mode 100644 index 0000000..05820e0 --- /dev/null +++ b/example/demo/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class DemoConfig(AppConfig): + name = "demo" diff --git a/example/demo/migrations/0001_initial.py b/example/demo/migrations/0001_initial.py new file mode 100644 index 0000000..5a0e15d --- /dev/null +++ b/example/demo/migrations/0001_initial.py @@ -0,0 +1,2113 @@ +# Generated by Django 6.0.1 on 2026-01-28 15:30 + +import django_nepkit.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Person", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100)), + ("birth_date", django_nepkit.models.NepaliDateField()), + ( + "registration_time", + django_nepkit.models.NepaliTimeField(auto_now_add=True), + ), + ( + "phone_number", + django_nepkit.models.NepaliPhoneNumberField(max_length=20), + ), + ( + "province", + django_nepkit.models.ProvinceField( + choices=[ + ("Sudurpaschim Province", "Sudurpaschim Province"), + ("Madhesh Province", "Madhesh Province"), + ("Gandaki Province", "Gandaki Province"), + ("Bagmati Province", "Bagmati Province"), + ("Lumbini Province", "Lumbini Province"), + ("Karnali Province", "Karnali Province"), + ("Province 1", "Province 1"), + ], + max_length=100, + ), + ), + ( + "district", + django_nepkit.models.DistrictField( + choices=[ + ("Bajura", "Bajura"), + ("Bajhang", "Bajhang"), + ("Baitadi", "Baitadi"), + ("Dadeldhura", "Dadeldhura"), + ("Doti", "Doti"), + ("Achham", "Achham"), + ("Kailali", "Kailali"), + ("Darchula", "Darchula"), + ("Kanchanpur", "Kanchanpur"), + ("Saptari", "Saptari"), + ("Siraha", "Siraha"), + ("Dhanusa", "Dhanusa"), + ("Mahottari", "Mahottari"), + ("Sarlahi", "Sarlahi"), + ("Rautahat", "Rautahat"), + ("Bara", "Bara"), + ("Parsa", "Parsa"), + ("Gorkha", "Gorkha"), + ("Manang", "Manang"), + ("Mustang", "Mustang"), + ("Myagdi", "Myagdi"), + ("Kaski", "Kaski"), + ("Lamjung", "Lamjung"), + ("Tanahu", "Tanahu"), + ("Nawalparasi East", "Nawalparasi East"), + ("Syangja", "Syangja"), + ("Parbat", "Parbat"), + ("Baglung", "Baglung"), + ("Dolakha", "Dolakha"), + ("Sindhupalchok", "Sindhupalchok"), + ("Rasuwa", "Rasuwa"), + ("Dhading", "Dhading"), + ("Nuwakot", "Nuwakot"), + ("Kathmandu", "Kathmandu"), + ("Bhaktapur", "Bhaktapur"), + ("Lalitpur", "Lalitpur"), + ("Kavrepalanchok", "Kavrepalanchok"), + ("Ramechhap", "Ramechhap"), + ("Sindhuli", "Sindhuli"), + ("Makwanpur", "Makwanpur"), + ("Chitawan", "Chitawan"), + ("Rukum East", "Rukum East"), + ("Rolpa", "Rolpa"), + ("Pyuthan", "Pyuthan"), + ("Gulmi", "Gulmi"), + ("Arghakhanchi", "Arghakhanchi"), + ("Palpa", "Palpa"), + ("Rupandehi", "Rupandehi"), + ("Kapilvastu", "Kapilvastu"), + ("Dang", "Dang"), + ("Banke", "Banke"), + ("Bardiya", "Bardiya"), + ("Nawalparasi West", "Nawalparasi West"), + ("Dolpa", "Dolpa"), + ("Mugu", "Mugu"), + ("Jumla", "Jumla"), + ("Kalikot", "Kalikot"), + ("Dailekh", "Dailekh"), + ("Jajarkot", "Jajarkot"), + ("Salyan", "Salyan"), + ("Surkhet", "Surkhet"), + ("Humla", "Humla"), + ("Rukum West", "Rukum West"), + ("Taplejung", "Taplejung"), + ("Sankhuwasabha", "Sankhuwasabha"), + ("Solukhumbu", "Solukhumbu"), + ("Okhaldhunga", "Okhaldhunga"), + ("Khotang", "Khotang"), + ("Bhojpur", "Bhojpur"), + ("Dhankuta", "Dhankuta"), + ("Tehrathum", "Tehrathum"), + ("Panchthar", "Panchthar"), + ("Ilam", "Ilam"), + ("Jhapa", "Jhapa"), + ("Morang", "Morang"), + ("Sunsari", "Sunsari"), + ("Udayapur", "Udayapur"), + ], + max_length=100, + ), + ), + ( + "municipality", + django_nepkit.models.MunicipalityField( + choices=[ + ("Himali Rural Municipality", "Himali Rural Municipality"), + ("Gaumul Rural Municipality", "Gaumul Rural Municipality"), + ("Badimalika Municipality", "Badimalika Municipality"), + ( + "Chhededaha Rural Municipality", + "Chhededaha Rural Municipality", + ), + ("Budhiganga Municipality", "Budhiganga Municipality"), + ("Tribeni Municipality", "Tribeni Municipality"), + ( + "Jagannath Rural Municipality", + "Jagannath Rural Municipality", + ), + ("Budhinanda Municipality", "Budhinanda Municipality"), + ( + "Swami Kartik Rural Municipality", + "Swami Kartik Rural Municipality", + ), + ("Kanda Rural Municipality", "Kanda Rural Municipality"), + ("Bungal Municipality", "Bungal Municipality"), + ("Talkot Rural Municipality", "Talkot Rural Municipality"), + ("Masta Rural Municipality", "Masta Rural Municipality"), + ("Jayaprithbi Municipality", "Jayaprithbi Municipality"), + ( + "Chhabis Pathibhara Rural Municipality", + "Chhabis Pathibhara Rural Municipality", + ), + ( + "Durgathali Rural Municipality", + "Durgathali Rural Municipality", + ), + ( + "Bitthadchir Rural Municipality", + "Bitthadchir Rural Municipality", + ), + ( + "Thalara Rural Municipality", + "Thalara Rural Municipality", + ), + ( + "Khaptad Chhanna Rural Municipality", + "Khaptad Chhanna Rural Municipality", + ), + ("Surma Rural Municipality", "Surma Rural Municipality"), + ( + "Kedarsyun Rural Municipality", + "Kedarsyun Rural Municipality", + ), + ( + "Dilasaini Rural Municipality", + "Dilasaini Rural Municipality", + ), + ( + "Dogada Kedar Rural Municipality", + "Dogada Kedar Rural Municipality", + ), + ("Puchaundi Municipality", "Puchaundi Municipality"), + ( + "Surnaya Rural Municipality", + "Surnaya Rural Municipality", + ), + ( + "Dasharathchand Municipality", + "Dasharathchand Municipality", + ), + ( + "Shivanath Rural Municipality", + "Shivanath Rural Municipality", + ), + ("Melauli Municipality", "Melauli Municipality"), + ("Patan Municipality", "Patan Municipality"), + ("Sigas Rural Municipality", "Sigas Rural Municipality"), + ( + "Pancheshwor Rural Municipality", + "Pancheshwor Rural Municipality", + ), + ( + "Nawadurga Rural Municipality", + "Nawadurga Rural Municipality", + ), + ("Amargadhi Municipality", "Amargadhi Municipality"), + ( + "Bhageshwor Rural Municipality", + "Bhageshwor Rural Municipality", + ), + ("Parashuram Municipality", "Parashuram Municipality"), + ( + "Aalital Rural Municipality", + "Aalital Rural Municipality", + ), + ( + "Ganyapdhura Rural Municipality", + "Ganyapdhura Rural Municipality", + ), + ( + "Ajayameru Rural Municipality", + "Ajayameru Rural Municipality", + ), + ( + "Purbichouki Rural Municipality", + "Purbichouki Rural Municipality", + ), + ("Sayal Rural Municipality", "Sayal Rural Municipality"), + ( + "K.I. Singh Rural Municipality", + "K.I. Singh Rural Municipality", + ), + ( + "Bogatan Foodsil Rural Municipality", + "Bogatan Foodsil Rural Municipality", + ), + ( + "Badi Kedar Rural Municipality", + "Badi Kedar Rural Municipality", + ), + ( + "Jorayal Rural Municipality", + "Jorayal Rural Municipality", + ), + ( + "Aadarsha Rural Municipality", + "Aadarsha Rural Municipality", + ), + ("Shikhar Municipality", "Shikhar Municipality"), + ( + "Dipayal Silgadhi Municipality", + "Dipayal Silgadhi Municipality", + ), + ( + "Ramaroshan Rural Municipality", + "Ramaroshan Rural Municipality", + ), + ( + "Mellekh Rural Municipality", + "Mellekh Rural Municipality", + ), + ("Sanphebagar Municipality", "Sanphebagar Municipality"), + ( + "Chaurpati Rural Municipality", + "Chaurpati Rural Municipality", + ), + ("Mangalsen Municipality", "Mangalsen Municipality"), + ("Kamal Bazar Municipality", "Kamal Bazar Municipality"), + ( + "Dhakari Rural Municipality", + "Dhakari Rural Municipality", + ), + ( + "Turmakhand Rural Municipality", + "Turmakhand Rural Municipality", + ), + ( + "Panchdebal Binayak Municipality", + "Panchdebal Binayak Municipality", + ), + ( + "Bannigadhi Jayagadh Rural Municipality", + "Bannigadhi Jayagadh Rural Municipality", + ), + ( + "Mohanyal Rural Municipality", + "Mohanyal Rural Municipality", + ), + ("Chure Rural Municipality", "Chure Rural Municipality"), + ("Godawari Municipality", "Godawari Municipality"), + ("Gauriganga Municipality", "Gauriganga Municipality"), + ( + "Bardagoriya Rural Municipality", + "Bardagoriya Rural Municipality", + ), + ("Lamki Chuha Municipality", "Lamki Chuha Municipality"), + ("Janaki Rural Municipality", "Janaki Rural Municipality"), + ( + "Joshipur Rural Municipality", + "Joshipur Rural Municipality", + ), + ("Tikapur Municipality", "Tikapur Municipality"), + ("Bhajani Municipality", "Bhajani Municipality"), + ( + "Dhangadhi Sub-Metropolitan City", + "Dhangadhi Sub-Metropolitan City", + ), + ("Ghodaghodi Municipality", "Ghodaghodi Municipality"), + ( + "Kailari Rural Municipality", + "Kailari Rural Municipality", + ), + ("Byas Rural Municipality", "Byas Rural Municipality"), + ("Duhun Rural Municipality", "Duhun Rural Municipality"), + ("Mahakali Municipality", "Mahakali Municipality"), + ( + "Apihimal Rural Municipality", + "Apihimal Rural Municipality", + ), + ("Marma Rural Municipality", "Marma Rural Municipality"), + ( + "Shailyashikhar Municipality", + "Shailyashikhar Municipality", + ), + ("Lekam Rural Municipality", "Lekam Rural Municipality"), + ("Naugad Rural Municipality", "Naugad Rural Municipality"), + ( + "Malikarjun Rural Municipality", + "Malikarjun Rural Municipality", + ), + ("Krishnapur Municipality", "Krishnapur Municipality"), + ("Shuklaphanta Municipality", "Shuklaphanta Municipality"), + ("Bedkot Municipality", "Bedkot Municipality"), + ("Bhimdatta Municipality", "Bhimdatta Municipality"), + ( + "Laljhadi Rural Municipality", + "Laljhadi Rural Municipality", + ), + ("Punarbas Municipality", "Punarbas Municipality"), + ("Belouri Municipality", "Belouri Municipality"), + ( + "Beldandi Rural Municipality", + "Beldandi Rural Municipality", + ), + ("Mahakali Municipality", "Mahakali Municipality"), + ("Saptakoshi Municipality", "Saptakoshi Municipality"), + ("Kanchanrup Municipality", "Kanchanrup Municipality"), + ("Rupani Rural Municipality", "Rupani Rural Municipality"), + ("Shambhunath Municipality", "Shambhunath Municipality"), + ("Surunga Municipality", "Surunga Municipality"), + ( + "Balan-Bihul Rural Municipality", + "Balan-Bihul Rural Municipality", + ), + ("BodeBarsain Municipality", "BodeBarsain Municipality"), + ( + "Belhi Chapena Rural Municipality", + "Belhi Chapena Rural Municipality", + ), + ( + "Bishnupur Rural Municipality", + "Bishnupur Rural Municipality", + ), + ("Rajbiraj Municipality", "Rajbiraj Municipality"), + ( + "Mahadewa Rural Municipality", + "Mahadewa Rural Municipality", + ), + ( + "Tirahut Rural Municipality", + "Tirahut Rural Municipality", + ), + ( + "Tilathi Koiladi Rural Municipality", + "Tilathi Koiladi Rural Municipality", + ), + ( + "Chhinnamasta Rural Municipality", + "Chhinnamasta Rural Municipality", + ), + ( + "Agnisair Krishna Sabaran Rural Municipality", + "Agnisair Krishna Sabaran Rural Municipality", + ), + ("Khadak Municipality", "Khadak Municipality"), + ("Dakneshwori Municipality", "Dakneshwori Municipality"), + ( + "Hanumannagar Kankalini Municipality", + "Hanumannagar Kankalini Municipality", + ), + ("Lahan Municipality", "Lahan Municipality"), + ("Dhangadhimai Municipality", "Dhangadhimai Municipality"), + ("Golbazar Municipality", "Golbazar Municipality"), + ("Mirchaiya Municipality", "Mirchaiya Municipality"), + ("Karjanha Municipality", "Karjanha Municipality"), + ("Naraha Rural Municipality", "Naraha Rural Municipality"), + ( + "Bishnupur Rural Municipality", + "Bishnupur Rural Municipality", + ), + ("Arnama Rural Municipality", "Arnama Rural Municipality"), + ("Sukhipur Municipality", "Sukhipur Municipality"), + ( + "Sakhuwa Nankarkatti Rural Municipality", + "Sakhuwa Nankarkatti Rural Municipality", + ), + ( + "Bhagawanpur Rural Municipality", + "Bhagawanpur Rural Municipality", + ), + ( + "Nawarajpur Rural Municipality", + "Nawarajpur Rural Municipality", + ), + ( + "Bariyarpatti Rural Municipality", + "Bariyarpatti Rural Municipality", + ), + ("Aurahi Rural Municipality", "Aurahi Rural Municipality"), + ("Siraha Municipality", "Siraha Municipality"), + ("Kalyanpur Municipality", "Kalyanpur Municipality"), + ( + "Laxmipur Patari Rural Municipality", + "Laxmipur Patari Rural Municipality", + ), + ("Dhanushadham Municipality", "Dhanushadham Municipality"), + ("Mithila Municipality", "Mithila Municipality"), + ( + "Bateshwor Rural Municipality", + "Bateshwor Rural Municipality", + ), + ( + "Chhireshwornath Municipality", + "Chhireshwornath Municipality", + ), + ( + "Laxminiya Rural Municipality", + "Laxminiya Rural Municipality", + ), + ( + "Mithila Bihari Municipality", + "Mithila Bihari Municipality", + ), + ("Hansapur Municipality", "Hansapur Municipality"), + ("Shahidnagar Municipality", "Shahidnagar Municipality"), + ("Kamala Municipality", "Kamala Municipality"), + ( + "Janak Nandini Rural Municipality", + "Janak Nandini Rural Municipality", + ), + ("Bideha Municipality", "Bideha Municipality"), + ( + "Janakpur Sub-Metropolitan City", + "Janakpur Sub-Metropolitan City", + ), + ( + "Dhanauji Rural Municipality", + "Dhanauji Rural Municipality", + ), + ("Nagarain Municipality", "Nagarain Municipality"), + ( + "Ganeshman Charnath Municipality", + "Ganeshman Charnath Municipality", + ), + ("Sabaila Municipality", "Sabaila Municipality"), + ("Aurahi Rural Municipality", "Aurahi Rural Municipality"), + ( + "Mukhiyapatti Musharniya Rural Municipality", + "Mukhiyapatti Musharniya Rural Municipality", + ), + ("Bardibas Municipality", "Bardibas Municipality"), + ("Gaushala Municipality", "Gaushala Municipality"), + ("Sonama Rural Municipality", "Sonama Rural Municipality"), + ("Aurahi Municipality", "Aurahi Municipality"), + ("Loharpatti Municipality", "Loharpatti Municipality"), + ("Balawa Municipality", "Balawa Municipality"), + ("Ram Gopalpur Municipality", "Ram Gopalpur Municipality"), + ("Samsi Rural Municipality", "Samsi Rural Municipality"), + ( + "Ekadara Rural Municipality", + "Ekadara Rural Municipality", + ), + ( + "Mahottari Rural Municipality", + "Mahottari Rural Municipality", + ), + ("Pipara Rural Municipality", "Pipara Rural Municipality"), + ("Matihani Municipality", "Matihani Municipality"), + ("Jaleshwor Municipality", "Jaleshwor Municipality"), + ("Bhangaha Municipality", "Bhangaha Municipality"), + ( + "Manara Shiswa Municipality", + "Manara Shiswa Municipality", + ), + ("Lalbandi Municipality", "Lalbandi Municipality"), + ("Bagmati Municipality", "Bagmati Municipality"), + ("Barahathawa Municipality", "Barahathawa Municipality"), + ("Haripur Municipality", "Haripur Municipality"), + ("Ishworpur Municipality", "Ishworpur Municipality"), + ("Parsa Rural Municipality", "Parsa Rural Municipality"), + ( + "Brahmapuri Rural Municipality", + "Brahmapuri Rural Municipality", + ), + ( + "Chandranagar Rural Municipality", + "Chandranagar Rural Municipality", + ), + ("Kabilashi Municipality", "Kabilashi Municipality"), + ( + "Chakraghatta Rural Municipality", + "Chakraghatta Rural Municipality", + ), + ( + "Dhanakaul Rural Municipality", + "Dhanakaul Rural Municipality", + ), + ( + "Ramnagar Rural Municipality", + "Ramnagar Rural Municipality", + ), + ("Balara Municipality", "Balara Municipality"), + ("Godaita Municipality", "Godaita Municipality"), + ("Bishnu Rural Municipality", "Bishnu Rural Municipality"), + ("Malangawa Municipality", "Malangawa Municipality"), + ("Hariwan Municipality", "Hariwan Municipality"), + ("Haripurwa Municipality", "Haripurwa Municipality"), + ( + "Basbariya Rural Municipality", + "Basbariya Rural Municipality", + ), + ( + "Kaudena Rural Municipality", + "Kaudena Rural Municipality", + ), + ("Chandrapur Municipality", "Chandrapur Municipality"), + ("Gujara Municipality", "Gujara Municipality"), + ( + "Phatuwa Bijayapur Municipality", + "Phatuwa Bijayapur Municipality", + ), + ("Katahariya Municipality", "Katahariya Municipality"), + ("Brindaban Municipality", "Brindaban Municipality"), + ( + "Madhav Narayan Municipality", + "Madhav Narayan Municipality", + ), + ("Garuda Municipality", "Garuda Municipality"), + ( + "Dewahi Gonahi Municipality", + "Dewahi Gonahi Municipality", + ), + ("Maulapur Municipality", "Maulapur Municipality"), + ("Boudhimai Municipality", "Boudhimai Municipality"), + ("Paroha Municipality", "Paroha Municipality"), + ( + "Yamunamai Rural Municipality", + "Yamunamai Rural Municipality", + ), + ("Rajdevi Municipality", "Rajdevi Municipality"), + ("Gaur Municipality", "Gaur Municipality"), + ("Ishanath Municipality", "Ishanath Municipality"), + ("Gadhimai Municipality", "Gadhimai Municipality"), + ("Rajpur Municipality", "Rajpur Municipality"), + ( + "Durga Bhagawati Rural Municipality", + "Durga Bhagawati Rural Municipality", + ), + ("Nijagadh Municipality", "Nijagadh Municipality"), + ("Kolhabi Municipality", "Kolhabi Municipality"), + ( + "Parawanipur Rural Municipality", + "Parawanipur Rural Municipality", + ), + ( + "Prasauni Rural Municipality", + "Prasauni Rural Municipality", + ), + ( + "Bishrampur Rural Municipality", + "Bishrampur Rural Municipality", + ), + ("Pheta Rural Municipality", "Pheta Rural Municipality"), + ( + "Kalaiya Sub-Metropolitan City", + "Kalaiya Sub-Metropolitan City", + ), + ( + "Karaiyamai Rural Municipality", + "Karaiyamai Rural Municipality", + ), + ( + "Aadarsha Kotwal Rural Municipality", + "Aadarsha Kotwal Rural Municipality", + ), + ("Simroungadh Municipality", "Simroungadh Municipality"), + ("Pacharauta Municipality", "Pacharauta Municipality"), + ("Mahagadhimai Municipality", "Mahagadhimai Municipality"), + ("Devtal Rural Municipality", "Devtal Rural Municipality"), + ( + "Jitpur Simara Sub-Metropolitan City", + "Jitpur Simara Sub-Metropolitan City", + ), + ( + "Baragadhi Rural Municipality", + "Baragadhi Rural Municipality", + ), + ( + "Subarna Rural Municipality", + "Subarna Rural Municipality", + ), + ("Thori Rural Municipality", "Thori Rural Municipality"), + ( + "Jirabhawani Rural Municipality", + "Jirabhawani Rural Municipality", + ), + ( + "Jagarnathpur Rural Municipality", + "Jagarnathpur Rural Municipality", + ), + ( + "Sakhuwa Prasauni Rural Municipality", + "Sakhuwa Prasauni Rural Municipality", + ), + ("Parsagadhi Municipality", "Parsagadhi Municipality"), + ("Birgunj Metropolitan City", "Birgunj Metropolitan City"), + ("Bahudarmai Municipality", "Bahudarmai Municipality"), + ("Pokhariya Municipality", "Pokhariya Municipality"), + ( + "Kalikamai Rural Municipality", + "Kalikamai Rural Municipality", + ), + ( + "Dhobini Rural Municipality", + "Dhobini Rural Municipality", + ), + ( + "Pakaha Mainpur Rural Municipality", + "Pakaha Mainpur Rural Municipality", + ), + ( + "Bindabasini Rural Municipality", + "Bindabasini Rural Municipality", + ), + ( + "Paterwa Sugauli Rural Municipality", + "Paterwa Sugauli Rural Municipality", + ), + ( + "Chhipaharmai Rural Municipality", + "Chhipaharmai Rural Municipality", + ), + ( + "Chumanubri Rural Municipality", + "Chumanubri Rural Municipality", + ), + ( + "Sulikot Rural Municipality", + "Sulikot Rural Municipality", + ), + ( + "Dharche Rural Municipality", + "Dharche Rural Municipality", + ), + ( + "Aarughat Rural Municipality", + "Aarughat Rural Municipality", + ), + ( + "Bhimsen Rural Municipality", + "Bhimsen Rural Municipality", + ), + ( + "Siranchowk Rural Municipality", + "Siranchowk Rural Municipality", + ), + ("Gorkha Municipality", "Gorkha Municipality"), + ( + "Shahid Lakhan Rural Municipality", + "Shahid Lakhan Rural Municipality", + ), + ( + "Gandaki Rural Municipality", + "Gandaki Rural Municipality", + ), + ( + "Ajirkot Rural Municipality", + "Ajirkot Rural Municipality", + ), + ("Palungtar Municipality", "Palungtar Municipality"), + ( + "Narpa Bhumi Rural Municipality", + "Narpa Bhumi Rural Municipality", + ), + ("Chame Rural Municipality", "Chame Rural Municipality"), + ( + "Nashong Rural Municipality", + "Nashong Rural Municipality", + ), + ( + "Neshang Rural Municipality", + "Neshang Rural Municipality", + ), + ("Dalome Rural Municipality", "Dalome Rural Municipality"), + ( + "Gharpajhong Rural Municipality", + "Gharpajhong Rural Municipality", + ), + ( + "Lomanthang Rural Municipality", + "Lomanthang Rural Municipality", + ), + ( + "Thasang Rural Municipality", + "Thasang Rural Municipality", + ), + ( + "Bahragaun Muktikshetra Rural Municipality", + "Bahragaun Muktikshetra Rural Municipality", + ), + ( + "Annapurna Rural Municipality", + "Annapurna Rural Municipality", + ), + ( + "Raghuganga Rural Municipality", + "Raghuganga Rural Municipality", + ), + ( + "Dhawalagiri Rural Municipality", + "Dhawalagiri Rural Municipality", + ), + ("Malika Rural Municipality", "Malika Rural Municipality"), + ( + "Mangala Rural Municipality", + "Mangala Rural Municipality", + ), + ("Beni Municipality", "Beni Municipality"), + ("Madi Rural Municipality", "Madi Rural Municipality"), + ( + "Annapurna Rural Municipality", + "Annapurna Rural Municipality", + ), + ("Pokhara Metropolitan City", "Pokhara Metropolitan City"), + ("Rupa Rural Municipality", "Rupa Rural Municipality"), + ( + "Machhapuchchhre Rural Municipality", + "Machhapuchchhre Rural Municipality", + ), + ("Dordi Rural Municipality", "Dordi Rural Municipality"), + ( + "Marshyangdi Rural Municipality", + "Marshyangdi Rural Municipality", + ), + ("Madhya Nepal Municipality", "Madhya Nepal Municipality"), + ("Bensi Shahar Municipality", "Bensi Shahar Municipality"), + ("Rainas Municipality", "Rainas Municipality"), + ( + "Kwhola Sothar Rural Municipality", + "Kwhola Sothar Rural Municipality", + ), + ("Sundarbazar Municipality", "Sundarbazar Municipality"), + ( + "Dudhapokhari Rural Municipality", + "Dudhapokhari Rural Municipality", + ), + ("Bhanu Municipality", "Bhanu Municipality"), + ("Byas Municipality", "Byas Municipality"), + ("Myagde Rural Municipality", "Myagde Rural Municipality"), + ( + "Shuklagandaki Municipality", + "Shuklagandaki Municipality", + ), + ("Bhimad Municipality", "Bhimad Municipality"), + ( + "Ghiring Rural Municipality", + "Ghiring Rural Municipality", + ), + ( + "Devghat Rural Municipality", + "Devghat Rural Municipality", + ), + ( + "Bandipur Rural Municipality", + "Bandipur Rural Municipality", + ), + ( + "Aanbookhaireni Rural Municipality", + "Aanbookhaireni Rural Municipality", + ), + ( + "Rhishing Rural Municipality", + "Rhishing Rural Municipality", + ), + ("Gaidakot Municipality", "Gaidakot Municipality"), + ( + "Bulingtar Rural Municipality", + "Bulingtar Rural Municipality", + ), + ( + "Hupsekot Rural Municipality", + "Hupsekot Rural Municipality", + ), + ("Devchuli Municipality", "Devchuli Municipality"), + ("Kawasoti Municipality", "Kawasoti Municipality"), + ("Madhyabindu Municipality", "Madhyabindu Municipality"), + ( + "Binayi Tribeni Rural Municipality", + "Binayi Tribeni Rural Municipality", + ), + ( + "Bungdikali Rural Municipality", + "Bungdikali Rural Municipality", + ), + ( + "Phedikhola Rural Municipality", + "Phedikhola Rural Municipality", + ), + ( + "Aandhikhola Rural Municipality", + "Aandhikhola Rural Municipality", + ), + ( + "Arjun Choupari Rural Municipality", + "Arjun Choupari Rural Municipality", + ), + ("Bhirkot Municipality", "Bhirkot Municipality"), + ("Biruwa Rural Municipality", "Biruwa Rural Municipality"), + ("Chapakot Municipality", "Chapakot Municipality"), + ("Walling Municipality", "Walling Municipality"), + ("Galyang Municipality", "Galyang Municipality"), + ( + "Kaligandaki Rural Municipality", + "Kaligandaki Rural Municipality", + ), + ("Putalibazar Municipality", "Putalibazar Municipality"), + ( + "Harinas Rural Municipality", + "Harinas Rural Municipality", + ), + ("Modi Rural Municipality", "Modi Rural Municipality"), + ("Kushma Municipality", "Kushma Municipality"), + ("Phalebas Municipality", "Phalebas Municipality"), + ( + "Mahashila Rural Municipality", + "Mahashila Rural Municipality", + ), + ("Bihadi Rural Municipality", "Bihadi Rural Municipality"), + ("Paiyu Rural Municipality", "Paiyu Rural Municipality"), + ( + "Jaljala Rural Municipality", + "Jaljala Rural Municipality", + ), + ("Baglung Municipality", "Baglung Municipality"), + ( + "Tarakhola Rural Municipality", + "Tarakhola Rural Municipality", + ), + ( + "Tamankhola Rural Municipality", + "Tamankhola Rural Municipality", + ), + ("Dhorpatan Municipality", "Dhorpatan Municipality"), + ( + "Nisikhola Rural Municipality", + "Nisikhola Rural Municipality", + ), + ( + "Badigad Rural Municipality", + "Badigad Rural Municipality", + ), + ("Galkot Municipality", "Galkot Municipality"), + ("Jaimuni Municipality", "Jaimuni Municipality"), + ( + "Kathekhola Rural Municipality", + "Kathekhola Rural Municipality", + ), + ("Bareng Rural Municipality", "Bareng Rural Municipality"), + ( + "Gaurishankar Rural Municipality", + "Gaurishankar Rural Municipality", + ), + ("Bigu Rural Municipality", "Bigu Rural Municipality"), + ( + "Kalinchowk Rural Municipality", + "Kalinchowk Rural Municipality", + ), + ("Jiri Municipality", "Jiri Municipality"), + ( + "Tamakoshi Rural Municipality", + "Tamakoshi Rural Municipality", + ), + ("Melung Rural Municipality", "Melung Rural Municipality"), + ( + "Shailung Rural Municipality", + "Shailung Rural Municipality", + ), + ("Bhimeshwor Municipality", "Bhimeshwor Municipality"), + ( + "Baitedhar Rural Municipality", + "Baitedhar Rural Municipality", + ), + ("Jugal Rural Municipality", "Jugal Rural Municipality"), + ( + "Panchpokhari Thangpal Rural Municipality", + "Panchpokhari Thangpal Rural Municipality", + ), + ( + "Helambu Rural Municipality", + "Helambu Rural Municipality", + ), + ( + "Choutara Sangachowkgadhi Municipality", + "Choutara Sangachowkgadhi Municipality", + ), + ( + "Balephi Rural Municipality", + "Balephi Rural Municipality", + ), + ("Bahrabise Municipality", "Bahrabise Municipality"), + ( + "Lisankhu Pakhar Rural Municipality", + "Lisankhu Pakhar Rural Municipality", + ), + ( + "Sunkoshi Rural Municipality", + "Sunkoshi Rural Municipality", + ), + ( + "Bhotekoshi Rural Municipality", + "Bhotekoshi Rural Municipality", + ), + ("Melanchi Municipality", "Melanchi Municipality"), + ( + "Indrawoti Rural Municipality", + "Indrawoti Rural Municipality", + ), + ( + "Tripurasundari Rural Municipality", + "Tripurasundari Rural Municipality", + ), + ( + "Gosaikunda Rural Municipality", + "Gosaikunda Rural Municipality", + ), + ( + "Aama Chhodingmo Rural Municipality", + "Aama Chhodingmo Rural Municipality", + ), + ( + "Uttargaya Rural Municipality", + "Uttargaya Rural Municipality", + ), + ("Kalika Rural Municipality", "Kalika Rural Municipality"), + ( + "Naukunda Rural Municipality", + "Naukunda Rural Municipality", + ), + ( + "Khaniyabas Rural Municipality", + "Khaniyabas Rural Municipality", + ), + ( + "Ganga Jamuna Rural Municipality", + "Ganga Jamuna Rural Municipality", + ), + ( + "Tripurasundari Rural Municipality", + "Tripurasundari Rural Municipality", + ), + ( + "Netrawati Rural Municipality", + "Netrawati Rural Municipality", + ), + ("Neelakantha Municipality", "Neelakantha Municipality"), + ( + "Siddhalek Rural Municipality", + "Siddhalek Rural Municipality", + ), + ( + "Benighat Rorang Rural Municipality", + "Benighat Rorang Rural Municipality", + ), + ("Gajuri Rural Municipality", "Gajuri Rural Municipality"), + ( + "Galchhi Rural Municipality", + "Galchhi Rural Municipality", + ), + ("Thakre Rural Municipality", "Thakre Rural Municipality"), + ("Dhunibenshi Municipality", "Dhunibenshi Municipality"), + ( + "Rubi Valley Rural Municipality", + "Rubi Valley Rural Municipality", + ), + ( + "Jwalamukhi Rural Municipality", + "Jwalamukhi Rural Municipality", + ), + ("Tadi Rural Municipality", "Tadi Rural Municipality"), + ( + "Suryagadhi Rural Municipality", + "Suryagadhi Rural Municipality", + ), + ("Bidur Municipality", "Bidur Municipality"), + ( + "Kispang Rural Municipality", + "Kispang Rural Municipality", + ), + ( + "Meghang Rural Municipality", + "Meghang Rural Municipality", + ), + ("Belkotgadhi Municipality", "Belkotgadhi Municipality"), + ("Likhu Rural Municipality", "Likhu Rural Municipality"), + ( + "Panchakanya Rural Municipality", + "Panchakanya Rural Municipality", + ), + ( + "Shivapuri Rural Municipality", + "Shivapuri Rural Municipality", + ), + ("Kakani Rural Municipality", "Kakani Rural Municipality"), + ( + "Dupcheshwor Rural Municipality", + "Dupcheshwor Rural Municipality", + ), + ( + "Tarakeshwor Rural Municipality", + "Tarakeshwor Rural Municipality", + ), + ( + "Kageshwori Manahara Municipality", + "Kageshwori Manahara Municipality", + ), + ("Gokarneshwor Municipality", "Gokarneshwor Municipality"), + ( + "Budhanilkhantha Municipality", + "Budhanilkhantha Municipality", + ), + ("Tokha Municipality", "Tokha Municipality"), + ("Tarakeshwor Municipality", "Tarakeshwor Municipality"), + ("Nagarjun Municipality", "Nagarjun Municipality"), + ("Kirtipur Municipality", "Kirtipur Municipality"), + ("Dakshinkali Municipality", "Dakshinkali Municipality"), + ("Shankharapur Municipality", "Shankharapur Municipality"), + ( + "Kathmandu Metropolitan City", + "Kathmandu Metropolitan City", + ), + ("Chandragiri Municipality", "Chandragiri Municipality"), + ("Bhaktapur Municipality", "Bhaktapur Municipality"), + ( + "Madhyapur Thimi Municipality", + "Madhyapur Thimi Municipality", + ), + ("Suryabinayak Municipality", "Suryabinayak Municipality"), + ( + "Changunarayan Municipality", + "Changunarayan Municipality", + ), + ("Mahalaxmi Municipality", "Mahalaxmi Municipality"), + ( + "Lalitpur Metropolitan City", + "Lalitpur Metropolitan City", + ), + ("Godawari Municipality", "Godawari Municipality"), + ( + "Mahankal Rural Municipality", + "Mahankal Rural Municipality", + ), + ( + "Bagmati Rural Municipality", + "Bagmati Rural Municipality", + ), + ( + "Konjyosom Rural Municipality", + "Konjyosom Rural Municipality", + ), + ( + "Chauri Deurali Rural Municipality", + "Chauri Deurali Rural Municipality", + ), + ("Bhumlu Rural Municipality", "Bhumlu Rural Municipality"), + ( + "Mandan Deupur Municipality", + "Mandan Deupur Municipality", + ), + ("Banepa Municipality", "Banepa Municipality"), + ("Panchkhal Municipality", "Panchkhal Municipality"), + ("Temal Rural Municipality", "Temal Rural Municipality"), + ("Namobuddha Municipality", "Namobuddha Municipality"), + ("Panauti Municipality", "Panauti Municipality"), + ("Roshi Rural Municipality", "Roshi Rural Municipality"), + ( + "Mahabharat Rural Municipality", + "Mahabharat Rural Municipality", + ), + ( + "Khanikhola Rural Municipality", + "Khanikhola Rural Municipality", + ), + ("Dhulikhel Municipality", "Dhulikhel Municipality"), + ( + "Bethanchowk Rural Municipality", + "Bethanchowk Rural Municipality", + ), + ( + "Umakunda Rural Municipality", + "Umakunda Rural Municipality", + ), + ( + "Gokulganga Rural Municipality", + "Gokulganga Rural Municipality", + ), + ("Likhu Rural Municipality", "Likhu Rural Municipality"), + ("Manthali Municipality", "Manthali Municipality"), + ( + "Khandadevi Rural Municipality", + "Khandadevi Rural Municipality", + ), + ( + "Doramba Rural Municipality", + "Doramba Rural Municipality", + ), + ( + "Sunapati Rural Municipality", + "Sunapati Rural Municipality", + ), + ("Ramechhap Municipality", "Ramechhap Municipality"), + ("Dudhouli Municipality", "Dudhouli Municipality"), + ( + "Tinpatan Rural Municipality", + "Tinpatan Rural Municipality", + ), + ( + "Golanjor Rural Municipality", + "Golanjor Rural Municipality", + ), + ("Kamalamai Municipality", "Kamalamai Municipality"), + ( + "Sunkoshi Rural Municipality", + "Sunkoshi Rural Municipality", + ), + ("Marin Rural Municipality", "Marin Rural Municipality"), + ( + "Hariharpurgaghi Rural Municipality", + "Hariharpurgaghi Rural Municipality", + ), + ( + "Phikkal Rural Municipality", + "Phikkal Rural Municipality", + ), + ( + "Ghyanglekha Rural Municipality", + "Ghyanglekha Rural Municipality", + ), + ( + "Indrasarowar Rural Municipality", + "Indrasarowar Rural Municipality", + ), + ("Thaha Municipality", "Thaha Municipality"), + ( + "Kailash Rural Municipality", + "Kailash Rural Municipality", + ), + ( + "Manahari Rural Municipality", + "Manahari Rural Municipality", + ), + ( + "Hetauda Sub-Metropolitan City", + "Hetauda Sub-Metropolitan City", + ), + ( + "Makawanpurgadhi Rural Municipality", + "Makawanpurgadhi Rural Municipality", + ), + ( + "Bakaiya Rural Municipality", + "Bakaiya Rural Municipality", + ), + ( + "Bagmati Rural Municipality", + "Bagmati Rural Municipality", + ), + ( + "Raksirang Rural Municipality", + "Raksirang Rural Municipality", + ), + ( + "Bhimphedi Rural Municipality", + "Bhimphedi Rural Municipality", + ), + ("Rapti Municipality", "Rapti Municipality"), + ("Kalika Municipality", "Kalika Municipality"), + ( + "Bharatpur Metropolitan City", + "Bharatpur Metropolitan City", + ), + ("Ratnanagar Municipality", "Ratnanagar Municipality"), + ("Khairahani Municipality", "Khairahani Municipality"), + ("Madi Municipality", "Madi Municipality"), + ( + "Ichchha Kamana Rural Municipality", + "Ichchha Kamana Rural Municipality", + ), + ("Sisne Rural Municipality", "Sisne Rural Municipality"), + ("Bhoome Rural Municipality", "Bhoome Rural Municipality"), + ( + "Putha Uttanganga Rural Municipality", + "Putha Uttanganga Rural Municipality", + ), + ( + "Sunchhahari Rural Municipality", + "Sunchhahari Rural Municipality", + ), + ( + "Thawang Rural Municipality", + "Thawang Rural Municipality", + ), + ( + "Duikholi Rural Municipality", + "Duikholi Rural Municipality", + ), + ("Madi Rural Municipality", "Madi Rural Municipality"), + ( + "Tribeni Rural Municipality", + "Tribeni Rural Municipality", + ), + ("Rolpa Municipality", "Rolpa Municipality"), + ( + "Runtigadhi Rural Municipality", + "Runtigadhi Rural Municipality", + ), + ( + "Sunilsmiriti Rural Municipality", + "Sunilsmiriti Rural Municipality", + ), + ("Lungri Rural Municipality", "Lungri Rural Municipality"), + ( + "Gangadev Rural Municipality", + "Gangadev Rural Municipality", + ), + ( + "Naubahini Rural Municipality", + "Naubahini Rural Municipality", + ), + ( + "Jhimaruk Rural Municipality", + "Jhimaruk Rural Municipality", + ), + ("Pyuthan Municipality", "Pyuthan Municipality"), + ("Sworgadwari Municipality", "Sworgadwari Municipality"), + ( + "Mandavi Rural Municipality", + "Mandavi Rural Municipality", + ), + ( + "Aairawati Rural Municipality", + "Aairawati Rural Municipality", + ), + ( + "Sarumarani Rural Municipality", + "Sarumarani Rural Municipality", + ), + ( + "Gaumukhi Rural Municipality", + "Gaumukhi Rural Municipality", + ), + ( + "Mallarani Rural Municipality", + "Mallarani Rural Municipality", + ), + ( + "Kali Gandaki Rural Municipality", + "Kali Gandaki Rural Municipality", + ), + ( + "Satyawoti Rural Municipality", + "Satyawoti Rural Municipality", + ), + ("Musikot Municipality", "Musikot Municipality"), + ("Isma Rural Municipality", "Isma Rural Municipality"), + ("Malika Rural Municipality", "Malika Rural Municipality"), + ("Madane Rural Municipality", "Madane Rural Municipality"), + ( + "Dhurkot Rural Municipality", + "Dhurkot Rural Municipality", + ), + ("Resunga Municipality", "Resunga Municipality"), + ( + "Chhatrakot Rural Municipality", + "Chhatrakot Rural Municipality", + ), + ("Ruru Rural Municipality", "Ruru Rural Municipality"), + ( + "Chandrakot Rural Municipality", + "Chandrakot Rural Municipality", + ), + ( + "Gulmi Durbar Rural Municipality", + "Gulmi Durbar Rural Municipality", + ), + ( + "Chhatradev Rural Municipality", + "Chhatradev Rural Municipality", + ), + ( + "Malarani Rural Municipality", + "Malarani Rural Municipality", + ), + ("Bhumikasthan Municipality", "Bhumikasthan Municipality"), + ("Sandhikharka Municipality", "Sandhikharka Municipality"), + ("Shitaganga Municipality", "Shitaganga Municipality"), + ("Panini Rural Municipality", "Panini Rural Municipality"), + ("Rampur Municipality", "Rampur Municipality"), + ( + "Purbakhola Rural Municipality", + "Purbakhola Rural Municipality", + ), + ("Rambha Rural Municipality", "Rambha Rural Municipality"), + ( + "Baganaskali Rural Municipality", + "Baganaskali Rural Municipality", + ), + ("Tansen Municipality", "Tansen Municipality"), + ( + "Rainadevi Chhahara Rural Municipality", + "Rainadevi Chhahara Rural Municipality", + ), + ( + "Mathagadhi Rural Municipality", + "Mathagadhi Rural Municipality", + ), + ( + "Ribdikot Rural Municipality", + "Ribdikot Rural Municipality", + ), + ("Tinau Rural Municipality", "Tinau Rural Municipality"), + ("Nisdi Rural Municipality", "Nisdi Rural Municipality"), + ("Devdaha Municipality", "Devdaha Municipality"), + ( + "Butwal Sub-Metropolitan City", + "Butwal Sub-Metropolitan City", + ), + ("Sainamaina Municipality", "Sainamaina Municipality"), + ( + "Kanchan Rural Municipality", + "Kanchan Rural Municipality", + ), + ( + "Suddhodhan Rural Municipality", + "Suddhodhan Rural Municipality", + ), + ("Siyari Rural Municipality", "Siyari Rural Municipality"), + ("Tilottama Municipality", "Tilottama Municipality"), + ( + "Om Satiya Rural Municipality", + "Om Satiya Rural Municipality", + ), + ("Rohini Rural Municipality", "Rohini Rural Municipality"), + ( + "Mayadevi Rural Municipality", + "Mayadevi Rural Municipality", + ), + ( + "Lumbini Sanskritik Municipality", + "Lumbini Sanskritik Municipality", + ), + ( + "Kotahimai Rural Municipality", + "Kotahimai Rural Municipality", + ), + ( + "Sammarimai Rural Municipality", + "Sammarimai Rural Municipality", + ), + ( + "Marchawari Rural Municipality", + "Marchawari Rural Municipality", + ), + ( + "Gaidahawa Rural Municipality", + "Gaidahawa Rural Municipality", + ), + ( + "Siddharthanagar Municipality", + "Siddharthanagar Municipality", + ), + ("Banganga Municipality", "Banganga Municipality"), + ("Shivaraj Municipality", "Shivaraj Municipality"), + ( + "Bijayanagar Rural Municipality", + "Bijayanagar Rural Municipality", + ), + ("Krishnanagar Municipality", "Krishnanagar Municipality"), + ("Maharajganj Municipality", "Maharajganj Municipality"), + ( + "Yasodhara Rural Municipality", + "Yasodhara Rural Municipality", + ), + ( + "Mayadevi Rural Municipality", + "Mayadevi Rural Municipality", + ), + ( + "Shuddhodhan Rural Municipality", + "Shuddhodhan Rural Municipality", + ), + ("Buddhabhumi Municipality", "Buddhabhumi Municipality"), + ("Kapilvastu Municipality", "Kapilvastu Municipality"), + ( + "Bangalachuli Rural Municipality", + "Bangalachuli Rural Municipality", + ), + ( + "Tulsipur Sub-Metropolitan City", + "Tulsipur Sub-Metropolitan City", + ), + ( + "Shantinagar Rural Municipality", + "Shantinagar Rural Municipality", + ), + ("Babai Rural Municipality", "Babai Rural Municipality"), + ( + "Dangisharan Rural Municipality", + "Dangisharan Rural Municipality", + ), + ("Lamahi Municipality", "Lamahi Municipality"), + ( + "Ghorahi Sub-Metropolitan City", + "Ghorahi Sub-Metropolitan City", + ), + ("Rapti Rural Municipality", "Rapti Rural Municipality"), + ( + "Gadhawa Rural Municipality", + "Gadhawa Rural Municipality", + ), + ("Rajpur Rural Municipality", "Rajpur Rural Municipality"), + ( + "Khajura Rural Municipality", + "Khajura Rural Municipality", + ), + ("Janaki Rural Municipality", "Janaki Rural Municipality"), + ( + "Rapti Sonari Rural Municipality", + "Rapti Sonari Rural Municipality", + ), + ("Kohalpur Municipality", "Kohalpur Municipality"), + ( + "Baijanath Rural Municipality", + "Baijanath Rural Municipality", + ), + ( + "Nepalgunj Sub-Metropolitan City", + "Nepalgunj Sub-Metropolitan City", + ), + ("Duduwa Rural Municipality", "Duduwa Rural Municipality"), + ( + "Narainapur Rural Municipality", + "Narainapur Rural Municipality", + ), + ("Thakurbaba Municipality", "Thakurbaba Municipality"), + ("Geruwa Rural Municipality", "Geruwa Rural Municipality"), + ("Rajapur Municipality", "Rajapur Municipality"), + ("Madhuwan Municipality", "Madhuwan Municipality"), + ("Bansgadhi Municipality", "Bansgadhi Municipality"), + ("Barbardiya Municipality", "Barbardiya Municipality"), + ("Gulariya Municipality", "Gulariya Municipality"), + ( + "Badhaiyatal Rural Municipality", + "Badhaiyatal Rural Municipality", + ), + ("Bardaghat Municipality", "Bardaghat Municipality"), + ("Sunawal Municipality", "Sunawal Municipality"), + ("Ramgram Municipality", "Ramgram Municipality"), + ( + "Palhinandan Rural Municipality", + "Palhinandan Rural Municipality", + ), + ( + "Sarawal Rural Municipality", + "Sarawal Rural Municipality", + ), + ("Susta Rural Municipality", "Susta Rural Municipality"), + ( + "Pratapapur Rural Municipality", + "Pratapapur Rural Municipality", + ), + ( + "Mudkechula Rural Municipality", + "Mudkechula Rural Municipality", + ), + ( + "Tripurasundari Municipality", + "Tripurasundari Municipality", + ), + ("Thulibheri Municipality", "Thulibheri Municipality"), + ("Kaike Rural Municipality", "Kaike Rural Municipality"), + ( + "Chharka Tangsong Rural Municipality", + "Chharka Tangsong Rural Municipality", + ), + ( + "Shey Phoksundo Rural Municipality", + "Shey Phoksundo Rural Municipality", + ), + ( + "Jagadulla Rural Municipality", + "Jagadulla Rural Municipality", + ), + ( + "Dolpo Buddha Rural Municipality", + "Dolpo Buddha Rural Municipality", + ), + ( + "Mugumakarmarog Rural Municipality", + "Mugumakarmarog Rural Municipality", + ), + ( + "Chhayanath Rara Municipality", + "Chhayanath Rara Municipality", + ), + ( + "Khatyad Rural Municipality", + "Khatyad Rural Municipality", + ), + ("Soru Rural Municipality", "Soru Rural Municipality"), + ( + "Patarasi Rural Municipality", + "Patarasi Rural Municipality", + ), + ( + "Kanaka Sundari Rural Municipality", + "Kanaka Sundari Rural Municipality", + ), + ("Sinja Rural Municipality", "Sinja Rural Municipality"), + ("Chandannath Municipality", "Chandannath Municipality"), + ( + "Guthichaur Rural Municipality", + "Guthichaur Rural Municipality", + ), + ( + "Tatopani Rural Municipality", + "Tatopani Rural Municipality", + ), + ("Tila Rural Municipality", "Tila Rural Municipality"), + ("Hima Rural Municipality", "Hima Rural Municipality"), + ( + "Mahawai Rural Municipality", + "Mahawai Rural Municipality", + ), + ("Kalika Rural Municipality", "Kalika Rural Municipality"), + ("Palata Rural Municipality", "Palata Rural Municipality"), + ( + "Pachal Jharana Rural Municipality", + "Pachal Jharana Rural Municipality", + ), + ("Raskot Municipality", "Raskot Municipality"), + ( + "Sanni Tribeni Rural Municipality", + "Sanni Tribeni Rural Municipality", + ), + ( + "Naraharinath Rural Municipality", + "Naraharinath Rural Municipality", + ), + ("Khandachakra Municipality", "Khandachakra Municipality"), + ("Tilagupha Municipality", "Tilagupha Municipality"), + ( + "Naumule Rural Municipality", + "Naumule Rural Municipality", + ), + ("Mahabu Rural Municipality", "Mahabu Rural Municipality"), + ( + "Bhairabi Rural Municipality", + "Bhairabi Rural Municipality", + ), + ( + "Thantikandh Rural Municipality", + "Thantikandh Rural Municipality", + ), + ("Aathbis Municipality", "Aathbis Municipality"), + ( + "Chamunda Bindrasaini Municipality", + "Chamunda Bindrasaini Municipality", + ), + ("Dullu Municipality", "Dullu Municipality"), + ("Narayan Municipality", "Narayan Municipality"), + ( + "Bhagawatimai Rural Municipality", + "Bhagawatimai Rural Municipality", + ), + ( + "Dungeshwor Rural Municipality", + "Dungeshwor Rural Municipality", + ), + ("Gurans Rural Municipality", "Gurans Rural Municipality"), + ( + "Shivalaya Rural Municipality", + "Shivalaya Rural Municipality", + ), + ("Bheri Municipality", "Bheri Municipality"), + ( + "Tribeni Nalagad Municipality", + "Tribeni Nalagad Municipality", + ), + ( + "Barekot Rural Municipality", + "Barekot Rural Municipality", + ), + ("Kuse Rural Municipality", "Kuse Rural Municipality"), + ( + "Junichande Rural Municipality", + "Junichande Rural Municipality", + ), + ("Chhedagad Municipality", "Chhedagad Municipality"), + ("Darma Rural Municipality", "Darma Rural Municipality"), + ( + "Kumakh Malika Rural Municipality", + "Kumakh Malika Rural Municipality", + ), + ( + "Banagad Kupinde Municipality", + "Banagad Kupinde Municipality", + ), + ( + "Dhorchaur Rural Municipality", + "Dhorchaur Rural Municipality", + ), + ("Bagachour Municipality", "Bagachour Municipality"), + ( + "Chhatreshwori Rural Municipality", + "Chhatreshwori Rural Municipality", + ), + ("Sharada Municipality", "Sharada Municipality"), + ( + "Kalimati Rural Municipality", + "Kalimati Rural Municipality", + ), + ( + "Tribeni Rural Municipality", + "Tribeni Rural Municipality", + ), + ( + "Kapurkot Rural Municipality", + "Kapurkot Rural Municipality", + ), + ("Simta Rural Municipality", "Simta Rural Municipality"), + ( + "Chingad Rural Municipality", + "Chingad Rural Municipality", + ), + ("Lekabeshi Municipality", "Lekabeshi Municipality"), + ("Gurbhakot Municipality", "Gurbhakot Municipality"), + ("Bheriganga Municipality", "Bheriganga Municipality"), + ( + "Birendranagar Municipality", + "Birendranagar Municipality", + ), + ( + "Barahatal Rural Municipality", + "Barahatal Rural Municipality", + ), + ("Panchapuri Municipality", "Panchapuri Municipality"), + ( + "Chaukune Rural Municipality", + "Chaukune Rural Municipality", + ), + ( + "Chankheli Rural Municipality", + "Chankheli Rural Municipality", + ), + ( + "Kharpunath Rural Municipality", + "Kharpunath Rural Municipality", + ), + ("Simkot Rural Municipality", "Simkot Rural Municipality"), + ("Namkha Rural Municipality", "Namkha Rural Municipality"), + ( + "Sarkegad Rural Municipality", + "Sarkegad Rural Municipality", + ), + ( + "Adanchuli Rural Municipality", + "Adanchuli Rural Municipality", + ), + ( + "Tanjakot Rural Municipality", + "Tanjakot Rural Municipality", + ), + ("Aathabisakot Municipality", "Aathabisakot Municipality"), + ( + "Sanibheri Rural Municipality", + "Sanibheri Rural Municipality", + ), + ( + "Banphikot Rural Municipality", + "Banphikot Rural Municipality", + ), + ("Musikot Municipality", "Musikot Municipality"), + ( + "Tribeni Rural Municipality", + "Tribeni Rural Municipality", + ), + ("Chaurjahari Municipality", "Chaurjahari Municipality"), + ( + "Mikwakhola Rural Municipality", + "Mikwakhola Rural Municipality", + ), + ( + "Maiwakhola Rural Municipality", + "Maiwakhola Rural Municipality", + ), + ( + "Aatharai Tribeni Rural Municipality", + "Aatharai Tribeni Rural Municipality", + ), + ("Phungling Municipality", "Phungling Municipality"), + ( + "Pathivara Yangwarak Rural Municipality", + "Pathivara Yangwarak Rural Municipality", + ), + ( + "Sirijanga Rural Municipality", + "Sirijanga Rural Municipality", + ), + ( + "Phaktanlung Rural Municipality", + "Phaktanlung Rural Municipality", + ), + ( + "Meringden Rural Municipality", + "Meringden Rural Municipality", + ), + ( + "Sidingba Rural Municipality", + "Sidingba Rural Municipality", + ), + ( + "Bhotkhola Rural Municipality", + "Bhotkhola Rural Municipality", + ), + ("Makalu Rural Municipality", "Makalu Rural Municipality"), + ( + "Silichong Rural Municipality", + "Silichong Rural Municipality", + ), + ( + "Chichila Rural Municipality", + "Chichila Rural Municipality", + ), + ( + "Sabhapokhari Rural Municipality", + "Sabhapokhari Rural Municipality", + ), + ("Panchakhapan Municipality", "Panchakhapan Municipality"), + ("Chainapur Municipality", "Chainapur Municipality"), + ("Madi Municipality", "Madi Municipality"), + ("Dharmadevi Municipality", "Dharmadevi Municipality"), + ("Khandabari Municipality", "Khandabari Municipality"), + ( + "Mahakulung Rural Municipality", + "Mahakulung Rural Municipality", + ), + ("Sotang Rural Municipality", "Sotang Rural Municipality"), + ( + "Dhudhakoshi Rural Municipality", + "Dhudhakoshi Rural Municipality", + ), + ( + "Dhudha Koushika Rural Municipality", + "Dhudha Koushika Rural Municipality", + ), + ( + "Necha Salyan Rural Municipality", + "Necha Salyan Rural Municipality", + ), + ( + "Solu Dhudhakunda Municipality", + "Solu Dhudhakunda Municipality", + ), + ( + "Likhu Pike Rural Municipality", + "Likhu Pike Rural Municipality", + ), + ( + "Khumbu Pasanglhamu Rural Municipality", + "Khumbu Pasanglhamu Rural Municipality", + ), + ("Siddhicharan Municipality", "Siddhicharan Municipality"), + ("Molung Rural Municipality", "Molung Rural Municipality"), + ( + "Khiji Demba Rural Municipality", + "Khiji Demba Rural Municipality", + ), + ("Likhu Rural Municipality", "Likhu Rural Municipality"), + ( + "Champadevi Rural Municipality", + "Champadevi Rural Municipality", + ), + ( + "Sunkoshi Rural Municipality", + "Sunkoshi Rural Municipality", + ), + ( + "Chishankhu Gadhi Rural Municipality", + "Chishankhu Gadhi Rural Municipality", + ), + ( + "Manebhanjyang Rural Municipality", + "Manebhanjyang Rural Municipality", + ), + ( + "Kepilasgadhi Rural Municipality", + "Kepilasgadhi Rural Municipality", + ), + ( + "Aiselukharka Rural Municipality", + "Aiselukharka Rural Municipality", + ), + ( + "Lamidanda Rural Municipality", + "Lamidanda Rural Municipality", + ), + ( + "Halesi Tuwachung Municipality", + "Halesi Tuwachung Municipality", + ), + ("Sakela Rural Municipality", "Sakela Rural Municipality"), + ( + "Diprung Rural Municipality", + "Diprung Rural Municipality", + ), + ( + "Khotehang Rural Municipality", + "Khotehang Rural Municipality", + ), + ( + "Jante Dhunga Rural Municipality", + "Jante Dhunga Rural Municipality", + ), + ( + "Barahapokhari Rural Municipality", + "Barahapokhari Rural Municipality", + ), + ( + "Diktel Rupakot Majhuwagadhi Municipality", + "Diktel Rupakot Majhuwagadhi Municipality", + ), + ("Shadananda Municipality", "Shadananda Municipality"), + ( + "Tyamke Maiyum Rural Municipality", + "Tyamke Maiyum Rural Municipality", + ), + ("Bhojpur Municipality", "Bhojpur Municipality"), + ( + "Pauwa Dunma Rural Municipality", + "Pauwa Dunma Rural Municipality", + ), + ( + "Ramprasad Rai Rural Municipality", + "Ramprasad Rai Rural Municipality", + ), + ( + "Hatuwagadhi Rural Municipality", + "Hatuwagadhi Rural Municipality", + ), + ( + "Aamchowk Rural Municipality", + "Aamchowk Rural Municipality", + ), + ( + "Salpa Silichho Rural Municipality", + "Salpa Silichho Rural Municipality", + ), + ("Arun Rural Municipality", "Arun Rural Municipality"), + ("Pakhribas Municipality", "Pakhribas Municipality"), + ( + "Chhathar Jorpati Rural Municipality", + "Chhathar Jorpati Rural Municipality", + ), + ("Dhankuta Municipality", "Dhankuta Municipality"), + ( + "Sangurigadhi Rural Municipality", + "Sangurigadhi Rural Municipality", + ), + ( + "Chaubise Rural Municipality", + "Chaubise Rural Municipality", + ), + ("Mahalaxmi Municipality", "Mahalaxmi Municipality"), + ( + "Khalsa Chhintang Sahidbhumi Rural Municipality", + "Khalsa Chhintang Sahidbhumi Rural Municipality", + ), + ( + "Aatharai Rural Municipality", + "Aatharai Rural Municipality", + ), + ("Phedap Rural Municipality", "Phedap Rural Municipality"), + ( + "Menchhayayem Rural Municipality", + "Menchhayayem Rural Municipality", + ), + ("Myanglung Municipality", "Myanglung Municipality"), + ("Laligurans Municipality", "Laligurans Municipality"), + ( + "Chhathar Rural Municipality", + "Chhathar Rural Municipality", + ), + ( + "Hilihan Rural Municipality", + "Hilihan Rural Municipality", + ), + ( + "Falelung Rural Municipality", + "Falelung Rural Municipality", + ), + ("Phidim Municipality", "Phidim Municipality"), + ( + "Falgunanda Rural Municipality", + "Falgunanda Rural Municipality", + ), + ( + "Kummayak Rural Municipality", + "Kummayak Rural Municipality", + ), + ( + "Miklajung Rural Municipality", + "Miklajung Rural Municipality", + ), + ( + "Yangbarak Rural Municipality", + "Yangbarak Rural Municipality", + ), + ( + "Tumbewa Rural Municipality", + "Tumbewa Rural Municipality", + ), + ( + "Mai Jogmai Rural Municipality", + "Mai Jogmai Rural Municipality", + ), + ( + "Sandakpur Rural Municipality", + "Sandakpur Rural Municipality", + ), + ("Ilam Municipality", "Ilam Municipality"), + ("Deumai Municipality", "Deumai Municipality"), + ( + "Mangsebung Rural Municipality", + "Mangsebung Rural Municipality", + ), + ( + "Chulachuli Rural Municipality", + "Chulachuli Rural Municipality", + ), + ("Mai Municipality", "Mai Municipality"), + ("Suryodaya Municipality", "Suryodaya Municipality"), + ("Rong Rural Municipality", "Rong Rural Municipality"), + ( + "Fakfokathum Rural Municipality", + "Fakfokathum Rural Municipality", + ), + ("Mechinagar Municipality", "Mechinagar Municipality"), + ("Arjundhara Municipality", "Arjundhara Municipality"), + ("Kankai Municipality", "Kankai Municipality"), + ( + "Shivasatakshi Municipality", + "Shivasatakshi Municipality", + ), + ("Kamal Rural Municipality", "Kamal Rural Municipality"), + ("Damak Municipality", "Damak Municipality"), + ("Gauradaha Municipality", "Gauradaha Municipality"), + ( + "Barhadashi Rural Municipality", + "Barhadashi Rural Municipality", + ), + ( + "Haldibari Rural Municipality", + "Haldibari Rural Municipality", + ), + ("Bhadrapur Municipality", "Bhadrapur Municipality"), + ( + "Kachanakawal Rural Municipality", + "Kachanakawal Rural Municipality", + ), + ( + "Buddhashanti Rural Municipality", + "Buddhashanti Rural Municipality", + ), + ( + "Gauriganj Rural Municipality", + "Gauriganj Rural Municipality", + ), + ("Jhapa Rural Municipality", "Jhapa Rural Municipality"), + ("Birtamod Municipality", "Birtamod Municipality"), + ( + "Miklajung Rural Municipality", + "Miklajung Rural Municipality", + ), + ("Letang Municipality", "Letang Municipality"), + ( + "Sundarharaicha Municipality", + "Sundarharaicha Municipality", + ), + ("Belbari Municipality", "Belbari Municipality"), + ( + "Kanepokhari Rural Municipality", + "Kanepokhari Rural Municipality", + ), + ( + "Pathari Shanishchare Municipality", + "Pathari Shanishchare Municipality", + ), + ("Urlabari Municipality", "Urlabari Municipality"), + ("Ratuwamai Municipality", "Ratuwamai Municipality"), + ("Rangeli Municipality", "Rangeli Municipality"), + ( + "Gramthan Rural Municipality", + "Gramthan Rural Municipality", + ), + ( + "Budhiganga Rural Municipality", + "Budhiganga Rural Municipality", + ), + ( + "Biratnagar Metropolitan City", + "Biratnagar Metropolitan City", + ), + ( + "Dhanapalthan Rural Municipality", + "Dhanapalthan Rural Municipality", + ), + ("Jahada Rural Municipality", "Jahada Rural Municipality"), + ( + "Kerabari Rural Municipality", + "Kerabari Rural Municipality", + ), + ("Sunwarshi Municipality", "Sunwarshi Municipality"), + ( + "Katahari Rural Municipality", + "Katahari Rural Municipality", + ), + ( + "Dharan Sub-Metropolitan City", + "Dharan Sub-Metropolitan City", + ), + ("Baraha Municipality", "Baraha Municipality"), + ("Koshi Rural Municipality", "Koshi Rural Municipality"), + ("Ramdhuni Municipality", "Ramdhuni Municipality"), + ( + "Itahari Sub-Metropolitan City", + "Itahari Sub-Metropolitan City", + ), + ("Duhabi Municipality", "Duhabi Municipality"), + ("Gadhi Rural Municipality", "Gadhi Rural Municipality"), + ("Inaruwa Municipality", "Inaruwa Municipality"), + ( + "Harinagara Rural Municipality", + "Harinagara Rural Municipality", + ), + ("Barju Rural Municipality", "Barju Rural Municipality"), + ( + "Bhokraha Rural Municipality", + "Bhokraha Rural Municipality", + ), + ( + "Dewangunj Rural Municipality", + "Dewangunj Rural Municipality", + ), + ("Belaka Municipality", "Belaka Municipality"), + ( + "Chaudandigadhi Municipality", + "Chaudandigadhi Municipality", + ), + ("Triyuga Municipality", "Triyuga Municipality"), + ( + "Rautamai Rural Municipality", + "Rautamai Rural Municipality", + ), + ( + "Sunkoshi Rural Municipality", + "Sunkoshi Rural Municipality", + ), + ("Katari Municipality", "Katari Municipality"), + ( + "Udayapurgadhi Rural Municipality", + "Udayapurgadhi Rural Municipality", + ), + ("Tapli Rural Municipality", "Tapli Rural Municipality"), + ], + max_length=100, + ), + ), + ], + ), + ] diff --git a/example/demo/migrations/0002_alter_person_phone_number.py b/example/demo/migrations/0002_alter_person_phone_number.py new file mode 100644 index 0000000..9e63047 --- /dev/null +++ b/example/demo/migrations/0002_alter_person_phone_number.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.1 on 2026-01-28 15:37 + +import django_nepkit.models +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("demo", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="person", + name="phone_number", + field=django_nepkit.models.NepaliPhoneNumberField(max_length=10), + ), + ] diff --git a/example/demo/migrations/0003_person_created_at_person_updated_at.py b/example/demo/migrations/0003_person_created_at_person_updated_at.py new file mode 100644 index 0000000..5f9abb2 --- /dev/null +++ b/example/demo/migrations/0003_person_created_at_person_updated_at.py @@ -0,0 +1,27 @@ +# Generated by Django 6.0.1 on 2026-01-28 15:38 + +import django.utils.timezone +import django_nepkit.models +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("demo", "0002_alter_person_phone_number"), + ] + + operations = [ + migrations.AddField( + model_name="person", + name="created_at", + field=django_nepkit.models.NepaliTimeField( + auto_now_add=True, default=django.utils.timezone.now + ), + preserve_default=False, + ), + migrations.AddField( + model_name="person", + name="updated_at", + field=django_nepkit.models.NepaliTimeField(auto_now=True), + ), + ] diff --git a/example/demo/migrations/0004_alter_person_created_at_alter_person_updated_at.py b/example/demo/migrations/0004_alter_person_created_at_alter_person_updated_at.py new file mode 100644 index 0000000..858804d --- /dev/null +++ b/example/demo/migrations/0004_alter_person_created_at_alter_person_updated_at.py @@ -0,0 +1,23 @@ +# Generated by Django 6.0.1 on 2026-01-28 15:39 + +import django_nepkit.models +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("demo", "0003_person_created_at_person_updated_at"), + ] + + operations = [ + migrations.AlterField( + model_name="person", + name="created_at", + field=django_nepkit.models.NepaliDateField(auto_now_add=True), + ), + migrations.AlterField( + model_name="person", + name="updated_at", + field=django_nepkit.models.NepaliDateField(auto_now=True), + ), + ] diff --git a/example/demo/migrations/0005_alter_person_created_at_alter_person_updated_at.py b/example/demo/migrations/0005_alter_person_created_at_alter_person_updated_at.py new file mode 100644 index 0000000..be5f940 --- /dev/null +++ b/example/demo/migrations/0005_alter_person_created_at_alter_person_updated_at.py @@ -0,0 +1,27 @@ +# Generated by Django 6.0.1 on 2026-01-28 16:55 + +import django_nepkit.models +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("demo", "0004_alter_person_created_at_alter_person_updated_at"), + ] + + operations = [ + migrations.AlterField( + model_name="person", + name="created_at", + field=django_nepkit.models.NepaliDateTimeField( + auto_now_add=True, blank=True, editable=False, max_length=19 + ), + ), + migrations.AlterField( + model_name="person", + name="updated_at", + field=django_nepkit.models.NepaliDateTimeField( + auto_now=True, blank=True, editable=False, max_length=19 + ), + ), + ] diff --git a/example/demo/migrations/__init__.py b/example/demo/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/example/demo/models.py b/example/demo/models.py new file mode 100644 index 0000000..2c3f28f --- /dev/null +++ b/example/demo/models.py @@ -0,0 +1,27 @@ +from django.db import models +from django_nepkit import ( + NepaliDateField, + NepaliTimeField, + NepaliPhoneNumberField, + NepaliDateTimeField, + ProvinceField, + DistrictField, + MunicipalityField, +) + + +class Person(models.Model): + name = models.CharField(max_length=100) + birth_date = NepaliDateField() + registration_time = NepaliTimeField(auto_now_add=True) + phone_number = NepaliPhoneNumberField() + + # Address chaining + province = ProvinceField() + district = DistrictField() + municipality = MunicipalityField() + created_at = NepaliDateTimeField(auto_now_add=True) + updated_at = NepaliDateTimeField(auto_now=True) + + def __str__(self): + return self.name diff --git a/example/demo/templates/demo/person_form.html b/example/demo/templates/demo/person_form.html new file mode 100644 index 0000000..e719960 --- /dev/null +++ b/example/demo/templates/demo/person_form.html @@ -0,0 +1,34 @@ + + + + Nepali Fields Demo - Add + + {{ form.media }} + + +

Add New Person

+
+ {% csrf_token %} + {% for field in form %} +
+ {{ field.label_tag }} + {{ field }} + {% if field.help_text %} + {{ field.help_text }} + {% endif %} + {% for error in field.errors %} +

{{ error }}

+ {% endfor %} +
+ {% endfor %} + +
+
+ Back to List + + diff --git a/example/demo/templates/demo/person_list.html b/example/demo/templates/demo/person_list.html new file mode 100644 index 0000000..b80b9d2 --- /dev/null +++ b/example/demo/templates/demo/person_list.html @@ -0,0 +1,48 @@ +{% load nepali %} + + + + Nepali Fields Demo - List + + + +

Person List

+ Add New Person +
+ + + + + + + + + + + + + + {% for person in persons %} + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
NameBirth Date (BS)Birth Date (Devanagari)PhoneProvinceDistrictMunicipality
{{ person.name }}{{ person.birth_date|nepali_date }}{{ person.birth_date|nepali_date_ne }}{{ person.phone_number }}{{ person.province }}{{ person.district }}{{ person.municipality }}
No records found.
+ + diff --git a/example/demo/tests.py b/example/demo/tests.py new file mode 100644 index 0000000..a39b155 --- /dev/null +++ b/example/demo/tests.py @@ -0,0 +1 @@ +# Create your tests here. diff --git a/example/demo/urls.py b/example/demo/urls.py new file mode 100644 index 0000000..5d7c7a3 --- /dev/null +++ b/example/demo/urls.py @@ -0,0 +1,9 @@ +from django.urls import path +from . import views + +app_name = "demo" + +urlpatterns = [ + path("", views.person_list, name="person-list"), + path("add/", views.person_create, name="person-create"), +] diff --git a/example/demo/views.py b/example/demo/views.py new file mode 100644 index 0000000..09c8968 --- /dev/null +++ b/example/demo/views.py @@ -0,0 +1,32 @@ +from django.shortcuts import render, redirect +from .models import Person +from django import forms + + +class PersonForm(forms.ModelForm): + class Meta: + model = Person + fields = [ + "name", + "birth_date", + "phone_number", + "province", + "district", + "municipality", + ] + + +def person_list(request): + persons = Person.objects.all() + return render(request, "demo/person_list.html", {"persons": persons}) + + +def person_create(request): + if request.method == "POST": + form = PersonForm(request.POST) + if form.is_valid(): + form.save() + return redirect("demo:person-list") + else: + form = PersonForm() + return render(request, "demo/person_form.html", {"form": form}) diff --git a/example/example_project/__init__.py b/example/example_project/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/example/example_project/settings.py b/example/example_project/settings.py new file mode 100644 index 0000000..283cc77 --- /dev/null +++ b/example/example_project/settings.py @@ -0,0 +1,76 @@ +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent.parent + +SECRET_KEY = "django-insecure-#=02g9%#k0t!(0968j4f#=+u%l-ws3nfti*0n601@n_zo(&x%w" +DEBUG = True +ALLOWED_HOSTS = [] + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "django_nepkit", + "demo", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "example_project.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "example_project.wsgi.application" + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } +} + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + +LANGUAGE_CODE = "en-us" +TIME_ZONE = "Asia/Kathmandu" +USE_I18N = True +USE_TZ = True + +STATIC_URL = "static/" diff --git a/example/example_project/urls.py b/example/example_project/urls.py new file mode 100644 index 0000000..00750f2 --- /dev/null +++ b/example/example_project/urls.py @@ -0,0 +1,8 @@ +from django.contrib import admin +from django.urls import include, path + +urlpatterns = [ + path("admin/", admin.site.urls), + path("nepkit/", include("django_nepkit.urls")), + path("", include("demo.urls")), +] diff --git a/example/example_project/wsgi.py b/example/example_project/wsgi.py new file mode 100644 index 0000000..34deab7 --- /dev/null +++ b/example/example_project/wsgi.py @@ -0,0 +1,7 @@ +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_project.settings") + +application = get_wsgi_application() diff --git a/example/manage.py b/example/manage.py new file mode 100755 index 0000000..4ea518b --- /dev/null +++ b/example/manage.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" + +import os +import sys + + +def main(): + """Run administrative tasks.""" + sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_project.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml index dba3c12..f90b2e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,6 +52,11 @@ classifiers = [ "Natural Language :: English" ] +[project.optional-dependencies] +drf = [ + "djangorestframework>=3.14", +] + [project.urls] Homepage = "https://github.com/S4NKALP/django-nepkit" @@ -62,4 +67,3 @@ dev = [ "pre-commit>=4.5.1", "ruff>=0.14.14", ] - diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..87e16ae --- /dev/null +++ b/setup.py @@ -0,0 +1,39 @@ +from setuptools import find_packages, setup + +setup( + name="django-nepkit", + version="0.1.0", + include_package_data=True, + author="Sankalp Tharu", + author_email="sankalptharu50028@gmail.com", + description="Django Nepali date, time, datetime, phone, and address fields with helpers.", + long_description=open("README.md").read(), + long_description_content_type="text/markdown", + url="https://github.com/S4NKALP/django-nepkit", + classifiers=[ + "Framework :: Django", + "Framework :: Django :: 4.2", + "Framework :: Django :: 5.0", + "Framework :: Django :: 6.0", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "License :: OSI Approved :: MIT License", + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Utilities", + "Operating System :: OS Independent", + "Natural Language :: English", + ], + python_requires=">=3.11", + packages=find_packages(exclude=("example*", "tests*")), + install_requires=[ + "django>=4.2", + "nepali>=1.1.3", + ], + extras_require={ + "drf": ["djangorestframework>=3.14"], + }, +) diff --git a/uv.lock b/uv.lock index 0ba39bf..781b52e 100644 --- a/uv.lock +++ b/uv.lock @@ -77,6 +77,11 @@ dependencies = [ { name = "nepali" }, ] +[package.optional-dependencies] +drf = [ + { name = "djangorestframework" }, +] + [package.dev-dependencies] dev = [ { name = "pre-commit" }, @@ -86,8 +91,10 @@ dev = [ [package.metadata] requires-dist = [ { name = "django", specifier = ">=4.2" }, + { name = "djangorestframework", marker = "extra == 'drf'", specifier = ">=3.14" }, { name = "nepali", specifier = ">=1.1.3" }, ] +provides-extras = ["drf"] [package.metadata.requires-dev] dev = [ @@ -95,6 +102,19 @@ dev = [ { name = "ruff", specifier = ">=0.14.14" }, ] +[[package]] +name = "djangorestframework" +version = "3.16.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django", version = "5.2.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "django", version = "6.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/95/5376fe618646fde6899b3cdc85fd959716bb67542e273a76a80d9f326f27/djangorestframework-3.16.1.tar.gz", hash = "sha256:166809528b1aced0a17dc66c24492af18049f2c9420dbd0be29422029cfc3ff7", size = 1089735, upload-time = "2025-08-06T17:50:53.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/ce/bf8b9d3f415be4ac5588545b5fcdbbb841977db1c1d923f7568eeabe1689/djangorestframework-3.16.1-py3-none-any.whl", hash = "sha256:33a59f47fb9c85ede792cbf88bde71893bcda0667bc573f784649521f1102cec", size = 1080442, upload-time = "2025-08-06T17:50:50.667Z" }, +] + [[package]] name = "filelock" version = "3.20.3"