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,18 +1,65 @@
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 (
15
+ FieldDoesNotExist,
16
+ ImproperlyConfigured,
17
+ ValidationError,
18
+ )
19
+ from django.db.models import ForeignKey, OneToOneField
20
+ from django.template.loader import render_to_string
21
+ from django.urls import reverse
22
+ from django.utils.formats import get_format
23
+ from django.utils.http import urlencode
24
+ from django.utils.safestring import mark_safe
25
+ from django.utils.translation import gettext_lazy as _, get_language
10
26
  from django.views.generic.base import ContextMixin
27
+ from filer.fields.file import AdminFileWidget as FilerAdminFileWidget
11
28
  from filer.fields.image import AdminImageWidget
29
+ from filer.models import File
12
30
 
31
+ from django_smartbase_admin.admin.site import sb_admin_site
32
+ from django_smartbase_admin.engine.admin_base_view import (
33
+ SBADMIN_PARENT_INSTANCE_PK_VAR,
34
+ SBADMIN_PARENT_INSTANCE_LABEL_VAR,
35
+ )
13
36
  from django_smartbase_admin.engine.filter_widgets import (
14
37
  AutocompleteFilterWidget,
38
+ SBAdminTreeWidgetMixin,
39
+ )
40
+ from django_smartbase_admin.services.thread_local import SBAdminThreadLocalService
41
+ from django_smartbase_admin.templatetags.sb_admin_tags import (
42
+ SBAdminJSONEncoder,
15
43
  )
44
+ from django_smartbase_admin.utils import is_modal
45
+
46
+ try:
47
+ # Django >= 5.0
48
+ from django.contrib.admin.exceptions import NotRegistered
49
+ except ImportError:
50
+ from django.contrib.admin.sites import NotRegistered
51
+
52
+ logger = logging.getLogger(__name__)
53
+
54
+
55
+ def get_datetime_placeholder(lang=None):
56
+ lang = lang or get_language()
57
+ sb_admin_settings = getattr(settings, "SB_ADMIN_SETTINGS", {})
58
+ placeholder_setting = sb_admin_settings.get("DATETIME_PLACEHOLDER", {})
59
+ return placeholder_setting.get(
60
+ lang,
61
+ placeholder_setting.get("default", {"date": "mm.dd.yyyy", "time": "hh:mm"}),
62
+ )
16
63
 
17
64
 
18
65
  class SBAdminBaseWidget(ContextMixin):
@@ -28,6 +75,30 @@ class SBAdminBaseWidget(ContextMixin):
28
75
  def get_context(self, name, value, attrs):
29
76
  context = super().get_context(name, value, attrs)
30
77
  context["widget"]["form_field"] = self.form_field
78
+ opts = None
79
+
80
+ if self.form_field:
81
+ view = getattr(self.form_field, "view", None)
82
+ if view:
83
+ if hasattr(view, "opts"):
84
+ opts = view.opts
85
+ elif hasattr(view, "view") and hasattr(view.view, "opts"):
86
+ opts = view.view.opts
87
+
88
+ if opts:
89
+ modal_prefix = ""
90
+ try:
91
+ modal_prefix = (
92
+ "modal_"
93
+ if is_modal(SBAdminThreadLocalService.get_request())
94
+ else ""
95
+ )
96
+ except:
97
+ pass
98
+ widget_id = f"{modal_prefix}{opts.app_label}_{opts.model_name}_{context['widget']['attrs']['id']}"
99
+ context["widget"]["attrs"]["id"] = widget_id
100
+ # needed for BoundField.id_for_label to work correctly
101
+ self.attrs["id"] = widget_id
31
102
  return context
32
103
 
33
104
 
@@ -90,12 +161,28 @@ class SBAdminToggleWidget(SBAdminBaseWidget, forms.CheckboxInput):
90
161
 
91
162
  class SBAdminCKEditorWidget(SBAdminBaseWidget, CKEditorWidget):
92
163
 
93
- def __init__(self, form_field=None, attrs=None):
164
+ def __init__(
165
+ self,
166
+ config_name="default",
167
+ extra_plugins=None,
168
+ external_plugin_resources=None,
169
+ form_field=None,
170
+ attrs=None,
171
+ ):
94
172
  super().__init__(
95
- form_field, template_name="sb_admin/widgets/ckeditor.html", attrs=attrs
173
+ form_field,
174
+ template_name="sb_admin/widgets/ckeditor.html",
175
+ attrs=attrs,
176
+ config_name=config_name,
177
+ extra_plugins=extra_plugins,
178
+ external_plugin_resources=external_plugin_resources,
96
179
  )
97
180
 
98
181
 
182
+ class SBAdminCKEditorUploadingWidget(CKEditorUploadingWidget, SBAdminCKEditorWidget):
183
+ pass
184
+
185
+
99
186
  class SBAdminSelectWidget(SBAdminBaseWidget, forms.Select):
100
187
  template_name = "sb_admin/widgets/select.html"
101
188
  option_template_name = "sb_admin/widgets/select_option.html"
@@ -116,8 +203,20 @@ class SBAdminRadioWidget(SBAdminBaseWidget, forms.RadioSelect):
116
203
  )
117
204
 
118
205
 
206
+ class SBAdminRadioDropdownWidget(SBAdminBaseWidget, forms.RadioSelect):
207
+ template_name = "sb_admin/widgets/radio_dropdown.html"
208
+ option_template_name = "sb_admin/widgets/radio_option.html"
209
+
210
+ def __init__(self, form_field=None, attrs=None, choices=()):
211
+ super().__init__(
212
+ form_field,
213
+ attrs={"class": "radio radio-list", **(attrs or {})},
214
+ choices=choices,
215
+ )
216
+
217
+
119
218
  class SBAdminMultipleChoiceWidget(SBAdminBaseWidget, forms.CheckboxSelectMultiple):
120
- template_name = "sb_admin/widgets/checkbox_select.html"
219
+ template_name = "sb_admin/widgets/checkbox_dropdown.html"
121
220
  option_template_name = "sb_admin/widgets/checkbox_option.html"
122
221
 
123
222
  def __init__(self, form_field=None, attrs=None, choices=()):
@@ -146,7 +245,26 @@ class SBAdminDateWidget(SBAdminBaseWidget, forms.DateInput):
146
245
 
147
246
  def __init__(self, form_field=None, attrs=None):
148
247
  super().__init__(
149
- form_field, attrs={"class": "input js-datepicker", **(attrs or {})}
248
+ form_field,
249
+ format="%Y-%m-%d",
250
+ attrs={
251
+ "class": "input js-datepicker",
252
+ "data-sbadmin-datepicker": self.get_data(),
253
+ "placeholder": get_datetime_placeholder()["date"],
254
+ **(attrs or {}),
255
+ },
256
+ )
257
+
258
+ def get_data(self):
259
+ return json.dumps(
260
+ {
261
+ "flatpickrOptions": {
262
+ "dateFormat": "Y-m-d",
263
+ "altInput": True,
264
+ "altFormat": get_format("SHORT_DATE_FORMAT"),
265
+ },
266
+ },
267
+ cls=SBAdminJSONEncoder,
150
268
  )
151
269
 
152
270
 
@@ -155,7 +273,13 @@ class SBAdminTimeWidget(SBAdminBaseWidget, forms.TimeInput):
155
273
 
156
274
  def __init__(self, form_field=None, attrs=None):
157
275
  super().__init__(
158
- form_field, attrs={"class": "input js-timepicker", **(attrs or {})}
276
+ form_field,
277
+ attrs={
278
+ "class": "input js-timepicker",
279
+ "placeholder": get_datetime_placeholder()["time"],
280
+ "autocomplete": "do-not-autofill",
281
+ **(attrs or {}),
282
+ },
159
283
  )
160
284
 
161
285
 
@@ -209,6 +333,29 @@ class SBAdminArrayWidget(SBAdminTextInputWidget):
209
333
  return context
210
334
 
211
335
 
336
+ class SBAdminAttributesWidget(SBAdminTextInputWidget):
337
+ template_name = "sb_admin/widgets/attributes.html"
338
+
339
+ def get_context(self, name, value, attrs):
340
+ context = super().get_context(name, value, attrs)
341
+ widget = context.get("widget", None)
342
+ dict_widgets = []
343
+ template_widget = {"attrs": {"class": "input"}}
344
+ if widget and value:
345
+ if isinstance(value, str):
346
+ value = json.loads(value)
347
+ dict_widgets = [
348
+ {
349
+ "key": {"value": key, **template_widget},
350
+ "value": {"value": value, **template_widget},
351
+ }
352
+ for key, value in value.items()
353
+ ]
354
+ context["dict_widgets"] = dict_widgets
355
+ context["template_widget"] = template_widget
356
+ return context
357
+
358
+
212
359
  class SBAdminAutocompleteWidget(
213
360
  SBAdminBaseWidget, AutocompleteFilterWidget, forms.Widget
214
361
  ):
@@ -216,15 +363,36 @@ class SBAdminAutocompleteWidget(
216
363
  view = None
217
364
  form = None
218
365
  field_name = None
219
- threadsafe_request = None
220
366
  initialised = None
367
+ allow_add = None
368
+ create_value_field = None
369
+ default_create_data = None
370
+ forward_to_create = None
371
+ reload_on_save = None
372
+ REQUEST_CREATED_DATA_KEY = "autocomplete_created_data"
221
373
 
222
374
  def __init__(self, form_field=None, *args, **kwargs):
223
375
  attrs = kwargs.pop("attrs", None)
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", [])
224
380
  super().__init__(form_field, *args, **kwargs)
225
381
  self.attrs = {} if attrs is None else attrs.copy()
226
-
227
- def init_widget_dynamic(self, form, form_field, field_name, view, request):
382
+ if self.multiselect and self.allow_add:
383
+ raise ImproperlyConfigured(
384
+ "Multiselect with creation is currently not supported."
385
+ )
386
+
387
+ def get_id(self):
388
+ base_id = super().get_id()
389
+ if self.form:
390
+ base_id += f"_{self.form.__class__.__name__}"
391
+ return base_id
392
+
393
+ def init_widget_dynamic(
394
+ self, form, form_field, field_name, view, request, default_create_data=None
395
+ ):
228
396
  super().init_widget_dynamic(form, form_field, field_name, view, request)
229
397
  if self.initialised:
230
398
  return
@@ -232,11 +400,11 @@ class SBAdminAutocompleteWidget(
232
400
  self.field_name = field_name
233
401
  self.view = view
234
402
  self.form = form
235
- self.threadsafe_request = request
403
+ self.default_create_data = default_create_data or {}
236
404
  self.init_autocomplete_widget_static(
237
405
  self.field_name,
238
406
  self.model,
239
- self.threadsafe_request.request_data.configuration,
407
+ request.request_data.configuration,
240
408
  )
241
409
 
242
410
  def get_field_name(self):
@@ -247,6 +415,7 @@ class SBAdminAutocompleteWidget(
247
415
  self.input_id = (
248
416
  context["widget"]["attrs"]["id"] or f'id_{context["widget"]["name"]}'
249
417
  )
418
+
250
419
  context["widget"]["type"] = "hidden"
251
420
  context["widget"]["attrs"]["id"] = self.input_id
252
421
  context["widget"]["attrs"]["class"] = "js-autocomplete-detail"
@@ -254,38 +423,304 @@ class SBAdminAutocompleteWidget(
254
423
  getattr(self.form_field, "empty_label", "---------") or "---------"
255
424
  )
256
425
  query_suffix = "__in"
426
+ threadsafe_request = SBAdminThreadLocalService.get_request()
257
427
  if not self.is_multiselect():
258
428
  query_suffix = ""
259
429
  self.multiselect = False
430
+ context["widget"]["attrs"]["preselect_field"] = threadsafe_request.GET.get(
431
+ "sbadmin_parent_instance_field"
432
+ )
433
+ context["widget"]["attrs"]["preselect_field_label"] = (
434
+ threadsafe_request.GET.get(SBADMIN_PARENT_INSTANCE_LABEL_VAR)
435
+ )
436
+ context["widget"]["attrs"]["preselect_field_value"] = (
437
+ threadsafe_request.GET.get(SBADMIN_PARENT_INSTANCE_PK_VAR)
438
+ )
439
+ parsed_value = None
260
440
  if value:
261
- parsed_value = self.parse_value_from_input(self.threadsafe_request, value)
441
+ parsed_value = self.parse_value_from_input(threadsafe_request, value)
442
+ is_create = self.parse_is_create_from_input(
443
+ threadsafe_request,
444
+ threadsafe_request.request_data.request_post.get(name),
445
+ )
446
+ selected_options = []
447
+ if is_create:
448
+ errors = getattr(self.form, "errors", {})
449
+ if errors.get(self.field_name):
450
+ parsed_value = None
262
451
  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
452
+ if self.is_multiselect() and not isinstance(parsed_value, list):
453
+ parsed_value = [parsed_value]
454
+
455
+ try:
456
+ for item in self.get_queryset(threadsafe_request).filter(
457
+ **{f"{self.get_value_field()}{query_suffix}": parsed_value}
458
+ ):
459
+ selected_options.append(
460
+ {
461
+ "value": self.get_value(threadsafe_request, item),
462
+ "label": self.get_label(threadsafe_request, item),
463
+ }
464
+ )
465
+ except ValueError as e:
466
+ new_object_id = threadsafe_request.request_data.additional_data.get(
467
+ self.REQUEST_CREATED_DATA_KEY, {}
468
+ ).get(self.field_name)
469
+ if new_object_id:
470
+ selected_options.append(
471
+ {
472
+ "value": new_object_id,
473
+ "label": value,
474
+ }
475
+ )
476
+ elif hasattr(self.form, "add_error"):
477
+ self.form.add_error(
478
+ self.field_name,
479
+ _(
480
+ "The new value was created but became unselected due to another validation error. Please select it again."
481
+ ),
482
+ )
483
+
484
+ context["widget"]["value"] = json.dumps(selected_options)
485
+ context["widget"]["value_list"] = selected_options
486
+
487
+ if (
488
+ threadsafe_request.request_data.configuration.autocomplete_show_related_buttons(
489
+ self.model,
490
+ field_name=self.field_name,
491
+ current_view=self.view,
492
+ request=threadsafe_request,
493
+ )
494
+ and not self.is_multiselect()
495
+ ):
496
+ self.add_related_buttons_urls(parsed_value, threadsafe_request, context)
497
+ context["reload_on_save"] = self.reload_on_save
498
+
275
499
  return context
276
500
 
501
+ def add_related_buttons_urls(self, parsed_value, request, context):
502
+ try:
503
+ if hasattr(sb_admin_site, "get_model_admin"):
504
+ # Django >= 5.0
505
+ related_model_admin = sb_admin_site.get_model_admin(self.model)
506
+ else:
507
+ related_model_admin = sb_admin_site._registry.get(self.model)
508
+ if not related_model_admin:
509
+ return
510
+ if parsed_value and related_model_admin.has_view_or_change_permission(
511
+ request
512
+ ):
513
+ context["widget"]["attrs"]["related_edit_url"] = (
514
+ related_model_admin.get_detail_url(parsed_value)
515
+ )
516
+ if related_model_admin.has_add_permission(request):
517
+ context["widget"]["attrs"]["related_add_url"] = (
518
+ related_model_admin.get_new_url(request)
519
+ )
520
+ except NotRegistered:
521
+ pass
522
+
277
523
  def is_multiselect(self):
278
524
  if self.multiselect is not None:
279
525
  return self.multiselect
280
526
  model_field = getattr(self.field, "model_field", None)
281
527
  return not (model_field and (model_field.one_to_one or model_field.many_to_one))
282
528
 
529
+ def _is_in_validation_context(self):
530
+ """
531
+ Check if value_from_datadict is being called during form validation
532
+ (full_clean, _clean_fields, etc.) vs. during change detection by formsets.
533
+
534
+ Returns True if called during actual validation, False if called during
535
+ change detection or other non-validation contexts.
536
+
537
+ Uses sys._getframe() instead of inspect.currentframe() for better performance,
538
+ as this method is called frequently during form processing.
539
+ """
540
+ # Get the call stack - using sys._getframe() for better performance
541
+ # sys._getframe(1) gets the caller's frame (skipping this method)
542
+ try:
543
+ current_frame = sys._getframe(1)
544
+ except ValueError:
545
+ # Fallback if _getframe is not available (unlikely in CPython)
546
+ return False
547
+
548
+ # Look for validation-related methods in the call stack
549
+ validation_methods = {
550
+ "_clean_bound_field",
551
+ }
552
+
553
+ # Walk up the call stack
554
+ depth = 0
555
+ while current_frame and depth < 5: # Limit depth to avoid infinite loops
556
+ method_name = current_frame.f_code.co_name
557
+ if method_name in validation_methods:
558
+ return True
559
+ current_frame = current_frame.f_back
560
+ depth += 1
561
+
562
+ return False
563
+
564
+ def get_forward_data(self, request, name):
565
+ """
566
+ Parse forward data from request.request_data.request_post.
567
+
568
+ For each field in self.forward, use name as base field name and replace
569
+ in it current field name with forward field name, return dict.
570
+
571
+ Args:
572
+ request: The request object
573
+ name: The base field name (e.g., "product__category")
574
+
575
+ Returns:
576
+ dict: Forward data with keys being forward field names and values
577
+ from request data
578
+ """
579
+ forward_data = {}
580
+ if not getattr(self, "forward", None):
581
+ return forward_data
582
+
583
+ post_data = getattr(request.request_data, "request_post", {})
584
+ if not post_data:
585
+ return forward_data
586
+
587
+ # For each field in self.forward list
588
+ for forward_field in self.forward:
589
+ # Replace only from end of name, separated by last -
590
+ # Example: if name="prefix-field_name", self.field_name="field_name",
591
+ # forward_field="parent" -> result="prefix-parent"
592
+ name_parts = name.split("-")
593
+
594
+ # Replace only if the last part matches self.field_name
595
+ if name_parts and name_parts[-1] == self.field_name:
596
+ # Replace the last part with forward_field and join back
597
+ name_parts[-1] = forward_field
598
+ forward_field_name = "-".join(name_parts)
599
+ else:
600
+ # If last part doesn't match, don't create forward field name
601
+ continue
602
+
603
+ # Get value from post_data if it exists
604
+ if forward_field_name in post_data:
605
+ forward_data[forward_field] = post_data.get(forward_field_name)
606
+
607
+ return forward_data
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
+
283
637
  def value_from_datadict(self, data, files, name):
284
638
  input_value = super().value_from_datadict(data, files, name)
285
- parsed_value = self.parse_value_from_input(self.threadsafe_request, input_value)
639
+ threadsafe_request = SBAdminThreadLocalService.get_request()
640
+ parsed_value = self.parse_value_from_input(threadsafe_request, input_value)
286
641
  if parsed_value is None:
287
642
  return parsed_value
288
- return parsed_value if self.is_multiselect() else next(iter(parsed_value), None)
643
+
644
+ if not self.is_multiselect():
645
+ parsed_value = next(iter(parsed_value), None)
646
+
647
+ # Only perform validation during actual form cleaning, not during change detection
648
+ # by inline formsets or during HTML rendering
649
+ is_in_validation = self._is_in_validation_context()
650
+ if is_in_validation:
651
+ try:
652
+ has_changed = self.form_field.has_changed(
653
+ self.form.initial.get(self.field_name, None), parsed_value
654
+ )
655
+ except AttributeError:
656
+ has_changed = False
657
+ if has_changed:
658
+ parsed_is_create = self.parse_is_create_from_input(
659
+ threadsafe_request, input_value
660
+ )
661
+ if not self.is_multiselect():
662
+ parsed_is_create = next(iter(parsed_is_create), None)
663
+ base_qs = self.get_queryset(threadsafe_request)
664
+ forward_data = self.get_forward_data(threadsafe_request, name)
665
+ qs = self.filter_search_queryset(
666
+ threadsafe_request,
667
+ base_qs,
668
+ forward_data=forward_data,
669
+ )
670
+ self.form_field.queryset = qs
671
+ parsed_value = self.validate(
672
+ parsed_value,
673
+ qs,
674
+ threadsafe_request,
675
+ forward_data,
676
+ parsed_is_create,
677
+ )
678
+
679
+ return parsed_value
680
+
681
+ def should_create_new_obj(self):
682
+ return self.allow_add and self.create_value_field
683
+
684
+ def create_new_obj(self, value, queryset, request, forward_data):
685
+ if isinstance(value, list):
686
+ # TODO: multiselect creation
687
+ return self.form_field.to_python(value)
688
+ else:
689
+ forward_data_to_create = self.get_forward_data_to_create(
690
+ request, forward_data
691
+ )
692
+ data_to_create = {
693
+ self.create_value_field: value,
694
+ **self.default_create_data,
695
+ **forward_data_to_create,
696
+ }
697
+ new_obj = queryset.model.objects.create(**data_to_create)
698
+ try:
699
+ return self.form_field.to_python(new_obj.id)
700
+ except ValidationError:
701
+ new_obj.delete()
702
+ raise ValidationError(
703
+ self.form_field.error_messages["invalid_choice"],
704
+ code="invalid_choice",
705
+ params={"value": value},
706
+ )
707
+
708
+ def validate(self, value, queryset, request, forward_data, is_create=False):
709
+ is_create_value = (
710
+ True in is_create if isinstance(is_create, list) else is_create
711
+ )
712
+ if is_create_value and self.should_create_new_obj():
713
+ new_object = self.create_new_obj(value, queryset, request, forward_data)
714
+ request.request_data.additional_data[self.REQUEST_CREATED_DATA_KEY] = (
715
+ request.request_data.additional_data.get(
716
+ self.REQUEST_CREATED_DATA_KEY, {}
717
+ )
718
+ )
719
+ request.request_data.additional_data[self.REQUEST_CREATED_DATA_KEY][
720
+ self.field_name
721
+ ] = new_object.pk
722
+ return new_object
723
+ return self.form_field.to_python(value)
289
724
 
290
725
  @classmethod
291
726
  def apply_to_model_field(cls, model_field):
@@ -311,6 +746,75 @@ class SBAdminImageWidget(SBAdminBaseWidget, AdminImageWidget):
311
746
  )
312
747
 
313
748
 
749
+ class SBAdminFilerFileWidget(SBAdminBaseWidget, FilerAdminFileWidget):
750
+ def __init__(self, form_field=None, *args, **kwargs):
751
+ self.form_field = form_field
752
+ super(FilerAdminFileWidget, self).__init__(
753
+ form_field.rel, form_field.view.admin_site, *args, **kwargs
754
+ )
755
+
756
+ def render(self, name, value, attrs=None, renderer=None):
757
+ obj = self.obj_for_value(value)
758
+ css_id = attrs.get("id", "id_image_x")
759
+ related_url = None
760
+ change_url = ""
761
+ if value:
762
+ try:
763
+ file_obj = File.objects.get(pk=value)
764
+ if file_obj.logical_folder.is_root:
765
+ related_url = reverse("sb_admin:filer-directory_listing-root")
766
+ else:
767
+ related_url = reverse(
768
+ "sb_admin:filer-directory_listing",
769
+ args=(file_obj.logical_folder.id,),
770
+ )
771
+ change_url = reverse(
772
+ "sb_admin:{}_{}_change".format(
773
+ file_obj._meta.app_label,
774
+ file_obj._meta.model_name,
775
+ ),
776
+ args=(file_obj.pk,),
777
+ )
778
+ except Exception as e:
779
+ # catch exception and manage it. We can re-raise it for debugging
780
+ # purposes and/or just logging it, provided user configured
781
+ # proper logging configuration
782
+ if settings.FILER_ENABLE_LOGGING:
783
+ logger.error("Error while rendering file widget: %s", e)
784
+ if settings.FILER_DEBUG:
785
+ raise
786
+ if not related_url:
787
+ related_url = reverse("sb_admin:filer-directory_listing-last")
788
+ params = self.url_parameters()
789
+ params["_pick"] = "file"
790
+ if params:
791
+ lookup_url = "?" + urlencode(sorted(params.items()))
792
+ else:
793
+ lookup_url = ""
794
+ if "class" not in attrs:
795
+ # The JavaScript looks for this hook.
796
+ attrs["class"] = "vForeignKeyRawIdAdminField"
797
+ # rendering the super for ForeignKeyRawIdWidget on purpose here because
798
+ # we only need the input and none of the other stuff that
799
+ # ForeignKeyRawIdWidget adds
800
+ hidden_input = super(ForeignKeyRawIdWidget, self).render(
801
+ name, value, attrs
802
+ ) # grandparent super
803
+ context = {
804
+ "hidden_input": hidden_input,
805
+ "lookup_url": "{}{}".format(related_url, lookup_url),
806
+ "change_url": change_url,
807
+ "object": obj,
808
+ "lookup_name": name,
809
+ "id": css_id,
810
+ "admin_icon_delete": "admin/img/icon-deletelink.svg",
811
+ }
812
+ # using template name directly to prevent override of template_name
813
+ # when calling render of ForeignKeyRawIdWidget
814
+ html = render_to_string("sb_admin/widgets/filer_file.html", context)
815
+ return mark_safe(html)
816
+
817
+
314
818
  class SBAdminReadOnlyPasswordHashWidget(SBAdminBaseWidget, ReadOnlyPasswordHashWidget):
315
819
  template_name = "sb_admin/widgets/read_only_password_hash.html"
316
820
 
@@ -355,3 +859,109 @@ class SBAdminCodeWidget(SBAdminBaseWidget, forms.Widget):
355
859
 
356
860
  class SBAdminHTMLWidget(SBAdminBaseWidget, forms.Widget):
357
861
  template_name = "sb_admin/widgets/html_read_only.html"
862
+
863
+
864
+ class SBAdminColorWidget(SBAdminTextInputWidget):
865
+ template_name = "sb_admin/widgets/color_field.html"
866
+ color_swatches = getattr(
867
+ settings,
868
+ "SB_ADMIN_COLOR_SWATCHES",
869
+ [
870
+ "#ffbe76",
871
+ "#f9ca24",
872
+ "#f0932b",
873
+ "#ff7979",
874
+ "#eb4d4b",
875
+ "#badc58",
876
+ "#6ab04c",
877
+ "#c7ecee",
878
+ "#7ed6df",
879
+ "#22a6b3",
880
+ "#e056fd",
881
+ "#be2edd",
882
+ "#686de0",
883
+ "#4834d4",
884
+ "#30336b",
885
+ "#130f40",
886
+ "#95afc0",
887
+ "#535c68",
888
+ ],
889
+ )
890
+
891
+ class Media:
892
+ css = {
893
+ "all": [
894
+ "sb_admin/css/coloris/coloris.min.css",
895
+ ],
896
+ }
897
+ js = [
898
+ "sb_admin/js/coloris/coloris.min.js",
899
+ ]
900
+
901
+
902
+ class SBAdminTreeWidget(SBAdminTreeWidgetMixin, SBAdminAutocompleteWidget):
903
+ template_name = "sb_admin/widgets/tree_select.html"
904
+
905
+ def get_context(self, name, value, attrs):
906
+ context = super().get_context(name, value, attrs)
907
+ context["widget"]["raw_value"] = value
908
+ context["widget"]["relationship_pick_mode"] = self.relationship_pick_mode
909
+ context["widget"]["value_dict"] = {
910
+ item["value"]: item["label"]
911
+ for item in context["widget"].get("value_list", [])
912
+ }
913
+ context["widget"]["additional_columns"] = self.additional_columns
914
+ context["widget"]["tree_strings"] = self.tree_strings
915
+ context["fancytree_filter_settings"] = {}
916
+ return context
917
+
918
+ @classmethod
919
+ def get_descendants_from_tree_data(cls, tree_data, parent_id):
920
+ parent_item = cls.find_parent_in_tree_data(tree_data, parent_id)
921
+ descendants = cls.get_descendats_from_item(parent_item)
922
+ return descendants
923
+
924
+ @classmethod
925
+ def get_descendats_from_item(cls, item):
926
+ descendants = []
927
+ if not item:
928
+ return descendants
929
+ for child in item.get("children", []):
930
+ descendants.append(child)
931
+ descendants.extend(cls.get_descendats_from_item(child))
932
+ return descendants
933
+
934
+ @classmethod
935
+ def find_parent_in_tree_data(cls, tree_data, parent_id):
936
+ str_parent_id = str(parent_id)
937
+ for item in tree_data:
938
+ if item["key"] == str_parent_id:
939
+ return item
940
+ parent = cls.find_parent_in_tree_data(
941
+ item.get("children", []), str_parent_id
942
+ )
943
+ if parent:
944
+ return parent
945
+ return None
946
+
947
+ def value_from_datadict(self, data, files, name):
948
+ input_value = data.get(name)
949
+ threadsafe_request = SBAdminThreadLocalService.get_request()
950
+ parsed_value = self.parse_value_from_input(threadsafe_request, input_value)
951
+ obj = self.form.instance
952
+ if (
953
+ obj
954
+ and parsed_value
955
+ and self.relationship_pick_mode == self.RELATIONSHIP_PICK_MODE_PARENT
956
+ ):
957
+ if obj.id == parsed_value:
958
+ raise ValidationError(_("Cannot set parent to itself"))
959
+ qs = self.get_queryset(threadsafe_request).order_by(*self.order_by)
960
+ tree_data = self.format_tree_data(threadsafe_request, qs)
961
+ children = self.get_descendants_from_tree_data(tree_data, obj.id)
962
+ children_ids = []
963
+ for child in children:
964
+ children_ids.append(child.get("key"))
965
+ if input_value in children_ids:
966
+ raise ValidationError(_("Cannot set parent to it's own child"))
967
+ return parsed_value