django-unfold 0.46.0__py3-none-any.whl → 0.48.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.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
|