django-unfold 0.46.0__py3-none-any.whl → 0.47.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.47.0.dist-info}/METADATA +5 -6
- {django_unfold-0.46.0.dist-info → django_unfold-0.47.0.dist-info}/RECORD +38 -31
- {django_unfold-0.46.0.dist-info → django_unfold-0.47.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 +2 -2
- unfold/decorators.py +4 -3
- unfold/settings.py +2 -2
- unfold/sites.py +156 -140
- unfold/static/unfold/css/styles.css +1 -1
- unfold/static/unfold/js/app.js +2 -2
- unfold/templates/admin/filter.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/tab_list.html +7 -1
- 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.47.0.dist-info}/LICENSE.md +0 -0
unfold/sites.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
from http import HTTPStatus
|
2
|
-
from typing import Any, Callable,
|
2
|
+
from typing import Any, Callable, Optional, Union
|
3
3
|
from urllib.parse import parse_qs, urlparse
|
4
4
|
|
5
5
|
from django.contrib.admin import AdminSite
|
@@ -13,6 +13,8 @@ from django.utils.functional import lazy
|
|
13
13
|
from django.utils.module_loading import import_string
|
14
14
|
from django.views.decorators.cache import never_cache
|
15
15
|
|
16
|
+
from unfold.dataclasses import Favicon
|
17
|
+
|
16
18
|
try:
|
17
19
|
from django.contrib.auth.decorators import login_not_required
|
18
20
|
except ImportError:
|
@@ -21,7 +23,6 @@ except ImportError:
|
|
21
23
|
return func
|
22
24
|
|
23
25
|
|
24
|
-
from .dataclasses import Favicon
|
25
26
|
from .settings import get_config
|
26
27
|
from .utils import hex_to_rgb
|
27
28
|
from .widgets import CHECKBOX_CLASSES, INPUT_CLASSES
|
@@ -39,16 +40,7 @@ class UnfoldAdminSite(AdminSite):
|
|
39
40
|
if self.login_form is None:
|
40
41
|
self.login_form = AuthenticationForm
|
41
42
|
|
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]:
|
43
|
+
def get_urls(self) -> list[URLPattern]:
|
52
44
|
extra_urls = []
|
53
45
|
|
54
46
|
if hasattr(self, "extra_urls") and callable(self.extra_urls):
|
@@ -69,69 +61,45 @@ class UnfoldAdminSite(AdminSite):
|
|
69
61
|
|
70
62
|
return urlpatterns
|
71
63
|
|
72
|
-
def each_context(self, request: HttpRequest) ->
|
64
|
+
def each_context(self, request: HttpRequest) -> dict[str, Any]:
|
73
65
|
context = super().each_context(request)
|
74
66
|
|
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
|
-
"theme": get_config(self.settings_name).get("THEME"),
|
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"]
|
67
|
+
sidebar_config = self._get_config("SIDEBAR", request)
|
68
|
+
data = {
|
69
|
+
"form_classes": {
|
70
|
+
"text_input": INPUT_CLASSES,
|
71
|
+
"checkbox": CHECKBOX_CLASSES,
|
72
|
+
},
|
73
|
+
"site_title": self._get_config("SITE_TITLE", request),
|
74
|
+
"site_header": self._get_config("SITE_HEADER", request),
|
75
|
+
"site_url": self._get_config("SITE_URL", request),
|
76
|
+
"site_logo": self._get_theme_images("SITE_LOGO", request),
|
77
|
+
"site_icon": self._get_theme_images("SITE_ICON", request),
|
78
|
+
"site_symbol": self._get_config("SITE_SYMBOL", request),
|
79
|
+
"site_favicons": self._get_favicons("SITE_FAVICONS", request),
|
80
|
+
"show_history": self._get_config("SHOW_HISTORY", request),
|
81
|
+
"show_view_on_site": self._get_config("SHOW_VIEW_ON_SITE", request),
|
82
|
+
"show_languages": self._get_config("SHOW_LANGUAGES", request),
|
83
|
+
"show_back_button": self._get_config("SHOW_BACK_BUTTON", request),
|
84
|
+
"theme": self._get_config("THEME", request),
|
85
|
+
"border_radius": self._get_config("BORDER_RADIUS", request),
|
86
|
+
"colors": self._get_colors("COLORS", request),
|
87
|
+
"environment": self._get_config("ENVIRONMENT", request),
|
88
|
+
"tab_list": self.get_tabs_list(request),
|
89
|
+
"styles": self._get_list("STYLES", request),
|
90
|
+
"scripts": self._get_list("SCRIPTS", request),
|
91
|
+
"sidebar_show_all_applications": self._get_value(
|
92
|
+
sidebar_config.get("show_all_applications"), request
|
93
|
+
),
|
94
|
+
"sidebar_show_search": self._get_value(
|
95
|
+
sidebar_config.get("show_search"), request
|
96
|
+
),
|
97
|
+
"sidebar_navigation": self.get_sidebar_list(request)
|
98
|
+
if self.has_permission(request)
|
99
|
+
else [],
|
100
|
+
}
|
128
101
|
|
129
|
-
|
130
|
-
try:
|
131
|
-
callback = import_string(environment)
|
132
|
-
context.update({"environment": callback(request)})
|
133
|
-
except ImportError:
|
134
|
-
pass
|
102
|
+
context.update(data)
|
135
103
|
|
136
104
|
if hasattr(self, "extra_context") and callable(self.extra_context):
|
137
105
|
return self.extra_context(context, request)
|
@@ -139,7 +107,7 @@ class UnfoldAdminSite(AdminSite):
|
|
139
107
|
return context
|
140
108
|
|
141
109
|
def index(
|
142
|
-
self, request: HttpRequest, extra_context: Optional[
|
110
|
+
self, request: HttpRequest, extra_context: Optional[dict[str, Any]] = None
|
143
111
|
) -> TemplateResponse:
|
144
112
|
app_list = self.get_app_list(request)
|
145
113
|
|
@@ -164,7 +132,7 @@ class UnfoldAdminSite(AdminSite):
|
|
164
132
|
)
|
165
133
|
|
166
134
|
def toggle_sidebar(
|
167
|
-
self, request: HttpRequest, extra_context: Optional[
|
135
|
+
self, request: HttpRequest, extra_context: Optional[dict[str, Any]] = None
|
168
136
|
) -> HttpResponse:
|
169
137
|
if "toggle_sidebar" not in request.session:
|
170
138
|
request.session["toggle_sidebar"] = True
|
@@ -174,7 +142,7 @@ class UnfoldAdminSite(AdminSite):
|
|
174
142
|
return HttpResponse(status=HTTPStatus.OK)
|
175
143
|
|
176
144
|
def search(
|
177
|
-
self, request: HttpRequest, extra_context: Optional[
|
145
|
+
self, request: HttpRequest, extra_context: Optional[dict[str, Any]] = None
|
178
146
|
) -> TemplateResponse:
|
179
147
|
query = request.GET.get("s").lower()
|
180
148
|
app_list = super().get_app_list(request)
|
@@ -209,7 +177,7 @@ class UnfoldAdminSite(AdminSite):
|
|
209
177
|
@method_decorator(never_cache)
|
210
178
|
@login_not_required
|
211
179
|
def login(
|
212
|
-
self, request: HttpRequest, extra_context: Optional[
|
180
|
+
self, request: HttpRequest, extra_context: Optional[dict[str, Any]] = None
|
213
181
|
) -> HttpResponse:
|
214
182
|
extra_context = {} if extra_context is None else extra_context
|
215
183
|
image = self._get_value(
|
@@ -233,7 +201,7 @@ class UnfoldAdminSite(AdminSite):
|
|
233
201
|
return super().login(request, extra_context)
|
234
202
|
|
235
203
|
def password_change(
|
236
|
-
self, request: HttpRequest, extra_context: Optional[
|
204
|
+
self, request: HttpRequest, extra_context: Optional[dict[str, Any]] = None
|
237
205
|
) -> HttpResponse:
|
238
206
|
from django.contrib.auth.views import PasswordChangeView
|
239
207
|
|
@@ -250,9 +218,11 @@ class UnfoldAdminSite(AdminSite):
|
|
250
218
|
request.current_app = self.name
|
251
219
|
return PasswordChangeView.as_view(**defaults)(request)
|
252
220
|
|
253
|
-
def get_sidebar_list(self, request: HttpRequest) ->
|
254
|
-
navigation =
|
255
|
-
|
221
|
+
def get_sidebar_list(self, request: HttpRequest) -> list[dict[str, Any]]:
|
222
|
+
navigation = self._get_value(
|
223
|
+
self._get_config("SIDEBAR", request).get("navigation"), request
|
224
|
+
)
|
225
|
+
tabs = self._get_value(self._get_config("TABS", request), request) or []
|
256
226
|
results = []
|
257
227
|
|
258
228
|
for group in navigation:
|
@@ -307,8 +277,11 @@ class UnfoldAdminSite(AdminSite):
|
|
307
277
|
|
308
278
|
return results
|
309
279
|
|
310
|
-
def get_tabs_list(self, request: HttpRequest) ->
|
311
|
-
tabs =
|
280
|
+
def get_tabs_list(self, request: HttpRequest) -> list[dict[str, Any]]:
|
281
|
+
tabs = self._get_config("TABS", request)
|
282
|
+
|
283
|
+
if not tabs:
|
284
|
+
return []
|
312
285
|
|
313
286
|
for tab in tabs:
|
314
287
|
allowed_items = []
|
@@ -321,29 +294,17 @@ class UnfoldAdminSite(AdminSite):
|
|
321
294
|
if isinstance(item["link"], Callable):
|
322
295
|
item["link_callback"] = lazy(item["link"])(request)
|
323
296
|
|
324
|
-
|
325
|
-
|
326
|
-
|
297
|
+
if "active" not in item:
|
298
|
+
item["active"] = self._get_is_active(
|
299
|
+
request, item.get("link_callback") or item["link"], True
|
300
|
+
)
|
301
|
+
|
327
302
|
allowed_items.append(item)
|
328
303
|
|
329
304
|
tab["items"] = allowed_items
|
330
305
|
|
331
306
|
return tabs
|
332
307
|
|
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
308
|
def _call_permission_callback(
|
348
309
|
self, callback: Union[str, Callable, None], request: HttpRequest
|
349
310
|
) -> bool:
|
@@ -363,21 +324,7 @@ class UnfoldAdminSite(AdminSite):
|
|
363
324
|
|
364
325
|
return False
|
365
326
|
|
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):
|
327
|
+
def _replace_values(self, target: dict, source: dict, request: HttpRequest):
|
381
328
|
for key in source.keys():
|
382
329
|
if source[key] is not None and callable(source[key]):
|
383
330
|
target[key] = source[key](request)
|
@@ -386,31 +333,6 @@ class UnfoldAdminSite(AdminSite):
|
|
386
333
|
|
387
334
|
return target
|
388
335
|
|
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
336
|
def _get_is_active(
|
415
337
|
self, request: HttpRequest, link: str, is_tab: bool = False
|
416
338
|
) -> bool:
|
@@ -424,7 +346,7 @@ class UnfoldAdminSite(AdminSite):
|
|
424
346
|
if link_path == request.path == index_path:
|
425
347
|
return True
|
426
348
|
|
427
|
-
if link_path in request.path and link_path != index_path:
|
349
|
+
if link_path != "" and link_path in request.path and link_path != index_path:
|
428
350
|
query_params = parse_qs(urlparse(link).query)
|
429
351
|
request_params = parse_qs(request.GET.urlencode())
|
430
352
|
|
@@ -437,3 +359,97 @@ class UnfoldAdminSite(AdminSite):
|
|
437
359
|
return True
|
438
360
|
|
439
361
|
return False
|
362
|
+
|
363
|
+
def _get_config(self, key: str, *args) -> Any:
|
364
|
+
config = get_config(self.settings_name)
|
365
|
+
|
366
|
+
if key in config and config[key]:
|
367
|
+
return self._get_value(config[key], *args)
|
368
|
+
|
369
|
+
def _get_theme_images(
|
370
|
+
self, key: str, *args: Any
|
371
|
+
) -> Union[dict[str, str], str, None]:
|
372
|
+
images = self._get_config(key, *args)
|
373
|
+
|
374
|
+
if isinstance(images, dict):
|
375
|
+
if "light" in images and "dark" in images:
|
376
|
+
return {
|
377
|
+
"light": self._get_value(images["light"], *args),
|
378
|
+
"dark": self._get_value(images["dark"], *args),
|
379
|
+
}
|
380
|
+
|
381
|
+
return None
|
382
|
+
|
383
|
+
return images
|
384
|
+
|
385
|
+
def _get_colors(self, key: str, *args) -> dict[str, dict[str, str]]:
|
386
|
+
colors = self._get_config(key, *args)
|
387
|
+
|
388
|
+
def rgb_to_values(value: str) -> str:
|
389
|
+
return " ".join(
|
390
|
+
list(
|
391
|
+
map(
|
392
|
+
str.strip,
|
393
|
+
value.removeprefix("rgb(").removesuffix(")").split(","),
|
394
|
+
)
|
395
|
+
)
|
396
|
+
)
|
397
|
+
|
398
|
+
def hex_to_values(value: str) -> str:
|
399
|
+
return " ".join(str(item) for item in hex_to_rgb(value))
|
400
|
+
|
401
|
+
for name, weights in colors.items():
|
402
|
+
weights = self._get_value(weights, *args)
|
403
|
+
colors[name] = weights
|
404
|
+
|
405
|
+
for weight, value in weights.items():
|
406
|
+
if value[0] == "#":
|
407
|
+
colors[name][weight] = hex_to_values(value)
|
408
|
+
elif value.startswith("rgb"):
|
409
|
+
colors[name][weight] = rgb_to_values(value)
|
410
|
+
|
411
|
+
return colors
|
412
|
+
|
413
|
+
def _get_list(self, key: str, *args) -> list[Any]:
|
414
|
+
items = get_config(self.settings_name)[key]
|
415
|
+
|
416
|
+
if isinstance(items, list):
|
417
|
+
return [self._get_value(item, *args) for item in items]
|
418
|
+
|
419
|
+
return []
|
420
|
+
|
421
|
+
def _get_favicons(self, key: str, *args) -> list[Favicon]:
|
422
|
+
favicons = self._get_config(key, *args)
|
423
|
+
|
424
|
+
if not favicons:
|
425
|
+
return []
|
426
|
+
|
427
|
+
return [
|
428
|
+
Favicon(
|
429
|
+
href=self._get_value(item["href"], *args),
|
430
|
+
rel=item.get("rel"),
|
431
|
+
sizes=item.get("sizes"),
|
432
|
+
type=item.get("type"),
|
433
|
+
)
|
434
|
+
for item in favicons
|
435
|
+
]
|
436
|
+
|
437
|
+
def _get_value(
|
438
|
+
self, value: Union[str, Callable, lazy, None], *args: Any
|
439
|
+
) -> Optional[str]:
|
440
|
+
if value is None:
|
441
|
+
return None
|
442
|
+
|
443
|
+
if isinstance(value, str):
|
444
|
+
try:
|
445
|
+
callback = import_string(value)
|
446
|
+
return callback(*args)
|
447
|
+
except ImportError:
|
448
|
+
pass
|
449
|
+
|
450
|
+
return value
|
451
|
+
|
452
|
+
if isinstance(value, Callable):
|
453
|
+
return value(*args)
|
454
|
+
|
455
|
+
return value
|