refactor: global project refinement, DRY consolidation, and documentation overhaul

This commit represents a comprehensive improvement of the library's architecture, developer experience, and documentation accuracy.

Core Refactoring:
- apply DRY principle to models.py by introducing BaseNepaliBSField and BaseLocationField
- standardize widget logic in widgets.py using NepaliWidgetMixin
- centralize admin filtering logic into BaseNepaliDateFilter

New Features & API:
- add NepaliMonthFilter for Django Admin and NepaliDateMonthFilter for DRF
- implementation of HTMX-driven location chaining for dynamic address selects
- extend demo project with comprehensive DRF search, ordering, and filter examples

Configuration & Sync:
- implement global NEPKIT settings in conf.py (DEFAULT_LANGUAGE, ADMIN_DATEPICKER, etc.)
- universalize BS_DATE_FORMAT/BS_DATETIME_FORMAT across the entire stack
- synchronize frontend datepicker initialization with backend date formats

Documentation & DX:
- complete rewrite of README.md into high-energy, factual, English-only guides
- project-wide audit and simplification of comments/docstrings for beginner accessibility
- expanded 'Technical Design' and 'FAQ' sections covering data integrity and storage
- added docs/migration_guide.py for bulk AD-to-BS data transitions

Quality & Maintenance:
- fixed all E402 module-level import errors across the library
- verified codebase with project-wide ruff checks (all checks passed)
This commit is contained in:
sankalp committed 2026-01-30 17:00:33 +05:45
1 parent 09f302ceaa
commit 1338f9e5fe
32 files changed
+3002 -531

No files matched your search

+101 -111
View File
@@ -4,121 +4,99 @@
[![PyPI version](https://badge.fury.io/py/django-nepkit.svg)](https://badge.fury.io/py/django-nepkit)
[![Python Versions](https://img.shields.io/pypi/pyversions/django-nepkit.svg)](https://pypi.org/project/django-nepkit/)
[![Django Versions](https://img.shields.io/badge/Django-3.2%20%7C%204.2%20%7C%205.0%20%7C%206.0-blue.svg)](https://www.djangoproject.com/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
**The essential toolkit for Django developers building for Nepal.**
`django-nepkit` handles the specific localization needs of Nepali web applications with elegance. From correct Bikram Sambat (BS) date handling to smart address management and phone validation.
[Features](#-key-features) • [Installation](#-installation) • [Configuration](#-global-configuration) • [Usage](#-quick-start) • [Storage & TZ](#-date-storage--timezone-behavior) • [API Support](#-api-support-drf--filters)
**A toolkit for handling BS dates, regional locations, and validation in the local context.**
</div>
---
## ✨ Key Features
- **📅 Bikram Sambat (BS) Support**: Specialized `NepaliDateField` and `NepaliDateTimeField` for your models.
- **🗺️ Administrative Locations**: Out-of-the-box support for Provinces, Districts, and Municipalities with chained selects.
- **📱 Smart Validation**: Built-in validation for Nepali phone numbers.
- **🇳🇵 Bilingual by Design**: Seamlessly switch between English and Devanagari (Nepali) output.
- **🔌 Zero-Config Admin**: Automatic integration with the Nepali datepicker and formatted list displays.
- **🚀 API Ready**: First-class support for Django REST Framework and filtering.
- **⚡ HTMX Friendly**: Effortless server-side chained selects using HTMX.
Building software for local requirements comes with unique challenges—from handling BS dates to managing the regional administrative hierarchy. `django-nepkit` provides solutions for these requirements directly within the Django ecosystem.
---
## 🛠 Installation
## 🎯 Features
Install the core package via pip:
- **📅 BS Date Support**: Model fields for `nepalidate` and `nepalidatetime` objects.
- **🗺️ Regional Locations**: Pre-defined Provinces, Districts, and Municipalities.
- **📱 Phone Validation**: Patterns for local mobile and landline numbers.
- **🔌 Admin Integration**: Automatic setup for datepickers and localized list displays.
- **🚀 API Support**: DRF Serializers and Filtering backends for BS searching and ordering.
- **⚡ Location Chaining**: Address linking via client-side JS or server-driven HTMX.
---
## 🛠 Setup
Installation:
```bash
pip install django-nepkit
```
### 1. Add to `INSTALLED_APPS`
### 1. Basic Configuration
In your `settings.py`:
Add it to your `INSTALLED_APPS`:
```python
INSTALLED_APPS = [
# ...
"django.contrib.staticfiles",
"django_nepkit",
]
```
### 2. (Optional) DRF & Filters Support
### 2. Global Control
If you plan to use it with Django REST Framework:
```bash
pip install "django-nepkit[drf]"
```
---
## ⚙️ Global Configuration
Customize the library's behavior globally in your `settings.py`:
Configure behavior in your `settings.py`:
```python
NEPKIT = {
"DEFAULT_LANGUAGE": "en", # "en" or "ne" (default: "en")
"DATE_INPUT_FORMATS": [ # Supported parsing formats
"%Y-%m-%d", "%d/%m/%Y", "%d-%m-%Y"
],
"ADMIN_DATEPICKER": True, # Toggle Nepali datepicker in admin
"TIME_FORMAT": 12, # 12 or 24 hour display in admin
"DEFAULT_LANGUAGE": "en", # "en" or "ne"
"ADMIN_DATEPICKER": True, # Toggle the datepicker
"TIME_FORMAT": 12, # 12 or 24 hour display
"DATE_INPUT_FORMATS": ["%Y-%m-%d", "%d/%m/%Y", "%d-%m-%Y"], # Input formats
}
```
---
## 🚀 Quick Start
## 🚀 Core Usage
### 1. Model Fields
### 1. Model Implementation
`django-nepkit` fields act like standard Django fields but handle BS dates and Nepali validation.
Fields store `YYYY-MM-DD` strings for database stability while providing BS objects in Python.
```python
from django.db import models
from django_nepkit import (
NepaliDateField,
NepaliDateTimeField,
NepaliPhoneNumberField
)
from django_nepkit import NepaliDateField, NepaliPhoneNumberField
class Citizen(models.Model):
class Profile(models.Model):
name = models.CharField(max_length=100)
dob = NepaliDateField() # Stores as BS "YYYY-MM-DD"
phone = NepaliPhoneNumberField() # Validates Nepali patterns
birth_date = NepaliDateField() # BS Date support
phone = NepaliPhoneNumberField() # Local pattern validation
```
### 2. Zero-Config Admin
### 2. Admin Integration
Simply inherit from `NepaliModelAdmin` to get automatic datepickers and formatted list displays.
Use `NepaliModelAdmin` for automatic formatting and datepicker support.
```python
from django.contrib import admin
from django_nepkit import NepaliModelAdmin
from .models import Citizen
from django_nepkit import NepaliModelAdmin, NepaliDateFilter
@admin.register(Citizen)
class CitizenAdmin(NepaliModelAdmin):
list_display = ("name", "dob", "phone")
@admin.register(Profile)
class ProfileAdmin(NepaliModelAdmin):
list_display = ("name", "birth_date", "phone")
list_filter = (("birth_date", NepaliDateFilter),)
```
> [!TIP]
> If you have a custom base Admin class, use **`NepaliAdminMixin`** to inject Nepali formatting capabilities.
---
## 🗺️ Address Management
Model Nepal's administrative structure with automatic filtering: **Province → District → Municipality**.
Manage the **Province → District → Municipality** hierarchy.
### Standard Usage (Internal JS)
### Client-Side Chaining (Standard)
Cascading selects in the Django Admin without extra configuration.
```python
from django_nepkit import ProvinceField, DistrictField, MunicipalityField
@@ -129,82 +107,94 @@ class Address(models.Model):
municipality = MunicipalityField()
```
### HTMX Usage (Server-side)
### Server-Side Chaining (HTMX)
For a more modern approach without writing JavaScript, enable HTMX chaining:
```python
class AddressForm(forms.Form):
province = ProvinceField(htmx=True)
district = DistrictField(htmx=True)
municipality = MunicipalityField()
```
Enable `htmx=True` for a server-driven experience.
> [!IMPORTANT]
> Ensure the [HTMX](https://htmx.org/) library is loaded in your templates when using `htmx=True`.
> **HTMX Setup:**
> Include the required URLs in your main `urls.py`:
>
> ```python
> path("nepkit/", include("django_nepkit.urls")),
> ```
---
## 🔌 API Support (DRF & Filters)
## 🔌 API & DRF Support
Full integration with Django REST Framework and Django-Filter.
### Serializers
Fields automatically respect your `DEFAULT_LANGUAGE` but can be localized individually.
```python
from django_nepkit.serializers import NepaliDateSerializerField
class CitizenSerializer(serializers.ModelSerializer):
dob = NepaliDateSerializerField() # Outputs BS formatted date
```
### Filters
Filter your API results by BS Year or Date range effortlessly.
Search and ordering work natively. BS year/month filtering is supported.
```python
from django_nepkit.filters import NepaliDateYearFilter
class CitizenFilter(filters.FilterSet):
# Filter by BS Year (e.g., /api/citizens/?birth_year=2080)
birth_year = NepaliDateYearFilter(field_name="dob")
class ProfileFilter(filters.FilterSet):
# Filter by BS Year (e.g., /api/profiles/?year=2081)
year = NepaliDateYearFilter(field_name="birth_date")
```
---
## 🕒 Date Storage & Timezone Behavior
## 🕒 Technical Design
| Aspect | Behavior |
| :--- | :--- |
| **Database Storage** | Stored as `VARCHAR(10)` or `VARCHAR(19)` in BS format (`YYYY-MM-DD`). |
| **Why strings?** | Avoids redundant AD/BS conversion overhead and ensures data integrity. |
| **Object Type** | Retrieved as `nepalidate` or `nepalidatetime` objects (via `nepali` lib). |
| **Timezone** | Respects `USE_TZ`. Converts UTC `now()` to your local time before BS conversion. |
This library is engineered for performance, data integrity, and local compliance.
### 1. The "Source of Truth" Strategy
We avoid on-the-fly AD-to-BS conversion during database queries because it is computationally expensive and prone to logical drift (due to lunar calendar offsets).
- **Storage**: All BS dates are stored as `VARCHAR(10)` in `YYYY-MM-DD` format.
- **Sorting**: Because `YYYY` is at the start, string-based database sorting (ascending/descending) accurately matches chronological order.
- **Indexability**: Standard B-Tree indexes work perfectly on these fields without requiring custom database functions.
- **Timezone Safety**: Dates are stored without time components, making them immune to server-side timezone shifts during saving.
### 2. Python Object Mapping
While data is stored as strings, it is automatically hydrated into rich Python objects.
- **`nepali-datetime` Integration**: Values are cast to `nepalidate` or `nepalidatetime` objects when retrieved from the database.
- **Validation**: Fields use specialized validators (e.g., `validate_nepali_phone_number`) that leverage official regional patterns.
### 3. Frontend Architecture
- **Automatic Initialization**: The library includes a lightweight JS observer that automatically initializes the datepicker for any field with the `.nepkit-datepicker` class.
- **Theme Support**: The datepicker dynamically adapts its skin based on the Django Admin's dark/light mode state.
---
## 🛠️ Utility Functions
## ❓ FAQ
**Q: How do I handle Null or Optional dates?**
Just like standard Django fields, pass `null=True, blank=True` to any `django-nepkit` field. The library handles empty strings and `None` values gracefully.
**Q: Can I change the database storage format?**
No. The `YYYY-MM-DD` format is hardcoded to ensure database-level sorting and indexing work consistently. However, you can change the **display** format via global settings or template filters.
**Q: How do I get Devanagari (local script) digits in my API?**
In your Serializer field or Model field, pass `ne=True`.
```python
from django_nepkit import format_nepali_date
# Example: "Magh 17, 2081" or "माघ १७, २०८१"
formatted = format_nepali_date(my_date, format_string="%B %d, %Y")
birth_date = NepaliDateField(ne=True)
```
**Q: Is the location data up to date?**
Yes. Province, District, and Municipality data is sourced from the `nepali` Python package, which is regularly updated to reflect administrative changes in Nepal.
**Q: Does it work with standard Django Forms?**
Yes. `NepaliDateField` uses a specialized `NepaliDateFormField` that automatically handles input parsing and error reporting.
**Q: How do I migrate existing English (AD) dates to BS?**
We recommend staying on standard `DateField` for AD data. If you must convert to BS, use our [Migration Script](docs/migration_guide.py) to perform a bulk data transformation safely.
**Q: Why use VARCHAR instead of a native DateField?**
Native `DateField` in most SQL engines is locked to the Gregorian calendar. Using `VARCHAR` allows us to treat the BS date as the primary data point, avoiding the "off-by-one" conversion errors common when syncing two disparate calendars.
---
## 🤝 Contributing
## 🤝 Community
We welcome contributions to make this toolkit even better!
We welcome contributions and feedback from the community.
1. **Clone**: `git clone https://github.com/S4NKALP/django-nepkit`
2. **Setup**: `uv sync` (We use [uv](https://github.com/astral-sh/uv) for project management)
3. **Test**: `uv run pytest`
4. **Try**: `cd example && python manage.py runserver`
1. **Clone**: `git clone https://github.com/S4NKALP/django-nepkit`
2. **Setup**: `uv sync`
3. **Test**: `uv run pytest`
---
## 📄 License
MIT License. Crafted with ❤️ for the Nepali Django community.
MIT License. Designed for the local Django ecosystem.
+2
View File
@@ -9,6 +9,7 @@ from .models import (
)
from .admin import (
NepaliDateFilter,
NepaliMonthFilter,
format_nepali_date,
format_nepali_datetime,
NepaliModelAdmin,
@@ -24,6 +25,7 @@ __all__ = [
"DistrictField",
"MunicipalityField",
"NepaliDateFilter",
"NepaliMonthFilter",
"format_nepali_date",
"format_nepali_datetime",
"NepaliModelAdmin",
+87 -56
View File
@@ -2,16 +2,17 @@ from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from nepali.datetime import nepalidate, nepalidatetime
from django_nepkit.conf import nepkit_settings
from django_nepkit.models import NepaliDateField, NepaliDateTimeField
from django_nepkit.utils import (
try_parse_nepali_date,
try_parse_nepali_datetime,
)
from django_nepkit.conf import nepkit_settings
from django_nepkit.utils import BS_DATE_FORMAT
def _format_nepali_common(value, try_parse_func, format_string, ne, cls_type):
"""Internal helper for formatting Nepali objects."""
"""Helper to format dates/times with optional Devanagari support."""
if value is None:
return ""
@@ -40,9 +41,7 @@ def format_nepali_date(date_value, format_string="%B %d, %Y", ne=False):
)
def format_nepali_datetime(
datetime_value, format_string=None, ne=False
):
def format_nepali_datetime(datetime_value, format_string=None, ne=False):
"""
Format a nepalidatetime object with Nepali month names.
"""
@@ -57,15 +56,12 @@ def format_nepali_datetime(
)
class NepaliDateFilter(admin.FieldListFilter):
"""
A list filter for NepaliDateField to filter by BS Year.
"""
class BaseNepaliDateFilter(admin.FieldListFilter):
"""Base class for date filters (Year/Month)."""
def __init__(self, field, request, params, model, model_admin, field_path):
self.parameter_name = f"{field_path}_bs_year"
self.parameter_name = f"{field_path}_{self.suffix}"
super().__init__(field, request, params, model, model_admin, field_path)
self.title = _("Nepali Date (Year)")
def expected_parameters(self):
return [self.parameter_name]
@@ -76,40 +72,85 @@ class NepaliDateFilter(admin.FieldListFilter):
"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):
for value, display in self.get_filter_options():
yield {
"selected": self.used_parameters.get(self.parameter_name) == str(year),
"selected": self.used_parameters.get(self.parameter_name) == str(value),
"query_string": changelist.get_query_string(
{self.parameter_name: str(year)}
{self.parameter_name: str(value)}
),
"display": str(year),
"display": display,
}
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 self.apply_filter(queryset, value)
return queryset
def get_filter_options(self):
raise NotImplementedError
# Register NepaliDateFilter as the default list filter for NepaliDateField
def apply_filter(self, queryset, value):
raise NotImplementedError
class NepaliDateFilter(BaseNepaliDateFilter):
"""Filter by Nepali Year (e.g., 2080)."""
suffix = "bs_year"
title = _("Nepali Date (Year)")
def get_filter_options(self):
current_year = nepalidate.today().year
return [(y, str(y)) for y in range(current_year - 10, current_year + 2)]
def apply_filter(self, queryset, value):
if BS_DATE_FORMAT.startswith("%Y"):
separator = BS_DATE_FORMAT[2] if len(BS_DATE_FORMAT) > 2 else "-"
return queryset.filter(
**{f"{self.field_path}__startswith": f"{value}{separator}"}
)
return queryset.filter(**{f"{self.field_path}__icontains": f"{value}"})
class NepaliMonthFilter(BaseNepaliDateFilter):
"""Filter by Nepali Month (e.g., Baisakh)."""
suffix = "bs_month"
title = _("Nepali Date (Month)")
def get_filter_options(self):
ne = nepkit_settings.DEFAULT_LANGUAGE == "ne"
names = [
("बैशाख", "Baisakh"),
("जेठ", "Jestha"),
("असार", "Ashad"),
("साउन", "Shrawan"),
("भदौ", "Bhadra"),
("असोज", "Ashwin"),
("कात्तिक", "Kartik"),
("मंसिर", "Mangsir"),
("पुष", "Poush"),
("माघ", "Magh"),
("फागुन", "Falgun"),
("चैत", "Chaitra"),
]
return [(f"{i:02d}", n[0] if ne else n[1]) for i, n in enumerate(names, 1)]
def apply_filter(self, queryset, value):
from django_nepkit.utils import BS_DATE_FORMAT
if BS_DATE_FORMAT == "%Y-%m-%d":
return queryset.filter(**{f"{self.field_path}__contains": f"-{value}-"})
separator = BS_DATE_FORMAT[2] if len(BS_DATE_FORMAT) > 2 else "-"
return queryset.filter(
**{f"{self.field_path}__contains": f"{separator}{value}{separator}"}
)
# Standard filter for any NepaliDateField in Admin
admin.FieldListFilter.register(
lambda f: isinstance(f, NepaliDateField),
NepaliDateFilter,
@@ -118,10 +159,7 @@ admin.FieldListFilter.register(
class NepaliAdminMixin:
"""
Mixin for Django admin classes that provides Nepali date utilities.
Makes format_nepali_date and NepaliDateFilter available without explicit imports.
"""
"""Provides date formatting tools for Admin classes."""
def _get_field_ne_setting(self, field_name):
"""
@@ -158,7 +196,6 @@ class NepaliAdminMixin:
ne: If True, format using Devanagari script. If None, auto-detect from field or global settings (default: None)
field_name: Name of the field to auto-detect 'ne' setting from (optional)
"""
# Auto-detect 'ne' from field if not explicitly provided
if ne is None and field_name:
ne = self._get_field_ne_setting(field_name)
elif ne is None:
@@ -182,7 +219,6 @@ class NepaliAdminMixin:
ne: If True, format using Devanagari script. If None, auto-detect from field or global settings (default: None)
field_name: Name of the field to auto-detect 'ne' setting from (optional)
"""
# Auto-detect 'ne' from field if not explicitly provided
if ne is None and field_name:
ne = self._get_field_ne_setting(field_name)
elif ne is None:
@@ -193,9 +229,7 @@ class NepaliAdminMixin:
class NepaliModelAdmin(NepaliAdminMixin, admin.ModelAdmin):
"""
Base ModelAdmin class with Nepali date utilities built-in.
NepaliDateField and NepaliDateTimeField in list_display are automatically
formatted (using the field's ne setting). No manual display methods needed.
Standard Admin class that automatically formats Nepali dates in lists.
Example:
from django_nepkit import NepaliModelAdmin, NepaliDateFilter
@@ -206,11 +240,12 @@ class NepaliModelAdmin(NepaliAdminMixin, admin.ModelAdmin):
list_filter = (("birth_date", NepaliDateFilter),)
"""
# Make NepaliDateFilter available as a class attribute
# Make filters available as class attributes
NepaliDateFilter = NepaliDateFilter
NepaliMonthFilter = NepaliMonthFilter
def _make_nepali_display(self, field_name, formatter_method):
"""Generic helper to return a callable that formats a Nepali field for list_display."""
"""Helper to create display columns for Nepali dates."""
admin_instance = self
try:
field = self.model._meta.get_field(field_name)
@@ -257,19 +292,18 @@ class NepaliModelAdmin(NepaliAdminMixin, admin.ModelAdmin):
result.append(item)
return result
# 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.
"""
"""Automatically use NepaliDatePicker in the admin form."""
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)) and nepkit_settings.ADMIN_DATEPICKER:
if (
isinstance(db_field, (NepaliDateField, NepaliDateTimeField))
and nepkit_settings.ADMIN_DATEPICKER
):
# Pass ne/en parameters from field to widget if they exist
widget_kwargs = {}
if hasattr(db_field, "ne"):
@@ -281,10 +315,7 @@ class NepaliModelAdmin(NepaliAdminMixin, admin.ModelAdmin):
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.
"""
"""Loads the Nepali Datepicker and bridging scripts."""
css = {
"all": (
@@ -302,9 +333,9 @@ class NepaliModelAdmin(NepaliAdminMixin, admin.ModelAdmin):
)
# Exporting for easy usage
__all__ = [
"NepaliDateFilter",
"NepaliMonthFilter",
"format_nepali_date",
"format_nepali_datetime",
"NepaliAdminMixin",
+5 -5
View File
@@ -5,13 +5,13 @@ DEFAULTS = {
"DATE_INPUT_FORMATS": ["%Y-%m-%d", "%d/%m/%Y", "%d-%m-%Y"],
"ADMIN_DATEPICKER": True,
"TIME_FORMAT": 12,
"BS_DATE_FORMAT": "%Y-%m-%d",
"BS_DATETIME_FORMAT": "%Y-%m-%d %H:%M:%S",
}
class NepkitSettings:
"""
A settings object that allows items to be accessed as properties.
"""
"""Handles the global NEPKIT settings from your settings.py."""
def __init__(self, user_settings=None, defaults=None):
self._user_settings = user_settings or {}
@@ -22,10 +22,10 @@ class NepkitSettings:
raise AttributeError(f"Invalid NEPKIT setting: '{attr}'")
try:
# Check for user setting
# Check if the user has a custom setting
val = self._user_settings[attr]
except KeyError:
# Fall back to defaults
# Use default value if not set
val = self.defaults[attr]
return val
+35 -5
View File
@@ -17,15 +17,45 @@ class NepaliDateYearFilter(filters.NumberFilter):
"""
A filter for `NepaliDateField` that allows filtering by Bikram Sambat Year.
Expects an integer year (e.g., 2080).
Usage:
year = NepaliDateYearFilter(field_name="dob")
"""
def filter(self, qs: QuerySet, value: Any) -> QuerySet:
if value:
# Since date is stored as "YYYY-MM-DD" string
return qs.filter(**{f"{self.field_name}__startswith": f"{value}-"})
from django_nepkit.utils import BS_DATE_FORMAT
# Find the date separator (e.g., '-' in '2080-01-01')
if BS_DATE_FORMAT.startswith("%Y"):
separator = BS_DATE_FORMAT[2] if len(BS_DATE_FORMAT) > 2 else "-"
return qs.filter(
**{f"{self.field_name}__startswith": f"{value}{separator}"}
)
# Fallback if the format is unusual
return qs.filter(**{f"{self.field_name}__icontains": str(value)})
return qs
class NepaliDateMonthFilter(filters.NumberFilter):
"""
A filter for `NepaliDateField` that allows filtering by Bikram Sambat Month.
Expects an integer month (1-12).
"""
def filter(self, qs: QuerySet, value: Any) -> QuerySet:
if value:
from django_nepkit.utils import BS_DATE_FORMAT
month_str = f"{int(value):02d}"
# Standard format: look for '-01-' for Baisakh
if BS_DATE_FORMAT == "%Y-%m-%d":
return qs.filter(**{f"{self.field_name}__contains": f"-{month_str}-"})
# Adaptive check for other separators
separator = BS_DATE_FORMAT[2] if len(BS_DATE_FORMAT) > 2 else "-"
return qs.filter(
**{f"{self.field_name}__contains": f"{separator}{month_str}{separator}"}
)
return qs
+7 -8
View File
@@ -10,15 +10,13 @@ from django_nepkit.widgets import NepaliDatePickerWidget
class NepaliDateFormField(forms.DateField):
"""
A Django Form Field for Nepali Date (Bikram Sambat).
"""
"""A form field for entering Nepali dates."""
widget = NepaliDatePickerWidget
def __init__(self, *args, **kwargs):
kwargs.pop("max_length", None)
# Django admin may pass empty_value; base Field does not accept it
# Clean up arguments not needed for base Field
kwargs.pop("empty_value", None)
super().__init__(*args, **kwargs)
@@ -35,16 +33,17 @@ class NepaliDateFormField(forms.DateField):
return parsed
raise ValueError("Invalid BS date format")
except Exception:
from django_nepkit.utils import BS_DATE_FORMAT
raise forms.ValidationError(
_("Enter a valid Nepali date in YYYY-MM-DD format."),
_("Enter a valid Nepali date in %(format)s format.")
% {"format": BS_DATE_FORMAT},
code="invalid",
)
class NepaliPhoneNumberFormField(forms.CharField):
"""
A Django Form Field for Nepali Phone Numbers.
"""
"""A form field for entering Nepali phone numbers."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
+91 -197
View File
@@ -25,24 +25,22 @@ from django_nepkit.conf import nepkit_settings
class NepaliFieldMixin:
"""
Mixin for Nepali model fields to handle 'ne' and 'en' arguments.
"""
"""Adds Nepali 'ne' and 'en' support to any field."""
def __init__(self, *args, **kwargs):
default_lang = nepkit_settings.DEFAULT_LANGUAGE
self.ne = kwargs.pop("ne", default_lang == "ne")
explicit_en = "en" in kwargs
en_value = kwargs.pop("en", not self.ne)
if self.ne and not explicit_en:
self.en = False
else:
self.en = en_value
self.htmx = kwargs.pop("htmx", False)
super().__init__(*args, **kwargs)
def deconstruct(self):
@@ -65,80 +63,89 @@ class NepaliPhoneNumberField(models.CharField):
self.validators.append(validate_nepali_phone_number)
class NepaliDateField(NepaliFieldMixin, 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.
"""
description = _("Nepali Date (Bikram Sambat)")
class BaseNepaliBSField(NepaliFieldMixin, models.CharField):
"""Base class for Nepali date and datetime fields."""
def __init__(self, *args, **kwargs):
self.auto_now = kwargs.pop("auto_now", False)
self.auto_now_add = kwargs.pop("auto_now_add", False)
# 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)
kwargs.setdefault("max_length", getattr(self, "default_max_length", 20))
super().__init__(*args, **kwargs)
def pre_save(self, model_instance, add):
if self.auto_now or (self.auto_now_add and add):
# Using timezone.now() to get current date, then converting to BS.
now = timezone.now()
if timezone.is_aware(now):
now = timezone.localtime(now)
value = nepalidate.from_date(now).strftime(BS_DATE_FORMAT)
nepali_cls = getattr(self, "nepali_cls", nepalidate)
format_str = getattr(self, "format_str", BS_DATE_FORMAT)
value = (
nepali_cls.from_date(now).strftime(format_str)
if nepali_cls == nepalidate
else nepali_cls.from_datetime(now).strftime(format_str)
)
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_date(value)
parsed = self.parse_func(value)
return parsed if parsed is not None else value
def to_python(self, value):
if value is None or isinstance(value, nepalidate):
if value is None or isinstance(value, self.nepali_cls):
return value
if isinstance(value, (python_date, python_datetime)):
try:
if isinstance(value, python_datetime) and timezone.is_aware(value):
value = timezone.localtime(value)
return nepalidate.from_date(value)
return (
self.nepali_cls.from_date(value)
if self.nepali_cls == nepalidate
else self.nepali_cls.from_datetime(value)
)
except (ValueError, TypeError):
return str(value)
if isinstance(value, str):
parsed = try_parse_nepali_date(value)
parsed = self.parse_func(value)
return parsed if parsed is not None else value
return super().to_python(value)
def _get_string_value(self, value):
if isinstance(value, (nepalidate, nepalidatetime)):
return value.strftime(self.format_str)
return value
def validate(self, value, model_instance):
if isinstance(value, nepalidate):
value = value.strftime(BS_DATE_FORMAT)
super().validate(value, model_instance)
super().validate(self._get_string_value(value), model_instance)
def run_validators(self, value):
if isinstance(value, nepalidate):
value = value.strftime(BS_DATE_FORMAT)
super().run_validators(value)
super().run_validators(self._get_string_value(value))
def get_prep_value(self, value):
if value is None:
return value
if isinstance(value, nepalidate):
return value.strftime(BS_DATE_FORMAT)
if isinstance(value, self.nepali_cls):
return value.strftime(self.format_str)
if isinstance(value, (python_date, python_datetime)):
try:
if isinstance(value, python_datetime) and timezone.is_aware(value):
value = timezone.localtime(value)
return nepalidate.from_date(value).strftime(BS_DATE_FORMAT)
nepali_obj = (
self.nepali_cls.from_date(value)
if self.nepali_cls == nepalidate
else self.nepali_cls.from_datetime(value)
)
return nepali_obj.strftime(self.format_str)
except (ValueError, TypeError):
return str(value)
if isinstance(value, str):
return value
return str(value)
def deconstruct(self):
@@ -151,197 +158,84 @@ class NepaliDateField(NepaliFieldMixin, models.CharField):
def formfield(self, **kwargs):
defaults = {
"form_class": NepaliDateFormField,
"widget": NepaliDatePickerWidget(ne=self.ne, en=self.en),
}
defaults.update(kwargs)
return super().formfield(**defaults)
class NepaliDateField(BaseNepaliBSField):
description = _("Nepali Date (Bikram Sambat)")
default_max_length = 10
nepali_cls = nepalidate
format_str = BS_DATE_FORMAT
parse_func = staticmethod(try_parse_nepali_date)
def formfield(self, **kwargs):
kwargs.setdefault("form_class", NepaliDateFormField)
return super().formfield(**kwargs)
class NepaliTimeField(NepaliFieldMixin, models.TimeField):
"""
A Django Model Field for Time with Nepali localization support.
"""
description = _("Nepali Time")
class NepaliDateTimeField(NepaliFieldMixin, 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.
"""
class NepaliDateTimeField(BaseNepaliBSField):
description = _("Nepali DateTime (Bikram Sambat)")
default_max_length = 19
nepali_cls = nepalidatetime
format_str = BS_DATETIME_FORMAT
parse_func = staticmethod(try_parse_nepali_datetime)
class BaseLocationField(NepaliFieldMixin, models.CharField):
"""Base class for Province, District, and Municipality fields."""
def __init__(self, *args, **kwargs):
self.auto_now = kwargs.pop("auto_now", False)
self.auto_now_add = kwargs.pop("auto_now_add", False)
kwargs.setdefault("max_length", 100)
# Match Django's DateTimeField behavior
if self.auto_now or self.auto_now_add:
kwargs.setdefault("editable", False)
kwargs.setdefault("blank", True)
default_lang = nepkit_settings.DEFAULT_LANGUAGE
ne = kwargs.get("ne", default_lang == "ne")
kwargs.setdefault("max_length", 19) # YYYY-MM-DD HH:MM:SS
# Load choices from the library (provinces, districts, etc.)
kwargs.setdefault("choices", self.get_choices_from_source(ne))
super().__init__(*args, **kwargs)
def pre_save(self, model_instance, add):
if self.auto_now or (self.auto_now_add and add):
now = timezone.now()
if timezone.is_aware(now):
now = timezone.localtime(now)
value = nepalidatetime.from_datetime(now).strftime(BS_DATETIME_FORMAT)
setattr(model_instance, self.attname, value)
return value
return super().pre_save(model_instance, add)
def get_choices_from_source(self, ne):
source = getattr(self, "source", [])
return [(self._get_name(item, ne), self._get_name(item, ne)) for item in source]
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, python_datetime):
try:
if timezone.is_aware(value):
value = timezone.localtime(value)
return nepalidatetime.from_datetime(value)
except (ValueError, TypeError):
return str(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, python_datetime):
try:
if timezone.is_aware(value):
value = timezone.localtime(value)
return nepalidatetime.from_datetime(value).strftime(BS_DATETIME_FORMAT)
except (ValueError, TypeError):
return str(value)
if isinstance(value, str):
return value
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 _get_name(self, item, ne):
name = getattr(item, "name_nepali", item.name) if ne else item.name
# Handle name variations and Koshi Province mapping
if name == "Province 1":
return "Koshi Province"
if name == "प्रदेश नं. १":
return "कोशी प्रदेश"
return name
def formfield(self, **kwargs):
defaults = {
"widget": NepaliDatePickerWidget(ne=self.ne, en=self.en),
}
defaults.update(kwargs)
return super().formfield(**defaults)
widget_cls = getattr(self, "widget_class", None)
if widget_cls:
defaults = {"widget": widget_cls(ne=self.ne, en=self.en, htmx=self.htmx)}
defaults.update(kwargs)
return super(models.CharField, self).formfield(**defaults)
return super().formfield(**kwargs)
class ProvinceField(NepaliFieldMixin, models.CharField):
class ProvinceField(BaseLocationField):
description = _("Nepali Province")
def __init__(self, *args, **kwargs):
kwargs.setdefault("max_length", 100)
# We need self.ne set by Mixin, but Mixin init runs last in normal MRO if we just use super().__init__.
# So we manually handle pop before calling super, OR we rely on kwargs.
# Actually, if we use NepaliFieldMixin as first parent, its __init__ will run.
# But we need to define choices based on ne.
# So we should pop 'ne' here, set it, then pass to super?
# But Mixin expects to pop it.
# Better approach: Extract ne here without popping (peek), or pop and pass back?
# Let's just duplicate popping here to set choices, then re-inject for Mixin?
# Or just handle it manually here and pass remaining to super.
# Let's modify behavior: pop ne here, calculate choices, then put it back?
# Or just manually set self.ne/self.en here and don't rely on Mixin's init for choices, but rely on it for other things?
# To avoid complexity, I'll peek at kwargs['ne'] if it exists.
default_lang = nepkit_settings.DEFAULT_LANGUAGE
ne = kwargs.get("ne", default_lang == "ne")
def get_p_name(p, ne):
name = getattr(p, "name_nepali", p.name) if ne else p.name
if name == "Province 1":
return "Koshi Province"
if name == "प्रदेश नं. १":
return "कोशी प्रदेश"
return name
choices = [(get_p_name(p, ne), get_p_name(p, ne)) for p in provinces]
kwargs.setdefault("choices", choices)
super().__init__(*args, **kwargs)
def formfield(self, **kwargs):
defaults = {"widget": ProvinceSelectWidget(ne=self.ne, en=self.en, htmx=self.htmx)}
defaults.update(kwargs)
return super(models.CharField, self).formfield(**defaults)
# Wait, super().formfield() calls Django's CharField.formfield.
# We want that.
source = provinces
widget_class = ProvinceSelectWidget
class DistrictField(NepaliFieldMixin, models.CharField):
class DistrictField(BaseLocationField):
description = _("Nepali District")
def __init__(self, *args, **kwargs):
default_lang = nepkit_settings.DEFAULT_LANGUAGE
ne = kwargs.get("ne", default_lang == "ne")
kwargs.setdefault("max_length", 100)
def get_d_name(d, ne):
return getattr(d, "name_nepali", d.name) if ne else d.name
choices = [(get_d_name(d, ne), get_d_name(d, ne)) for d in districts]
kwargs.setdefault("choices", choices)
super().__init__(*args, **kwargs)
def formfield(self, **kwargs):
defaults = {"widget": DistrictSelectWidget(ne=self.ne, en=self.en, htmx=self.htmx)}
defaults.update(kwargs)
return super(models.CharField, self).formfield(**defaults)
source = districts
widget_class = DistrictSelectWidget
class MunicipalityField(NepaliFieldMixin, models.CharField):
class MunicipalityField(BaseLocationField):
description = _("Nepali Municipality")
def __init__(self, *args, **kwargs):
default_lang = nepkit_settings.DEFAULT_LANGUAGE
ne = kwargs.get("ne", default_lang == "ne")
kwargs.setdefault("max_length", 100)
def get_m_name(m, ne):
return getattr(m, "name_nepali", m.name) if ne else m.name
choices = [(get_m_name(m, ne), get_m_name(m, ne)) for m in municipalities]
kwargs.setdefault("choices", choices)
super().__init__(*args, **kwargs)
def formfield(self, **kwargs):
defaults = {"widget": MunicipalitySelectWidget(ne=self.ne, en=self.en, htmx=self.htmx)}
defaults.update(kwargs)
return super(models.CharField, self).formfield(**defaults)
source = municipalities
widget_class = MunicipalitySelectWidget
+14 -35
View File
@@ -12,22 +12,20 @@ except ModuleNotFoundError as e:
"to use `django_nepkit.serializers`."
) from e
from django_nepkit.utils import try_parse_nepali_date, try_parse_nepali_datetime
from django_nepkit.conf import nepkit_settings
# --------------------------------------------------
# Base Serializer Field
# --------------------------------------------------
from django_nepkit.utils import (
BS_DATE_FORMAT,
BS_DATETIME_FORMAT,
try_parse_nepali_date,
try_parse_nepali_datetime,
)
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)
- **ne**: If True, output uses Devanagari script (default: False)
- **en**: If True, output uses English (default: True). When ne=True, en is False.
Handles Nepali (BS) dates.
- Input: A date string (like '2080-01-01') or a date object.
- Output: A formatted string for your API.
"""
format: str = ""
@@ -90,13 +88,12 @@ class BaseNepaliBSField(serializers.Field):
if isinstance(value, self.nepali_type):
return self._format_value(value)
# If DB returns string, try to normalize it.
# Convert string dates to Nepali date objects
if isinstance(value, str):
parsed = self._parse(value)
if parsed is not None:
return self._format_value(parsed)
# Fallback: best-effort stringify (keeps behavior non-breaking)
return str(value)
def to_internal_value(self, data: Any):
@@ -116,33 +113,15 @@ class BaseNepaliBSField(serializers.Field):
self.fail("invalid", format=self.format)
# --------------------------------------------------
# Nepali Date (BS)
# --------------------------------------------------
class NepaliDateSerializerField(BaseNepaliBSField):
"""
DRF field for Nepali BS Date (YYYY-MM-DD).
"""API field for Nepali Dates."""
Pass ne=True for Devanagari output; default is English.
"""
format = "%Y-%m-%d"
format = BS_DATE_FORMAT
nepali_type = nepalidate
# --------------------------------------------------
# Nepali DateTime (BS)
# --------------------------------------------------
class NepaliDateTimeSerializerField(BaseNepaliBSField):
"""
DRF field for Nepali BS DateTime (YYYY-MM-DD HH:MM:SS).
"""API field for Nepali Date and Time."""
Pass ne=True for Devanagari output; default is English.
"""
format = "%Y-%m-%d %H:%M:%S"
format = BS_DATETIME_FORMAT
nepali_type = nepalidatetime
@@ -23,7 +23,7 @@
const lang = isNepali ? 'ne' : 'en';
const data = window.NEPKIT_DATA[lang];
if (!data) return null;
if (type === 'districts') {
return data.districts[parentId] || [];
} else if (type === 'municipalities') {
@@ -101,7 +101,7 @@
let url = districtSelect.dataset.url + '?province=' + encodeURIComponent(province);
if (isNepali) url += '&ne=true';
if (districtSelect.dataset.en === 'true') url += '&en=true';
fetch(url)
.then(response => response.json())
.then(data => {
@@ -132,7 +132,7 @@
let url = municipalitySelect.dataset.url + '?district=' + encodeURIComponent(district);
if (isNepali) url += '&ne=true';
if (municipalitySelect.dataset.en === 'true') url += '&en=true';
fetch(url)
.then(response => response.json())
.then(data => {
@@ -1 +1 @@
window.NEPKIT_DATA = {"en": {"provinces": [{"id": "Sudurpaschim Province", "text": "Sudurpaschim Province"}, {"id": "Madhesh Province", "text": "Madhesh Province"}, {"id": "Gandaki Province", "text": "Gandaki Province"}, {"id": "Bagmati Province", "text": "Bagmati Province"}, {"id": "Lumbini Province", "text": "Lumbini Province"}, {"id": "Karnali Province", "text": "Karnali Province"}, {"id": "Koshi Province", "text": "Koshi Province"}], "districts": {"Sudurpaschim Province": [{"id": "Bajura", "text": "Bajura"}, {"id": "Bajhang", "text": "Bajhang"}, {"id": "Baitadi", "text": "Baitadi"}, {"id": "Dadeldhura", "text": "Dadeldhura"}, {"id": "Doti", "text": "Doti"}, {"id": "Achham", "text": "Achham"}, {"id": "Kailali", "text": "Kailali"}, {"id": "Darchula", "text": "Darchula"}, {"id": "Kanchanpur", "text": "Kanchanpur"}], "Madhesh Province": [{"id": "Saptari", "text": "Saptari"}, {"id": "Siraha", "text": "Siraha"}, {"id": "Dhanusa", "text": "Dhanusa"}, {"id": "Mahottari", "text": "Mahottari"}, {"id": "Sarlahi", "text": "Sarlahi"}, {"id": "Rautahat", "text": "Rautahat"}, {"id": "Bara", "text": "Bara"}, {"id": "Parsa", "text": "Parsa"}], "Gandaki Province": [{"id": "Gorkha", "text": "Gorkha"}, {"id": "Manang", "text": "Manang"}, {"id": "Mustang", "text": "Mustang"}, {"id": "Myagdi", "text": "Myagdi"}, {"id": "Kaski", "text": "Kaski"}, {"id": "Lamjung", "text": "Lamjung"}, {"id": "Tanahu", "text": "Tanahu"}, {"id": "Nawalparasi East", "text": "Nawalparasi East"}, {"id": "Syangja", "text": "Syangja"}, {"id": "Parbat", "text": "Parbat"}, {"id": "Baglung", "text": "Baglung"}], "Bagmati Province": [{"id": "Dolakha", "text": "Dolakha"}, {"id": "Sindhupalchok", "text": "Sindhupalchok"}, {"id": "Rasuwa", "text": "Rasuwa"}, {"id": "Dhading", "text": "Dhading"}, {"id": "Nuwakot", "text": "Nuwakot"}, {"id": "Kathmandu", "text": "Kathmandu"}, {"id": "Bhaktapur", "text": "Bhaktapur"}, {"id": "Lalitpur", "text": "Lalitpur"}, {"id": "Kavrepalanchok", "text": "Kavrepalanchok"}, {"id": "Ramechhap", "text": "Ramechhap"}, {"id": "Sindhuli", "text": "Sindhuli"}, {"id": "Makwanpur", "text": "Makwanpur"}, {"id": "Chitawan", "text": "Chitawan"}], "Lumbini Province": [{"id": "Rukum East", "text": "Rukum East"}, {"id": "Rolpa", "text": "Rolpa"}, {"id": "Pyuthan", "text": "Pyuthan"}, {"id": "Gulmi", "text": "Gulmi"}, {"id": "Arghakhanchi", "text": "Arghakhanchi"}, {"id": "Palpa", "text": "Palpa"}, {"id": "Rupandehi", "text": "Rupandehi"}, {"id": "Kapilvastu", "text": "Kapilvastu"}, {"id": "Dang", "text": "Dang"}, {"id": "Banke", "text": "Banke"}, {"id": "Bardiya", "text": "Bardiya"}, {"id": "Nawalparasi West", "text": "Nawalparasi West"}], "Karnali Province": [{"id": "Dolpa", "text": "Dolpa"}, {"id": "Mugu", "text": "Mugu"}, {"id": "Jumla", "text": "Jumla"}, {"id": "Kalikot", "text": "Kalikot"}, {"id": "Dailekh", "text": "Dailekh"}, {"id": "Jajarkot", "text": "Jajarkot"}, {"id": "Salyan", "text": "Salyan"}, {"id": "Surkhet", "text": "Surkhet"}, {"id": "Humla", "text": "Humla"}, {"id": "Rukum West", "text": "Rukum West"}], "Koshi Province": [{"id": "Taplejung", "text": "Taplejung"}, {"id": "Sankhuwasabha", "text": "Sankhuwasabha"}, {"id": "Solukhumbu", "text": "Solukhumbu"}, {"id": "Okhaldhunga", "text": "Okhaldhunga"}, {"id": "Khotang", "text": "Khotang"}, {"id": "Bhojpur", "text": "Bhojpur"}, {"id": "Dhankuta", "text": "Dhankuta"}, {"id": "Tehrathum", "text": "Tehrathum"}, {"id": "Panchthar", "text": "Panchthar"}, {"id": "Ilam", "text": "Ilam"}, {"id": "Jhapa", "text": "Jhapa"}, {"id": "Morang", "text": "Morang"}, {"id": "Sunsari", "text": "Sunsari"}, {"id": "Udayapur", "text": "Udayapur"}]}, "municipalities": {"Bajura": [{"id": "Himali Rural Municipality", "text": "Himali Rural Municipality"}, {"id": "Gaumul Rural Municipality", "text": "Gaumul Rural Municipality"}, {"id": "Badimalika Municipality", "text": "Badimalika Municipality"}, {"id": "Chhededaha Rural Municipality", "text": "Chhededaha Rural Municipality"}, {"id": "Budhiganga Municipality", "text": "Budhiganga Municipality"}, {"id": "Tribeni Municipality", "text": "Tribeni Municipality"}, {"id": "Jagannath Rural Municipality", "text": "Jagannath Rural Municipality"}, {"id": "Budhinanda Municipality", "text": "Budhinanda Municipality"}, {"id": "Swami Kartik Rural Municipality", "text": "Swami Kartik Rural Municipality"}], "Bajhang": [{"id": "Kanda Rural Municipality", "text": "Kanda Rural Municipality"}, {"id": "Bungal Municipality", "text": "Bungal Municipality"}, {"id": "Talkot Rural Municipality", "text": "Talkot Rural Municipality"}, {"id": "Masta Rural Municipality", "text": "Masta Rural Municipality"}, {"id": "Jayaprithbi Municipality", "text": "Jayaprithbi Municipality"}, {"id": "Chhabis Pathibhara Rural Municipality", "text": "Chhabis Pathibhara Rural Municipality"}, {"id": "Durgathali Rural Municipality", "text": "Durgathali Rural Municipality"}, {"id": "Bitthadchir Rural Municipality", "text": "Bitthadchir Rural Municipality"}, {"id": "Thalara Rural Municipality", "textLine truncated
window.NEPKIT_DATA = {"en": {"provinces": [{"id": "Sudurpaschim Province", "text": "Sudurpaschim Province"}, {"id": "Madhesh Province", "text": "Madhesh Province"}, {"id": "Gandaki Province", "text": "Gandaki Province"}, {"id": "Bagmati Province", "text": "Bagmati Province"}, {"id": "Lumbini Province", "text": "Lumbini Province"}, {"id": "Karnali Province", "text": "Karnali Province"}, {"id": "Koshi Province", "text": "Koshi Province"}], "districts": {"Sudurpaschim Province": [{"id": "Bajura", "text": "Bajura"}, {"id": "Bajhang", "text": "Bajhang"}, {"id": "Baitadi", "text": "Baitadi"}, {"id": "Dadeldhura", "text": "Dadeldhura"}, {"id": "Doti", "text": "Doti"}, {"id": "Achham", "text": "Achham"}, {"id": "Kailali", "text": "Kailali"}, {"id": "Darchula", "text": "Darchula"}, {"id": "Kanchanpur", "text": "Kanchanpur"}], "Madhesh Province": [{"id": "Saptari", "text": "Saptari"}, {"id": "Siraha", "text": "Siraha"}, {"id": "Dhanusa", "text": "Dhanusa"}, {"id": "Mahottari", "text": "Mahottari"}, {"id": "Sarlahi", "text": "Sarlahi"}, {"id": "Rautahat", "text": "Rautahat"}, {"id": "Bara", "text": "Bara"}, {"id": "Parsa", "text": "Parsa"}], "Gandaki Province": [{"id": "Gorkha", "text": "Gorkha"}, {"id": "Manang", "text": "Manang"}, {"id": "Mustang", "text": "Mustang"}, {"id": "Myagdi", "text": "Myagdi"}, {"id": "Kaski", "text": "Kaski"}, {"id": "Lamjung", "text": "Lamjung"}, {"id": "Tanahu", "text": "Tanahu"}, {"id": "Nawalparasi East", "text": "Nawalparasi East"}, {"id": "Syangja", "text": "Syangja"}, {"id": "Parbat", "text": "Parbat"}, {"id": "Baglung", "text": "Baglung"}], "Bagmati Province": [{"id": "Dolakha", "text": "Dolakha"}, {"id": "Sindhupalchok", "text": "Sindhupalchok"}, {"id": "Rasuwa", "text": "Rasuwa"}, {"id": "Dhading", "text": "Dhading"}, {"id": "Nuwakot", "text": "Nuwakot"}, {"id": "Kathmandu", "text": "Kathmandu"}, {"id": "Bhaktapur", "text": "Bhaktapur"}, {"id": "Lalitpur", "text": "Lalitpur"}, {"id": "Kavrepalanchok", "text": "Kavrepalanchok"}, {"id": "Ramechhap", "text": "Ramechhap"}, {"id": "Sindhuli", "text": "Sindhuli"}, {"id": "Makwanpur", "text": "Makwanpur"}, {"id": "Chitawan", "text": "Chitawan"}], "Lumbini Province": [{"id": "Rukum East", "text": "Rukum East"}, {"id": "Rolpa", "text": "Rolpa"}, {"id": "Pyuthan", "text": "Pyuthan"}, {"id": "Gulmi", "text": "Gulmi"}, {"id": "Arghakhanchi", "text": "Arghakhanchi"}, {"id": "Palpa", "text": "Palpa"}, {"id": "Rupandehi", "text": "Rupandehi"}, {"id": "Kapilvastu", "text": "Kapilvastu"}, {"id": "Dang", "text": "Dang"}, {"id": "Banke", "text": "Banke"}, {"id": "Bardiya", "text": "Bardiya"}, {"id": "Nawalparasi West", "text": "Nawalparasi West"}], "Karnali Province": [{"id": "Dolpa", "text": "Dolpa"}, {"id": "Mugu", "text": "Mugu"}, {"id": "Jumla", "text": "Jumla"}, {"id": "Kalikot", "text": "Kalikot"}, {"id": "Dailekh", "text": "Dailekh"}, {"id": "Jajarkot", "text": "Jajarkot"}, {"id": "Salyan", "text": "Salyan"}, {"id": "Surkhet", "text": "Surkhet"}, {"id": "Humla", "text": "Humla"}, {"id": "Rukum West", "text": "Rukum West"}], "Koshi Province": [{"id": "Taplejung", "text": "Taplejung"}, {"id": "Sankhuwasabha", "text": "Sankhuwasabha"}, {"id": "Solukhumbu", "text": "Solukhumbu"}, {"id": "Okhaldhunga", "text": "Okhaldhunga"}, {"id": "Khotang", "text": "Khotang"}, {"id": "Bhojpur", "text": "Bhojpur"}, {"id": "Dhankuta", "text": "Dhankuta"}, {"id": "Tehrathum", "text": "Tehrathum"}, {"id": "Panchthar", "text": "Panchthar"}, {"id": "Ilam", "text": "Ilam"}, {"id": "Jhapa", "text": "Jhapa"}, {"id": "Morang", "text": "Morang"}, {"id": "Sunsari", "text": "Sunsari"}, {"id": "Udayapur", "text": "Udayapur"}]}, "municipalities": {"Bajura": [{"id": "Himali Rural Municipality", "text": "Himali Rural Municipality"}, {"id": "Gaumul Rural Municipality", "text": "Gaumul Rural Municipality"}, {"id": "Badimalika Municipality", "text": "Badimalika Municipality"}, {"id": "Chhededaha Rural Municipality", "text": "Chhededaha Rural Municipality"}, {"id": "Budhiganga Municipality", "text": "Budhiganga Municipality"}, {"id": "Tribeni Municipality", "text": "Tribeni Municipality"}, {"id": "Jagannath Rural Municipality", "text": "Jagannath Rural Municipality"}, {"id": "Budhinanda Municipality", "text": "Budhinanda Municipality"}, {"id": "Swami Kartik Rural Municipality", "text": "Swami Kartik Rural Municipality"}], "Bajhang": [{"id": "Kanda Rural Municipality", "text": "Kanda Rural Municipality"}, {"id": "Bungal Municipality", "text": "Bungal Municipality"}, {"id": "Talkot Rural Municipality", "text": "Talkot Rural Municipality"}, {"id": "Masta Rural Municipality", "text": "Masta Rural Municipality"}, {"id": "Jayaprithbi Municipality", "text": "Jayaprithbi Municipality"}, {"id": "Chhabis Pathibhara Rural Municipality", "text": "Chhabis Pathibhara Rural Municipality"}, {"id": "Durgathali Rural Municipality", "text": "Durgathali Rural Municipality"}, {"id": "Bitthadchir Rural Municipality", "text": "Bitthadchir Rural Municipality"}, {"id": "Thalara Rural Municipality", "textLine truncated
@@ -45,8 +45,12 @@
if (!inputs || !inputs.length) return;
inputs.forEach(function (el) {
var format = el.dataset.format || 'YYYY-MM-DD';
// Convert strftime format to datepicker format
format = format.replace(/%Y/g, 'YYYY').replace(/%m/g, 'MM').replace(/%d/g, 'DD');
var options = {
dateFormat: 'YYYY-MM-DD',
dateFormat: format,
closeOnDateSelect: true
};
+5 -16
View File
@@ -8,10 +8,7 @@ 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).
"""
"""Convert an English (AD) date to a Nepali date."""
if isinstance(value, datetime.datetime):
if timezone.is_aware(value):
value = timezone.localtime(value)
@@ -23,9 +20,7 @@ def _coerce_ad_date_to_bs(value):
@register.filter
def nepali_date(value, format_str="%Y-%m-%d"):
"""
Formats a date or datetime object into a Nepali date string.
"""
"""Format a date as a Nepali string (e.g. 2080-01-01)."""
if value is None:
return ""
@@ -38,9 +33,7 @@ def nepali_date(value, format_str="%Y-%m-%d"):
@register.filter
def nepali_date_ne(value, format_str="%Y-%m-%d"):
"""
Formats a date or datetime object into a Nepali date string (Devanagari).
"""
"""Format a date using Nepali digits (e.g. २०८०-०१-०१)."""
if value is None:
return ""
@@ -53,9 +46,7 @@ def nepali_date_ne(value, format_str="%Y-%m-%d"):
@register.filter
def nepali_number(value):
"""
Converts a number to Devanagari.
"""
"""Convert any number to Nepali digits (123 -> १२३)."""
if value is None:
return ""
return nepalinumber(value).str_ne()
@@ -63,9 +54,7 @@ def nepali_number(value):
@register.filter
def nepali_humanize(value, threshold=None, format_str=None):
"""
Returns a human-readable "time ago" string in Nepali.
"""
"""Show 'time ago' in Nepali (like '५ मिनेट अगाडि')."""
if value is None:
return ""
+9 -19
View File
@@ -7,12 +7,12 @@ from nepali.locations import districts, provinces
from django_nepkit.conf import nepkit_settings
BS_DATE_FORMAT = "%Y-%m-%d"
BS_DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
BS_DATE_FORMAT = nepkit_settings.BS_DATE_FORMAT
BS_DATETIME_FORMAT = nepkit_settings.BS_DATETIME_FORMAT
def _try_parse_nepali(value: Any, cls: Any, fallback_fmt: str) -> Any:
"""Internal helper to parse Nepali date/datetime."""
"""Helper to turn a string into a Nepali date object."""
if value in (None, ""):
return None
if isinstance(value, cls):
@@ -31,29 +31,23 @@ def _try_parse_nepali(value: Any, cls: Any, fallback_fmt: str) -> Any:
def try_parse_nepali_date(value: Any) -> Optional[nepalidate]:
"""
Best-effort conversion to `nepalidate`.
"""
"""Convert any value to a Nepali Date."""
return _try_parse_nepali(value, nepalidate, BS_DATE_FORMAT)
def try_parse_nepali_datetime(value: Any) -> Optional[nepalidatetime]:
"""
Best-effort conversion to `nepalidatetime`.
"""
"""Convert any value to a Nepali Date and Time."""
return _try_parse_nepali(value, nepalidatetime, BS_DATETIME_FORMAT)
def _get_location_children(parent_list, parent_name, child_attr, ne=False):
"""
Internal helper to finding a parent in a list and returning its children (districts/municipalities).
"""
"""Find children (like districts) of a parent (like a province)."""
selected_parent = None
for p in parent_list:
p_name = p.name
p_name_ne = getattr(p, "name_nepali", None)
# Mapping fix for lookups
# Handle province name variations
if parent_name == "Koshi Province":
if p_name == "Province 1":
selected_parent = p
@@ -85,15 +79,11 @@ def _get_location_children(parent_list, parent_name, child_attr, ne=False):
def get_districts_by_province(province_name, ne=False, en=True):
"""
Returns a list of districts in the given province.
"""
"""Get all districts for a province."""
# Logic note: if ne=True is passed, we shouldn't care about en=True (handled by caller typically)
return _get_location_children(provinces, province_name, "districts", ne=ne)
def get_municipalities_by_district(district_name, ne=False, en=True):
"""
Returns a list of municipalities in the given district.
"""
"""Get all municipalities for a district."""
return _get_location_children(districts, district_name, "municipalities", ne=ne)
+1 -1
View File
@@ -3,7 +3,7 @@ from django.utils.translation import gettext_lazy as _
from nepali import phone_number
# validates the given value is a valid nepali phone number
# Check if a phone number is valid in Nepal
def validate_nepali_phone_number(value):
if not phone_number.is_valid(value):
raise ValidationError(
+31 -13
View File
@@ -1,5 +1,4 @@
from django.http import JsonResponse, HttpResponse
from django.template.loader import render_to_string
from django_nepkit.utils import (
get_districts_by_province,
@@ -7,6 +6,9 @@ from django_nepkit.utils import (
)
from django_nepkit.conf import nepkit_settings
def _render_options(data, placeholder):
"""Internal helper to render list of options as HTML."""
options = [f'<option value="">{placeholder}</option>']
@@ -17,7 +19,7 @@ def _render_options(data, placeholder):
def district_list_view(request):
province = request.GET.get("province")
# Fallback: take the first non-internal parameter as province
if not province:
for key, value in request.GET.items():
@@ -28,22 +30,30 @@ def district_list_view(request):
if not province:
return JsonResponse([], safe=False)
ne = request.GET.get("ne", "false").lower() == "true"
en = request.GET.get("en", "true").lower() == "true"
as_html = request.GET.get("html", "false").lower() == "true" or request.headers.get("HX-Request") == "true"
default_lang = nepkit_settings.DEFAULT_LANGUAGE
ne_param = request.GET.get("ne")
ne = ne_param.lower() == "true" if ne_param else default_lang == "ne"
en_param = request.GET.get("en")
en = en_param.lower() == "true" if en_param else not ne
as_html = (
request.GET.get("html", "false").lower() == "true"
or request.headers.get("HX-Request") == "true"
)
data = get_districts_by_province(province, ne=ne, en=en)
if as_html:
placeholder = "जिल्ला छान्नुहोस्" if ne else "Select District"
return _render_options(data, placeholder)
return JsonResponse(data, safe=False)
def municipality_list_view(request):
district = request.GET.get("district")
# Fallback: take the first non-internal parameter as district
if not district:
for key, value in request.GET.items():
@@ -54,14 +64,22 @@ def municipality_list_view(request):
if not district:
return JsonResponse([], safe=False)
ne = request.GET.get("ne", "false").lower() == "true"
en = request.GET.get("en", "true").lower() == "true"
as_html = request.GET.get("html", "false").lower() == "true" or request.headers.get("HX-Request") == "true"
default_lang = nepkit_settings.DEFAULT_LANGUAGE
ne_param = request.GET.get("ne")
ne = ne_param.lower() == "true" if ne_param else default_lang == "ne"
en_param = request.GET.get("en")
en = en_param.lower() == "true" if en_param else not ne
as_html = (
request.GET.get("html", "false").lower() == "true"
or request.headers.get("HX-Request") == "true"
)
data = get_municipalities_by_district(district, ne=ne, en=en)
if as_html:
placeholder = "नगरपालिका छान्नुहोस्" if ne else "Select Municipality"
return _render_options(data, placeholder)
return JsonResponse(data, safe=False)
+56 -35
View File
@@ -1,14 +1,13 @@
from django import forms
from django.urls import reverse_lazy
from django.urls import reverse, NoReverseMatch
from nepali.datetime import nepalidate
from django_nepkit.utils import BS_DATE_FORMAT
from django_nepkit.conf import nepkit_settings
def _append_css_class(attrs, class_name: str):
"""
Django-idiomatic helper to append a CSS class without clobbering existing ones.
"""
"""Helper to add a CSS class safely."""
existing = (attrs.get("class") or "").strip()
attrs["class"] = (f"{existing} {class_name}").strip() if existing else class_name
return attrs
@@ -18,27 +17,56 @@ class NepaliWidgetMixin:
def __init__(self, *args, **kwargs):
default_lang = nepkit_settings.DEFAULT_LANGUAGE
self.ne = kwargs.pop("ne", default_lang == "ne")
self.en = kwargs.pop("en", not self.ne)
self.htmx = kwargs.pop("htmx", False)
attrs = kwargs.get("attrs", {}) or {}
# Add data attributes
# Language settings
if self.ne:
attrs["data-ne"] = "true"
if self.en:
attrs["data-en"] = "true"
# Hook for subclasses to add specific classes/attrs
# Pass the date format to JavaScript
attrs["data-format"] = BS_DATE_FORMAT
self._configure_attrs(attrs)
kwargs["attrs"] = attrs
super().__init__(*args, **kwargs)
def _configure_attrs(self, attrs):
"""Override in subclasses to add specific classes or attributes."""
pass
"""Override in subclasses or use class attributes."""
css_class = getattr(self, "css_class", None)
if css_class:
_append_css_class(attrs, css_class)
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
widget_attrs = context.get("widget", {}).get("attrs", {})
# Handle URLs for data fetching
url_name = getattr(self, "_url_name", None)
if url_name:
try:
widget_attrs["data-url"] = reverse(url_name)
except NoReverseMatch:
pass
# Handle HTMX dynamic updates
if self.htmx:
hx_url_name = getattr(self, "_hx_url_name", None)
if hx_url_name:
try:
widget_attrs["hx-get"] = reverse(hx_url_name)
widget_attrs["hx-target"] = getattr(self, "_hx_target", "")
widget_attrs["hx-trigger"] = "change"
except NoReverseMatch:
pass
return context
class ChainedSelectWidget(forms.Select):
@@ -50,37 +78,29 @@ class ChainedSelectWidget(forms.Select):
class ProvinceSelectWidget(NepaliWidgetMixin, ChainedSelectWidget):
def _configure_attrs(self, attrs):
_append_css_class(attrs, "nepkit-province-select")
css_class = "nepkit-province-select"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.htmx:
url = reverse_lazy("django_nepkit:district-list")
attrs.update({
"hx-get": url,
"hx-target": ".nepkit-district-select",
"hx-trigger": "change",
})
self._hx_url_name = "django_nepkit:district-list"
self._hx_target = ".nepkit-district-select"
class DistrictSelectWidget(NepaliWidgetMixin, ChainedSelectWidget):
def _configure_attrs(self, attrs):
_append_css_class(attrs, "nepkit-district-select")
url = reverse_lazy("django_nepkit:district-list")
attrs["data-url"] = url
css_class = "nepkit-district-select"
_url_name = "django_nepkit:district-list"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.htmx:
url_muni = reverse_lazy("django_nepkit:municipality-list")
attrs.update({
"hx-get": url_muni,
"hx-target": ".nepkit-municipality-select",
"hx-trigger": "change",
})
self._hx_url_name = "django_nepkit:municipality-list"
self._hx_target = ".nepkit-municipality-select"
class MunicipalitySelectWidget(NepaliWidgetMixin, ChainedSelectWidget):
def _configure_attrs(self, attrs):
_append_css_class(attrs, "nepkit-municipality-select")
url = reverse_lazy("django_nepkit:municipality-list")
attrs["data-url"] = url
css_class = "nepkit-municipality-select"
_url_name = "django_nepkit:municipality-list"
class NepaliDatePickerWidget(NepaliWidgetMixin, forms.TextInput):
@@ -99,7 +119,6 @@ class NepaliDatePickerWidget(NepaliWidgetMixin, forms.TextInput):
)
def _configure_attrs(self, attrs):
# Clean up Django admin class if present
classes = attrs.get("class", "")
if "vDateField" in classes:
classes = classes.replace("vDateField", "")
@@ -107,7 +126,9 @@ class NepaliDatePickerWidget(NepaliWidgetMixin, forms.TextInput):
_append_css_class(attrs, "nepkit-datepicker")
attrs["autocomplete"] = "off"
attrs["placeholder"] = "YYYY-MM-DD"
attrs["placeholder"] = (
BS_DATE_FORMAT.replace("%Y", "YYYY").replace("%m", "MM").replace("%d", "DD")
)
def format_value(self, value):
if value is None:
@@ -115,6 +136,6 @@ class NepaliDatePickerWidget(NepaliWidgetMixin, forms.TextInput):
if self.ne and isinstance(value, nepalidate):
if hasattr(value, "strftime_ne"):
return value.strftime_ne("%Y-%m-%d")
return value.strftime_ne(BS_DATE_FORMAT)
return super().format_value(value)
+45
View File
@@ -0,0 +1,45 @@
from django.db import migrations
from nepali.datetime import nepalidate
def convert_ad_to_bs(apps, schema_editor):
"""Logic to convert English (AD) dates to Nepali (BS)."""
# Set your app and model name here
MyModel = apps.get_model("YourApp", "YourModel")
# Loop through all records and update them
for obj in MyModel.objects.all():
# Name of your old and new fields
if obj.birth_date_ad:
try:
# Turn English date into Nepali date
bs_date = nepalidate.from_date(obj.birth_date_ad)
# Format the date correctly for the database
obj.birth_date = bs_date.strftime("%Y-%m-%d")
obj.save(update_fields=["birth_date"])
except Exception as e:
print(f"Error converting object {obj.pk}: {e}")
def reverse_bs_to_ad(apps, schema_editor):
"""Optional: Logic to convert back to English dates."""
# Note: BS to AD conversion is also supported by the 'nepali' library.
pass
class Migration(migrations.Migration):
dependencies = [
("YourApp", "previous_migration_name"),
]
operations = [
# 1. Add the new field
# migrations.AddField(
# model_name='yourmodel',
# name='birth_date',
# field=django_nepkit.models.NepaliDateField(null=True, blank=True),
# ),
# 2. Run the conversion
migrations.RunPython(convert_ad_to_bs, reverse_code=reverse_bs_to_ad),
# 3. Clean up the old field
]
+38 -3
View File
@@ -1,10 +1,16 @@
from django.contrib import admin
from django_nepkit import NepaliModelAdmin
from django_nepkit import (
NepaliModelAdmin,
NepaliAdminMixin,
NepaliDateFilter,
NepaliMonthFilter,
)
from .models import Person
from .models import Person, Citizen, AuditedPerson
# Example 1: Basic setup
@admin.register(Person)
class PersonAdmin(NepaliModelAdmin):
list_display = (
@@ -21,5 +27,34 @@ class PersonAdmin(NepaliModelAdmin):
"created_at",
"updated_at",
)
list_filter = ("birth_date", "province", "district")
# Added Year and Month filters
list_filter = (
("birth_date", NepaliDateFilter),
("birth_date", NepaliMonthFilter),
"province",
)
search_fields = ("name", "phone_number")
# Example 2: Using a Mixin
class BaseAuditAdmin(admin.ModelAdmin):
"""A fake base class for testing."""
pass
# Use the Mixin if you already have a base Admin class
@admin.register(AuditedPerson)
class CustomPersonAdmin(NepaliAdminMixin, BaseAuditAdmin):
list_display = ("name", "birth_date", "created_at")
list_filter = (
("created_at", NepaliMonthFilter), # Filter by Nepali Month
)
# Example 3: HTMX support
@admin.register(Citizen)
class CitizenAdmin(NepaliModelAdmin):
list_display = ("name", "province", "district", "municipality")
# Filters still work with HTMX
list_filter = ("province", "district")
@@ -5,7 +5,6 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("demo", "0001_initial"),
]
@@ -5,7 +5,6 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("demo", "0002_alter_person_district_en_and_more"),
]
@@ -4,7 +4,6 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("demo", "0003_alter_person_birth_date_alter_person_birth_date_en_and_more"),
]
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,115 @@
# Generated by Django 6.0.1 on 2026-01-30 09:43
import django_nepkit.models
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("demo", "0005_citizen"),
]
operations = [
migrations.AlterField(
model_name="citizen",
name="district",
field=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,
),
),
migrations.AlterField(
model_name="citizen",
name="province",
field=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"),
("Koshi Province", "Koshi Province"),
],
max_length=100,
),
),
]
@@ -0,0 +1,35 @@
# Generated by Django 6.0.1 on 2026-01-30 09:47
import django_nepkit.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("demo", "0006_alter_citizen_district_alter_citizen_province"),
]
operations = [
migrations.CreateModel(
name="AuditedPerson",
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(max_length=10)),
(
"created_at",
django_nepkit.models.NepaliDateTimeField(
auto_now_add=True, blank=True, editable=False, max_length=19
),
),
],
),
]
+34 -13
View File
@@ -12,22 +12,43 @@ from django_nepkit import (
class Person(models.Model):
name = models.CharField(max_length=100)
# Date/time fields - now default to English via NEPKIT['DEFAULT_LANGUAGE']
birth_date = NepaliDateField() # Defaults to en=True
birth_date_ne = NepaliDateField(ne=True, blank=True, null=True) # Explicitly Nepali
registration_time = NepaliTimeField(auto_now_add=True) # Defaults to en=True
# Dates and times
birth_date = NepaliDateField() # English by default
birth_date_ne = NepaliDateField(ne=True, blank=True, null=True) # Using Devanagari
registration_time = NepaliTimeField(auto_now_add=True) # English digits by default
phone_number = NepaliPhoneNumberField()
# Address chaining - now default to English via NEPKIT['DEFAULT_LANGUAGE']
province = ProvinceField() # Defaults to en=True
province_ne = ProvinceField(ne=True, blank=True, null=True) # Explicitly Nepali
district = DistrictField() # Defaults to en=True
district_ne = DistrictField(ne=True, blank=True, null=True) # Explicitly Nepali
municipality = MunicipalityField() # Defaults to en=True
municipality_ne = MunicipalityField(ne=True, blank=True, null=True) # Explicitly Nepali
# Location fields
province = ProvinceField() # English names by default
province_ne = ProvinceField(ne=True, blank=True, null=True) # In Devanagari
district = DistrictField()
district_ne = DistrictField(ne=True, blank=True, null=True)
municipality = MunicipalityField()
municipality_ne = MunicipalityField(ne=True, blank=True, null=True)
created_at = NepaliDateTimeField(auto_now_add=True) # Defaults to en=True
updated_at = NepaliDateTimeField(auto_now=True) # Defaults to en=True
created_at = NepaliDateTimeField(auto_now_add=True)
updated_at = NepaliDateTimeField(auto_now=True)
def __str__(self):
return self.name
class Citizen(models.Model):
name = models.CharField(max_length=100)
# Chaining works automatically
province = ProvinceField()
district = DistrictField()
municipality = MunicipalityField()
def __str__(self):
return self.name
class AuditedPerson(models.Model):
name = models.CharField(max_length=100)
birth_date = NepaliDateField()
created_at = NepaliDateTimeField(auto_now_add=True)
def __str__(self):
return self.name
+25
View File
@@ -0,0 +1,25 @@
from rest_framework import serializers
from django_nepkit.serializers import NepaliDateSerializerField
from .models import Person, Citizen, AuditedPerson
class PersonSerializer(serializers.ModelSerializer):
birth_date = NepaliDateSerializerField()
class Meta:
model = Person
fields = "__all__"
class CitizenSerializer(serializers.ModelSerializer):
class Meta:
model = Citizen
fields = "__all__"
class AuditedPersonSerializer(serializers.ModelSerializer):
birth_date = NepaliDateSerializerField()
class Meta:
model = AuditedPerson
fields = "__all__"
+8 -2
View File
@@ -1,9 +1,15 @@
from django.urls import path
from . import views
from rest_framework.routers import DefaultRouter
from django.urls import path
app_name = "demo"
router = DefaultRouter()
router.register("api/persons", views.PersonViewSet, basename="person-api")
router.register("api/citizens", views.CitizenViewSet, basename="citizen-api")
router.register("api/audited", views.AuditedPersonViewSet, basename="audited-api")
urlpatterns = [
path("", views.person_list, name="person-list"),
path("add/", views.person_create, name="person-create"),
]
] + router.urls
+54 -2
View File
@@ -1,6 +1,10 @@
from django.shortcuts import render, redirect
from .models import Person
from django import forms
from django.shortcuts import render, redirect
from rest_framework import viewsets, filters as drf_filters
from django_filters import rest_framework as django_filters
from django_nepkit.filters import NepaliDateYearFilter, NepaliDateMonthFilter
from .models import Person, Citizen, AuditedPerson
from .serializers import PersonSerializer, CitizenSerializer, AuditedPersonSerializer
class PersonForm(forms.ModelForm):
@@ -34,3 +38,51 @@ def person_create(request):
else:
form = PersonForm()
return render(request, "demo/person_form.html", {"form": form})
# --- API Support (DRF) ---
class PersonViewSet(viewsets.ModelViewSet):
queryset = Person.objects.all()
serializer_class = PersonSerializer
# Sort and Search work automatically with BS date strings
filter_backends = [
django_filters.DjangoFilterBackend,
drf_filters.SearchFilter,
drf_filters.OrderingFilter,
]
ordering_fields = ["birth_date", "created_at"]
search_fields = ["name", "birth_date", "phone_number"]
# Simple exact date filter
filterset_fields = {
"birth_date": ["exact"],
}
class PersonFilter(django_filters.FilterSet):
year = NepaliDateYearFilter(field_name="birth_date")
month = NepaliDateMonthFilter(field_name="birth_date")
class Meta:
model = Person
fields = ["province", "district"]
filterset_class = PersonFilter
class CitizenViewSet(viewsets.ModelViewSet):
queryset = Citizen.objects.all()
serializer_class = CitizenSerializer
filter_backends = [drf_filters.OrderingFilter]
ordering_fields = ["province", "district"]
class AuditedPersonViewSet(viewsets.ModelViewSet):
queryset = AuditedPerson.objects.all()
serializer_class = AuditedPersonSerializer
filter_backends = [drf_filters.SearchFilter, drf_filters.OrderingFilter]
search_fields = ["name", "birth_date"]
ordering_fields = ["created_at"]
+2
View File
@@ -14,6 +14,8 @@ INSTALLED_APPS = [
"django.contrib.messages",
"django.contrib.staticfiles",
"django_nepkit",
"rest_framework",
"django_filters",
"demo",
]
+2 -2
View File
@@ -1,12 +1,12 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
"""Django utility for running commands."""
import os
import sys
def main():
"""Run administrative tasks."""
"""Main entry point for Django commands."""
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_project.settings")
try:
+3
View File
@@ -15,7 +15,10 @@ license = { text = "MIT" }
requires-python = ">=3.11"
dependencies = [
"django>=4.2",
"django-filter>=25.2",
"djangorestframework>=3.16.1",
"nepali>=1.1.3",
"pytest>=9.0.2",
]
keywords = [
"django",
Generated
+82
View File
@@ -24,6 +24,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "distlib"
version = "0.4.0"
@@ -67,6 +76,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/95/b5/814ed98bd21235c116fd3436a7ed44d47560329a6d694ec8aac2982dbb93/django-6.0.1-py3-none-any.whl", hash = "sha256:a92a4ff14f664a896f9849009cb8afaca7abe0d6fc53325f3d1895a15253433d", size = 8338791, upload-time = "2026-01-06T18:55:46.175Z" },
]
[[package]]
name = "django-filter"
version = "25.2"
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/2c/e4/465d2699cd388c0005fb8d6ae6709f239917c6d8790ac35719676fffdcf3/django_filter-25.2.tar.gz", hash = "sha256:760e984a931f4468d096f5541787efb8998c61217b73006163bf2f9523fe8f23", size = 143818, upload-time = "2025-10-05T09:51:31.521Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c1/40/6a02495c5658beb1f31eb09952d8aa12ef3c2a66342331ce3a35f7132439/django_filter-25.2-py3-none-any.whl", hash = "sha256:9c0f8609057309bba611062fe1b720b4a873652541192d232dd28970383633e3", size = 94145, upload-time = "2025-10-05T09:51:29.728Z" },
]
[[package]]
name = "django-nepkit"
version = "0.1.0"
@@ -74,11 +96,15 @@ source = { editable = "." }
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'" },
{ name = "django-filter" },
{ name = "djangorestframework" },
{ name = "nepali" },
{ name = "pytest" },
]
[package.optional-dependencies]
drf = [
{ name = "django-filter" },
{ name = "djangorestframework" },
]
@@ -91,8 +117,12 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "django", specifier = ">=4.2" },
{ name = "django-filter", specifier = ">=25.2" },
{ name = "django-filter", marker = "extra == 'drf'", specifier = ">=23.1" },
{ name = "djangorestframework", specifier = ">=3.16.1" },
{ name = "djangorestframework", marker = "extra == 'drf'", specifier = ">=3.14" },
{ name = "nepali", specifier = ">=1.1.3" },
{ name = "pytest", specifier = ">=9.0.2" },
]
provides-extras = ["drf"]
@@ -133,6 +163,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0", size = 99202, upload-time = "2026-01-12T18:58:56.627Z" },
]
[[package]]
name = "iniconfig"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[package]]
name = "nepali"
version = "1.1.3"
@@ -151,6 +190,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" },
]
[[package]]
name = "packaging"
version = "26.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
]
[[package]]
name = "platformdirs"
version = "4.5.1"
@@ -160,6 +208,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "pre-commit"
version = "4.5.1"
@@ -176,6 +233,31 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" },
]
[[package]]
name = "pygments"
version = "2.19.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
[[package]]
name = "pytest"
version = "9.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
]
[[package]]
name = "pyyaml"
version = "6.0.3"