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.
Files changed (38) hide show
  1. {django_unfold-0.46.0.dist-info → django_unfold-0.47.0.dist-info}/METADATA +5 -6
  2. {django_unfold-0.46.0.dist-info → django_unfold-0.47.0.dist-info}/RECORD +38 -31
  3. {django_unfold-0.46.0.dist-info → django_unfold-0.47.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 +2 -2
  21. unfold/decorators.py +4 -3
  22. unfold/settings.py +2 -2
  23. unfold/sites.py +156 -140
  24. unfold/static/unfold/css/styles.css +1 -1
  25. unfold/static/unfold/js/app.js +2 -2
  26. unfold/templates/admin/filter.html +1 -1
  27. unfold/templates/unfold/helpers/change_list_filter.html +2 -2
  28. unfold/templates/unfold/helpers/change_list_filter_actions.html +1 -1
  29. unfold/templates/unfold/helpers/header_back_button.html +2 -2
  30. unfold/templates/unfold/helpers/tab_list.html +7 -1
  31. unfold/templates/unfold/layouts/skeleton.html +1 -1
  32. unfold/templatetags/unfold.py +55 -22
  33. unfold/templatetags/unfold_list.py +2 -2
  34. unfold/typing.py +5 -4
  35. unfold/utils.py +3 -2
  36. unfold/views.py +2 -2
  37. unfold/widgets.py +27 -27
  38. {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, Dict, List, Optional, Union
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
- 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]:
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) -> Dict[str, Any]:
64
+ def each_context(self, request: HttpRequest) -> dict[str, Any]:
73
65
  context = super().each_context(request)
74
66
 
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"]
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
- if environment and isinstance(environment, str):
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[Dict[str, Any]] = None
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[Dict[str, Any]] = None
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[Dict[str, Any]] = None
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[Dict[str, Any]] = None
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[Dict[str, Any]] = None
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) -> List[Dict[str, Any]]:
254
- navigation = get_config(self.settings_name)["SIDEBAR"].get("navigation", [])
255
- tabs = get_config(self.settings_name)["TABS"]
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) -> List[Dict[str, Any]]:
311
- tabs = get_config(self.settings_name)["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
- item["active"] = self._get_is_active(
325
- request, item.get("link_callback") or item["link"], True
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 _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):
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