django-smartbase-admin 1.0.5__py3-none-any.whl → 1.0.6b1__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 (25) hide show
  1. django_smartbase_admin/admin/admin_base.py +142 -35
  2. django_smartbase_admin/admin/widgets.py +81 -23
  3. django_smartbase_admin/engine/admin_base_view.py +30 -21
  4. django_smartbase_admin/engine/configuration.py +30 -21
  5. django_smartbase_admin/static/sb_admin/dist/main.js +1 -1
  6. django_smartbase_admin/static/sb_admin/dist/main_style.css +1 -1
  7. django_smartbase_admin/static/sb_admin/sprites/sb_admin/Caution.svg +3 -0
  8. django_smartbase_admin/static/sb_admin/src/css/components/_input.css +19 -15
  9. django_smartbase_admin/static/sb_admin/src/js/autocomplete.js +18 -0
  10. django_smartbase_admin/static/sb_admin/src/js/choices.js +8 -0
  11. django_smartbase_admin/static/sb_admin/src/js/datepicker.js +7 -5
  12. django_smartbase_admin/static/sb_admin/src/js/main.js +64 -26
  13. django_smartbase_admin/static/sb_admin/src/js/range.js +3 -2
  14. django_smartbase_admin/templates/sb_admin/actions/change_form.html +75 -36
  15. django_smartbase_admin/templates/sb_admin/inlines/table_inline.html +49 -33
  16. django_smartbase_admin/templates/sb_admin/sprites/sb_admin.svg +1 -1
  17. django_smartbase_admin/templates/sb_admin/widgets/autocomplete.html +13 -2
  18. django_smartbase_admin/templates/sb_admin/widgets/date.html +1 -1
  19. django_smartbase_admin/templates/sb_admin/widgets/includes/related_item_buttons.html +31 -0
  20. django_smartbase_admin/templates/sb_admin/widgets/time.html +1 -1
  21. django_smartbase_admin/utils.py +5 -0
  22. {django_smartbase_admin-1.0.5.dist-info → django_smartbase_admin-1.0.6b1.dist-info}/METADATA +2 -2
  23. {django_smartbase_admin-1.0.5.dist-info → django_smartbase_admin-1.0.6b1.dist-info}/RECORD +25 -23
  24. {django_smartbase_admin-1.0.5.dist-info → django_smartbase_admin-1.0.6b1.dist-info}/LICENSE.md +0 -0
  25. {django_smartbase_admin-1.0.5.dist-info → django_smartbase_admin-1.0.6b1.dist-info}/WHEEL +0 -0
@@ -1,4 +1,5 @@
1
1
  import json
2
+ import logging
2
3
  import urllib.parse
3
4
  from collections.abc import Iterable
4
5
  from functools import partial
@@ -12,6 +13,8 @@ from django.contrib.admin.options import get_content_type_for_model
12
13
  from django.contrib.admin.utils import unquote
13
14
  from django.contrib.admin.widgets import AdminTextareaWidget
14
15
  from django.contrib.auth.forms import UsernameField, ReadOnlyPasswordHashWidget
16
+ from django.contrib.contenttypes.forms import BaseGenericInlineFormSet
17
+ from django.contrib.contenttypes.models import ContentType
15
18
  from django.core.exceptions import (
16
19
  FieldDoesNotExist,
17
20
  ImproperlyConfigured,
@@ -24,15 +27,18 @@ from django.forms.models import (
24
27
  ModelFormMetaclass,
25
28
  modelform_factory,
26
29
  )
30
+ from django.http import HttpResponse
27
31
  from django.template.loader import render_to_string
28
32
  from django.template.response import TemplateResponse
29
- from django.urls import reverse
33
+ from django.urls import reverse, NoReverseMatch
30
34
  from django.utils.safestring import mark_safe, SafeString
31
35
  from django.utils.text import capfirst
32
36
  from django.utils.translation import gettext_lazy as _
33
37
  from django_admin_inline_paginator.admin import TabularInlinePaginated
38
+ from django_htmx.http import trigger_client_event
34
39
  from filer.fields.file import FilerFileField
35
40
  from filer.fields.image import AdminImageFormField, FilerImageField
41
+ from nested_admin.formsets import NestedInlineFormSet
36
42
  from nested_admin.nested import (
37
43
  NestedModelAdmin,
38
44
  NestedTabularInline,
@@ -43,7 +49,7 @@ from nested_admin.nested import (
43
49
 
44
50
  from django_smartbase_admin.engine.actions import SBAdminCustomAction
45
51
  from django_smartbase_admin.services.thread_local import SBAdminThreadLocalService
46
- from django_smartbase_admin.utils import FormFieldsetMixin
52
+ from django_smartbase_admin.utils import FormFieldsetMixin, is_modal
47
53
 
48
54
  parler_enabled = None
49
55
  try:
@@ -68,7 +74,6 @@ try:
68
74
  except ImportError:
69
75
  pass
70
76
 
71
-
72
77
  django_cms_attributes = None
73
78
  try:
74
79
  from djangocms_attributes_field.fields import AttributesFormField
@@ -77,7 +82,6 @@ try:
77
82
  except ImportError:
78
83
  pass
79
84
 
80
-
81
85
  color_field_enabled = None
82
86
  try:
83
87
  from colorfield.fields import ColorField
@@ -86,7 +90,6 @@ try:
86
90
  except ImportError:
87
91
  pass
88
92
 
89
-
90
93
  from django_smartbase_admin.admin.widgets import (
91
94
  SBAdminTextInputWidget,
92
95
  SBAdminTextareaWidget,
@@ -117,6 +120,10 @@ from django_smartbase_admin.engine.admin_base_view import (
117
120
  SBAdminBaseListView,
118
121
  SBAdminBaseView,
119
122
  SBAdminBaseQuerysetMixin,
123
+ SBADMIN_IS_MODAL_VAR,
124
+ SBADMIN_PARENT_INSTANCE_PK_VAR,
125
+ SBADMIN_PARENT_INSTANCE_LABEL_VAR,
126
+ SBADMIN_PARENT_INSTANCE_FIELD_NAME_VAR,
120
127
  )
121
128
  from django_smartbase_admin.engine.const import (
122
129
  OBJECT_ID_PLACEHOLDER,
@@ -126,6 +133,8 @@ from django_smartbase_admin.engine.const import (
126
133
  from django_smartbase_admin.services.translations import SBAdminTranslationsService
127
134
  from django_smartbase_admin.services.views import SBAdminViewService
128
135
 
136
+ logger = logging.getLogger(__name__)
137
+
129
138
 
130
139
  class SBAdminFormFieldWidgetsMixin:
131
140
  formfield_widgets = {
@@ -186,7 +195,7 @@ class SBAdminFormFieldWidgetsMixin:
186
195
  )
187
196
 
188
197
  def get_autocomplete_widget(
189
- self, request, form_field, db_field, model, multiselect=False
198
+ self, request, form_field, db_field, model, multiselect=False
190
199
  ):
191
200
  return request.request_data.configuration.get_autocomplete_widget(
192
201
  self, request, form_field, db_field, model, multiselect
@@ -277,7 +286,7 @@ class SBAdminFormFieldWidgetsMixin:
277
286
  )
278
287
  form_field.widget = form_field_widget_instance
279
288
  if form_field.help_text == _(
280
- "Hold down “Control”, or “Command” on a Mac, to select more than one."
289
+ "Hold down “Control”, or “Command” on a Mac, to select more than one."
281
290
  ):
282
291
  form_field.help_text = ""
283
292
  return form_field
@@ -358,25 +367,25 @@ if parler_enabled:
358
367
  for translations_model in form_model._parler_meta.get_all_models():
359
368
  fields = getattr(form_new_meta, "fields", form_meta.fields)
360
369
  exclude = (
361
- getattr(form_new_meta, "exclude", form_meta.exclude) or ()
370
+ getattr(form_new_meta, "exclude", form_meta.exclude) or ()
362
371
  )
363
372
  widgets = (
364
- getattr(form_new_meta, "widgets", form_meta.widgets) or ()
373
+ getattr(form_new_meta, "widgets", form_meta.widgets) or ()
365
374
  )
366
375
  labels = (
367
- getattr(form_new_meta, "labels", form_meta.labels) or ()
376
+ getattr(form_new_meta, "labels", form_meta.labels) or ()
368
377
  )
369
378
  help_texts = (
370
- getattr(form_new_meta, "help_texts", form_meta.help_texts)
371
- or ()
379
+ getattr(form_new_meta, "help_texts", form_meta.help_texts)
380
+ or ()
372
381
  )
373
382
  error_messages = (
374
- getattr(
375
- form_new_meta,
376
- "error_messages",
377
- form_meta.error_messages,
378
- )
379
- or ()
383
+ getattr(
384
+ form_new_meta,
385
+ "error_messages",
386
+ form_meta.error_messages,
387
+ )
388
+ or ()
380
389
  )
381
390
  formfield_callback = attrs.get("formfield_callback", None)
382
391
 
@@ -397,10 +406,10 @@ if parler_enabled:
397
406
  # The next code holds the same logic as fields_for_model()
398
407
  # The f.editable check happens in _get_model_form_field()
399
408
  elif (
400
- f_name not in form_base_fields
401
- and (fields is None or f_name in fields)
402
- and f_name not in exclude
403
- and not f_name in attrs
409
+ f_name not in form_base_fields
410
+ and (fields is None or f_name in fields)
411
+ and f_name not in exclude
412
+ and not f_name in attrs
404
413
  ):
405
414
  # Get declared widget kwargs
406
415
  if f_name in widgets:
@@ -421,7 +430,7 @@ if parler_enabled:
421
430
  # See if this formfield was previously defined using a TranslatedField placeholder.
422
431
  placeholder = _get_mro_attribute(bases, f_name)
423
432
  if placeholder and isinstance(
424
- placeholder, TranslatedField
433
+ placeholder, TranslatedField
425
434
  ):
426
435
  kwargs.update(placeholder.kwargs)
427
436
 
@@ -454,6 +463,7 @@ if parler_enabled:
454
463
  mcs.parler_orig__new__(mcs, name, bases, attrs)
455
464
  return super().__new__(mcs, name, bases, attrs)
456
465
 
466
+
457
467
  class SBTranslatableModelForm(
458
468
  BaseTranslatableModelForm,
459
469
  SBAdminBaseForm,
@@ -504,7 +514,7 @@ class SBAdminInlineAndAdminCommon(SBAdminFormFieldWidgetsMixin):
504
514
  def init_view_dynamic(self, request, request_data=None, **kwargs) -> None:
505
515
  if SBAdminTranslationsService.is_translated_model(self.model):
506
516
  has_default_form = (
507
- self.form == TranslatableModelForm or self.form == forms.ModelForm
517
+ self.form == TranslatableModelForm or self.form == forms.ModelForm
508
518
  )
509
519
  if not self.form or has_default_form:
510
520
  self.form = SBTranslatableModelForm
@@ -558,12 +568,12 @@ class SBAdminThirdParty(SBAdminInlineAndAdminCommon, SBAdminBaseView):
558
568
 
559
569
  class SBAdminTranslationStatusMixin:
560
570
  def sbadmin_translation_status_row_context(
561
- self,
562
- language,
563
- languages_count,
564
- main_language_code,
565
- current_lang_code,
566
- translations_edit_url,
571
+ self,
572
+ language,
573
+ languages_count,
574
+ main_language_code,
575
+ current_lang_code,
576
+ translations_edit_url,
567
577
  ) -> dict[str, Any]:
568
578
  language_code = language[0]
569
579
  language_title = language[1]
@@ -648,6 +658,27 @@ class SBAdminTranslationStatusMixin:
648
658
  return mark_safe(result)
649
659
 
650
660
 
661
+ class SBAdminInlineFormSetMixin:
662
+ @classmethod
663
+ def get_default_prefix(cls):
664
+ view = getattr(cls.form, "view", None)
665
+ if view and view.parent_model and view.opts:
666
+ parent_opts = view.parent_model._meta
667
+ opts = view.opts
668
+ modal_prefix = "modal__" if is_modal(SBAdminThreadLocalService.get_request()) else ""
669
+ return f"{modal_prefix}{parent_opts.app_label}_{parent_opts.model_name}_{opts.app_label}-{opts.model_name}"
670
+
671
+ return super().get_default_prefix()
672
+
673
+
674
+ class SBAdminGenericInlineFormSet(SBAdminInlineFormSetMixin, BaseGenericInlineFormSet):
675
+ pass
676
+
677
+
678
+ class SBAdminNestedInlineFormSet(SBAdminInlineFormSetMixin, NestedInlineFormSet):
679
+ pass
680
+
681
+
651
682
  class SBAdmin(
652
683
  SBAdminInlineAndAdminCommon,
653
684
  SBAdminBaseQuerysetMixin,
@@ -668,6 +699,10 @@ class SBAdmin(
668
699
  sbadmin_tabs = None
669
700
  request_data = None
670
701
  menu_label = None
702
+ sbadmin_is_generic_model = False
703
+
704
+ def __init__(self, model, admin_site):
705
+ super().__init__(model, admin_site)
671
706
 
672
707
  def save_formset(self, request, form, formset, change):
673
708
  if not change and hasattr(formset, "inline_instance"):
@@ -688,7 +723,7 @@ class SBAdmin(
688
723
  return self.get_model_path()
689
724
 
690
725
  def get_sbadmin_fieldsets(
691
- self, request, object_id=None
726
+ self, request, object_id=None
692
727
  ) -> Iterable[tuple[str | None, dict[str, Any]]]:
693
728
  fieldsets = self.sbadmin_fieldsets or self.fieldsets
694
729
  if fieldsets:
@@ -702,7 +737,7 @@ class SBAdmin(
702
737
  self.get_form(request)()
703
738
 
704
739
  def get_fieldsets(
705
- self, request, obj=None
740
+ self, request, obj=None
706
741
  ) -> list[tuple[str | None, dict[str, Any]]]:
707
742
  fieldsets = []
708
743
  object_id = obj.id if obj else None
@@ -719,7 +754,7 @@ class SBAdmin(
719
754
  return fieldsets
720
755
 
721
756
  def get_fieldsets_context(
722
- self, request, object_id
757
+ self, request, object_id
723
758
  ) -> dict[str, dict[str | None, dict[str, Any]]]:
724
759
  fielsets_context = {}
725
760
  for fieldset in self.get_sbadmin_fieldsets(request, object_id):
@@ -903,6 +938,50 @@ class SBAdmin(
903
938
  except Exception as e:
904
939
  return super().history_view(request, object_id, extra_context)
905
940
 
941
+ @classmethod
942
+ def get_modal_save_response(cls, request, obj):
943
+ response = HttpResponse()
944
+ trigger_client_event(
945
+ response,
946
+ "sbadmin:modal-change-form-response",
947
+ {
948
+ "field": request.POST.get("sb_admin_source_field"),
949
+ "id": obj.pk,
950
+ "label": str(obj),
951
+ "reload": request.POST.get("sbadmin_reload_on_save") == "1",
952
+ },
953
+ )
954
+ trigger_client_event(response, "hideModal", {"elt": "sb-admin-modal"})
955
+ return response
956
+
957
+ def response_add(self, request, obj, post_url_continue=None):
958
+ if is_modal(request):
959
+ return self.get_modal_save_response(request, obj)
960
+ return super().response_add(request, obj, post_url_continue)
961
+
962
+ def response_change(self, request, obj):
963
+ if is_modal(request):
964
+ return self.get_modal_save_response(request, obj)
965
+ return super().response_change(request, obj)
966
+
967
+ @classmethod
968
+ def set_generic_relation_from_parent(cls, request, obj):
969
+ parent_model_path = request.POST.get(SBADMIN_PARENT_INSTANCE_FIELD_NAME_VAR)
970
+ parent_pk = request.POST.get(SBADMIN_PARENT_INSTANCE_PK_VAR)
971
+
972
+ if parent_model_path and parent_pk:
973
+ app_label, model_name, field, parent_model = parent_model_path.split("_", 4)
974
+ content_type = ContentType.objects.get(
975
+ app_label=app_label, model=parent_model
976
+ )
977
+ obj.content_type = content_type
978
+ obj.object_id = int(parent_pk)
979
+
980
+ def save_model(self, request, obj, form, change):
981
+ if self.sbadmin_is_generic_model and SBADMIN_IS_MODAL_VAR in request.POST:
982
+ self.set_generic_relation_from_parent(request, obj)
983
+ super().save_model(request, obj, form, change)
984
+
906
985
 
907
986
  class SBAdminInline(
908
987
  SBAdminInlineAndAdminCommon, SBAdminBaseQuerysetMixin, SBAdminBaseView
@@ -914,6 +993,7 @@ class SBAdminInline(
914
993
  extra = 0
915
994
  ordering = None
916
995
  all_base_fields_form = None
996
+ sb_admin_add_modal = False
917
997
 
918
998
  def get_readonly_fields(self, request, obj=None):
919
999
  readonly_fields = super().get_readonly_fields(request, obj)
@@ -961,12 +1041,37 @@ class SBAdminInline(
961
1041
 
962
1042
  def get_context_data(self, request) -> dict[str, Any]:
963
1043
  is_sortable_active: bool = self.sortable_field_name and (
964
- self.has_add_permission(request) or self.has_change_permission(request)
1044
+ self.has_add_permission(request) or self.has_change_permission(request)
965
1045
  )
966
- return {
1046
+ add_url = None
1047
+ try:
1048
+ if self.sb_admin_add_modal and self.has_add_permission(request):
1049
+ add_url = reverse(
1050
+ "sb_admin:{}_{}_add".format(
1051
+ self.opts.app_label, self.opts.model_name
1052
+ )
1053
+ )
1054
+ except NoReverseMatch:
1055
+ logger.warning(
1056
+ "To use Add in modal, You have to specify SBAdmin view for %s model",
1057
+ self.opts.model_name,
1058
+ )
1059
+ context_data = {
967
1060
  "inline_list_actions": self.get_sbadmin_inline_list_actions(request),
968
1061
  "is_sortable_active": is_sortable_active,
1062
+ "add_url": add_url,
969
1063
  }
1064
+ if self.parent_instance:
1065
+ context_data["parent_data"] = {
1066
+ SBADMIN_PARENT_INSTANCE_PK_VAR: self.parent_instance.pk,
1067
+ SBADMIN_PARENT_INSTANCE_LABEL_VAR: str(self.parent_instance),
1068
+ SBADMIN_PARENT_INSTANCE_FIELD_NAME_VAR: "{}_{}_id_{}".format(
1069
+ self.model._meta.app_label,
1070
+ self.model._meta.model_name,
1071
+ self.parent_model._meta.model_name,
1072
+ ),
1073
+ }
1074
+ return context_data
970
1075
 
971
1076
  def init_sortable_field(self) -> None:
972
1077
  if not self.sortable_field_name:
@@ -1012,10 +1117,12 @@ class SBAdminInline(
1012
1117
 
1013
1118
  class SBAdminTableInline(SBAdminInline, NestedTabularInline):
1014
1119
  template = "sb_admin/inlines/table_inline.html"
1120
+ formset = SBAdminNestedInlineFormSet
1015
1121
 
1016
1122
 
1017
1123
  class SBAdminGenericTableInline(SBAdminInline, NestedGenericTabularInline):
1018
1124
  template = "sb_admin/inlines/table_inline.html"
1125
+ formset = SBAdminGenericInlineFormSet
1019
1126
 
1020
1127
 
1021
1128
  class SBAdminTableInlinePaginated(SBAdminTableInline, TabularInlinePaginated):
@@ -12,7 +12,7 @@ from django.contrib.admin.widgets import (
12
12
  from django.contrib.auth.forms import ReadOnlyPasswordHashWidget
13
13
  from django.core.exceptions import ValidationError
14
14
  from django.template.loader import render_to_string
15
- from django.urls import reverse
15
+ from django.urls import reverse, NoReverseMatch
16
16
  from django.utils.formats import get_format
17
17
  from django.utils.http import urlencode
18
18
  from django.utils.safestring import mark_safe
@@ -22,12 +22,17 @@ from filer.fields.file import AdminFileWidget as FilerAdminFileWidget
22
22
  from filer.fields.image import AdminImageWidget
23
23
  from filer.models import File
24
24
 
25
+ from django_smartbase_admin.engine.admin_base_view import (
26
+ SBADMIN_PARENT_INSTANCE_PK_VAR,
27
+ SBADMIN_PARENT_INSTANCE_LABEL_VAR,
28
+ )
25
29
  from django_smartbase_admin.engine.filter_widgets import (
26
30
  AutocompleteFilterWidget,
27
31
  SBAdminTreeWidgetMixin,
28
32
  )
29
33
  from django_smartbase_admin.services.thread_local import SBAdminThreadLocalService
30
34
  from django_smartbase_admin.templatetags.sb_admin_tags import SBAdminJSONEncoder
35
+ from django_smartbase_admin.utils import is_modal
31
36
 
32
37
  logger = logging.getLogger(__name__)
33
38
 
@@ -55,6 +60,19 @@ class SBAdminBaseWidget(ContextMixin):
55
60
  def get_context(self, name, value, attrs):
56
61
  context = super().get_context(name, value, attrs)
57
62
  context["widget"]["form_field"] = self.form_field
63
+ opts = (
64
+ self.form_field.view.opts
65
+ if self.form_field
66
+ and hasattr(self.form_field, "view")
67
+ and hasattr(self.form_field.view, "opts")
68
+ else None
69
+ )
70
+ modal_prefix = "modal__" if is_modal(SBAdminThreadLocalService.get_request()) else ""
71
+ if opts:
72
+ context["widget"]["attrs"][
73
+ "id"
74
+ ] = f"{modal_prefix}{opts.app_label}_{opts.model_name}_{context['widget']['attrs']['id']}"
75
+
58
76
  return context
59
77
 
60
78
 
@@ -118,12 +136,12 @@ class SBAdminToggleWidget(SBAdminBaseWidget, forms.CheckboxInput):
118
136
  class SBAdminCKEditorWidget(SBAdminBaseWidget, CKEditorWidget):
119
137
 
120
138
  def __init__(
121
- self,
122
- config_name="default",
123
- extra_plugins=None,
124
- external_plugin_resources=None,
125
- form_field=None,
126
- attrs=None,
139
+ self,
140
+ config_name="default",
141
+ extra_plugins=None,
142
+ external_plugin_resources=None,
143
+ form_field=None,
144
+ attrs=None,
127
145
  ):
128
146
  super().__init__(
129
147
  form_field,
@@ -340,25 +358,35 @@ class SBAdminAutocompleteWidget(
340
358
  def get_context(self, name, value, attrs):
341
359
  context = super().get_context(name, value, attrs)
342
360
  self.input_id = (
343
- context["widget"]["attrs"]["id"] or f'id_{context["widget"]["name"]}'
361
+ context["widget"]["attrs"]["id"] or f'id_{context["widget"]["name"]}'
344
362
  )
345
363
  context["widget"]["type"] = "hidden"
346
364
  context["widget"]["attrs"]["id"] = self.input_id
347
365
  context["widget"]["attrs"]["class"] = "js-autocomplete-detail"
348
366
  context["widget"]["attrs"]["data-empty-label"] = (
349
- getattr(self.form_field, "empty_label", "---------") or "---------"
367
+ getattr(self.form_field, "empty_label", "---------") or "---------"
350
368
  )
351
369
  query_suffix = "__in"
352
370
  threadsafe_request = SBAdminThreadLocalService.get_request()
353
371
  if not self.is_multiselect():
354
372
  query_suffix = ""
355
373
  self.multiselect = False
374
+ context["widget"]["attrs"]["preselect_field"] = threadsafe_request.GET.get(
375
+ "sbadmin_parent_instance_field"
376
+ )
377
+ context["widget"]["attrs"]["preselect_field_label"] = (
378
+ threadsafe_request.GET.get(SBADMIN_PARENT_INSTANCE_LABEL_VAR)
379
+ )
380
+ context["widget"]["attrs"]["preselect_field_value"] = (
381
+ threadsafe_request.GET.get(SBADMIN_PARENT_INSTANCE_PK_VAR)
382
+ )
383
+ parsed_value = None
356
384
  if value:
357
385
  parsed_value = self.parse_value_from_input(threadsafe_request, value)
358
386
  if parsed_value:
359
387
  selected_options = []
360
388
  for item in self.get_queryset(threadsafe_request).filter(
361
- **{f"{self.get_value_field()}{query_suffix}": parsed_value}
389
+ **{f"{self.get_value_field()}{query_suffix}": parsed_value}
362
390
  ):
363
391
  selected_options.append(
364
392
  {
@@ -366,10 +394,40 @@ class SBAdminAutocompleteWidget(
366
394
  "label": self.get_label(threadsafe_request, item),
367
395
  }
368
396
  )
397
+
369
398
  context["widget"]["value"] = json.dumps(selected_options)
370
399
  context["widget"]["value_list"] = selected_options
400
+
401
+ if (
402
+ threadsafe_request.request_data.configuration.autocomplete_show_related_buttons(
403
+ self.model,
404
+ field_name=self.field_name,
405
+ current_view=self.view,
406
+ request=threadsafe_request,
407
+ )
408
+ ):
409
+ self.add_related_buttons_urls(parsed_value, context)
410
+
371
411
  return context
372
412
 
413
+ def add_related_buttons_urls(self, parsed_value, context):
414
+ related_model = self.model
415
+ app_label = related_model._meta.app_label
416
+ model_name = related_model._meta.model_name
417
+
418
+ try:
419
+ if parsed_value:
420
+ change_url = reverse(
421
+ "sb_admin:{}_{}_change".format(app_label, model_name),
422
+ args=(parsed_value,),
423
+ )
424
+ context["widget"]["attrs"]["related_edit_url"] = change_url
425
+
426
+ add_url = reverse("sb_admin:{}_{}_add".format(app_label, model_name))
427
+ context["widget"]["attrs"]["related_add_url"] = add_url
428
+ except NoReverseMatch:
429
+ pass
430
+
373
431
  def is_multiselect(self):
374
432
  if self.multiselect is not None:
375
433
  return self.multiselect
@@ -493,16 +551,16 @@ class SBAdminCodeWidget(SBAdminBaseWidget, forms.Widget):
493
551
  def __init__(self, form_field=None, *args, **kwargs):
494
552
  super().__init__(form_field, *args, **kwargs)
495
553
  self.attrs = {
496
- "code-mirror-options": json.dumps(
497
- {
498
- "mode": "django",
499
- "theme": "dracula",
500
- "lineWrapping": "true",
501
- }
502
- ),
503
- "code-mirror-width": "100%",
504
- "code-mirror-height": "300",
505
- } | self.attrs
554
+ "code-mirror-options": json.dumps(
555
+ {
556
+ "mode": "django",
557
+ "theme": "dracula",
558
+ "lineWrapping": "true",
559
+ }
560
+ ),
561
+ "code-mirror-width": "100%",
562
+ "code-mirror-height": "300",
563
+ } | self.attrs
506
564
 
507
565
  class Media:
508
566
  css = {
@@ -612,9 +670,9 @@ class SBAdminTreeWidget(SBAdminTreeWidgetMixin, SBAdminAutocompleteWidget):
612
670
  parsed_value = self.parse_value_from_input(threadsafe_request, input_value)
613
671
  obj = self.form.instance
614
672
  if (
615
- obj
616
- and parsed_value
617
- and self.relationship_pick_mode == self.RELATIONSHIP_PICK_MODE_PARENT
673
+ obj
674
+ and parsed_value
675
+ and self.relationship_pick_mode == self.RELATIONSHIP_PICK_MODE_PARENT
618
676
  ):
619
677
  if obj.id == parsed_value:
620
678
  raise ValidationError(_("Cannot set parent to itself"))