django-nepkit 0.1.0__py3-none-any.whl → 0.2.1__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 +20 -0
  2. django_nepkit/admin.py +243 -93
  3. django_nepkit/conf.py +34 -0
  4. django_nepkit/constants.py +129 -0
  5. django_nepkit/filters.py +113 -0
  6. django_nepkit/forms.py +9 -9
  7. django_nepkit/lang_utils.py +52 -0
  8. django_nepkit/models.py +138 -161
  9. django_nepkit/serializers.py +127 -28
  10. django_nepkit/static/django_nepkit/js/address-chaining.js +129 -29
  11. django_nepkit/static/django_nepkit/js/nepal-data.js +1 -0
  12. django_nepkit/static/django_nepkit/js/nepali-datepicker-init.js +55 -46
  13. django_nepkit/utils.py +270 -46
  14. django_nepkit/validators.py +1 -1
  15. django_nepkit/views.py +127 -10
  16. django_nepkit/widgets.py +100 -31
  17. django_nepkit-0.2.1.dist-info/METADATA +308 -0
  18. django_nepkit-0.2.1.dist-info/RECORD +37 -0
  19. example/demo/admin.py +45 -21
  20. example/demo/models.py +41 -4
  21. example/demo/serializers.py +39 -0
  22. example/demo/urls.py +13 -2
  23. example/demo/views.py +125 -3
  24. example/example_project/settings.py +10 -0
  25. example/example_project/urls.py +1 -1
  26. example/manage.py +2 -2
  27. django_nepkit/templatetags/__init__.py +0 -0
  28. django_nepkit/templatetags/nepali.py +0 -74
  29. django_nepkit-0.1.0.dist-info/METADATA +0 -377
  30. django_nepkit-0.1.0.dist-info/RECORD +0 -39
  31. example/demo/migrations/0001_initial.py +0 -2113
  32. example/demo/migrations/0002_alter_person_phone_number.py +0 -18
  33. example/demo/migrations/0003_person_created_at_person_updated_at.py +0 -27
  34. example/demo/migrations/0004_alter_person_created_at_alter_person_updated_at.py +0 -23
  35. example/demo/migrations/0005_alter_person_created_at_alter_person_updated_at.py +0 -27
  36. example/demo/migrations/__init__.py +0 -0
  37. {django_nepkit-0.1.0.dist-info → django_nepkit-0.2.1.dist-info}/WHEEL +0 -0
  38. {django_nepkit-0.1.0.dist-info → django_nepkit-0.2.1.dist-info}/licenses/LICENSE +0 -0
  39. {django_nepkit-0.1.0.dist-info → django_nepkit-0.2.1.dist-info}/top_level.txt +0 -0
@@ -12,19 +12,21 @@ except ModuleNotFoundError as e:
12
12
  "to use `django_nepkit.serializers`."
13
13
  ) from e
14
14
 
15
- from django_nepkit.utils import try_parse_nepali_date, try_parse_nepali_datetime
16
-
17
- # --------------------------------------------------
18
- # Base Serializer Field
19
- # --------------------------------------------------
15
+ from django_nepkit.conf import nepkit_settings
16
+ from django_nepkit.utils import (
17
+ BS_DATE_FORMAT,
18
+ BS_DATETIME_FORMAT,
19
+ format_nepali_currency,
20
+ try_parse_nepali_date,
21
+ try_parse_nepali_datetime,
22
+ )
20
23
 
21
24
 
22
25
  class BaseNepaliBSField(serializers.Field):
23
26
  """
24
- Base DRF field for Nepali (BS) Date / DateTime.
25
-
26
- - **Input**: BS string, or an already-parsed `nepalidate`/`nepalidatetime`
27
- - **Output**: formatted BS string (configurable)
27
+ Handles Nepali (BS) dates.
28
+ - Input: A date string (like '2080-01-01') or a date object.
29
+ - Output: A formatted string for your API.
28
30
  """
29
31
 
30
32
  format: str = ""
@@ -35,14 +37,36 @@ class BaseNepaliBSField(serializers.Field):
35
37
  "invalid_type": "Invalid type. Expected a string.",
36
38
  }
37
39
 
38
- def __init__(self, *, format: Optional[str] = None, **kwargs: Any) -> None:
40
+ def __init__(
41
+ self,
42
+ *,
43
+ format: Optional[str] = None,
44
+ ne: Optional[bool] = None,
45
+ en: Optional[bool] = None,
46
+ **kwargs: Any,
47
+ ) -> None:
39
48
  """
40
49
  Args:
41
50
  format: Optional `strftime` format used for representation.
42
51
  If not provided, uses the class default.
52
+ ne: If True, output in Devanagari script. If None, uses DEFAULT_LANGUAGE.
53
+ en: If True, output in English. If None, derived from ne.
43
54
  """
44
55
  if format is not None:
45
56
  self.format = format
57
+
58
+ default_lang = nepkit_settings.DEFAULT_LANGUAGE
59
+
60
+ if ne is None:
61
+ self.ne = default_lang == "ne"
62
+ else:
63
+ self.ne = ne
64
+
65
+ if en is None:
66
+ self.en = not self.ne
67
+ else:
68
+ self.en = en
69
+
46
70
  super().__init__(**kwargs)
47
71
 
48
72
  def _parse(self, value: str):
@@ -52,20 +76,25 @@ class BaseNepaliBSField(serializers.Field):
52
76
  return try_parse_nepali_datetime(value)
53
77
  return None
54
78
 
79
+ def _format_value(self, value: Any) -> str:
80
+ """Format value using strftime or strftime_ne (Devanagari) based on self.ne."""
81
+ if self.ne and hasattr(value, "strftime_ne"):
82
+ return value.strftime_ne(self.format) # type: ignore[attr-defined]
83
+ return value.strftime(self.format) # type: ignore[attr-defined]
84
+
55
85
  def to_representation(self, value: Any) -> Optional[str]:
56
86
  if value is None:
57
87
  return None
58
88
 
59
89
  if isinstance(value, self.nepali_type):
60
- return value.strftime(self.format) # type: ignore[attr-defined]
90
+ return self._format_value(value)
61
91
 
62
- # If DB returns string, try to normalize it.
92
+ # Convert string dates to Nepali date objects
63
93
  if isinstance(value, str):
64
94
  parsed = self._parse(value)
65
95
  if parsed is not None:
66
- return parsed.strftime(self.format) # type: ignore[attr-defined]
96
+ return self._format_value(parsed)
67
97
 
68
- # Fallback: best-effort stringify (keeps behavior non-breaking)
69
98
  return str(value)
70
99
 
71
100
  def to_internal_value(self, data: Any):
@@ -85,29 +114,99 @@ class BaseNepaliBSField(serializers.Field):
85
114
  self.fail("invalid", format=self.format)
86
115
 
87
116
 
88
- # --------------------------------------------------
89
- # Nepali Date (BS)
90
- # --------------------------------------------------
117
+ class NepaliDateSerializerField(BaseNepaliBSField):
118
+ """API field for Nepali Dates."""
91
119
 
120
+ format = BS_DATE_FORMAT
121
+ nepali_type = nepalidate
92
122
 
93
- class NepaliDateSerializerField(BaseNepaliBSField):
123
+
124
+ class NepaliDateTimeSerializerField(BaseNepaliBSField):
125
+ """API field for Nepali Date and Time."""
126
+
127
+ format = BS_DATETIME_FORMAT
128
+ nepali_type = nepalidatetime
129
+
130
+
131
+ class NepaliCurrencySerializerField(serializers.Field):
94
132
  """
95
- DRF field for Nepali BS Date (YYYY-MM-DD)
133
+ API field for Nepali Currency.
134
+ Formats decimal values with Nepali-style commas.
96
135
  """
97
136
 
98
- format = "%Y-%m-%d"
99
- nepali_type = nepalidate
137
+ def __init__(self, currency_symbol="Rs.", ne=None, **kwargs):
138
+ self.currency_symbol = currency_symbol
139
+ self.ne = ne
140
+ super().__init__(**kwargs)
141
+
142
+ def to_representation(self, value):
143
+ if value is None:
144
+ return None
100
145
 
146
+ ne = self.ne
147
+ if ne is None:
148
+ ne = self.context.get("ne", nepkit_settings.DEFAULT_LANGUAGE == "ne")
101
149
 
102
- # --------------------------------------------------
103
- # Nepali DateTime (BS)
104
- # --------------------------------------------------
150
+ return format_nepali_currency(
151
+ value, currency_symbol=self.currency_symbol, ne=ne
152
+ )
105
153
 
154
+ def to_internal_value(self, data):
155
+ # We don't support converting back from formatted string to decimal here
156
+ # Users should send raw decimal/float values for input.
157
+ return data
106
158
 
107
- class NepaliDateTimeSerializerField(BaseNepaliBSField):
159
+
160
+ class NepaliLocalizedSerializerMixin:
108
161
  """
109
- DRF field for Nepali BS DateTime (YYYY-MM-DD HH:MM:SS)
162
+ A mixin for ModelSerializer that automatically adds localized counterparts
163
+ for eligible fields if `ne=True` is passed in the context.
164
+ Eligible fields: NepaliDateField, NepaliDateTimeField, NepaliCurrencyField.
165
+
166
+ Usage:
167
+ class MySerializer(NepaliLocalizedSerializerMixin, serializers.ModelSerializer):
168
+ ...
110
169
  """
111
170
 
112
- format = "%Y-%m-%d %H:%M:%S"
113
- nepali_type = nepalidatetime
171
+ def to_representation(self, instance):
172
+ ret = super().to_representation(instance) # type: ignore[misc]
173
+ ne = self.context.get("ne", False)
174
+ if not ne:
175
+ return ret
176
+
177
+ from django_nepkit.models import (
178
+ NepaliCurrencyField,
179
+ NepaliDateField,
180
+ NepaliDateTimeField,
181
+ )
182
+
183
+ model = getattr(self.Meta, "model", None) # type: ignore[attr-defined]
184
+ if not model:
185
+ return ret
186
+
187
+ for field_name, value in list(ret.items()):
188
+ try:
189
+ model_field = model._meta.get_field(field_name)
190
+ localized_name = f"{field_name}_ne"
191
+
192
+ if localized_name in ret:
193
+ continue
194
+
195
+ if isinstance(model_field, NepaliDateField):
196
+ raw_val = getattr(instance, field_name)
197
+ if hasattr(raw_val, "strftime_ne"):
198
+ ret[localized_name] = raw_val.strftime_ne(BS_DATE_FORMAT)
199
+
200
+ elif isinstance(model_field, NepaliDateTimeField):
201
+ raw_val = getattr(instance, field_name)
202
+ if hasattr(raw_val, "strftime_ne"):
203
+ ret[localized_name] = raw_val.strftime_ne(BS_DATETIME_FORMAT)
204
+
205
+ elif isinstance(model_field, NepaliCurrencyField):
206
+ ret[localized_name] = format_nepali_currency(
207
+ value, currency_symbol="", ne=True
208
+ )
209
+ except Exception:
210
+ continue
211
+
212
+ return ret
@@ -1,6 +1,20 @@
1
1
  (function() {
2
2
  'use strict';
3
3
 
4
+ // Placeholder text constants
5
+ const PLACEHOLDERS = {
6
+ province: { ne: 'प्रदेश छान्नुहोस्', en: 'Select Province' },
7
+ district: { ne: 'जिल्ला छान्नुहोस्', en: 'Select District' },
8
+ municipality: { ne: 'नगरपालिका छान्नुहोस्', en: 'Select Municipality' }
9
+ };
10
+
11
+ /**
12
+ * Get placeholder text for a specific location type
13
+ */
14
+ function getPlaceholder(type, isNepali) {
15
+ return PLACEHOLDERS[type][isNepali ? 'ne' : 'en'];
16
+ }
17
+
4
18
  function updateOptions(selectElement, data, placeholder) {
5
19
  selectElement.innerHTML = '';
6
20
  if (placeholder) {
@@ -18,47 +32,133 @@
18
32
  selectElement.dispatchEvent(new Event('change'));
19
33
  }
20
34
 
35
+ function getLocalData(type, parentId, isNepali) {
36
+ if (!window.NEPKIT_DATA) return null;
37
+ const lang = isNepali ? 'ne' : 'en';
38
+ const data = window.NEPKIT_DATA[lang];
39
+ if (!data) return null;
40
+
41
+ if (type === 'districts') {
42
+ return data.districts[parentId] || [];
43
+ } else if (type === 'municipalities') {
44
+ return data.municipalities[parentId] || [];
45
+ }
46
+ return null;
47
+ }
48
+
49
+ function getMatchingSelect(container, selector, isNepali) {
50
+ const matches = container.querySelectorAll(selector);
51
+ for (let el of matches) {
52
+ const elIsNepali = el.dataset.ne === 'true';
53
+ if (elIsNepali === isNepali) {
54
+ return el;
55
+ }
56
+ }
57
+ return null;
58
+ }
59
+
60
+ /**
61
+ * Fetch data from server endpoint
62
+ */
63
+ function fetchData(selectElement, paramName, paramValue, placeholderType, isNepali) {
64
+ if (!selectElement.dataset.url) return;
65
+
66
+ let url = selectElement.dataset.url + '?' + paramName + '=' + encodeURIComponent(paramValue);
67
+ if (isNepali) url += '&ne=true';
68
+ if (selectElement.dataset.en === 'true') url += '&en=true';
69
+
70
+ fetch(url)
71
+ .then(response => response.json())
72
+ .then(data => {
73
+ updateOptions(selectElement, data, getPlaceholder(placeholderType, isNepali));
74
+ });
75
+ }
76
+
77
+ /**
78
+ * Update dependent select element based on parent value
79
+ */
80
+ function updateDependentSelect(childSelect, dataType, parentValue, placeholderType, isNepali, paramName) {
81
+ if (!childSelect) return;
82
+
83
+ if (!parentValue) {
84
+ updateOptions(childSelect, [], getPlaceholder(placeholderType, isNepali));
85
+ return;
86
+ }
87
+
88
+ const localData = getLocalData(dataType, parentValue, isNepali);
89
+
90
+ if (localData) {
91
+ updateOptions(childSelect, localData, getPlaceholder(placeholderType, isNepali));
92
+ } else {
93
+ fetchData(childSelect, paramName, parentValue, placeholderType, isNepali);
94
+ }
95
+ }
96
+
97
+ function init() {
98
+ // Initialize province selects
99
+ document.querySelectorAll('.nepkit-province-select').forEach(provinceSelect => {
100
+ const isNepali = provinceSelect.dataset.ne === 'true';
101
+ const container = provinceSelect.closest('form') || document;
102
+ const districtSelect = getMatchingSelect(container, '.nepkit-district-select', isNepali);
103
+ const municipalitySelect = getMatchingSelect(container, '.nepkit-municipality-select', isNepali);
104
+
105
+ if (!provinceSelect.value) {
106
+ if (districtSelect && !districtSelect.value) {
107
+ updateOptions(districtSelect, [], getPlaceholder('district', isNepali));
108
+ }
109
+ if (municipalitySelect && !municipalitySelect.value) {
110
+ updateOptions(municipalitySelect, [], getPlaceholder('municipality', isNepali));
111
+ }
112
+ }
113
+ });
114
+
115
+ // Initialize district selects
116
+ document.querySelectorAll('.nepkit-district-select').forEach(districtSelect => {
117
+ const isNepali = districtSelect.dataset.ne === 'true';
118
+ const container = districtSelect.closest('form') || document;
119
+ const municipalitySelect = getMatchingSelect(container, '.nepkit-municipality-select', isNepali);
120
+
121
+ if (!districtSelect.value) {
122
+ if (municipalitySelect && !municipalitySelect.value) {
123
+ updateOptions(municipalitySelect, [], getPlaceholder('municipality', isNepali));
124
+ }
125
+ }
126
+ });
127
+ }
128
+
21
129
  document.addEventListener('change', function(e) {
130
+ // Handle province select change
22
131
  if (e.target.matches('.nepkit-province-select')) {
23
132
  const province = e.target.value;
133
+ const isNepali = e.target.dataset.ne === 'true';
24
134
  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
- }
135
+ const districtSelect = getMatchingSelect(container, '.nepkit-district-select', isNepali);
136
+ const municipalitySelect = getMatchingSelect(container, '.nepkit-municipality-select', isNepali);
34
137
 
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
- });
138
+ // Always clear municipality if province changes
139
+ if (municipalitySelect) {
140
+ updateOptions(municipalitySelect, [], getPlaceholder('municipality', isNepali));
41
141
  }
142
+
143
+ // Update district options
144
+ updateDependentSelect(districtSelect, 'districts', province, 'district', isNepali, 'province');
42
145
  }
43
146
 
147
+ // Handle district select change
44
148
  if (e.target.matches('.nepkit-district-select')) {
45
149
  const district = e.target.value;
150
+ const isNepali = e.target.dataset.ne === 'true';
46
151
  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
- }
152
+ const municipalitySelect = getMatchingSelect(container, '.nepkit-municipality-select', isNepali);
54
153
 
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
- }
154
+ // Update municipality options
155
+ updateDependentSelect(municipalitySelect, 'municipalities', district, 'municipality', isNepali, 'district');
62
156
  }
63
157
  });
158
+
159
+ if (document.readyState === 'loading') {
160
+ document.addEventListener('DOMContentLoaded', init);
161
+ } else {
162
+ init();
163
+ }
64
164
  })();