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
@@ -0,0 +1,329 @@
|
|
1
|
+
from typing import Any, Callable, Optional, Union
|
2
|
+
|
3
|
+
from django.db.models import Model
|
4
|
+
from django.forms import Form
|
5
|
+
from django.http import HttpRequest
|
6
|
+
from django.template.response import TemplateResponse
|
7
|
+
from django.urls import reverse
|
8
|
+
|
9
|
+
from unfold.dataclasses import UnfoldAction
|
10
|
+
from unfold.exceptions import UnfoldException
|
11
|
+
|
12
|
+
|
13
|
+
class ActionModelAdminMixin:
|
14
|
+
"""
|
15
|
+
Adds support for various ModelAdmin actions (list, detail, row, submit line)
|
16
|
+
"""
|
17
|
+
|
18
|
+
actions_list = () # Displayed in changelist at the top
|
19
|
+
actions_row = () # Displayed in changelist for each row in the table
|
20
|
+
actions_detail = () # Displayed in changeform at the top
|
21
|
+
actions_submit_line = () # Displayed in changeform in the submit line (form buttons)
|
22
|
+
|
23
|
+
def changelist_view(
|
24
|
+
self, request: HttpRequest, extra_context: Optional[dict[str, str]] = None
|
25
|
+
) -> TemplateResponse:
|
26
|
+
"""
|
27
|
+
Changelist contains `actions_list` and `actions_row` custom actions. In case of `actions_row` they
|
28
|
+
are displayed in the each row of the table.
|
29
|
+
"""
|
30
|
+
extra_context = extra_context or {}
|
31
|
+
|
32
|
+
actions_row = [
|
33
|
+
{
|
34
|
+
"title": action.description,
|
35
|
+
"icon": action.icon,
|
36
|
+
"attrs": action.method.attrs,
|
37
|
+
# This is just a path name as string and in template is used in {% url %} tag
|
38
|
+
# with custom instance pk value
|
39
|
+
"raw_path": f"{self.admin_site.name}:{action.action_name}",
|
40
|
+
}
|
41
|
+
for action in self.get_actions_row(request)
|
42
|
+
]
|
43
|
+
|
44
|
+
# `actions_list` may contain custom structure with dropdowns so it is needed
|
45
|
+
# to use `_get_actions_navigation` to build the final structure for the template
|
46
|
+
actions_list = self._get_actions_navigation(
|
47
|
+
self.actions_list, self.get_actions_list(request)
|
48
|
+
)
|
49
|
+
|
50
|
+
extra_context.update(
|
51
|
+
{
|
52
|
+
"actions_list": actions_list,
|
53
|
+
"actions_row": actions_row,
|
54
|
+
}
|
55
|
+
)
|
56
|
+
|
57
|
+
return super().changelist_view(request, extra_context)
|
58
|
+
|
59
|
+
def changeform_view(
|
60
|
+
self,
|
61
|
+
request: HttpRequest,
|
62
|
+
object_id: Optional[str] = None,
|
63
|
+
form_url: str = "",
|
64
|
+
extra_context: Optional[dict[str, bool]] = None,
|
65
|
+
) -> Any:
|
66
|
+
"""
|
67
|
+
Changeform contains `actions_submit_line` and `actions_detail` custom actions.
|
68
|
+
"""
|
69
|
+
extra_context = extra_context or {}
|
70
|
+
|
71
|
+
# `actions_submit_line` is a list of actions that are displayed in the submit line they
|
72
|
+
# are displayed as form buttons
|
73
|
+
actions_submit_line = self.get_actions_submit_line(request, object_id)
|
74
|
+
|
75
|
+
# `actions_detail` may contain custom structure with dropdowns so it is needed
|
76
|
+
# to use `_get_actions_navigation` to build the final structure for the template
|
77
|
+
actions_detail = self._get_actions_navigation(
|
78
|
+
self.actions_detail,
|
79
|
+
self.get_actions_detail(request, object_id),
|
80
|
+
object_id,
|
81
|
+
)
|
82
|
+
|
83
|
+
extra_context.update(
|
84
|
+
{
|
85
|
+
"actions_submit_line": actions_submit_line,
|
86
|
+
"actions_detail": actions_detail,
|
87
|
+
}
|
88
|
+
)
|
89
|
+
|
90
|
+
return super().changeform_view(request, object_id, form_url, extra_context)
|
91
|
+
|
92
|
+
def save_model(
|
93
|
+
self, request: HttpRequest, obj: Model, form: Form, change: Any
|
94
|
+
) -> None:
|
95
|
+
"""
|
96
|
+
When saving object, run all appropriate actions from `actions_submit_line`
|
97
|
+
"""
|
98
|
+
super().save_model(request, obj, form, change)
|
99
|
+
|
100
|
+
# After saving object, check if any button from `actions_submit_line` was pressed
|
101
|
+
# and call the corresponding method
|
102
|
+
for action in self.get_actions_submit_line(request, obj.pk):
|
103
|
+
if action.action_name not in request.POST:
|
104
|
+
continue
|
105
|
+
|
106
|
+
action.method(request, obj)
|
107
|
+
|
108
|
+
def get_unfold_action(self, action: str) -> UnfoldAction:
|
109
|
+
"""
|
110
|
+
Converts action name into UnfoldAction object.
|
111
|
+
"""
|
112
|
+
method = self._get_instance_method(action)
|
113
|
+
|
114
|
+
return UnfoldAction(
|
115
|
+
action_name=f"{self.model._meta.app_label}_{self.model._meta.model_name}_{action}",
|
116
|
+
method=method,
|
117
|
+
description=self._get_action_description(method, action),
|
118
|
+
path=getattr(method, "url_path", action),
|
119
|
+
attrs=method.attrs if hasattr(method, "attrs") else None,
|
120
|
+
icon=method.icon if hasattr(method, "icon") else None,
|
121
|
+
)
|
122
|
+
|
123
|
+
def get_actions_list(self, request: HttpRequest) -> list[UnfoldAction]:
|
124
|
+
"""
|
125
|
+
Filters `actions_list` by permissions and returns list of UnfoldAction objects.
|
126
|
+
"""
|
127
|
+
return self._filter_unfold_actions_by_permissions(
|
128
|
+
request, self._get_base_actions_list()
|
129
|
+
)
|
130
|
+
|
131
|
+
def get_actions_detail(
|
132
|
+
self, request: HttpRequest, object_id: int
|
133
|
+
) -> list[UnfoldAction]:
|
134
|
+
"""
|
135
|
+
Filters `actions_detail` by permissions and returns list of UnfoldAction objects.
|
136
|
+
"""
|
137
|
+
return self._filter_unfold_actions_by_permissions(
|
138
|
+
request, self._get_base_actions_detail(), object_id
|
139
|
+
)
|
140
|
+
|
141
|
+
def get_actions_row(self, request: HttpRequest) -> list[UnfoldAction]:
|
142
|
+
"""
|
143
|
+
Filters `actions_row` by permissions and returns list of UnfoldAction objects.
|
144
|
+
"""
|
145
|
+
return self._filter_unfold_actions_by_permissions(
|
146
|
+
request, self._get_base_actions_row()
|
147
|
+
)
|
148
|
+
|
149
|
+
def get_actions_submit_line(
|
150
|
+
self, request: HttpRequest, object_id: int
|
151
|
+
) -> list[UnfoldAction]:
|
152
|
+
"""
|
153
|
+
Filters `actions_submit_line` by permissions and returns list of UnfoldAction objects.
|
154
|
+
"""
|
155
|
+
return self._filter_unfold_actions_by_permissions(
|
156
|
+
request, self._get_base_actions_submit_line(), object_id
|
157
|
+
)
|
158
|
+
|
159
|
+
def _extract_action_names(self, actions: list[Union[str, dict]]) -> list[str]:
|
160
|
+
"""
|
161
|
+
Gets the list of only actions names from the actions structure provided in ModelAdmin
|
162
|
+
"""
|
163
|
+
results = []
|
164
|
+
|
165
|
+
for action in actions or []:
|
166
|
+
if isinstance(action, dict) and "items" in action:
|
167
|
+
results.extend(action["items"])
|
168
|
+
else:
|
169
|
+
results.append(action)
|
170
|
+
|
171
|
+
return results
|
172
|
+
|
173
|
+
def _get_base_actions_list(self) -> list[UnfoldAction]:
|
174
|
+
"""
|
175
|
+
Returns list of UnfoldAction objects for `actions_list`.
|
176
|
+
"""
|
177
|
+
return [
|
178
|
+
self.get_unfold_action(action)
|
179
|
+
for action in self._extract_action_names(self.actions_list)
|
180
|
+
]
|
181
|
+
|
182
|
+
def _get_base_actions_detail(self) -> list[UnfoldAction]:
|
183
|
+
"""
|
184
|
+
Returns list of UnfoldAction objects for `actions_detail`.
|
185
|
+
"""
|
186
|
+
return [
|
187
|
+
self.get_unfold_action(action)
|
188
|
+
for action in self._extract_action_names(self.actions_detail) or []
|
189
|
+
]
|
190
|
+
|
191
|
+
def _get_base_actions_row(self) -> list[UnfoldAction]:
|
192
|
+
"""
|
193
|
+
Returns list of UnfoldAction objects for `actions_row`.
|
194
|
+
"""
|
195
|
+
return [
|
196
|
+
self.get_unfold_action(action)
|
197
|
+
for action in self._extract_action_names(self.actions_row) or []
|
198
|
+
]
|
199
|
+
|
200
|
+
def _get_base_actions_submit_line(self) -> list[UnfoldAction]:
|
201
|
+
"""
|
202
|
+
Returns list of UnfoldAction objects for `actions_submit_line`.
|
203
|
+
"""
|
204
|
+
return [
|
205
|
+
self.get_unfold_action(action)
|
206
|
+
for action in self._extract_action_names(self.actions_submit_line) or []
|
207
|
+
]
|
208
|
+
|
209
|
+
def _get_instance_method(self, method_name: str) -> Callable:
|
210
|
+
"""
|
211
|
+
Searches for method on self instance based on method_name and returns it if it exists.
|
212
|
+
If it does not exist or is not callable, it raises UnfoldException
|
213
|
+
"""
|
214
|
+
try:
|
215
|
+
method = getattr(self, method_name)
|
216
|
+
except AttributeError as e:
|
217
|
+
raise UnfoldException(
|
218
|
+
f"Method {method_name} specified does not exist on current object"
|
219
|
+
) from e
|
220
|
+
|
221
|
+
if not callable(method):
|
222
|
+
raise UnfoldException(f"{method_name} is not callable")
|
223
|
+
|
224
|
+
return method
|
225
|
+
|
226
|
+
def _get_actions_navigation(
|
227
|
+
self,
|
228
|
+
provided_actions: list[Union[str, dict]],
|
229
|
+
allowed_actions: list[UnfoldAction],
|
230
|
+
object_id: Optional[str] = None,
|
231
|
+
) -> list[Union[str, dict]]:
|
232
|
+
"""
|
233
|
+
Builds navigation structure for the actions which is going to be provided to the template.
|
234
|
+
"""
|
235
|
+
navigation = []
|
236
|
+
|
237
|
+
def get_action_by_name(name: str) -> UnfoldAction:
|
238
|
+
"""
|
239
|
+
Searches for an action in allowed_actions by its name.
|
240
|
+
"""
|
241
|
+
for action in allowed_actions:
|
242
|
+
full_action_name = (
|
243
|
+
f"{self.model._meta.app_label}_{self.model._meta.model_name}_{name}"
|
244
|
+
)
|
245
|
+
|
246
|
+
if action.action_name == full_action_name:
|
247
|
+
return action
|
248
|
+
|
249
|
+
def get_action_path(action: UnfoldAction) -> str:
|
250
|
+
"""
|
251
|
+
Returns the URL path for an action.
|
252
|
+
"""
|
253
|
+
path_name = f"{self.admin_site.name}:{action.action_name}"
|
254
|
+
|
255
|
+
if object_id:
|
256
|
+
return reverse(path_name, args=(object_id,))
|
257
|
+
|
258
|
+
return reverse(path_name)
|
259
|
+
|
260
|
+
def get_action_attrs(action: UnfoldAction) -> dict:
|
261
|
+
"""
|
262
|
+
Returns the attributes for an action which will be used in the template.
|
263
|
+
"""
|
264
|
+
return {
|
265
|
+
"title": action.description,
|
266
|
+
"icon": action.icon,
|
267
|
+
"attrs": action.method.attrs,
|
268
|
+
"path": get_action_path(action),
|
269
|
+
}
|
270
|
+
|
271
|
+
def build_dropdown(nav_item: dict) -> Optional[dict]:
|
272
|
+
"""
|
273
|
+
Builds a dropdown structure for the action.
|
274
|
+
"""
|
275
|
+
dropdown = {
|
276
|
+
"title": nav_item["title"],
|
277
|
+
"icon": nav_item.get("icon"),
|
278
|
+
"items": [],
|
279
|
+
}
|
280
|
+
|
281
|
+
for child in nav_item["items"]:
|
282
|
+
if action := get_action_by_name(child):
|
283
|
+
dropdown["items"].append(get_action_attrs(action))
|
284
|
+
|
285
|
+
if len(dropdown["items"]) > 0:
|
286
|
+
return dropdown
|
287
|
+
|
288
|
+
for nav_item in provided_actions:
|
289
|
+
if isinstance(nav_item, str):
|
290
|
+
if action := get_action_by_name(nav_item):
|
291
|
+
navigation.append(get_action_attrs(action))
|
292
|
+
elif isinstance(nav_item, dict):
|
293
|
+
if dropdown := build_dropdown(nav_item):
|
294
|
+
navigation.append(dropdown)
|
295
|
+
|
296
|
+
return navigation
|
297
|
+
|
298
|
+
def _filter_unfold_actions_by_permissions(
|
299
|
+
self,
|
300
|
+
request: HttpRequest,
|
301
|
+
actions: list[UnfoldAction],
|
302
|
+
object_id: Optional[Union[int, str]] = None,
|
303
|
+
) -> list[UnfoldAction]:
|
304
|
+
"""
|
305
|
+
Filters out actions that the user doesn't have access to.
|
306
|
+
"""
|
307
|
+
filtered_actions = []
|
308
|
+
|
309
|
+
for action in actions:
|
310
|
+
if not hasattr(action.method, "allowed_permissions"):
|
311
|
+
filtered_actions.append(action)
|
312
|
+
continue
|
313
|
+
|
314
|
+
permission_checks = (
|
315
|
+
getattr(self, f"has_{permission}_permission")
|
316
|
+
for permission in action.method.allowed_permissions
|
317
|
+
)
|
318
|
+
|
319
|
+
if object_id:
|
320
|
+
if all(
|
321
|
+
has_permission(request, object_id)
|
322
|
+
for has_permission in permission_checks
|
323
|
+
):
|
324
|
+
filtered_actions.append(action)
|
325
|
+
else:
|
326
|
+
if all(has_permission(request) for has_permission in permission_checks):
|
327
|
+
filtered_actions.append(action)
|
328
|
+
|
329
|
+
return filtered_actions
|
@@ -0,0 +1,110 @@
|
|
1
|
+
import copy
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
from django.contrib.admin.sites import AdminSite
|
5
|
+
from django.contrib.admin.widgets import RelatedFieldWidgetWrapper
|
6
|
+
from django.db import models
|
7
|
+
from django.db.models.fields import Field
|
8
|
+
from django.db.models.fields.related import ForeignKey, ManyToManyField
|
9
|
+
from django.forms.fields import TypedChoiceField
|
10
|
+
from django.forms.models import ModelChoiceField, ModelMultipleChoiceField
|
11
|
+
from django.forms.widgets import SelectMultiple
|
12
|
+
from django.http import HttpRequest
|
13
|
+
from django.utils.translation import gettext_lazy as _
|
14
|
+
|
15
|
+
from unfold import widgets
|
16
|
+
from unfold.overrides import FORMFIELD_OVERRIDES
|
17
|
+
|
18
|
+
|
19
|
+
class BaseModelAdminMixin:
|
20
|
+
def __init__(self, model: models.Model, admin_site: AdminSite) -> None:
|
21
|
+
overrides = copy.deepcopy(FORMFIELD_OVERRIDES)
|
22
|
+
|
23
|
+
for k, v in self.formfield_overrides.items():
|
24
|
+
overrides.setdefault(k, {}).update(v)
|
25
|
+
|
26
|
+
self.formfield_overrides = overrides
|
27
|
+
|
28
|
+
super().__init__(model, admin_site)
|
29
|
+
|
30
|
+
def formfield_for_choice_field(
|
31
|
+
self, db_field: Field, request: HttpRequest, **kwargs
|
32
|
+
) -> TypedChoiceField:
|
33
|
+
if "widget" not in kwargs:
|
34
|
+
if db_field.name in self.radio_fields:
|
35
|
+
kwargs["widget"] = widgets.UnfoldAdminRadioSelectWidget(
|
36
|
+
radio_style=self.radio_fields[db_field.name]
|
37
|
+
)
|
38
|
+
else:
|
39
|
+
kwargs["widget"] = widgets.UnfoldAdminSelectWidget()
|
40
|
+
|
41
|
+
if "choices" not in kwargs:
|
42
|
+
kwargs["choices"] = db_field.get_choices(
|
43
|
+
include_blank=db_field.blank, blank_choice=[("", _("Select value"))]
|
44
|
+
)
|
45
|
+
|
46
|
+
return super().formfield_for_choice_field(db_field, request, **kwargs)
|
47
|
+
|
48
|
+
def formfield_for_foreignkey(
|
49
|
+
self, db_field: ForeignKey, request: HttpRequest, **kwargs
|
50
|
+
) -> Optional[ModelChoiceField]:
|
51
|
+
db = kwargs.get("using")
|
52
|
+
|
53
|
+
# Overrides widgets for all related fields
|
54
|
+
if "widget" not in kwargs:
|
55
|
+
if db_field.name in self.raw_id_fields:
|
56
|
+
kwargs["widget"] = widgets.UnfoldForeignKeyRawIdWidget(
|
57
|
+
db_field.remote_field, self.admin_site, using=db
|
58
|
+
)
|
59
|
+
elif (
|
60
|
+
db_field.name not in self.get_autocomplete_fields(request)
|
61
|
+
and db_field.name not in self.radio_fields
|
62
|
+
):
|
63
|
+
kwargs["widget"] = widgets.UnfoldAdminSelectWidget()
|
64
|
+
kwargs["empty_label"] = _("Select value")
|
65
|
+
|
66
|
+
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
67
|
+
|
68
|
+
def formfield_for_manytomany(
|
69
|
+
self,
|
70
|
+
db_field: ManyToManyField,
|
71
|
+
request: HttpRequest,
|
72
|
+
**kwargs,
|
73
|
+
) -> ModelMultipleChoiceField:
|
74
|
+
if "widget" not in kwargs:
|
75
|
+
if db_field.name in self.raw_id_fields:
|
76
|
+
kwargs["widget"] = widgets.UnfoldAdminTextInputWidget()
|
77
|
+
|
78
|
+
form_field = super().formfield_for_manytomany(db_field, request, **kwargs)
|
79
|
+
|
80
|
+
# If M2M uses intermediary model, form_field will be None
|
81
|
+
if not form_field:
|
82
|
+
return None
|
83
|
+
|
84
|
+
if isinstance(form_field.widget, SelectMultiple):
|
85
|
+
form_field.widget.attrs["class"] = " ".join(widgets.SELECT_CLASSES)
|
86
|
+
|
87
|
+
return form_field
|
88
|
+
|
89
|
+
def formfield_for_nullboolean_field(
|
90
|
+
self, db_field: Field, request: HttpRequest, **kwargs
|
91
|
+
) -> Optional[Field]:
|
92
|
+
if "widget" not in kwargs:
|
93
|
+
kwargs["widget"] = widgets.UnfoldAdminNullBooleanSelectWidget()
|
94
|
+
|
95
|
+
return db_field.formfield(**kwargs)
|
96
|
+
|
97
|
+
def formfield_for_dbfield(
|
98
|
+
self, db_field: Field, request: HttpRequest, **kwargs
|
99
|
+
) -> Optional[Field]:
|
100
|
+
if isinstance(db_field, models.BooleanField) and db_field.null is True:
|
101
|
+
return self.formfield_for_nullboolean_field(db_field, request, **kwargs)
|
102
|
+
|
103
|
+
formfield = super().formfield_for_dbfield(db_field, request, **kwargs)
|
104
|
+
|
105
|
+
if formfield and isinstance(formfield.widget, RelatedFieldWidgetWrapper):
|
106
|
+
formfield.widget.template_name = (
|
107
|
+
"unfold/widgets/related_widget_wrapper.html"
|
108
|
+
)
|
109
|
+
|
110
|
+
return formfield
|
unfold/overrides.py
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
import copy
|
2
|
+
|
3
|
+
from django import forms
|
4
|
+
from django.db import models
|
5
|
+
|
6
|
+
from unfold import widgets
|
7
|
+
|
8
|
+
FORMFIELD_OVERRIDES = {
|
9
|
+
models.DateTimeField: {
|
10
|
+
"form_class": forms.SplitDateTimeField,
|
11
|
+
"widget": widgets.UnfoldAdminSplitDateTimeWidget,
|
12
|
+
},
|
13
|
+
models.DateField: {"widget": widgets.UnfoldAdminSingleDateWidget},
|
14
|
+
models.TimeField: {"widget": widgets.UnfoldAdminSingleTimeWidget},
|
15
|
+
models.EmailField: {"widget": widgets.UnfoldAdminEmailInputWidget},
|
16
|
+
models.CharField: {"widget": widgets.UnfoldAdminTextInputWidget},
|
17
|
+
models.URLField: {"widget": widgets.UnfoldAdminURLInputWidget},
|
18
|
+
models.GenericIPAddressField: {"widget": widgets.UnfoldAdminTextInputWidget},
|
19
|
+
models.UUIDField: {"widget": widgets.UnfoldAdminUUIDInputWidget},
|
20
|
+
models.TextField: {"widget": widgets.UnfoldAdminTextareaWidget},
|
21
|
+
models.NullBooleanField: {"widget": widgets.UnfoldAdminNullBooleanSelectWidget},
|
22
|
+
models.BooleanField: {"widget": widgets.UnfoldBooleanSwitchWidget},
|
23
|
+
models.IntegerField: {"widget": widgets.UnfoldAdminIntegerFieldWidget},
|
24
|
+
models.BigIntegerField: {"widget": widgets.UnfoldAdminBigIntegerFieldWidget},
|
25
|
+
models.DecimalField: {"widget": widgets.UnfoldAdminDecimalFieldWidget},
|
26
|
+
models.FloatField: {"widget": widgets.UnfoldAdminDecimalFieldWidget},
|
27
|
+
models.FileField: {"widget": widgets.UnfoldAdminFileFieldWidget},
|
28
|
+
models.ImageField: {"widget": widgets.UnfoldAdminImageFieldWidget},
|
29
|
+
models.JSONField: {"widget": widgets.UnfoldAdminTextareaWidget},
|
30
|
+
models.DurationField: {"widget": widgets.UnfoldAdminTextInputWidget},
|
31
|
+
}
|
32
|
+
|
33
|
+
######################################################################
|
34
|
+
# Postgres
|
35
|
+
######################################################################
|
36
|
+
try:
|
37
|
+
from django.contrib.postgres.fields import ArrayField, IntegerRangeField
|
38
|
+
from django.contrib.postgres.search import SearchVectorField
|
39
|
+
|
40
|
+
FORMFIELD_OVERRIDES.update(
|
41
|
+
{
|
42
|
+
ArrayField: {"widget": widgets.UnfoldAdminTextareaWidget},
|
43
|
+
SearchVectorField: {"widget": widgets.UnfoldAdminTextareaWidget},
|
44
|
+
IntegerRangeField: {"widget": widgets.UnfoldAdminIntegerRangeWidget},
|
45
|
+
}
|
46
|
+
)
|
47
|
+
except ImportError:
|
48
|
+
pass
|
49
|
+
|
50
|
+
######################################################################
|
51
|
+
# Django Money
|
52
|
+
######################################################################
|
53
|
+
try:
|
54
|
+
from djmoney.models.fields import MoneyField
|
55
|
+
|
56
|
+
FORMFIELD_OVERRIDES.update(
|
57
|
+
{
|
58
|
+
MoneyField: {"widget": widgets.UnfoldAdminMoneyWidget},
|
59
|
+
}
|
60
|
+
)
|
61
|
+
except ImportError:
|
62
|
+
pass
|
63
|
+
|
64
|
+
######################################################################
|
65
|
+
# Inlines
|
66
|
+
######################################################################
|
67
|
+
FORMFIELD_OVERRIDES_INLINE = copy.deepcopy(FORMFIELD_OVERRIDES)
|
68
|
+
|
69
|
+
FORMFIELD_OVERRIDES_INLINE.update(
|
70
|
+
{
|
71
|
+
models.ImageField: {"widget": widgets.UnfoldAdminImageSmallFieldWidget},
|
72
|
+
}
|
73
|
+
)
|
unfold/sites.py
CHANGED
@@ -71,11 +71,11 @@ class UnfoldAdminSite(AdminSite):
|
|
71
71
|
"text_input": INPUT_CLASSES,
|
72
72
|
"checkbox": CHECKBOX_CLASSES,
|
73
73
|
},
|
74
|
-
"site_title": self._get_config("SITE_TITLE", request),
|
75
|
-
"site_header": self._get_config("SITE_HEADER", request),
|
74
|
+
"site_title": self._get_config("SITE_TITLE", request) or self.site_title,
|
75
|
+
"site_header": self._get_config("SITE_HEADER", request) or self.site_header,
|
76
|
+
"site_url": self._get_config("SITE_URL", request) or self.site_url,
|
76
77
|
"site_subheader": self._get_config("SITE_SUBHEADER", request),
|
77
78
|
"site_dropdown": self._get_site_dropdown_items("SITE_DROPDOWN", request),
|
78
|
-
"site_url": self._get_config("SITE_URL", request),
|
79
79
|
"site_logo": self._get_theme_images("SITE_LOGO", request),
|
80
80
|
"site_icon": self._get_theme_images("SITE_ICON", request),
|
81
81
|
"site_symbol": self._get_config("SITE_SYMBOL", request),
|