django-nepkit 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. django_nepkit/__init__.py +31 -0
  2. django_nepkit/admin.py +211 -0
  3. django_nepkit/forms.py +50 -0
  4. django_nepkit/models.py +269 -0
  5. django_nepkit/serializers.py +113 -0
  6. django_nepkit/static/django_nepkit/css/admin-nepali-datepicker.css +37 -0
  7. django_nepkit/static/django_nepkit/js/address-chaining.js +64 -0
  8. django_nepkit/static/django_nepkit/js/admin-jquery-bridge.js +10 -0
  9. django_nepkit/static/django_nepkit/js/nepali-datepicker-init.js +108 -0
  10. django_nepkit/templatetags/__init__.py +0 -0
  11. django_nepkit/templatetags/nepali.py +74 -0
  12. django_nepkit/urls.py +10 -0
  13. django_nepkit/utils.py +77 -0
  14. django_nepkit/validators.py +12 -0
  15. django_nepkit/views.py +22 -0
  16. django_nepkit/widgets.py +72 -0
  17. django_nepkit-0.1.0.dist-info/METADATA +377 -0
  18. django_nepkit-0.1.0.dist-info/RECORD +39 -0
  19. django_nepkit-0.1.0.dist-info/WHEEL +5 -0
  20. django_nepkit-0.1.0.dist-info/licenses/LICENSE +21 -0
  21. django_nepkit-0.1.0.dist-info/top_level.txt +2 -0
  22. example/demo/__init__.py +0 -0
  23. example/demo/admin.py +42 -0
  24. example/demo/apps.py +5 -0
  25. example/demo/migrations/0001_initial.py +2113 -0
  26. example/demo/migrations/0002_alter_person_phone_number.py +18 -0
  27. example/demo/migrations/0003_person_created_at_person_updated_at.py +27 -0
  28. example/demo/migrations/0004_alter_person_created_at_alter_person_updated_at.py +23 -0
  29. example/demo/migrations/0005_alter_person_created_at_alter_person_updated_at.py +27 -0
  30. example/demo/migrations/__init__.py +0 -0
  31. example/demo/models.py +27 -0
  32. example/demo/tests.py +1 -0
  33. example/demo/urls.py +9 -0
  34. example/demo/views.py +32 -0
  35. example/example_project/__init__.py +0 -0
  36. example/example_project/settings.py +76 -0
  37. example/example_project/urls.py +8 -0
  38. example/example_project/wsgi.py +7 -0
  39. example/manage.py +24 -0
@@ -0,0 +1,37 @@
1
+ .ndp-container.ndp-dark{
2
+ background:#1f2937; /* slate-800-ish */
3
+ color:#e5e7eb;
4
+ border:1px solid rgba(255,255,255,.12);
5
+ box-shadow:0 10px 25px rgba(0,0,0,.45);
6
+ }
7
+
8
+ .ndp-container.ndp-dark .ndp-header{
9
+ border-bottom:1px solid rgba(255,255,255,.12);
10
+ }
11
+
12
+ .ndp-container.ndp-dark .ndp-header .ndp-header-link{
13
+ color:#e5e7eb;
14
+ }
15
+
16
+ .ndp-container.ndp-dark .ndc-nav-button:hover,
17
+ .ndp-container.ndp-dark .ndp-header-link:hover,
18
+ .ndp-container.ndp-dark .ndp-table td:hover{
19
+ background:rgba(255,255,255,.08);
20
+ }
21
+
22
+ .ndp-container.ndp-dark .ndp-table th{
23
+ color:rgba(229,231,235,.75);
24
+ }
25
+
26
+ .ndp-container.ndp-dark .ndp-table td{
27
+ color:#e5e7eb;
28
+ }
29
+
30
+ .ndp-container.ndp-dark .ndp-table td.ndp-selected{
31
+ background:#2563eb; /* blue-600 */
32
+ color:#fff;
33
+ }
34
+
35
+ .ndp-container.ndp-dark .ndp-table td.ndp-today{
36
+ color:#93c5fd; /* blue-300 */
37
+ }
@@ -0,0 +1,64 @@
1
+ (function() {
2
+ 'use strict';
3
+
4
+ function updateOptions(selectElement, data, placeholder) {
5
+ selectElement.innerHTML = '';
6
+ if (placeholder) {
7
+ const opt = document.createElement('option');
8
+ opt.value = '';
9
+ opt.textContent = placeholder;
10
+ selectElement.appendChild(opt);
11
+ }
12
+ data.forEach(item => {
13
+ const opt = document.createElement('option');
14
+ opt.value = item.id;
15
+ opt.textContent = item.text;
16
+ selectElement.appendChild(opt);
17
+ });
18
+ selectElement.dispatchEvent(new Event('change'));
19
+ }
20
+
21
+ document.addEventListener('change', function(e) {
22
+ if (e.target.matches('.nepkit-province-select')) {
23
+ const province = e.target.value;
24
+ const container = e.target.closest('form') || document;
25
+ const districtSelect = container.querySelector('.nepkit-district-select');
26
+ const municipalitySelect = container.querySelector('.nepkit-municipality-select');
27
+
28
+ if (districtSelect) {
29
+ if (!province) {
30
+ updateOptions(districtSelect, [], 'Select District');
31
+ if (municipalitySelect) updateOptions(municipalitySelect, [], 'Select Municipality');
32
+ return;
33
+ }
34
+
35
+ const url = districtSelect.dataset.url + '?province=' + encodeURIComponent(province);
36
+ fetch(url)
37
+ .then(response => response.json())
38
+ .then(data => {
39
+ updateOptions(districtSelect, data, 'Select District');
40
+ });
41
+ }
42
+ }
43
+
44
+ if (e.target.matches('.nepkit-district-select')) {
45
+ const district = e.target.value;
46
+ const container = e.target.closest('form') || document;
47
+ const municipalitySelect = container.querySelector('.nepkit-municipality-select');
48
+
49
+ if (municipalitySelect) {
50
+ if (!district) {
51
+ updateOptions(municipalitySelect, [], 'Select Municipality');
52
+ return;
53
+ }
54
+
55
+ const url = municipalitySelect.dataset.url + '?district=' + encodeURIComponent(district);
56
+ fetch(url)
57
+ .then(response => response.json())
58
+ .then(data => {
59
+ updateOptions(municipalitySelect, data, 'Select Municipality');
60
+ });
61
+ }
62
+ }
63
+ });
64
+ })();
@@ -0,0 +1,10 @@
1
+ (function () {
2
+ "use strict";
3
+
4
+ // Django admin exposes jQuery as `django.jQuery`.
5
+ // Many third-party plugins expect `window.jQuery`/`window.$`.
6
+ if (typeof window !== "undefined" && window.django && window.django.jQuery) {
7
+ window.jQuery = window.django.jQuery;
8
+ window.$ = window.django.jQuery;
9
+ }
10
+ })();
@@ -0,0 +1,108 @@
1
+ (function () {
2
+ 'use strict';
3
+
4
+ function isDarkTheme() {
5
+ var docEl = document.documentElement;
6
+ var body = document.body;
7
+
8
+ // Common theme markers used by Django admin themes / dark-mode packages
9
+ var markers = [
10
+ docEl && docEl.getAttribute('data-theme'),
11
+ docEl && docEl.getAttribute('data-color-scheme'),
12
+ body && body.getAttribute('data-theme'),
13
+ body && body.getAttribute('data-color-scheme')
14
+ ].filter(Boolean).join(' ').toLowerCase();
15
+
16
+ var classNames = [
17
+ docEl && docEl.className,
18
+ body && body.className
19
+ ].filter(Boolean).join(' ').toLowerCase();
20
+
21
+ if (markers.indexOf('dark') !== -1 || classNames.indexOf('dark') !== -1) return true;
22
+
23
+ // Fallback to OS preference
24
+ try {
25
+ return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
26
+ } catch (e) {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ function applyThemeToDatepickerContainers() {
32
+ var dark = isDarkTheme();
33
+ var containers = document.querySelectorAll('.ndp-container');
34
+ if (!containers || !containers.length) return;
35
+
36
+ containers.forEach(function (c) {
37
+ c.classList.remove('ndp-dark');
38
+ c.classList.remove('ndp-light');
39
+ c.classList.add(dark ? 'ndp-dark' : 'ndp-light');
40
+ });
41
+ }
42
+
43
+ function initNepaliDatePickers() {
44
+ var inputs = document.querySelectorAll('.nepkit-datepicker:not(.nepali-datepicker-initialized)');
45
+ if (!inputs || !inputs.length) return;
46
+
47
+ inputs.forEach(function (el) {
48
+ // Nepali Datepicker v5 exposes `element.NepaliDatePicker(options)`
49
+ if (typeof el.NepaliDatePicker === 'function') {
50
+ el.NepaliDatePicker({
51
+ dateFormat: 'YYYY-MM-DD'
52
+ });
53
+ el.classList.add('nepali-datepicker-initialized');
54
+
55
+ // Ensure theme is applied when the picker is actually shown.
56
+ // (The plugin often creates/inserts DOM on focus.)
57
+ var applySoon = function () {
58
+ // Run twice: once immediately, once after paint.
59
+ applyThemeToDatepickerContainers();
60
+ window.setTimeout(applyThemeToDatepickerContainers, 0);
61
+ };
62
+
63
+ el.addEventListener('focus', applySoon);
64
+ el.addEventListener('click', applySoon);
65
+ }
66
+ });
67
+ }
68
+
69
+ // Initialize on page load
70
+ if (document.readyState === 'loading') {
71
+ document.addEventListener('DOMContentLoaded', initNepaliDatePickers);
72
+ } else {
73
+ initNepaliDatePickers();
74
+ }
75
+
76
+ // Keep in sync if system theme changes (fallback path)
77
+ if (window.matchMedia) {
78
+ try {
79
+ var mql = window.matchMedia('(prefers-color-scheme: dark)');
80
+ if (mql && typeof mql.addEventListener === 'function') {
81
+ mql.addEventListener('change', applyThemeToDatepickerContainers);
82
+ } else if (mql && typeof mql.addListener === 'function') {
83
+ mql.addListener(applyThemeToDatepickerContainers);
84
+ }
85
+ } catch (e) {
86
+ // ignore
87
+ }
88
+ }
89
+
90
+ // Re-initialize when Django admin adds inlines dynamically
91
+ if (typeof django !== 'undefined' && django.jQuery) {
92
+ django.jQuery(document).on('formset:added', function () {
93
+ initNepaliDatePickers();
94
+ });
95
+ }
96
+
97
+ // Also listen for DOM changes (admin popups/other dynamic content)
98
+ if (typeof MutationObserver !== 'undefined') {
99
+ var observer = new MutationObserver(function () {
100
+ initNepaliDatePickers();
101
+ applyThemeToDatepickerContainers();
102
+ });
103
+ observer.observe(document.body, {
104
+ childList: true,
105
+ subtree: true
106
+ });
107
+ }
108
+ })();
File without changes
@@ -0,0 +1,74 @@
1
+ from django import template
2
+ from nepali.datetime import nepalidate, nepalihumanize
3
+ from nepali.number import nepalinumber
4
+ import datetime
5
+
6
+ register = template.Library()
7
+
8
+
9
+ def _coerce_ad_date_to_bs(value):
10
+ """
11
+ Internal helper: if value is AD date/datetime, convert to `nepalidate`.
12
+ Does NOT attempt to parse strings (keeps template behavior minimal).
13
+ """
14
+ if isinstance(value, (datetime.datetime, datetime.date)):
15
+ return nepalidate.from_date(value)
16
+ return value
17
+
18
+
19
+ @register.filter
20
+ def nepali_date(value, format_str="%Y-%m-%d"):
21
+ """
22
+ Formats a date or datetime object into a Nepali date string.
23
+ """
24
+ if value is None:
25
+ return ""
26
+
27
+ value = _coerce_ad_date_to_bs(value)
28
+
29
+ if hasattr(value, "strftime"):
30
+ return value.strftime(format_str)
31
+ return value
32
+
33
+
34
+ @register.filter
35
+ def nepali_date_ne(value, format_str="%Y-%m-%d"):
36
+ """
37
+ Formats a date or datetime object into a Nepali date string (Devanagari).
38
+ """
39
+ if value is None:
40
+ return ""
41
+
42
+ value = _coerce_ad_date_to_bs(value)
43
+
44
+ if hasattr(value, "strftime_ne"):
45
+ return value.strftime_ne(format_str)
46
+ return value
47
+
48
+
49
+ @register.filter
50
+ def nepali_number(value):
51
+ """
52
+ Converts a number to Devanagari.
53
+ """
54
+ if value is None:
55
+ return ""
56
+ return nepalinumber(value).str_ne()
57
+
58
+
59
+ @register.filter
60
+ def nepali_humanize(value, threshold=None, format_str=None):
61
+ """
62
+ Returns a human-readable "time ago" string in Nepali.
63
+ """
64
+ if value is None:
65
+ return ""
66
+
67
+ # Threshold and format_str are optional
68
+ kwargs = {}
69
+ if threshold:
70
+ kwargs["threshold"] = threshold
71
+ if format_str:
72
+ kwargs["format"] = format_str
73
+
74
+ return nepalihumanize(value, **kwargs)
django_nepkit/urls.py ADDED
@@ -0,0 +1,10 @@
1
+ from django.urls import path
2
+
3
+ from django_nepkit.views import district_list_view, municipality_list_view
4
+
5
+ app_name = "django_nepkit"
6
+
7
+ urlpatterns = [
8
+ path("districts/", district_list_view, name="district-list"),
9
+ path("municipalities/", municipality_list_view, name="municipality-list"),
10
+ ]
django_nepkit/utils.py ADDED
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Optional
4
+
5
+ from nepali.datetime import nepalidate, nepalidatetime
6
+
7
+
8
+ BS_DATE_FORMAT = "%Y-%m-%d"
9
+ BS_DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
10
+
11
+
12
+ def try_parse_nepali_date(value: Any) -> Optional[nepalidate]:
13
+ """
14
+ Best-effort conversion to `nepalidate`.
15
+
16
+ - Returns `None` for empty values.
17
+ - Returns `nepalidate` for valid inputs.
18
+ - Returns `None` if the value cannot be parsed.
19
+
20
+ Callers decide whether to raise, fallback, etc.
21
+ """
22
+ if value in (None, ""):
23
+ return None
24
+ if isinstance(value, nepalidate):
25
+ return value
26
+ if isinstance(value, str):
27
+ try:
28
+ return nepalidate.strptime(value.strip(), BS_DATE_FORMAT)
29
+ except Exception:
30
+ return None
31
+ return None
32
+
33
+
34
+ def try_parse_nepali_datetime(value: Any) -> Optional[nepalidatetime]:
35
+ """
36
+ Best-effort conversion to `nepalidatetime`.
37
+
38
+ - Returns `None` for empty values.
39
+ - Returns `nepalidatetime` for valid inputs.
40
+ - Returns `None` if the value cannot be parsed.
41
+
42
+ Callers decide whether to raise, fallback, etc.
43
+ """
44
+ if value in (None, ""):
45
+ return None
46
+ if isinstance(value, nepalidatetime):
47
+ return value
48
+ if isinstance(value, str):
49
+ try:
50
+ return nepalidatetime.strptime(value.strip(), BS_DATETIME_FORMAT)
51
+ except Exception:
52
+ return None
53
+ return None
54
+
55
+
56
+ def get_districts_by_province(province_name):
57
+ """
58
+ Returns a list of districts in the given province.
59
+ """
60
+ from nepali.locations import provinces
61
+
62
+ selected_province = next((p for p in provinces if p.name == province_name), None)
63
+ if not selected_province:
64
+ return []
65
+ return [{"id": d.name, "text": d.name} for d in selected_province.districts]
66
+
67
+
68
+ def get_municipalities_by_district(district_name):
69
+ """
70
+ Returns a list of municipalities in the given district.
71
+ """
72
+ from nepali.locations import districts
73
+
74
+ selected_district = next((d for d in districts if d.name == district_name), None)
75
+ if not selected_district:
76
+ return []
77
+ return [{"id": m.name, "text": m.name} for m in selected_district.municipalities]
@@ -0,0 +1,12 @@
1
+ from django.core.exceptions import ValidationError
2
+ from django.utils.translation import gettext_lazy as _
3
+ from nepali import phone_number
4
+
5
+
6
+ # validates the given value is a valid nepali phone number
7
+ def validate_nepali_phone_number(value):
8
+ if not phone_number.is_valid(value):
9
+ raise ValidationError(
10
+ _("%(value)s is not a valid nepali phone number"),
11
+ params={"value": value},
12
+ )
django_nepkit/views.py ADDED
@@ -0,0 +1,22 @@
1
+ from django.http import JsonResponse
2
+
3
+ from django_nepkit.utils import (
4
+ get_districts_by_province,
5
+ get_municipalities_by_district,
6
+ )
7
+
8
+
9
+ def district_list_view(request):
10
+ province = request.GET.get("province")
11
+ if not province:
12
+ return JsonResponse([], safe=False)
13
+ data = get_districts_by_province(province)
14
+ return JsonResponse(data, safe=False)
15
+
16
+
17
+ def municipality_list_view(request):
18
+ district = request.GET.get("district")
19
+ if not district:
20
+ return JsonResponse([], safe=False)
21
+ data = get_municipalities_by_district(district)
22
+ return JsonResponse(data, safe=False)
@@ -0,0 +1,72 @@
1
+ from django import forms
2
+ from django.urls import reverse_lazy
3
+
4
+
5
+ def _append_css_class(attrs, class_name: str):
6
+ """
7
+ Django-idiomatic helper to append a CSS class without clobbering existing ones.
8
+ """
9
+ existing = (attrs.get("class") or "").strip()
10
+ attrs["class"] = (f"{existing} {class_name}").strip() if existing else class_name
11
+ return attrs
12
+
13
+
14
+ class ChainedSelectWidget(forms.Select):
15
+ class Media:
16
+ js = ("django_nepkit/js/address-chaining.js",)
17
+
18
+
19
+ class ProvinceSelectWidget(ChainedSelectWidget):
20
+ def __init__(self, *args, **kwargs):
21
+ attrs = kwargs.get("attrs", {})
22
+ _append_css_class(attrs, "nepkit-province-select")
23
+ kwargs["attrs"] = attrs
24
+ super().__init__(*args, **kwargs)
25
+
26
+
27
+ class DistrictSelectWidget(ChainedSelectWidget):
28
+ def __init__(self, *args, **kwargs):
29
+ attrs = kwargs.get("attrs", {})
30
+ _append_css_class(attrs, "nepkit-district-select")
31
+ attrs["data-url"] = reverse_lazy("django_nepkit:district-list")
32
+ kwargs["attrs"] = attrs
33
+ super().__init__(*args, **kwargs)
34
+
35
+
36
+ class MunicipalitySelectWidget(ChainedSelectWidget):
37
+ def __init__(self, *args, **kwargs):
38
+ attrs = kwargs.get("attrs", {})
39
+ _append_css_class(attrs, "nepkit-municipality-select")
40
+ attrs["data-url"] = reverse_lazy("django_nepkit:municipality-list")
41
+ kwargs["attrs"] = attrs
42
+ super().__init__(*args, **kwargs)
43
+
44
+
45
+ class NepaliDatePickerWidget(forms.TextInput):
46
+ input_type = "text"
47
+
48
+ class Media:
49
+ css = {
50
+ "all": (
51
+ "https://unpkg.com/nepali-date-picker@2.0.2/dist/nepaliDatePicker.min.css",
52
+ )
53
+ }
54
+ js = (
55
+ "https://code.jquery.com/jquery-3.5.1.slim.min.js",
56
+ "https://unpkg.com/nepali-date-picker@2.0.2/dist/nepaliDatePicker.min.js",
57
+ "django_nepkit/js/nepali-datepicker-init.js",
58
+ )
59
+
60
+ def __init__(self, *args, **kwargs):
61
+ attrs = kwargs.get("attrs", {})
62
+ # Ensure we don't have vDateField class which triggers Django admin calendar
63
+ classes = attrs.get("class", "")
64
+ if "vDateField" in classes:
65
+ classes = classes.replace("vDateField", "")
66
+
67
+ attrs["class"] = (classes or "").strip()
68
+ _append_css_class(attrs, "nepkit-datepicker")
69
+ attrs["autocomplete"] = "off"
70
+ attrs["placeholder"] = "YYYY-MM-DD"
71
+ kwargs["attrs"] = attrs
72
+ super().__init__(*args, **kwargs)