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