django-unfold 0.46.0__py3-none-any.whl → 0.48.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- {django_unfold-0.46.0.dist-info → django_unfold-0.48.0.dist-info}/METADATA +5 -6
- {django_unfold-0.46.0.dist-info → django_unfold-0.48.0.dist-info}/RECORD +52 -43
- {django_unfold-0.46.0.dist-info → django_unfold-0.48.0.dist-info}/WHEEL +1 -1
- unfold/admin.py +15 -16
- unfold/checks.py +4 -4
- unfold/components.py +5 -5
- unfold/contrib/filters/admin/__init__.py +43 -0
- unfold/contrib/filters/admin/autocomplete_filters.py +16 -0
- unfold/contrib/filters/admin/datetime_filters.py +212 -0
- unfold/contrib/filters/admin/dropdown_filters.py +100 -0
- unfold/contrib/filters/admin/mixins.py +146 -0
- unfold/contrib/filters/admin/numeric_filters.py +196 -0
- unfold/contrib/filters/admin/text_filters.py +65 -0
- unfold/contrib/filters/admin.py +32 -32
- unfold/contrib/filters/forms.py +68 -17
- unfold/contrib/forms/widgets.py +9 -9
- unfold/contrib/inlines/checks.py +2 -4
- unfold/contrib/simple_history/templates/simple_history/object_history.html +17 -1
- unfold/contrib/simple_history/templates/simple_history/object_history_list.html +1 -1
- unfold/dataclasses.py +9 -2
- unfold/decorators.py +4 -3
- unfold/settings.py +4 -2
- unfold/sites.py +176 -140
- unfold/static/unfold/css/styles.css +1 -1
- unfold/static/unfold/js/app.js +2 -2
- unfold/templates/admin/app_index.html +1 -5
- unfold/templates/admin/base_site.html +1 -1
- unfold/templates/admin/filter.html +1 -1
- unfold/templates/admin/index.html +1 -5
- unfold/templates/admin/login.html +1 -1
- unfold/templates/admin/search_form.html +4 -2
- unfold/templates/unfold/helpers/account_links.html +1 -1
- unfold/templates/unfold/helpers/actions_row.html +1 -1
- unfold/templates/unfold/helpers/change_list_filter.html +2 -2
- unfold/templates/unfold/helpers/change_list_filter_actions.html +1 -1
- unfold/templates/unfold/helpers/header_back_button.html +2 -2
- unfold/templates/unfold/helpers/language_switch.html +1 -1
- unfold/templates/unfold/helpers/navigation_header.html +15 -5
- unfold/templates/unfold/helpers/site_branding.html +9 -0
- unfold/templates/unfold/helpers/site_dropdown.html +19 -0
- unfold/templates/unfold/helpers/site_icon.html +10 -2
- unfold/templates/unfold/helpers/tab_list.html +7 -1
- unfold/templates/unfold/helpers/theme_switch.html +1 -1
- unfold/templates/unfold/layouts/base.html +1 -5
- unfold/templates/unfold/layouts/skeleton.html +1 -1
- unfold/templatetags/unfold.py +55 -22
- unfold/templatetags/unfold_list.py +2 -2
- unfold/typing.py +5 -4
- unfold/utils.py +3 -2
- unfold/views.py +2 -2
- unfold/widgets.py +27 -27
- {django_unfold-0.46.0.dist-info → django_unfold-0.48.0.dist-info}/LICENSE.md +0 -0
unfold/sites.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
|
+
import copy
|
1
2
|
from http import HTTPStatus
|
2
|
-
from typing import Any, Callable,
|
3
|
+
from typing import Any, Callable, Optional, Union
|
3
4
|
from urllib.parse import parse_qs, urlparse
|
4
5
|
|
5
6
|
from django.contrib.admin import AdminSite
|
@@ -13,6 +14,8 @@ from django.utils.functional import lazy
|
|
13
14
|
from django.utils.module_loading import import_string
|
14
15
|
from django.views.decorators.cache import never_cache
|
15
16
|
|
17
|
+
from unfold.dataclasses import DropdownItem, Favicon
|
18
|
+
|
16
19
|
try:
|
17
20
|
from django.contrib.auth.decorators import login_not_required
|
18
21
|
except ImportError:
|
@@ -21,7 +24,6 @@ except ImportError:
|
|
21
24
|
return func
|
22
25
|
|
23
26
|
|
24
|
-
from .dataclasses import Favicon
|
25
27
|
from .settings import get_config
|
26
28
|
from .utils import hex_to_rgb
|
27
29
|
from .widgets import CHECKBOX_CLASSES, INPUT_CLASSES
|
@@ -39,16 +41,7 @@ class UnfoldAdminSite(AdminSite):
|
|
39
41
|
if self.login_form is None:
|
40
42
|
self.login_form = AuthenticationForm
|
41
43
|
|
42
|
-
|
43
|
-
self.site_title = get_config(self.settings_name)["SITE_TITLE"]
|
44
|
-
|
45
|
-
if get_config(self.settings_name)["SITE_HEADER"]:
|
46
|
-
self.site_header = get_config(self.settings_name)["SITE_HEADER"]
|
47
|
-
|
48
|
-
if get_config(self.settings_name)["SITE_URL"]:
|
49
|
-
self.site_url = get_config(self.settings_name)["SITE_URL"]
|
50
|
-
|
51
|
-
def get_urls(self) -> List[URLPattern]:
|
44
|
+
def get_urls(self) -> list[URLPattern]:
|
52
45
|
extra_urls = []
|
53
46
|
|
54
47
|
if hasattr(self, "extra_urls") and callable(self.extra_urls):
|
@@ -69,69 +62,47 @@ class UnfoldAdminSite(AdminSite):
|
|
69
62
|
|
70
63
|
return urlpatterns
|
71
64
|
|
72
|
-
def each_context(self, request: HttpRequest) ->
|
65
|
+
def each_context(self, request: HttpRequest) -> dict[str, Any]:
|
73
66
|
context = super().each_context(request)
|
74
67
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
"
|
103
|
-
|
104
|
-
|
105
|
-
"
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
"scripts": [
|
112
|
-
self._get_value(script, request)
|
113
|
-
for script in get_config(self.settings_name)["SCRIPTS"]
|
114
|
-
],
|
115
|
-
"sidebar_show_all_applications": get_config(self.settings_name)[
|
116
|
-
"SIDEBAR"
|
117
|
-
].get("show_all_applications"),
|
118
|
-
"sidebar_show_search": get_config(self.settings_name)["SIDEBAR"].get(
|
119
|
-
"show_search"
|
120
|
-
),
|
121
|
-
"sidebar_navigation": self.get_sidebar_list(request)
|
122
|
-
if self.has_permission(request)
|
123
|
-
else [],
|
124
|
-
}
|
125
|
-
)
|
126
|
-
|
127
|
-
environment = get_config(self.settings_name)["ENVIRONMENT"]
|
68
|
+
sidebar_config = self._get_config("SIDEBAR", request)
|
69
|
+
data = {
|
70
|
+
"form_classes": {
|
71
|
+
"text_input": INPUT_CLASSES,
|
72
|
+
"checkbox": CHECKBOX_CLASSES,
|
73
|
+
},
|
74
|
+
"site_title": self._get_config("SITE_TITLE", request),
|
75
|
+
"site_header": self._get_config("SITE_HEADER", request),
|
76
|
+
"site_subheader": self._get_config("SITE_SUBHEADER", request),
|
77
|
+
"site_dropdown": self._get_site_dropdown_items("SITE_DROPDOWN", request),
|
78
|
+
"site_url": self._get_config("SITE_URL", request),
|
79
|
+
"site_logo": self._get_theme_images("SITE_LOGO", request),
|
80
|
+
"site_icon": self._get_theme_images("SITE_ICON", request),
|
81
|
+
"site_symbol": self._get_config("SITE_SYMBOL", request),
|
82
|
+
"site_favicons": self._get_favicons("SITE_FAVICONS", request),
|
83
|
+
"show_history": self._get_config("SHOW_HISTORY", request),
|
84
|
+
"show_view_on_site": self._get_config("SHOW_VIEW_ON_SITE", request),
|
85
|
+
"show_languages": self._get_config("SHOW_LANGUAGES", request),
|
86
|
+
"show_back_button": self._get_config("SHOW_BACK_BUTTON", request),
|
87
|
+
"theme": self._get_config("THEME", request),
|
88
|
+
"border_radius": self._get_config("BORDER_RADIUS", request),
|
89
|
+
"colors": self._get_colors("COLORS", request),
|
90
|
+
"environment": self._get_config("ENVIRONMENT", request),
|
91
|
+
"tab_list": self.get_tabs_list(request),
|
92
|
+
"styles": self._get_list("STYLES", request),
|
93
|
+
"scripts": self._get_list("SCRIPTS", request),
|
94
|
+
"sidebar_show_all_applications": self._get_value(
|
95
|
+
sidebar_config.get("show_all_applications"), request
|
96
|
+
),
|
97
|
+
"sidebar_show_search": self._get_value(
|
98
|
+
sidebar_config.get("show_search"), request
|
99
|
+
),
|
100
|
+
"sidebar_navigation": self.get_sidebar_list(request)
|
101
|
+
if self.has_permission(request)
|
102
|
+
else [],
|
103
|
+
}
|
128
104
|
|
129
|
-
|
130
|
-
try:
|
131
|
-
callback = import_string(environment)
|
132
|
-
context.update({"environment": callback(request)})
|
133
|
-
except ImportError:
|
134
|
-
pass
|
105
|
+
context.update(data)
|
135
106
|
|
136
107
|
if hasattr(self, "extra_context") and callable(self.extra_context):
|
137
108
|
return self.extra_context(context, request)
|
@@ -139,7 +110,7 @@ class UnfoldAdminSite(AdminSite):
|
|
139
110
|
return context
|
140
111
|
|
141
112
|
def index(
|
142
|
-
self, request: HttpRequest, extra_context: Optional[
|
113
|
+
self, request: HttpRequest, extra_context: Optional[dict[str, Any]] = None
|
143
114
|
) -> TemplateResponse:
|
144
115
|
app_list = self.get_app_list(request)
|
145
116
|
|
@@ -164,7 +135,7 @@ class UnfoldAdminSite(AdminSite):
|
|
164
135
|
)
|
165
136
|
|
166
137
|
def toggle_sidebar(
|
167
|
-
self, request: HttpRequest, extra_context: Optional[
|
138
|
+
self, request: HttpRequest, extra_context: Optional[dict[str, Any]] = None
|
168
139
|
) -> HttpResponse:
|
169
140
|
if "toggle_sidebar" not in request.session:
|
170
141
|
request.session["toggle_sidebar"] = True
|
@@ -174,7 +145,7 @@ class UnfoldAdminSite(AdminSite):
|
|
174
145
|
return HttpResponse(status=HTTPStatus.OK)
|
175
146
|
|
176
147
|
def search(
|
177
|
-
self, request: HttpRequest, extra_context: Optional[
|
148
|
+
self, request: HttpRequest, extra_context: Optional[dict[str, Any]] = None
|
178
149
|
) -> TemplateResponse:
|
179
150
|
query = request.GET.get("s").lower()
|
180
151
|
app_list = super().get_app_list(request)
|
@@ -209,7 +180,7 @@ class UnfoldAdminSite(AdminSite):
|
|
209
180
|
@method_decorator(never_cache)
|
210
181
|
@login_not_required
|
211
182
|
def login(
|
212
|
-
self, request: HttpRequest, extra_context: Optional[
|
183
|
+
self, request: HttpRequest, extra_context: Optional[dict[str, Any]] = None
|
213
184
|
) -> HttpResponse:
|
214
185
|
extra_context = {} if extra_context is None else extra_context
|
215
186
|
image = self._get_value(
|
@@ -233,7 +204,7 @@ class UnfoldAdminSite(AdminSite):
|
|
233
204
|
return super().login(request, extra_context)
|
234
205
|
|
235
206
|
def password_change(
|
236
|
-
self, request: HttpRequest, extra_context: Optional[
|
207
|
+
self, request: HttpRequest, extra_context: Optional[dict[str, Any]] = None
|
237
208
|
) -> HttpResponse:
|
238
209
|
from django.contrib.auth.views import PasswordChangeView
|
239
210
|
|
@@ -250,9 +221,11 @@ class UnfoldAdminSite(AdminSite):
|
|
250
221
|
request.current_app = self.name
|
251
222
|
return PasswordChangeView.as_view(**defaults)(request)
|
252
223
|
|
253
|
-
def get_sidebar_list(self, request: HttpRequest) ->
|
254
|
-
navigation =
|
255
|
-
|
224
|
+
def get_sidebar_list(self, request: HttpRequest) -> list[dict[str, Any]]:
|
225
|
+
navigation = self._get_value(
|
226
|
+
self._get_config("SIDEBAR", request).get("navigation"), request
|
227
|
+
)
|
228
|
+
tabs = self._get_value(self._get_config("TABS", request), request) or []
|
256
229
|
results = []
|
257
230
|
|
258
231
|
for group in navigation:
|
@@ -307,8 +280,11 @@ class UnfoldAdminSite(AdminSite):
|
|
307
280
|
|
308
281
|
return results
|
309
282
|
|
310
|
-
def get_tabs_list(self, request: HttpRequest) ->
|
311
|
-
tabs =
|
283
|
+
def get_tabs_list(self, request: HttpRequest) -> list[dict[str, Any]]:
|
284
|
+
tabs = copy.deepcopy(self._get_config("TABS", request))
|
285
|
+
|
286
|
+
if not tabs:
|
287
|
+
return []
|
312
288
|
|
313
289
|
for tab in tabs:
|
314
290
|
allowed_items = []
|
@@ -321,29 +297,19 @@ class UnfoldAdminSite(AdminSite):
|
|
321
297
|
if isinstance(item["link"], Callable):
|
322
298
|
item["link_callback"] = lazy(item["link"])(request)
|
323
299
|
|
324
|
-
|
325
|
-
|
326
|
-
|
300
|
+
if "active" not in item:
|
301
|
+
item["active"] = self._get_is_active(
|
302
|
+
request, item.get("link_callback") or item["link"], True
|
303
|
+
)
|
304
|
+
else:
|
305
|
+
item["active"] = self._get_value(item["active"], request)
|
306
|
+
|
327
307
|
allowed_items.append(item)
|
328
308
|
|
329
309
|
tab["items"] = allowed_items
|
330
310
|
|
331
311
|
return tabs
|
332
312
|
|
333
|
-
def _get_mode_images(
|
334
|
-
self, images: Union[Dict[str, callable], callable, str], request: HttpRequest
|
335
|
-
) -> Union[Dict[str, str], str, None]:
|
336
|
-
if isinstance(images, dict):
|
337
|
-
if "light" in images and "dark" in images:
|
338
|
-
return {
|
339
|
-
"light": self._get_value(images["light"], request),
|
340
|
-
"dark": self._get_value(images["dark"], request),
|
341
|
-
}
|
342
|
-
|
343
|
-
return None
|
344
|
-
|
345
|
-
return self._get_value(images, request)
|
346
|
-
|
347
313
|
def _call_permission_callback(
|
348
314
|
self, callback: Union[str, Callable, None], request: HttpRequest
|
349
315
|
) -> bool:
|
@@ -363,21 +329,7 @@ class UnfoldAdminSite(AdminSite):
|
|
363
329
|
|
364
330
|
return False
|
365
331
|
|
366
|
-
def
|
367
|
-
self, instance: Union[str, Callable, None], *args: Any
|
368
|
-
) -> Optional[str]:
|
369
|
-
if instance is None:
|
370
|
-
return None
|
371
|
-
|
372
|
-
if isinstance(instance, str):
|
373
|
-
return instance
|
374
|
-
|
375
|
-
if isinstance(instance, Callable):
|
376
|
-
return instance(*args)
|
377
|
-
|
378
|
-
return None
|
379
|
-
|
380
|
-
def _replace_values(self, target: Dict, source: Dict, request: HttpRequest):
|
332
|
+
def _replace_values(self, target: dict, source: dict, request: HttpRequest):
|
381
333
|
for key in source.keys():
|
382
334
|
if source[key] is not None and callable(source[key]):
|
383
335
|
target[key] = source[key](request)
|
@@ -386,31 +338,6 @@ class UnfoldAdminSite(AdminSite):
|
|
386
338
|
|
387
339
|
return target
|
388
340
|
|
389
|
-
def _process_favicons(
|
390
|
-
self, request: HttpRequest, favicons: List[Dict]
|
391
|
-
) -> List[Favicon]:
|
392
|
-
return [
|
393
|
-
Favicon(
|
394
|
-
href=self._get_value(item["href"], request),
|
395
|
-
rel=item.get("rel"),
|
396
|
-
sizes=item.get("sizes"),
|
397
|
-
type=item.get("type"),
|
398
|
-
)
|
399
|
-
for item in favicons
|
400
|
-
]
|
401
|
-
|
402
|
-
def _process_colors(
|
403
|
-
self, colors: Dict[str, Dict[str, str]]
|
404
|
-
) -> Dict[str, Dict[str, str]]:
|
405
|
-
for name, weights in colors.items():
|
406
|
-
for weight, value in weights.items():
|
407
|
-
if value[0] != "#":
|
408
|
-
continue
|
409
|
-
|
410
|
-
colors[name][weight] = " ".join(str(item) for item in hex_to_rgb(value))
|
411
|
-
|
412
|
-
return colors
|
413
|
-
|
414
341
|
def _get_is_active(
|
415
342
|
self, request: HttpRequest, link: str, is_tab: bool = False
|
416
343
|
) -> bool:
|
@@ -424,7 +351,7 @@ class UnfoldAdminSite(AdminSite):
|
|
424
351
|
if link_path == request.path == index_path:
|
425
352
|
return True
|
426
353
|
|
427
|
-
if link_path in request.path and link_path != index_path:
|
354
|
+
if link_path != "" and link_path in request.path and link_path != index_path:
|
428
355
|
query_params = parse_qs(urlparse(link).query)
|
429
356
|
request_params = parse_qs(request.GET.urlencode())
|
430
357
|
|
@@ -437,3 +364,112 @@ class UnfoldAdminSite(AdminSite):
|
|
437
364
|
return True
|
438
365
|
|
439
366
|
return False
|
367
|
+
|
368
|
+
def _get_config(self, key: str, *args) -> Any:
|
369
|
+
config = get_config(self.settings_name)
|
370
|
+
|
371
|
+
if key in config and config[key]:
|
372
|
+
return self._get_value(config[key], *args)
|
373
|
+
|
374
|
+
def _get_theme_images(
|
375
|
+
self, key: str, *args: Any
|
376
|
+
) -> Union[dict[str, str], str, None]:
|
377
|
+
images = self._get_config(key, *args)
|
378
|
+
|
379
|
+
if isinstance(images, dict):
|
380
|
+
if "light" in images and "dark" in images:
|
381
|
+
return {
|
382
|
+
"light": self._get_value(images["light"], *args),
|
383
|
+
"dark": self._get_value(images["dark"], *args),
|
384
|
+
}
|
385
|
+
|
386
|
+
return None
|
387
|
+
|
388
|
+
return images
|
389
|
+
|
390
|
+
def _get_colors(self, key: str, *args) -> dict[str, dict[str, str]]:
|
391
|
+
colors = self._get_config(key, *args)
|
392
|
+
|
393
|
+
def rgb_to_values(value: str) -> str:
|
394
|
+
return " ".join(
|
395
|
+
list(
|
396
|
+
map(
|
397
|
+
str.strip,
|
398
|
+
value.removeprefix("rgb(").removesuffix(")").split(","),
|
399
|
+
)
|
400
|
+
)
|
401
|
+
)
|
402
|
+
|
403
|
+
def hex_to_values(value: str) -> str:
|
404
|
+
return " ".join(str(item) for item in hex_to_rgb(value))
|
405
|
+
|
406
|
+
for name, weights in colors.items():
|
407
|
+
weights = self._get_value(weights, *args)
|
408
|
+
colors[name] = weights
|
409
|
+
|
410
|
+
for weight, value in weights.items():
|
411
|
+
if value[0] == "#":
|
412
|
+
colors[name][weight] = hex_to_values(value)
|
413
|
+
elif value.startswith("rgb"):
|
414
|
+
colors[name][weight] = rgb_to_values(value)
|
415
|
+
|
416
|
+
return colors
|
417
|
+
|
418
|
+
def _get_list(self, key: str, *args) -> list[Any]:
|
419
|
+
items = get_config(self.settings_name)[key]
|
420
|
+
|
421
|
+
if isinstance(items, list):
|
422
|
+
return [self._get_value(item, *args) for item in items]
|
423
|
+
|
424
|
+
return []
|
425
|
+
|
426
|
+
def _get_favicons(self, key: str, *args) -> list[Favicon]:
|
427
|
+
favicons = self._get_config(key, *args)
|
428
|
+
|
429
|
+
if not favicons:
|
430
|
+
return []
|
431
|
+
|
432
|
+
return [
|
433
|
+
Favicon(
|
434
|
+
href=self._get_value(item["href"], *args),
|
435
|
+
rel=item.get("rel"),
|
436
|
+
sizes=item.get("sizes"),
|
437
|
+
type=item.get("type"),
|
438
|
+
)
|
439
|
+
for item in favicons
|
440
|
+
]
|
441
|
+
|
442
|
+
def _get_site_dropdown_items(self, key: str, *args) -> list[dict[str, Any]]:
|
443
|
+
items = self._get_config(key, *args)
|
444
|
+
|
445
|
+
if not items:
|
446
|
+
return []
|
447
|
+
|
448
|
+
return [
|
449
|
+
DropdownItem(
|
450
|
+
title=item.get("title"),
|
451
|
+
link=self._get_value(item["link"], *args),
|
452
|
+
icon=item.get("icon"),
|
453
|
+
)
|
454
|
+
for item in items
|
455
|
+
]
|
456
|
+
|
457
|
+
def _get_value(
|
458
|
+
self, value: Union[str, Callable, lazy, None], *args: Any
|
459
|
+
) -> Optional[str]:
|
460
|
+
if value is None:
|
461
|
+
return None
|
462
|
+
|
463
|
+
if isinstance(value, str):
|
464
|
+
try:
|
465
|
+
callback = import_string(value)
|
466
|
+
return callback(*args)
|
467
|
+
except ImportError:
|
468
|
+
pass
|
469
|
+
|
470
|
+
return value
|
471
|
+
|
472
|
+
if isinstance(value, Callable):
|
473
|
+
return value(*args)
|
474
|
+
|
475
|
+
return value
|