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:
32 files changed
+3002
-531
No files matched your search
@@ -4,121 +4,99 @@
|
||||
|
||||
[](https://badge.fury.io/py/django-nepkit)
|
||||
[](https://pypi.org/project/django-nepkit/)
|
||||
[](https://www.djangoproject.com/)
|
||||
[](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.
|
||||
@@ -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
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
@@ -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
@@ -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"),
|
||||
]
|
||||
|
||||
-1
@@ -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
@@ -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
|
||||
@@ -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__"
|
||||
@@ -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
@@ -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"]
|
||||
@@ -14,6 +14,8 @@ INSTALLED_APPS = [
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"django_nepkit",
|
||||
"rest_framework",
|
||||
"django_filters",
|
||||
"demo",
|
||||
]
|
||||
|
||||
|
||||
+2
-2
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user