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.
- django_smartbase_admin/actions/admin_action_list.py +74 -38
- django_smartbase_admin/actions/advanced_filters.py +24 -1
- django_smartbase_admin/admin/admin_base.py +401 -96
- django_smartbase_admin/admin/site.py +93 -35
- django_smartbase_admin/admin/widgets.py +589 -26
- django_smartbase_admin/apps.py +2 -0
- django_smartbase_admin/engine/actions.py +34 -16
- django_smartbase_admin/engine/admin_base_view.py +252 -115
- django_smartbase_admin/engine/configuration.py +186 -4
- django_smartbase_admin/engine/const.py +6 -0
- django_smartbase_admin/engine/dashboard.py +44 -23
- django_smartbase_admin/engine/fake_inline.py +15 -11
- django_smartbase_admin/engine/field.py +42 -12
- django_smartbase_admin/engine/field_formatter.py +22 -8
- django_smartbase_admin/engine/filter_widgets.py +309 -20
- django_smartbase_admin/engine/menu_item.py +8 -5
- django_smartbase_admin/engine/modal_view.py +12 -7
- django_smartbase_admin/engine/request.py +2 -0
- django_smartbase_admin/integration/__init__.py +0 -0
- django_smartbase_admin/integration/django_cms.py +43 -0
- django_smartbase_admin/locale/sk/LC_MESSAGES/django.mo +0 -0
- django_smartbase_admin/locale/sk/LC_MESSAGES/django.po +268 -37
- django_smartbase_admin/migrations/0005_sbadminuserconfiguration.py +26 -0
- django_smartbase_admin/migrations/0006_alter_sbadminuserconfiguration_color_scheme.py +18 -0
- django_smartbase_admin/models.py +22 -0
- django_smartbase_admin/monkeypatch/admin_readonly_field_monkeypatch.py +96 -0
- django_smartbase_admin/monkeypatch/fake_inline_monkeypatch.py +1 -1
- django_smartbase_admin/querysets.py +3 -0
- django_smartbase_admin/services/configuration.py +30 -0
- django_smartbase_admin/services/thread_local.py +6 -19
- django_smartbase_admin/services/views.py +80 -13
- django_smartbase_admin/services/xlsx_export.py +6 -0
- django_smartbase_admin/static/sb_admin/build/tailwind.config.js +1 -0
- django_smartbase_admin/static/sb_admin/build/tailwind_config_partials/colors.js +4 -0
- django_smartbase_admin/static/sb_admin/build/tailwind_config_partials/spacing.js +1 -0
- django_smartbase_admin/static/sb_admin/build/webpack.common.js +11 -8
- django_smartbase_admin/static/sb_admin/css/ckeditor/ckeditor_content_dark.css +208 -0
- django_smartbase_admin/static/sb_admin/css/coloris/coloris.min.css +1 -0
- django_smartbase_admin/static/sb_admin/dist/calendar.js +1 -0
- django_smartbase_admin/static/sb_admin/dist/calendar_style.css +1 -0
- django_smartbase_admin/static/sb_admin/dist/calendar_style.js +0 -0
- django_smartbase_admin/static/sb_admin/dist/chart.js +1 -1
- django_smartbase_admin/static/sb_admin/dist/main.js +1 -1
- django_smartbase_admin/static/sb_admin/dist/main_style.css +1 -1
- django_smartbase_admin/static/sb_admin/dist/table.js +1 -1
- django_smartbase_admin/static/sb_admin/dist/tree_widget.js +1 -0
- django_smartbase_admin/static/sb_admin/dist/tree_widget_style.css +1 -0
- django_smartbase_admin/static/sb_admin/dist/tree_widget_style.js +0 -0
- django_smartbase_admin/static/sb_admin/fancytree/jquery.fancytree-all-deps.min.js +1 -0
- django_smartbase_admin/static/sb_admin/images/file_types/file-csv.svg +11 -0
- django_smartbase_admin/static/sb_admin/images/file_types/file-doc.svg +11 -0
- django_smartbase_admin/static/sb_admin/images/file_types/file-docx.svg +11 -0
- django_smartbase_admin/static/sb_admin/images/file_types/file-other.svg +13 -0
- django_smartbase_admin/static/sb_admin/images/file_types/file-pdf.svg +11 -0
- django_smartbase_admin/static/sb_admin/images/file_types/file-ppt.svg +11 -0
- django_smartbase_admin/static/sb_admin/images/file_types/file-xls.svg +11 -0
- django_smartbase_admin/static/sb_admin/images/file_types/file-xlsx.svg +11 -0
- django_smartbase_admin/static/sb_admin/images/file_types/file-zip.svg +18 -0
- django_smartbase_admin/static/sb_admin/images/flags/de-at.png +0 -0
- django_smartbase_admin/static/sb_admin/images/flags/de-ch.png +0 -0
- django_smartbase_admin/static/sb_admin/images/logo_light.svg +21 -0
- django_smartbase_admin/static/sb_admin/js/coloris/coloris.min.js +6 -0
- django_smartbase_admin/static/sb_admin/js/fullcalendar.min.js +14804 -0
- django_smartbase_admin/static/sb_admin/sprites/sb_admin/Bolt-one.svg +3 -0
- django_smartbase_admin/static/sb_admin/sprites/sb_admin/Calendar.svg +3 -0
- django_smartbase_admin/static/sb_admin/sprites/sb_admin/Caution.svg +3 -0
- django_smartbase_admin/static/sb_admin/sprites/sb_admin/Electric-drill.svg +3 -0
- django_smartbase_admin/static/sb_admin/sprites/sb_admin/Fire-extinguisher.svg +3 -0
- django_smartbase_admin/static/sb_admin/sprites/sb_admin/Gas.svg +3 -0
- django_smartbase_admin/static/sb_admin/sprites/sb_admin/Lightning-fill.svg +3 -0
- django_smartbase_admin/static/sb_admin/sprites/sb_admin/Moon.svg +3 -0
- django_smartbase_admin/static/sb_admin/sprites/sb_admin/Phone-telephone.svg +3 -0
- django_smartbase_admin/static/sb_admin/sprites/sb_admin/Printer.svg +3 -0
- django_smartbase_admin/static/sb_admin/sprites/sb_admin/Pull.svg +3 -0
- django_smartbase_admin/static/sb_admin/sprites/sb_admin/Sun-one.svg +3 -0
- django_smartbase_admin/static/sb_admin/sprites/sb_admin/Time.svg +3 -0
- django_smartbase_admin/static/sb_admin/src/css/_base.css +5 -1
- django_smartbase_admin/static/sb_admin/src/css/_colors.css +257 -82
- django_smartbase_admin/static/sb_admin/src/css/_components.css +61 -13
- django_smartbase_admin/static/sb_admin/src/css/_datepicker.css +8 -1
- django_smartbase_admin/static/sb_admin/src/css/_filer.css +60 -0
- django_smartbase_admin/static/sb_admin/src/css/_inlines.css +51 -10
- django_smartbase_admin/static/sb_admin/src/css/_tabulator.css +8 -2
- django_smartbase_admin/static/sb_admin/src/css/calendar.css +162 -0
- django_smartbase_admin/static/sb_admin/src/css/components/_button.css +41 -1
- django_smartbase_admin/static/sb_admin/src/css/components/_dropdown.css +26 -8
- django_smartbase_admin/static/sb_admin/src/css/components/_input.css +62 -20
- django_smartbase_admin/static/sb_admin/src/css/components/_modal.css +1 -1
- django_smartbase_admin/static/sb_admin/src/css/components/_query-builder.css +21 -2
- django_smartbase_admin/static/sb_admin/src/css/components/_toggle.css +12 -1
- django_smartbase_admin/static/sb_admin/src/css/components/_tooltip.css +8 -22
- django_smartbase_admin/static/sb_admin/src/css/style.css +17 -0
- django_smartbase_admin/static/sb_admin/src/css/tree_widget.css +411 -0
- django_smartbase_admin/static/sb_admin/src/js/autocomplete.js +63 -5
- django_smartbase_admin/static/sb_admin/src/js/calendar.js +56 -0
- django_smartbase_admin/static/sb_admin/src/js/chart.js +8 -22
- django_smartbase_admin/static/sb_admin/src/js/choices.js +18 -8
- django_smartbase_admin/static/sb_admin/src/js/datepicker.js +97 -336
- django_smartbase_admin/static/sb_admin/src/js/datepicker_plugins.js +357 -0
- django_smartbase_admin/static/sb_admin/src/js/main.js +304 -31
- django_smartbase_admin/static/sb_admin/src/js/multiselect.js +50 -41
- django_smartbase_admin/static/sb_admin/src/js/range.js +3 -2
- django_smartbase_admin/static/sb_admin/src/js/table.js +34 -5
- django_smartbase_admin/static/sb_admin/src/js/table_modules/advanced_filter_module.js +43 -20
- django_smartbase_admin/static/sb_admin/src/js/table_modules/data_edit_module.js +8 -10
- django_smartbase_admin/static/sb_admin/src/js/table_modules/filter_module.js +3 -3
- django_smartbase_admin/static/sb_admin/src/js/table_modules/header_tabs_module.js +11 -11
- django_smartbase_admin/static/sb_admin/src/js/table_modules/selection_module.js +28 -8
- django_smartbase_admin/static/sb_admin/src/js/table_modules/table_params_module.js +6 -0
- django_smartbase_admin/static/sb_admin/src/js/table_modules/views_module.js +6 -0
- django_smartbase_admin/static/sb_admin/src/js/tree_widget.js +406 -0
- django_smartbase_admin/static/sb_admin/src/js/utils.js +56 -21
- django_smartbase_admin/templates/sb_admin/actions/change_form.html +169 -114
- django_smartbase_admin/templates/sb_admin/actions/dashboard.html +2 -2
- django_smartbase_admin/templates/sb_admin/actions/list.html +79 -39
- django_smartbase_admin/templates/sb_admin/actions/partials/action_link.html +14 -0
- django_smartbase_admin/templates/sb_admin/actions/partials/tabulator_header_v2.html +2 -2
- django_smartbase_admin/templates/sb_admin/actions/tree_list.html +63 -0
- django_smartbase_admin/templates/sb_admin/authentification/login_base.html +5 -1
- django_smartbase_admin/templates/sb_admin/components/columns.html +1 -1
- django_smartbase_admin/templates/sb_admin/components/filters_v2.html +99 -85
- django_smartbase_admin/templates/sb_admin/dashboard/calendar_widget.html +69 -0
- django_smartbase_admin/templates/sb_admin/dashboard/chart_widget.html +21 -2
- django_smartbase_admin/templates/sb_admin/dashboard/list_widget.html +6 -0
- django_smartbase_admin/templates/sb_admin/dashboard/widget_base.html +1 -1
- django_smartbase_admin/templates/sb_admin/filter_widgets/advanced_filters/date_field.html +18 -8
- django_smartbase_admin/templates/sb_admin/filter_widgets/advanced_filters/multiple_choice_field.html +1 -1
- django_smartbase_admin/templates/sb_admin/filter_widgets/advanced_filters/tree_select_filter.html +2 -0
- django_smartbase_admin/templates/sb_admin/filter_widgets/date_field.html +18 -4
- django_smartbase_admin/templates/sb_admin/filter_widgets/multiple_choice_field.html +14 -0
- django_smartbase_admin/templates/sb_admin/filter_widgets/partials/clear.html +10 -5
- django_smartbase_admin/templates/sb_admin/filter_widgets/radio_choice_field.html +2 -2
- django_smartbase_admin/templates/sb_admin/filter_widgets/tree_select_filter.html +16 -0
- django_smartbase_admin/templates/sb_admin/includes/change_form_title.html +3 -1
- django_smartbase_admin/templates/sb_admin/includes/inline_fieldset.html +48 -39
- django_smartbase_admin/templates/sb_admin/includes/notifications.html +2 -1
- django_smartbase_admin/templates/sb_admin/includes/readonly_boolean_field.html +9 -0
- django_smartbase_admin/templates/sb_admin/includes/readonly_field.html +12 -0
- django_smartbase_admin/templates/sb_admin/includes/table_inline_delete_button.html +4 -5
- django_smartbase_admin/templates/sb_admin/inlines/stacked_inline.html +68 -40
- django_smartbase_admin/templates/sb_admin/inlines/table_inline.html +76 -34
- django_smartbase_admin/templates/sb_admin/integrations/filer/folder_list.html +18 -0
- django_smartbase_admin/templates/sb_admin/navigation.html +166 -158
- django_smartbase_admin/templates/sb_admin/partials/modal/modal_content.html +2 -6
- django_smartbase_admin/templates/sb_admin/sb_admin_base.html +49 -4
- django_smartbase_admin/templates/sb_admin/sb_admin_base_no_sidebar.html +27 -11
- django_smartbase_admin/templates/sb_admin/sb_admin_js_trans.html +3 -0
- django_smartbase_admin/templates/sb_admin/sprites/sb_admin.svg +1 -1
- django_smartbase_admin/templates/sb_admin/tailwind_whitelist.html +6 -3
- django_smartbase_admin/templates/sb_admin/widgets/array.html +0 -1
- django_smartbase_admin/templates/sb_admin/widgets/attributes.html +68 -0
- django_smartbase_admin/templates/sb_admin/widgets/autocomplete.html +13 -2
- django_smartbase_admin/templates/sb_admin/widgets/{checkbox_select.html → checkbox_dropdown.html} +2 -2
- django_smartbase_admin/templates/sb_admin/widgets/clearable_file_input.html +2 -2
- django_smartbase_admin/templates/sb_admin/widgets/color_field.html +30 -0
- django_smartbase_admin/templates/sb_admin/widgets/date.html +8 -1
- django_smartbase_admin/templates/sb_admin/widgets/filer_file.html +84 -0
- django_smartbase_admin/templates/sb_admin/widgets/includes/related_item_buttons.html +38 -0
- django_smartbase_admin/templates/sb_admin/widgets/multiwidget.html +1 -1
- django_smartbase_admin/templates/sb_admin/widgets/radio.html +3 -2
- django_smartbase_admin/templates/sb_admin/widgets/radio_dropdown.html +30 -0
- django_smartbase_admin/templates/sb_admin/widgets/read_only_password_hash.html +3 -0
- django_smartbase_admin/templates/sb_admin/widgets/time.html +8 -1
- django_smartbase_admin/templates/sb_admin/widgets/toggle.html +1 -1
- django_smartbase_admin/templates/sb_admin/widgets/tree_base.html +59 -0
- django_smartbase_admin/templates/sb_admin/widgets/tree_select.html +24 -0
- django_smartbase_admin/templates/sb_admin/widgets/tree_select_inline.html +12 -0
- django_smartbase_admin/templatetags/sb_admin_tags.py +85 -4
- django_smartbase_admin/utils.py +22 -3
- django_smartbase_admin/views/dashboard_view.py +6 -0
- django_smartbase_admin/views/global_filter_view.py +8 -2
- django_smartbase_admin/views/translations_view.py +12 -5
- django_smartbase_admin/views/user_config_view.py +52 -0
- django_smartbase_admin-1.0.38.dist-info/METADATA +166 -0
- {django_smartbase_admin-0.2.54.dist-info → django_smartbase_admin-1.0.38.dist-info}/RECORD +177 -115
- {django_smartbase_admin-0.2.54.dist-info → django_smartbase_admin-1.0.38.dist-info}/WHEEL +1 -1
- django_smartbase_admin/templates/sb_admin/integrations/sorting/change_list.html +0 -401
- django_smartbase_admin-0.2.54.dist-info/METADATA +0 -25
- {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.
|
|
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__(
|
|
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,
|
|
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/
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
{
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|