django-smartbase-admin 0.2.54__py3-none-any.whl → 1.0.38__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 (179) hide show
  1. django_smartbase_admin/actions/admin_action_list.py +74 -38
  2. django_smartbase_admin/actions/advanced_filters.py +24 -1
  3. django_smartbase_admin/admin/admin_base.py +401 -96
  4. django_smartbase_admin/admin/site.py +93 -35
  5. django_smartbase_admin/admin/widgets.py +589 -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 +44 -23
  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 +22 -8
  15. django_smartbase_admin/engine/filter_widgets.py +309 -20
  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/sprites/sb_admin/Bolt-one.svg +3 -0
  65. django_smartbase_admin/static/sb_admin/sprites/sb_admin/Calendar.svg +3 -0
  66. django_smartbase_admin/static/sb_admin/sprites/sb_admin/Caution.svg +3 -0
  67. django_smartbase_admin/static/sb_admin/sprites/sb_admin/Electric-drill.svg +3 -0
  68. django_smartbase_admin/static/sb_admin/sprites/sb_admin/Fire-extinguisher.svg +3 -0
  69. django_smartbase_admin/static/sb_admin/sprites/sb_admin/Gas.svg +3 -0
  70. django_smartbase_admin/static/sb_admin/sprites/sb_admin/Lightning-fill.svg +3 -0
  71. django_smartbase_admin/static/sb_admin/sprites/sb_admin/Moon.svg +3 -0
  72. django_smartbase_admin/static/sb_admin/sprites/sb_admin/Phone-telephone.svg +3 -0
  73. django_smartbase_admin/static/sb_admin/sprites/sb_admin/Printer.svg +3 -0
  74. django_smartbase_admin/static/sb_admin/sprites/sb_admin/Pull.svg +3 -0
  75. django_smartbase_admin/static/sb_admin/sprites/sb_admin/Sun-one.svg +3 -0
  76. django_smartbase_admin/static/sb_admin/sprites/sb_admin/Time.svg +3 -0
  77. django_smartbase_admin/static/sb_admin/src/css/_base.css +5 -1
  78. django_smartbase_admin/static/sb_admin/src/css/_colors.css +257 -82
  79. django_smartbase_admin/static/sb_admin/src/css/_components.css +61 -13
  80. django_smartbase_admin/static/sb_admin/src/css/_datepicker.css +8 -1
  81. django_smartbase_admin/static/sb_admin/src/css/_filer.css +60 -0
  82. django_smartbase_admin/static/sb_admin/src/css/_inlines.css +51 -10
  83. django_smartbase_admin/static/sb_admin/src/css/_tabulator.css +8 -2
  84. django_smartbase_admin/static/sb_admin/src/css/calendar.css +162 -0
  85. django_smartbase_admin/static/sb_admin/src/css/components/_button.css +41 -1
  86. django_smartbase_admin/static/sb_admin/src/css/components/_dropdown.css +26 -8
  87. django_smartbase_admin/static/sb_admin/src/css/components/_input.css +62 -20
  88. django_smartbase_admin/static/sb_admin/src/css/components/_modal.css +1 -1
  89. django_smartbase_admin/static/sb_admin/src/css/components/_query-builder.css +21 -2
  90. django_smartbase_admin/static/sb_admin/src/css/components/_toggle.css +12 -1
  91. django_smartbase_admin/static/sb_admin/src/css/components/_tooltip.css +8 -22
  92. django_smartbase_admin/static/sb_admin/src/css/style.css +17 -0
  93. django_smartbase_admin/static/sb_admin/src/css/tree_widget.css +411 -0
  94. django_smartbase_admin/static/sb_admin/src/js/autocomplete.js +63 -5
  95. django_smartbase_admin/static/sb_admin/src/js/calendar.js +56 -0
  96. django_smartbase_admin/static/sb_admin/src/js/chart.js +8 -22
  97. django_smartbase_admin/static/sb_admin/src/js/choices.js +18 -8
  98. django_smartbase_admin/static/sb_admin/src/js/datepicker.js +97 -336
  99. django_smartbase_admin/static/sb_admin/src/js/datepicker_plugins.js +357 -0
  100. django_smartbase_admin/static/sb_admin/src/js/main.js +304 -31
  101. django_smartbase_admin/static/sb_admin/src/js/multiselect.js +50 -41
  102. django_smartbase_admin/static/sb_admin/src/js/range.js +3 -2
  103. django_smartbase_admin/static/sb_admin/src/js/table.js +34 -5
  104. django_smartbase_admin/static/sb_admin/src/js/table_modules/advanced_filter_module.js +43 -20
  105. django_smartbase_admin/static/sb_admin/src/js/table_modules/data_edit_module.js +8 -10
  106. django_smartbase_admin/static/sb_admin/src/js/table_modules/filter_module.js +3 -3
  107. django_smartbase_admin/static/sb_admin/src/js/table_modules/header_tabs_module.js +11 -11
  108. django_smartbase_admin/static/sb_admin/src/js/table_modules/selection_module.js +28 -8
  109. django_smartbase_admin/static/sb_admin/src/js/table_modules/table_params_module.js +6 -0
  110. django_smartbase_admin/static/sb_admin/src/js/table_modules/views_module.js +6 -0
  111. django_smartbase_admin/static/sb_admin/src/js/tree_widget.js +406 -0
  112. django_smartbase_admin/static/sb_admin/src/js/utils.js +56 -21
  113. django_smartbase_admin/templates/sb_admin/actions/change_form.html +169 -114
  114. django_smartbase_admin/templates/sb_admin/actions/dashboard.html +2 -2
  115. django_smartbase_admin/templates/sb_admin/actions/list.html +79 -39
  116. django_smartbase_admin/templates/sb_admin/actions/partials/action_link.html +14 -0
  117. django_smartbase_admin/templates/sb_admin/actions/partials/tabulator_header_v2.html +2 -2
  118. django_smartbase_admin/templates/sb_admin/actions/tree_list.html +63 -0
  119. django_smartbase_admin/templates/sb_admin/authentification/login_base.html +5 -1
  120. django_smartbase_admin/templates/sb_admin/components/columns.html +1 -1
  121. django_smartbase_admin/templates/sb_admin/components/filters_v2.html +99 -85
  122. django_smartbase_admin/templates/sb_admin/dashboard/calendar_widget.html +69 -0
  123. django_smartbase_admin/templates/sb_admin/dashboard/chart_widget.html +21 -2
  124. django_smartbase_admin/templates/sb_admin/dashboard/list_widget.html +6 -0
  125. django_smartbase_admin/templates/sb_admin/dashboard/widget_base.html +1 -1
  126. django_smartbase_admin/templates/sb_admin/filter_widgets/advanced_filters/date_field.html +18 -8
  127. django_smartbase_admin/templates/sb_admin/filter_widgets/advanced_filters/multiple_choice_field.html +1 -1
  128. django_smartbase_admin/templates/sb_admin/filter_widgets/advanced_filters/tree_select_filter.html +2 -0
  129. django_smartbase_admin/templates/sb_admin/filter_widgets/date_field.html +18 -4
  130. django_smartbase_admin/templates/sb_admin/filter_widgets/multiple_choice_field.html +14 -0
  131. django_smartbase_admin/templates/sb_admin/filter_widgets/partials/clear.html +10 -5
  132. django_smartbase_admin/templates/sb_admin/filter_widgets/radio_choice_field.html +2 -2
  133. django_smartbase_admin/templates/sb_admin/filter_widgets/tree_select_filter.html +16 -0
  134. django_smartbase_admin/templates/sb_admin/includes/change_form_title.html +3 -1
  135. django_smartbase_admin/templates/sb_admin/includes/inline_fieldset.html +48 -39
  136. django_smartbase_admin/templates/sb_admin/includes/notifications.html +2 -1
  137. django_smartbase_admin/templates/sb_admin/includes/readonly_boolean_field.html +9 -0
  138. django_smartbase_admin/templates/sb_admin/includes/readonly_field.html +12 -0
  139. django_smartbase_admin/templates/sb_admin/includes/table_inline_delete_button.html +4 -5
  140. django_smartbase_admin/templates/sb_admin/inlines/stacked_inline.html +68 -40
  141. django_smartbase_admin/templates/sb_admin/inlines/table_inline.html +76 -34
  142. django_smartbase_admin/templates/sb_admin/integrations/filer/folder_list.html +18 -0
  143. django_smartbase_admin/templates/sb_admin/navigation.html +166 -158
  144. django_smartbase_admin/templates/sb_admin/partials/modal/modal_content.html +2 -6
  145. django_smartbase_admin/templates/sb_admin/sb_admin_base.html +49 -4
  146. django_smartbase_admin/templates/sb_admin/sb_admin_base_no_sidebar.html +27 -11
  147. django_smartbase_admin/templates/sb_admin/sb_admin_js_trans.html +3 -0
  148. django_smartbase_admin/templates/sb_admin/sprites/sb_admin.svg +1 -1
  149. django_smartbase_admin/templates/sb_admin/tailwind_whitelist.html +6 -3
  150. django_smartbase_admin/templates/sb_admin/widgets/array.html +0 -1
  151. django_smartbase_admin/templates/sb_admin/widgets/attributes.html +68 -0
  152. django_smartbase_admin/templates/sb_admin/widgets/autocomplete.html +13 -2
  153. django_smartbase_admin/templates/sb_admin/widgets/{checkbox_select.html → checkbox_dropdown.html} +2 -2
  154. django_smartbase_admin/templates/sb_admin/widgets/clearable_file_input.html +2 -2
  155. django_smartbase_admin/templates/sb_admin/widgets/color_field.html +30 -0
  156. django_smartbase_admin/templates/sb_admin/widgets/date.html +8 -1
  157. django_smartbase_admin/templates/sb_admin/widgets/filer_file.html +84 -0
  158. django_smartbase_admin/templates/sb_admin/widgets/includes/related_item_buttons.html +38 -0
  159. django_smartbase_admin/templates/sb_admin/widgets/multiwidget.html +1 -1
  160. django_smartbase_admin/templates/sb_admin/widgets/radio.html +3 -2
  161. django_smartbase_admin/templates/sb_admin/widgets/radio_dropdown.html +30 -0
  162. django_smartbase_admin/templates/sb_admin/widgets/read_only_password_hash.html +3 -0
  163. django_smartbase_admin/templates/sb_admin/widgets/time.html +8 -1
  164. django_smartbase_admin/templates/sb_admin/widgets/toggle.html +1 -1
  165. django_smartbase_admin/templates/sb_admin/widgets/tree_base.html +59 -0
  166. django_smartbase_admin/templates/sb_admin/widgets/tree_select.html +24 -0
  167. django_smartbase_admin/templates/sb_admin/widgets/tree_select_inline.html +12 -0
  168. django_smartbase_admin/templatetags/sb_admin_tags.py +85 -4
  169. django_smartbase_admin/utils.py +22 -3
  170. django_smartbase_admin/views/dashboard_view.py +6 -0
  171. django_smartbase_admin/views/global_filter_view.py +8 -2
  172. django_smartbase_admin/views/translations_view.py +12 -5
  173. django_smartbase_admin/views/user_config_view.py +52 -0
  174. django_smartbase_admin-1.0.38.dist-info/METADATA +166 -0
  175. {django_smartbase_admin-0.2.54.dist-info → django_smartbase_admin-1.0.38.dist-info}/RECORD +177 -115
  176. {django_smartbase_admin-0.2.54.dist-info → django_smartbase_admin-1.0.38.dist-info}/WHEEL +1 -1
  177. django_smartbase_admin/templates/sb_admin/integrations/sorting/change_list.html +0 -401
  178. django_smartbase_admin-0.2.54.dist-info/METADATA +0 -25
  179. {django_smartbase_admin-0.2.54.dist-info → django_smartbase_admin-1.0.38.dist-info}/LICENSE.md +0 -0
@@ -1,18 +1,60 @@
1
1
  import json
2
+ import logging
3
+ import sys
2
4
 
3
5
  from ckeditor.widgets import CKEditorWidget
6
+ from ckeditor_uploader.widgets import CKEditorUploadingWidget
4
7
  from django import forms
8
+ from django.conf import settings
5
9
  from django.contrib.admin.widgets import (
6
10
  AdminURLFieldWidget,
11
+ ForeignKeyRawIdWidget,
7
12
  )
8
13
  from django.contrib.auth.forms import ReadOnlyPasswordHashWidget
9
- from django.utils.translation import gettext_lazy as _
14
+ from django.core.exceptions import ValidationError, ImproperlyConfigured
15
+ from django.template.loader import render_to_string
16
+ from django.urls import reverse
17
+ from django.utils.formats import get_format
18
+ from django.utils.http import urlencode
19
+ from django.utils.safestring import mark_safe
20
+ from django.utils.translation import gettext_lazy as _, get_language
10
21
  from django.views.generic.base import ContextMixin
22
+ from filer.fields.file import AdminFileWidget as FilerAdminFileWidget
11
23
  from filer.fields.image import AdminImageWidget
24
+ from filer.models import File
12
25
 
26
+ from django_smartbase_admin.admin.site import sb_admin_site
27
+ from django_smartbase_admin.engine.admin_base_view import (
28
+ SBADMIN_PARENT_INSTANCE_PK_VAR,
29
+ SBADMIN_PARENT_INSTANCE_LABEL_VAR,
30
+ )
13
31
  from django_smartbase_admin.engine.filter_widgets import (
14
32
  AutocompleteFilterWidget,
33
+ SBAdminTreeWidgetMixin,
34
+ )
35
+ from django_smartbase_admin.services.thread_local import SBAdminThreadLocalService
36
+ from django_smartbase_admin.templatetags.sb_admin_tags import (
37
+ SBAdminJSONEncoder,
15
38
  )
39
+ from django_smartbase_admin.utils import is_modal
40
+
41
+ try:
42
+ # Django >= 5.0
43
+ from django.contrib.admin.exceptions import NotRegistered
44
+ except ImportError:
45
+ from django.contrib.admin.sites import NotRegistered
46
+
47
+ logger = logging.getLogger(__name__)
48
+
49
+
50
+ def get_datetime_placeholder(lang=None):
51
+ lang = lang or get_language()
52
+ sb_admin_settings = getattr(settings, "SB_ADMIN_SETTINGS", {})
53
+ placeholder_setting = sb_admin_settings.get("DATETIME_PLACEHOLDER", {})
54
+ return placeholder_setting.get(
55
+ lang,
56
+ placeholder_setting.get("default", {"date": "mm.dd.yyyy", "time": "hh:mm"}),
57
+ )
16
58
 
17
59
 
18
60
  class SBAdminBaseWidget(ContextMixin):
@@ -28,6 +70,30 @@ class SBAdminBaseWidget(ContextMixin):
28
70
  def get_context(self, name, value, attrs):
29
71
  context = super().get_context(name, value, attrs)
30
72
  context["widget"]["form_field"] = self.form_field
73
+ opts = None
74
+
75
+ if self.form_field:
76
+ view = getattr(self.form_field, "view", None)
77
+ if view:
78
+ if hasattr(view, "opts"):
79
+ opts = view.opts
80
+ elif hasattr(view, "view") and hasattr(view.view, "opts"):
81
+ opts = view.view.opts
82
+
83
+ if opts:
84
+ modal_prefix = ""
85
+ try:
86
+ modal_prefix = (
87
+ "modal_"
88
+ if is_modal(SBAdminThreadLocalService.get_request())
89
+ else ""
90
+ )
91
+ except:
92
+ pass
93
+ widget_id = f"{modal_prefix}{opts.app_label}_{opts.model_name}_{context['widget']['attrs']['id']}"
94
+ context["widget"]["attrs"]["id"] = widget_id
95
+ # needed for BoundField.id_for_label to work correctly
96
+ self.attrs["id"] = widget_id
31
97
  return context
32
98
 
33
99
 
@@ -90,12 +156,28 @@ class SBAdminToggleWidget(SBAdminBaseWidget, forms.CheckboxInput):
90
156
 
91
157
  class SBAdminCKEditorWidget(SBAdminBaseWidget, CKEditorWidget):
92
158
 
93
- def __init__(self, form_field=None, attrs=None):
159
+ def __init__(
160
+ self,
161
+ config_name="default",
162
+ extra_plugins=None,
163
+ external_plugin_resources=None,
164
+ form_field=None,
165
+ attrs=None,
166
+ ):
94
167
  super().__init__(
95
- form_field, template_name="sb_admin/widgets/ckeditor.html", attrs=attrs
168
+ form_field,
169
+ template_name="sb_admin/widgets/ckeditor.html",
170
+ attrs=attrs,
171
+ config_name=config_name,
172
+ extra_plugins=extra_plugins,
173
+ external_plugin_resources=external_plugin_resources,
96
174
  )
97
175
 
98
176
 
177
+ class SBAdminCKEditorUploadingWidget(CKEditorUploadingWidget, SBAdminCKEditorWidget):
178
+ pass
179
+
180
+
99
181
  class SBAdminSelectWidget(SBAdminBaseWidget, forms.Select):
100
182
  template_name = "sb_admin/widgets/select.html"
101
183
  option_template_name = "sb_admin/widgets/select_option.html"
@@ -116,8 +198,20 @@ class SBAdminRadioWidget(SBAdminBaseWidget, forms.RadioSelect):
116
198
  )
117
199
 
118
200
 
201
+ class SBAdminRadioDropdownWidget(SBAdminBaseWidget, forms.RadioSelect):
202
+ template_name = "sb_admin/widgets/radio_dropdown.html"
203
+ option_template_name = "sb_admin/widgets/radio_option.html"
204
+
205
+ def __init__(self, form_field=None, attrs=None, choices=()):
206
+ super().__init__(
207
+ form_field,
208
+ attrs={"class": "radio radio-list", **(attrs or {})},
209
+ choices=choices,
210
+ )
211
+
212
+
119
213
  class SBAdminMultipleChoiceWidget(SBAdminBaseWidget, forms.CheckboxSelectMultiple):
120
- template_name = "sb_admin/widgets/checkbox_select.html"
214
+ template_name = "sb_admin/widgets/checkbox_dropdown.html"
121
215
  option_template_name = "sb_admin/widgets/checkbox_option.html"
122
216
 
123
217
  def __init__(self, form_field=None, attrs=None, choices=()):
@@ -146,7 +240,26 @@ class SBAdminDateWidget(SBAdminBaseWidget, forms.DateInput):
146
240
 
147
241
  def __init__(self, form_field=None, attrs=None):
148
242
  super().__init__(
149
- form_field, attrs={"class": "input js-datepicker", **(attrs or {})}
243
+ form_field,
244
+ format="%Y-%m-%d",
245
+ attrs={
246
+ "class": "input js-datepicker",
247
+ "data-sbadmin-datepicker": self.get_data(),
248
+ "placeholder": get_datetime_placeholder()["date"],
249
+ **(attrs or {}),
250
+ },
251
+ )
252
+
253
+ def get_data(self):
254
+ return json.dumps(
255
+ {
256
+ "flatpickrOptions": {
257
+ "dateFormat": "Y-m-d",
258
+ "altInput": True,
259
+ "altFormat": get_format("SHORT_DATE_FORMAT"),
260
+ },
261
+ },
262
+ cls=SBAdminJSONEncoder,
150
263
  )
151
264
 
152
265
 
@@ -155,7 +268,13 @@ class SBAdminTimeWidget(SBAdminBaseWidget, forms.TimeInput):
155
268
 
156
269
  def __init__(self, form_field=None, attrs=None):
157
270
  super().__init__(
158
- form_field, attrs={"class": "input js-timepicker", **(attrs or {})}
271
+ form_field,
272
+ attrs={
273
+ "class": "input js-timepicker",
274
+ "placeholder": get_datetime_placeholder()["time"],
275
+ "autocomplete": "do-not-autofill",
276
+ **(attrs or {}),
277
+ },
159
278
  )
160
279
 
161
280
 
@@ -209,6 +328,29 @@ class SBAdminArrayWidget(SBAdminTextInputWidget):
209
328
  return context
210
329
 
211
330
 
331
+ class SBAdminAttributesWidget(SBAdminTextInputWidget):
332
+ template_name = "sb_admin/widgets/attributes.html"
333
+
334
+ def get_context(self, name, value, attrs):
335
+ context = super().get_context(name, value, attrs)
336
+ widget = context.get("widget", None)
337
+ dict_widgets = []
338
+ template_widget = {"attrs": {"class": "input"}}
339
+ if widget and value:
340
+ if isinstance(value, str):
341
+ value = json.loads(value)
342
+ dict_widgets = [
343
+ {
344
+ "key": {"value": key, **template_widget},
345
+ "value": {"value": value, **template_widget},
346
+ }
347
+ for key, value in value.items()
348
+ ]
349
+ context["dict_widgets"] = dict_widgets
350
+ context["template_widget"] = template_widget
351
+ return context
352
+
353
+
212
354
  class SBAdminAutocompleteWidget(
213
355
  SBAdminBaseWidget, AutocompleteFilterWidget, forms.Widget
214
356
  ):
@@ -216,15 +358,30 @@ class SBAdminAutocompleteWidget(
216
358
  view = None
217
359
  form = None
218
360
  field_name = None
219
- threadsafe_request = None
220
361
  initialised = None
362
+ default_create_data = None
363
+ reload_on_save = None
364
+ REQUEST_CREATED_DATA_KEY = "autocomplete_created_data"
221
365
 
222
366
  def __init__(self, form_field=None, *args, **kwargs):
223
367
  attrs = kwargs.pop("attrs", None)
368
+ self.reload_on_save = kwargs.pop("reload_on_save", False)
224
369
  super().__init__(form_field, *args, **kwargs)
225
370
  self.attrs = {} if attrs is None else attrs.copy()
226
-
227
- def init_widget_dynamic(self, form, form_field, field_name, view, request):
371
+ if self.multiselect and self.allow_add:
372
+ raise ImproperlyConfigured(
373
+ "Multiselect with creation is currently not supported."
374
+ )
375
+
376
+ def get_id(self):
377
+ base_id = super().get_id()
378
+ if self.form:
379
+ base_id += f"_{self.form.__class__.__name__}"
380
+ return base_id
381
+
382
+ def init_widget_dynamic(
383
+ self, form, form_field, field_name, view, request, default_create_data=None
384
+ ):
228
385
  super().init_widget_dynamic(form, form_field, field_name, view, request)
229
386
  if self.initialised:
230
387
  return
@@ -232,11 +389,11 @@ class SBAdminAutocompleteWidget(
232
389
  self.field_name = field_name
233
390
  self.view = view
234
391
  self.form = form
235
- self.threadsafe_request = request
392
+ self.default_create_data = default_create_data or {}
236
393
  self.init_autocomplete_widget_static(
237
394
  self.field_name,
238
395
  self.model,
239
- self.threadsafe_request.request_data.configuration,
396
+ request.request_data.configuration,
240
397
  )
241
398
 
242
399
  def get_field_name(self):
@@ -247,6 +404,7 @@ class SBAdminAutocompleteWidget(
247
404
  self.input_id = (
248
405
  context["widget"]["attrs"]["id"] or f'id_{context["widget"]["name"]}'
249
406
  )
407
+
250
408
  context["widget"]["type"] = "hidden"
251
409
  context["widget"]["attrs"]["id"] = self.input_id
252
410
  context["widget"]["attrs"]["class"] = "js-autocomplete-detail"
@@ -254,38 +412,268 @@ class SBAdminAutocompleteWidget(
254
412
  getattr(self.form_field, "empty_label", "---------") or "---------"
255
413
  )
256
414
  query_suffix = "__in"
415
+ threadsafe_request = SBAdminThreadLocalService.get_request()
257
416
  if not self.is_multiselect():
258
417
  query_suffix = ""
259
418
  self.multiselect = False
419
+ context["widget"]["attrs"]["preselect_field"] = threadsafe_request.GET.get(
420
+ "sbadmin_parent_instance_field"
421
+ )
422
+ context["widget"]["attrs"]["preselect_field_label"] = (
423
+ threadsafe_request.GET.get(SBADMIN_PARENT_INSTANCE_LABEL_VAR)
424
+ )
425
+ context["widget"]["attrs"]["preselect_field_value"] = (
426
+ threadsafe_request.GET.get(SBADMIN_PARENT_INSTANCE_PK_VAR)
427
+ )
428
+ parsed_value = None
260
429
  if value:
261
- parsed_value = self.parse_value_from_input(self.threadsafe_request, value)
430
+ parsed_value = self.parse_value_from_input(threadsafe_request, value)
431
+ is_create = self.parse_is_create_from_input(
432
+ threadsafe_request,
433
+ threadsafe_request.request_data.request_post.get(name),
434
+ )
435
+ selected_options = []
436
+ if is_create:
437
+ errors = getattr(self.form, "errors", {})
438
+ if errors.get(self.field_name):
439
+ parsed_value = None
262
440
  if parsed_value:
263
- selected_options = []
264
- for item in self.get_queryset(self.threadsafe_request).filter(
265
- **{f"{self.get_value_field()}{query_suffix}": parsed_value}
266
- ):
267
- selected_options.append(
268
- {
269
- "value": self.get_value(self.threadsafe_request, item),
270
- "label": self.get_label(self.threadsafe_request, item),
271
- }
272
- )
273
- context["widget"]["value"] = json.dumps(selected_options)
274
- context["widget"]["value_list"] = selected_options
441
+ if self.is_multiselect() and not isinstance(parsed_value, list):
442
+ parsed_value = [parsed_value]
443
+
444
+ try:
445
+ for item in self.get_queryset(threadsafe_request).filter(
446
+ **{f"{self.get_value_field()}{query_suffix}": parsed_value}
447
+ ):
448
+ selected_options.append(
449
+ {
450
+ "value": self.get_value(threadsafe_request, item),
451
+ "label": self.get_label(threadsafe_request, item),
452
+ }
453
+ )
454
+ except ValueError as e:
455
+ new_object_id = threadsafe_request.request_data.additional_data.get(
456
+ self.REQUEST_CREATED_DATA_KEY, {}
457
+ ).get(self.field_name)
458
+ if new_object_id:
459
+ selected_options.append(
460
+ {
461
+ "value": new_object_id,
462
+ "label": value,
463
+ }
464
+ )
465
+ elif hasattr(self.form, "add_error"):
466
+ self.form.add_error(
467
+ self.field_name,
468
+ _(
469
+ "The new value was created but became unselected due to another validation error. Please select it again."
470
+ ),
471
+ )
472
+
473
+ context["widget"]["value"] = json.dumps(selected_options)
474
+ context["widget"]["value_list"] = selected_options
475
+
476
+ if (
477
+ threadsafe_request.request_data.configuration.autocomplete_show_related_buttons(
478
+ self.model,
479
+ field_name=self.field_name,
480
+ current_view=self.view,
481
+ request=threadsafe_request,
482
+ )
483
+ and not self.is_multiselect()
484
+ ):
485
+ self.add_related_buttons_urls(parsed_value, threadsafe_request, context)
486
+ context["reload_on_save"] = self.reload_on_save
487
+
275
488
  return context
276
489
 
490
+ def add_related_buttons_urls(self, parsed_value, request, context):
491
+ try:
492
+ if hasattr(sb_admin_site, "get_model_admin"):
493
+ # Django >= 5.0
494
+ related_model_admin = sb_admin_site.get_model_admin(self.model)
495
+ else:
496
+ related_model_admin = sb_admin_site._registry.get(self.model)
497
+ if not related_model_admin:
498
+ return
499
+ if parsed_value and related_model_admin.has_view_or_change_permission(
500
+ request
501
+ ):
502
+ context["widget"]["attrs"]["related_edit_url"] = (
503
+ related_model_admin.get_detail_url(parsed_value)
504
+ )
505
+ if related_model_admin.has_add_permission(request):
506
+ context["widget"]["attrs"]["related_add_url"] = (
507
+ related_model_admin.get_new_url(request)
508
+ )
509
+ except NotRegistered:
510
+ pass
511
+
277
512
  def is_multiselect(self):
278
513
  if self.multiselect is not None:
279
514
  return self.multiselect
280
515
  model_field = getattr(self.field, "model_field", None)
281
516
  return not (model_field and (model_field.one_to_one or model_field.many_to_one))
282
517
 
518
+ def _is_in_validation_context(self):
519
+ """
520
+ Check if value_from_datadict is being called during form validation
521
+ (full_clean, _clean_fields, etc.) vs. during change detection by formsets.
522
+
523
+ Returns True if called during actual validation, False if called during
524
+ change detection or other non-validation contexts.
525
+
526
+ Uses sys._getframe() instead of inspect.currentframe() for better performance,
527
+ as this method is called frequently during form processing.
528
+ """
529
+ # Get the call stack - using sys._getframe() for better performance
530
+ # sys._getframe(1) gets the caller's frame (skipping this method)
531
+ try:
532
+ current_frame = sys._getframe(1)
533
+ except ValueError:
534
+ # Fallback if _getframe is not available (unlikely in CPython)
535
+ return False
536
+
537
+ # Look for validation-related methods in the call stack
538
+ validation_methods = {
539
+ "_clean_bound_field",
540
+ }
541
+
542
+ # Walk up the call stack
543
+ depth = 0
544
+ while current_frame and depth < 5: # Limit depth to avoid infinite loops
545
+ method_name = current_frame.f_code.co_name
546
+ if method_name in validation_methods:
547
+ return True
548
+ current_frame = current_frame.f_back
549
+ depth += 1
550
+
551
+ return False
552
+
553
+ def get_forward_data(self, request, name):
554
+ """
555
+ Parse forward data from request.request_data.request_post.
556
+
557
+ For each field in self.forward, use name as base field name and replace
558
+ in it current field name with forward field name, return dict.
559
+
560
+ Args:
561
+ request: The request object
562
+ name: The base field name (e.g., "product__category")
563
+
564
+ Returns:
565
+ dict: Forward data with keys being forward field names and values
566
+ from request data
567
+ """
568
+ forward_data = {}
569
+ if not getattr(self, "forward", None):
570
+ return forward_data
571
+
572
+ post_data = getattr(request.request_data, "request_post", {})
573
+ if not post_data:
574
+ return forward_data
575
+
576
+ # For each field in self.forward list
577
+ for forward_field in self.forward:
578
+ # Replace only from end of name, separated by last -
579
+ # Example: if name="prefix-field_name", self.field_name="field_name",
580
+ # forward_field="parent" -> result="prefix-parent"
581
+ name_parts = name.split("-")
582
+
583
+ # Replace only if the last part matches self.field_name
584
+ if name_parts and name_parts[-1] == self.field_name:
585
+ # Replace the last part with forward_field and join back
586
+ name_parts[-1] = forward_field
587
+ forward_field_name = "-".join(name_parts)
588
+ else:
589
+ # If last part doesn't match, don't create forward field name
590
+ continue
591
+
592
+ # Get value from post_data if it exists
593
+ if forward_field_name in post_data:
594
+ forward_data[forward_field] = post_data.get(forward_field_name)
595
+
596
+ return forward_data
597
+
283
598
  def value_from_datadict(self, data, files, name):
284
599
  input_value = super().value_from_datadict(data, files, name)
285
- parsed_value = self.parse_value_from_input(self.threadsafe_request, input_value)
600
+ threadsafe_request = SBAdminThreadLocalService.get_request()
601
+ parsed_value = self.parse_value_from_input(threadsafe_request, input_value)
286
602
  if parsed_value is None:
287
603
  return parsed_value
288
- return parsed_value if self.is_multiselect() else next(iter(parsed_value), None)
604
+
605
+ if not self.is_multiselect():
606
+ parsed_value = next(iter(parsed_value), None)
607
+
608
+ # Only perform validation during actual form cleaning, not during change detection
609
+ # by inline formsets or during HTML rendering
610
+ is_in_validation = self._is_in_validation_context()
611
+ if is_in_validation:
612
+ try:
613
+ has_changed = self.form_field.has_changed(
614
+ self.form.initial.get(self.field_name, None), parsed_value
615
+ )
616
+ except AttributeError:
617
+ has_changed = False
618
+ if has_changed:
619
+ parsed_is_create = self.parse_is_create_from_input(
620
+ threadsafe_request, input_value
621
+ )
622
+ if not self.is_multiselect():
623
+ parsed_is_create = next(iter(parsed_is_create), None)
624
+ base_qs = self.get_queryset(threadsafe_request)
625
+ forward_data = self.get_forward_data(threadsafe_request, name)
626
+ qs = self.filter_search_queryset(
627
+ threadsafe_request,
628
+ base_qs,
629
+ forward_data=forward_data,
630
+ )
631
+ self.form_field.queryset = qs
632
+ parsed_value = self.validate(
633
+ parsed_value, qs, threadsafe_request, parsed_is_create
634
+ )
635
+
636
+ return parsed_value
637
+
638
+ def should_create_new_obj(self):
639
+ return self.allow_add and self.create_value_field
640
+
641
+ def create_new_obj(self, value, queryset, is_create):
642
+ if isinstance(value, list):
643
+ # TODO: multiselect creation
644
+ return self.form_field.to_python(value)
645
+ else:
646
+ data_to_create = {
647
+ self.create_value_field: value,
648
+ **self.default_create_data,
649
+ }
650
+ new_obj = queryset.model.objects.create(**data_to_create)
651
+ try:
652
+ return self.form_field.to_python(new_obj.id)
653
+ except ValidationError:
654
+ new_obj.delete()
655
+ raise ValidationError(
656
+ self.form_field.error_messages["invalid_choice"],
657
+ code="invalid_choice",
658
+ params={"value": value},
659
+ )
660
+
661
+ def validate(self, value, queryset, request, is_create=False):
662
+ is_create_value = (
663
+ True in is_create if isinstance(is_create, list) else is_create
664
+ )
665
+ if is_create_value and self.should_create_new_obj():
666
+ new_object = self.create_new_obj(value, queryset, is_create)
667
+ request.request_data.additional_data[self.REQUEST_CREATED_DATA_KEY] = (
668
+ request.request_data.additional_data.get(
669
+ self.REQUEST_CREATED_DATA_KEY, {}
670
+ )
671
+ )
672
+ request.request_data.additional_data[self.REQUEST_CREATED_DATA_KEY][
673
+ self.field_name
674
+ ] = new_object.pk
675
+ return new_object
676
+ return self.form_field.to_python(value)
289
677
 
290
678
  @classmethod
291
679
  def apply_to_model_field(cls, model_field):
@@ -311,6 +699,75 @@ class SBAdminImageWidget(SBAdminBaseWidget, AdminImageWidget):
311
699
  )
312
700
 
313
701
 
702
+ class SBAdminFilerFileWidget(SBAdminBaseWidget, FilerAdminFileWidget):
703
+ def __init__(self, form_field=None, *args, **kwargs):
704
+ self.form_field = form_field
705
+ super(FilerAdminFileWidget, self).__init__(
706
+ form_field.rel, form_field.view.admin_site, *args, **kwargs
707
+ )
708
+
709
+ def render(self, name, value, attrs=None, renderer=None):
710
+ obj = self.obj_for_value(value)
711
+ css_id = attrs.get("id", "id_image_x")
712
+ related_url = None
713
+ change_url = ""
714
+ if value:
715
+ try:
716
+ file_obj = File.objects.get(pk=value)
717
+ if file_obj.logical_folder.is_root:
718
+ related_url = reverse("sb_admin:filer-directory_listing-root")
719
+ else:
720
+ related_url = reverse(
721
+ "sb_admin:filer-directory_listing",
722
+ args=(file_obj.logical_folder.id,),
723
+ )
724
+ change_url = reverse(
725
+ "sb_admin:{}_{}_change".format(
726
+ file_obj._meta.app_label,
727
+ file_obj._meta.model_name,
728
+ ),
729
+ args=(file_obj.pk,),
730
+ )
731
+ except Exception as e:
732
+ # catch exception and manage it. We can re-raise it for debugging
733
+ # purposes and/or just logging it, provided user configured
734
+ # proper logging configuration
735
+ if settings.FILER_ENABLE_LOGGING:
736
+ logger.error("Error while rendering file widget: %s", e)
737
+ if settings.FILER_DEBUG:
738
+ raise
739
+ if not related_url:
740
+ related_url = reverse("sb_admin:filer-directory_listing-last")
741
+ params = self.url_parameters()
742
+ params["_pick"] = "file"
743
+ if params:
744
+ lookup_url = "?" + urlencode(sorted(params.items()))
745
+ else:
746
+ lookup_url = ""
747
+ if "class" not in attrs:
748
+ # The JavaScript looks for this hook.
749
+ attrs["class"] = "vForeignKeyRawIdAdminField"
750
+ # rendering the super for ForeignKeyRawIdWidget on purpose here because
751
+ # we only need the input and none of the other stuff that
752
+ # ForeignKeyRawIdWidget adds
753
+ hidden_input = super(ForeignKeyRawIdWidget, self).render(
754
+ name, value, attrs
755
+ ) # grandparent super
756
+ context = {
757
+ "hidden_input": hidden_input,
758
+ "lookup_url": "{}{}".format(related_url, lookup_url),
759
+ "change_url": change_url,
760
+ "object": obj,
761
+ "lookup_name": name,
762
+ "id": css_id,
763
+ "admin_icon_delete": "admin/img/icon-deletelink.svg",
764
+ }
765
+ # using template name directly to prevent override of template_name
766
+ # when calling render of ForeignKeyRawIdWidget
767
+ html = render_to_string("sb_admin/widgets/filer_file.html", context)
768
+ return mark_safe(html)
769
+
770
+
314
771
  class SBAdminReadOnlyPasswordHashWidget(SBAdminBaseWidget, ReadOnlyPasswordHashWidget):
315
772
  template_name = "sb_admin/widgets/read_only_password_hash.html"
316
773
 
@@ -355,3 +812,109 @@ class SBAdminCodeWidget(SBAdminBaseWidget, forms.Widget):
355
812
 
356
813
  class SBAdminHTMLWidget(SBAdminBaseWidget, forms.Widget):
357
814
  template_name = "sb_admin/widgets/html_read_only.html"
815
+
816
+
817
+ class SBAdminColorWidget(SBAdminTextInputWidget):
818
+ template_name = "sb_admin/widgets/color_field.html"
819
+ color_swatches = getattr(
820
+ settings,
821
+ "SB_ADMIN_COLOR_SWATCHES",
822
+ [
823
+ "#ffbe76",
824
+ "#f9ca24",
825
+ "#f0932b",
826
+ "#ff7979",
827
+ "#eb4d4b",
828
+ "#badc58",
829
+ "#6ab04c",
830
+ "#c7ecee",
831
+ "#7ed6df",
832
+ "#22a6b3",
833
+ "#e056fd",
834
+ "#be2edd",
835
+ "#686de0",
836
+ "#4834d4",
837
+ "#30336b",
838
+ "#130f40",
839
+ "#95afc0",
840
+ "#535c68",
841
+ ],
842
+ )
843
+
844
+ class Media:
845
+ css = {
846
+ "all": [
847
+ "sb_admin/css/coloris/coloris.min.css",
848
+ ],
849
+ }
850
+ js = [
851
+ "sb_admin/js/coloris/coloris.min.js",
852
+ ]
853
+
854
+
855
+ class SBAdminTreeWidget(SBAdminTreeWidgetMixin, SBAdminAutocompleteWidget):
856
+ template_name = "sb_admin/widgets/tree_select.html"
857
+
858
+ def get_context(self, name, value, attrs):
859
+ context = super().get_context(name, value, attrs)
860
+ context["widget"]["raw_value"] = value
861
+ context["widget"]["relationship_pick_mode"] = self.relationship_pick_mode
862
+ context["widget"]["value_dict"] = {
863
+ item["value"]: item["label"]
864
+ for item in context["widget"].get("value_list", [])
865
+ }
866
+ context["widget"]["additional_columns"] = self.additional_columns
867
+ context["widget"]["tree_strings"] = self.tree_strings
868
+ context["fancytree_filter_settings"] = {}
869
+ return context
870
+
871
+ @classmethod
872
+ def get_descendants_from_tree_data(cls, tree_data, parent_id):
873
+ parent_item = cls.find_parent_in_tree_data(tree_data, parent_id)
874
+ descendants = cls.get_descendats_from_item(parent_item)
875
+ return descendants
876
+
877
+ @classmethod
878
+ def get_descendats_from_item(cls, item):
879
+ descendants = []
880
+ if not item:
881
+ return descendants
882
+ for child in item.get("children", []):
883
+ descendants.append(child)
884
+ descendants.extend(cls.get_descendats_from_item(child))
885
+ return descendants
886
+
887
+ @classmethod
888
+ def find_parent_in_tree_data(cls, tree_data, parent_id):
889
+ str_parent_id = str(parent_id)
890
+ for item in tree_data:
891
+ if item["key"] == str_parent_id:
892
+ return item
893
+ parent = cls.find_parent_in_tree_data(
894
+ item.get("children", []), str_parent_id
895
+ )
896
+ if parent:
897
+ return parent
898
+ return None
899
+
900
+ def value_from_datadict(self, data, files, name):
901
+ input_value = data.get(name)
902
+ threadsafe_request = SBAdminThreadLocalService.get_request()
903
+ parsed_value = self.parse_value_from_input(threadsafe_request, input_value)
904
+ obj = self.form.instance
905
+ if (
906
+ obj
907
+ and parsed_value
908
+ and self.relationship_pick_mode == self.RELATIONSHIP_PICK_MODE_PARENT
909
+ ):
910
+ if obj.id == parsed_value:
911
+ raise ValidationError(_("Cannot set parent to itself"))
912
+ qs = self.get_queryset(threadsafe_request).order_by(*self.order_by)
913
+ tree_data = self.format_tree_data(threadsafe_request, qs)
914
+ children = self.get_descendants_from_tree_data(tree_data, obj.id)
915
+ children_ids = []
916
+ for child in children:
917
+ children_ids.append(child.get("key"))
918
+ if input_value in children_ids:
919
+ raise ValidationError(_("Cannot set parent to it's own child"))
920
+ return parsed_value