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