django-unfold 0.63.0__py3-none-any.whl → 0.64.1__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 (55) hide show
  1. {django_unfold-0.63.0.dist-info → django_unfold-0.64.1.dist-info}/METADATA +1 -1
  2. {django_unfold-0.63.0.dist-info → django_unfold-0.64.1.dist-info}/RECORD +55 -51
  3. unfold/admin.py +1 -1
  4. unfold/contrib/constance/templates/admin/constance/change_list.html +0 -18
  5. unfold/contrib/filters/templates/unfold/filters/filters_numeric_slider.html +1 -1
  6. unfold/contrib/guardian/templates/admin/guardian/model/obj_perms_manage.html +0 -24
  7. unfold/contrib/guardian/templates/admin/guardian/model/obj_perms_manage_group.html +0 -26
  8. unfold/contrib/guardian/templates/admin/guardian/model/obj_perms_manage_user.html +0 -26
  9. unfold/contrib/import_export/templates/admin/import_export/export.html +0 -23
  10. unfold/contrib/import_export/templates/admin/import_export/import.html +0 -23
  11. unfold/contrib/simple_history/templates/simple_history/object_history_form.html +1 -33
  12. unfold/contrib/simple_history/templates/simple_history/object_history_list.html +0 -1
  13. unfold/dataclasses.py +8 -0
  14. unfold/settings.py +28 -22
  15. unfold/sites.py +120 -43
  16. unfold/static/unfold/css/styles.css +1 -1
  17. unfold/static/unfold/js/app.js +147 -9
  18. unfold/styles.css +32 -32
  19. unfold/templates/admin/app_index.html +0 -18
  20. unfold/templates/admin/auth/user/change_password.html +1 -26
  21. unfold/templates/admin/base.html +0 -16
  22. unfold/templates/admin/change_form.html +0 -33
  23. unfold/templates/admin/change_list.html +0 -19
  24. unfold/templates/admin/change_list_results.html +2 -2
  25. unfold/templates/admin/delete_confirmation.html +0 -21
  26. unfold/templates/admin/delete_selected_confirmation.html +0 -22
  27. unfold/templates/admin/index.html +0 -2
  28. unfold/templates/admin/object_history.html +0 -24
  29. unfold/templates/registration/password_change_done.html +0 -17
  30. unfold/templates/registration/password_change_form.html +0 -17
  31. unfold/templates/unfold/components/navigation.html +2 -2
  32. unfold/templates/unfold/components/table.html +55 -9
  33. unfold/templates/unfold/helpers/boolean.html +6 -6
  34. unfold/templates/unfold/helpers/command.html +53 -0
  35. unfold/templates/unfold/helpers/command_history.html +54 -0
  36. unfold/templates/unfold/helpers/command_results.html +50 -0
  37. unfold/templates/unfold/helpers/header_back_button.html +10 -2
  38. unfold/templates/unfold/helpers/header_title.html +11 -0
  39. unfold/templates/unfold/helpers/label.html +1 -1
  40. unfold/templates/unfold/helpers/navigation_header.html +2 -2
  41. unfold/templates/unfold/helpers/search.html +40 -22
  42. unfold/templates/unfold/helpers/search_results.html +2 -2
  43. unfold/templates/unfold/helpers/shortcut.html +1 -1
  44. unfold/templates/unfold/helpers/site_dropdown.html +1 -1
  45. unfold/templates/unfold/helpers/tab_items.html +15 -33
  46. unfold/templates/unfold/helpers/tab_list.html +1 -1
  47. unfold/templates/unfold/helpers/welcomemsg.html +6 -18
  48. unfold/templates/unfold/layouts/base_simple.html +0 -6
  49. unfold/templates/unfold/layouts/skeleton.html +4 -1
  50. unfold/templates/unfold/layouts/unauthenticated.html +0 -2
  51. unfold/templatetags/unfold.py +141 -0
  52. unfold/utils.py +29 -10
  53. unfold/views.py +4 -1
  54. {django_unfold-0.63.0.dist-info → django_unfold-0.64.1.dist-info}/LICENSE.md +0 -0
  55. {django_unfold-0.63.0.dist-info → django_unfold-0.64.1.dist-info}/WHEEL +0 -0
unfold/sites.py CHANGED
@@ -1,17 +1,20 @@
1
1
  import copy
2
+ import time
2
3
  from http import HTTPStatus
3
4
  from typing import Any, Callable, Optional, Union
4
5
  from urllib.parse import parse_qs, urlparse
5
6
 
6
7
  from django.contrib.admin import AdminSite
8
+ from django.core.cache import cache
7
9
  from django.core.validators import EMPTY_VALUES
8
10
  from django.http import HttpRequest, HttpResponse
9
11
  from django.template.response import TemplateResponse
10
12
  from django.urls import URLPattern, path, reverse, reverse_lazy
11
13
  from django.utils.functional import lazy
12
14
  from django.utils.module_loading import import_string
15
+ from django.utils.text import slugify
13
16
 
14
- from unfold.dataclasses import DropdownItem, Favicon
17
+ from unfold.dataclasses import DropdownItem, Favicon, SearchResult
15
18
 
16
19
  try:
17
20
  from django.contrib.auth.decorators import login_not_required
@@ -22,7 +25,7 @@ except ImportError:
22
25
 
23
26
 
24
27
  from unfold.settings import get_config
25
- from unfold.utils import hex_to_rgb
28
+ from unfold.utils import convert_color
26
29
  from unfold.widgets import (
27
30
  BUTTON_CLASSES,
28
31
  CHECKBOX_CLASSES,
@@ -112,6 +115,12 @@ class UnfoldAdminSite(AdminSite):
112
115
  "tab_list": self.get_tabs_list(request),
113
116
  "styles": self._get_list("STYLES", request),
114
117
  "scripts": self._get_list("SCRIPTS", request),
118
+ "command_show_history": self._get_config("COMMAND", request).get(
119
+ "show_history"
120
+ ),
121
+ "sidebar_command_search": self._get_config("SIDEBAR", request).get(
122
+ "command_search"
123
+ ),
115
124
  "sidebar_show_all_applications": self._get_value(
116
125
  sidebar_config.get("show_all_applications"), request
117
126
  ),
@@ -165,26 +174,21 @@ class UnfoldAdminSite(AdminSite):
165
174
 
166
175
  return HttpResponse(status=HTTPStatus.OK)
167
176
 
168
- def search(
169
- self, request: HttpRequest, extra_context: Optional[dict[str, Any]] = None
170
- ) -> TemplateResponse:
171
- query = request.GET.get("s").lower()
172
- app_list = super().get_app_list(request)
173
- apps = []
177
+ def _search_apps(
178
+ self, app_list: list[dict[str, Any]], search_term: str
179
+ ) -> list[SearchResult]:
174
180
  results = []
175
-
176
- if query in EMPTY_VALUES:
177
- return HttpResponse()
181
+ apps = []
178
182
 
179
183
  for app in app_list:
180
- if query in app["name"].lower():
184
+ if search_term in app["name"].lower():
181
185
  apps.append(app)
182
186
  continue
183
187
 
184
188
  models = []
185
189
 
186
190
  for model in app["models"]:
187
- if query in model["name"].lower():
191
+ if search_term in model["name"].lower():
188
192
  models.append(model)
189
193
 
190
194
  if len(models) > 0:
@@ -194,17 +198,111 @@ class UnfoldAdminSite(AdminSite):
194
198
  for app in apps:
195
199
  for model in app["models"]:
196
200
  results.append(
197
- {
198
- "app": app,
199
- "model": model,
200
- }
201
+ SearchResult(
202
+ title=str(model["name"]),
203
+ description=app["name"],
204
+ link=model["admin_url"],
205
+ icon="tag",
206
+ )
207
+ )
208
+
209
+ return results
210
+
211
+ def _search_models(
212
+ self, request: HttpRequest, app_list: list[dict[str, Any]], search_term: str
213
+ ) -> list[SearchResult]:
214
+ results = []
215
+
216
+ for app in app_list:
217
+ for model in app["models"]:
218
+ admin_instance = self._registry.get(model["model"])
219
+ search_fields = admin_instance.get_search_fields(request)
220
+
221
+ if not search_fields:
222
+ continue
223
+
224
+ pks = []
225
+
226
+ qs = admin_instance.get_queryset(request)
227
+ search_results, _has_duplicates = admin_instance.get_search_results(
228
+ request, qs, search_term
201
229
  )
202
230
 
231
+ for item in search_results:
232
+ if item.pk in pks:
233
+ continue
234
+
235
+ pks.append(item.pk)
236
+
237
+ link = reverse_lazy(
238
+ f"{self.name}:{admin_instance.model._meta.app_label}_{admin_instance.model._meta.model_name}_change",
239
+ args=(item.pk,),
240
+ )
241
+
242
+ results.append(
243
+ SearchResult(
244
+ title=str(item),
245
+ description=f"{item._meta.app_label.capitalize()} - {item._meta.verbose_name.capitalize()}",
246
+ link=link,
247
+ icon="data_object",
248
+ )
249
+ )
250
+
251
+ return results
252
+
253
+ def search(
254
+ self, request: HttpRequest, extra_context: Optional[dict[str, Any]] = None
255
+ ) -> TemplateResponse:
256
+ start_time = time.time()
257
+
258
+ CACHE_TIMEOUT = 10
259
+
260
+ search_term = request.GET.get("s")
261
+ extended_search = "extended" in request.GET
262
+ app_list = super().get_app_list(request)
263
+ template_name = "unfold/helpers/search_results.html"
264
+
265
+ if search_term in EMPTY_VALUES:
266
+ return HttpResponse()
267
+
268
+ search_term = search_term.lower()
269
+ cache_key = f"unfold_search_{request.user.pk}_{slugify(search_term)}"
270
+ cache_results = cache.get(cache_key)
271
+
272
+ if extended_search:
273
+ template_name = "unfold/helpers/command_results.html"
274
+
275
+ if cache_results:
276
+ results = cache_results
277
+ else:
278
+ results = self._search_apps(app_list, search_term)
279
+ search_models = self._get_config("COMMAND", request).get("search_models")
280
+ search_callback = self._get_config("COMMAND", request).get(
281
+ "search_callback"
282
+ )
283
+
284
+ if extended_search:
285
+ if search_callback:
286
+ results.extend(
287
+ self._get_value(search_callback, request, search_term)
288
+ )
289
+
290
+ if search_models is True:
291
+ results.extend(self._search_models(request, app_list, search_term))
292
+
293
+ cache.set(cache_key, results, timeout=CACHE_TIMEOUT)
294
+
295
+ execution_time = time.time() - start_time
296
+
203
297
  return TemplateResponse(
204
298
  request,
205
- template="unfold/helpers/search_results.html",
299
+ template=template_name,
206
300
  context={
207
301
  "results": results,
302
+ "execution_time": execution_time,
303
+ "command_show_history": self._get_config("COMMAND", request).get(
304
+ "show_history"
305
+ ),
208
306
  },
209
307
  headers={
210
308
  "HX-Trigger": "search",
@@ -260,8 +358,8 @@ class UnfoldAdminSite(AdminSite):
260
358
  # Checks if any tab item is active and then marks the sidebar link as active
261
359
  if (
262
360
  tabs
263
- and (is_active := self._get_is_tab_active(request, tabs, link))
264
- and is_active
361
+ and self._get_is_tab_active(request, tabs, link)
362
+ and "active" not in item
265
363
  ):
266
364
  item["active"] = True
267
365
 
@@ -408,7 +506,7 @@ class UnfoldAdminSite(AdminSite):
408
506
  request, tab_item.get("link_callback") or tab_item["link"]
409
507
  ):
410
508
  has_tab_link_active = True
411
- break
509
+ continue
412
510
 
413
511
  if has_primary_link and has_tab_link_active:
414
512
  return True
@@ -440,33 +538,12 @@ class UnfoldAdminSite(AdminSite):
440
538
  def _get_colors(self, key: str, *args) -> dict[str, dict[str, str]]:
441
539
  colors = self._get_config(key, *args)
442
540
 
443
- def rgb_to_values(value: str) -> str:
444
- return ", ".join(
445
- list(
446
- map(
447
- str.strip,
448
- value.removeprefix("rgb(").removesuffix(")").split(","),
449
- )
450
- )
451
- )
452
-
453
- def hex_to_values(value: str) -> str:
454
- return ", ".join(str(item) for item in hex_to_rgb(value))
455
-
456
541
  for name, weights in colors.items():
457
542
  weights = self._get_value(weights, *args)
458
543
  colors[name] = weights
459
544
 
460
545
  for weight, value in weights.items():
461
- if value[0] == "#":
462
- colors[name][weight] = hex_to_values(value)
463
- elif value.startswith("rgb"):
464
- colors[name][weight] = rgb_to_values(value)
465
- elif isinstance(value, str) and all(
466
- part.isdigit() for part in value.split()
467
- ):
468
- colors[name][weight] = ", ".join(value.split(" "))
469
- pass
546
+ colors[name][weight] = convert_color(value)
470
547
 
471
548
  return colors
472
549