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.
Files changed (52) hide show
  1. {django_unfold-0.46.0.dist-info → django_unfold-0.48.0.dist-info}/METADATA +5 -6
  2. {django_unfold-0.46.0.dist-info → django_unfold-0.48.0.dist-info}/RECORD +52 -43
  3. {django_unfold-0.46.0.dist-info → django_unfold-0.48.0.dist-info}/WHEEL +1 -1
  4. unfold/admin.py +15 -16
  5. unfold/checks.py +4 -4
  6. unfold/components.py +5 -5
  7. unfold/contrib/filters/admin/__init__.py +43 -0
  8. unfold/contrib/filters/admin/autocomplete_filters.py +16 -0
  9. unfold/contrib/filters/admin/datetime_filters.py +212 -0
  10. unfold/contrib/filters/admin/dropdown_filters.py +100 -0
  11. unfold/contrib/filters/admin/mixins.py +146 -0
  12. unfold/contrib/filters/admin/numeric_filters.py +196 -0
  13. unfold/contrib/filters/admin/text_filters.py +65 -0
  14. unfold/contrib/filters/admin.py +32 -32
  15. unfold/contrib/filters/forms.py +68 -17
  16. unfold/contrib/forms/widgets.py +9 -9
  17. unfold/contrib/inlines/checks.py +2 -4
  18. unfold/contrib/simple_history/templates/simple_history/object_history.html +17 -1
  19. unfold/contrib/simple_history/templates/simple_history/object_history_list.html +1 -1
  20. unfold/dataclasses.py +9 -2
  21. unfold/decorators.py +4 -3
  22. unfold/settings.py +4 -2
  23. unfold/sites.py +176 -140
  24. unfold/static/unfold/css/styles.css +1 -1
  25. unfold/static/unfold/js/app.js +2 -2
  26. unfold/templates/admin/app_index.html +1 -5
  27. unfold/templates/admin/base_site.html +1 -1
  28. unfold/templates/admin/filter.html +1 -1
  29. unfold/templates/admin/index.html +1 -5
  30. unfold/templates/admin/login.html +1 -1
  31. unfold/templates/admin/search_form.html +4 -2
  32. unfold/templates/unfold/helpers/account_links.html +1 -1
  33. unfold/templates/unfold/helpers/actions_row.html +1 -1
  34. unfold/templates/unfold/helpers/change_list_filter.html +2 -2
  35. unfold/templates/unfold/helpers/change_list_filter_actions.html +1 -1
  36. unfold/templates/unfold/helpers/header_back_button.html +2 -2
  37. unfold/templates/unfold/helpers/language_switch.html +1 -1
  38. unfold/templates/unfold/helpers/navigation_header.html +15 -5
  39. unfold/templates/unfold/helpers/site_branding.html +9 -0
  40. unfold/templates/unfold/helpers/site_dropdown.html +19 -0
  41. unfold/templates/unfold/helpers/site_icon.html +10 -2
  42. unfold/templates/unfold/helpers/tab_list.html +7 -1
  43. unfold/templates/unfold/helpers/theme_switch.html +1 -1
  44. unfold/templates/unfold/layouts/base.html +1 -5
  45. unfold/templates/unfold/layouts/skeleton.html +1 -1
  46. unfold/templatetags/unfold.py +55 -22
  47. unfold/templatetags/unfold_list.py +2 -2
  48. unfold/typing.py +5 -4
  49. unfold/utils.py +3 -2
  50. unfold/views.py +2 -2
  51. unfold/widgets.py +27 -27
  52. {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, Dict, List, Optional, Union
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
- if get_config(self.settings_name)["SITE_TITLE"]:
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) -> Dict[str, Any]:
65
+ def each_context(self, request: HttpRequest) -> dict[str, Any]:
73
66
  context = super().each_context(request)
74
67
 
75
- context.update(
76
- {
77
- "form_classes": {
78
- "text_input": INPUT_CLASSES,
79
- "checkbox": CHECKBOX_CLASSES,
80
- },
81
- "site_logo": self._get_mode_images(
82
- get_config(self.settings_name)["SITE_LOGO"], request
83
- ),
84
- "site_icon": self._get_mode_images(
85
- get_config(self.settings_name)["SITE_ICON"], request
86
- ),
87
- "site_symbol": self._get_value(
88
- get_config(self.settings_name)["SITE_SYMBOL"], request
89
- ),
90
- "site_favicons": self._process_favicons(
91
- request, get_config(self.settings_name)["SITE_FAVICONS"]
92
- ),
93
- "show_history": get_config(self.settings_name)["SHOW_HISTORY"],
94
- "show_view_on_site": get_config(self.settings_name)[
95
- "SHOW_VIEW_ON_SITE"
96
- ],
97
- "show_languages": get_config(self.settings_name)["SHOW_LANGUAGES"],
98
- "show_back_button": get_config(self.settings_name)["SHOW_BACK_BUTTON"],
99
- "colors": self._process_colors(
100
- get_config(self.settings_name)["COLORS"]
101
- ),
102
- "border_radius": get_config(self.settings_name).get(
103
- "BORDER_RADIUS", "6px"
104
- ),
105
- "tab_list": self.get_tabs_list(request),
106
- "styles": [
107
- self._get_value(style, request)
108
- for style in get_config(self.settings_name)["STYLES"]
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"]
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
- if environment and isinstance(environment, str):
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[Dict[str, Any]] = None
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[Dict[str, Any]] = None
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[Dict[str, Any]] = None
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[Dict[str, Any]] = None
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[Dict[str, Any]] = None
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) -> List[Dict[str, Any]]:
254
- navigation = get_config(self.settings_name)["SIDEBAR"].get("navigation", [])
255
- tabs = get_config(self.settings_name)["TABS"]
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) -> List[Dict[str, Any]]:
311
- tabs = get_config(self.settings_name)["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
- item["active"] = self._get_is_active(
325
- request, item.get("link_callback") or item["link"], True
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 _get_value(
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