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.
- django_smartbase_admin/actions/admin_action_list.py +80 -51
- django_smartbase_admin/actions/advanced_filters.py +55 -20
- django_smartbase_admin/admin/admin_base.py +477 -89
- django_smartbase_admin/admin/site.py +104 -34
- django_smartbase_admin/admin/widgets.py +598 -26
- django_smartbase_admin/apps.py +2 -0
- django_smartbase_admin/engine/actions.py +34 -16
- django_smartbase_admin/engine/admin_base_view.py +253 -115
- django_smartbase_admin/engine/configuration.py +186 -4
- django_smartbase_admin/engine/const.py +7 -0
- django_smartbase_admin/engine/dashboard.py +44 -23
- django_smartbase_admin/engine/fake_inline.py +44 -7
- django_smartbase_admin/engine/field.py +54 -10
- django_smartbase_admin/engine/field_formatter.py +32 -9
- django_smartbase_admin/engine/filter_widgets.py +356 -21
- 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 +82 -27
- 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/table.js.LICENSE.txt +9 -0
- 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 +66 -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 +307 -26
- 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/sb_ajax_params_tabulator_modifier.js +21 -0
- django_smartbase_admin/static/sb_admin/src/js/table.js +38 -13
- 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 +19 -3
- 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 -117
- django_smartbase_admin/templates/sb_admin/actions/dashboard.html +2 -2
- django_smartbase_admin/templates/sb_admin/actions/delete_selected_confirmation.html +56 -32
- django_smartbase_admin/templates/sb_admin/actions/list.html +79 -42
- django_smartbase_admin/templates/sb_admin/actions/object_history.html +2 -2
- django_smartbase_admin/templates/sb_admin/actions/partials/action_link.html +14 -0
- django_smartbase_admin/templates/sb_admin/actions/partials/selected_rows_actions.html +2 -2
- 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/config/view.html +0 -1
- 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/components.html +5 -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 +78 -36
- 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 +35 -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/checkbox_group.html +15 -0
- 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/html_read_only.html +1 -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 +163 -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.47.dist-info → django_smartbase_admin-1.0.38.dist-info}/RECORD +186 -121
- {django_smartbase_admin-0.2.47.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.47.dist-info/METADATA +0 -25
- {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.
|
|
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=()):
|
|
@@ -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,
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
{
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|