django-unfold 0.48.0__py3-none-any.whl → 0.49.1__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- {django_unfold-0.48.0.dist-info → django_unfold-0.49.1.dist-info}/METADATA +1 -1
- {django_unfold-0.48.0.dist-info → django_unfold-0.49.1.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 +330 -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.1.dist-info}/LICENSE.md +0 -0
- {django_unfold-0.48.0.dist-info → django_unfold-0.49.1.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=FI326hzuGzoPBvQrl9UF-mGcczMhJJaW5Qcb43_hH8I,11803
|
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.1.dist-info/LICENSE.md,sha256=Ltk_quRyyvV3J5v3brtOqmibeZSw2Hrb8bY1W3ya0Ik,1077
|
239
|
+
django_unfold-0.49.1.dist-info/METADATA,sha256=TV-wBErjvceg-rCW_Qe4rP748lxMQJq2u2ggVrZmD6M,4840
|
240
|
+
django_unfold-0.49.1.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
|
241
|
+
django_unfold-0.49.1.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
|
|