django-unfold 0.58.0__py3-none-any.whl → 0.60.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 (60) hide show
  1. {django_unfold-0.58.0.dist-info → django_unfold-0.60.0.dist-info}/METADATA +3 -2
  2. {django_unfold-0.58.0.dist-info → django_unfold-0.60.0.dist-info}/RECORD +60 -50
  3. unfold/admin.py +45 -13
  4. unfold/checks.py +24 -2
  5. unfold/contrib/filters/templates/unfold/filters/filters_numeric_slider.html +2 -2
  6. unfold/contrib/inlines/admin.py +11 -6
  7. unfold/contrib/inlines/forms.py +3 -1
  8. unfold/contrib/location_field/__init__.py +0 -0
  9. unfold/contrib/location_field/apps.py +6 -0
  10. unfold/contrib/location_field/templates/location_field/map_widget.html +5 -0
  11. unfold/decorators.py +27 -13
  12. unfold/fields.py +18 -20
  13. unfold/forms.py +99 -3
  14. unfold/layout.py +16 -0
  15. unfold/mixins/action_model_admin.py +22 -14
  16. unfold/mixins/base_model_admin.py +15 -1
  17. unfold/paginator.py +12 -1
  18. unfold/settings.py +8 -1
  19. unfold/sites.py +27 -29
  20. unfold/static/admin/js/admin/RelatedObjectLookups.js +9 -3
  21. unfold/static/unfold/css/styles.css +1 -1
  22. unfold/styles.css +12 -16
  23. unfold/templates/admin/app_list.html +4 -1
  24. unfold/templates/admin/base.html +1 -1
  25. unfold/templates/admin/change_form.html +2 -1
  26. unfold/templates/admin/edit_inline/stacked.html +12 -8
  27. unfold/templates/admin/edit_inline/tabular.html +2 -0
  28. unfold/templates/admin/includes/fieldset.html +9 -3
  29. unfold/templates/admin/login.html +45 -88
  30. unfold/templates/admin/pagination.html +1 -1
  31. unfold/templates/unfold/components/button.html +10 -1
  32. unfold/templates/unfold/helpers/account_links.html +14 -6
  33. unfold/templates/unfold/helpers/app_list.html +5 -2
  34. unfold/templates/unfold/helpers/app_list_default.html +2 -1
  35. unfold/templates/unfold/helpers/change_list_actions.html +2 -2
  36. unfold/templates/unfold/helpers/change_list_filter_actions.html +1 -1
  37. unfold/templates/unfold/helpers/edit_inline/tabular_heading.html +1 -1
  38. unfold/templates/unfold/helpers/empty_results.html +2 -2
  39. unfold/templates/unfold/helpers/field.html +5 -3
  40. unfold/templates/unfold/helpers/header.html +1 -1
  41. unfold/templates/unfold/helpers/language_form.html +10 -0
  42. unfold/templates/unfold/helpers/language_switch.html +17 -19
  43. unfold/templates/unfold/helpers/navigation.html +1 -1
  44. unfold/templates/unfold/helpers/pagination_infinite.html +3 -3
  45. unfold/templates/unfold/helpers/pagination_inline.html +28 -0
  46. unfold/templates/unfold/helpers/theme_switch.html +29 -27
  47. unfold/templates/unfold/helpers/unauthenticated_header.html +15 -0
  48. unfold/templates/unfold/helpers/unauthenticated_title.html +11 -0
  49. unfold/templates/unfold/helpers/userlinks.html +2 -6
  50. unfold/templates/unfold/helpers/welcomemsg.html +9 -7
  51. unfold/templates/unfold/layouts/unauthenticated.html +37 -0
  52. unfold/templates/unfold/widgets/select.html +1 -1
  53. unfold/templates/unfold/widgets/text.html +28 -0
  54. unfold/templates/unfold_crispy/layout/fieldset.html +3 -1
  55. unfold/templates/unfold_crispy/layout/fieldset_subheader.html +3 -0
  56. unfold/templatetags/unfold.py +37 -2
  57. unfold/utils.py +2 -2
  58. unfold/widgets.py +49 -3
  59. {django_unfold-0.58.0.dist-info → django_unfold-0.60.0.dist-info}/LICENSE.md +0 -0
  60. {django_unfold-0.58.0.dist-info → django_unfold-0.60.0.dist-info}/WHEEL +0 -0
@@ -1,32 +1,34 @@
1
1
  {% load i18n %}
2
2
 
3
- <div class="relative" x-data="{ openTheme: false }">
4
- <a class="block cursor-pointer h-[18px] leading-none hover:text-base-700 dark:hover:text-base-200" x-on:click="openTheme = !openTheme">
5
- <span class="material-symbols-outlined">
6
- <span x-text="adminTheme == 'dark' && 'dark_mode' || adminTheme == 'light' && 'light_mode' || 'computer'"></span>
7
- </span>
8
- </a>
9
-
10
- <nav class="absolute bg-white border border-base-200 flex flex-col leading-none py-1 -right-2 rounded-default shadow-lg top-7 w-40 z-50 dark:bg-base-800 dark:border-base-700" x-cloak x-show="openTheme" x-transition x-on:click.outside="openTheme = false">
11
- <a class="cursor-pointer flex flex-row leading-none mx-1 px-3 py-1.5 rounded-default hover:bg-base-100 hover:text-base-700 dark:hover:bg-base-700 dark:hover:text-base-200"
12
- x-on:click="adminTheme = 'dark'"
13
- x-bind:class="adminTheme == 'dark' && 'text-primary-600 dark:text-primary-500 dark:hover:text-primary-500! hover:text-primary-600!'">
14
- <span class="material-symbols-outlined mr-2">dark_mode</span>
15
- <span class="leading-none self-center">{% trans "Dark" %}</span>
3
+ {% if not theme %}
4
+ <div class="relative" x-data="{ openTheme: false }">
5
+ <a class="block cursor-pointer h-[18px] leading-none hover:text-base-700 dark:hover:text-base-200" x-on:click="openTheme = !openTheme">
6
+ <span class="material-symbols-outlined">
7
+ <span x-text="adminTheme == 'dark' && 'dark_mode' || adminTheme == 'light' && 'light_mode' || 'computer'"></span>
8
+ </span>
16
9
  </a>
17
10
 
18
- <a class="cursor-pointer flex flex-row mx-1 px-3 py-1.5 rounded-default hover:bg-base-100 hover:text-base-700 dark:hover:bg-base-700 dark:hover:text-base-200"
19
- x-on:click="adminTheme = 'light'"
20
- x-bind:class="adminTheme == 'light' && 'text-primary-600 dark:text-primary-500 dark:hover:text-primary-500! hover:text-primary-600!'">
21
- <span class="material-symbols-outlined mr-2">light_mode</span>
22
- <span class="leading-none self-center">{% trans "Light" %}</span>
23
- </a>
11
+ <nav class="absolute bg-white border border-base-200 flex flex-col leading-none py-1 -right-2 rounded-default shadow-lg top-7 w-40 z-50 dark:bg-base-800 dark:border-base-700" x-cloak x-show="openTheme" x-transition x-on:click.outside="openTheme = false">
12
+ <a class="cursor-pointer flex flex-row leading-none mx-1 px-3 py-1.5 rounded-default hover:bg-base-100 hover:text-base-700 dark:hover:bg-base-700 dark:hover:text-base-200"
13
+ x-on:click="adminTheme = 'dark'"
14
+ x-bind:class="adminTheme == 'dark' && 'text-primary-600 dark:text-primary-500 dark:hover:text-primary-500! hover:text-primary-600!'">
15
+ <span class="material-symbols-outlined mr-2">dark_mode</span>
16
+ <span class="leading-none self-center">{% trans "Dark" %}</span>
17
+ </a>
24
18
 
25
- <a class="cursor-pointer flex flex-row mx-1 px-3 py-1.5 rounded-default hover:bg-base-100 hover:text-base-700 dark:hover:bg-base-700 dark:hover:text-base-200"
26
- x-on:click="adminTheme = 'auto'"
27
- x-bind:class="adminTheme == 'auto' && 'text-primary-600 dark:text-primary-500 dark:hover:text-primary-500! hover:text-primary-600!'">
28
- <span class="material-symbols-outlined mr-2">computer</span>
29
- <span class="leading-none self-center">{% trans "System" %}</span>
30
- </a>
31
- </nav>
32
- </div>
19
+ <a class="cursor-pointer flex flex-row mx-1 px-3 py-1.5 rounded-default hover:bg-base-100 hover:text-base-700 dark:hover:bg-base-700 dark:hover:text-base-200"
20
+ x-on:click="adminTheme = 'light'"
21
+ x-bind:class="adminTheme == 'light' && 'text-primary-600 dark:text-primary-500 dark:hover:text-primary-500! hover:text-primary-600!'">
22
+ <span class="material-symbols-outlined mr-2">light_mode</span>
23
+ <span class="leading-none self-center">{% trans "Light" %}</span>
24
+ </a>
25
+
26
+ <a class="cursor-pointer flex flex-row mx-1 px-3 py-1.5 rounded-default hover:bg-base-100 hover:text-base-700 dark:hover:bg-base-700 dark:hover:text-base-200"
27
+ x-on:click="adminTheme = 'auto'"
28
+ x-bind:class="adminTheme == 'auto' && 'text-primary-600 dark:text-primary-500 dark:hover:text-primary-500! hover:text-primary-600!'">
29
+ <span class="material-symbols-outlined mr-2">computer</span>
30
+ <span class="leading-none self-center">{% trans "System" %}</span>
31
+ </a>
32
+ </nav>
33
+ </div>
34
+ {% endif %}
@@ -0,0 +1,15 @@
1
+ {% load i18n %}
2
+
3
+ <div class="absolute flex flex-row items-center justify-between left-0 m-4 right-0 top-0">
4
+ {% if site_url %}
5
+ <a href="{{ site_url }}" class="flex font-medium items-center text-sm text-primary-600 dark:text-primary-500">
6
+ <span class="material-symbols-outlined mr-2">arrow_back</span> {% trans 'Return to site' %}
7
+ </a>
8
+ {% endif %}
9
+
10
+ {% if not theme %}
11
+ <div class="ml-auto">
12
+ {% include "unfold/helpers/theme_switch.html" %}
13
+ </div>
14
+ {% endif %}
15
+ </div>
@@ -0,0 +1,11 @@
1
+ {% load i18n %}
2
+
3
+ <div class="border-b border-base-200 mb-8 pb-6 dark:border-base-800">
4
+ <h1 class="font-semibold">
5
+ {% if subtitle %}
6
+ <span class="block text-font-important-light dark:text-font-important-dark">{{ subtitle }} </span>
7
+ {% endif %}
8
+
9
+ <span class="block text-primary-600 text-xl dark:text-primary-500">{{ title }}</span>
10
+ </h1>
11
+ </div>
@@ -11,13 +11,9 @@
11
11
  {{ extra_userlinks }}
12
12
  {% endif %}
13
13
 
14
- {% if show_languages %}
15
- {% include "unfold/helpers/language_switch.html" %}
16
- {% endif %}
14
+ {% include "unfold/helpers/language_switch.html" %}
17
15
 
18
- {% if not theme %}
19
- {% include "unfold/helpers/theme_switch.html" %}
20
- {% endif %}
16
+ {% include "unfold/helpers/theme_switch.html" %}
21
17
 
22
18
  {% include "unfold/helpers/account_links.html" %}
23
19
  </div>
@@ -1,15 +1,17 @@
1
1
  {% load unfold i18n %}
2
2
 
3
3
  <div class="flex flex-row grow font-semibold items-center min-w-0 mr-3">
4
- <span class="cursor-pointer flex flex-row items-center">
5
- <span class="material-symbols-outlined hidden! xl:block!" hx-get="{% url "admin:toggle_sidebar" %}" hx-swap="none" x-on:click="sidebarDesktopOpen = !sidebarDesktopOpen">
6
- dock_to_right
7
- </span>
4
+ {% if is_nav_sidebar_enabled %}
5
+ <span class="cursor-pointer flex flex-row items-center">
6
+ <span class="material-symbols-outlined hidden! xl:block!" hx-get="{% url "admin:toggle_sidebar" %}" hx-swap="none" x-on:click="sidebarDesktopOpen = !sidebarDesktopOpen">
7
+ dock_to_right
8
+ </span>
8
9
 
9
- <span class="material-symbols-outlined block! xl:hidden!" x-on:click="sidebarMobileOpen = !sidebarMobileOpen">
10
- dock_to_right
10
+ <span class="material-symbols-outlined block! xl:hidden!" x-on:click="sidebarMobileOpen = !sidebarMobileOpen">
11
+ dock_to_right
12
+ </span>
11
13
  </span>
12
- </span>
14
+ {% endif %}
13
15
 
14
16
  <span class="block bg-base-200 h-5 mx-3 w-px dark:bg-base-700"></span>
15
17
 
@@ -0,0 +1,37 @@
1
+ {% extends 'unfold/layouts/skeleton.html' %}
2
+
3
+ {% load i18n static unfold %}
4
+
5
+
6
+ {% block bodyclass %}{{ block.super }}bg-base-50 login dark:bg-base-900{% endblock %}
7
+
8
+ {% block usertools %}{% endblock %}
9
+
10
+ {% block nav-global %}{% endblock %}
11
+
12
+ {% block nav-sidebar %}{% endblock %}
13
+
14
+ {% block content_title %}{% endblock %}
15
+
16
+ {% block breadcrumbs %}{% endblock %}
17
+
18
+ {% block title %}
19
+ {{ title }} | {{ site_title }}
20
+ {% endblock %}
21
+
22
+ {% block base %}
23
+ <div id="page" class="bg-white flex min-h-screen dark:bg-base-900 ">
24
+ <div class="flex grow items-center justify-center mx-auto px-4 relative">
25
+ <div class="w-full sm:w-96">
26
+ {% block content %}{% endblock %}
27
+ </div>
28
+
29
+ {% include "unfold/helpers/unauthenticated_header.html" %}
30
+ </div>
31
+
32
+ {% if login_image %}
33
+ <div class="bg-cover grow hidden max-w-3xl xl:max-w-4xl xl:block" style="background-image: url('{{ login_image }}')">
34
+ </div>
35
+ {% endif %}
36
+ </div>
37
+ {% endblock %}
@@ -5,5 +5,5 @@
5
5
  </optgroup>{% endif %}{% endfor %}
6
6
  </select>
7
7
 
8
- <span class="material-symbols-outlined absolute group-[.primary]:text-white -ml-[31px] pointer-events-none text-base-400 top-0 top-1/2 hover:text-base-700 dark:text-base-500 dark:hover:text-base-200 -translate-y-1/2">expand_more</span>
8
+ <span class="material-symbols-outlined absolute group-[.primary]:text-white pointer-events-none mr-[12px] right-0 text-base-400 top-0 top-1/2 hover:text-base-700 dark:text-base-500 dark:hover:text-base-200 -translate-y-1/2">expand_more</span>
9
9
  </div>
@@ -0,0 +1,28 @@
1
+
2
+ <div class="max-w-2xl relative">
3
+ {% if widget.prefix %}
4
+ <span class="absolute left-3 top-0 bottom-0 flex items-center justify-center">
5
+ {{ widget.prefix }}
6
+ </span>
7
+ {% endif %}
8
+
9
+ {% if widget.prefix_icon %}
10
+ <span class="material-symbols-outlined absolute left-3 top-0 bottom-0 flex items-center justify-center text-base-400 dark:text-base-500">
11
+ {{ widget.prefix_icon }}
12
+ </span>
13
+ {% endif %}
14
+
15
+ {% include "django/forms/widgets/input.html" %}
16
+
17
+ {% if widget.suffix %}
18
+ <span class="absolute right-3 top-0 bottom-0 flex items-center justify-center">
19
+ {{ widget.suffix }}
20
+ </span>
21
+ {% endif %}
22
+
23
+ {% if widget.suffix_icon %}
24
+ <span class="material-symbols-outlined absolute right-3 top-0 bottom-0 flex items-center justify-center text-base-400 dark:text-base-500">
25
+ {{ widget.suffix_icon }}
26
+ </span>
27
+ {% endif %}
28
+ </div>
@@ -5,5 +5,7 @@
5
5
  </legend>
6
6
  {% endif %}
7
7
 
8
- {{ fields|safe }}
8
+ <div class="flex flex-col gap-5">
9
+ {{ fields|safe }}
10
+ </div>
9
11
  </fieldset>
@@ -0,0 +1,3 @@
1
+ <div class="border-b border-t border-base-200 font-semibold -mx-3 py-2.5 px-3 text-font-important-light first:border-t-0 first:-mt-5 dark:border-base-800 dark:text-font-important-dark">
2
+ {{ title }}
3
+ </div>
@@ -4,12 +4,13 @@ from typing import Any, Optional, Union
4
4
 
5
5
  from django import template
6
6
  from django.contrib.admin.helpers import AdminForm, Fieldset
7
- from django.contrib.admin.views.main import ChangeList
7
+ from django.contrib.admin.views.main import PAGE_VAR, ChangeList
8
8
  from django.contrib.admin.widgets import RelatedFieldWidgetWrapper
9
+ from django.core.paginator import Paginator
9
10
  from django.db.models import Model
10
11
  from django.db.models.options import Options
11
12
  from django.forms import BoundField, CheckboxSelectMultiple, Field
12
- from django.http import HttpRequest
13
+ from django.http import HttpRequest, QueryDict
13
14
  from django.template import Context, Library, Node, RequestContext, TemplateSyntaxError
14
15
  from django.template.base import NodeList, Parser, Token, token_kwargs
15
16
  from django.template.loader import render_to_string
@@ -569,3 +570,37 @@ def changeform_condition(field: BoundField) -> BoundField:
569
570
  field.field.field.widget.attrs["x-model.fill"] = field.field.name
570
571
 
571
572
  return field
573
+
574
+
575
+ @register.simple_tag
576
+ def infinite_paginator_url(cl, i):
577
+ return cl.get_query_string({PAGE_VAR: i})
578
+
579
+
580
+ @register.simple_tag
581
+ def elided_page_range(
582
+ paginator: Paginator, number: int
583
+ ) -> Optional[list[Union[int, str]]]:
584
+ if not paginator or not number:
585
+ return None
586
+
587
+ return paginator.get_elided_page_range(number=number)
588
+
589
+
590
+ @register.simple_tag(takes_context=True)
591
+ def querystring_params(
592
+ context: RequestContext, query_key: str, query_value: str
593
+ ) -> str:
594
+ request = context.get("request")
595
+ result = QueryDict(mutable=True)
596
+
597
+ for key, values in request.GET.lists():
598
+ if key == query_key:
599
+ continue
600
+
601
+ for value in values:
602
+ result[key] = value
603
+
604
+ result[query_key] = query_value
605
+
606
+ return result.urlencode()
unfold/utils.py CHANGED
@@ -150,7 +150,7 @@ def hex_to_rgb(hex_color: str) -> list[int]:
150
150
  return (r, g, b)
151
151
 
152
152
 
153
- def prettify_json(data: Any) -> Optional[str]:
153
+ def prettify_json(data: Any, encoder: Any) -> Optional[str]:
154
154
  try:
155
155
  from pygments import highlight
156
156
  from pygments.formatters import HtmlFormatter
@@ -167,7 +167,7 @@ def prettify_json(data: Any) -> Optional[str]:
167
167
  )
168
168
  return highlight(response, JsonLexer(), formatter)
169
169
 
170
- response = json.dumps(data, sort_keys=True, indent=4)
170
+ response = json.dumps(data, sort_keys=True, indent=4, cls=encoder)
171
171
 
172
172
  return mark_safe(
173
173
  f'<div class="block dark:hidden">{format_response(response, "colorful")}</div>'
unfold/widgets.py CHANGED
@@ -123,9 +123,10 @@ TEXTAREA_EXPANDABLE_CLASSES = [
123
123
 
124
124
  SELECT_CLASSES = [
125
125
  *BASE_INPUT_CLASSES,
126
- "pr-8",
126
+ "pr-8!",
127
127
  "max-w-2xl",
128
128
  "appearance-none",
129
+ "truncate",
129
130
  ]
130
131
 
131
132
  PROSE_CLASSES = [
@@ -287,7 +288,47 @@ FILE_CLASSES = [
287
288
  ]
288
289
 
289
290
 
290
- class UnfoldAdminTextInputWidget(AdminTextInputWidget):
291
+ class UnfoldPrefixSuffixMixin:
292
+ def get_context(self, name, value, attrs):
293
+ widget = {}
294
+
295
+ if "prefix" in self.attrs:
296
+ widget["prefix"] = self.attrs["prefix"]
297
+ del self.attrs["prefix"]
298
+
299
+ if "prefix_icon" in self.attrs:
300
+ widget["prefix_icon"] = self.attrs["prefix_icon"]
301
+ self.attrs["class"] = " ".join([self.attrs["class"], "pl-9"])
302
+ del self.attrs["prefix_icon"]
303
+
304
+ if "suffix" in self.attrs:
305
+ widget["suffix"] = self.attrs["suffix"]
306
+ del self.attrs["suffix"]
307
+
308
+ if "suffix_icon" in self.attrs:
309
+ widget["suffix_icon"] = self.attrs["suffix_icon"]
310
+ self.attrs["class"] = " ".join([self.attrs["class"], "pr-9"])
311
+ del self.attrs["suffix_icon"]
312
+
313
+ widget.update(
314
+ {
315
+ "name": name,
316
+ "is_hidden": self.is_hidden,
317
+ "required": self.is_required,
318
+ "value": self.format_value(value),
319
+ "attrs": self.build_attrs(self.attrs, attrs),
320
+ "template_name": self.template_name,
321
+ }
322
+ )
323
+
324
+ return {
325
+ "widget": widget,
326
+ }
327
+
328
+
329
+ class UnfoldAdminTextInputWidget(UnfoldPrefixSuffixMixin, AdminTextInputWidget):
330
+ template_name = "unfold/widgets/text.html"
331
+
291
332
  def __init__(self, attrs: Optional[dict[str, Any]] = None) -> None:
292
333
  super().__init__(
293
334
  attrs={
@@ -759,8 +800,13 @@ try:
759
800
  template_name = "unfold/widgets/split_money.html"
760
801
 
761
802
  def __init__(self, *args, **kwargs):
803
+ if "attrs" in kwargs:
804
+ attrs = kwargs.pop("attrs")
805
+ else:
806
+ attrs = {}
807
+
762
808
  super().__init__(
763
- amount_widget=UnfoldAdminTextInputWidget,
809
+ amount_widget=UnfoldAdminTextInputWidget(attrs=attrs),
764
810
  currency_widget=UnfoldAdminSelectWidget(
765
811
  choices=CURRENCY_CHOICES,
766
812
  attrs={