feat(model): added nepalidatefield with calendar picker

This commit is contained in:
sankalp committed 2026-01-29 00:19:27 +05:45
1 parent 67a213162d
commit 5ee2e8f32b
6 files changed
+414

No files matched your search

+155
View File
@@ -0,0 +1,155 @@
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
+28
View File
@@ -1,9 +1,37 @@
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.validators import validate_nepali_phone_number
from django_nepkit.widgets import NepaliDatePickerWidget
class NepaliPhoneNumberField(forms.CharField):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.validators.append(validate_nepali_phone_number)
class NepaliDateFormField(forms.DateField):
widget = NepaliDatePickerWidget
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:
return nepalidate.strptime(str(value).strip(), "%Y-%m-%d")
except Exception:
raise forms.ValidationError(
_("Enter a valid Nepali date in YYYY-MM-DD format."),
code="invalid",
)
+91
View File
@@ -1,11 +1,16 @@
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
from nepali.locations import districts, municipalities, provices
from django_nepkit.forms import NepaliDateFormField
from django_nepkit.validators import validate_nepali_phone_number
from django_nepkit.widgets import (
DistrictSelectWidget,
MunicipalitySelectWidget,
NepaliDatePickerWidget,
ProvinceSelectWidget,
)
@@ -63,3 +68,89 @@ class MunicipalityField(models.CharField):
class NepaliTimeField(models.TimeField):
description = _("Nepali Time")
class NepaliDateField(models.CharField):
description = _("Nepali Date (Bikram Sambat)")
def __init__(self, *args, **kwargs):
self.auto_now = kwargs.pop("auto_now", False)
self.auto_now_add = kwargs.pop("auto_now_add", False)
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 pre_save(self, model_instance, add):
if self.auto_now or (self.auto_now_add and add):
value = nepalidate.today().strftime("%Y-%m-%d")
setattr(model_instance, self.attname, value)
return value
return super().pre_save(model_instance, add)
def from_db_value(self, value, expression, connection):
if value is None:
return value
try:
return nepalidate.strptime(value, "%Y-%m-%d")
except (ValueError, TypeError):
return value
def to_python(self, value):
if value is None or isinstance(value, nepalidate):
return value
if isinstance(value, python_date):
try:
return nepalidate.from_date(value)
except (ValueError, TypeError):
return str(value)
if isinstance(value, str):
try:
return nepalidate.strptime(value.strip(), "%Y-%m-%d")
except (ValueError, TypeError):
return value
return super().to_python(value)
def validate(self, value, model_instance):
if isinstance(value, nepalidate):
value = value.strftime("%Y-%m-%d")
super().validate(value, model_instance)
def run_validators(self, value):
if isinstance(value, nepalidate):
value = value.strftime("%Y-%m-%d")
super().run_validators(value)
def get_prep_value(self, value):
if value is None:
return value
if isinstance(value, nepalidate):
return value.strftime("%Y-%m-%d")
if isinstance(value, python_date):
try:
return nepalidate.from_date(value).strftime("%Y-%m-%d")
except (ValueError, TypeError):
return str(value)
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):
defaults = {
"form_class": NepaliDateFormField,
"widget": NepaliDatePickerWidget,
}
defaults.update(kwargs)
return super().formfield(**defaults)
@@ -0,0 +1,45 @@
(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);
+67
View File
@@ -0,0 +1,67 @@
import datetime
from django import template
from nepali.datetime import nepalidate, nepalihumanize
from nepali.number import nepalinumber
register = template.Library()
@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 ""
if isinstance(value, (datetime.datetime, datetime.date)):
value = nepalidate.from_date(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 ""
if isinstance(value, (datetime.datetime, datetime.date)):
value = nepalidate.from_date(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)
+28
View File
@@ -31,3 +31,31 @@ class MunicipalitySelectWidget(ChainedSelectWidget):
attrs["data-url"] = reverse_lazy("django_nepkit:municipality-list")
kwargs["attrs"] = attrs
super().__init__(*args, **kwargs)
class NepaliDatePickerWidget(forms.TextInput):
input_type = "text"
class Media:
css = {
"all": (
"https://unpkg.com/nepali-date-picker@2.0.2/dist/nepaliDatePicker.min.css",
)
}
js = (
"https://code.jquery.com/jquery-3.5.1.slim.min.js",
"https://unpkg.com/nepali-date-picker@2.0.2/dist/nepaliDatePicker.min.js",
"django_nepkit/js/nepali-datepicker-init.js",
)
def __init__(self, *args, **kwargs):
attrs = kwargs.get("attrs", {})
classes = attrs.get("class", "")
if "vDateField" in classes:
classes = classes.replace("vDateField", "")
attrs["class"] = (classes + " nepkit-datepicker").strip()
attrs["autocomplete"] = "off"
attrs["placeholder"] = "YYYY-MM-DD"
kwargs["attrs"] = attrs
super().__init__(*args, **kwargs)