use dry principle possible and added example project
This commit is contained in:
46 files changed
+3521
-772
No files matched your search
@@ -0,0 +1,26 @@
|
||||
name: pre-commit
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
pre-commit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: "uv.lock"
|
||||
|
||||
- name: Set up Python
|
||||
run: uv python install 3.10
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --all-extras --dev
|
||||
|
||||
- name: Run pre-commit
|
||||
run: uv run pre-commit run --all-files --show-diff-on-failure
|
||||
@@ -0,0 +1,27 @@
|
||||
name: Publish to PyPI
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
environment: release
|
||||
permissions:
|
||||
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.10'
|
||||
cache: 'pip'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install build twine
|
||||
- name: Build package
|
||||
run: python -m build
|
||||
- name: Publish package to PyPI
|
||||
run: python -m twine upload dist/* --verbose
|
||||
+2
-2
@@ -182,9 +182,9 @@ cython_debug/
|
||||
.abstra/
|
||||
|
||||
# Visual Studio Code
|
||||
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
||||
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
||||
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
||||
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
||||
# you could uncomment the following to ignore the entire vscode folder
|
||||
# .vscode/
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: check-yaml
|
||||
- id: check-toml
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- id: check-added-large-files
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.14.14
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix]
|
||||
- id: ruff-format
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
include LICENSE
|
||||
include README.md
|
||||
recursive-include django_nepkit/static *
|
||||
@@ -0,0 +1,13 @@
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install django-nepkit
|
||||
```
|
||||
|
||||
### Optional: Django REST Framework support
|
||||
|
||||
If you want serializer fields from `django_nepkit.serializers`, install the extra:
|
||||
|
||||
```bash
|
||||
pip install "django-nepkit[drf]"
|
||||
```
|
||||
@@ -1,155 +0,0 @@
|
||||
from django.contrib import admin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from nepali.datetime import nepalidate, nepalidatetime
|
||||
|
||||
from django_nepkit.models import NepaliDateField
|
||||
|
||||
|
||||
def format_nepali_date(date_value, format_string="%B %d, %Y"):
|
||||
"""
|
||||
Format a nepalidate object with Nepali month names.
|
||||
|
||||
Args:
|
||||
date_value: A nepalidate object or string in YYYY-MM-DD format
|
||||
format_string: strftime format string (default: '%B %d, %Y')
|
||||
%B = Full month name (Baishak, Jestha, etc.)
|
||||
%b = Short month name
|
||||
%d = Day of month
|
||||
%Y = Year
|
||||
|
||||
Returns:
|
||||
Formatted date string with Nepali month names, or empty string if invalid
|
||||
"""
|
||||
|
||||
if date_value is None:
|
||||
return ""
|
||||
|
||||
try:
|
||||
if isinstance(date_value, str):
|
||||
date_value = nepalidate.strptime(date_value, "%Y-%m-%d")
|
||||
|
||||
if isinstance(date_value, nepalidate):
|
||||
return date_value.strftime(format_string)
|
||||
except (ValueError, TypeError, AttributeError):
|
||||
pass
|
||||
|
||||
return str(date_value) if date_value else ""
|
||||
|
||||
|
||||
def format_nepali_datetime(datetime_value, format_string="%B %d, %Y %I:%M %p"):
|
||||
"""
|
||||
Format a nepalidatetime object with Nepali month names.
|
||||
|
||||
Default output uses **12-hour time with AM/PM**.
|
||||
"""
|
||||
if datetime_value is None:
|
||||
return ""
|
||||
|
||||
try:
|
||||
if isinstance(datetime_value, str):
|
||||
datetime_value = nepalidatetime.strptime(
|
||||
datetime_value, "%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
|
||||
if isinstance(datetime_value, nepalidatetime):
|
||||
return datetime_value.strftime(format_string)
|
||||
except (ValueError, TypeError, AttributeError):
|
||||
pass
|
||||
|
||||
return str(datetime_value) if datetime_value else ""
|
||||
|
||||
|
||||
class NepaliDateFilter(admin.FieldListFilter):
|
||||
"""
|
||||
A list filter for NepaliDateField to filter by BS Year.
|
||||
"""
|
||||
|
||||
def __init__(self, field, request, params, model, model_admin, field_path):
|
||||
self.parameter_name = f"{field_path}_bs_year"
|
||||
super().__init__(field, request, params, model, model_admin, field_path)
|
||||
self.title = _("Nepali Date (Year)")
|
||||
|
||||
def expected_parameters(self):
|
||||
return [self.parameter_name]
|
||||
|
||||
def choices(self, changelist):
|
||||
yield {
|
||||
"selected": self.used_parameters.get(self.parameter_name) is None,
|
||||
"query_string": changelist.get_query_string(remove=[self.parameter_name]),
|
||||
"display": _("All"),
|
||||
}
|
||||
current_year = nepalidate.today().year
|
||||
for year in range(current_year - 10, current_year + 2):
|
||||
yield {
|
||||
"selected": self.used_parameters.get(self.parameter_name) == str(year),
|
||||
"query_string": changelist.get_query_string(
|
||||
{self.parameter_name: str(year)}
|
||||
),
|
||||
"display": str(year),
|
||||
}
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
value = self.used_parameters.get(self.parameter_name)
|
||||
if value:
|
||||
year = int(value)
|
||||
# Convert BS year range to AD date range
|
||||
start_date_bs = nepalidate(year, 1, 1)
|
||||
# Find last day of the year. Some BS years end on the 30th,
|
||||
# so we try 31 first and then fall back to 30 if that date
|
||||
# is not valid.
|
||||
try:
|
||||
end_date_bs = nepalidate(year, 12, 31)
|
||||
except ValueError:
|
||||
end_date_bs = nepalidate(year, 12, 30)
|
||||
|
||||
start_date_ad = start_date_bs.to_date()
|
||||
end_date_ad = end_date_bs.to_date()
|
||||
|
||||
return queryset.filter(
|
||||
**{f"{self.field_path}__range": (start_date_ad, end_date_ad)}
|
||||
)
|
||||
return queryset
|
||||
|
||||
|
||||
admin.FieldListFilter.register(
|
||||
lambda f: isinstance(f, NepaliDateField), NepaliDateFilter, take_priority=True
|
||||
)
|
||||
|
||||
|
||||
class NepaliAdminMixin:
|
||||
"""
|
||||
Mixin for Django admin classes that provides Nepali date utilities.
|
||||
Makes format_nepali_date and NepaliDateFilter available without explicit imports.
|
||||
"""
|
||||
|
||||
def format_nepali_date(self, date_value, format_string="%B %d, %Y"):
|
||||
"""
|
||||
Format a nepalidate object with Nepali month names.
|
||||
Available as a method on admin classes using this mixin.
|
||||
"""
|
||||
return format_nepali_date(date_value, format_string)
|
||||
|
||||
def format_nepali_datetime(
|
||||
self, datetime_value, format_string="%B %d, %Y %I:%M %p"
|
||||
):
|
||||
return format_nepali_datetime(datetime_value, format_string)
|
||||
|
||||
|
||||
class NepaliModelAdmin(NepaliAdminMixin, admin.ModelAdmin):
|
||||
"""
|
||||
Base ModelAdmin class with Nepali date utilities built-in.
|
||||
Use this instead of admin.ModelAdmin to have format_nepali_date available.
|
||||
|
||||
Example:
|
||||
from django_nepkit import NepaliModelAdmin, NepaliDateFilter
|
||||
|
||||
@admin.register(MyModel)
|
||||
class MyModelAdmin(NepaliModelAdmin):
|
||||
list_filter = (('date_field', NepaliDateFilter),)
|
||||
|
||||
def display_date(self, obj):
|
||||
return self.format_nepali_date(obj.date_field)
|
||||
"""
|
||||
|
||||
# Make NepaliDateFilter available as a class attribute
|
||||
NepaliDateFilter = NepaliDateFilter
|
||||
@@ -0,0 +1,31 @@
|
||||
from .models import (
|
||||
NepaliDateField,
|
||||
NepaliTimeField,
|
||||
NepaliDateTimeField,
|
||||
NepaliPhoneNumberField,
|
||||
ProvinceField,
|
||||
DistrictField,
|
||||
MunicipalityField,
|
||||
)
|
||||
from .admin import (
|
||||
NepaliDateFilter,
|
||||
format_nepali_date,
|
||||
format_nepali_datetime,
|
||||
NepaliModelAdmin,
|
||||
NepaliAdminMixin,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"NepaliDateField",
|
||||
"NepaliTimeField",
|
||||
"NepaliDateTimeField",
|
||||
"NepaliPhoneNumberField",
|
||||
"ProvinceField",
|
||||
"DistrictField",
|
||||
"MunicipalityField",
|
||||
"NepaliDateFilter",
|
||||
"format_nepali_date",
|
||||
"format_nepali_datetime",
|
||||
"NepaliModelAdmin",
|
||||
"NepaliAdminMixin",
|
||||
]
|
||||
+139
-102
@@ -1,143 +1,122 @@
|
||||
from django.contrib import admin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from nepali.datetime import nepalidate, nepalidatetime
|
||||
|
||||
from django_nepkit.models import NepaliDateField
|
||||
from django_nepkit.utils import (
|
||||
try_parse_nepali_date,
|
||||
try_parse_nepali_datetime,
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# Internal helpers
|
||||
# --------------------------------------------------
|
||||
|
||||
def _parse_nepali_date(value):
|
||||
def format_nepali_date(date_value, format_string="%B %d, %Y"):
|
||||
"""
|
||||
Normalize string / nepalidate to nepalidate.
|
||||
Format a nepalidate object with Nepali month names.
|
||||
|
||||
Args:
|
||||
date_value: A nepalidate object or string in YYYY-MM-DD format
|
||||
format_string: strftime format string (default: '%B %d, %Y')
|
||||
%B = Full month name (Baishak, Jestha, etc.)
|
||||
%b = Short month name
|
||||
%d = Day of month
|
||||
%Y = Year
|
||||
|
||||
Returns:
|
||||
Formatted date string with Nepali month names, or empty string if invalid
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
if date_value is None:
|
||||
return ""
|
||||
|
||||
if isinstance(value, nepalidate):
|
||||
return value
|
||||
try:
|
||||
parsed = try_parse_nepali_date(date_value)
|
||||
if parsed is not None:
|
||||
return parsed.strftime(format_string)
|
||||
if isinstance(date_value, nepalidate):
|
||||
return date_value.strftime(
|
||||
format_string
|
||||
) # defensive; should be covered above
|
||||
except (ValueError, TypeError, AttributeError):
|
||||
pass
|
||||
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return nepalidate.strptime(value, "%Y-%m-%d")
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
return None
|
||||
return str(date_value) if date_value else ""
|
||||
|
||||
|
||||
def _parse_nepali_datetime(value):
|
||||
def format_nepali_datetime(datetime_value, format_string="%B %d, %Y %I:%M %p"):
|
||||
"""
|
||||
Normalize string / nepalidatetime to nepalidatetime.
|
||||
Format a nepalidatetime object with Nepali month names.
|
||||
|
||||
Default output uses **12-hour time with AM/PM**.
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
if datetime_value is None:
|
||||
return ""
|
||||
|
||||
if isinstance(value, nepalidatetime):
|
||||
return value
|
||||
try:
|
||||
parsed = try_parse_nepali_datetime(datetime_value)
|
||||
if parsed is not None:
|
||||
return parsed.strftime(format_string)
|
||||
if isinstance(datetime_value, nepalidatetime):
|
||||
return datetime_value.strftime(
|
||||
format_string
|
||||
) # defensive; should be covered above
|
||||
except (ValueError, TypeError, AttributeError):
|
||||
pass
|
||||
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return nepalidatetime.strptime(value, "%Y-%m-%d %H:%M:%S")
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
return str(datetime_value) if datetime_value else ""
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# Formatting helpers
|
||||
# --------------------------------------------------
|
||||
|
||||
def format_nepali_date(value, format_string="%B %d, %Y"):
|
||||
"""
|
||||
Format a Nepali (BS) date with Nepali month names.
|
||||
"""
|
||||
nd = _parse_nepali_date(value)
|
||||
return nd.strftime(format_string) if nd else ""
|
||||
|
||||
|
||||
def format_nepali_datetime(value, format_string="%B %d, %Y %I:%M %p"):
|
||||
"""
|
||||
Format a Nepali (BS) datetime (12-hour format by default).
|
||||
"""
|
||||
ndt = _parse_nepali_datetime(value)
|
||||
return ndt.strftime(format_string) if ndt else ""
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# Admin List Filter (BS Year)
|
||||
# --------------------------------------------------
|
||||
|
||||
class NepaliDateFilter(admin.FieldListFilter):
|
||||
"""
|
||||
Admin list filter for NepaliDateField by BS year.
|
||||
A list filter for NepaliDateField to filter by BS Year.
|
||||
"""
|
||||
|
||||
title = _("Nepali Date (Year)")
|
||||
|
||||
def __init__(self, field, request, params, model, model_admin, field_path):
|
||||
self.field_path = field_path
|
||||
self.parameter_name = f"{field_path}_bs_year"
|
||||
super().__init__(field, request, params, model, model_admin, field_path)
|
||||
self.title = _("Nepali Date (Year)")
|
||||
|
||||
def expected_parameters(self):
|
||||
return [self.parameter_name]
|
||||
|
||||
def _year_choices(self):
|
||||
current_year = nepalidate.today().year
|
||||
return range(current_year - 10, current_year + 2)
|
||||
|
||||
def choices(self, changelist):
|
||||
yield {
|
||||
"selected": self.parameter_name not in self.used_parameters,
|
||||
"selected": self.used_parameters.get(self.parameter_name) is None,
|
||||
"query_string": changelist.get_query_string(remove=[self.parameter_name]),
|
||||
"display": _("All"),
|
||||
}
|
||||
|
||||
for year in self._year_choices():
|
||||
current_year = nepalidate.today().year
|
||||
for year in range(current_year - 10, current_year + 2):
|
||||
yield {
|
||||
"selected": self.used_parameters.get(self.parameter_name) == str(year),
|
||||
"query_string": changelist.get_query_string(
|
||||
{self.parameter_name: year}
|
||||
{self.parameter_name: str(year)}
|
||||
),
|
||||
"display": year,
|
||||
"display": str(year),
|
||||
}
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
value = self.used_parameters.get(self.parameter_name)
|
||||
if not value:
|
||||
return queryset
|
||||
|
||||
try:
|
||||
if value:
|
||||
year = int(value)
|
||||
except (TypeError, ValueError):
|
||||
return queryset
|
||||
|
||||
start_bs = nepalidate(year, 1, 1)
|
||||
|
||||
# BS year length varies (30/31 days)
|
||||
for last_day in (31, 30):
|
||||
# Convert BS year range to AD date range
|
||||
start_date_bs = nepalidate(year, 1, 1)
|
||||
# Find last day of the year. Some BS years end on the 30th,
|
||||
# so we try 31 first and then fall back to 30 if that date
|
||||
# is not valid.
|
||||
try:
|
||||
end_bs = nepalidate(year, 12, last_day)
|
||||
break
|
||||
end_date_bs = nepalidate(year, 12, 31)
|
||||
except ValueError:
|
||||
continue
|
||||
end_date_bs = nepalidate(year, 12, 30)
|
||||
|
||||
return queryset.filter(
|
||||
**{
|
||||
f"{self.field_path}__range": (
|
||||
start_bs.to_date(),
|
||||
end_bs.to_date(),
|
||||
)
|
||||
}
|
||||
)
|
||||
start_date_ad = start_date_bs.to_date()
|
||||
end_date_ad = end_date_bs.to_date()
|
||||
|
||||
return queryset.filter(
|
||||
**{f"{self.field_path}__range": (start_date_ad, end_date_ad)}
|
||||
)
|
||||
return queryset
|
||||
|
||||
|
||||
# Register filter automatically for NepaliDateField
|
||||
# Register NepaliDateFilter as the default list filter for NepaliDateField
|
||||
admin.FieldListFilter.register(
|
||||
lambda f: isinstance(f, NepaliDateField),
|
||||
NepaliDateFilter,
|
||||
@@ -145,30 +124,88 @@ admin.FieldListFilter.register(
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# Admin mixins
|
||||
# --------------------------------------------------
|
||||
|
||||
class NepaliAdminMixin:
|
||||
"""
|
||||
Adds Nepali date formatting helpers to admin classes.
|
||||
Mixin for Django admin classes that provides Nepali date utilities.
|
||||
Makes format_nepali_date and NepaliDateFilter available without explicit imports.
|
||||
"""
|
||||
|
||||
format_nepali_date = staticmethod(format_nepali_date)
|
||||
format_nepali_datetime = staticmethod(format_nepali_datetime)
|
||||
def format_nepali_date(self, date_value, format_string="%B %d, %Y"):
|
||||
"""
|
||||
Format a nepalidate object with Nepali month names.
|
||||
Available as a method on admin classes using this mixin.
|
||||
"""
|
||||
return format_nepali_date(date_value, format_string)
|
||||
|
||||
def format_nepali_datetime(
|
||||
self, datetime_value, format_string="%B %d, %Y %I:%M %p"
|
||||
):
|
||||
return format_nepali_datetime(datetime_value, format_string)
|
||||
|
||||
|
||||
class NepaliModelAdmin(NepaliAdminMixin, admin.ModelAdmin):
|
||||
"""
|
||||
Base ModelAdmin with Nepali utilities.
|
||||
Base ModelAdmin class with Nepali date utilities built-in.
|
||||
Use this instead of admin.ModelAdmin to have format_nepali_date available.
|
||||
|
||||
Example:
|
||||
from django_nepkit import NepaliModelAdmin, NepaliDateFilter
|
||||
|
||||
@admin.register(MyModel)
|
||||
class MyModelAdmin(NepaliModelAdmin):
|
||||
list_filter = (('date_bs', NepaliDateFilter),)
|
||||
list_filter = (('date_field', NepaliDateFilter),)
|
||||
|
||||
def date_display(self, obj):
|
||||
return self.format_nepali_date(obj.date_bs)
|
||||
def display_date(self, obj):
|
||||
return self.format_nepali_date(obj.date_field)
|
||||
"""
|
||||
|
||||
# Make NepaliDateFilter available as a class attribute
|
||||
NepaliDateFilter = NepaliDateFilter
|
||||
|
||||
# Ensure admin forms render Nepali fields with the proper widget,
|
||||
# even if a project doesn't provide custom ModelForms.
|
||||
def formfield_for_dbfield(self, db_field, request, **kwargs):
|
||||
"""
|
||||
Force Nepali widgets in Django admin without requiring user forms.
|
||||
"""
|
||||
try:
|
||||
from django_nepkit.models import NepaliDateField, NepaliDateTimeField
|
||||
from django_nepkit.widgets import NepaliDatePickerWidget
|
||||
except Exception:
|
||||
return super().formfield_for_dbfield(db_field, request, **kwargs)
|
||||
|
||||
if isinstance(db_field, (NepaliDateField, NepaliDateTimeField)):
|
||||
kwargs.setdefault("widget", NepaliDatePickerWidget)
|
||||
|
||||
return super().formfield_for_dbfield(db_field, request, **kwargs)
|
||||
|
||||
class Media:
|
||||
"""
|
||||
Django admin ships jQuery as `django.jQuery` (not `window.jQuery`).
|
||||
The Nepali date picker library expects a global `jQuery`, so we bridge it.
|
||||
"""
|
||||
|
||||
css = {
|
||||
"all": (
|
||||
"https://nepalidatepicker.sajanmaharjan.com.np/v5/nepali.datepicker/css/nepali.datepicker.v5.0.6.min.css",
|
||||
"django_nepkit/css/admin-nepali-datepicker.css",
|
||||
)
|
||||
}
|
||||
js = (
|
||||
# Bridge admin's `django.jQuery` -> `window.jQuery`
|
||||
"django_nepkit/js/admin-jquery-bridge.js",
|
||||
# Date picker lib
|
||||
"https://nepalidatepicker.sajanmaharjan.com.np/v5/nepali.datepicker/js/nepali.datepicker.v5.0.6.min.js",
|
||||
# Init
|
||||
"django_nepkit/js/nepali-datepicker-init.js",
|
||||
)
|
||||
|
||||
|
||||
# Exporting for easy usage
|
||||
__all__ = [
|
||||
"NepaliDateFilter",
|
||||
"format_nepali_date",
|
||||
"format_nepali_datetime",
|
||||
"NepaliAdminMixin",
|
||||
"NepaliModelAdmin",
|
||||
]
|
||||
+25
-52
@@ -2,76 +2,49 @@ from datetime import date as python_date
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from nepali.datetime import nepalidate
|
||||
|
||||
from django_nepkit.utils import try_parse_nepali_date
|
||||
from django_nepkit.validators import validate_nepali_phone_number
|
||||
from django_nepkit.widgets import NepaliDatePickerWidget
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# Nepali Phone Number Form Field
|
||||
# --------------------------------------------------
|
||||
|
||||
class NepaliPhoneNumberField(forms.CharField):
|
||||
class NepaliDateFormField(forms.DateField):
|
||||
"""
|
||||
Form field for Nepali phone numbers.
|
||||
A Django Form Field for Nepali Date (Bikram Sambat).
|
||||
"""
|
||||
|
||||
default_error_messages = {
|
||||
"invalid": _("Enter a valid Nepali phone number."),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.validators.append(validate_nepali_phone_number)
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# Nepali Date (BS) Form Field
|
||||
# --------------------------------------------------
|
||||
|
||||
class NepaliDateFormField(forms.Field):
|
||||
"""
|
||||
Form field for Nepali (Bikram Sambat) date.
|
||||
|
||||
Accepts:
|
||||
- YYYY-MM-DD (string)
|
||||
- python date
|
||||
- nepalidate instance
|
||||
|
||||
Returns:
|
||||
- nepalidate instance
|
||||
"""
|
||||
from .widgets import NepaliDatePickerWidget
|
||||
|
||||
widget = NepaliDatePickerWidget
|
||||
|
||||
default_error_messages = {
|
||||
"invalid": _("Enter a valid Nepali date in YYYY-MM-DD format."),
|
||||
}
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.pop("max_length", None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def to_python(self, value):
|
||||
if value in self.empty_values:
|
||||
return None
|
||||
|
||||
if isinstance(value, nepalidate):
|
||||
return value
|
||||
|
||||
if isinstance(value, python_date):
|
||||
return nepalidate.from_date(value)
|
||||
try:
|
||||
parsed = try_parse_nepali_date(str(value))
|
||||
if parsed is not None:
|
||||
return parsed
|
||||
raise ValueError("Invalid BS date format")
|
||||
except Exception:
|
||||
raise forms.ValidationError(
|
||||
_("Enter a valid Nepali date in YYYY-MM-DD format."),
|
||||
code="invalid",
|
||||
)
|
||||
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return nepalidate.strptime(value.strip(), "%Y-%m-%d")
|
||||
except (ValueError, TypeError):
|
||||
self.fail("invalid")
|
||||
|
||||
self.fail("invalid")
|
||||
class NepaliPhoneNumberFormField(forms.CharField):
|
||||
"""
|
||||
A Django Form Field for Nepali Phone Numbers.
|
||||
"""
|
||||
|
||||
def validate(self, value):
|
||||
"""
|
||||
Override default validation to allow nepalidate objects.
|
||||
"""
|
||||
if value is None:
|
||||
return
|
||||
if not isinstance(value, nepalidate):
|
||||
self.fail("invalid")
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.validators.append(validate_nepali_phone_number)
|
||||
+210
-152
@@ -3,24 +3,18 @@ from datetime import date as python_date
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from nepali.datetime import nepalidate, nepalidatetime
|
||||
from nepali.locations import districts, municipalities, provinces
|
||||
|
||||
from django_nepkit.forms import NepaliDateFormField
|
||||
from django_nepkit.validators import validate_nepali_phone_number
|
||||
from django_nepkit.widgets import (
|
||||
DistrictSelectWidget,
|
||||
MunicipalitySelectWidget,
|
||||
NepaliDatePickerWidget,
|
||||
ProvinceSelectWidget,
|
||||
from django_nepkit.utils import (
|
||||
BS_DATE_FORMAT,
|
||||
BS_DATETIME_FORMAT,
|
||||
try_parse_nepali_date,
|
||||
try_parse_nepali_datetime,
|
||||
)
|
||||
|
||||
# --------------------------------------------------
|
||||
# Phone Number
|
||||
# --------------------------------------------------
|
||||
from django_nepkit.validators import validate_nepali_phone_number
|
||||
|
||||
|
||||
class NepaliPhoneNumberField(models.CharField):
|
||||
description = _("Nepali phone number")
|
||||
description = _("Nepali Phone Number")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault("max_length", 10)
|
||||
@@ -28,133 +22,77 @@ class NepaliPhoneNumberField(models.CharField):
|
||||
self.validators.append(validate_nepali_phone_number)
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# Location Fields
|
||||
# --------------------------------------------------
|
||||
class NepaliDateField(models.CharField):
|
||||
"""
|
||||
A Django Model Field that stores Nepali (Bikram Sambat) Date.
|
||||
Internally it stores the date as BS string (YYYY-MM-DD) in the database.
|
||||
"""
|
||||
|
||||
|
||||
class ProvinceField(models.CharField):
|
||||
description = _("Nepal Province")
|
||||
description = _("Nepali Date (Bikram Sambat)")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault("max_length", 100)
|
||||
kwargs.setdefault(
|
||||
"choices",
|
||||
[(p.name, p.name) for p in provinces],
|
||||
)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.auto_now = kwargs.pop("auto_now", False)
|
||||
self.auto_now_add = kwargs.pop("auto_now_add", False)
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
kwargs.setdefault("widget", ProvinceSelectWidget)
|
||||
return super().formfield(**kwargs)
|
||||
|
||||
|
||||
class DistrictField(models.CharField):
|
||||
description = _("Nepal District")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault("max_length", 100)
|
||||
kwargs.setdefault(
|
||||
"choices",
|
||||
[(d.name, d.name) for d in districts],
|
||||
)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
kwargs.setdefault("widget", DistrictSelectWidget)
|
||||
return super().formfield(**kwargs)
|
||||
|
||||
|
||||
class MunicipalityField(models.CharField):
|
||||
description = _("Nepal Municipality")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault("max_length", 100)
|
||||
kwargs.setdefault(
|
||||
"choices",
|
||||
[(m.name, m.name) for m in municipalities],
|
||||
)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
kwargs.setdefault("widget", MunicipalitySelectWidget)
|
||||
return super().formfield(**kwargs)
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# Time Field
|
||||
# --------------------------------------------------
|
||||
class NepaliTimeField(models.TimeField):
|
||||
"""
|
||||
Time field for Nepal (standard time).
|
||||
This is essentially Django's TimeField,
|
||||
kept for semantic clarity and future extensions.
|
||||
"""
|
||||
|
||||
description = _("Nepali Time")
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
return super().formfield(**kwargs)
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# Base Nepali BS Date / DateTime Field
|
||||
# --------------------------------------------------
|
||||
|
||||
|
||||
class BaseNepaliDateCharField(models.CharField):
|
||||
"""
|
||||
Base class for Nepali (Bikram Sambat) Date / DateTime fields.
|
||||
Stored as string in DB, returned as nepali date objects in Python.
|
||||
"""
|
||||
|
||||
format = None
|
||||
nepali_type = None
|
||||
|
||||
def __init__(self, *args, auto_now=False, auto_now_add=False, **kwargs):
|
||||
self.auto_now = auto_now
|
||||
self.auto_now_add = auto_now_add
|
||||
|
||||
if auto_now or auto_now_add:
|
||||
# Match Django's DateField behavior
|
||||
if self.auto_now or self.auto_now_add:
|
||||
kwargs.setdefault("editable", False)
|
||||
kwargs.setdefault("blank", True)
|
||||
|
||||
kwargs.setdefault("max_length", 10)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _now(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def pre_save(self, model_instance, add):
|
||||
if self.auto_now or (self.auto_now_add and add):
|
||||
value = self._now().strftime(self.format)
|
||||
# Using nepalidate.today() to get current BS date.
|
||||
# This is BS-native as much as possible.
|
||||
value = nepalidate.today().strftime(BS_DATE_FORMAT)
|
||||
setattr(model_instance, self.attname, value)
|
||||
return value
|
||||
return super().pre_save(model_instance, add)
|
||||
|
||||
def to_python(self, value):
|
||||
if value is None or isinstance(value, self.nepali_type):
|
||||
return value
|
||||
|
||||
if isinstance(value, python_date):
|
||||
return self.nepali_type.from_date(value)
|
||||
|
||||
if isinstance(value, str):
|
||||
return self.nepali_type.strptime(value.strip(), self.format)
|
||||
|
||||
return value
|
||||
|
||||
def from_db_value(self, value, expression, connection):
|
||||
if value is None:
|
||||
parsed = try_parse_nepali_date(value)
|
||||
return parsed if parsed is not None else value
|
||||
|
||||
def to_python(self, value):
|
||||
if value is None or isinstance(value, nepalidate):
|
||||
return value
|
||||
return self.nepali_type.strptime(value, self.format)
|
||||
if isinstance(value, python_date):
|
||||
# If we MUST handle AD date object, we convert it to BS.
|
||||
# But we should prefer strings or nepalidate.
|
||||
try:
|
||||
return nepalidate.from_date(value)
|
||||
except (ValueError, TypeError):
|
||||
return str(value)
|
||||
if isinstance(value, str):
|
||||
parsed = try_parse_nepali_date(value)
|
||||
return parsed if parsed is not None else value
|
||||
return super().to_python(value)
|
||||
|
||||
def validate(self, value, model_instance):
|
||||
if isinstance(value, nepalidate):
|
||||
value = value.strftime(BS_DATE_FORMAT)
|
||||
super().validate(value, model_instance)
|
||||
|
||||
def run_validators(self, value):
|
||||
if isinstance(value, nepalidate):
|
||||
value = value.strftime(BS_DATE_FORMAT)
|
||||
super().run_validators(value)
|
||||
|
||||
def get_prep_value(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
if isinstance(value, self.nepali_type):
|
||||
return value.strftime(self.format)
|
||||
|
||||
return value
|
||||
if isinstance(value, nepalidate):
|
||||
return value.strftime(BS_DATE_FORMAT)
|
||||
if isinstance(value, python_date):
|
||||
try:
|
||||
return nepalidate.from_date(value).strftime(BS_DATE_FORMAT)
|
||||
except (ValueError, TypeError):
|
||||
return str(value)
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
# Fallback: convert to string
|
||||
return str(value)
|
||||
|
||||
def deconstruct(self):
|
||||
@@ -165,47 +103,167 @@ class BaseNepaliDateCharField(models.CharField):
|
||||
kwargs["auto_now_add"] = True
|
||||
return name, path, args, kwargs
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# Nepali Date Field (BS)
|
||||
# --------------------------------------------------
|
||||
|
||||
|
||||
class NepaliDateField(BaseNepaliDateCharField):
|
||||
description = _("Nepali Date (Bikram Sambat)")
|
||||
format = "%Y-%m-%d"
|
||||
nepali_type = nepalidate
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault("max_length", 10)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _now(self):
|
||||
return nepalidate.today()
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
kwargs.setdefault("form_class", NepaliDateFormField)
|
||||
kwargs.setdefault("widget", NepaliDatePickerWidget)
|
||||
return super().formfield(**kwargs)
|
||||
from .forms import NepaliDateFormField
|
||||
from .widgets import NepaliDatePickerWidget
|
||||
|
||||
defaults = {
|
||||
"form_class": NepaliDateFormField,
|
||||
"widget": NepaliDatePickerWidget,
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
return super().formfield(**defaults)
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# Nepali DateTime Field (BS)
|
||||
# --------------------------------------------------
|
||||
class NepaliTimeField(models.TimeField):
|
||||
"""
|
||||
A Django Model Field for Time with Nepali localization support.
|
||||
Supports auto_now and auto_now_add like standard Django TimeField.
|
||||
"""
|
||||
|
||||
description = _("Nepali Time")
|
||||
|
||||
|
||||
class NepaliDateTimeField(BaseNepaliDateCharField):
|
||||
class NepaliDateTimeField(models.CharField):
|
||||
"""
|
||||
A Django Model Field that stores Nepali (Bikram Sambat) DateTime.
|
||||
Internally it stores the datetime as BS string (YYYY-MM-DD HH:MM:SS) in the database.
|
||||
"""
|
||||
|
||||
description = _("Nepali DateTime (Bikram Sambat)")
|
||||
format = "%Y-%m-%d %H:%M:%S"
|
||||
nepali_type = nepalidatetime
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault("max_length", 19)
|
||||
self.auto_now = kwargs.pop("auto_now", False)
|
||||
self.auto_now_add = kwargs.pop("auto_now_add", False)
|
||||
|
||||
# Match Django's DateTimeField behavior
|
||||
if self.auto_now or self.auto_now_add:
|
||||
kwargs.setdefault("editable", False)
|
||||
kwargs.setdefault("blank", True)
|
||||
|
||||
kwargs.setdefault("max_length", 19) # YYYY-MM-DD HH:MM:SS
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _now(self):
|
||||
return nepalidatetime.now()
|
||||
def pre_save(self, model_instance, add):
|
||||
if self.auto_now or (self.auto_now_add and add):
|
||||
# Using nepalidatetime.now() to get current BS datetime
|
||||
value = nepalidatetime.now().strftime(BS_DATETIME_FORMAT)
|
||||
setattr(model_instance, self.attname, value)
|
||||
return value
|
||||
return super().pre_save(model_instance, add)
|
||||
|
||||
def from_db_value(self, value, expression, connection):
|
||||
parsed = try_parse_nepali_datetime(value)
|
||||
return parsed if parsed is not None else value
|
||||
|
||||
def to_python(self, value):
|
||||
if value is None or isinstance(value, nepalidatetime):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
parsed = try_parse_nepali_datetime(value)
|
||||
return parsed if parsed is not None else value
|
||||
return super().to_python(value)
|
||||
|
||||
def validate(self, value, model_instance):
|
||||
if isinstance(value, nepalidatetime):
|
||||
value = value.strftime(BS_DATETIME_FORMAT)
|
||||
super().validate(value, model_instance)
|
||||
|
||||
def run_validators(self, value):
|
||||
if isinstance(value, nepalidatetime):
|
||||
value = value.strftime(BS_DATETIME_FORMAT)
|
||||
super().run_validators(value)
|
||||
|
||||
def get_prep_value(self, value):
|
||||
if value is None:
|
||||
return value
|
||||
if isinstance(value, nepalidatetime):
|
||||
return value.strftime(BS_DATETIME_FORMAT)
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
# Fallback: convert to string
|
||||
return str(value)
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super().deconstruct()
|
||||
if self.auto_now:
|
||||
kwargs["auto_now"] = True
|
||||
if self.auto_now_add:
|
||||
kwargs["auto_now_add"] = True
|
||||
return name, path, args, kwargs
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
kwargs.setdefault("widget", NepaliDatePickerWidget)
|
||||
return super().formfield(**kwargs)
|
||||
from .widgets import NepaliDatePickerWidget
|
||||
|
||||
defaults = {
|
||||
"widget": NepaliDatePickerWidget,
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
return super().formfield(**defaults)
|
||||
|
||||
|
||||
class ProvinceField(models.CharField):
|
||||
"""
|
||||
A Django Model Field for Nepali Provinces.
|
||||
"""
|
||||
|
||||
description = _("Nepali Province")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
from nepali.locations import provinces
|
||||
|
||||
kwargs.setdefault("max_length", 100)
|
||||
kwargs.setdefault("choices", [(p.name, p.name) for p in provinces])
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
from .widgets import ProvinceSelectWidget
|
||||
|
||||
defaults = {"widget": ProvinceSelectWidget}
|
||||
defaults.update(kwargs)
|
||||
return super().formfield(**defaults)
|
||||
|
||||
|
||||
class DistrictField(models.CharField):
|
||||
"""
|
||||
A Django Model Field for Nepali Districts.
|
||||
"""
|
||||
|
||||
description = _("Nepali District")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
from nepali.locations import districts
|
||||
|
||||
kwargs.setdefault("max_length", 100)
|
||||
kwargs.setdefault("choices", [(d.name, d.name) for d in districts])
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
from .widgets import DistrictSelectWidget
|
||||
|
||||
defaults = {"widget": DistrictSelectWidget}
|
||||
defaults.update(kwargs)
|
||||
return super().formfield(**defaults)
|
||||
|
||||
|
||||
class MunicipalityField(models.CharField):
|
||||
"""
|
||||
A Django Model Field for Nepali Municipalities.
|
||||
Includes Metropolitan, Sub-Metropolitan, Municipality, and Rural Municipality.
|
||||
"""
|
||||
|
||||
description = _("Nepali Municipality")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
from nepali.locations import municipalities
|
||||
|
||||
kwargs.setdefault("max_length", 100)
|
||||
kwargs.setdefault("choices", [(m.name, m.name) for m in municipalities])
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
from .widgets import MunicipalitySelectWidget
|
||||
|
||||
defaults = {"widget": MunicipalitySelectWidget}
|
||||
defaults.update(kwargs)
|
||||
return super().formfield(**defaults)
|
||||
@@ -1,5 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Optional, Type
|
||||
|
||||
from nepali.datetime import nepalidate, nepalidatetime
|
||||
from rest_framework import serializers
|
||||
|
||||
try:
|
||||
from rest_framework import serializers
|
||||
except ModuleNotFoundError as e:
|
||||
raise ModuleNotFoundError(
|
||||
"django-nepkit DRF support is optional. Install with `django-nepkit[drf]` "
|
||||
"to use `django_nepkit.serializers`."
|
||||
) from e
|
||||
|
||||
from django_nepkit.utils import try_parse_nepali_date, try_parse_nepali_datetime
|
||||
|
||||
# --------------------------------------------------
|
||||
# Base Serializer Field
|
||||
@@ -9,34 +22,65 @@ from rest_framework import serializers
|
||||
class BaseNepaliBSField(serializers.Field):
|
||||
"""
|
||||
Base DRF field for Nepali (BS) Date / DateTime.
|
||||
|
||||
- **Input**: BS string, or an already-parsed `nepalidate`/`nepalidatetime`
|
||||
- **Output**: formatted BS string (configurable)
|
||||
"""
|
||||
|
||||
format = None
|
||||
nepali_type = None
|
||||
error_messages = {"invalid": "Invalid Bikram Sambat format. Expected {format}."}
|
||||
format: str = ""
|
||||
nepali_type: Type[object] = object
|
||||
|
||||
def to_representation(self, value):
|
||||
default_error_messages = {
|
||||
"invalid": "Invalid Bikram Sambat value. Expected format: {format}.",
|
||||
"invalid_type": "Invalid type. Expected a string.",
|
||||
}
|
||||
|
||||
def __init__(self, *, format: Optional[str] = None, **kwargs: Any) -> None:
|
||||
"""
|
||||
Args:
|
||||
format: Optional `strftime` format used for representation.
|
||||
If not provided, uses the class default.
|
||||
"""
|
||||
if format is not None:
|
||||
self.format = format
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def _parse(self, value: str):
|
||||
if self.nepali_type is nepalidate:
|
||||
return try_parse_nepali_date(value)
|
||||
if self.nepali_type is nepalidatetime:
|
||||
return try_parse_nepali_datetime(value)
|
||||
return None
|
||||
|
||||
def to_representation(self, value: Any) -> Optional[str]:
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
if isinstance(value, self.nepali_type):
|
||||
return value.strftime(self.format)
|
||||
return value.strftime(self.format) # type: ignore[attr-defined]
|
||||
|
||||
# Fallback: DB string
|
||||
# If DB returns string, try to normalize it.
|
||||
if isinstance(value, str):
|
||||
parsed = self._parse(value)
|
||||
if parsed is not None:
|
||||
return parsed.strftime(self.format) # type: ignore[attr-defined]
|
||||
|
||||
# Fallback: best-effort stringify (keeps behavior non-breaking)
|
||||
return str(value)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
def to_internal_value(self, data: Any):
|
||||
if data in (None, ""):
|
||||
return None
|
||||
|
||||
if isinstance(data, self.nepali_type):
|
||||
return data
|
||||
|
||||
if isinstance(data, str):
|
||||
try:
|
||||
return self.nepali_type.strptime(data.strip(), self.format)
|
||||
except Exception:
|
||||
self.fail("invalid", format=self.format)
|
||||
if not isinstance(data, str):
|
||||
self.fail("invalid_type")
|
||||
|
||||
parsed = self._parse(data)
|
||||
if parsed is not None:
|
||||
return parsed
|
||||
|
||||
self.fail("invalid", format=self.format)
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
.ndp-container.ndp-dark{
|
||||
background:#1f2937; /* slate-800-ish */
|
||||
color:#e5e7eb;
|
||||
border:1px solid rgba(255,255,255,.12);
|
||||
box-shadow:0 10px 25px rgba(0,0,0,.45);
|
||||
}
|
||||
|
||||
.ndp-container.ndp-dark .ndp-header{
|
||||
border-bottom:1px solid rgba(255,255,255,.12);
|
||||
}
|
||||
|
||||
.ndp-container.ndp-dark .ndp-header .ndp-header-link{
|
||||
color:#e5e7eb;
|
||||
}
|
||||
|
||||
.ndp-container.ndp-dark .ndc-nav-button:hover,
|
||||
.ndp-container.ndp-dark .ndp-header-link:hover,
|
||||
.ndp-container.ndp-dark .ndp-table td:hover{
|
||||
background:rgba(255,255,255,.08);
|
||||
}
|
||||
|
||||
.ndp-container.ndp-dark .ndp-table th{
|
||||
color:rgba(229,231,235,.75);
|
||||
}
|
||||
|
||||
.ndp-container.ndp-dark .ndp-table td{
|
||||
color:#e5e7eb;
|
||||
}
|
||||
|
||||
.ndp-container.ndp-dark .ndp-table td.ndp-selected{
|
||||
background:#2563eb; /* blue-600 */
|
||||
color:#fff;
|
||||
}
|
||||
|
||||
.ndp-container.ndp-dark .ndp-table td.ndp-today{
|
||||
color:#93c5fd; /* blue-300 */
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
function updateOptions(selectElement, data, placeholder) {
|
||||
selectElement.innerHTML = '';
|
||||
if (placeholder) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = '';
|
||||
opt.textContent = placeholder;
|
||||
selectElement.appendChild(opt);
|
||||
}
|
||||
data.forEach(item => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = item.id;
|
||||
opt.textContent = item.text;
|
||||
selectElement.appendChild(opt);
|
||||
});
|
||||
selectElement.dispatchEvent(new Event('change'));
|
||||
}
|
||||
|
||||
document.addEventListener('change', function(e) {
|
||||
if (e.target.matches('.nepkit-province-select')) {
|
||||
const province = e.target.value;
|
||||
const container = e.target.closest('form') || document;
|
||||
const districtSelect = container.querySelector('.nepkit-district-select');
|
||||
const municipalitySelect = container.querySelector('.nepkit-municipality-select');
|
||||
|
||||
if (districtSelect) {
|
||||
if (!province) {
|
||||
updateOptions(districtSelect, [], 'Select District');
|
||||
if (municipalitySelect) updateOptions(municipalitySelect, [], 'Select Municipality');
|
||||
return;
|
||||
}
|
||||
|
||||
const url = districtSelect.dataset.url + '?province=' + encodeURIComponent(province);
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
updateOptions(districtSelect, data, 'Select District');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (e.target.matches('.nepkit-district-select')) {
|
||||
const district = e.target.value;
|
||||
const container = e.target.closest('form') || document;
|
||||
const municipalitySelect = container.querySelector('.nepkit-municipality-select');
|
||||
|
||||
if (municipalitySelect) {
|
||||
if (!district) {
|
||||
updateOptions(municipalitySelect, [], 'Select Municipality');
|
||||
return;
|
||||
}
|
||||
|
||||
const url = municipalitySelect.dataset.url + '?district=' + encodeURIComponent(district);
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
updateOptions(municipalitySelect, data, 'Select Municipality');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
// Django admin exposes jQuery as `django.jQuery`.
|
||||
// Many third-party plugins expect `window.jQuery`/`window.$`.
|
||||
if (typeof window !== "undefined" && window.django && window.django.jQuery) {
|
||||
window.jQuery = window.django.jQuery;
|
||||
window.$ = window.django.jQuery;
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function isDarkTheme() {
|
||||
var docEl = document.documentElement;
|
||||
var body = document.body;
|
||||
|
||||
// Common theme markers used by Django admin themes / dark-mode packages
|
||||
var markers = [
|
||||
docEl && docEl.getAttribute('data-theme'),
|
||||
docEl && docEl.getAttribute('data-color-scheme'),
|
||||
body && body.getAttribute('data-theme'),
|
||||
body && body.getAttribute('data-color-scheme')
|
||||
].filter(Boolean).join(' ').toLowerCase();
|
||||
|
||||
var classNames = [
|
||||
docEl && docEl.className,
|
||||
body && body.className
|
||||
].filter(Boolean).join(' ').toLowerCase();
|
||||
|
||||
if (markers.indexOf('dark') !== -1 || classNames.indexOf('dark') !== -1) return true;
|
||||
|
||||
// Fallback to OS preference
|
||||
try {
|
||||
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function applyThemeToDatepickerContainers() {
|
||||
var dark = isDarkTheme();
|
||||
var containers = document.querySelectorAll('.ndp-container');
|
||||
if (!containers || !containers.length) return;
|
||||
|
||||
containers.forEach(function (c) {
|
||||
c.classList.remove('ndp-dark');
|
||||
c.classList.remove('ndp-light');
|
||||
c.classList.add(dark ? 'ndp-dark' : 'ndp-light');
|
||||
});
|
||||
}
|
||||
|
||||
function initNepaliDatePickers() {
|
||||
var inputs = document.querySelectorAll('.nepkit-datepicker:not(.nepali-datepicker-initialized)');
|
||||
if (!inputs || !inputs.length) return;
|
||||
|
||||
inputs.forEach(function (el) {
|
||||
// Nepali Datepicker v5 exposes `element.NepaliDatePicker(options)`
|
||||
if (typeof el.NepaliDatePicker === 'function') {
|
||||
el.NepaliDatePicker({
|
||||
dateFormat: 'YYYY-MM-DD'
|
||||
});
|
||||
el.classList.add('nepali-datepicker-initialized');
|
||||
|
||||
// Ensure theme is applied when the picker is actually shown.
|
||||
// (The plugin often creates/inserts DOM on focus.)
|
||||
var applySoon = function () {
|
||||
// Run twice: once immediately, once after paint.
|
||||
applyThemeToDatepickerContainers();
|
||||
window.setTimeout(applyThemeToDatepickerContainers, 0);
|
||||
};
|
||||
|
||||
el.addEventListener('focus', applySoon);
|
||||
el.addEventListener('click', applySoon);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initNepaliDatePickers);
|
||||
} else {
|
||||
initNepaliDatePickers();
|
||||
}
|
||||
|
||||
// Keep in sync if system theme changes (fallback path)
|
||||
if (window.matchMedia) {
|
||||
try {
|
||||
var mql = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
if (mql && typeof mql.addEventListener === 'function') {
|
||||
mql.addEventListener('change', applyThemeToDatepickerContainers);
|
||||
} else if (mql && typeof mql.addListener === 'function') {
|
||||
mql.addListener(applyThemeToDatepickerContainers);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
// Re-initialize when Django admin adds inlines dynamically
|
||||
if (typeof django !== 'undefined' && django.jQuery) {
|
||||
django.jQuery(document).on('formset:added', function () {
|
||||
initNepaliDatePickers();
|
||||
});
|
||||
}
|
||||
|
||||
// Also listen for DOM changes (admin popups/other dynamic content)
|
||||
if (typeof MutationObserver !== 'undefined') {
|
||||
var observer = new MutationObserver(function () {
|
||||
initNepaliDatePickers();
|
||||
applyThemeToDatepickerContainers();
|
||||
});
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
})();
|
||||
@@ -1,89 +0,0 @@
|
||||
(function() {
|
||||
"use strict";
|
||||
|
||||
function updateOptions(selectElement, data, placeholder) {
|
||||
selectElement.innerHTML = "";
|
||||
if (placeholder) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = "";
|
||||
opt.textContent = placeholder;
|
||||
selectElement.appendChild(opt);
|
||||
}
|
||||
data.forEach((item) => {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = item.id;
|
||||
opt.textContent = item.text;
|
||||
selectElement.appendChild(opt);
|
||||
});
|
||||
selectElement.dispatchEvent(new Event("change"));
|
||||
}
|
||||
|
||||
document.addEventListener("change", function(e) {
|
||||
if (e.target.matches(".nepkit-province-select")) {
|
||||
const province = e.target.value;
|
||||
const container = e.target.closest("form") || document;
|
||||
const districtSelect = container.querySelector(
|
||||
".nepkit-district-select",
|
||||
);
|
||||
const municipalitySelect = container.querySelector(
|
||||
".nepkit-municipality-select",
|
||||
);
|
||||
|
||||
if (districtSelect) {
|
||||
if (!province) {
|
||||
updateOptions(districtSelect, [], "Select District");
|
||||
if (municipalitySelect)
|
||||
updateOptions(
|
||||
municipalitySelect,
|
||||
[],
|
||||
"Select Municipality",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const url =
|
||||
districtSelect.dataset.url +
|
||||
"?province=" +
|
||||
encodeURIComponent(province);
|
||||
fetch(url)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
updateOptions(districtSelect, data, "Select District");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (e.target.matches(".nepkit-district-select")) {
|
||||
const district = e.target.value;
|
||||
const container = e.target.closest("form") || document;
|
||||
const municipalitySelect = container.querySelector(
|
||||
".nepkit-municipality-select",
|
||||
);
|
||||
|
||||
if (municipalitySelect) {
|
||||
if (!district) {
|
||||
updateOptions(
|
||||
municipalitySelect,
|
||||
[],
|
||||
"Select Municipality",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const url =
|
||||
municipalitySelect.dataset.url +
|
||||
"?district=" +
|
||||
encodeURIComponent(district);
|
||||
fetch(url)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
updateOptions(
|
||||
municipalitySelect,
|
||||
data,
|
||||
"Select Municipality",
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -1,45 +0,0 @@
|
||||
(function($) {
|
||||
"use strict";
|
||||
|
||||
function initNepaliDatePickers() {
|
||||
if ($.fn.nepaliDatePicker) {
|
||||
// Initialize any date pickers that aren't already initialized
|
||||
$(".nepkit-datepicker")
|
||||
.not(".nepali-datepicker-initialized")
|
||||
.each(function() {
|
||||
$(this)
|
||||
.nepaliDatePicker({
|
||||
dateFormat: "%y-%m-%d",
|
||||
closeOnDateSelect: true,
|
||||
minDate: "१९७५-१-१",
|
||||
maxDate: "२१००-१२-३०",
|
||||
})
|
||||
.addClass("nepali-datepicker-initialized");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
$(document).ready(function() {
|
||||
initNepaliDatePickers();
|
||||
});
|
||||
|
||||
// Re-initialize when Django admin adds forms dynamically (e.g., inlines, popups)
|
||||
// This handles Django admin's dynamic form loading
|
||||
if (typeof django !== "undefined" && django.jQuery) {
|
||||
django.jQuery(document).on("formset:added", function() {
|
||||
initNepaliDatePickers();
|
||||
});
|
||||
}
|
||||
|
||||
// Also listen for DOM changes (for admin popups and other dynamic content)
|
||||
if (typeof MutationObserver !== "undefined") {
|
||||
var observer = new MutationObserver(function(mutations) {
|
||||
initNepaliDatePickers();
|
||||
});
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
}
|
||||
})(window.jQuery || window.django.jQuery);
|
||||
@@ -1,100 +0,0 @@
|
||||
import datetime
|
||||
|
||||
from django import template
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from nepali.datetime import nepalidate, nepalihumanize
|
||||
from nepali.number import nepalinumber
|
||||
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# Internal helpers
|
||||
# --------------------------------------------------
|
||||
|
||||
def _to_nepali_date(value):
|
||||
"""
|
||||
Normalize AD date/datetime or BS date to nepalidate.
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
if isinstance(value, nepalidate):
|
||||
return value
|
||||
|
||||
if isinstance(value, (datetime.date, datetime.datetime)):
|
||||
return nepalidate.from_date(value)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# Date filters
|
||||
# --------------------------------------------------
|
||||
|
||||
@register.filter(is_safe=True)
|
||||
def nepali_date(value, format_str="%Y-%m-%d"):
|
||||
"""
|
||||
Format a date/datetime as Nepali (BS) date.
|
||||
"""
|
||||
nd = _to_nepali_date(value)
|
||||
if not nd:
|
||||
return ""
|
||||
|
||||
return nd.strftime(format_str)
|
||||
|
||||
|
||||
@register.filter(is_safe=True)
|
||||
def nepali_date_ne(value, format_str="%Y-%m-%d"):
|
||||
"""
|
||||
Format a date/datetime as Nepali (BS) date in Devanagari.
|
||||
"""
|
||||
nd = _to_nepali_date(value)
|
||||
if not nd:
|
||||
return ""
|
||||
|
||||
return nd.strftime_ne(format_str)
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# Number filter
|
||||
# --------------------------------------------------
|
||||
|
||||
@register.filter(is_safe=True)
|
||||
def nepali_number(value):
|
||||
"""
|
||||
Convert a number to Nepali (Devanagari) digits.
|
||||
"""
|
||||
if value in (None, ""):
|
||||
return ""
|
||||
|
||||
try:
|
||||
return nepalinumber(value).str_ne()
|
||||
except Exception:
|
||||
return value
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# Humanize filter
|
||||
# --------------------------------------------------
|
||||
|
||||
@register.filter(is_safe=True)
|
||||
def nepali_humanize(value, threshold=None, format_str=None):
|
||||
"""
|
||||
Human-readable Nepali time difference.
|
||||
"""
|
||||
if value is None:
|
||||
return ""
|
||||
|
||||
kwargs = {}
|
||||
if threshold is not None:
|
||||
kwargs["threshold"] = threshold
|
||||
if format_str is not None:
|
||||
kwargs["format"] = format_str
|
||||
|
||||
try:
|
||||
return nepalihumanize(value, **kwargs)
|
||||
except Exception:
|
||||
return value
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
from django import template
|
||||
from nepali.datetime import nepalidate, nepalihumanize
|
||||
from nepali.number import nepalinumber
|
||||
import datetime
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
def _coerce_ad_date_to_bs(value):
|
||||
"""
|
||||
Internal helper: if value is AD date/datetime, convert to `nepalidate`.
|
||||
Does NOT attempt to parse strings (keeps template behavior minimal).
|
||||
"""
|
||||
if isinstance(value, (datetime.datetime, datetime.date)):
|
||||
return nepalidate.from_date(value)
|
||||
return value
|
||||
|
||||
|
||||
@register.filter
|
||||
def nepali_date(value, format_str="%Y-%m-%d"):
|
||||
"""
|
||||
Formats a date or datetime object into a Nepali date string.
|
||||
"""
|
||||
if value is None:
|
||||
return ""
|
||||
|
||||
value = _coerce_ad_date_to_bs(value)
|
||||
|
||||
if hasattr(value, "strftime"):
|
||||
return value.strftime(format_str)
|
||||
return value
|
||||
|
||||
|
||||
@register.filter
|
||||
def nepali_date_ne(value, format_str="%Y-%m-%d"):
|
||||
"""
|
||||
Formats a date or datetime object into a Nepali date string (Devanagari).
|
||||
"""
|
||||
if value is None:
|
||||
return ""
|
||||
|
||||
value = _coerce_ad_date_to_bs(value)
|
||||
|
||||
if hasattr(value, "strftime_ne"):
|
||||
return value.strftime_ne(format_str)
|
||||
return value
|
||||
|
||||
|
||||
@register.filter
|
||||
def nepali_number(value):
|
||||
"""
|
||||
Converts a number to Devanagari.
|
||||
"""
|
||||
if value is None:
|
||||
return ""
|
||||
return nepalinumber(value).str_ne()
|
||||
|
||||
|
||||
@register.filter
|
||||
def nepali_humanize(value, threshold=None, format_str=None):
|
||||
"""
|
||||
Returns a human-readable "time ago" string in Nepali.
|
||||
"""
|
||||
if value is None:
|
||||
return ""
|
||||
|
||||
# Threshold and format_str are optional
|
||||
kwargs = {}
|
||||
if threshold:
|
||||
kwargs["threshold"] = threshold
|
||||
if format_str:
|
||||
kwargs["format"] = format_str
|
||||
|
||||
return nepalihumanize(value, **kwargs)
|
||||
+65
-3
@@ -1,14 +1,76 @@
|
||||
from nepali.locations import districts, provices
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Optional
|
||||
|
||||
from nepali.datetime import nepalidate, nepalidatetime
|
||||
|
||||
|
||||
def get_districts_by_province(provice_name):
|
||||
selected_province = next((p for p in provices if p.name == provice_name), None)
|
||||
BS_DATE_FORMAT = "%Y-%m-%d"
|
||||
BS_DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
|
||||
def try_parse_nepali_date(value: Any) -> Optional[nepalidate]:
|
||||
"""
|
||||
Best-effort conversion to `nepalidate`.
|
||||
|
||||
- Returns `None` for empty values.
|
||||
- Returns `nepalidate` for valid inputs.
|
||||
- Returns `None` if the value cannot be parsed.
|
||||
|
||||
Callers decide whether to raise, fallback, etc.
|
||||
"""
|
||||
if value in (None, ""):
|
||||
return None
|
||||
if isinstance(value, nepalidate):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return nepalidate.strptime(value.strip(), BS_DATE_FORMAT)
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def try_parse_nepali_datetime(value: Any) -> Optional[nepalidatetime]:
|
||||
"""
|
||||
Best-effort conversion to `nepalidatetime`.
|
||||
|
||||
- Returns `None` for empty values.
|
||||
- Returns `nepalidatetime` for valid inputs.
|
||||
- Returns `None` if the value cannot be parsed.
|
||||
|
||||
Callers decide whether to raise, fallback, etc.
|
||||
"""
|
||||
if value in (None, ""):
|
||||
return None
|
||||
if isinstance(value, nepalidatetime):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return nepalidatetime.strptime(value.strip(), BS_DATETIME_FORMAT)
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def get_districts_by_province(province_name):
|
||||
"""
|
||||
Returns a list of districts in the given province.
|
||||
"""
|
||||
from nepali.locations import provinces
|
||||
|
||||
selected_province = next((p for p in provinces if p.name == province_name), None)
|
||||
if not selected_province:
|
||||
return []
|
||||
return [{"id": d.name, "text": d.name} for d in selected_province.districts]
|
||||
|
||||
|
||||
def get_municipalities_by_district(district_name):
|
||||
"""
|
||||
Returns a list of municipalities in the given district.
|
||||
"""
|
||||
from nepali.locations import districts
|
||||
|
||||
selected_district = next((d for d in districts if d.name == district_name), None)
|
||||
if not selected_district:
|
||||
return []
|
||||
|
||||
+24
-58
@@ -2,82 +2,48 @@ from django import forms
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# Utility mixin
|
||||
# --------------------------------------------------
|
||||
|
||||
class ClassAttrMixin:
|
||||
def _append_css_class(attrs, class_name: str):
|
||||
"""
|
||||
Safely append CSS classes to widget attrs.
|
||||
Django-idiomatic helper to append a CSS class without clobbering existing ones.
|
||||
"""
|
||||
|
||||
css_class = ""
|
||||
|
||||
def _build_attrs(self, attrs):
|
||||
attrs = attrs or {}
|
||||
classes = attrs.get("class", "").split()
|
||||
if self.css_class:
|
||||
classes.append(self.css_class)
|
||||
attrs["class"] = " ".join(dict.fromkeys(classes))
|
||||
return attrs
|
||||
existing = (attrs.get("class") or "").strip()
|
||||
attrs["class"] = (f"{existing} {class_name}").strip() if existing else class_name
|
||||
return attrs
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# Chained Select Base
|
||||
# --------------------------------------------------
|
||||
|
||||
class ChainedSelectWidget(ClassAttrMixin, forms.Select):
|
||||
"""
|
||||
Base widget for chained location selects.
|
||||
"""
|
||||
|
||||
class ChainedSelectWidget(forms.Select):
|
||||
class Media:
|
||||
js = ("django_nepkit/js/address-chaining.js",)
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# Province / District / Municipality Widgets
|
||||
# --------------------------------------------------
|
||||
|
||||
class ProvinceSelectWidget(ChainedSelectWidget):
|
||||
css_class = "nepkit-province-select"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs["attrs"] = self._build_attrs(kwargs.get("attrs"))
|
||||
attrs = kwargs.get("attrs", {})
|
||||
_append_css_class(attrs, "nepkit-province-select")
|
||||
kwargs["attrs"] = attrs
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class DistrictSelectWidget(ChainedSelectWidget):
|
||||
css_class = "nepkit-district-select"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
attrs = self._build_attrs(kwargs.get("attrs"))
|
||||
attrs.setdefault("data-url", reverse_lazy("django_nepkit:district-list"))
|
||||
attrs = kwargs.get("attrs", {})
|
||||
_append_css_class(attrs, "nepkit-district-select")
|
||||
attrs["data-url"] = reverse_lazy("django_nepkit:district-list")
|
||||
kwargs["attrs"] = attrs
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class MunicipalitySelectWidget(ChainedSelectWidget):
|
||||
css_class = "nepkit-municipality-select"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
attrs = self._build_attrs(kwargs.get("attrs"))
|
||||
attrs.setdefault("data-url", reverse_lazy("django_nepkit:municipality-list"))
|
||||
attrs = kwargs.get("attrs", {})
|
||||
_append_css_class(attrs, "nepkit-municipality-select")
|
||||
attrs["data-url"] = reverse_lazy("django_nepkit:municipality-list")
|
||||
kwargs["attrs"] = attrs
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# Nepali Date Picker
|
||||
# --------------------------------------------------
|
||||
|
||||
class NepaliDatePickerWidget(ClassAttrMixin, forms.TextInput):
|
||||
"""
|
||||
Nepali BS date picker widget.
|
||||
"""
|
||||
|
||||
class NepaliDatePickerWidget(forms.TextInput):
|
||||
input_type = "text"
|
||||
css_class = "nepkit-datepicker"
|
||||
|
||||
class Media:
|
||||
css = {
|
||||
@@ -93,14 +59,14 @@ class NepaliDatePickerWidget(ClassAttrMixin, forms.TextInput):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
attrs = kwargs.get("attrs", {})
|
||||
# Ensure we don't have vDateField class which triggers Django admin calendar
|
||||
classes = attrs.get("class", "")
|
||||
if "vDateField" in classes:
|
||||
classes = classes.replace("vDateField", "")
|
||||
|
||||
# Remove Django admin default class
|
||||
classes = attrs.get("class", "").replace("vDateField", "").strip()
|
||||
attrs["class"] = classes
|
||||
|
||||
attrs = self._build_attrs(attrs)
|
||||
attrs.setdefault("autocomplete", "off")
|
||||
attrs.setdefault("placeholder", "YYYY-MM-DD")
|
||||
|
||||
attrs["class"] = (classes or "").strip()
|
||||
_append_css_class(attrs, "nepkit-datepicker")
|
||||
attrs["autocomplete"] = "off"
|
||||
attrs["placeholder"] = "YYYY-MM-DD"
|
||||
kwargs["attrs"] = attrs
|
||||
super().__init__(*args, **kwargs)
|
||||
Whitespace-only changes.
@@ -0,0 +1,42 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from django_nepkit import NepaliModelAdmin
|
||||
|
||||
from .models import Person
|
||||
|
||||
|
||||
@admin.register(Person)
|
||||
class PersonAdmin(NepaliModelAdmin):
|
||||
list_display = (
|
||||
"name",
|
||||
"display_birth_date",
|
||||
"phone_number",
|
||||
"province",
|
||||
"district",
|
||||
"municipality",
|
||||
"display_created_at",
|
||||
"display_updated_at",
|
||||
)
|
||||
list_filter = ("birth_date", "province", "district")
|
||||
search_fields = ("name", "phone_number")
|
||||
|
||||
def display_birth_date(self, obj):
|
||||
"""Display birth date with Nepali month names"""
|
||||
return self.format_nepali_date(obj.birth_date)
|
||||
|
||||
display_birth_date.short_description = "Birth Date"
|
||||
display_birth_date.admin_order_field = "birth_date"
|
||||
|
||||
def display_created_at(self, obj):
|
||||
"""Display created date with Nepali month names"""
|
||||
return self.format_nepali_datetime(obj.created_at)
|
||||
|
||||
display_created_at.short_description = "Created At"
|
||||
display_created_at.admin_order_field = "created_at"
|
||||
|
||||
def display_updated_at(self, obj):
|
||||
"""Display updated date with Nepali month names"""
|
||||
return self.format_nepali_datetime(obj.updated_at)
|
||||
|
||||
display_updated_at.short_description = "Updated At"
|
||||
display_updated_at.admin_order_field = "updated_at"
|
||||
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class DemoConfig(AppConfig):
|
||||
name = "demo"
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 6.0.1 on 2026-01-28 15:37
|
||||
|
||||
import django_nepkit.models
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("demo", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="person",
|
||||
name="phone_number",
|
||||
field=django_nepkit.models.NepaliPhoneNumberField(max_length=10),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,27 @@
|
||||
# Generated by Django 6.0.1 on 2026-01-28 15:38
|
||||
|
||||
import django.utils.timezone
|
||||
import django_nepkit.models
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("demo", "0002_alter_person_phone_number"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="person",
|
||||
name="created_at",
|
||||
field=django_nepkit.models.NepaliTimeField(
|
||||
auto_now_add=True, default=django.utils.timezone.now
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="person",
|
||||
name="updated_at",
|
||||
field=django_nepkit.models.NepaliTimeField(auto_now=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 6.0.1 on 2026-01-28 15:39
|
||||
|
||||
import django_nepkit.models
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("demo", "0003_person_created_at_person_updated_at"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="person",
|
||||
name="created_at",
|
||||
field=django_nepkit.models.NepaliDateField(auto_now_add=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="person",
|
||||
name="updated_at",
|
||||
field=django_nepkit.models.NepaliDateField(auto_now=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,27 @@
|
||||
# Generated by Django 6.0.1 on 2026-01-28 16:55
|
||||
|
||||
import django_nepkit.models
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("demo", "0004_alter_person_created_at_alter_person_updated_at"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="person",
|
||||
name="created_at",
|
||||
field=django_nepkit.models.NepaliDateTimeField(
|
||||
auto_now_add=True, blank=True, editable=False, max_length=19
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="person",
|
||||
name="updated_at",
|
||||
field=django_nepkit.models.NepaliDateTimeField(
|
||||
auto_now=True, blank=True, editable=False, max_length=19
|
||||
),
|
||||
),
|
||||
]
|
||||
Whitespace-only changes.
@@ -0,0 +1,27 @@
|
||||
from django.db import models
|
||||
from django_nepkit import (
|
||||
NepaliDateField,
|
||||
NepaliTimeField,
|
||||
NepaliPhoneNumberField,
|
||||
NepaliDateTimeField,
|
||||
ProvinceField,
|
||||
DistrictField,
|
||||
MunicipalityField,
|
||||
)
|
||||
|
||||
|
||||
class Person(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
birth_date = NepaliDateField()
|
||||
registration_time = NepaliTimeField(auto_now_add=True)
|
||||
phone_number = NepaliPhoneNumberField()
|
||||
|
||||
# Address chaining
|
||||
province = ProvinceField()
|
||||
district = DistrictField()
|
||||
municipality = MunicipalityField()
|
||||
created_at = NepaliDateTimeField(auto_now_add=True)
|
||||
updated_at = NepaliDateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Nepali Fields Demo - Add</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; margin: 20px; }
|
||||
.form-group { margin-bottom: 15px; }
|
||||
label { display: block; margin-bottom: 5px; }
|
||||
input, select { width: 300px; padding: 8px; }
|
||||
</style>
|
||||
{{ form.media }}
|
||||
</head>
|
||||
<body>
|
||||
<h1>Add New Person</h1>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% for field in form %}
|
||||
<div class="form-group">
|
||||
{{ field.label_tag }}
|
||||
{{ field }}
|
||||
{% if field.help_text %}
|
||||
<small style="color: grey">{{ field.help_text }}</small>
|
||||
{% endif %}
|
||||
{% for error in field.errors %}
|
||||
<p style="color: red">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
<br>
|
||||
<a href="{% url 'demo:person-list' %}">Back to List</a>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,48 @@
|
||||
{% load nepali %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Nepali Fields Demo - List</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; margin: 20px; }
|
||||
table { border-collapse: collapse; width: 100%; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
||||
th { background-color: #f2f2f2; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Person List</h1>
|
||||
<a href="{% url 'demo:person-create' %}">Add New Person</a>
|
||||
<hr>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Birth Date (BS)</th>
|
||||
<th>Birth Date (Devanagari)</th>
|
||||
<th>Phone</th>
|
||||
<th>Province</th>
|
||||
<th>District</th>
|
||||
<th>Municipality</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for person in persons %}
|
||||
<tr>
|
||||
<td>{{ person.name }}</td>
|
||||
<td>{{ person.birth_date|nepali_date }}</td>
|
||||
<td>{{ person.birth_date|nepali_date_ne }}</td>
|
||||
<td>{{ person.phone_number }}</td>
|
||||
<td>{{ person.province }}</td>
|
||||
<td>{{ person.district }}</td>
|
||||
<td>{{ person.municipality }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="7">No records found.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
# Create your tests here.
|
||||
@@ -0,0 +1,9 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = "demo"
|
||||
|
||||
urlpatterns = [
|
||||
path("", views.person_list, name="person-list"),
|
||||
path("add/", views.person_create, name="person-create"),
|
||||
]
|
||||
@@ -0,0 +1,32 @@
|
||||
from django.shortcuts import render, redirect
|
||||
from .models import Person
|
||||
from django import forms
|
||||
|
||||
|
||||
class PersonForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Person
|
||||
fields = [
|
||||
"name",
|
||||
"birth_date",
|
||||
"phone_number",
|
||||
"province",
|
||||
"district",
|
||||
"municipality",
|
||||
]
|
||||
|
||||
|
||||
def person_list(request):
|
||||
persons = Person.objects.all()
|
||||
return render(request, "demo/person_list.html", {"persons": persons})
|
||||
|
||||
|
||||
def person_create(request):
|
||||
if request.method == "POST":
|
||||
form = PersonForm(request.POST)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return redirect("demo:person-list")
|
||||
else:
|
||||
form = PersonForm()
|
||||
return render(request, "demo/person_form.html", {"form": form})
|
||||
Whitespace-only changes.
@@ -0,0 +1,76 @@
|
||||
from pathlib import Path
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
SECRET_KEY = "django-insecure-#=02g9%#k0t!(0968j4f#=+u%l-ws3nfti*0n601@n_zo(&x%w"
|
||||
DEBUG = True
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"django_nepkit",
|
||||
"demo",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "example_project.urls"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = "example_project.wsgi.application"
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": BASE_DIR / "db.sqlite3",
|
||||
}
|
||||
}
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||
},
|
||||
]
|
||||
|
||||
LANGUAGE_CODE = "en-us"
|
||||
TIME_ZONE = "Asia/Kathmandu"
|
||||
USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
STATIC_URL = "static/"
|
||||
@@ -0,0 +1,8 @@
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("nepkit/", include("django_nepkit.urls")),
|
||||
path("", include("demo.urls")),
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_project.settings")
|
||||
|
||||
application = get_wsgi_application()
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_project.settings")
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+5
-1
@@ -52,6 +52,11 @@ classifiers = [
|
||||
"Natural Language :: English"
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
drf = [
|
||||
"djangorestframework>=3.14",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/S4NKALP/django-nepkit"
|
||||
|
||||
@@ -62,4 +67,3 @@ dev = [
|
||||
"pre-commit>=4.5.1",
|
||||
"ruff>=0.14.14",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
setup(
|
||||
name="django-nepkit",
|
||||
version="0.1.0",
|
||||
include_package_data=True,
|
||||
author="Sankalp Tharu",
|
||||
author_email="sankalptharu50028@gmail.com",
|
||||
description="Django Nepali date, time, datetime, phone, and address fields with helpers.",
|
||||
long_description=open("README.md").read(),
|
||||
long_description_content_type="text/markdown",
|
||||
url="https://github.com/S4NKALP/django-nepkit",
|
||||
classifiers=[
|
||||
"Framework :: Django",
|
||||
"Framework :: Django :: 4.2",
|
||||
"Framework :: Django :: 5.0",
|
||||
"Framework :: Django :: 6.0",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Intended Audience :: Developers",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
"Topic :: Utilities",
|
||||
"Operating System :: OS Independent",
|
||||
"Natural Language :: English",
|
||||
],
|
||||
python_requires=">=3.11",
|
||||
packages=find_packages(exclude=("example*", "tests*")),
|
||||
install_requires=[
|
||||
"django>=4.2",
|
||||
"nepali>=1.1.3",
|
||||
],
|
||||
extras_require={
|
||||
"drf": ["djangorestframework>=3.14"],
|
||||
},
|
||||
)
|
||||
@@ -77,6 +77,11 @@ dependencies = [
|
||||
{ name = "nepali" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
drf = [
|
||||
{ name = "djangorestframework" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "pre-commit" },
|
||||
@@ -86,8 +91,10 @@ dev = [
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "django", specifier = ">=4.2" },
|
||||
{ name = "djangorestframework", marker = "extra == 'drf'", specifier = ">=3.14" },
|
||||
{ name = "nepali", specifier = ">=1.1.3" },
|
||||
]
|
||||
provides-extras = ["drf"]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
@@ -95,6 +102,19 @@ dev = [
|
||||
{ name = "ruff", specifier = ">=0.14.14" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "djangorestframework"
|
||||
version = "3.16.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "django", version = "5.2.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
|
||||
{ name = "django", version = "6.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8a/95/5376fe618646fde6899b3cdc85fd959716bb67542e273a76a80d9f326f27/djangorestframework-3.16.1.tar.gz", hash = "sha256:166809528b1aced0a17dc66c24492af18049f2c9420dbd0be29422029cfc3ff7", size = 1089735, upload-time = "2025-08-06T17:50:53.251Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/ce/bf8b9d3f415be4ac5588545b5fcdbbb841977db1c1d923f7568eeabe1689/djangorestframework-3.16.1-py3-none-any.whl", hash = "sha256:33a59f47fb9c85ede792cbf88bde71893bcda0667bc573f784649521f1102cec", size = 1080442, upload-time = "2025-08-06T17:50:50.667Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.20.3"
|
||||
|
||||
Reference in new issue
Block a user