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.
- {django_unfold-0.48.0.dist-info → django_unfold-0.49.0.dist-info}/METADATA +1 -1
- {django_unfold-0.48.0.dist-info → django_unfold-0.49.0.dist-info}/RECORD +26 -22
- unfold/admin.py +17 -412
- unfold/dataclasses.py +1 -0
- unfold/decorators.py +14 -1
- unfold/fields.py +6 -5
- unfold/mixins/__init__.py +4 -0
- unfold/mixins/action_model_admin.py +329 -0
- unfold/mixins/base_model_admin.py +110 -0
- unfold/overrides.py +73 -0
- unfold/sites.py +3 -3
- unfold/static/unfold/css/styles.css +1 -1
- unfold/static/unfold/fonts/inter/Inter-Bold.woff2 +0 -0
- unfold/static/unfold/fonts/inter/Inter-Medium.woff2 +0 -0
- unfold/static/unfold/fonts/inter/Inter-Regular.woff2 +0 -0
- unfold/static/unfold/fonts/inter/Inter-SemiBold.woff2 +0 -0
- unfold/templates/admin/submit_line.html +7 -1
- unfold/templates/unfold/helpers/actions_row.html +14 -4
- unfold/templates/unfold/helpers/navigation_header.html +6 -3
- unfold/templates/unfold/helpers/site_dropdown.html +1 -1
- unfold/templates/unfold/helpers/site_icon.html +13 -11
- unfold/templates/unfold/helpers/tab_action.html +35 -2
- unfold/typing.py +2 -1
- unfold/widgets.py +2 -0
- {django_unfold-0.48.0.dist-info → django_unfold-0.49.0.dist-info}/LICENSE.md +0 -0
- {django_unfold-0.48.0.dist-info → django_unfold-0.49.0.dist-info}/WHEEL +0 -0
@@ -1,5 +1,5 @@
|
|
1
1
|
unfold/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
unfold/admin.py,sha256=
|
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=
|
76
|
-
unfold/decorators.py,sha256=
|
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=
|
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=
|
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=
|
86
|
-
unfold/static/unfold/fonts/inter/Inter-Bold.woff2,sha256
|
87
|
-
unfold/static/unfold/fonts/inter/Inter-Medium.woff2,sha256=
|
88
|
-
unfold/static/unfold/fonts/inter/Inter-Regular.woff2,sha256=
|
89
|
-
unfold/static/unfold/fonts/inter/Inter-SemiBold.woff2,sha256=
|
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=
|
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=
|
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=
|
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=
|
200
|
-
unfold/templates/unfold/helpers/site_icon.html,sha256=
|
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=
|
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=
|
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=
|
234
|
-
django_unfold-0.
|
235
|
-
django_unfold-0.
|
236
|
-
django_unfold-0.
|
237
|
-
django_unfold-0.
|
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
|
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.
|
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 .
|
29
|
-
from .
|
30
|
-
from .
|
31
|
-
from .
|
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
|
-
{
|
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
|
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(
|
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(
|
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
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
|
-
|
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 .
|
21
|
-
from .
|
22
|
-
from .
|
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
|
28
|
+
from .admin import ModelAdmin
|
28
29
|
|
29
30
|
if not isinstance(self.model_admin, ModelAdmin) and not isinstance(
|
30
|
-
self.model_admin,
|
31
|
+
self.model_admin, BaseModelAdminMixin
|
31
32
|
):
|
32
33
|
return super().label_tag()
|
33
34
|
|