use dry principle possible and added example project

This commit is contained in:
sankalp committed 2026-01-29 01:37:41 +05:45
1 parent 2bb6458c8b
commit 1a1dc3bee4
46 files changed
+3521 -772

No files matched your search

+26
View File
@@ -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
+27
View File
@@ -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
View File
@@ -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/
+17
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
include LICENSE
include README.md
recursive-include django_nepkit/static *
+13
View File
@@ -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]"
```
-155
View File
@@ -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
+31
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+57 -13
View File
@@ -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);
-100
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
+74
View File
@@ -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
View File
@@ -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
View File
@@ -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)
View File
Whitespace-only changes.
+42
View File
@@ -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"
+5
View File
@@ -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
),
),
]
View File
Whitespace-only changes.
+27
View File
@@ -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>
+1
View File
@@ -0,0 +1 @@
# Create your tests here.
+9
View File
@@ -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"),
]
+32
View File
@@ -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})
View File
Whitespace-only changes.
+76
View File
@@ -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/"
+8
View File
@@ -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")),
]
+7
View File
@@ -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()
+24
View File
@@ -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
View File
@@ -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",
]
+39
View File
@@ -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"],
},
)
Generated
+20
View File
@@ -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"