django-unfold 0.48.0__py3-none-any.whl → 0.49.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-unfold
3
- Version: 0.48.0
3
+ Version: 0.49.0
4
4
  Summary: Modern Django admin theme for seamless interface development
5
5
  License: MIT
6
6
  Keywords: django,admin,tailwind,theme
@@ -1,5 +1,5 @@
1
1
  unfold/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- unfold/admin.py,sha256=kIMEWKwiqnBo5eEDa_sBUlHh1XsZUhCt-Ct9bQ_H9vg,20018
2
+ unfold/admin.py,sha256=JOT4AbbCcGKDi_Qwi_yuFngkLbo78SHd-aHiHNLaI8M,6093
3
3
  unfold/apps.py,sha256=SlBXPYrUd2uXn67qFbRvbXSUk3XFWrF4-5WELgDCvho,381
4
4
  unfold/checks.py,sha256=8I3i4xR_KgyJdpQyZUZzKNeyYf-sNzg6PAlsREuMfgI,1664
5
5
  unfold/components.py,sha256=vqkQzseYUvLXDohmTVAlbKopALjyX4WA9yglvdfhqu4,1283
@@ -72,21 +72,25 @@ unfold/contrib/simple_history/templates/simple_history/object_history.html,sha25
72
72
  unfold/contrib/simple_history/templates/simple_history/object_history_form.html,sha256=JKCe-QTBZMB2ol5LPiX3gNuNxFpi_rqlXC9X92PFBPE,2296
73
73
  unfold/contrib/simple_history/templates/simple_history/object_history_list.html,sha256=ls_xEAAgGzbu0IXdjZk7scEyEJx0S7i3Khr9ND98gzY,6842
74
74
  unfold/contrib/simple_history/templates/simple_history/submit_line.html,sha256=y8BleGHPNztA_R37rTB-zjJ68uDbE8flYSvMD0Jfl5Y,1734
75
- unfold/dataclasses.py,sha256=41LuzojpU1NehkpbPP4FaJwMTXZtwb6jKNhc0iLQlqY,587
76
- unfold/decorators.py,sha256=7VNVSz3KoaMwCRAcBGO18wURXozXWrf_B0qPQity46c,3359
75
+ unfold/dataclasses.py,sha256=p3sqaIRfmGYEMq_-cSQIRYdsTeuqgiJ4ge_neQQrpd4,618
76
+ unfold/decorators.py,sha256=xofDxfJhVSmeyTh3fXiCXZtUKliuJOlxCQVQg3f1Fss,3740
77
77
  unfold/exceptions.py,sha256=gcCj1ox61E137bk_0Cqy4YC3SttdPgB-fiJUqpmyHSE,43
78
- unfold/fields.py,sha256=yhtpDfqycpOxqdQlbncCg9qhELxGk3AtXizkZyzzH0g,7410
78
+ unfold/fields.py,sha256=J3goIMlP_XOmQ85Xgh2v5MylXSP1o5I7o9_So2FQn2k,7461
79
79
  unfold/forms.py,sha256=BKv7eCbv29eCIuf1d_ZBmD4-_OIIzRaopiFd2nKbXNY,4520
80
+ unfold/mixins/__init__.py,sha256=YlhoDs9Y_DXQ8Ejr3enxuHMS1-Ij9fwukxNwpeOS4Co,190
81
+ unfold/mixins/action_model_admin.py,sha256=Km-_Tz17EKEgPr8ZvDT21KCx0JCOb_iWEPdxux2K54Y,11717
82
+ unfold/mixins/base_model_admin.py,sha256=PR4eqU48yB4CNy9Bjd7fjxbOFiFpZM4etYmBOSaRX_M,4140
83
+ unfold/overrides.py,sha256=ERVY9boZiXgjDYcTEZcIp7KWoeBZ45VM_zN31yAOjyQ,2922
80
84
  unfold/settings.py,sha256=5gs0iewBoUUrsF9LHHqcGOhW1HnyfZWY_zXuATjJOzE,2671
81
- unfold/sites.py,sha256=EVGzu9GvMW6iwl22htt7OaPnXAzTNN4uAwLrj149X2I,15998
85
+ unfold/sites.py,sha256=iwpJ9Qykxe0q4-alGE_07bJdBSpmIeTDxZil-C_vWSc,16054
82
86
  unfold/static/admin/js/admin/RelatedObjectLookups.js,sha256=alI0-yq7YPDJJJn-yg1ce79-Cv88yQDUrfaGqFZnsaY,9048
83
87
  unfold/static/admin/js/inlines.js,sha256=gxmQEUlJ9yasELjz9qpluogFZB-bD1KJfHs4PI7UjsA,14687
84
88
  unfold/static/unfold/css/simplebar.css,sha256=5LLaEM11pKi6JFCOLt4XKuZxTpT9rpdq_tNlaQytFlU,4647
85
- unfold/static/unfold/css/styles.css,sha256=2e50NohpwTJCkCq4h0lC17mpFLF3uwz-cKTxZCtF7G0,158571
86
- unfold/static/unfold/fonts/inter/Inter-Bold.woff2,sha256=O88EyjAeRPE_QEyKBKpK5wf2epUOEu8wwjj5bnhCZqE,46552
87
- unfold/static/unfold/fonts/inter/Inter-Medium.woff2,sha256=O88EyjAeRPE_QEyKBKpK5wf2epUOEu8wwjj5bnhCZqE,46552
88
- unfold/static/unfold/fonts/inter/Inter-Regular.woff2,sha256=O88EyjAeRPE_QEyKBKpK5wf2epUOEu8wwjj5bnhCZqE,46552
89
- unfold/static/unfold/fonts/inter/Inter-SemiBold.woff2,sha256=O88EyjAeRPE_QEyKBKpK5wf2epUOEu8wwjj5bnhCZqE,46552
89
+ unfold/static/unfold/css/styles.css,sha256=ORq4t5sGDPl16B-05v-3ahusgoLVu9r6pH2kiYuNbPY,158172
90
+ unfold/static/unfold/fonts/inter/Inter-Bold.woff2,sha256=-oiBJ7baAVtlVp8DUfO1w5GtkokElR8cIOn4RiqNleo,114840
91
+ unfold/static/unfold/fonts/inter/Inter-Medium.woff2,sha256=D_PpRhThST61VjFP0keubEqFp3g7TMhr5TmUDPg_Kkg,114348
92
+ unfold/static/unfold/fonts/inter/Inter-Regular.woff2,sha256=4G9rG8VTqupORmgCPtCrChRxKcMQf1Ebx9A9NhsK4IU,111268
93
+ unfold/static/unfold/fonts/inter/Inter-SemiBold.woff2,sha256=XLcQPk5gWYmv68A9mJx5IB5UshtRg9szmB9w25F4owE,114812
90
94
  unfold/static/unfold/fonts/inter/styles.css,sha256=QqdgevXVfcD6vZGo8ciSkaEG_62_YcZg0hvHTNgJ56E,608
91
95
  unfold/static/unfold/fonts/material-symbols/Material-Symbols-Outlined.woff2,sha256=V6mmjk69Ee-coQvMsGV0tcNgV3E1nb7N9IKW8HVUBzk,256424
92
96
  unfold/static/unfold/fonts/material-symbols/styles.css,sha256=ZX1icA_BXCVL0eoY2H6XR8VT_Fxl_KnV6vFBm_OImHA,535
@@ -127,7 +131,7 @@ unfold/templates/admin/nav_sidebar.html,sha256=cadJwXcReRHwVTpXGycF3uv5VZ5JDcIbY
127
131
  unfold/templates/admin/object_history.html,sha256=bfad4_lfLQFcoMV67yW7BnAi1Hwrptmgxj2tgPYQjtI,4816
128
132
  unfold/templates/admin/pagination.html,sha256=1UIc6Hm2LnDDAHUE5UjuPzdFdiAat4tF6sk058UfRP0,2251
129
133
  unfold/templates/admin/search_form.html,sha256=eQkwq4OWVgNUSjZPzxAjtT4LcxUNcTSgDVKJ4lWwslQ,1447
130
- unfold/templates/admin/submit_line.html,sha256=5mxHoCVwMuocYW27P2YORmXduCNd251jTuJgHjxXvfo,4501
134
+ unfold/templates/admin/submit_line.html,sha256=cN2bUfhNBz6OCdCeHqNYhXT2SD01YDwsQ-PVQaCGPPY,4782
131
135
  unfold/templates/auth/widgets/read_only_password_hash.html,sha256=bQiYKDKJkyVP0_2eJG16oKviB4WEA0V7lSp6sGfgows,751
132
136
  unfold/templates/registration/logged_out.html,sha256=GgLzqmOGpiw5FcA-Aw_T6YvhYHhGFWhyES40T28waLQ,1002
133
137
  unfold/templates/registration/password_change_done.html,sha256=9HDpSraz3Kxdy0Fghbn-Lv0GAtpPgG6cuO7fvfMiYg8,1077
@@ -148,7 +152,7 @@ unfold/templates/unfold/components/text.html,sha256=-GjxvdiaBQIaNfPSzT6SSIwnc3R2
148
152
  unfold/templates/unfold/components/title.html,sha256=aMYSO30ybFYqPnqwlJVh2uNmcgnVrRycZkHTjJTjpX8,171
149
153
  unfold/templates/unfold/components/tracker.html,sha256=-PvDHdAAB2G5mKXpQUtljWJYhhavsa1aEoBvLrCiCJo,284
150
154
  unfold/templates/unfold/helpers/account_links.html,sha256=8LBsa4uvwcbqc2kewHfSC8HaQAL3B-hPQIzV81rw_TU,1886
151
- unfold/templates/unfold/helpers/actions_row.html,sha256=0sz8i-dIDbCvmp-5JfMPZU3d_pfH43V7eXa4lIzF0EM,1977
155
+ unfold/templates/unfold/helpers/actions_row.html,sha256=ZENvQJM2hGG3qvfwm-o9Ip665zXwqV-gl8VBof2WXgQ,2531
152
156
  unfold/templates/unfold/helpers/add_link.html,sha256=Z0TzUavXH8AQ7A-I0SQ79t-UI87dyP_PZEpsDrm13Q8,810
153
157
  unfold/templates/unfold/helpers/app_list.html,sha256=eIvBuP_g8oHvXZTy-IMjyynFCC1CChtMq4qZia6i77E,5751
154
158
  unfold/templates/unfold/helpers/app_list_default.html,sha256=74x9GIhXHp4J-qxfyS7siSaV8A4XlRwc_ivzvvKw_FY,3902
@@ -190,17 +194,17 @@ unfold/templates/unfold/helpers/messages/success.html,sha256=FVg3HNLW7i6VVZLqiUz
190
194
  unfold/templates/unfold/helpers/messages/warning.html,sha256=1bJj813EfxNSWcdQj6rM9bon4QTgMyrMydZY521h53E,149
191
195
  unfold/templates/unfold/helpers/messages.html,sha256=I9RIKfi4T65AG16-2s0C32RwbFkn0qLhdSqDfXpSbyE,915
192
196
  unfold/templates/unfold/helpers/navigation.html,sha256=HNr9bxZOg84zsbgORkOhJFXKSp0c2im438yiWCfonb0,787
193
- unfold/templates/unfold/helpers/navigation_header.html,sha256=NrRCeHo_vrEPj4y8YYju9J7hNCGoLYL0AVR77N2_Tk8,1328
197
+ unfold/templates/unfold/helpers/navigation_header.html,sha256=upSSMGsqVzk_HfNINcapUtmR_d8kV2yxdwPEjMbRknY,1402
194
198
  unfold/templates/unfold/helpers/pagination_current_item.html,sha256=4cZ2KLVcP0Y7xuGyXgexDQ07r94cgM5Gnmtv11dkRPQ,69
195
199
  unfold/templates/unfold/helpers/pagination_ellipsis.html,sha256=8g0KUUKtqRkXx_EBLGtsJsiYQO4tPS3GazZjxT90e0M,56
196
200
  unfold/templates/unfold/helpers/search.html,sha256=w8Ute3qVfSIqUXzA3TKOx91RzW_qnffFi2Oe9qedtV4,1643
197
201
  unfold/templates/unfold/helpers/search_results.html,sha256=5gxOAfFBPOkRgDziyYio6ZXcDG1bzFw7t-KXGFaWrUk,653
198
202
  unfold/templates/unfold/helpers/site_branding.html,sha256=_pUyOvdjlV624eRq0wjpJkgXpn9DCDGc-0U6Mw0FupI,266
199
- unfold/templates/unfold/helpers/site_dropdown.html,sha256=2UrE2DA-sbgTEce97rP57WctAuB-1XGGJLWbUDy-xVY,869
200
- unfold/templates/unfold/helpers/site_icon.html,sha256=5Mr6VmLmKpGbgROlfJHVkaCes-K1i42ZhStdTFMfOEk,1207
203
+ unfold/templates/unfold/helpers/site_dropdown.html,sha256=Zwg5ZGVIEHAI2h-YcWDDBz3QD2nYsP9m530wYArFk5I,878
204
+ unfold/templates/unfold/helpers/site_icon.html,sha256=rB6oc6VBB0T7wtyGOPcrq6qepDRaA54F6-Wxtaru-ks,1297
201
205
  unfold/templates/unfold/helpers/site_logo.html,sha256=S_QJoT2qh0xw0ciaKxoT4GJ6QIH5eqgRSC0abbWWkOI,423
202
206
  unfold/templates/unfold/helpers/submit.html,sha256=4Mgf4lx7Atm8GPqD6LTJK3NA9zoSJjs9VPig7sDp8Ao,203
203
- unfold/templates/unfold/helpers/tab_action.html,sha256=l7TBZm5FjDjlJpTsYXFonLSFBDpN91jDiILY3hjjj5s,376
207
+ unfold/templates/unfold/helpers/tab_action.html,sha256=qnMmPJOeMLAx64uTf1pdPq1OIKm1MQ2Et6vTU6WQb-8,2065
204
208
  unfold/templates/unfold/helpers/tab_list.html,sha256=ULuTZ7VaUFsgtqNr9eCb-A_Qmhp7gqt43V8sdu5fEII,5032
205
209
  unfold/templates/unfold/helpers/theme_switch.html,sha256=Kh3Q8RuBWCUuY8YJLwBwd5yKzDhK6y0Ggx_ERoGEGv8,2218
206
210
  unfold/templates/unfold/helpers/userlinks.html,sha256=oZqiwCxG_zRecAbzYrr8_hQvkndVB6-liP6LEZM1UZc,863
@@ -227,11 +231,11 @@ unfold/templates/unfold/widgets/url.html,sha256=IRLgW44VTKN7UrSWeywJwaxQhfG5jhhX
227
231
  unfold/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
228
232
  unfold/templatetags/unfold.py,sha256=C2x_fqjVCWku4WdyaD_u4UjXq9eue1CgnmEz5PCfrS8,12364
229
233
  unfold/templatetags/unfold_list.py,sha256=-KOU6Ib2k4gokuw_CgRooFCto8xJkiDyotbQbKAqkEY,14233
230
- unfold/typing.py,sha256=ZVeXbABOYjUFgtGlFLMh6jdiM9bHVCwxMW0pNtbvwuw,651
234
+ unfold/typing.py,sha256=i7LM2LiwYTAjT5-OLDUPVn5b9X-DMmHnjlZG2toWwSE,692
231
235
  unfold/utils.py,sha256=L0oC9-j1B__BZ21M7i6rTXNbzCQaZ4T0qHYQcNupk0A,5480
232
236
  unfold/views.py,sha256=nqv-meiwGtCeET2r8WBVyijWnE94Dssc4QmdUVMMcEM,997
233
- unfold/widgets.py,sha256=ZsjOAlQgEGmllPvXfN5ApbtW3VFs51lJzW4V44v9Ilg,20172
234
- django_unfold-0.48.0.dist-info/LICENSE.md,sha256=Ltk_quRyyvV3J5v3brtOqmibeZSw2Hrb8bY1W3ya0Ik,1077
235
- django_unfold-0.48.0.dist-info/METADATA,sha256=Fw263kbNqMSQtyLw3NIplBzm6GyLISsCIE1HuojBADU,4840
236
- django_unfold-0.48.0.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
237
- django_unfold-0.48.0.dist-info/RECORD,,
237
+ unfold/widgets.py,sha256=esW1wxdbZwUhF31ensitBF8_6kVq_Gp0_iDgv6F5Rr0,20246
238
+ django_unfold-0.49.0.dist-info/LICENSE.md,sha256=Ltk_quRyyvV3J5v3brtOqmibeZSw2Hrb8bY1W3ya0Ik,1077
239
+ django_unfold-0.49.0.dist-info/METADATA,sha256=Rn9yuoaPYQ_xomHRKms5bRBF_qlQvArpTT5lJE38r3Q,4840
240
+ django_unfold-0.49.0.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
241
+ django_unfold-0.49.0.dist-info/RECORD,,
unfold/admin.py CHANGED
@@ -1,127 +1,33 @@
1
- import copy
2
1
  from functools import update_wrapper
3
- from typing import Any, Callable, Optional, Union
2
+ from typing import Optional
4
3
 
5
4
  from django import forms
6
5
  from django.contrib.admin import ModelAdmin as BaseModelAdmin
7
6
  from django.contrib.admin import StackedInline as BaseStackedInline
8
7
  from django.contrib.admin import TabularInline as BaseTabularInline
9
8
  from django.contrib.admin import display, helpers
10
- from django.contrib.admin.widgets import RelatedFieldWidgetWrapper
11
- from django.db import models
12
9
  from django.db.models import BLANK_CHOICE_DASH, Model
13
- from django.db.models.fields import Field
14
- from django.db.models.fields.related import ForeignKey, ManyToManyField
15
- from django.forms import Form
16
- from django.forms.fields import TypedChoiceField
17
- from django.forms.models import ModelChoiceField, ModelMultipleChoiceField
18
- from django.forms.widgets import SelectMultiple
19
10
  from django.http import HttpRequest, HttpResponse
20
11
  from django.shortcuts import redirect
21
- from django.template.response import TemplateResponse
22
- from django.urls import URLPattern, path, reverse
12
+ from django.urls import URLPattern, path
23
13
  from django.utils.safestring import mark_safe
24
14
  from django.utils.translation import gettext_lazy as _
25
15
  from django.views import View
26
16
 
27
- from .checks import UnfoldModelAdminChecks
28
- from .dataclasses import UnfoldAction
29
- from .exceptions import UnfoldException
30
- from .fields import UnfoldAdminField, UnfoldAdminReadonlyField
31
- from .forms import ActionForm
32
- from .typing import FieldsetsType
33
- from .widgets import (
34
- SELECT_CLASSES,
35
- UnfoldAdminBigIntegerFieldWidget,
36
- UnfoldAdminDecimalFieldWidget,
37
- UnfoldAdminEmailInputWidget,
38
- UnfoldAdminFileFieldWidget,
39
- UnfoldAdminImageFieldWidget,
40
- UnfoldAdminImageSmallFieldWidget,
41
- UnfoldAdminIntegerFieldWidget,
42
- UnfoldAdminIntegerRangeWidget,
43
- UnfoldAdminMoneyWidget,
44
- UnfoldAdminNullBooleanSelectWidget,
45
- UnfoldAdminRadioSelectWidget,
46
- UnfoldAdminSelectWidget,
47
- UnfoldAdminSingleDateWidget,
48
- UnfoldAdminSingleTimeWidget,
49
- UnfoldAdminSplitDateTimeWidget,
50
- UnfoldAdminTextareaWidget,
51
- UnfoldAdminTextInputWidget,
52
- UnfoldAdminURLInputWidget,
53
- UnfoldAdminUUIDInputWidget,
54
- UnfoldBooleanSwitchWidget,
55
- UnfoldBooleanWidget,
56
- UnfoldForeignKeyRawIdWidget,
57
- )
58
-
59
- try:
60
- from django.contrib.postgres.fields import ArrayField, IntegerRangeField
61
- from django.contrib.postgres.search import SearchVectorField
62
-
63
- HAS_PSYCOPG = True
64
- except ImportError:
65
- HAS_PSYCOPG = False
66
-
67
- try:
68
- from djmoney.models.fields import MoneyField
69
-
70
- HAS_MONEY = True
71
- except ImportError:
72
- HAS_MONEY = False
73
-
74
- FORMFIELD_OVERRIDES = {
75
- models.DateTimeField: {
76
- "form_class": forms.SplitDateTimeField,
77
- "widget": UnfoldAdminSplitDateTimeWidget,
78
- },
79
- models.DateField: {"widget": UnfoldAdminSingleDateWidget},
80
- models.TimeField: {"widget": UnfoldAdminSingleTimeWidget},
81
- models.EmailField: {"widget": UnfoldAdminEmailInputWidget},
82
- models.CharField: {"widget": UnfoldAdminTextInputWidget},
83
- models.URLField: {"widget": UnfoldAdminURLInputWidget},
84
- models.GenericIPAddressField: {"widget": UnfoldAdminTextInputWidget},
85
- models.UUIDField: {"widget": UnfoldAdminUUIDInputWidget},
86
- models.TextField: {"widget": UnfoldAdminTextareaWidget},
87
- models.NullBooleanField: {"widget": UnfoldAdminNullBooleanSelectWidget},
88
- models.BooleanField: {"widget": UnfoldBooleanSwitchWidget},
89
- models.IntegerField: {"widget": UnfoldAdminIntegerFieldWidget},
90
- models.BigIntegerField: {"widget": UnfoldAdminBigIntegerFieldWidget},
91
- models.DecimalField: {"widget": UnfoldAdminDecimalFieldWidget},
92
- models.FloatField: {"widget": UnfoldAdminDecimalFieldWidget},
93
- models.FileField: {"widget": UnfoldAdminFileFieldWidget},
94
- models.ImageField: {"widget": UnfoldAdminImageFieldWidget},
95
- models.JSONField: {"widget": UnfoldAdminTextareaWidget},
96
- models.DurationField: {"widget": UnfoldAdminTextInputWidget},
97
- }
98
-
99
- if HAS_PSYCOPG:
100
- FORMFIELD_OVERRIDES.update(
101
- {
102
- ArrayField: {"widget": UnfoldAdminTextareaWidget},
103
- SearchVectorField: {"widget": UnfoldAdminTextareaWidget},
104
- IntegerRangeField: {"widget": UnfoldAdminIntegerRangeWidget},
105
- }
106
- )
107
-
108
- if HAS_MONEY:
109
- FORMFIELD_OVERRIDES.update(
110
- {
111
- MoneyField: {"widget": UnfoldAdminMoneyWidget},
112
- }
113
- )
114
-
115
- FORMFIELD_OVERRIDES_INLINE = copy.deepcopy(FORMFIELD_OVERRIDES)
116
-
117
- FORMFIELD_OVERRIDES_INLINE.update(
118
- {
119
- models.ImageField: {"widget": UnfoldAdminImageSmallFieldWidget},
120
- }
121
- )
17
+ from unfold.checks import UnfoldModelAdminChecks
18
+ from unfold.fields import UnfoldAdminField, UnfoldAdminReadonlyField
19
+ from unfold.forms import ActionForm
20
+ from unfold.mixins import ActionModelAdminMixin, BaseModelAdminMixin
21
+ from unfold.overrides import FORMFIELD_OVERRIDES_INLINE
22
+ from unfold.typing import FieldsetsType
23
+ from unfold.widgets import UnfoldBooleanWidget
122
24
 
123
25
  checkbox = UnfoldBooleanWidget(
124
- {"class": "action-select", "aria-label": _("Select record")}, lambda value: False
26
+ {
27
+ "class": "action-select",
28
+ "aria-label": _("Select record"),
29
+ },
30
+ lambda value: False,
125
31
  )
126
32
 
127
33
  helpers.AdminField = UnfoldAdminField
@@ -129,106 +35,8 @@ helpers.AdminField = UnfoldAdminField
129
35
  helpers.AdminReadonlyField = UnfoldAdminReadonlyField
130
36
 
131
37
 
132
- class ModelAdminMixin:
133
- def __init__(self, model, admin_site):
134
- overrides = copy.deepcopy(FORMFIELD_OVERRIDES)
135
-
136
- for k, v in self.formfield_overrides.items():
137
- overrides.setdefault(k, {}).update(v)
138
-
139
- self.formfield_overrides = overrides
140
-
141
- super().__init__(model, admin_site)
142
-
143
- def formfield_for_choice_field(
144
- self, db_field: Field, request: HttpRequest, **kwargs
145
- ) -> TypedChoiceField:
146
- if "widget" not in kwargs:
147
- if db_field.name in self.radio_fields:
148
- kwargs["widget"] = UnfoldAdminRadioSelectWidget(
149
- radio_style=self.radio_fields[db_field.name]
150
- )
151
- else:
152
- kwargs["widget"] = UnfoldAdminSelectWidget()
153
-
154
- if "choices" not in kwargs:
155
- kwargs["choices"] = db_field.get_choices(
156
- include_blank=db_field.blank, blank_choice=[("", _("Select value"))]
157
- )
158
-
159
- return super().formfield_for_choice_field(db_field, request, **kwargs)
160
-
161
- def formfield_for_foreignkey(
162
- self, db_field: ForeignKey, request: HttpRequest, **kwargs
163
- ) -> Optional[ModelChoiceField]:
164
- db = kwargs.get("using")
165
-
166
- # Overrides widgets for all related fields
167
- if "widget" not in kwargs:
168
- if db_field.name in self.raw_id_fields:
169
- kwargs["widget"] = UnfoldForeignKeyRawIdWidget(
170
- db_field.remote_field, self.admin_site, using=db
171
- )
172
- elif (
173
- db_field.name not in self.get_autocomplete_fields(request)
174
- and db_field.name not in self.radio_fields
175
- ):
176
- kwargs["widget"] = UnfoldAdminSelectWidget()
177
- kwargs["empty_label"] = _("Select value")
178
-
179
- return super().formfield_for_foreignkey(db_field, request, **kwargs)
180
-
181
- def formfield_for_manytomany(
182
- self,
183
- db_field: ManyToManyField,
184
- request: HttpRequest,
185
- **kwargs,
186
- ) -> ModelMultipleChoiceField:
187
- if "widget" not in kwargs:
188
- if db_field.name in self.raw_id_fields:
189
- kwargs["widget"] = UnfoldAdminTextInputWidget()
190
-
191
- form_field = super().formfield_for_manytomany(db_field, request, **kwargs)
192
-
193
- # If M2M uses intermediary model, form_field will be None
194
- if not form_field:
195
- return None
196
-
197
- if isinstance(form_field.widget, SelectMultiple):
198
- form_field.widget.attrs["class"] = " ".join(SELECT_CLASSES)
199
-
200
- return form_field
201
-
202
- def formfield_for_nullboolean_field(
203
- self, db_field: Field, request: HttpRequest, **kwargs
204
- ) -> Optional[Field]:
205
- if "widget" not in kwargs:
206
- kwargs["widget"] = UnfoldAdminNullBooleanSelectWidget()
207
-
208
- return db_field.formfield(**kwargs)
209
-
210
- def formfield_for_dbfield(
211
- self, db_field: Field, request: HttpRequest, **kwargs
212
- ) -> Optional[Field]:
213
- if isinstance(db_field, models.BooleanField) and db_field.null is True:
214
- return self.formfield_for_nullboolean_field(db_field, request, **kwargs)
215
-
216
- formfield = super().formfield_for_dbfield(db_field, request, **kwargs)
217
-
218
- if formfield and isinstance(formfield.widget, RelatedFieldWidgetWrapper):
219
- formfield.widget.template_name = (
220
- "unfold/widgets/related_widget_wrapper.html"
221
- )
222
-
223
- return formfield
224
-
225
-
226
- class ModelAdmin(ModelAdminMixin, BaseModelAdmin):
38
+ class ModelAdmin(BaseModelAdminMixin, ActionModelAdminMixin, BaseModelAdmin):
227
39
  action_form = ActionForm
228
- actions_list = ()
229
- actions_row = ()
230
- actions_detail = ()
231
- actions_submit_line = ()
232
40
  custom_urls = ()
233
41
  add_fieldsets = ()
234
42
  list_horizontal_scrollbar_top = False
@@ -269,86 +77,6 @@ class ModelAdmin(ModelAdminMixin, BaseModelAdmin):
269
77
  return self.add_fieldsets
270
78
  return super().get_fieldsets(request, obj)
271
79
 
272
- def _filter_unfold_actions_by_permissions(
273
- self,
274
- request: HttpRequest,
275
- actions: list[UnfoldAction],
276
- object_id: Optional[Union[int, str]] = None,
277
- ) -> list[UnfoldAction]:
278
- """Filter out any Unfold actions that the user doesn't have access to."""
279
- filtered_actions = []
280
- for action in actions:
281
- if not hasattr(action.method, "allowed_permissions"):
282
- filtered_actions.append(action)
283
- continue
284
-
285
- permission_checks = (
286
- getattr(self, f"has_{permission}_permission")
287
- for permission in action.method.allowed_permissions
288
- )
289
-
290
- if object_id:
291
- if any(
292
- has_permission(request, object_id)
293
- for has_permission in permission_checks
294
- ):
295
- filtered_actions.append(action)
296
- else:
297
- if any(has_permission(request) for has_permission in permission_checks):
298
- filtered_actions.append(action)
299
-
300
- return filtered_actions
301
-
302
- def get_actions_list(self, request: HttpRequest) -> list[UnfoldAction]:
303
- return self._filter_unfold_actions_by_permissions(
304
- request, self._get_base_actions_list()
305
- )
306
-
307
- def _get_base_actions_list(self) -> list[UnfoldAction]:
308
- """
309
- Returns all available list global actions, prior to any filtering
310
- """
311
- return [self.get_unfold_action(action) for action in self.actions_list or []]
312
-
313
- def get_actions_detail(
314
- self, request: HttpRequest, object_id: int
315
- ) -> list[UnfoldAction]:
316
- return self._filter_unfold_actions_by_permissions(
317
- request, self._get_base_actions_detail(), object_id
318
- )
319
-
320
- def _get_base_actions_detail(self) -> list[UnfoldAction]:
321
- """
322
- Returns all available detail actions, prior to any filtering
323
- """
324
- return [self.get_unfold_action(action) for action in self.actions_detail or []]
325
-
326
- def get_actions_row(self, request: HttpRequest) -> list[UnfoldAction]:
327
- return self._filter_unfold_actions_by_permissions(
328
- request, self._get_base_actions_row()
329
- )
330
-
331
- def _get_base_actions_row(self) -> list[UnfoldAction]:
332
- """
333
- Returns all available row actions, prior to any filtering
334
- """
335
- return [self.get_unfold_action(action) for action in self.actions_row or []]
336
-
337
- def get_actions_submit_line(
338
- self, request: HttpRequest, object_id: int
339
- ) -> list[UnfoldAction]:
340
- return self._filter_unfold_actions_by_permissions(
341
- request, self._get_base_actions_submit_line(), object_id
342
- )
343
-
344
- def _get_base_actions_submit_line(self) -> list[UnfoldAction]:
345
- """
346
- Returns all available submit row actions, prior to any filtering
347
- """
348
- return [
349
- self.get_unfold_action(action) for action in self.actions_submit_line or []
350
- ]
351
-
352
80
  def get_custom_urls(self) -> tuple[tuple[str, str, View], ...]:
353
81
  """
354
82
  Method to get custom views for ModelAdmin with their urls
@@ -416,129 +144,6 @@ class ModelAdmin(ModelAdminMixin, BaseModelAdmin):
416
144
  name=custom_url[1],
417
145
  )
418
146
 
419
- def changeform_view(
420
- self,
421
- request: HttpRequest,
422
- object_id: Optional[str] = None,
423
- form_url: str = "",
424
- extra_context: Optional[dict[str, bool]] = None,
425
- ) -> Any:
426
- if extra_context is None:
427
- extra_context = {}
428
-
429
- actions = []
430
- if object_id:
431
- for action in self.get_actions_detail(request, object_id):
432
- actions.append(
433
- {
434
- "title": action.description,
435
- "attrs": action.method.attrs,
436
- "path": reverse(
437
- f"{self.admin_site.name}:{action.action_name}",
438
- args=(object_id,),
439
- ),
440
- }
441
- )
442
-
443
- extra_context.update(
444
- {
445
- "actions_submit_line": self.get_actions_submit_line(request, object_id),
446
- "actions_detail": actions,
447
- }
448
- )
449
-
450
- return super().changeform_view(request, object_id, form_url, extra_context)
451
-
452
- def changelist_view(
453
- self, request: HttpRequest, extra_context: Optional[dict[str, str]] = None
454
- ) -> TemplateResponse:
455
- if extra_context is None:
456
- extra_context = {}
457
-
458
- actions = [
459
- {
460
- "title": action.description,
461
- "attrs": action.method.attrs,
462
- "path": reverse(f"{self.admin_site.name}:{action.action_name}"),
463
- }
464
- for action in self.get_actions_list(request)
465
- ]
466
-
467
- actions_row = [
468
- {
469
- "title": action.description,
470
- "attrs": action.method.attrs,
471
- "raw_path": f"{self.admin_site.name}:{action.action_name}",
472
- }
473
- for action in self.get_actions_row(request)
474
- ]
475
-
476
- extra_context.update(
477
- {
478
- "actions_list": actions,
479
- "actions_row": actions_row,
480
- }
481
- )
482
-
483
- return super().changelist_view(request, extra_context)
484
-
485
- def get_unfold_action(self, action: str) -> UnfoldAction:
486
- """
487
- Converts action name to UnfoldAction
488
- :param action:
489
- :return:
490
- """
491
- method = self._get_instance_method(action)
492
-
493
- return UnfoldAction(
494
- action_name=f"{self.model._meta.app_label}_{self.model._meta.model_name}_{action}",
495
- method=method,
496
- description=self._get_action_description(method, action),
497
- path=self._get_action_url(method, action),
498
- attrs=method.attrs if hasattr(method, "attrs") else None,
499
- )
500
-
501
- @staticmethod
502
- def _get_action_url(func: Callable, name: str) -> str:
503
- """
504
- Returns action URL if it was specified in @action decorator.
505
- If it was not, name of the action is returned.
506
- :param func:
507
- :param name:
508
- :return:
509
- """
510
- return getattr(func, "url_path", name)
511
-
512
- def save_model(
513
- self, request: HttpRequest, obj: Model, form: Form, change: Any
514
- ) -> None:
515
- super().save_model(request, obj, form, change)
516
-
517
- for action in self.get_actions_submit_line(request, obj.pk):
518
- if action.action_name not in request.POST:
519
- continue
520
-
521
- action.method(request, obj)
522
-
523
- def _get_instance_method(self, method_name: str) -> Callable:
524
- """
525
- Searches for method on self instance based on method_name and returns it if it exists.
526
- If it does not exist or is not callable, it raises UnfoldException
527
- :param method_name: Name of the method to search for
528
- :return: method from self instance
529
- """
530
- try:
531
- method = getattr(self, method_name)
532
- except AttributeError as e:
533
- raise UnfoldException(
534
- f"Method {method_name} specified does not exist on current object"
535
- ) from e
536
-
537
- if not callable(method):
538
- raise UnfoldException(f"{method_name} is not callable")
539
-
540
- return method
541
-
542
147
  def get_action_choices(
543
148
  self, request: HttpRequest, default_choices=BLANK_CHOICE_DASH
544
149
  ):
@@ -564,14 +169,14 @@ class ModelAdmin(ModelAdminMixin, BaseModelAdmin):
564
169
  return res
565
170
 
566
171
 
567
- class TabularInline(ModelAdminMixin, BaseTabularInline):
172
+ class TabularInline(BaseModelAdminMixin, BaseTabularInline):
568
173
  formfield_overrides = FORMFIELD_OVERRIDES_INLINE
569
174
  readonly_preprocess_fields = {}
570
175
  ordering_field = None
571
176
  hide_ordering_field = False
572
177
 
573
178
 
574
- class StackedInline(ModelAdminMixin, BaseStackedInline):
179
+ class StackedInline(BaseModelAdminMixin, BaseStackedInline):
575
180
  formfield_overrides = FORMFIELD_OVERRIDES_INLINE
576
181
  readonly_preprocess_fields = {}
577
182
  ordering_field = None
unfold/dataclasses.py CHANGED
@@ -12,6 +12,7 @@ class UnfoldAction:
12
12
  path: str
13
13
  attrs: Optional[dict] = None
14
14
  object_id: Optional[Union[int, str]] = None
15
+ icon: Optional[str] = None
15
16
 
16
17
 
17
18
  @dataclass
unfold/decorators.py CHANGED
@@ -17,6 +17,7 @@ def action(
17
17
  description: Optional[str] = None,
18
18
  url_path: Optional[str] = None,
19
19
  attrs: Optional[dict[str, Any]] = None,
20
+ icon: Optional[str] = None,
20
21
  ) -> ActionFunction:
21
22
  def decorator(func: Callable) -> ActionFunction:
22
23
  def inner(
@@ -33,8 +34,14 @@ def action(
33
34
  # Permissions methods have following syntax: has_<some>_permission(self, request, obj=None):
34
35
  # But obj is not examined by default in django admin and it would also require additional
35
36
  # fetch from database, therefore it is not supported yet
36
- if not any(
37
+ has_object_argument = (
38
+ func.__name__ in model_admin.actions_detail
39
+ or func.__name__ in model_admin.actions_submit_line
40
+ )
41
+ if not all(
37
42
  has_permission(request, kwargs.get("object_id"))
43
+ if has_object_argument
44
+ else has_permission(request)
38
45
  for has_permission in permission_checks
39
46
  ):
40
47
  raise PermissionDenied
@@ -42,10 +49,16 @@ def action(
42
49
 
43
50
  if permissions is not None:
44
51
  inner.allowed_permissions = permissions
52
+
45
53
  if description is not None:
46
54
  inner.short_description = description
55
+
47
56
  if url_path is not None:
48
57
  inner.url_path = url_path
58
+
59
+ if icon is not None:
60
+ inner.icon = icon
61
+
49
62
  inner.attrs = attrs or {}
50
63
  return inner
51
64
 
unfold/fields.py CHANGED
@@ -17,17 +17,18 @@ from django.utils.module_loading import import_string
17
17
  from django.utils.safestring import SafeText, mark_safe
18
18
  from django.utils.text import capfirst
19
19
 
20
- from .settings import get_config
21
- from .utils import display_for_field, prettify_json
22
- from .widgets import CHECKBOX_LABEL_CLASSES, LABEL_CLASSES
20
+ from unfold.mixins import BaseModelAdminMixin
21
+ from unfold.settings import get_config
22
+ from unfold.utils import display_for_field, prettify_json
23
+ from unfold.widgets import CHECKBOX_LABEL_CLASSES, LABEL_CLASSES
23
24
 
24
25
 
25
26
  class UnfoldAdminReadonlyField(helpers.AdminReadonlyField):
26
27
  def label_tag(self) -> SafeText:
27
- from .admin import ModelAdmin, ModelAdminMixin
28
+ from .admin import ModelAdmin
28
29
 
29
30
  if not isinstance(self.model_admin, ModelAdmin) and not isinstance(
30
- self.model_admin, ModelAdminMixin
31
+ self.model_admin, BaseModelAdminMixin
31
32
  ):
32
33
  return super().label_tag()
33
34
 
@@ -0,0 +1,4 @@
1
+ from unfold.mixins.action_model_admin import ActionModelAdminMixin
2
+ from unfold.mixins.base_model_admin import BaseModelAdminMixin
3
+
4
+ __all__ = ["BaseModelAdminMixin", "ActionModelAdminMixin"]