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,23 +1,45 @@
1
1
  import json
2
+ import logging
2
3
  import urllib.parse
4
+ from collections.abc import Iterable
5
+ from functools import partial
6
+ from typing import Any
7
+ from urllib.parse import urlparse
3
8
 
4
9
  from ckeditor.fields import RichTextFormField
5
10
  from ckeditor_uploader.fields import RichTextUploadingFormField
6
11
  from django import forms
7
12
  from django.contrib import admin
13
+ from django.contrib.admin.options import get_content_type_for_model
14
+ from django.contrib.admin.utils import unquote
8
15
  from django.contrib.admin.widgets import AdminTextareaWidget
9
16
  from django.contrib.auth.forms import UsernameField, ReadOnlyPasswordHashWidget
10
- from django.contrib.postgres.forms import SimpleArrayField
11
- from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
17
+ from django.contrib.contenttypes.forms import BaseGenericInlineFormSet
18
+ from django.contrib.contenttypes.models import ContentType
19
+ from django.core.exceptions import (
20
+ FieldDoesNotExist,
21
+ ImproperlyConfigured,
22
+ PermissionDenied,
23
+ )
12
24
  from django.db import models
25
+ from django.db.models import QuerySet, Q, Model
13
26
  from django.forms import HiddenInput
14
- from django.forms.models import ModelFormMetaclass
27
+ from django.forms.models import (
28
+ ModelFormMetaclass,
29
+ modelform_factory,
30
+ )
31
+ from django.http import HttpResponse
15
32
  from django.template.loader import render_to_string
16
- from django.urls import reverse
17
- from django.utils.safestring import mark_safe
33
+ from django.template.response import TemplateResponse
34
+ from django.urls import reverse, NoReverseMatch, resolve
35
+ from django.utils.safestring import mark_safe, SafeString
36
+ from django.utils.text import capfirst
18
37
  from django.utils.translation import gettext_lazy as _
19
38
  from django_admin_inline_paginator.admin import TabularInlinePaginated
20
- from filer.fields.image import AdminImageFormField
39
+ from django_htmx.http import trigger_client_event
40
+ from filer.fields.file import FilerFileField
41
+ from filer.fields.image import AdminImageFormField, FilerImageField
42
+ from nested_admin.formsets import NestedInlineFormSet
21
43
  from nested_admin.nested import (
22
44
  NestedModelAdmin,
23
45
  NestedTabularInline,
@@ -26,12 +48,14 @@ from nested_admin.nested import (
26
48
  NestedGenericStackedInline,
27
49
  )
28
50
 
29
- from django_smartbase_admin.actions.admin_action_list import SBAdminListAction
30
51
  from django_smartbase_admin.engine.actions import SBAdminCustomAction
31
- from django_smartbase_admin.utils import FormFieldsetMixin
52
+ from django_smartbase_admin.services.thread_local import SBAdminThreadLocalService
53
+ from django_smartbase_admin.utils import FormFieldsetMixin, is_modal
32
54
 
33
55
  parler_enabled = None
34
56
  try:
57
+ from parler.admin import TranslatableAdmin
58
+
35
59
  from parler.forms import (
36
60
  TranslatableModelForm,
37
61
  TranslatableModelFormMetaclass,
@@ -45,6 +69,30 @@ try:
45
69
  except ImportError:
46
70
  pass
47
71
 
72
+ postrgres_enabled = None
73
+ try:
74
+ from django.contrib.postgres.forms import SimpleArrayField
75
+
76
+ postrgres_enabled = True
77
+ except ImportError:
78
+ pass
79
+
80
+ django_cms_attributes = None
81
+ try:
82
+ from djangocms_attributes_field.fields import AttributesFormField
83
+
84
+ django_cms_attributes = True
85
+ except ImportError:
86
+ pass
87
+
88
+ color_field_enabled = None
89
+ try:
90
+ from colorfield.fields import ColorField
91
+
92
+ color_field_enabled = True
93
+ except ImportError:
94
+ pass
95
+
48
96
  from django_smartbase_admin.admin.widgets import (
49
97
  SBAdminTextInputWidget,
50
98
  SBAdminTextareaWidget,
@@ -57,7 +105,6 @@ from django_smartbase_admin.admin.widgets import (
57
105
  SBAdminSplitDateTimeWidget,
58
106
  SBAdminTimeWidget,
59
107
  SBAdminDateTimeWidget,
60
- SBAdminAutocompleteWidget,
61
108
  SBAdminFileWidget,
62
109
  SBAdminToggleWidget,
63
110
  SBAdminNullBooleanSelectWidget,
@@ -66,19 +113,32 @@ from django_smartbase_admin.admin.widgets import (
66
113
  SBAdminPasswordInputWidget,
67
114
  SBAdminReadOnlyPasswordHashWidget,
68
115
  SBAdminHiddenWidget,
116
+ SBAdminCKEditorUploadingWidget,
117
+ SBAdminAttributesWidget,
118
+ SBAdminMultipleChoiceInlineWidget,
119
+ SBAdminColorWidget,
120
+ SBAdminFilerFileWidget,
69
121
  )
70
122
  from django_smartbase_admin.engine.admin_base_view import (
71
123
  SBAdminBaseListView,
72
124
  SBAdminBaseView,
73
125
  SBAdminBaseQuerysetMixin,
126
+ SBADMIN_IS_MODAL_VAR,
127
+ SBADMIN_PARENT_INSTANCE_PK_VAR,
128
+ SBADMIN_PARENT_INSTANCE_LABEL_VAR,
129
+ SBADMIN_PARENT_INSTANCE_FIELD_NAME_VAR,
130
+ SBADMIN_RELOAD_ON_SAVE_VAR,
74
131
  )
75
132
  from django_smartbase_admin.engine.const import (
76
133
  OBJECT_ID_PLACEHOLDER,
77
134
  TRANSLATIONS_SELECTED_LANGUAGES,
135
+ ROW_CLASS_FIELD,
78
136
  )
79
137
  from django_smartbase_admin.services.translations import SBAdminTranslationsService
80
138
  from django_smartbase_admin.services.views import SBAdminViewService
81
139
 
140
+ logger = logging.getLogger(__name__)
141
+
82
142
 
83
143
  class SBAdminFormFieldWidgetsMixin:
84
144
  formfield_widgets = {
@@ -101,15 +161,26 @@ class SBAdminFormFieldWidgetsMixin:
101
161
  forms.BooleanField: SBAdminToggleWidget,
102
162
  forms.SlugField: SBAdminTextInputWidget,
103
163
  RichTextFormField: SBAdminCKEditorWidget,
104
- RichTextUploadingFormField: SBAdminCKEditorWidget,
164
+ RichTextUploadingFormField: SBAdminCKEditorUploadingWidget,
105
165
  forms.ChoiceField: SBAdminSelectWidget,
106
166
  forms.TypedChoiceField: SBAdminSelectWidget,
167
+ forms.MultipleChoiceField: SBAdminMultipleChoiceInlineWidget,
168
+ forms.TypedMultipleChoiceField: SBAdminMultipleChoiceInlineWidget,
107
169
  forms.NullBooleanField: SBAdminNullBooleanSelectWidget,
108
- SimpleArrayField: SBAdminArrayWidget,
109
170
  AdminImageFormField: SBAdminImageWidget,
110
171
  ReadOnlyPasswordHashWidget: SBAdminReadOnlyPasswordHashWidget,
111
172
  forms.HiddenInput: SBAdminHiddenWidget,
112
173
  }
174
+ db_field_widgets = {
175
+ FilerImageField: SBAdminFilerFileWidget,
176
+ FilerFileField: SBAdminFilerFileWidget,
177
+ }
178
+ if postrgres_enabled:
179
+ formfield_widgets[SimpleArrayField] = SBAdminArrayWidget
180
+ if django_cms_attributes:
181
+ formfield_widgets[AttributesFormField] = SBAdminAttributesWidget
182
+ if color_field_enabled:
183
+ db_field_widgets[ColorField] = SBAdminColorWidget
113
184
 
114
185
  django_widget_to_widget = {
115
186
  forms.PasswordInput: SBAdminPasswordInputWidget,
@@ -117,7 +188,9 @@ class SBAdminFormFieldWidgetsMixin:
117
188
  }
118
189
 
119
190
  def get_form_field_widget_class(self, form_field, db_field, request):
120
- default_widget_class = self.formfield_widgets.get(form_field.__class__)
191
+ default_widget_class = self.db_field_widgets.get(
192
+ db_field.__class__, self.formfield_widgets.get(form_field.__class__)
193
+ )
121
194
  if not hasattr(request, "request_data"):
122
195
  # in case of login the view is not wrapped and we have no request_data present
123
196
  return default_widget_class
@@ -153,7 +226,19 @@ class SBAdminFormFieldWidgetsMixin:
153
226
  widget_attrs.pop(
154
227
  "class", None
155
228
  ) # remove origin classes to prevent override our custom widget class
156
- form_field.widget = widget(form_field=form_field, attrs=widget_attrs)
229
+ kwargs = {}
230
+ if isinstance(form_field, RichTextFormField):
231
+ kwargs["config_name"] = getattr(
232
+ form_field.widget, "config_name", None
233
+ ) or getattr(db_field, "config_name", "default")
234
+
235
+ kwargs["external_plugin_resources"] = getattr(
236
+ form_field.widget, "external_plugin_resources", None
237
+ ) or getattr(db_field, "external_plugin_resources", [])
238
+ kwargs["extra_plugins"] = getattr(
239
+ form_field.widget, "extra_plugins", None
240
+ ) or getattr(db_field, "extra_plugins", [])
241
+ form_field.widget = widget(form_field=form_field, attrs=widget_attrs, **kwargs)
157
242
  return form_field
158
243
 
159
244
  def formfield_for_foreignkey(self, db_field, request, **kwargs):
@@ -225,13 +310,21 @@ class SBAdminFormFieldWidgetsMixin:
225
310
 
226
311
 
227
312
  class SBAdminBaseFormInit(SBAdminFormFieldWidgetsMixin, FormFieldsetMixin):
228
- threadsafe_request = None
229
313
  view = None
230
314
 
231
315
  def __init__(self, *args, **kwargs):
232
316
  self.view = kwargs.pop("view", self.view)
233
- self.threadsafe_request = kwargs.pop("request", self.threadsafe_request)
317
+ threadsafe_request = kwargs.pop(
318
+ "request", SBAdminThreadLocalService.get_request()
319
+ )
234
320
  super().__init__(*args, **kwargs)
321
+ self.init_widgets_dynamic(threadsafe_request)
322
+ for field in self.declared_fields:
323
+ form_field = self.fields.get(field)
324
+ if form_field:
325
+ self.assign_widget_to_form_field(form_field, request=threadsafe_request)
326
+
327
+ def init_widgets_dynamic(self, request):
235
328
  for field in self.fields:
236
329
  if not hasattr(self.fields[field].widget, "init_widget_dynamic"):
237
330
  continue
@@ -240,19 +333,11 @@ class SBAdminBaseFormInit(SBAdminFormFieldWidgetsMixin, FormFieldsetMixin):
240
333
  self.fields[field],
241
334
  field,
242
335
  self.view,
243
- self.threadsafe_request,
336
+ request,
244
337
  )
245
- for field in self.declared_fields:
246
- form_field = self.fields.get(field)
247
- if form_field:
248
- self.assign_widget_to_form_field(
249
- form_field, request=self.threadsafe_request
250
- )
251
338
 
252
339
 
253
- class SBAdminBaseForm(
254
- SBAdminBaseFormInit, forms.ModelForm, SBAdminFormFieldWidgetsMixin
255
- ):
340
+ class SBAdminBaseForm(SBAdminBaseFormInit, forms.ModelForm):
256
341
  pass
257
342
 
258
343
 
@@ -395,6 +480,7 @@ if parler_enabled:
395
480
 
396
481
  class SBAdminInlineAndAdminCommon(SBAdminFormFieldWidgetsMixin):
397
482
  sbadmin_fake_inlines = None
483
+ all_base_fields_form = None
398
484
 
399
485
  def init_view_static(self, configuration, model, admin_site):
400
486
  configuration.view_map[self.get_id()] = self
@@ -405,35 +491,33 @@ class SBAdminInlineAndAdminCommon(SBAdminFormFieldWidgetsMixin):
405
491
  for inline_view in inlines:
406
492
  if issubclass(inline_view, SBAdminInline):
407
493
  inline_view_instance = inline_view(model, admin_site)
408
- configuration.view_map[inline_view_instance.get_id()] = (
409
- inline_view_instance
410
- )
411
494
  inline_view_instance.init_view_static(
412
495
  configuration, inline_view_instance.model, admin_site
413
496
  )
414
497
 
415
- def get_sbadmin_fake_inlines(self, request, obj):
498
+ def get_sbadmin_fake_inlines(self, request, obj) -> Iterable:
416
499
  return self.sbadmin_fake_inlines or []
417
500
 
418
- def get_inline_instances(self, request, obj=None):
501
+ def get_inline_instances(self, request, obj=None) -> list:
419
502
  inline_classes = self.get_inlines(request, obj)
420
503
  inline_classes = [*inline_classes] or []
421
504
  inline_classes.extend(self.get_sbadmin_fake_inlines(request, obj))
422
505
  inlines = []
423
506
  for inline_class in inline_classes:
424
507
  inline = inline_class(self.model, self.admin_site)
508
+ if hasattr(inline, "init_inline_dynamic"):
509
+ inline.init_inline_dynamic(request, obj)
425
510
  if request:
426
511
  if not inline.has_view_or_change_permission(request, obj):
427
512
  continue
428
513
  if not inline.has_add_permission(request, obj):
429
514
  inline.max_num = 0
430
- if hasattr(inline, "init_inline_dynamic"):
431
- inline.init_inline_dynamic(request, obj)
515
+ if hasattr(inline, "init_view_dynamic"):
432
516
  inline.init_view_dynamic(request, request.request_data)
433
517
  inlines.append(inline)
434
518
  return inlines
435
519
 
436
- def init_view_dynamic(self, request, request_data=None, **kwargs):
520
+ def init_view_dynamic(self, request, request_data=None, **kwargs) -> None:
437
521
  if SBAdminTranslationsService.is_translated_model(self.model):
438
522
  has_default_form = (
439
523
  self.form == TranslatableModelForm or self.form == forms.ModelForm
@@ -445,23 +529,26 @@ class SBAdminInlineAndAdminCommon(SBAdminFormFieldWidgetsMixin):
445
529
  f"Admin '{self}' form class '{self.form}' needs to extend SBTranslatableModelForm in case of translatable model."
446
530
  )
447
531
  super().init_view_dynamic(request, request_data, **kwargs)
448
- self.initialize_form_class(self.form)
532
+ self.initialize_form_class(self.form, request)
449
533
 
450
- def initialize_form_class(self, form):
534
+ def initialize_form_class(self, form, request) -> None:
451
535
  if form:
452
536
  form.view = self
453
537
 
454
- def initialize_form_class_threadsafe(self, form, request):
455
- self.initialize_form_class(form)
456
- if form:
457
- form.threadsafe_request = request
538
+ def initialize_all_base_fields_form(self, request) -> None:
539
+ params = {
540
+ "form": self.form,
541
+ "fields": "__all__",
542
+ "formfield_callback": partial(self.formfield_for_dbfield, request=request),
543
+ }
544
+ self.all_base_fields_form = modelform_factory(self.model, **params)
458
545
 
459
546
 
460
547
  class SBAdminThirdParty(SBAdminInlineAndAdminCommon, SBAdminBaseView):
461
- def get_menu_view_url(self, request):
548
+ def get_menu_view_url(self, request) -> str:
462
549
  return reverse(f"sb_admin:{self.get_id()}_changelist")
463
550
 
464
- def get_id(self):
551
+ def get_id(self) -> str:
465
552
  return self.get_model_path()
466
553
 
467
554
  def change_view(self, request, object_id, form_url="", extra_context=None):
@@ -474,10 +561,14 @@ class SBAdminThirdParty(SBAdminInlineAndAdminCommon, SBAdminBaseView):
474
561
  extra_context.update(self.get_global_context(request))
475
562
  return super().changelist_view(request, extra_context)
476
563
 
477
- def get_action_url(self, action, modifier="template"):
564
+ def get_action_url(self, action, modifier="template") -> str:
478
565
  return reverse(
479
- f"sb_admin:{self.get_id()}_action",
480
- kwargs={"action": action, "modifier": modifier},
566
+ "sb_admin:sb_admin_base",
567
+ kwargs={
568
+ "view": self.get_id(),
569
+ "action": action,
570
+ "modifier": modifier,
571
+ },
481
572
  )
482
573
 
483
574
 
@@ -489,7 +580,7 @@ class SBAdminTranslationStatusMixin:
489
580
  main_language_code,
490
581
  current_lang_code,
491
582
  translations_edit_url,
492
- ):
583
+ ) -> dict[str, Any]:
493
584
  language_code = language[0]
494
585
  language_title = language[1]
495
586
  this_lang_count = languages_count.get(language_code, 0)
@@ -525,11 +616,11 @@ class SBAdminTranslationStatusMixin:
525
616
  }
526
617
 
527
618
  @classmethod
528
- def get_empty_state(cls):
619
+ def get_empty_state(cls) -> SafeString:
529
620
  return mark_safe("<div class='is-empty'></div>")
530
621
 
531
622
  @admin.display(description="")
532
- def sbadmin_translation_status(self, obj):
623
+ def sbadmin_translation_status(self, obj) -> SafeString:
533
624
  if not SBAdminTranslationsService.is_i18n_enabled():
534
625
  return self.get_empty_state()
535
626
 
@@ -573,6 +664,29 @@ class SBAdminTranslationStatusMixin:
573
664
  return mark_safe(result)
574
665
 
575
666
 
667
+ class SBAdminInlineFormSetMixin:
668
+ @classmethod
669
+ def get_default_prefix(cls):
670
+ view = getattr(cls.form, "view", None)
671
+ if view and view.parent_model and view.opts:
672
+ parent_opts = view.parent_model._meta
673
+ opts = view.opts
674
+ modal_prefix = (
675
+ "modal_" if is_modal(SBAdminThreadLocalService.get_request()) else ""
676
+ )
677
+ return f"{modal_prefix}{parent_opts.app_label}_{parent_opts.model_name}_{opts.app_label}-{opts.model_name}"
678
+
679
+ return super().get_default_prefix()
680
+
681
+
682
+ class SBAdminGenericInlineFormSet(SBAdminInlineFormSetMixin, BaseGenericInlineFormSet):
683
+ pass
684
+
685
+
686
+ class SBAdminNestedInlineFormSet(SBAdminInlineFormSetMixin, NestedInlineFormSet):
687
+ pass
688
+
689
+
576
690
  class SBAdmin(
577
691
  SBAdminInlineAndAdminCommon,
578
692
  SBAdminBaseQuerysetMixin,
@@ -581,8 +695,8 @@ class SBAdmin(
581
695
  NestedModelAdmin,
582
696
  ):
583
697
  change_list_template = "sb_admin/actions/list.html"
698
+ reorder_list_template = "sb_admin/actions/list.html"
584
699
  change_form_template = "sb_admin/actions/change_form.html"
585
- delete_confirmation_template = "sb_admin/actions/delete_confirmation.html"
586
700
  delete_selected_confirmation_template = (
587
701
  "sb_admin/actions/delete_selected_confirmation.html"
588
702
  )
@@ -593,19 +707,29 @@ class SBAdmin(
593
707
  sbadmin_tabs = None
594
708
  request_data = None
595
709
  menu_label = None
710
+ sbadmin_is_generic_model = False
711
+
712
+ def save_formset(self, request, form, formset, change):
713
+ if not change and hasattr(formset, "inline_instance"):
714
+ # update inline_instance parent_instance on formset when creating new object
715
+ formset.inline_instance.parent_instance = form.instance
716
+ super().save_formset(request, form, formset, change)
596
717
 
597
- def get_sbadmin_list_filter(self, request):
718
+ def get_sbadmin_list_filter(self, request) -> Iterable:
598
719
  return self.sbadmin_list_filter or self.get_list_filter(request)
599
720
 
600
721
  def get_form(self, request, obj=None, **kwargs):
722
+ self.initialize_all_base_fields_form(request)
601
723
  form = super().get_form(request, obj, **kwargs)
602
- self.initialize_form_class_threadsafe(form, request)
724
+ self.initialize_form_class(form, request)
603
725
  return form
604
726
 
605
- def get_id(self):
727
+ def get_id(self) -> str:
606
728
  return self.get_model_path()
607
729
 
608
- def get_sbadmin_fieldsets(self, request, object_id=None):
730
+ def get_sbadmin_fieldsets(
731
+ self, request, object_id=None
732
+ ) -> list[tuple[str | None, dict[str, Any]]]:
609
733
  fieldsets = self.sbadmin_fieldsets or self.fieldsets
610
734
  if fieldsets:
611
735
  return fieldsets
@@ -614,9 +738,12 @@ class SBAdmin(
614
738
  )
615
739
 
616
740
  def register_autocomplete_views(self, request):
741
+ super().register_autocomplete_views(request)
617
742
  self.get_form(request)()
618
743
 
619
- def get_fieldsets(self, request, obj):
744
+ def get_fieldsets(
745
+ self, request, obj=None
746
+ ) -> list[tuple[str | None, dict[str, Any]]]:
620
747
  fieldsets = []
621
748
  object_id = obj.id if obj else None
622
749
  for fieldset in self.get_sbadmin_fieldsets(request, object_id):
@@ -631,7 +758,9 @@ class SBAdmin(
631
758
  fieldsets.append(fieldset_django)
632
759
  return fieldsets
633
760
 
634
- def get_fieldsets_context(self, request, object_id):
761
+ def get_fieldsets_context(
762
+ self, request, object_id
763
+ ) -> dict[str, dict[str | None, dict[str, Any]]]:
635
764
  fielsets_context = {}
636
765
  for fieldset in self.get_sbadmin_fieldsets(request, object_id):
637
766
  actions = fieldset[1].get("actions", [])
@@ -645,53 +774,69 @@ class SBAdmin(
645
774
  fielsets_context[fieldset[0]] = fieldset[1]
646
775
  return {"fieldsets_context": fielsets_context}
647
776
 
648
- def get_sbadmin_tabs(self, request, object_id):
777
+ def get_sbadmin_tabs(self, request, object_id) -> Iterable:
649
778
  return self.sbadmin_tabs
650
779
 
651
- def get_tabs_context(self, request, object_id):
780
+ def get_tabs_context(self, request, object_id) -> dict[str, Iterable]:
652
781
  return {"tabs_context": self.get_sbadmin_tabs(request, object_id)}
653
782
 
654
- def get_context_data(self, request):
783
+ def get_context_data(self, request) -> dict[str, Any]:
655
784
  return {
656
785
  "base_change_list_template": self.change_list_template,
657
786
  }
658
787
 
659
- def get_menu_view_url(self, request):
788
+ def get_menu_view_url(self, request) -> str:
660
789
  all_config = self.get_all_config(request)
661
790
  url_suffix = ""
662
791
  if all_config and all_config.get("all_params_changed", False):
663
- url_params_dict = all_config.get("url_params")
792
+ url_params_dict = SBAdminViewService.process_url_params(
793
+ view_id=self.get_id(),
794
+ url_params=all_config.get("url_params"),
795
+ filter_version=self.get_filters_version(request),
796
+ )
664
797
  if url_params_dict:
665
798
  url_suffix = f"?{SBAdminViewService.build_list_url(self.get_id(), url_params_dict)}"
666
799
 
667
800
  return f'{reverse(f"sb_admin:{self.get_id()}_changelist")}{url_suffix}'
668
801
 
669
- def get_menu_label(self):
802
+ def get_menu_label(self) -> str:
670
803
  return self.menu_label or self.model._meta.verbose_name_plural
671
804
 
672
- def get_action_url(self, action, modifier="template"):
805
+ def get_action_url(self, action, modifier="template") -> str:
673
806
  if not hasattr(self, action):
674
807
  raise ImproperlyConfigured(f"Action {action} does not exist on {self}")
675
808
  return reverse(
676
- f"sb_admin:{self.get_id()}_action",
809
+ "sb_admin:sb_admin_base",
677
810
  kwargs={
811
+ "view": self.get_id(),
678
812
  "action": action,
679
- "modifier": (
680
- urllib.parse.quote(str(modifier), safe="") if modifier else None
681
- ),
813
+ "modifier": modifier,
682
814
  },
683
815
  )
684
816
 
685
- def get_detail_url(self, object_id=None):
817
+ def get_detail_url(self, object_id=None) -> str:
686
818
  return reverse(
687
819
  f"sb_admin:{self.get_id()}_change",
688
820
  kwargs={"object_id": object_id or OBJECT_ID_PLACEHOLDER},
689
821
  )
690
822
 
691
- def get_new_url(self):
823
+ def get_new_url(self, request) -> str:
692
824
  return reverse(f"sb_admin:{self.get_id()}_add")
693
825
 
694
- def get_previous_next_context(self, request, object_id):
826
+ def get_additional_filter_for_previous_next_context(self, request, object_id) -> Q:
827
+ return Q()
828
+
829
+ def get_change_view_context(self, request, object_id) -> dict | dict[str, Any]:
830
+ return {
831
+ "show_back_button": True,
832
+ "back_url": reverse(
833
+ "sb_admin:{}_{}_changelist".format(
834
+ self.opts.app_label, self.opts.model_name
835
+ )
836
+ ),
837
+ }
838
+
839
+ def get_previous_next_context(self, request, object_id) -> dict | dict[str, Any]:
695
840
  if not self.sbadmin_previous_next_buttons_enabled or not object_id:
696
841
  return {}
697
842
  changelist_filters = request.GET.get("_changelist_filters", "")
@@ -703,15 +848,20 @@ class SBAdmin(
703
848
  )
704
849
  except:
705
850
  all_params = {}
706
- list_action = SBAdminListAction(self, request, all_params=all_params)
851
+ list_action = self.sbadmin_list_action_class(
852
+ self, request, all_params=all_params
853
+ )
854
+ additional_filter = self.get_additional_filter_for_previous_next_context(
855
+ request, object_id
856
+ )
707
857
  all_ids = list(
708
- list_action.build_final_data_count_queryset()
858
+ list_action.build_final_data_count_queryset(additional_filter)
709
859
  .order_by(*list_action.get_order_by_from_request())
710
860
  .values_list("id", flat=True)
711
861
  )
712
862
  index = all_ids.index(int(object_id))
713
- previous_id = None if index == 0 else all_ids[index - 1]
714
- next_id = None if index == len(all_ids) - 1 else all_ids[index + 1]
863
+ previous_id = all_ids[-1] if index == 0 else all_ids[index - 1]
864
+ next_id = all_ids[0] if index == len(all_ids) - 1 else all_ids[index + 1]
715
865
  return {
716
866
  "previous_url": (
717
867
  f"{self.get_detail_url(previous_id)}?_changelist_filters={changelist_filters}"
@@ -727,8 +877,16 @@ class SBAdmin(
727
877
  ),
728
878
  }
729
879
 
880
+ def add_view(self, request, form_url="", extra_context=None):
881
+ extra_context = extra_context or {}
882
+ extra_context.update(self.get_global_context(request, None))
883
+ extra_context.update(self.get_fieldsets_context(request, None))
884
+ extra_context.update(self.get_tabs_context(request, None))
885
+ return self.changeform_view(request, None, form_url, extra_context)
886
+
730
887
  def change_view(self, request, object_id, form_url="", extra_context=None):
731
888
  extra_context = extra_context or {}
889
+ extra_context.update(self.get_change_view_context(request, object_id))
732
890
  extra_context.update(self.get_global_context(request, object_id))
733
891
  extra_context.update(self.get_fieldsets_context(request, object_id))
734
892
  extra_context.update(self.get_tabs_context(request, object_id))
@@ -738,6 +896,116 @@ class SBAdmin(
738
896
  def changelist_view(self, request, extra_context=None):
739
897
  return self.action_list(request, extra_context=extra_context)
740
898
 
899
+ def history_view(self, request, object_id, extra_context=None):
900
+ try:
901
+ "The 'history' admin view for this model."
902
+ from django.contrib.admin.models import LogEntry
903
+ from django.contrib.admin.views.main import PAGE_VAR
904
+
905
+ # First check if the user can see this history.
906
+ model = self.model
907
+ obj = self.get_object(request, unquote(object_id))
908
+ if obj is None:
909
+ return self._get_obj_does_not_exist_redirect(
910
+ request, model._meta, object_id
911
+ )
912
+
913
+ if not self.has_view_or_change_permission(request, obj):
914
+ raise PermissionDenied
915
+
916
+ # Then get the history for this object.
917
+ app_label = self.opts.app_label
918
+ action_list = (
919
+ LogEntry.objects.filter(
920
+ object_id=unquote(object_id),
921
+ content_type=get_content_type_for_model(model),
922
+ )
923
+ .select_related()
924
+ .order_by("-action_time")
925
+ )
926
+
927
+ paginator = self.get_paginator(request, action_list, 100)
928
+ page_number = request.GET.get(PAGE_VAR, 1)
929
+ page_obj = paginator.get_page(page_number)
930
+ page_range = paginator.get_elided_page_range(page_obj.number)
931
+
932
+ context = {
933
+ **self.admin_site.each_context(request),
934
+ "title": _("Change history: %s") % obj,
935
+ "subtitle": None,
936
+ "action_list": page_obj,
937
+ "page_range": page_range,
938
+ "page_var": PAGE_VAR,
939
+ "pagination_required": paginator.count > 100,
940
+ "module_name": str(capfirst(self.opts.verbose_name_plural)),
941
+ "object": obj,
942
+ "opts": self.opts,
943
+ "preserved_filters": self.get_preserved_filters(request),
944
+ **(extra_context or {}),
945
+ }
946
+
947
+ request.current_app = self.admin_site.name
948
+
949
+ return TemplateResponse(
950
+ request,
951
+ self.object_history_template
952
+ or [
953
+ "admin/%s/%s/object_history.html"
954
+ % (app_label, self.opts.model_name),
955
+ "admin/%s/object_history.html" % app_label,
956
+ "admin/object_history.html",
957
+ ],
958
+ context,
959
+ )
960
+ except Exception as e:
961
+ return super().history_view(request, object_id, extra_context)
962
+
963
+ @classmethod
964
+ def get_modal_save_response(cls, request, obj):
965
+ response = HttpResponse()
966
+ trigger_client_event(
967
+ response,
968
+ "sbadmin:modal-change-form-response",
969
+ {
970
+ "field": request.POST.get("sb_admin_source_field"),
971
+ "id": obj.pk,
972
+ "label": str(obj),
973
+ "reload": request.POST.get(SBADMIN_RELOAD_ON_SAVE_VAR) == "1",
974
+ },
975
+ )
976
+ trigger_client_event(response, "hideModal", {"elt": "sb-admin-modal"})
977
+ return response
978
+
979
+ def response_add(self, request, obj, post_url_continue=None):
980
+ if is_modal(request):
981
+ return self.get_modal_save_response(request, obj)
982
+ return super().response_add(request, obj, post_url_continue)
983
+
984
+ def response_change(self, request, obj):
985
+ if is_modal(request):
986
+ return self.get_modal_save_response(request, obj)
987
+ return super().response_change(request, obj)
988
+
989
+ @classmethod
990
+ def set_generic_relation_from_parent(cls, request, obj):
991
+ parent_model_path = request.POST.get(SBADMIN_PARENT_INSTANCE_FIELD_NAME_VAR)
992
+ parent_pk = request.POST.get(SBADMIN_PARENT_INSTANCE_PK_VAR)
993
+
994
+ if parent_model_path and parent_pk:
995
+ prefix, app_label, model_name, field, parent_model = (
996
+ parent_model_path.split("_", 5)
997
+ )
998
+ content_type = ContentType.objects.get(
999
+ app_label=app_label, model=parent_model
1000
+ )
1001
+ obj.content_type = content_type
1002
+ obj.object_id = int(parent_pk)
1003
+
1004
+ def save_model(self, request, obj, form, change):
1005
+ if self.sbadmin_is_generic_model and SBADMIN_IS_MODAL_VAR in request.POST:
1006
+ self.set_generic_relation_from_parent(request, obj)
1007
+ super().save_model(request, obj, form, change)
1008
+
741
1009
 
742
1010
  class SBAdminInline(
743
1011
  SBAdminInlineAndAdminCommon, SBAdminBaseQuerysetMixin, SBAdminBaseView
@@ -748,21 +1016,43 @@ class SBAdminInline(
748
1016
  sbadmin_inline_list_actions = None
749
1017
  extra = 0
750
1018
  ordering = None
751
-
752
- def get_ordering(self, request):
1019
+ all_base_fields_form = None
1020
+ sb_admin_add_modal = False
1021
+
1022
+ def get_instance_label(self, request, obj: Model | None = None) -> str | None:
1023
+ if obj:
1024
+ return str(obj)
1025
+ return None
1026
+
1027
+ def get_readonly_fields(self, request, obj=None):
1028
+ readonly_fields = super().get_readonly_fields(request, obj)
1029
+ if ROW_CLASS_FIELD not in readonly_fields:
1030
+ readonly_fields += (ROW_CLASS_FIELD,)
1031
+ return readonly_fields
1032
+
1033
+ def get_fields(self, request, obj=None):
1034
+ fields = super().get_fields(request, obj)
1035
+ if ROW_CLASS_FIELD not in fields:
1036
+ fields += (ROW_CLASS_FIELD,)
1037
+ return fields
1038
+
1039
+ def get_sbadmin_row_class(self, obj):
1040
+ return ""
1041
+
1042
+ def get_ordering(self, request) -> tuple[str]:
753
1043
  """
754
1044
  Hook for specifying field ordering.
755
1045
  """
756
1046
  return self.ordering or ("-id",)
757
1047
 
758
- def get_queryset(self, request=None):
1048
+ def get_queryset(self, request=None) -> QuerySet:
759
1049
  qs = super().get_queryset(request)
760
1050
  return qs.order_by(*self.get_ordering(request))
761
1051
 
762
- def get_sbadmin_inline_list_actions(self):
1052
+ def get_sbadmin_inline_list_actions(self, request) -> list:
763
1053
  return [*(self.sbadmin_inline_list_actions or [])]
764
1054
 
765
- def get_action_url(self, action, modifier="template"):
1055
+ def get_action_url(self, action, modifier="template") -> str:
766
1056
  return reverse(
767
1057
  "sb_admin:sb_admin_base",
768
1058
  kwargs={
@@ -772,17 +1062,72 @@ class SBAdminInline(
772
1062
  },
773
1063
  )
774
1064
 
775
- def register_autocomplete_views(self, request):
1065
+ def register_autocomplete_views(self, request) -> None:
776
1066
  super().register_autocomplete_views(request)
777
1067
  form_class = self.get_formset(request, self.model()).form
778
- self.initialize_form_class_threadsafe(form_class, request)
1068
+ self.initialize_form_class(form_class, request)
779
1069
  form_class()
780
1070
 
781
- @property
782
- def get_context_data(self):
783
- return {"inline_list_actions": self.get_sbadmin_inline_list_actions()}
1071
+ def get_parent_instance_from_request(self):
1072
+ # Try to get parent instance from request referrer
1073
+ request = (
1074
+ getattr(self, "threadsafe_request", None)
1075
+ or SBAdminThreadLocalService.get_request()
1076
+ )
1077
+ allowed = SBAdminViewService.has_permission(
1078
+ request=request, model=self.parent_model, permission="view"
1079
+ )
1080
+ if not allowed:
1081
+ return None
1082
+
1083
+ referer = request.META.get("HTTP_REFERER")
1084
+ if not referer:
1085
+ return None
1086
+ resolved = resolve(urlparse(referer).path)
1087
+ # Try common kwargs for object ID
1088
+ object_id = resolved.kwargs.get("object_id")
1089
+ if not object_id:
1090
+ return None
1091
+ base_qs = SBAdminViewService.get_restricted_queryset(
1092
+ self.parent_model, request, request.request_data
1093
+ )
1094
+ return base_qs.get(pk=object_id)
784
1095
 
785
- def init_sortable_field(self):
1096
+ def get_context_data(self, request) -> dict[str, Any]:
1097
+ is_sortable_active: bool = self.sortable_field_name and (
1098
+ self.has_add_permission(request) or self.has_change_permission(request)
1099
+ )
1100
+ add_url = None
1101
+ try:
1102
+ if self.sb_admin_add_modal and self.has_add_permission(request):
1103
+ add_url = reverse(
1104
+ "sb_admin:{}_{}_add".format(
1105
+ self.opts.app_label, self.opts.model_name
1106
+ )
1107
+ )
1108
+ except NoReverseMatch:
1109
+ logger.warning(
1110
+ "To use Add in modal, You have to specify SBAdmin view for %s model",
1111
+ self.opts.model_name,
1112
+ )
1113
+ context_data = {
1114
+ "inline_list_actions": self.get_sbadmin_inline_list_actions(request),
1115
+ "is_sortable_active": is_sortable_active,
1116
+ "add_url": add_url,
1117
+ }
1118
+ if self.parent_instance:
1119
+ context_data["parent_data"] = {
1120
+ SBADMIN_PARENT_INSTANCE_PK_VAR: self.parent_instance.pk,
1121
+ SBADMIN_PARENT_INSTANCE_LABEL_VAR: str(self.parent_instance),
1122
+ SBADMIN_PARENT_INSTANCE_FIELD_NAME_VAR: "{}_{}_id_{}".format(
1123
+ self.model._meta.app_label,
1124
+ self.model._meta.model_name,
1125
+ self.parent_model._meta.model_name,
1126
+ ),
1127
+ }
1128
+ return context_data
1129
+
1130
+ def init_sortable_field(self) -> None:
786
1131
  if not self.sortable_field_name:
787
1132
  for field_name in self.sbadmin_sortable_field_options:
788
1133
  is_sortable_field_present = False
@@ -798,15 +1143,15 @@ class SBAdminInline(
798
1143
  self.init_sortable_field()
799
1144
  super().__init__(parent_model, admin_site)
800
1145
 
801
- def init_view_dynamic(self, request, request_data=None, **kwargs):
1146
+ def init_view_dynamic(self, request, request_data=None, **kwargs) -> None:
802
1147
  return super().init_view_dynamic(request, request_data, **kwargs)
803
1148
 
804
- def get_id(self):
1149
+ def get_id(self) -> str:
805
1150
  return (
806
1151
  f"{self.__class__.__name__}_{SBAdminViewService.get_model_path(self.model)}"
807
1152
  )
808
1153
 
809
- def init_inline_dynamic(self, request, obj=None):
1154
+ def init_inline_dynamic(self, request, obj=None) -> None:
810
1155
  self.threadsafe_request = request
811
1156
  self.parent_instance = obj
812
1157
 
@@ -817,18 +1162,21 @@ class SBAdminInline(
817
1162
  return formfield
818
1163
 
819
1164
  def get_formset(self, request, obj=None, **kwargs):
1165
+ self.initialize_all_base_fields_form(request)
820
1166
  formset = super().get_formset(request, obj, **kwargs)
821
1167
  form_class = formset.form
822
- self.initialize_form_class_threadsafe(form_class, request)
1168
+ self.initialize_form_class(form_class, request)
823
1169
  return formset
824
1170
 
825
1171
 
826
1172
  class SBAdminTableInline(SBAdminInline, NestedTabularInline):
827
1173
  template = "sb_admin/inlines/table_inline.html"
1174
+ formset = SBAdminNestedInlineFormSet
828
1175
 
829
1176
 
830
1177
  class SBAdminGenericTableInline(SBAdminInline, NestedGenericTabularInline):
831
1178
  template = "sb_admin/inlines/table_inline.html"
1179
+ formset = SBAdminGenericInlineFormSet
832
1180
 
833
1181
 
834
1182
  class SBAdminTableInlinePaginated(SBAdminTableInline, TabularInlinePaginated):
@@ -841,11 +1189,51 @@ class SBAdminGenericTableInlinePaginated(SBAdminGenericTableInline):
841
1189
  per_page = 50
842
1190
 
843
1191
 
844
- class SBAdminStackedInline(SBAdminInline, NestedStackedInline):
1192
+ class SBAdminStackedInlineBase(SBAdminInline):
1193
+ default_collapsed = False
1194
+
1195
+ def get_sbadmin_default_collapsed(self, request):
1196
+ return self.default_collapsed
1197
+
1198
+ def get_context_data(self, request) -> dict[str, Any]:
1199
+ context_data = super().get_context_data(request)
1200
+ context_data["default_collapsed"] = self.get_sbadmin_default_collapsed(request)
1201
+ return context_data
1202
+
1203
+ def get_sbadmin_inline_list_actions(self, request) -> list:
1204
+ actions = super().get_sbadmin_inline_list_actions(request)
1205
+ actions.append(
1206
+ SBAdminCustomAction(
1207
+ title="Collapse",
1208
+ css_class=f"collapse-all-stacked-inlines {'collapsed' if self.get_sbadmin_default_collapsed(request) else ''}",
1209
+ url=request.get_full_path(),
1210
+ )
1211
+ )
1212
+ return actions
1213
+
1214
+
1215
+ class SBAdminStackedInline(SBAdminStackedInlineBase, NestedStackedInline):
845
1216
  template = "sb_admin/inlines/stacked_inline.html"
846
1217
  fieldset_template = "sb_admin/includes/inline_fieldset.html"
1218
+ formset = SBAdminNestedInlineFormSet
847
1219
 
848
1220
 
849
- class SBAdminGenericStackedInline(SBAdminInline, NestedGenericStackedInline):
1221
+ class SBAdminGenericStackedInline(SBAdminStackedInlineBase, NestedGenericStackedInline):
850
1222
  template = "sb_admin/inlines/stacked_inline.html"
851
1223
  fieldset_template = "sb_admin/includes/inline_fieldset.html"
1224
+ formset = SBAdminGenericInlineFormSet
1225
+
1226
+
1227
+ if parler_enabled:
1228
+
1229
+ class SBTranslatableAdmin(SBAdmin, TranslatableAdmin):
1230
+ def get_readonly_fields(self, request, obj=...):
1231
+ readonly_fields = super().get_readonly_fields(request, obj)
1232
+ if "sbadmin_translation_status" not in readonly_fields:
1233
+ readonly_fields += ("sbadmin_translation_status",)
1234
+ return readonly_fields
1235
+
1236
+ def get_fieldsets(self, request, obj=...):
1237
+ fieldsets = super().get_fieldsets(request, obj)
1238
+ fieldsets.append(SBAdminTranslationsService.get_translation_fieldset())
1239
+ return fieldsets