django-unfold 0.34.0__py3-none-any.whl → 0.35.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-unfold
3
- Version: 0.34.0
3
+ Version: 0.35.0
4
4
  Summary: Modern Django admin theme for seamless interface development
5
5
  Home-page: https://unfoldadmin.com
6
6
  License: MIT
@@ -179,7 +179,7 @@ class CustomAdminClass(ModelAdmin):
179
179
  from django.contrib import admin
180
180
  from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
181
181
  from django.contrib.auth.admin import GroupAdmin as BaseGroupAdmin
182
- from django.contrib.auth.models import User
182
+ from django.contrib.auth.models import User, Group
183
183
 
184
184
  from unfold.admin import ModelAdmin
185
185
 
@@ -360,6 +360,9 @@ class CustomAdminClass(ModelAdmin):
360
360
  # Display fields in changeform in compressed mode
361
361
  compressed_fields = True # Default: False
362
362
 
363
+ # Warn before leaving unsaved changes in changeform
364
+ warn_unsaved_form = True # Default: False
365
+
363
366
  # Preprocess content of readonly fields before render
364
367
  readonly_preprocess_fields = {
365
368
  "model_field_name": "html.unescape",
@@ -645,7 +648,14 @@ The difference between them is that `ChoicesDropdownFilter` will collect a list
645
648
  from django.contrib import admin
646
649
  from django.contrib.auth.models import User
647
650
  from unfold.admin import ModelAdmin
648
- from unfold.contrib.filters.admin import ChoicesDropdownFilter, RelatedDropdownFilter, DropdownFilter
651
+ from unfold.contrib.filters.admin import (
652
+ ChoicesDropdownFilter,
653
+ MultipleChoicesDropdownFilter,
654
+ RelatedDropdownFilter,
655
+ MultipleRelatedDropdownFilter,
656
+ DropdownFilter,
657
+ MultipleDropdownFilter
658
+ )
649
659
 
650
660
 
651
661
  class CustomDropdownFilter(DropdownFilter):
@@ -672,7 +682,9 @@ class MyAdmin(ModelAdmin):
672
682
  list_filter = [
673
683
  CustomDropdownFilter,
674
684
  ("modelfield_with_choices", ChoicesDropdownFilter),
685
+ ("modelfield_with_choices_multiple", MultipleChoicesDropdownFilter),
675
686
  ("modelfield_with_foreign_key", RelatedDropdownFilter)
687
+ ("modelfield_with_foreign_key_multiple", MultipleRelatedDropdownFilter)
676
688
  ]
677
689
  ```
678
690
 
@@ -1363,8 +1375,8 @@ def dashboard_callback(request: HttpRequest) -> Dict:
1363
1375
  ```
1364
1376
 
1365
1377
  ```django-html
1366
- {% component "unfold/components/card" with title="Card title" %}
1367
- {% component "unfold/components/table.html" with table=table_data card_included=1 striped=1 %}{% encomponent %}
1378
+ {% component "unfold/components/card.html" with title="Card title" %}
1379
+ {% component "unfold/components/table.html" with table=table_data card_included=1 striped=1 %}{% endcomponent %}
1368
1380
  {% endcomponent %}
1369
1381
  ```
1370
1382
 
@@ -1,12 +1,12 @@
1
1
  unfold/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- unfold/admin.py,sha256=3yBehQeJIt9JbNJMhBdnGFyovYmTr-JPFBpoCiUWpeE,19404
2
+ unfold/admin.py,sha256=nUAouLLyo57CE3EpOREHDMjOaj68P7MZO6KEPYlutYU,19587
3
3
  unfold/apps.py,sha256=SlBXPYrUd2uXn67qFbRvbXSUk3XFWrF4-5WELgDCvho,381
4
4
  unfold/checks.py,sha256=Smgji9w19hnYjJElJ_FJnnyTEAE-E-OUB6otHu7lasY,1670
5
5
  unfold/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  unfold/contrib/filters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- unfold/contrib/filters/admin.py,sha256=hkrw-CthPsSubwsDpInqOiNw5vsl_zbQeYW23DnKSsY,20068
7
+ unfold/contrib/filters/admin.py,sha256=4dwK-LCXaopQeqW3818wUiZJhkJEDLNwbDLJboVJPHk,21138
8
8
  unfold/contrib/filters/apps.py,sha256=wEySJy0gMLzFLb9XNKE-RexiO05X7NaQ5QmxZyziJ_k,136
9
- unfold/contrib/filters/forms.py,sha256=cdBFNB45PPgKVzAbjrwbZVdYF6rZBUQPxxWMwZaKCpM,4736
9
+ unfold/contrib/filters/forms.py,sha256=N25YiD7YfBxg57-goSnaXc-eyEQFp1XYgDmmQwg7jQ0,5696
10
10
  unfold/contrib/filters/static/unfold/filters/css/nouislider.min.css,sha256=rddL_jOGGVEY6wR-aw0VYovAfz5fPeAIsulrlSNb1hc,4221
11
11
  unfold/contrib/filters/static/unfold/filters/js/DateTimeShortcuts.js,sha256=jgFNBDf6aHvUlyv0LEDHggXO-xA8pWOCWFWcVupdA30,19332
12
12
  unfold/contrib/filters/static/unfold/filters/js/admin-numeric-filter.js,sha256=nTkiiJk4Abn9d6KigxPSEsereFhFq-v5n_boiiY1eII,1668
@@ -65,7 +65,7 @@ unfold/contrib/simple_history/templates/simple_history/object_history_form.html,
65
65
  unfold/contrib/simple_history/templates/simple_history/object_history_list.html,sha256=f0RxTiPfC957uQzNfSkKoyMUzu8LRw4-MS6d3xZ2gew,6821
66
66
  unfold/contrib/simple_history/templates/simple_history/submit_line.html,sha256=9v7PfS8M6N0NccTEb_9QyTG28NgDBiIkjuBOfWGIQ18,1703
67
67
  unfold/dataclasses.py,sha256=4PAWLVlUhyGMq1Mwai7afCgrRO0Inulw0opFUKVEOBk,483
68
- unfold/decorators.py,sha256=6E4vPVwK0IQDAiDPg9pgyypRqciX_gR0jwITDcrSc8U,3367
68
+ unfold/decorators.py,sha256=jyjE2sunazp8gK1QIsNRJGEEDwO7RDKWDKnOI3GDWZE,3338
69
69
  unfold/exceptions.py,sha256=gcCj1ox61E137bk_0Cqy4YC3SttdPgB-fiJUqpmyHSE,43
70
70
  unfold/fields.py,sha256=yhtpDfqycpOxqdQlbncCg9qhELxGk3AtXizkZyzzH0g,7410
71
71
  unfold/forms.py,sha256=8-IoFZLwUNJKTNdev_oJwdxm-_xekvrn_84IXKse4Q8,4100
@@ -83,9 +83,10 @@ unfold/static/unfold/fonts/material-symbols/styles.css,sha256=U0oiGd2DS7LXnbuqgs
83
83
  unfold/static/unfold/js/alpine.anchor.js,sha256=2Jg9aUq749pjFynzr_H1NK3lf-nXrbHMtO9wvlWJyIQ,15524
84
84
  unfold/static/unfold/js/alpine.js,sha256=NY2a-7GrW--i9IBhowd25bzXcH9BCmBrqYX5i8OxwDQ,44659
85
85
  unfold/static/unfold/js/alpine.persist.js,sha256=jFBwr6faTqqhp3sVi4_VTxJ0FpaF9YGZN1ZGLl_5QYM,837
86
- unfold/static/unfold/js/app.js,sha256=2-56Kc20KH69Prjcj6ezURAKID3U8V2mHgbhPjeH6HA,6314
86
+ unfold/static/unfold/js/app.js,sha256=kL7gJu4bDcY7v61KYtYtjn7KxWd-rHVvL49GTD_q770,6884
87
87
  unfold/static/unfold/js/chart.js,sha256=22W6cFERR-CElMOKRgMMicueMVP0Vf7FBEBYH8Z8tCk,200633
88
88
  unfold/static/unfold/js/htmx.js,sha256=XOLqvnZiyEx46EW9vaJTBUaaWg8CGVVfXJkVsUmJbpI,42820
89
+ unfold/static/unfold/js/select2.init.js,sha256=SzpjSL1mD9tHedjSeXlmEwHjuuyqlnpilpSeEEKeYLE,265
89
90
  unfold/static/unfold/js/simplebar.js,sha256=t-uG1FAD6ZoiMeN--wac0XRS7SxoDVG6zvRnGuEp7X8,27176
90
91
  unfold/styles.css,sha256=egNdWhCzaGS5Y_tCiceIU6BTNn1MkrMMooX5zDv_r6I,18223
91
92
  unfold/templates/admin/actions.html,sha256=8GqELAxzywGyfQJiQeuhGIQUakdK_WeneILY06C7mEo,2746
@@ -95,7 +96,7 @@ unfold/templates/admin/auth/user/add_form.html,sha256=iLig-vd2YExXsj0xGBwYhZ4kGU
95
96
  unfold/templates/admin/auth/user/change_password.html,sha256=-Wa9ml3yss-kDz0YQxCiwoxs91KQD8eetCt5l6xekWM,2892
96
97
  unfold/templates/admin/base.html,sha256=RptiEpgXZ6seYrPu-1Stoe8ECHpi9PjkJjFheGecYF0,2358
97
98
  unfold/templates/admin/base_site.html,sha256=3ckWrcAdd7Pw1hk6Zwyknab_Qb-rteV9-mXhMnfo6VI,361
98
- unfold/templates/admin/change_form.html,sha256=oWK5wr0qv6QMJrFQ9Veacozg_CN1DwmBqbzPOuDtiA4,4443
99
+ unfold/templates/admin/change_form.html,sha256=FiVfbB1j1wb6qGfaLukdMUrLRXsO_sJ4wirKq1bit60,4528
99
100
  unfold/templates/admin/change_form_object_tools.html,sha256=eyeH-i2HgEM0Yi-OJA2D1VnKJyC19A_my1IDGxxoP8Y,593
100
101
  unfold/templates/admin/change_list.html,sha256=o8XnKa1xFot53-BxGeB8afJ0TF8N7TcJhyTA4vPnop0,5124
101
102
  unfold/templates/admin/change_list_object_tools.html,sha256=cmMiT2nT20Ph5yfpj9aHPr76Z-JP4aSXp0o-Rnad28s,147
@@ -114,7 +115,7 @@ unfold/templates/admin/nav_sidebar.html,sha256=cKuC28XU9NT17lrCVugn2cHf39kgkoX49
114
115
  unfold/templates/admin/object_history.html,sha256=mRD6nbIVmnsz2OG6UsuXdI55bflY3Vs8zbIE9X-Oi74,4816
115
116
  unfold/templates/admin/pagination.html,sha256=KrwOM6gRizfpTMP59TESQaNGcQzkga2k02opl_7BxLo,1130
116
117
  unfold/templates/admin/search_form.html,sha256=vKuruVUEMarx8FPrBp86yqqFtUXfFPzR_9GxhnOCs4U,1173
117
- unfold/templates/admin/submit_line.html,sha256=JpD8HhRxtEIlWsGvpUGbLeKYJd_X7Rm_IBzFIcgeq6M,4222
118
+ unfold/templates/admin/submit_line.html,sha256=6V9qtE9BecN905-YkI5ZOLY3ZYyb_mhN_p9Zkhv4h8Q,4248
118
119
  unfold/templates/auth/widgets/read_only_password_hash.html,sha256=I08wFPcDr0Gjrg64nlcKiB12Tz2g_nnJXemiy8tkoZg,785
119
120
  unfold/templates/registration/logged_out.html,sha256=4rjAazcxt8ZCqA5bConKRgAk4VrljNTksdtg7D_btrU,1028
120
121
  unfold/templates/registration/password_change_done.html,sha256=i1ZzfTwZHWNWoN9_xHZDdcgLdTOVbTFFD1HUSuG0LkY,1062
@@ -194,8 +195,8 @@ unfold/templatetags/unfold_list.py,sha256=q02u7jbMWyDi_6_rOk17W-jW6Yqe9dkuz7Cc_m
194
195
  unfold/typing.py,sha256=1P8PWM2oeaceUJtA5j071RbKEBpHYaux441u7Hd6wv4,643
195
196
  unfold/utils.py,sha256=zZdJE4FmwRd7p5a7sJiAoZjBOJitXJduOq7BulyppWM,4803
196
197
  unfold/views.py,sha256=hQCyeeMa9kcJV1IZeeYqj8PGW7J4QWME8n-5n0UGmiU,1003
197
- unfold/widgets.py,sha256=p95xr5NoG_CI10fSZUYZ37w3vqqHjSpWPOLAo4Z6CDM,16049
198
- django_unfold-0.34.0.dist-info/LICENSE.md,sha256=Ltk_quRyyvV3J5v3brtOqmibeZSw2Hrb8bY1W3ya0Ik,1077
199
- django_unfold-0.34.0.dist-info/METADATA,sha256=H7CNN1F46aqwHb5_aie9z1pcpZOdjtmfJD4EqAK1FTQ,57500
200
- django_unfold-0.34.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
201
- django_unfold-0.34.0.dist-info/RECORD,,
198
+ unfold/widgets.py,sha256=6hQp13LmuhDJ41YxTG70SPJEg0agmHcmUhCtCUwI6SU,16369
199
+ django_unfold-0.35.0.dist-info/LICENSE.md,sha256=Ltk_quRyyvV3J5v3brtOqmibeZSw2Hrb8bY1W3ya0Ik,1077
200
+ django_unfold-0.35.0.dist-info/METADATA,sha256=_lkCvpMErEFx3xEWaKWziYEDzsvy4jxiLlfVkJav5u0,57888
201
+ django_unfold-0.35.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
202
+ django_unfold-0.35.0.dist-info/RECORD,,
unfold/admin.py CHANGED
@@ -236,6 +236,7 @@ class ModelAdmin(ModelAdminMixin, BaseModelAdmin):
236
236
  list_disable_select_all = False
237
237
  compressed_fields = False
238
238
  readonly_preprocess_fields = {}
239
+ warn_unsaved_form = False
239
240
  checks_class = UnfoldModelAdminChecks
240
241
 
241
242
  @property
@@ -244,13 +245,14 @@ class ModelAdmin(ModelAdminMixin, BaseModelAdmin):
244
245
  additional_media = forms.Media()
245
246
 
246
247
  for filter in self.list_filter:
247
- if not isinstance(filter, (tuple, list)):
248
- continue
249
-
250
- if hasattr(filter[1], "form_class") and hasattr(
251
- filter[1].form_class, "Media"
248
+ if (
249
+ isinstance(filter, (tuple, list))
250
+ and hasattr(filter[1], "form_class")
251
+ and hasattr(filter[1].form_class, "Media")
252
252
  ):
253
253
  additional_media += forms.Media(filter[1].form_class.Media)
254
+ elif hasattr(filter, "form_class") and hasattr(filter.form_class, "Media"):
255
+ additional_media += forms.Media(filter.form_class.Media)
254
256
 
255
257
  return media + additional_media
256
258
 
@@ -41,12 +41,23 @@ class ValueMixin:
41
41
  )
42
42
 
43
43
 
44
+ class MultiValueMixin:
45
+ def value(self) -> Optional[List[str]]:
46
+ return (
47
+ self.lookup_val
48
+ if self.lookup_val not in EMPTY_VALUES
49
+ and isinstance(self.lookup_val, List)
50
+ and len(self.lookup_val) > 0
51
+ else self.lookup_val
52
+ )
53
+
54
+
44
55
  class DropdownMixin:
45
56
  template = "unfold/filters/filters_field.html"
46
57
  form_class = DropdownForm
47
58
  all_option = ["", _("All")]
48
59
 
49
- def queryset(self, request, queryset) -> QuerySet:
60
+ def queryset(self, request: HttpRequest, queryset: QuerySet) -> QuerySet:
50
61
  if self.value() not in EMPTY_VALUES:
51
62
  return super().queryset(request, queryset)
52
63
 
@@ -112,11 +123,23 @@ class DropdownFilter(admin.SimpleListFilter):
112
123
  name=self.parameter_name,
113
124
  choices=[self.all_option, *self.lookup_choices],
114
125
  data={self.parameter_name: self.value()},
126
+ multiple=self.multiple if hasattr(self, "multiple") else False,
115
127
  ),
116
128
  },
117
129
  )
118
130
 
119
131
 
132
+ class MultipleDropdownFilter(DropdownFilter):
133
+ multiple = True
134
+
135
+ def __init__(self, request, params, model, model_admin):
136
+ self.request = request
137
+ super().__init__(request, params, model, model_admin)
138
+
139
+ def value(self):
140
+ return self.request.GET.getlist(self.parameter_name)
141
+
142
+
120
143
  class ChoicesDropdownFilter(ValueMixin, DropdownMixin, admin.ChoicesFieldListFilter):
121
144
  def choices(self, changelist: ChangeList):
122
145
  choices = [self.all_option, *self.field.flatchoices]
@@ -127,10 +150,15 @@ class ChoicesDropdownFilter(ValueMixin, DropdownMixin, admin.ChoicesFieldListFil
127
150
  name=self.lookup_kwarg,
128
151
  choices=choices,
129
152
  data={self.lookup_kwarg: self.value()},
153
+ multiple=self.multiple if hasattr(self, "multiple") else False,
130
154
  ),
131
155
  }
132
156
 
133
157
 
158
+ class MultipleChoicesDropdownFilter(MultiValueMixin, ChoicesDropdownFilter):
159
+ multiple = True
160
+
161
+
134
162
  class RelatedDropdownFilter(ValueMixin, DropdownMixin, admin.RelatedFieldListFilter):
135
163
  def choices(self, changelist: ChangeList):
136
164
  yield {
@@ -139,10 +167,15 @@ class RelatedDropdownFilter(ValueMixin, DropdownMixin, admin.RelatedFieldListFil
139
167
  name=self.lookup_kwarg,
140
168
  choices=[self.all_option, *self.lookup_choices],
141
169
  data={self.lookup_kwarg: self.value()},
170
+ multiple=self.multiple if hasattr(self, "multiple") else False,
142
171
  ),
143
172
  }
144
173
 
145
174
 
175
+ class MultipleRelatedDropdownFilter(MultiValueMixin, RelatedDropdownFilter):
176
+ multiple = True
177
+
178
+
146
179
  class SingleNumericFilter(admin.FieldListFilter):
147
180
  request = None
148
181
  parameter_name = None
@@ -1,8 +1,10 @@
1
1
  from django import forms
2
+ from django.forms import ChoiceField, MultipleChoiceField
2
3
  from django.utils.translation import gettext_lazy as _
3
4
 
4
5
  from ...widgets import (
5
6
  INPUT_CLASSES,
7
+ UnfoldAdminSelectMultipleWidget,
6
8
  UnfoldAdminSelectWidget,
7
9
  UnfoldAdminSplitDateTimeVerticalWidget,
8
10
  UnfoldAdminTextInputWidget,
@@ -21,15 +23,46 @@ class SearchForm(forms.Form):
21
23
 
22
24
 
23
25
  class DropdownForm(forms.Form):
24
- def __init__(self, name, label, choices, *args, **kwargs):
26
+ widget = UnfoldAdminSelectWidget(
27
+ attrs={
28
+ "data-theme": "admin-autocomplete",
29
+ "class": "admin-autocomplete",
30
+ }
31
+ )
32
+ field = ChoiceField
33
+
34
+ def __init__(self, name, label, choices, multiple=False, *args, **kwargs):
25
35
  super().__init__(*args, **kwargs)
26
36
 
27
- self.fields[name] = forms.ChoiceField(
37
+ if multiple:
38
+ self.widget = UnfoldAdminSelectMultipleWidget(
39
+ attrs={
40
+ "data-theme": "admin-autocomplete",
41
+ "class": "admin-autocomplete",
42
+ }
43
+ )
44
+ self.field = MultipleChoiceField
45
+
46
+ self.fields[name] = self.field(
28
47
  label=label,
29
48
  required=False,
30
49
  choices=choices,
31
- widget=UnfoldAdminSelectWidget,
50
+ widget=self.widget,
51
+ )
52
+
53
+ class Media:
54
+ js = (
55
+ "admin/js/vendor/jquery/jquery.js",
56
+ "admin/js/vendor/select2/select2.full.js",
57
+ "admin/js/jquery.init.js",
58
+ "unfold/js/select2.init.js",
32
59
  )
60
+ css = {
61
+ "screen": (
62
+ "admin/css/vendor/select2/select2.css",
63
+ "admin/css/autocomplete.css",
64
+ ),
65
+ }
33
66
 
34
67
 
35
68
  class SingleNumericForm(forms.Form):
unfold/decorators.py CHANGED
@@ -29,12 +29,12 @@ def action(
29
29
  getattr(model_admin, f"has_{permission}_permission")
30
30
  for permission in permissions
31
31
  )
32
- # TODO add obj parameter into has_permission method call.
33
32
  # Permissions methods have following syntax: has_<some>_permission(self, request, obj=None):
34
33
  # But obj is not examined by default in django admin and it would also require additional
35
34
  # fetch from database, therefore it is not supported yet
36
35
  if not any(
37
- has_permission(request) for has_permission in permission_checks
36
+ has_permission(request, kwargs.get("object_id"))
37
+ for has_permission in permission_checks
38
38
  ):
39
39
  raise PermissionDenied
40
40
  return func(model_admin, request, *args, **kwargs)
@@ -8,8 +8,31 @@ window.addEventListener("load", (e) => {
8
8
  renderCharts();
9
9
 
10
10
  filterForm();
11
+
12
+ warnWithoutSaving();
11
13
  });
12
14
 
15
+ /*************************************************************
16
+ * Warn without saving
17
+ *************************************************************/
18
+ const warnWithoutSaving = () => {
19
+ let formChanged = false;
20
+
21
+ Array.from(
22
+ document.querySelectorAll(
23
+ "form.warn-unsaved-form input, form.warn-unsaved-form select"
24
+ )
25
+ ).forEach((field) => {
26
+ field.addEventListener("change", (e) => (formChanged = true));
27
+ });
28
+
29
+ window.addEventListener("beforeunload", (e) => {
30
+ if (formChanged) {
31
+ e.preventDefault();
32
+ }
33
+ });
34
+ };
35
+
13
36
  /*************************************************************
14
37
  * Filter form
15
38
  *************************************************************/
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ {
3
+ const $ = django.jQuery;
4
+
5
+ $.fn.djangoCustomSelect2 = function () {
6
+ $.each(this, function (i, element) {
7
+ $(element).select2();
8
+ });
9
+ return this;
10
+ };
11
+
12
+ $(function () {
13
+ $(".admin-autocomplete").djangoCustomSelect2();
14
+ });
15
+ }
@@ -59,7 +59,7 @@
59
59
 
60
60
  {% block content %}
61
61
  <div id="content-main">
62
- <form {% if has_file_field %}enctype="multipart/form-data" {% endif %}{% if form_url %}action="{{ form_url }}" {% endif %}method="post" id="{{ opts.model_name }}_form" novalidate>
62
+ <form {% if has_file_field %}enctype="multipart/form-data" {% endif %}{% if form_url %}action="{{ form_url }}" {% endif %}method="post" id="{{ opts.model_name }}_form" {% if adminform.model_admin.warn_unsaved_form %}class="warn-unsaved-form"{% endif %} novalidate>
63
63
  {% csrf_token %}
64
64
 
65
65
  {% block form_top %}{% endblock %}
@@ -42,7 +42,7 @@
42
42
  {% endif %}
43
43
 
44
44
  {% if show_save_as_new %}
45
- <button type="submit" name="_saveasnew" class="border font-medium px-3 py-2 rounded-md transition-all w-full hover:bg-gray-50 dark:border-gray-700 dark:hover:text-gray-200 dark:hover:bg-gray-900">
45
+ <button type="submit" name="_saveasnew" class="border font-medium hidden px-3 py-2 rounded-md transition-all w-full hover:bg-gray-50 lg:block lg:w-auto dark:border-gray-700 dark:hover:text-gray-200 dark:hover:bg-gray-900">
46
46
  {% translate 'Save as new' %}
47
47
  </button>
48
48
  {% endif %}
unfold/widgets.py CHANGED
@@ -25,6 +25,7 @@ from django.forms import (
25
25
  NumberInput,
26
26
  PasswordInput,
27
27
  Select,
28
+ SelectMultiple,
28
29
  )
29
30
  from django.utils.translation import gettext_lazy as _
30
31
 
@@ -480,7 +481,16 @@ class UnfoldAdminSelectWidget(Select):
480
481
  if attrs is None:
481
482
  attrs = {}
482
483
 
483
- attrs["class"] = " ".join(SELECT_CLASSES)
484
+ attrs["class"] = " ".join([*SELECT_CLASSES, attrs.get("class", "")])
485
+ super().__init__(attrs, choices)
486
+
487
+
488
+ class UnfoldAdminSelectMultipleWidget(SelectMultiple):
489
+ def __init__(self, attrs=None, choices=()):
490
+ if attrs is None:
491
+ attrs = {}
492
+
493
+ attrs["class"] = " ".join([*SELECT_CLASSES, attrs.get("class", "")])
484
494
  super().__init__(attrs, choices)
485
495
 
486
496