django-smartbase-admin 0.2.47__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 (188) hide show
  1. django_smartbase_admin/actions/admin_action_list.py +80 -51
  2. django_smartbase_admin/actions/advanced_filters.py +55 -20
  3. django_smartbase_admin/admin/admin_base.py +477 -89
  4. django_smartbase_admin/admin/site.py +104 -34
  5. django_smartbase_admin/admin/widgets.py +598 -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 +253 -115
  9. django_smartbase_admin/engine/configuration.py +186 -4
  10. django_smartbase_admin/engine/const.py +7 -0
  11. django_smartbase_admin/engine/dashboard.py +44 -23
  12. django_smartbase_admin/engine/fake_inline.py +44 -7
  13. django_smartbase_admin/engine/field.py +54 -10
  14. django_smartbase_admin/engine/field_formatter.py +32 -9
  15. django_smartbase_admin/engine/filter_widgets.py +356 -21
  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 +82 -27
  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/table.js.LICENSE.txt +9 -0
  47. django_smartbase_admin/static/sb_admin/dist/tree_widget.js +1 -0
  48. django_smartbase_admin/static/sb_admin/dist/tree_widget_style.css +1 -0
  49. django_smartbase_admin/static/sb_admin/dist/tree_widget_style.js +0 -0
  50. django_smartbase_admin/static/sb_admin/fancytree/jquery.fancytree-all-deps.min.js +1 -0
  51. django_smartbase_admin/static/sb_admin/images/file_types/file-csv.svg +11 -0
  52. django_smartbase_admin/static/sb_admin/images/file_types/file-doc.svg +11 -0
  53. django_smartbase_admin/static/sb_admin/images/file_types/file-docx.svg +11 -0
  54. django_smartbase_admin/static/sb_admin/images/file_types/file-other.svg +13 -0
  55. django_smartbase_admin/static/sb_admin/images/file_types/file-pdf.svg +11 -0
  56. django_smartbase_admin/static/sb_admin/images/file_types/file-ppt.svg +11 -0
  57. django_smartbase_admin/static/sb_admin/images/file_types/file-xls.svg +11 -0
  58. django_smartbase_admin/static/sb_admin/images/file_types/file-xlsx.svg +11 -0
  59. django_smartbase_admin/static/sb_admin/images/file_types/file-zip.svg +18 -0
  60. django_smartbase_admin/static/sb_admin/images/flags/de-at.png +0 -0
  61. django_smartbase_admin/static/sb_admin/images/flags/de-ch.png +0 -0
  62. django_smartbase_admin/static/sb_admin/images/logo_light.svg +21 -0
  63. django_smartbase_admin/static/sb_admin/js/coloris/coloris.min.js +6 -0
  64. django_smartbase_admin/static/sb_admin/js/fullcalendar.min.js +14804 -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 +66 -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 +26 -8
  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 +63 -5
  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 +307 -26
  102. django_smartbase_admin/static/sb_admin/src/js/multiselect.js +50 -41
  103. django_smartbase_admin/static/sb_admin/src/js/range.js +3 -2
  104. django_smartbase_admin/static/sb_admin/src/js/sb_ajax_params_tabulator_modifier.js +21 -0
  105. django_smartbase_admin/static/sb_admin/src/js/table.js +38 -13
  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/filter_module.js +3 -3
  109. django_smartbase_admin/static/sb_admin/src/js/table_modules/header_tabs_module.js +11 -11
  110. django_smartbase_admin/static/sb_admin/src/js/table_modules/selection_module.js +28 -8
  111. django_smartbase_admin/static/sb_admin/src/js/table_modules/table_params_module.js +6 -0
  112. django_smartbase_admin/static/sb_admin/src/js/table_modules/views_module.js +19 -3
  113. django_smartbase_admin/static/sb_admin/src/js/tree_widget.js +406 -0
  114. django_smartbase_admin/static/sb_admin/src/js/utils.js +56 -21
  115. django_smartbase_admin/templates/sb_admin/actions/change_form.html +169 -117
  116. django_smartbase_admin/templates/sb_admin/actions/dashboard.html +2 -2
  117. django_smartbase_admin/templates/sb_admin/actions/delete_selected_confirmation.html +56 -32
  118. django_smartbase_admin/templates/sb_admin/actions/list.html +79 -42
  119. django_smartbase_admin/templates/sb_admin/actions/object_history.html +2 -2
  120. django_smartbase_admin/templates/sb_admin/actions/partials/action_link.html +14 -0
  121. django_smartbase_admin/templates/sb_admin/actions/partials/selected_rows_actions.html +2 -2
  122. django_smartbase_admin/templates/sb_admin/actions/partials/tabulator_header_v2.html +2 -2
  123. django_smartbase_admin/templates/sb_admin/actions/tree_list.html +63 -0
  124. django_smartbase_admin/templates/sb_admin/authentification/login_base.html +5 -1
  125. django_smartbase_admin/templates/sb_admin/components/columns.html +1 -1
  126. django_smartbase_admin/templates/sb_admin/components/filters_v2.html +99 -85
  127. django_smartbase_admin/templates/sb_admin/config/view.html +0 -1
  128. django_smartbase_admin/templates/sb_admin/dashboard/calendar_widget.html +69 -0
  129. django_smartbase_admin/templates/sb_admin/dashboard/chart_widget.html +21 -2
  130. django_smartbase_admin/templates/sb_admin/dashboard/list_widget.html +6 -0
  131. django_smartbase_admin/templates/sb_admin/dashboard/widget_base.html +1 -1
  132. django_smartbase_admin/templates/sb_admin/filter_widgets/advanced_filters/date_field.html +18 -8
  133. django_smartbase_admin/templates/sb_admin/filter_widgets/advanced_filters/multiple_choice_field.html +1 -1
  134. django_smartbase_admin/templates/sb_admin/filter_widgets/advanced_filters/tree_select_filter.html +2 -0
  135. django_smartbase_admin/templates/sb_admin/filter_widgets/date_field.html +18 -4
  136. django_smartbase_admin/templates/sb_admin/filter_widgets/multiple_choice_field.html +14 -0
  137. django_smartbase_admin/templates/sb_admin/filter_widgets/partials/clear.html +10 -5
  138. django_smartbase_admin/templates/sb_admin/filter_widgets/radio_choice_field.html +2 -2
  139. django_smartbase_admin/templates/sb_admin/filter_widgets/tree_select_filter.html +16 -0
  140. django_smartbase_admin/templates/sb_admin/includes/change_form_title.html +3 -1
  141. django_smartbase_admin/templates/sb_admin/includes/components.html +5 -1
  142. django_smartbase_admin/templates/sb_admin/includes/inline_fieldset.html +48 -39
  143. django_smartbase_admin/templates/sb_admin/includes/notifications.html +2 -1
  144. django_smartbase_admin/templates/sb_admin/includes/readonly_boolean_field.html +9 -0
  145. django_smartbase_admin/templates/sb_admin/includes/readonly_field.html +12 -0
  146. django_smartbase_admin/templates/sb_admin/includes/table_inline_delete_button.html +4 -5
  147. django_smartbase_admin/templates/sb_admin/inlines/stacked_inline.html +68 -40
  148. django_smartbase_admin/templates/sb_admin/inlines/table_inline.html +78 -36
  149. django_smartbase_admin/templates/sb_admin/integrations/filer/folder_list.html +18 -0
  150. django_smartbase_admin/templates/sb_admin/navigation.html +166 -158
  151. django_smartbase_admin/templates/sb_admin/partials/modal/modal_content.html +2 -6
  152. django_smartbase_admin/templates/sb_admin/sb_admin_base.html +49 -4
  153. django_smartbase_admin/templates/sb_admin/sb_admin_base_no_sidebar.html +35 -11
  154. django_smartbase_admin/templates/sb_admin/sb_admin_js_trans.html +3 -0
  155. django_smartbase_admin/templates/sb_admin/sprites/sb_admin.svg +1 -1
  156. django_smartbase_admin/templates/sb_admin/tailwind_whitelist.html +6 -3
  157. django_smartbase_admin/templates/sb_admin/widgets/array.html +0 -1
  158. django_smartbase_admin/templates/sb_admin/widgets/attributes.html +68 -0
  159. django_smartbase_admin/templates/sb_admin/widgets/autocomplete.html +13 -2
  160. django_smartbase_admin/templates/sb_admin/widgets/{checkbox_select.html → checkbox_dropdown.html} +2 -2
  161. django_smartbase_admin/templates/sb_admin/widgets/checkbox_group.html +15 -0
  162. django_smartbase_admin/templates/sb_admin/widgets/clearable_file_input.html +2 -2
  163. django_smartbase_admin/templates/sb_admin/widgets/color_field.html +30 -0
  164. django_smartbase_admin/templates/sb_admin/widgets/date.html +8 -1
  165. django_smartbase_admin/templates/sb_admin/widgets/filer_file.html +84 -0
  166. django_smartbase_admin/templates/sb_admin/widgets/html_read_only.html +1 -0
  167. django_smartbase_admin/templates/sb_admin/widgets/includes/related_item_buttons.html +38 -0
  168. django_smartbase_admin/templates/sb_admin/widgets/multiwidget.html +1 -1
  169. django_smartbase_admin/templates/sb_admin/widgets/radio.html +3 -2
  170. django_smartbase_admin/templates/sb_admin/widgets/radio_dropdown.html +30 -0
  171. django_smartbase_admin/templates/sb_admin/widgets/read_only_password_hash.html +3 -0
  172. django_smartbase_admin/templates/sb_admin/widgets/time.html +8 -1
  173. django_smartbase_admin/templates/sb_admin/widgets/toggle.html +1 -1
  174. django_smartbase_admin/templates/sb_admin/widgets/tree_base.html +59 -0
  175. django_smartbase_admin/templates/sb_admin/widgets/tree_select.html +24 -0
  176. django_smartbase_admin/templates/sb_admin/widgets/tree_select_inline.html +12 -0
  177. django_smartbase_admin/templatetags/sb_admin_tags.py +163 -4
  178. django_smartbase_admin/utils.py +22 -3
  179. django_smartbase_admin/views/dashboard_view.py +6 -0
  180. django_smartbase_admin/views/global_filter_view.py +8 -2
  181. django_smartbase_admin/views/translations_view.py +12 -5
  182. django_smartbase_admin/views/user_config_view.py +52 -0
  183. django_smartbase_admin-1.0.38.dist-info/METADATA +166 -0
  184. {django_smartbase_admin-0.2.47.dist-info → django_smartbase_admin-1.0.38.dist-info}/RECORD +186 -121
  185. {django_smartbase_admin-0.2.47.dist-info → django_smartbase_admin-1.0.38.dist-info}/WHEEL +1 -1
  186. django_smartbase_admin/templates/sb_admin/integrations/sorting/change_list.html +0 -401
  187. django_smartbase_admin-0.2.47.dist-info/METADATA +0 -25
  188. {django_smartbase_admin-0.2.47.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=()):
@@ -128,6 +222,11 @@ class SBAdminMultipleChoiceWidget(SBAdminBaseWidget, forms.CheckboxSelectMultipl
128
222
  )
129
223
 
130
224
 
225
+ class SBAdminMultipleChoiceInlineWidget(SBAdminMultipleChoiceWidget):
226
+ template_name = "sb_admin/widgets/checkbox_group.html"
227
+ option_template_name = "sb_admin/widgets/checkbox.html"
228
+
229
+
131
230
  class SBAdminNullBooleanSelectWidget(SBAdminBaseWidget, forms.NullBooleanSelect):
132
231
  template_name = "sb_admin/widgets/select.html"
133
232
  option_template_name = "sb_admin/widgets/select_option.html"
@@ -141,7 +240,26 @@ class SBAdminDateWidget(SBAdminBaseWidget, forms.DateInput):
141
240
 
142
241
  def __init__(self, form_field=None, attrs=None):
143
242
  super().__init__(
144
- 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,
145
263
  )
146
264
 
147
265
 
@@ -150,7 +268,13 @@ class SBAdminTimeWidget(SBAdminBaseWidget, forms.TimeInput):
150
268
 
151
269
  def __init__(self, form_field=None, attrs=None):
152
270
  super().__init__(
153
- 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
+ },
154
278
  )
155
279
 
156
280
 
@@ -204,6 +328,29 @@ class SBAdminArrayWidget(SBAdminTextInputWidget):
204
328
  return context
205
329
 
206
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
+
207
354
  class SBAdminAutocompleteWidget(
208
355
  SBAdminBaseWidget, AutocompleteFilterWidget, forms.Widget
209
356
  ):
@@ -211,15 +358,30 @@ class SBAdminAutocompleteWidget(
211
358
  view = None
212
359
  form = None
213
360
  field_name = None
214
- threadsafe_request = None
215
361
  initialised = None
362
+ default_create_data = None
363
+ reload_on_save = None
364
+ REQUEST_CREATED_DATA_KEY = "autocomplete_created_data"
216
365
 
217
366
  def __init__(self, form_field=None, *args, **kwargs):
218
367
  attrs = kwargs.pop("attrs", None)
368
+ self.reload_on_save = kwargs.pop("reload_on_save", False)
219
369
  super().__init__(form_field, *args, **kwargs)
220
370
  self.attrs = {} if attrs is None else attrs.copy()
221
-
222
- 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
+ ):
223
385
  super().init_widget_dynamic(form, form_field, field_name, view, request)
224
386
  if self.initialised:
225
387
  return
@@ -227,11 +389,11 @@ class SBAdminAutocompleteWidget(
227
389
  self.field_name = field_name
228
390
  self.view = view
229
391
  self.form = form
230
- self.threadsafe_request = request
392
+ self.default_create_data = default_create_data or {}
231
393
  self.init_autocomplete_widget_static(
232
394
  self.field_name,
233
395
  self.model,
234
- self.threadsafe_request.request_data.configuration,
396
+ request.request_data.configuration,
235
397
  )
236
398
 
237
399
  def get_field_name(self):
@@ -242,6 +404,7 @@ class SBAdminAutocompleteWidget(
242
404
  self.input_id = (
243
405
  context["widget"]["attrs"]["id"] or f'id_{context["widget"]["name"]}'
244
406
  )
407
+
245
408
  context["widget"]["type"] = "hidden"
246
409
  context["widget"]["attrs"]["id"] = self.input_id
247
410
  context["widget"]["attrs"]["class"] = "js-autocomplete-detail"
@@ -249,38 +412,268 @@ class SBAdminAutocompleteWidget(
249
412
  getattr(self.form_field, "empty_label", "---------") or "---------"
250
413
  )
251
414
  query_suffix = "__in"
415
+ threadsafe_request = SBAdminThreadLocalService.get_request()
252
416
  if not self.is_multiselect():
253
417
  query_suffix = ""
254
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
255
429
  if value:
256
- 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
257
440
  if parsed_value:
258
- selected_options = []
259
- for item in self.get_queryset(self.threadsafe_request).filter(
260
- **{f"{self.get_value_field()}{query_suffix}": parsed_value}
261
- ):
262
- selected_options.append(
263
- {
264
- "value": self.get_value(self.threadsafe_request, item),
265
- "label": self.get_label(self.threadsafe_request, item),
266
- }
267
- )
268
- context["widget"]["value"] = json.dumps(selected_options)
269
- 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
+
270
488
  return context
271
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
+
272
512
  def is_multiselect(self):
273
513
  if self.multiselect is not None:
274
514
  return self.multiselect
275
515
  model_field = getattr(self.field, "model_field", None)
276
516
  return not (model_field and (model_field.one_to_one or model_field.many_to_one))
277
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
+
278
598
  def value_from_datadict(self, data, files, name):
279
599
  input_value = super().value_from_datadict(data, files, name)
280
- 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)
281
602
  if parsed_value is None:
282
603
  return parsed_value
283
- 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)
284
677
 
285
678
  @classmethod
286
679
  def apply_to_model_field(cls, model_field):
@@ -306,6 +699,75 @@ class SBAdminImageWidget(SBAdminBaseWidget, AdminImageWidget):
306
699
  )
307
700
 
308
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
+
309
771
  class SBAdminReadOnlyPasswordHashWidget(SBAdminBaseWidget, ReadOnlyPasswordHashWidget):
310
772
  template_name = "sb_admin/widgets/read_only_password_hash.html"
311
773
 
@@ -346,3 +808,113 @@ class SBAdminCodeWidget(SBAdminBaseWidget, forms.Widget):
346
808
  "sb_admin/js/codemirror/django.min.js",
347
809
  "sb_admin/src/js/code.js",
348
810
  ]
811
+
812
+
813
+ class SBAdminHTMLWidget(SBAdminBaseWidget, forms.Widget):
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