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.
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