django-smartbase-admin 0.2.54__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 (184) hide show
  1. django_smartbase_admin/actions/admin_action_list.py +79 -38
  2. django_smartbase_admin/actions/advanced_filters.py +24 -1
  3. django_smartbase_admin/admin/admin_base.py +402 -97
  4. django_smartbase_admin/admin/site.py +93 -35
  5. django_smartbase_admin/admin/widgets.py +636 -26
  6. django_smartbase_admin/apps.py +2 -0
  7. django_smartbase_admin/engine/actions.py +34 -16
  8. django_smartbase_admin/engine/admin_base_view.py +252 -115
  9. django_smartbase_admin/engine/configuration.py +186 -4
  10. django_smartbase_admin/engine/const.py +6 -0
  11. django_smartbase_admin/engine/dashboard.py +49 -24
  12. django_smartbase_admin/engine/fake_inline.py +15 -11
  13. django_smartbase_admin/engine/field.py +42 -12
  14. django_smartbase_admin/engine/field_formatter.py +38 -14
  15. django_smartbase_admin/engine/filter_widgets.py +348 -24
  16. django_smartbase_admin/engine/menu_item.py +8 -5
  17. django_smartbase_admin/engine/modal_view.py +12 -7
  18. django_smartbase_admin/engine/request.py +2 -0
  19. django_smartbase_admin/integration/__init__.py +0 -0
  20. django_smartbase_admin/integration/django_cms.py +43 -0
  21. django_smartbase_admin/locale/sk/LC_MESSAGES/django.mo +0 -0
  22. django_smartbase_admin/locale/sk/LC_MESSAGES/django.po +268 -37
  23. django_smartbase_admin/migrations/0005_sbadminuserconfiguration.py +26 -0
  24. django_smartbase_admin/migrations/0006_alter_sbadminuserconfiguration_color_scheme.py +18 -0
  25. django_smartbase_admin/models.py +22 -0
  26. django_smartbase_admin/monkeypatch/admin_readonly_field_monkeypatch.py +96 -0
  27. django_smartbase_admin/monkeypatch/fake_inline_monkeypatch.py +1 -1
  28. django_smartbase_admin/querysets.py +3 -0
  29. django_smartbase_admin/services/configuration.py +30 -0
  30. django_smartbase_admin/services/thread_local.py +6 -19
  31. django_smartbase_admin/services/views.py +80 -13
  32. django_smartbase_admin/services/xlsx_export.py +6 -0
  33. django_smartbase_admin/static/sb_admin/build/tailwind.config.js +1 -0
  34. django_smartbase_admin/static/sb_admin/build/tailwind_config_partials/colors.js +4 -0
  35. django_smartbase_admin/static/sb_admin/build/tailwind_config_partials/spacing.js +1 -0
  36. django_smartbase_admin/static/sb_admin/build/webpack.common.js +11 -8
  37. django_smartbase_admin/static/sb_admin/css/ckeditor/ckeditor_content_dark.css +208 -0
  38. django_smartbase_admin/static/sb_admin/css/coloris/coloris.min.css +1 -0
  39. django_smartbase_admin/static/sb_admin/dist/calendar.js +1 -0
  40. django_smartbase_admin/static/sb_admin/dist/calendar_style.css +1 -0
  41. django_smartbase_admin/static/sb_admin/dist/calendar_style.js +0 -0
  42. django_smartbase_admin/static/sb_admin/dist/chart.js +1 -1
  43. django_smartbase_admin/static/sb_admin/dist/main.js +1 -1
  44. django_smartbase_admin/static/sb_admin/dist/main_style.css +1 -1
  45. django_smartbase_admin/static/sb_admin/dist/table.js +1 -1
  46. django_smartbase_admin/static/sb_admin/dist/tree_widget.js +1 -0
  47. django_smartbase_admin/static/sb_admin/dist/tree_widget_style.css +1 -0
  48. django_smartbase_admin/static/sb_admin/dist/tree_widget_style.js +0 -0
  49. django_smartbase_admin/static/sb_admin/fancytree/jquery.fancytree-all-deps.min.js +1 -0
  50. django_smartbase_admin/static/sb_admin/images/file_types/file-csv.svg +11 -0
  51. django_smartbase_admin/static/sb_admin/images/file_types/file-doc.svg +11 -0
  52. django_smartbase_admin/static/sb_admin/images/file_types/file-docx.svg +11 -0
  53. django_smartbase_admin/static/sb_admin/images/file_types/file-other.svg +13 -0
  54. django_smartbase_admin/static/sb_admin/images/file_types/file-pdf.svg +11 -0
  55. django_smartbase_admin/static/sb_admin/images/file_types/file-ppt.svg +11 -0
  56. django_smartbase_admin/static/sb_admin/images/file_types/file-xls.svg +11 -0
  57. django_smartbase_admin/static/sb_admin/images/file_types/file-xlsx.svg +11 -0
  58. django_smartbase_admin/static/sb_admin/images/file_types/file-zip.svg +18 -0
  59. django_smartbase_admin/static/sb_admin/images/flags/de-at.png +0 -0
  60. django_smartbase_admin/static/sb_admin/images/flags/de-ch.png +0 -0
  61. django_smartbase_admin/static/sb_admin/images/logo_light.svg +21 -0
  62. django_smartbase_admin/static/sb_admin/js/coloris/coloris.min.js +6 -0
  63. django_smartbase_admin/static/sb_admin/js/fullcalendar.min.js +14804 -0
  64. django_smartbase_admin/static/sb_admin/js/sbadmin_prepopulated_fields_init.js +25 -0
  65. django_smartbase_admin/static/sb_admin/sprites/sb_admin/Bolt-one.svg +3 -0
  66. django_smartbase_admin/static/sb_admin/sprites/sb_admin/Calendar.svg +3 -0
  67. django_smartbase_admin/static/sb_admin/sprites/sb_admin/Caution.svg +3 -0
  68. django_smartbase_admin/static/sb_admin/sprites/sb_admin/Electric-drill.svg +3 -0
  69. django_smartbase_admin/static/sb_admin/sprites/sb_admin/Fire-extinguisher.svg +3 -0
  70. django_smartbase_admin/static/sb_admin/sprites/sb_admin/Gas.svg +3 -0
  71. django_smartbase_admin/static/sb_admin/sprites/sb_admin/Lightning-fill.svg +3 -0
  72. django_smartbase_admin/static/sb_admin/sprites/sb_admin/Moon.svg +3 -0
  73. django_smartbase_admin/static/sb_admin/sprites/sb_admin/Phone-telephone.svg +3 -0
  74. django_smartbase_admin/static/sb_admin/sprites/sb_admin/Printer.svg +3 -0
  75. django_smartbase_admin/static/sb_admin/sprites/sb_admin/Pull.svg +3 -0
  76. django_smartbase_admin/static/sb_admin/sprites/sb_admin/Sun-one.svg +3 -0
  77. django_smartbase_admin/static/sb_admin/sprites/sb_admin/Time.svg +3 -0
  78. django_smartbase_admin/static/sb_admin/src/css/_base.css +5 -1
  79. django_smartbase_admin/static/sb_admin/src/css/_colors.css +257 -82
  80. django_smartbase_admin/static/sb_admin/src/css/_components.css +61 -13
  81. django_smartbase_admin/static/sb_admin/src/css/_datepicker.css +8 -1
  82. django_smartbase_admin/static/sb_admin/src/css/_filer.css +60 -0
  83. django_smartbase_admin/static/sb_admin/src/css/_inlines.css +51 -10
  84. django_smartbase_admin/static/sb_admin/src/css/_tabulator.css +8 -2
  85. django_smartbase_admin/static/sb_admin/src/css/calendar.css +162 -0
  86. django_smartbase_admin/static/sb_admin/src/css/components/_button.css +41 -1
  87. django_smartbase_admin/static/sb_admin/src/css/components/_dropdown.css +31 -7
  88. django_smartbase_admin/static/sb_admin/src/css/components/_input.css +62 -20
  89. django_smartbase_admin/static/sb_admin/src/css/components/_modal.css +1 -1
  90. django_smartbase_admin/static/sb_admin/src/css/components/_query-builder.css +21 -2
  91. django_smartbase_admin/static/sb_admin/src/css/components/_toggle.css +12 -1
  92. django_smartbase_admin/static/sb_admin/src/css/components/_tooltip.css +8 -22
  93. django_smartbase_admin/static/sb_admin/src/css/style.css +17 -0
  94. django_smartbase_admin/static/sb_admin/src/css/tree_widget.css +411 -0
  95. django_smartbase_admin/static/sb_admin/src/js/autocomplete.js +69 -11
  96. django_smartbase_admin/static/sb_admin/src/js/calendar.js +56 -0
  97. django_smartbase_admin/static/sb_admin/src/js/chart.js +8 -22
  98. django_smartbase_admin/static/sb_admin/src/js/choices.js +18 -8
  99. django_smartbase_admin/static/sb_admin/src/js/datepicker.js +97 -336
  100. django_smartbase_admin/static/sb_admin/src/js/datepicker_plugins.js +357 -0
  101. django_smartbase_admin/static/sb_admin/src/js/main.js +306 -31
  102. django_smartbase_admin/static/sb_admin/src/js/multiselect.js +50 -41
  103. django_smartbase_admin/static/sb_admin/src/js/radio.js +31 -0
  104. django_smartbase_admin/static/sb_admin/src/js/range.js +3 -2
  105. django_smartbase_admin/static/sb_admin/src/js/table.js +34 -5
  106. django_smartbase_admin/static/sb_admin/src/js/table_modules/advanced_filter_module.js +43 -20
  107. django_smartbase_admin/static/sb_admin/src/js/table_modules/data_edit_module.js +8 -10
  108. django_smartbase_admin/static/sb_admin/src/js/table_modules/detail_view_module.js +50 -1
  109. django_smartbase_admin/static/sb_admin/src/js/table_modules/filter_module.js +10 -3
  110. django_smartbase_admin/static/sb_admin/src/js/table_modules/header_tabs_module.js +11 -11
  111. django_smartbase_admin/static/sb_admin/src/js/table_modules/selection_module.js +28 -8
  112. django_smartbase_admin/static/sb_admin/src/js/table_modules/table_params_module.js +6 -0
  113. django_smartbase_admin/static/sb_admin/src/js/table_modules/views_module.js +6 -0
  114. django_smartbase_admin/static/sb_admin/src/js/tree_widget.js +406 -0
  115. django_smartbase_admin/static/sb_admin/src/js/utils.js +56 -21
  116. django_smartbase_admin/templates/sb_admin/actions/change_form.html +176 -116
  117. django_smartbase_admin/templates/sb_admin/actions/dashboard.html +2 -2
  118. django_smartbase_admin/templates/sb_admin/actions/list.html +79 -39
  119. django_smartbase_admin/templates/sb_admin/actions/partials/action_link.html +14 -0
  120. django_smartbase_admin/templates/sb_admin/actions/partials/tabulator_header_v2.html +2 -2
  121. django_smartbase_admin/templates/sb_admin/actions/tree_list.html +63 -0
  122. django_smartbase_admin/templates/sb_admin/authentification/login_base.html +5 -1
  123. django_smartbase_admin/templates/sb_admin/components/columns.html +1 -1
  124. django_smartbase_admin/templates/sb_admin/components/filters.html +1 -0
  125. django_smartbase_admin/templates/sb_admin/components/filters_v2.html +99 -85
  126. django_smartbase_admin/templates/sb_admin/dashboard/calendar_widget.html +69 -0
  127. django_smartbase_admin/templates/sb_admin/dashboard/chart_widget.html +21 -2
  128. django_smartbase_admin/templates/sb_admin/dashboard/list_widget.html +6 -0
  129. django_smartbase_admin/templates/sb_admin/dashboard/widget_base.html +1 -1
  130. django_smartbase_admin/templates/sb_admin/filter_widgets/advanced_filters/date_field.html +18 -8
  131. django_smartbase_admin/templates/sb_admin/filter_widgets/advanced_filters/multiple_choice_field.html +1 -1
  132. django_smartbase_admin/templates/sb_admin/filter_widgets/advanced_filters/tree_select_filter.html +2 -0
  133. django_smartbase_admin/templates/sb_admin/filter_widgets/boolean_field.html +1 -14
  134. django_smartbase_admin/templates/sb_admin/filter_widgets/date_field.html +18 -4
  135. django_smartbase_admin/templates/sb_admin/filter_widgets/multiple_choice_field.html +14 -0
  136. django_smartbase_admin/templates/sb_admin/filter_widgets/partials/clear.html +12 -6
  137. django_smartbase_admin/templates/sb_admin/filter_widgets/radio_choice_field.html +5 -3
  138. django_smartbase_admin/templates/sb_admin/filter_widgets/tree_select_filter.html +16 -0
  139. django_smartbase_admin/templates/sb_admin/includes/change_form_title.html +3 -1
  140. django_smartbase_admin/templates/sb_admin/includes/inline_fieldset.html +48 -39
  141. django_smartbase_admin/templates/sb_admin/includes/notifications.html +2 -1
  142. django_smartbase_admin/templates/sb_admin/includes/readonly_boolean_field.html +9 -0
  143. django_smartbase_admin/templates/sb_admin/includes/readonly_field.html +12 -0
  144. django_smartbase_admin/templates/sb_admin/includes/table_inline_delete_button.html +4 -5
  145. django_smartbase_admin/templates/sb_admin/inlines/stacked_inline.html +68 -40
  146. django_smartbase_admin/templates/sb_admin/inlines/table_inline.html +76 -34
  147. django_smartbase_admin/templates/sb_admin/integrations/filer/folder_list.html +18 -0
  148. django_smartbase_admin/templates/sb_admin/navigation.html +166 -158
  149. django_smartbase_admin/templates/sb_admin/partials/modal/modal_content.html +2 -6
  150. django_smartbase_admin/templates/sb_admin/sb_admin_base.html +49 -4
  151. django_smartbase_admin/templates/sb_admin/sb_admin_base_no_sidebar.html +27 -11
  152. django_smartbase_admin/templates/sb_admin/sb_admin_js_trans.html +3 -0
  153. django_smartbase_admin/templates/sb_admin/sprites/sb_admin.svg +1 -1
  154. django_smartbase_admin/templates/sb_admin/tailwind_whitelist.html +6 -3
  155. django_smartbase_admin/templates/sb_admin/widgets/array.html +0 -1
  156. django_smartbase_admin/templates/sb_admin/widgets/attributes.html +68 -0
  157. django_smartbase_admin/templates/sb_admin/widgets/autocomplete.html +13 -2
  158. django_smartbase_admin/templates/sb_admin/widgets/{checkbox_select.html → checkbox_dropdown.html} +2 -2
  159. django_smartbase_admin/templates/sb_admin/widgets/clearable_file_input.html +2 -2
  160. django_smartbase_admin/templates/sb_admin/widgets/color_field.html +30 -0
  161. django_smartbase_admin/templates/sb_admin/widgets/date.html +8 -1
  162. django_smartbase_admin/templates/sb_admin/widgets/filer_file.html +84 -0
  163. django_smartbase_admin/templates/sb_admin/widgets/includes/related_item_buttons.html +38 -0
  164. django_smartbase_admin/templates/sb_admin/widgets/multiwidget.html +1 -1
  165. django_smartbase_admin/templates/sb_admin/widgets/radio.html +3 -2
  166. django_smartbase_admin/templates/sb_admin/widgets/radio_dropdown.html +30 -0
  167. django_smartbase_admin/templates/sb_admin/widgets/read_only_password_hash.html +3 -0
  168. django_smartbase_admin/templates/sb_admin/widgets/time.html +8 -1
  169. django_smartbase_admin/templates/sb_admin/widgets/toggle.html +1 -1
  170. django_smartbase_admin/templates/sb_admin/widgets/tree_base.html +59 -0
  171. django_smartbase_admin/templates/sb_admin/widgets/tree_select.html +24 -0
  172. django_smartbase_admin/templates/sb_admin/widgets/tree_select_inline.html +12 -0
  173. django_smartbase_admin/templatetags/sb_admin_tags.py +115 -4
  174. django_smartbase_admin/utils.py +22 -3
  175. django_smartbase_admin/views/dashboard_view.py +6 -0
  176. django_smartbase_admin/views/global_filter_view.py +8 -2
  177. django_smartbase_admin/views/translations_view.py +12 -5
  178. django_smartbase_admin/views/user_config_view.py +52 -0
  179. django_smartbase_admin-1.0.42.dist-info/METADATA +166 -0
  180. {django_smartbase_admin-0.2.54.dist-info → django_smartbase_admin-1.0.42.dist-info}/RECORD +182 -118
  181. {django_smartbase_admin-0.2.54.dist-info → django_smartbase_admin-1.0.42.dist-info}/WHEEL +1 -1
  182. django_smartbase_admin/templates/sb_admin/integrations/sorting/change_list.html +0 -401
  183. django_smartbase_admin-0.2.54.dist-info/METADATA +0 -25
  184. {django_smartbase_admin-0.2.54.dist-info → django_smartbase_admin-1.0.42.dist-info}/LICENSE.md +0 -0
@@ -1,6 +1,17 @@
1
+ from enum import Enum
2
+
1
3
  from django.template.defaultfilters import date, time
2
- from django.utils.translation import gettext_lazy as _
3
4
  from django.utils import timezone
5
+ from django.utils.html import format_html, format_html_join
6
+ from django.utils.safestring import mark_safe
7
+ from django.utils.translation import gettext_lazy as _
8
+
9
+
10
+ class BadgeType(Enum):
11
+ SUCCESS = "positive"
12
+ NOTICE = "notice"
13
+ WARNING = "warning"
14
+ ERROR = "negative"
4
15
 
5
16
 
6
17
  def datetime_formatter(object_id, value):
@@ -29,19 +40,26 @@ def datetime_formatter_with_format(date_format=None, time_format=None):
29
40
 
30
41
  def boolean_formatter(object_id, value):
31
42
  if value:
32
- return f'<span class="badge badge-simple badge-positive">{_("Yes")}</span>'
33
- return f'<span class="badge badge-simple badge-neutral">{_("No")}</span>'
43
+ return format_html(
44
+ '<span class="badge badge-simple badge-positive">{}</span>', _("Yes")
45
+ )
46
+ return format_html(
47
+ '<span class="badge badge-simple badge-neutral">{}</span>', _("No")
48
+ )
34
49
 
35
50
 
36
- def format_array(value_list, separator=""):
37
- result = ""
51
+ def format_array(value_list, separator="", badge_type: BadgeType = BadgeType.NOTICE):
38
52
  if not value_list:
39
- return result
40
- for value in value_list:
41
- if not value:
42
- continue
43
- result += f'<span class="badge badge-simple badge-notice mr-4">{value}</span>{separator}'
44
- return 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
+ )
45
63
 
46
64
 
47
65
  def array_badge_formatter(object_id, value_list):
@@ -49,12 +67,18 @@ def array_badge_formatter(object_id, value_list):
49
67
 
50
68
 
51
69
  def newline_separated_array_badge_formatter(object_id, value_list):
52
- return format_array(value_list, separator="<br>")
70
+ return format_html("<div>{}</div>", format_array(value_list, separator="<br>"))
53
71
 
54
72
 
55
73
  def rich_text_formatter(object_id, value):
56
- return 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 "",
78
+ )
57
79
 
58
80
 
59
81
  def link_formatter(object_id, value):
60
- return f'<a href="{value}">{value}</a>'
82
+ if not value:
83
+ return ""
84
+ return format_html('<a href="{0}">{0}</a>', value)
@@ -2,10 +2,11 @@ import json
2
2
  from datetime import datetime, timedelta
3
3
 
4
4
  from django.core.exceptions import ImproperlyConfigured
5
+ from django.contrib.postgres.fields import ArrayField
5
6
  from django.db.models import Q, fields, FilteredRelation, Count
6
7
  from django.http import JsonResponse
7
8
  from django.utils import timezone
8
- from django.utils.translation import gettext_lazy as _
9
+ from django.utils.translation import gettext_lazy as _, pgettext_lazy
9
10
 
10
11
  from django_smartbase_admin.actions.advanced_filters import (
11
12
  AllOperators,
@@ -20,6 +21,7 @@ from django_smartbase_admin.engine.const import (
20
21
  Action,
21
22
  AUTOCOMPLETE_PAGE_NUM,
22
23
  AUTOCOMPLETE_FORWARD_NAME,
24
+ SELECT_ALL_KEYWORD,
23
25
  )
24
26
  from django_smartbase_admin.services.translations import SBAdminTranslationsService
25
27
  from django_smartbase_admin.services.views import SBAdminViewService
@@ -44,6 +46,22 @@ class AutocompleteParseMixin:
44
46
  value = input_value
45
47
  return value
46
48
 
49
+ def parse_is_create_from_input(self, request, input_value):
50
+ try:
51
+ input_value = json.loads(input_value)
52
+ except:
53
+ pass
54
+ if isinstance(input_value, list):
55
+ value = []
56
+ for data in input_value:
57
+ if type(data) is dict:
58
+ value.append(data.get("create", False))
59
+ else:
60
+ value.append(False)
61
+ else:
62
+ value = False
63
+ return value
64
+
47
65
 
48
66
  class SBAdminFilterWidget(JSONSerializableMixin):
49
67
  template_name = None
@@ -57,6 +75,11 @@ class SBAdminFilterWidget(JSONSerializableMixin):
57
75
  default_label = None
58
76
  filter_query_lambda = None
59
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
60
83
 
61
84
  def __init__(
62
85
  self,
@@ -65,6 +88,8 @@ class SBAdminFilterWidget(JSONSerializableMixin):
65
88
  default_label=None,
66
89
  filter_query_lambda=None,
67
90
  exclude_null_operators=None,
91
+ close_dropdown_on_change=None,
92
+ allow_clear=None,
68
93
  **kwargs,
69
94
  ) -> None:
70
95
  super().__init__()
@@ -75,6 +100,10 @@ class SBAdminFilterWidget(JSONSerializableMixin):
75
100
  self.exclude_null_operators = (
76
101
  exclude_null_operators or self.exclude_null_operators
77
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
78
107
 
79
108
  def init_filter_widget_static(self, field, view, configuration):
80
109
  self.field = field
@@ -106,7 +135,9 @@ class SBAdminFilterWidget(JSONSerializableMixin):
106
135
  return original_query
107
136
 
108
137
  def to_json(self):
109
- return {"input_id": self.input_id}
138
+ return {
139
+ "input_id": self.input_id,
140
+ }
110
141
 
111
142
  def get_default_value(self):
112
143
  return self.default_value
@@ -144,6 +175,7 @@ class SBAdminFilterWidget(JSONSerializableMixin):
144
175
 
145
176
  class StringFilterWidget(SBAdminFilterWidget):
146
177
  template_name = "sb_admin/filter_widgets/string_field.html"
178
+ close_dropdown_on_change = True
147
179
 
148
180
  def get_advanced_filter_operators(self):
149
181
  return STRING_ATTRIBUTES
@@ -160,6 +192,29 @@ class StringFilterWidget(SBAdminFilterWidget):
160
192
 
161
193
  class BooleanFilterWidget(SBAdminFilterWidget):
162
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")))
163
218
 
164
219
  def parse_value_from_input(self, request, filter_value):
165
220
  input_value = super().parse_value_from_input(request, filter_value)
@@ -183,6 +238,7 @@ class BooleanFilterWidget(SBAdminFilterWidget):
183
238
  class ChoiceFilterWidget(SBAdminFilterWidget):
184
239
  template_name = "sb_admin/filter_widgets/choice_field.html"
185
240
  choices = None
241
+ close_dropdown_on_change = True
186
242
 
187
243
  def __init__(
188
244
  self,
@@ -211,17 +267,51 @@ class ChoiceFilterWidget(SBAdminFilterWidget):
211
267
  return found_label[0] if found_label else default_value
212
268
 
213
269
  def get_base_filter_query_for_parsed_value(self, request, filter_value):
270
+ if isinstance(self.model_field, ArrayField):
271
+ return Q(**{f"{self.field.filter_field}__contains": [filter_value]})
214
272
  return Q(**{self.field.filter_field: filter_value})
215
273
 
216
274
 
217
275
  class RadioChoiceFilterWidget(ChoiceFilterWidget):
218
276
  template_name = "sb_admin/filter_widgets/radio_choice_field.html"
277
+ close_dropdown_on_change = True
219
278
 
220
279
 
221
280
  class MultipleChoiceFilterWidget(AutocompleteParseMixin, ChoiceFilterWidget):
222
281
  template_name = "sb_admin/filter_widgets/multiple_choice_field.html"
282
+ enable_select_all = False
283
+ select_all_keyword = None
284
+ select_all_label = None
285
+ close_dropdown_on_change = False
286
+
287
+ def __init__(
288
+ self,
289
+ choices,
290
+ template_name=None,
291
+ default_value=None,
292
+ default_label=None,
293
+ enable_select_all=False,
294
+ select_all_keyword=SELECT_ALL_KEYWORD,
295
+ select_all_label=_("All"),
296
+ **kwargs,
297
+ ) -> None:
298
+ super().__init__(
299
+ choices=choices,
300
+ template_name=template_name,
301
+ default_value=default_value,
302
+ default_label=default_label,
303
+ **kwargs,
304
+ )
305
+ self.enable_select_all = enable_select_all
306
+ self.select_all_keyword = select_all_keyword
307
+ self.select_all_label = select_all_label
223
308
 
224
309
  def get_base_filter_query_for_parsed_value(self, request, filter_value):
310
+ if isinstance(self.model_field, ArrayField):
311
+ q_objects = Q()
312
+ for value in filter_value:
313
+ q_objects |= Q(**{f"{self.field.filter_field}__contains": [value]})
314
+ return q_objects
225
315
  return Q(**{f"{self.field.filter_field}__in": filter_value})
226
316
 
227
317
  def get_advanced_filter_operators(self):
@@ -284,6 +374,7 @@ class DateFilterWidget(SBAdminFilterWidget):
284
374
  "label": _("Last 12 months"),
285
375
  },
286
376
  ]
377
+ shortcuts_dict = {AllOperators.IN_THE_LAST.value: shortcuts}
287
378
  default_value_shortcut_index = None
288
379
 
289
380
  def __init__(
@@ -306,33 +397,43 @@ class DateFilterWidget(SBAdminFilterWidget):
306
397
  def get_advanced_filter_operators(self):
307
398
  return DATE_ATTRIBUTES
308
399
 
400
+ def process_shortcut(self, shortcut, now):
401
+ return shortcut
402
+
309
403
  def get_shortcuts(self):
310
404
  now = timezone.now()
311
405
  shortcuts = []
312
406
  for shortcut in self.shortcuts:
313
- shortcuts.append(
314
- {
315
- "label": shortcut["label"],
316
- "value": [
317
- now + timedelta(days=shortcut["value"][0]),
318
- now + timedelta(days=shortcut["value"][1]),
319
- ],
320
- }
321
- )
407
+ shortcuts.append(self.process_shortcut(shortcut, now))
322
408
  return shortcuts
323
409
 
410
+ def get_shortcuts_dict(self):
411
+ now = timezone.now()
412
+ shortcuts = {}
413
+ for key, shortcuts_group in self.shortcuts_dict.items():
414
+ shortcuts[key] = []
415
+ for shortcut in shortcuts_group:
416
+ shortcuts[key].append(self.process_shortcut(shortcut, now))
417
+ return shortcuts
418
+
419
+ def get_default_label(self):
420
+ if self.default_value_shortcut_index is not None:
421
+ return self.get_shortcuts()[self.default_value_shortcut_index]["label"]
422
+ return super().get_default_value()
423
+
324
424
  def get_default_value(self):
325
425
  if self.default_value_shortcut_index is not None:
326
426
  selected_shortcut_value = self.get_shortcuts()[
327
427
  self.default_value_shortcut_index
328
428
  ]["value"]
329
- return self.get_value_from_date_or_range(selected_shortcut_value)
429
+ return SBAdminViewService.json_dumps_for_url(
430
+ self.get_value_from_date_or_range(selected_shortcut_value)
431
+ )
330
432
  return super().get_default_value()
331
433
 
332
434
  def get_data(self):
333
435
  return json.dumps(
334
436
  {
335
- "shortcuts": self.get_shortcuts(),
336
437
  "flatpickrOptions": {
337
438
  "locale": {
338
439
  "rangeSeparator": self.DATE_RANGE_SPLIT,
@@ -342,6 +443,19 @@ class DateFilterWidget(SBAdminFilterWidget):
342
443
  cls=SBAdminJSONEncoder,
343
444
  )
344
445
 
446
+ def get_shortcuts_data(self):
447
+ return json.dumps(
448
+ self.get_shortcuts(),
449
+ cls=SBAdminJSONEncoder,
450
+ )
451
+
452
+ def get_shortcuts_dict_data(self):
453
+ # used for advanced filters with different calendar operators "in the last", "in the next", etc.
454
+ return json.dumps(
455
+ self.get_shortcuts_dict(),
456
+ cls=SBAdminJSONEncoder,
457
+ )
458
+
345
459
  @classmethod
346
460
  def is_used_for_model_field_type(cls, model_field):
347
461
  return isinstance(model_field, fields.DateField)
@@ -356,12 +470,28 @@ class DateFilterWidget(SBAdminFilterWidget):
356
470
  if filter_value is None:
357
471
  return [None, None]
358
472
  date_format = cls.DATE_FORMAT
359
- date_range = filter_value.split(cls.DATE_RANGE_SPLIT)
360
- if len(date_range) == 2:
361
- date_from = datetime.strptime(date_range[0], date_format)
362
- date_to = datetime.strptime(date_range[1], date_format)
363
- return [date_from, date_to]
364
- else:
473
+ if type(filter_value) is list:
474
+ if type(filter_value[0]) is int:
475
+ return [
476
+ timezone.now() + timedelta(days=filter_value[0]),
477
+ timezone.now() + timedelta(days=filter_value[1]),
478
+ ]
479
+ return [
480
+ datetime.strptime(filter_value[0], date_format),
481
+ datetime.strptime(filter_value[1], date_format),
482
+ ]
483
+ try:
484
+ days_range = json.loads(filter_value)
485
+ return [
486
+ timezone.now() + timedelta(days=days_range[0]),
487
+ timezone.now() + timedelta(days=days_range[1]),
488
+ ]
489
+ except json.decoder.JSONDecodeError:
490
+ date_range = filter_value.split(cls.DATE_RANGE_SPLIT)
491
+ if len(date_range) == 2:
492
+ date_from = datetime.strptime(date_range[0], date_format)
493
+ date_to = datetime.strptime(date_range[1], date_format)
494
+ return [date_from, date_to]
365
495
  date_value = datetime.strptime(filter_value, date_format)
366
496
  return [date_value, date_value]
367
497
 
@@ -369,9 +499,11 @@ class DateFilterWidget(SBAdminFilterWidget):
369
499
  def get_value_from_date_or_range(cls, date_or_range):
370
500
  if not isinstance(date_or_range, list):
371
501
  return datetime.strftime(date_or_range, cls.DATE_FORMAT)
502
+ if type(date_or_range[0]) is int:
503
+ return date_or_range
372
504
  date_from = datetime.strftime(date_or_range[0], cls.DATE_FORMAT)
373
505
  date_to = datetime.strftime(date_or_range[1], cls.DATE_FORMAT)
374
- return f"{date_from}{cls.DATE_RANGE_SPLIT}{date_to}"
506
+ return [date_from, date_to]
375
507
 
376
508
  def parse_value_from_input(self, request, filter_value):
377
509
  return self.get_range_from_value(filter_value)
@@ -400,9 +532,9 @@ class AutocompleteFilterWidget(
400
532
  forward = None
401
533
  label_lambda = None
402
534
  value_lambda = None
403
- allow_add = False
404
535
  hide_clear_button = False
405
536
  search_query_lambda = None
537
+ create_value_field = None
406
538
 
407
539
  def get_field_name(self):
408
540
  return self.field.name
@@ -422,7 +554,6 @@ class AutocompleteFilterWidget(
422
554
  value_lambda=None,
423
555
  multiselect=None,
424
556
  forward=None,
425
- allow_add=None,
426
557
  hide_clear_button=None,
427
558
  search_query_lambda=None,
428
559
  **kwargs,
@@ -431,14 +562,15 @@ class AutocompleteFilterWidget(
431
562
  self.model = model or self.model
432
563
  self.value_field = value_field or self.value_field
433
564
  self.filter_query_lambda = filter_query_lambda or self.filter_query_lambda
565
+ # filters queryset to search in
434
566
  self.filter_search_lambda = filter_search_lambda or self.filter_search_lambda
567
+ # defines fields to search on
435
568
  self.search_query_lambda = search_query_lambda or self.search_query_lambda
436
569
  self.label_lambda = label_lambda or self.label_lambda
437
570
  self.value_lambda = value_lambda or self.value_lambda
438
571
  self.multiselect = multiselect if multiselect is not None else self.multiselect
439
572
  self.multiselect = self.multiselect if self.multiselect is not None else True
440
573
  self.forward = forward or self.forward
441
- self.allow_add = allow_add or self.allow_add
442
574
  self.hide_clear_button = (
443
575
  hide_clear_button
444
576
  if hide_clear_button is not None
@@ -547,8 +679,9 @@ class AutocompleteFilterWidget(
547
679
  def get_value_field(self):
548
680
  return self.value_field or self.model._meta.pk.name
549
681
 
550
- def filter_search_queryset(self, request, qs, search_term, forward_data):
682
+ def filter_search_queryset(self, request, qs, search_term="", forward_data=None):
551
683
  if self.filter_search_lambda:
684
+ forward_data = forward_data or {}
552
685
  qs = qs.filter(
553
686
  self.filter_search_lambda(request, search_term, forward_data)
554
687
  )
@@ -560,9 +693,16 @@ class AutocompleteFilterWidget(
560
693
  page_num = int(post_data.get(AUTOCOMPLETE_PAGE_NUM, 1))
561
694
  from_item = (page_num - 1) * AUTOCOMPLETE_PAGE_SIZE
562
695
  to_item = (page_num) * AUTOCOMPLETE_PAGE_SIZE
696
+
697
+ # filter queryset
698
+ # base restricted queryset
563
699
  qs = self.get_queryset(request)
700
+ # filters queryset to search in, uses filter_search_lambda
564
701
  qs = self.filter_search_queryset(request, qs, search_term, forward_data)
702
+
703
+ # search in queryset
565
704
  if self.search_query_lambda:
705
+ # defines fields to search on
566
706
  qs = self.search_query_lambda(
567
707
  request,
568
708
  qs,
@@ -571,6 +711,7 @@ class AutocompleteFilterWidget(
571
711
  SBAdminTranslationsService.get_main_lang_code(),
572
712
  )
573
713
  else:
714
+ # defines default fields to search on - all char fields
574
715
  qs = self.get_default_search_query(
575
716
  request,
576
717
  qs,
@@ -598,6 +739,8 @@ class AutocompleteFilterWidget(
598
739
  def get_label(self, request, item):
599
740
  if self.label_lambda:
600
741
  return self.label_lambda(request, item)
742
+ if isinstance(item, list):
743
+ return ", ".join(map(str, item))
601
744
  return str(item)
602
745
 
603
746
  def get_context(self, name, value, attrs):
@@ -657,3 +800,184 @@ class FromValuesAutocompleteWidget(AutocompleteFilterWidget):
657
800
 
658
801
  def get_label(self, request, item):
659
802
  return item.get(self.field.name)
803
+
804
+
805
+ class SBAdminTreeWidgetMixin:
806
+ order_by = None
807
+ inline = False
808
+ RELATIONSHIP_PICK_MODE_NONE = None
809
+ RELATIONSHIP_PICK_MODE_PARENT = "parent"
810
+ relationship_pick_mode = RELATIONSHIP_PICK_MODE_NONE
811
+ additional_columns = None
812
+ tree_strings = {
813
+ "loading": pgettext_lazy("Tree widget", "Loading..."),
814
+ "loadError": pgettext_lazy("Tree widget", "Load error!"),
815
+ "moreData": pgettext_lazy("Tree widget", "More..."),
816
+ "noData": pgettext_lazy("Tree widget", "No data."),
817
+ }
818
+ model = None
819
+ path_field = "path"
820
+
821
+ def __init__(
822
+ self,
823
+ order_by=None,
824
+ relationship_pick_mode=None,
825
+ inline=None,
826
+ additional_columns=None,
827
+ tree_strings=None,
828
+ *args,
829
+ **kwargs,
830
+ ):
831
+ self.inline = inline if inline is not None else self.inline
832
+ self.order_by = order_by if order_by is not None else self.order_by
833
+ self.relationship_pick_mode = relationship_pick_mode
834
+ self.additional_columns = (
835
+ additional_columns
836
+ if additional_columns is not None
837
+ else self.additional_columns
838
+ )
839
+ self.tree_strings = (
840
+ tree_strings if tree_strings is not None else self.tree_strings
841
+ )
842
+ if self.inline:
843
+ self.template_name = "sb_admin/widgets/tree_select_inline.html"
844
+ super().__init__(*args, **kwargs)
845
+
846
+ def action_autocomplete(self, request, modifier):
847
+ result = self.format_tree_data(request, self.get_queryset(request))
848
+ return JsonResponse(data=result, safe=False)
849
+
850
+ def format_tree_data(self, request, queryset):
851
+ self_id = None
852
+ if self.relationship_pick_mode == self.RELATIONSHIP_PICK_MODE_PARENT:
853
+ # disable selecting self and children if selecting parent
854
+ self_id = self.form.instance.id if self.form.instance else None
855
+ return self.get_tree_data(request, queryset, self_id=self_id)
856
+
857
+ @classmethod
858
+ def get_tree_base_values(cls):
859
+ return ["id", cls.path_field]
860
+
861
+ @classmethod
862
+ def get_tree_key(cls, request, item):
863
+ return item.get(cls.path_field)
864
+
865
+ @classmethod
866
+ def get_tree_title(cls, request, item):
867
+ raise NotImplementedError
868
+
869
+ @classmethod
870
+ def get_value(cls, request, item):
871
+ return getattr(item, cls.path_field)
872
+
873
+ @classmethod
874
+ def get_label(cls, request, item):
875
+ raise NotImplementedError
876
+
877
+ @classmethod
878
+ def tree_process_global_data(cls, request, queryset, **kwargs):
879
+ return {}
880
+
881
+ @classmethod
882
+ def get_additional_data(cls, request, item, tree_process_global_data):
883
+ return {}
884
+
885
+ @classmethod
886
+ def get_tree_data(cls, request, queryset, values=None, self_id=None, **kwargs):
887
+ tree_values = cls.get_tree_base_values()
888
+ tree_values.extend(values if values else [])
889
+
890
+ queryset = queryset.order_by(*cls.order_by)
891
+ queryset = queryset.annotate(
892
+ **SBAdminViewService.get_annotates(cls.model, tree_values, [])
893
+ )
894
+ flat_data = []
895
+ tree_data, lnk = [], {}
896
+ tree_process_global_data = cls.tree_process_global_data(
897
+ request, queryset, **kwargs
898
+ )
899
+
900
+ data = list(queryset.values(*tree_values))
901
+ for item in data:
902
+ path = item.get("path")
903
+ depth = int(len(path) / cls.model.steplen)
904
+ item_id = cls.get_tree_key(request, item)
905
+ item_label = cls.get_tree_title(request, item)
906
+ newobj = {
907
+ "title": item_label,
908
+ "key": str(item_id),
909
+ "data": {"id": item.get("id")},
910
+ }
911
+ if item_id == self_id:
912
+ # disable selecting self and children if selecting parent
913
+ newobj["checkbox"] = False
914
+
915
+ additional_data = cls.get_additional_data(
916
+ request, item, tree_process_global_data
917
+ )
918
+ newobj.update(additional_data)
919
+
920
+ if depth == 1:
921
+ tree_data.append(newobj)
922
+ flat_data.append(newobj)
923
+ else:
924
+ parentpath = cls.model._get_basepath(path, depth - 1)
925
+ parentobj = lnk[parentpath]
926
+ if "children" not in parentobj:
927
+ parentobj["children"] = []
928
+ if parentobj.get("checkbox") is False:
929
+ # disable selecting self and children if selecting parent
930
+ newobj["checkbox"] = False
931
+ parentobj["children"].append(newobj)
932
+ flat_data.append(newobj)
933
+ lnk[path] = newobj
934
+ return tree_data
935
+
936
+ # tree_widget_data: [{"key":"path", "children": [{...}]}]
937
+ @classmethod
938
+ def process_treebeard_tree(
939
+ cls,
940
+ tree_widget_data,
941
+ treebeard_objs_by_path,
942
+ depth=1,
943
+ parent_path="",
944
+ path_base="",
945
+ ):
946
+ if not path_base:
947
+ path_base = ((cls.model.steplen - 1) * "0") + "1"
948
+ previous = None
949
+ objs_to_update = []
950
+ for tree_widget_node in tree_widget_data:
951
+ treebeard_obj = treebeard_objs_by_path.get(tree_widget_node["key"])
952
+ old_depth = treebeard_obj.depth
953
+ old_path = getattr(treebeard_obj, cls.path_field)
954
+ old_numchild = treebeard_obj.numchild
955
+ treebeard_obj.depth = depth
956
+ if not previous:
957
+ previous = treebeard_obj
958
+ setattr(treebeard_obj, cls.path_field, parent_path + path_base)
959
+ else:
960
+ setattr(treebeard_obj, cls.path_field, previous._inc_path())
961
+ previous = treebeard_obj
962
+ children = tree_widget_node.get("children", [])
963
+ treebeard_obj.numchild = len(children)
964
+ if (
965
+ treebeard_obj.depth != old_depth
966
+ or getattr(treebeard_obj, cls.path_field) != old_path
967
+ or treebeard_obj.numchild != old_numchild
968
+ ):
969
+ objs_to_update.append(treebeard_obj)
970
+ objs_to_update.extend(
971
+ cls.process_treebeard_tree(
972
+ children,
973
+ treebeard_objs_by_path,
974
+ depth + 1,
975
+ getattr(treebeard_obj, cls.path_field),
976
+ path_base,
977
+ )
978
+ )
979
+ return objs_to_update
980
+
981
+
982
+ class SBAdminTreeFilterWidget(SBAdminTreeWidgetMixin, AutocompleteFilterWidget):
983
+ template_name = "sb_admin/filter_widgets/tree_select_filter.html"
@@ -56,11 +56,14 @@ class SBAdminMenuItem(object):
56
56
  return self.label or self.view.get_menu_label()
57
57
 
58
58
  def get_url(self, request):
59
- return (
60
- self.url
61
- or (self.view.get_menu_view_url(request) if self.view else None)
62
- or ""
63
- )
59
+ if callable(self.url):
60
+ return self.url(request)
61
+ elif self.url:
62
+ return self.url
63
+ elif self.view:
64
+ return self.view.get_menu_view_url(request)
65
+ else:
66
+ return ""
64
67
 
65
68
  def get_icon(self):
66
69
  return self.icon or getattr(self.view, "icon", None)