django-smartbase-admin 1.0.38__py3-none-any.whl → 1.0.42__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 (27) hide show
  1. django_smartbase_admin/actions/admin_action_list.py +5 -0
  2. django_smartbase_admin/admin/admin_base.py +1 -1
  3. django_smartbase_admin/admin/widgets.py +52 -5
  4. django_smartbase_admin/engine/dashboard.py +5 -1
  5. django_smartbase_admin/engine/field_formatter.py +25 -15
  6. django_smartbase_admin/engine/filter_widgets.py +41 -6
  7. django_smartbase_admin/static/sb_admin/dist/main.js +1 -1
  8. django_smartbase_admin/static/sb_admin/dist/main_style.css +1 -1
  9. django_smartbase_admin/static/sb_admin/dist/table.js +1 -1
  10. django_smartbase_admin/static/sb_admin/js/sbadmin_prepopulated_fields_init.js +25 -0
  11. django_smartbase_admin/static/sb_admin/src/css/components/_dropdown.css +6 -0
  12. django_smartbase_admin/static/sb_admin/src/js/autocomplete.js +7 -7
  13. django_smartbase_admin/static/sb_admin/src/js/main.js +2 -0
  14. django_smartbase_admin/static/sb_admin/src/js/radio.js +31 -0
  15. django_smartbase_admin/static/sb_admin/src/js/table_modules/detail_view_module.js +50 -1
  16. django_smartbase_admin/static/sb_admin/src/js/table_modules/filter_module.js +7 -0
  17. django_smartbase_admin/templates/sb_admin/actions/change_form.html +7 -2
  18. django_smartbase_admin/templates/sb_admin/actions/list.html +1 -1
  19. django_smartbase_admin/templates/sb_admin/components/filters.html +1 -0
  20. django_smartbase_admin/templates/sb_admin/filter_widgets/boolean_field.html +1 -14
  21. django_smartbase_admin/templates/sb_admin/filter_widgets/partials/clear.html +2 -1
  22. django_smartbase_admin/templates/sb_admin/filter_widgets/radio_choice_field.html +3 -1
  23. django_smartbase_admin/templatetags/sb_admin_tags.py +30 -0
  24. {django_smartbase_admin-1.0.38.dist-info → django_smartbase_admin-1.0.42.dist-info}/METADATA +1 -1
  25. {django_smartbase_admin-1.0.38.dist-info → django_smartbase_admin-1.0.42.dist-info}/RECORD +27 -25
  26. {django_smartbase_admin-1.0.38.dist-info → django_smartbase_admin-1.0.42.dist-info}/LICENSE.md +0 -0
  27. {django_smartbase_admin-1.0.38.dist-info → django_smartbase_admin-1.0.42.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]
@@ -1064,7 +1064,7 @@ class SBAdminInline(
1064
1064
 
1065
1065
  def register_autocomplete_views(self, request) -> None:
1066
1066
  super().register_autocomplete_views(request)
1067
- form_class = self.get_formset(request, self.model()).form
1067
+ form_class = self.get_formset(request, None).form
1068
1068
  self.initialize_form_class(form_class, request)
1069
1069
  form_class()
1070
1070
 
@@ -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 ValidationError, ImproperlyConfigured
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, qs, threadsafe_request, parsed_is_create
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, is_create):
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, is_create)
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, {}
@@ -340,12 +340,14 @@ class SBAdminDashboardChartWidget(SBAdminDashboardWidget):
340
340
  sub_widget_data[sub_widget.get_id()] = sub_widget.get_data(
341
341
  request, sub_widget_qs
342
342
  )
343
+ active_sub_widget = self.get_active_sub_widget(request)
344
+ dataset_label = active_sub_widget.title if active_sub_widget else self.name
343
345
  return_data = {
344
346
  "main": {
345
347
  "labels": labels,
346
348
  "datasets": [
347
349
  {
348
- "label": self.name,
350
+ "label": dataset_label,
349
351
  "data": dataset_data,
350
352
  **self.get_dataset_options(request),
351
353
  }
@@ -429,6 +431,7 @@ class SBAdminDashboardChartWidgetByDate(SBAdminDashboardChartWidget):
429
431
  filter_widget=RadioChoiceFilterWidget(
430
432
  choices=self.DateResolutionsOptions.choices,
431
433
  default_value=self.default_date_resolution,
434
+ allow_clear=False,
432
435
  ),
433
436
  ),
434
437
  SBAdminField(
@@ -437,6 +440,7 @@ class SBAdminDashboardChartWidgetByDate(SBAdminDashboardChartWidget):
437
440
  filter_widget=RadioChoiceFilterWidget(
438
441
  choices=self.CompareOptions.choices,
439
442
  default_value=self.CompareOptions.values[0],
443
+ allow_clear=False,
440
444
  ),
441
445
  ),
442
446
  ]
@@ -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 mark_safe(
43
- f'<span class="badge badge-simple badge-positive">{_("Yes")}</span>'
43
+ return format_html(
44
+ '<span class="badge badge-simple badge-positive">{}</span>', _("Yes")
44
45
  )
45
- return mark_safe(f'<span class="badge badge-simple badge-neutral">{_("No")}</span>')
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 result
52
- for value in value_list:
53
- if not value:
54
- continue
55
- result += f'<span class="badge badge-simple badge-{badge_type.value} mr-4">{value}</span>{separator}'
56
- return mark_safe(result)
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 mark_safe(f'<div>{format_array(value_list, separator="<br>")}</div>')
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
- return mark_safe(
69
- f'<div style="max-width: 500px; white-space: normal;">{value}</div>'
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
- return mark_safe(f'<a href="{value}">{value}</a>')
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 {"input_id": self.input_id}
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