django-smartbase-admin 1.0.38__py3-none-any.whl → 1.0.41__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.
- django_smartbase_admin/actions/admin_action_list.py +5 -0
- django_smartbase_admin/admin/widgets.py +52 -5
- django_smartbase_admin/engine/dashboard.py +2 -0
- django_smartbase_admin/engine/field_formatter.py +25 -15
- django_smartbase_admin/engine/filter_widgets.py +41 -6
- django_smartbase_admin/static/sb_admin/dist/main.js +1 -1
- django_smartbase_admin/static/sb_admin/dist/main_style.css +1 -1
- django_smartbase_admin/static/sb_admin/dist/table.js +1 -1
- django_smartbase_admin/static/sb_admin/js/sbadmin_prepopulated_fields_init.js +25 -0
- django_smartbase_admin/static/sb_admin/src/css/components/_dropdown.css +6 -0
- django_smartbase_admin/static/sb_admin/src/js/autocomplete.js +7 -7
- django_smartbase_admin/static/sb_admin/src/js/main.js +2 -0
- django_smartbase_admin/static/sb_admin/src/js/radio.js +31 -0
- django_smartbase_admin/static/sb_admin/src/js/table_modules/detail_view_module.js +50 -1
- django_smartbase_admin/static/sb_admin/src/js/table_modules/filter_module.js +7 -0
- django_smartbase_admin/templates/sb_admin/actions/change_form.html +7 -2
- django_smartbase_admin/templates/sb_admin/actions/list.html +1 -1
- django_smartbase_admin/templates/sb_admin/components/filters.html +1 -0
- django_smartbase_admin/templates/sb_admin/filter_widgets/boolean_field.html +1 -14
- django_smartbase_admin/templates/sb_admin/filter_widgets/partials/clear.html +2 -1
- django_smartbase_admin/templates/sb_admin/filter_widgets/radio_choice_field.html +3 -1
- django_smartbase_admin/templatetags/sb_admin_tags.py +30 -0
- {django_smartbase_admin-1.0.38.dist-info → django_smartbase_admin-1.0.41.dist-info}/METADATA +1 -1
- {django_smartbase_admin-1.0.38.dist-info → django_smartbase_admin-1.0.41.dist-info}/RECORD +26 -24
- {django_smartbase_admin-1.0.38.dist-info → django_smartbase_admin-1.0.41.dist-info}/LICENSE.md +0 -0
- {django_smartbase_admin-1.0.38.dist-info → django_smartbase_admin-1.0.41.dist-info}/WHEEL +0 -0
|
@@ -427,6 +427,11 @@ class SBAdminListAction(SBAdminAction):
|
|
|
427
427
|
data: row.get(data, None)
|
|
428
428
|
for data in self.view.sbadmin_list_display_data
|
|
429
429
|
}
|
|
430
|
+
# Include supporting_annotates values in additional_data
|
|
431
|
+
for field in visible_columns:
|
|
432
|
+
if field.supporting_annotates:
|
|
433
|
+
for key in field.supporting_annotates.keys():
|
|
434
|
+
additional_data[key] = row.get(key, None)
|
|
430
435
|
for field_key, value in row.items():
|
|
431
436
|
if field_key in field_key_field_map:
|
|
432
437
|
field = field_key_field_map[field_key]
|
|
@@ -11,7 +11,12 @@ from django.contrib.admin.widgets import (
|
|
|
11
11
|
ForeignKeyRawIdWidget,
|
|
12
12
|
)
|
|
13
13
|
from django.contrib.auth.forms import ReadOnlyPasswordHashWidget
|
|
14
|
-
from django.core.exceptions import
|
|
14
|
+
from django.core.exceptions import (
|
|
15
|
+
FieldDoesNotExist,
|
|
16
|
+
ImproperlyConfigured,
|
|
17
|
+
ValidationError,
|
|
18
|
+
)
|
|
19
|
+
from django.db.models import ForeignKey, OneToOneField
|
|
15
20
|
from django.template.loader import render_to_string
|
|
16
21
|
from django.urls import reverse
|
|
17
22
|
from django.utils.formats import get_format
|
|
@@ -359,13 +364,19 @@ class SBAdminAutocompleteWidget(
|
|
|
359
364
|
form = None
|
|
360
365
|
field_name = None
|
|
361
366
|
initialised = None
|
|
367
|
+
allow_add = None
|
|
368
|
+
create_value_field = None
|
|
362
369
|
default_create_data = None
|
|
370
|
+
forward_to_create = None
|
|
363
371
|
reload_on_save = None
|
|
364
372
|
REQUEST_CREATED_DATA_KEY = "autocomplete_created_data"
|
|
365
373
|
|
|
366
374
|
def __init__(self, form_field=None, *args, **kwargs):
|
|
367
375
|
attrs = kwargs.pop("attrs", None)
|
|
368
376
|
self.reload_on_save = kwargs.pop("reload_on_save", False)
|
|
377
|
+
self.allow_add = kwargs.pop("allow_add", None)
|
|
378
|
+
self.create_value_field = kwargs.pop("create_value_field", None)
|
|
379
|
+
self.forward_to_create = kwargs.pop("forward_to_create", [])
|
|
369
380
|
super().__init__(form_field, *args, **kwargs)
|
|
370
381
|
self.attrs = {} if attrs is None else attrs.copy()
|
|
371
382
|
if self.multiselect and self.allow_add:
|
|
@@ -595,6 +606,34 @@ class SBAdminAutocompleteWidget(
|
|
|
595
606
|
|
|
596
607
|
return forward_data
|
|
597
608
|
|
|
609
|
+
def get_forward_data_to_create(self, request, forward_data):
|
|
610
|
+
forward_data_to_create = {}
|
|
611
|
+
for field_name in self.forward_to_create:
|
|
612
|
+
value = forward_data.get(field_name)
|
|
613
|
+
if value is None:
|
|
614
|
+
continue
|
|
615
|
+
# If forwarding a FK value from the parent form (e.g. for dependent dropdowns),
|
|
616
|
+
# store it under `<field>_id` so `Model(**kwargs)` accepts the raw PK.
|
|
617
|
+
store_key = field_name
|
|
618
|
+
form_model = getattr(getattr(self, "form", None), "model", None)
|
|
619
|
+
if form_model is not None:
|
|
620
|
+
try:
|
|
621
|
+
form_model_field = form_model._meta.get_field(field_name)
|
|
622
|
+
except FieldDoesNotExist:
|
|
623
|
+
form_model_field = None
|
|
624
|
+
if isinstance(form_model_field, (ForeignKey, OneToOneField)):
|
|
625
|
+
store_key = form_model_field.attname
|
|
626
|
+
|
|
627
|
+
forward_data_to_create[store_key] = self.parse_value_from_input(
|
|
628
|
+
request, value
|
|
629
|
+
)
|
|
630
|
+
if not self.is_multiselect():
|
|
631
|
+
forward_data_to_create[store_key] = next(
|
|
632
|
+
iter(forward_data_to_create[store_key]), None
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
return forward_data_to_create
|
|
636
|
+
|
|
598
637
|
def value_from_datadict(self, data, files, name):
|
|
599
638
|
input_value = super().value_from_datadict(data, files, name)
|
|
600
639
|
threadsafe_request = SBAdminThreadLocalService.get_request()
|
|
@@ -630,7 +669,11 @@ class SBAdminAutocompleteWidget(
|
|
|
630
669
|
)
|
|
631
670
|
self.form_field.queryset = qs
|
|
632
671
|
parsed_value = self.validate(
|
|
633
|
-
parsed_value,
|
|
672
|
+
parsed_value,
|
|
673
|
+
qs,
|
|
674
|
+
threadsafe_request,
|
|
675
|
+
forward_data,
|
|
676
|
+
parsed_is_create,
|
|
634
677
|
)
|
|
635
678
|
|
|
636
679
|
return parsed_value
|
|
@@ -638,14 +681,18 @@ class SBAdminAutocompleteWidget(
|
|
|
638
681
|
def should_create_new_obj(self):
|
|
639
682
|
return self.allow_add and self.create_value_field
|
|
640
683
|
|
|
641
|
-
def create_new_obj(self, value, queryset,
|
|
684
|
+
def create_new_obj(self, value, queryset, request, forward_data):
|
|
642
685
|
if isinstance(value, list):
|
|
643
686
|
# TODO: multiselect creation
|
|
644
687
|
return self.form_field.to_python(value)
|
|
645
688
|
else:
|
|
689
|
+
forward_data_to_create = self.get_forward_data_to_create(
|
|
690
|
+
request, forward_data
|
|
691
|
+
)
|
|
646
692
|
data_to_create = {
|
|
647
693
|
self.create_value_field: value,
|
|
648
694
|
**self.default_create_data,
|
|
695
|
+
**forward_data_to_create,
|
|
649
696
|
}
|
|
650
697
|
new_obj = queryset.model.objects.create(**data_to_create)
|
|
651
698
|
try:
|
|
@@ -658,12 +705,12 @@ class SBAdminAutocompleteWidget(
|
|
|
658
705
|
params={"value": value},
|
|
659
706
|
)
|
|
660
707
|
|
|
661
|
-
def validate(self, value, queryset, request, is_create=False):
|
|
708
|
+
def validate(self, value, queryset, request, forward_data, is_create=False):
|
|
662
709
|
is_create_value = (
|
|
663
710
|
True in is_create if isinstance(is_create, list) else is_create
|
|
664
711
|
)
|
|
665
712
|
if is_create_value and self.should_create_new_obj():
|
|
666
|
-
new_object = self.create_new_obj(value, queryset,
|
|
713
|
+
new_object = self.create_new_obj(value, queryset, request, forward_data)
|
|
667
714
|
request.request_data.additional_data[self.REQUEST_CREATED_DATA_KEY] = (
|
|
668
715
|
request.request_data.additional_data.get(
|
|
669
716
|
self.REQUEST_CREATED_DATA_KEY, {}
|
|
@@ -429,6 +429,7 @@ class SBAdminDashboardChartWidgetByDate(SBAdminDashboardChartWidget):
|
|
|
429
429
|
filter_widget=RadioChoiceFilterWidget(
|
|
430
430
|
choices=self.DateResolutionsOptions.choices,
|
|
431
431
|
default_value=self.default_date_resolution,
|
|
432
|
+
allow_clear=False,
|
|
432
433
|
),
|
|
433
434
|
),
|
|
434
435
|
SBAdminField(
|
|
@@ -437,6 +438,7 @@ class SBAdminDashboardChartWidgetByDate(SBAdminDashboardChartWidget):
|
|
|
437
438
|
filter_widget=RadioChoiceFilterWidget(
|
|
438
439
|
choices=self.CompareOptions.choices,
|
|
439
440
|
default_value=self.CompareOptions.values[0],
|
|
441
|
+
allow_clear=False,
|
|
440
442
|
),
|
|
441
443
|
),
|
|
442
444
|
]
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
2
|
|
|
3
3
|
from django.template.defaultfilters import date, time
|
|
4
|
+
from django.utils import timezone
|
|
5
|
+
from django.utils.html import format_html, format_html_join
|
|
4
6
|
from django.utils.safestring import mark_safe
|
|
5
7
|
from django.utils.translation import gettext_lazy as _
|
|
6
|
-
from django.utils import timezone
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class BadgeType(Enum):
|
|
@@ -39,21 +40,26 @@ def datetime_formatter_with_format(date_format=None, time_format=None):
|
|
|
39
40
|
|
|
40
41
|
def boolean_formatter(object_id, value):
|
|
41
42
|
if value:
|
|
42
|
-
return
|
|
43
|
-
|
|
43
|
+
return format_html(
|
|
44
|
+
'<span class="badge badge-simple badge-positive">{}</span>', _("Yes")
|
|
44
45
|
)
|
|
45
|
-
return
|
|
46
|
+
return format_html(
|
|
47
|
+
'<span class="badge badge-simple badge-neutral">{}</span>', _("No")
|
|
48
|
+
)
|
|
46
49
|
|
|
47
50
|
|
|
48
51
|
def format_array(value_list, separator="", badge_type: BadgeType = BadgeType.NOTICE):
|
|
49
|
-
result = ""
|
|
50
52
|
if not value_list:
|
|
51
|
-
return
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return
|
|
53
|
+
return ""
|
|
54
|
+
|
|
55
|
+
# `separator` is intended to be an internal constant (e.g. "" or "<br>").
|
|
56
|
+
# We mark it safe so HTML separators render as HTML rather than being escaped.
|
|
57
|
+
sep = mark_safe(separator) if separator else ""
|
|
58
|
+
return format_html_join(
|
|
59
|
+
sep,
|
|
60
|
+
'<span class="badge badge-simple badge-{} mr-4">{}</span>',
|
|
61
|
+
((badge_type.value, value) for value in value_list if value),
|
|
62
|
+
)
|
|
57
63
|
|
|
58
64
|
|
|
59
65
|
def array_badge_formatter(object_id, value_list):
|
|
@@ -61,14 +67,18 @@ def array_badge_formatter(object_id, value_list):
|
|
|
61
67
|
|
|
62
68
|
|
|
63
69
|
def newline_separated_array_badge_formatter(object_id, value_list):
|
|
64
|
-
return
|
|
70
|
+
return format_html("<div>{}</div>", format_array(value_list, separator="<br>"))
|
|
65
71
|
|
|
66
72
|
|
|
67
73
|
def rich_text_formatter(object_id, value):
|
|
68
|
-
|
|
69
|
-
|
|
74
|
+
# Intentionally renders HTML (e.g. from a rich text editor field).
|
|
75
|
+
return format_html(
|
|
76
|
+
'<div style="max-width: 500px; white-space: normal;">{}</div>',
|
|
77
|
+
mark_safe(value) if value else "",
|
|
70
78
|
)
|
|
71
79
|
|
|
72
80
|
|
|
73
81
|
def link_formatter(object_id, value):
|
|
74
|
-
|
|
82
|
+
if not value:
|
|
83
|
+
return ""
|
|
84
|
+
return format_html('<a href="{0}">{0}</a>', value)
|
|
@@ -75,6 +75,11 @@ class SBAdminFilterWidget(JSONSerializableMixin):
|
|
|
75
75
|
default_label = None
|
|
76
76
|
filter_query_lambda = None
|
|
77
77
|
exclude_null_operators = False
|
|
78
|
+
# If True, the filter dropdown closes after the filter value changes (frontend behavior).
|
|
79
|
+
# Useful for single-step filters; set to False for widgets where users typically make multiple
|
|
80
|
+
# changes before closing the dropdown.
|
|
81
|
+
close_dropdown_on_change = False
|
|
82
|
+
allow_clear = True
|
|
78
83
|
|
|
79
84
|
def __init__(
|
|
80
85
|
self,
|
|
@@ -83,6 +88,8 @@ class SBAdminFilterWidget(JSONSerializableMixin):
|
|
|
83
88
|
default_label=None,
|
|
84
89
|
filter_query_lambda=None,
|
|
85
90
|
exclude_null_operators=None,
|
|
91
|
+
close_dropdown_on_change=None,
|
|
92
|
+
allow_clear=None,
|
|
86
93
|
**kwargs,
|
|
87
94
|
) -> None:
|
|
88
95
|
super().__init__()
|
|
@@ -93,6 +100,10 @@ class SBAdminFilterWidget(JSONSerializableMixin):
|
|
|
93
100
|
self.exclude_null_operators = (
|
|
94
101
|
exclude_null_operators or self.exclude_null_operators
|
|
95
102
|
)
|
|
103
|
+
if close_dropdown_on_change is not None:
|
|
104
|
+
self.close_dropdown_on_change = close_dropdown_on_change
|
|
105
|
+
if allow_clear is not None:
|
|
106
|
+
self.allow_clear = allow_clear
|
|
96
107
|
|
|
97
108
|
def init_filter_widget_static(self, field, view, configuration):
|
|
98
109
|
self.field = field
|
|
@@ -124,7 +135,9 @@ class SBAdminFilterWidget(JSONSerializableMixin):
|
|
|
124
135
|
return original_query
|
|
125
136
|
|
|
126
137
|
def to_json(self):
|
|
127
|
-
return {
|
|
138
|
+
return {
|
|
139
|
+
"input_id": self.input_id,
|
|
140
|
+
}
|
|
128
141
|
|
|
129
142
|
def get_default_value(self):
|
|
130
143
|
return self.default_value
|
|
@@ -162,6 +175,7 @@ class SBAdminFilterWidget(JSONSerializableMixin):
|
|
|
162
175
|
|
|
163
176
|
class StringFilterWidget(SBAdminFilterWidget):
|
|
164
177
|
template_name = "sb_admin/filter_widgets/string_field.html"
|
|
178
|
+
close_dropdown_on_change = True
|
|
165
179
|
|
|
166
180
|
def get_advanced_filter_operators(self):
|
|
167
181
|
return STRING_ATTRIBUTES
|
|
@@ -178,6 +192,29 @@ class StringFilterWidget(SBAdminFilterWidget):
|
|
|
178
192
|
|
|
179
193
|
class BooleanFilterWidget(SBAdminFilterWidget):
|
|
180
194
|
template_name = "sb_admin/filter_widgets/boolean_field.html"
|
|
195
|
+
choices = None
|
|
196
|
+
close_dropdown_on_change = True
|
|
197
|
+
|
|
198
|
+
def __init__(
|
|
199
|
+
self,
|
|
200
|
+
template_name=None,
|
|
201
|
+
default_value=None,
|
|
202
|
+
default_label=None,
|
|
203
|
+
filter_query_lambda=None,
|
|
204
|
+
exclude_null_operators=None,
|
|
205
|
+
close_dropdown_on_change=None,
|
|
206
|
+
**kwargs,
|
|
207
|
+
) -> None:
|
|
208
|
+
super().__init__(
|
|
209
|
+
template_name,
|
|
210
|
+
default_value,
|
|
211
|
+
default_label,
|
|
212
|
+
filter_query_lambda,
|
|
213
|
+
exclude_null_operators,
|
|
214
|
+
close_dropdown_on_change,
|
|
215
|
+
**kwargs,
|
|
216
|
+
)
|
|
217
|
+
self.choices = ((True, _("Yes")), (False, _("No")))
|
|
181
218
|
|
|
182
219
|
def parse_value_from_input(self, request, filter_value):
|
|
183
220
|
input_value = super().parse_value_from_input(request, filter_value)
|
|
@@ -201,6 +238,7 @@ class BooleanFilterWidget(SBAdminFilterWidget):
|
|
|
201
238
|
class ChoiceFilterWidget(SBAdminFilterWidget):
|
|
202
239
|
template_name = "sb_admin/filter_widgets/choice_field.html"
|
|
203
240
|
choices = None
|
|
241
|
+
close_dropdown_on_change = True
|
|
204
242
|
|
|
205
243
|
def __init__(
|
|
206
244
|
self,
|
|
@@ -236,6 +274,7 @@ class ChoiceFilterWidget(SBAdminFilterWidget):
|
|
|
236
274
|
|
|
237
275
|
class RadioChoiceFilterWidget(ChoiceFilterWidget):
|
|
238
276
|
template_name = "sb_admin/filter_widgets/radio_choice_field.html"
|
|
277
|
+
close_dropdown_on_change = True
|
|
239
278
|
|
|
240
279
|
|
|
241
280
|
class MultipleChoiceFilterWidget(AutocompleteParseMixin, ChoiceFilterWidget):
|
|
@@ -243,6 +282,7 @@ class MultipleChoiceFilterWidget(AutocompleteParseMixin, ChoiceFilterWidget):
|
|
|
243
282
|
enable_select_all = False
|
|
244
283
|
select_all_keyword = None
|
|
245
284
|
select_all_label = None
|
|
285
|
+
close_dropdown_on_change = False
|
|
246
286
|
|
|
247
287
|
def __init__(
|
|
248
288
|
self,
|
|
@@ -492,7 +532,6 @@ class AutocompleteFilterWidget(
|
|
|
492
532
|
forward = None
|
|
493
533
|
label_lambda = None
|
|
494
534
|
value_lambda = None
|
|
495
|
-
allow_add = False
|
|
496
535
|
hide_clear_button = False
|
|
497
536
|
search_query_lambda = None
|
|
498
537
|
create_value_field = None
|
|
@@ -515,10 +554,8 @@ class AutocompleteFilterWidget(
|
|
|
515
554
|
value_lambda=None,
|
|
516
555
|
multiselect=None,
|
|
517
556
|
forward=None,
|
|
518
|
-
allow_add=None,
|
|
519
557
|
hide_clear_button=None,
|
|
520
558
|
search_query_lambda=None,
|
|
521
|
-
create_value_field=None,
|
|
522
559
|
**kwargs,
|
|
523
560
|
) -> None:
|
|
524
561
|
super().__init__(template_name, default_value, **kwargs)
|
|
@@ -534,8 +571,6 @@ class AutocompleteFilterWidget(
|
|
|
534
571
|
self.multiselect = multiselect if multiselect is not None else self.multiselect
|
|
535
572
|
self.multiselect = self.multiselect if self.multiselect is not None else True
|
|
536
573
|
self.forward = forward or self.forward
|
|
537
|
-
self.allow_add = allow_add or self.allow_add
|
|
538
|
-
self.create_value_field = create_value_field or self.create_value_field
|
|
539
574
|
self.hide_clear_button = (
|
|
540
575
|
hide_clear_button
|
|
541
576
|
if hide_clear_button is not None
|