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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-unfold
3
- Version: 0.48.0
3
+ Version: 0.49.1
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=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=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.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 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"]