feat(model): added nepalidatefield with calendar picker
This commit is contained in:
6 files changed
+414
No files matched your search
@@ -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
|
||||
@@ -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",
|
||||
)
|
||||
@@ -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);
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user