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
@@ -0,0 +1,330 @@
|
|
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
|
+
if object_id:
|
72
|
+
# `actions_submit_line` is a list of actions that are displayed in the submit line they
|
73
|
+
# are displayed as form buttons
|
74
|
+
actions_submit_line = self.get_actions_submit_line(request, object_id)
|
75
|
+
|
76
|
+
# `actions_detail` may contain custom structure with dropdowns so it is needed
|
77
|
+
# to use `_get_actions_navigation` to build the final structure for the template
|
78
|
+
actions_detail = self._get_actions_navigation(
|
79
|
+
self.actions_detail,
|
80
|
+
self.get_actions_detail(request, object_id),
|
81
|
+
object_id,
|
82
|
+
)
|
83
|
+
|
84
|
+
extra_context.update(
|
85
|
+
{
|
86
|
+
"actions_submit_line": actions_submit_line,
|
87
|
+
"actions_detail": actions_detail,
|
88
|
+
}
|
89
|
+
)
|
90
|
+
|
91
|
+
return super().changeform_view(request, object_id, form_url, extra_context)
|
92
|
+
|
93
|
+
def save_model(
|
94
|
+
self, request: HttpRequest, obj: Model, form: Form, change: Any
|
95
|
+
) -> None:
|
96
|
+
"""
|
97
|
+
When saving object, run all appropriate actions from `actions_submit_line`
|
98
|
+
"""
|
99
|
+
super().save_model(request, obj, form, change)
|
100
|
+
|
101
|
+
# After saving object, check if any button from `actions_submit_line` was pressed
|
102
|
+
# and call the corresponding method
|
103
|
+
for action in self.get_actions_submit_line(request, obj.pk):
|
104
|
+
if action.action_name not in request.POST:
|
105
|
+
continue
|
106
|
+
|
107
|
+
action.method(request, obj)
|
108
|
+
|
109
|
+
def get_unfold_action(self, action: str) -> UnfoldAction:
|
110
|
+
"""
|
111
|
+
Converts action name into UnfoldAction object.
|
112
|
+
"""
|
113
|
+
method = self._get_instance_method(action)
|
114
|
+
|
115
|
+
return UnfoldAction(
|
116
|
+
action_name=f"{self.model._meta.app_label}_{self.model._meta.model_name}_{action}",
|
117
|
+
method=method,
|
118
|
+
description=self._get_action_description(method, action),
|
119
|
+
path=getattr(method, "url_path", action),
|
120
|
+
attrs=method.attrs if hasattr(method, "attrs") else None,
|
121
|
+
icon=method.icon if hasattr(method, "icon") else None,
|
122
|
+
)
|
123
|
+
|
124
|
+
def get_actions_list(self, request: HttpRequest) -> list[UnfoldAction]:
|
125
|
+
"""
|
126
|
+
Filters `actions_list` by permissions and returns list of UnfoldAction objects.
|
127
|
+
"""
|
128
|
+
return self._filter_unfold_actions_by_permissions(
|
129
|
+
request, self._get_base_actions_list()
|
130
|
+
)
|
131
|
+
|
132
|
+
def get_actions_detail(
|
133
|
+
self, request: HttpRequest, object_id: int
|
134
|
+
) -> list[UnfoldAction]:
|
135
|
+
"""
|
136
|
+
Filters `actions_detail` by permissions and returns list of UnfoldAction objects.
|
137
|
+
"""
|
138
|
+
return self._filter_unfold_actions_by_permissions(
|
139
|
+
request, self._get_base_actions_detail(), object_id
|
140
|
+
)
|
141
|
+
|
142
|
+
def get_actions_row(self, request: HttpRequest) -> list[UnfoldAction]:
|
143
|
+
"""
|
144
|
+
Filters `actions_row` by permissions and returns list of UnfoldAction objects.
|
145
|
+
"""
|
146
|
+
return self._filter_unfold_actions_by_permissions(
|
147
|
+
request, self._get_base_actions_row()
|
148
|
+
)
|
149
|
+
|
150
|
+
def get_actions_submit_line(
|
151
|
+
self, request: HttpRequest, object_id: int
|
152
|
+
) -> list[UnfoldAction]:
|
153
|
+
"""
|
154
|
+
Filters `actions_submit_line` by permissions and returns list of UnfoldAction objects.
|
155
|
+
"""
|
156
|
+
return self._filter_unfold_actions_by_permissions(
|
157
|
+
request, self._get_base_actions_submit_line(), object_id
|
158
|
+
)
|
159
|
+
|
160
|
+
def _extract_action_names(self, actions: list[Union[str, dict]]) -> list[str]:
|
161
|
+
"""
|
162
|
+
Gets the list of only actions names from the actions structure provided in ModelAdmin
|
163
|
+
"""
|
164
|
+
results = []
|
165
|
+
|
166
|
+
for action in actions or []:
|
167
|
+
if isinstance(action, dict) and "items" in action:
|
168
|
+
results.extend(action["items"])
|
169
|
+
else:
|
170
|
+
results.append(action)
|
171
|
+
|
172
|
+
return results
|
173
|
+
|
174
|
+
def _get_base_actions_list(self) -> list[UnfoldAction]:
|
175
|
+
"""
|
176
|
+
Returns list of UnfoldAction objects for `actions_list`.
|
177
|
+
"""
|
178
|
+
return [
|
179
|
+
self.get_unfold_action(action)
|
180
|
+
for action in self._extract_action_names(self.actions_list)
|
181
|
+
]
|
182
|
+
|
183
|
+
def _get_base_actions_detail(self) -> list[UnfoldAction]:
|
184
|
+
"""
|
185
|
+
Returns list of UnfoldAction objects for `actions_detail`.
|
186
|
+
"""
|
187
|
+
return [
|
188
|
+
self.get_unfold_action(action)
|
189
|
+
for action in self._extract_action_names(self.actions_detail) or []
|
190
|
+
]
|
191
|
+
|
192
|
+
def _get_base_actions_row(self) -> list[UnfoldAction]:
|
193
|
+
"""
|
194
|
+
Returns list of UnfoldAction objects for `actions_row`.
|
195
|
+
"""
|
196
|
+
return [
|
197
|
+
self.get_unfold_action(action)
|
198
|
+
for action in self._extract_action_names(self.actions_row) or []
|
199
|
+
]
|
200
|
+
|
201
|
+
def _get_base_actions_submit_line(self) -> list[UnfoldAction]:
|
202
|
+
"""
|
203
|
+
Returns list of UnfoldAction objects for `actions_submit_line`.
|
204
|
+
"""
|
205
|
+
return [
|
206
|
+
self.get_unfold_action(action)
|
207
|
+
for action in self._extract_action_names(self.actions_submit_line) or []
|
208
|
+
]
|
209
|
+
|
210
|
+
def _get_instance_method(self, method_name: str) -> Callable:
|
211
|
+
"""
|
212
|
+
Searches for method on self instance based on method_name and returns it if it exists.
|
213
|
+
If it does not exist or is not callable, it raises UnfoldException
|
214
|
+
"""
|
215
|
+
try:
|
216
|
+
method = getattr(self, method_name)
|
217
|
+
except AttributeError as e:
|
218
|
+
raise UnfoldException(
|
219
|
+
f"Method {method_name} specified does not exist on current object"
|
220
|
+
) from e
|
221
|
+
|
222
|
+
if not callable(method):
|
223
|
+
raise UnfoldException(f"{method_name} is not callable")
|
224
|
+
|
225
|
+
return method
|
226
|
+
|
227
|
+
def _get_actions_navigation(
|
228
|
+
self,
|
229
|
+
provided_actions: list[Union[str, dict]],
|
230
|
+
allowed_actions: list[UnfoldAction],
|
231
|
+
object_id: Optional[str] = None,
|
232
|
+
) -> list[Union[str, dict]]:
|
233
|
+
"""
|
234
|
+
Builds navigation structure for the actions which is going to be provided to the template.
|
235
|
+
"""
|
236
|
+
navigation = []
|
237
|
+
|
238
|
+
def get_action_by_name(name: str) -> UnfoldAction:
|
239
|
+
"""
|
240
|
+
Searches for an action in allowed_actions by its name.
|
241
|
+
"""
|
242
|
+
for action in allowed_actions:
|
243
|
+
full_action_name = (
|
244
|
+
f"{self.model._meta.app_label}_{self.model._meta.model_name}_{name}"
|
245
|
+
)
|
246
|
+
|
247
|
+
if action.action_name == full_action_name:
|
248
|
+
return action
|
249
|
+
|
250
|
+
def get_action_path(action: UnfoldAction) -> str:
|
251
|
+
"""
|
252
|
+
Returns the URL path for an action.
|
253
|
+
"""
|
254
|
+
path_name = f"{self.admin_site.name}:{action.action_name}"
|
255
|
+
|
256
|
+
if object_id:
|
257
|
+
return reverse(path_name, args=(object_id,))
|
258
|
+
|
259
|
+
return reverse(path_name)
|
260
|
+
|
261
|
+
def get_action_attrs(action: UnfoldAction) -> dict:
|
262
|
+
"""
|
263
|
+
Returns the attributes for an action which will be used in the template.
|
264
|
+
"""
|
265
|
+
return {
|
266
|
+
"title": action.description,
|
267
|
+
"icon": action.icon,
|
268
|
+
"attrs": action.method.attrs,
|
269
|
+
"path": get_action_path(action),
|
270
|
+
}
|
271
|
+
|
272
|
+
def build_dropdown(nav_item: dict) -> Optional[dict]:
|
273
|
+
"""
|
274
|
+
Builds a dropdown structure for the action.
|
275
|
+
"""
|
276
|
+
dropdown = {
|
277
|
+
"title": nav_item["title"],
|
278
|
+
"icon": nav_item.get("icon"),
|
279
|
+
"items": [],
|
280
|
+
}
|
281
|
+
|
282
|
+
for child in nav_item["items"]:
|
283
|
+
if action := get_action_by_name(child):
|
284
|
+
dropdown["items"].append(get_action_attrs(action))
|
285
|
+
|
286
|
+
if len(dropdown["items"]) > 0:
|
287
|
+
return dropdown
|
288
|
+
|
289
|
+
for nav_item in provided_actions:
|
290
|
+
if isinstance(nav_item, str):
|
291
|
+
if action := get_action_by_name(nav_item):
|
292
|
+
navigation.append(get_action_attrs(action))
|
293
|
+
elif isinstance(nav_item, dict):
|
294
|
+
if dropdown := build_dropdown(nav_item):
|
295
|
+
navigation.append(dropdown)
|
296
|
+
|
297
|
+
return navigation
|
298
|
+
|
299
|
+
def _filter_unfold_actions_by_permissions(
|
300
|
+
self,
|
301
|
+
request: HttpRequest,
|
302
|
+
actions: list[UnfoldAction],
|
303
|
+
object_id: Optional[Union[int, str]] = None,
|
304
|
+
) -> list[UnfoldAction]:
|
305
|
+
"""
|
306
|
+
Filters out actions that the user doesn't have access to.
|
307
|
+
"""
|
308
|
+
filtered_actions = []
|
309
|
+
|
310
|
+
for action in actions:
|
311
|
+
if not hasattr(action.method, "allowed_permissions"):
|
312
|
+
filtered_actions.append(action)
|
313
|
+
continue
|
314
|
+
|
315
|
+
permission_checks = (
|
316
|
+
getattr(self, f"has_{permission}_permission")
|
317
|
+
for permission in action.method.allowed_permissions
|
318
|
+
)
|
319
|
+
|
320
|
+
if object_id:
|
321
|
+
if all(
|
322
|
+
has_permission(request, object_id)
|
323
|
+
for has_permission in permission_checks
|
324
|
+
):
|
325
|
+
filtered_actions.append(action)
|
326
|
+
else:
|
327
|
+
if all(has_permission(request) for has_permission in permission_checks):
|
328
|
+
filtered_actions.append(action)
|
329
|
+
|
330
|
+
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),
|