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
@@ -78,9 +78,9 @@ const filterForm = () => {
78
78
  }
79
79
 
80
80
  filterForm.addEventListener("formdata", (event) => {
81
- for (const [key, value] of event.formData.entries()) {
81
+ Array.from(event.formData.entries()).forEach(([key, value]) => {
82
82
  if (value === "") event.formData.delete(key);
83
- }
83
+ });
84
84
  });
85
85
  };
86
86
 
@@ -25,11 +25,7 @@
25
25
  {% endif %}
26
26
 
27
27
  {% block branding %}
28
- <h1 id="site-name">
29
- <a href="{% url 'admin:index' %}">
30
- {{ site_header|default:_('Django administration') }}
31
- </a>
32
- </h1>
28
+ {% include "unfold/helpers/site_branding.html" %}
33
29
  {% endblock %}
34
30
 
35
31
  {% block content %}
@@ -3,7 +3,7 @@
3
3
  {% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
4
4
 
5
5
  {% block branding %}
6
- <h1 id="site-name"><a href="{% url 'admin:index' %}">{{ site_header|default:_('Django administration') }}</a></h1>
6
+ {% include "unfold/helpers/site_branding.html" %}
7
7
  {% endblock %}
8
8
 
9
9
  {% block nav-global %}{% endblock %}
@@ -12,7 +12,7 @@
12
12
  {% endfor %}
13
13
 
14
14
  {% if spec|class_name == "BooleanFieldListFilter" %}
15
- <ul class="flex border min-w-20 rounded shadow-sm text-font-default-light dark:border-base-700 dark:text-font-default-dark w-full">
15
+ <ul class="dark:bg-base-900 border border-base-200 flex min-w-20 rounded shadow-sm text-font-default-light dark:border-base-700 dark:text-font-default-dark w-full">
16
16
  {% for choice in choices %}
17
17
  <li class="basis-1/3 border-r border-base-200 flex-grow truncate last:border-r-0 dark:border-base-700 {% if choice.selected %}font-semibold text-primary-600 dark:text-primary-500 {% else %}hover:text-base-700 dark:hover:text-base-200{% endif %}">
18
18
  <a href="{{ choice.query_string|iriencode }}" title="{{ choice.display }}" class="block px-3 py-2 text-center hover:text-primary-600 dark:hover:text-primary-500">
@@ -7,11 +7,7 @@
7
7
  {% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
8
8
 
9
9
  {% block branding %}
10
- <h1 id="site-name">
11
- <a href="{% url 'admin:index' %}">
12
- {{ site_header|default:_('Django administration') }}
13
- </a>
14
- </h1>
10
+ {% include "unfold/helpers/site_branding.html" %}
15
11
  {% endblock %}
16
12
 
17
13
  {% block content %}
@@ -29,7 +29,7 @@
29
29
  <div class="w-full sm:w-96">
30
30
  <h1 class="font-semibold mb-10">
31
31
  <span class="block text-font-important-light dark:text-font-important-dark">{% trans 'Welcome back to' %}</span>
32
- <span class="block text-primary-600 text-xl dark:text-primary-500">{{ site_title }}</span>
32
+ <span class="block text-primary-600 text-xl dark:text-primary-500">{{ site_title|default:_('Django site admin') }}</span>
33
33
  </h1>
34
34
 
35
35
  {% include "unfold/helpers/messages.html" %}
@@ -11,8 +11,10 @@
11
11
  </button>
12
12
  </div>
13
13
 
14
- {% for pair in cl.params.items %}
15
- {% if pair.0 != search_var %}<input type="hidden" name="{{ pair.0 }}" value="{{ pair.1 }}">{% endif %}
14
+ {% for pair in cl.filter_params.items %}
15
+ {% for val in pair.1 %}
16
+ {% if pair.0 != search_var %}<input type="hidden" name="{{ pair.0 }}" value="{{ val }}">{% endif %}
17
+ {% endfor %}
16
18
  {% endfor %}
17
19
  </form>
18
20
  </div>
@@ -5,7 +5,7 @@
5
5
  <span class="material-symbols-outlined">person</span>
6
6
  </a>
7
7
 
8
- <nav class="absolute bg-white border flex flex-col leading-none py-1 -right-2 rounded shadow-lg top-7 w-52 z-50 dark:bg-base-800 dark:border-base-700" x-cloak x-show="openUserLinks" @click.outside="openUserLinks = false">
8
+ <nav class="absolute bg-white border flex flex-col leading-none py-1 -right-2 rounded shadow-lg top-7 w-52 z-50 dark:bg-base-800 dark:border-base-700" x-cloak x-show="openUserLinks" x-transition x-on:click.outside="openUserLinks = false">
9
9
  <div class="border-b border-base-100 flex flex-row flex-shrink-0 items-start justify-start mb-1 pb-1 dark:border-base-700">
10
10
  <span class="block mx-1 px-3 py-2 truncate">
11
11
  {% firstof user.get_short_name user.get_username %}
@@ -9,7 +9,7 @@
9
9
  </span>
10
10
 
11
11
  <template x-teleport="body">
12
- <nav x-anchor.bottom-end.offset.4="$refs.rowDropdown{{ action_id }}" class="bg-white border flex flex-col leading-none py-1 rounded shadow-lg text-sm top-7 z-50 w-48 dark:bg-base-800 dark:border-base-700" x-cloak x-show="openActionsId{{ action_id }}" @click.outside="openActionsId{{ action_id }} = false">
12
+ <nav x-anchor.bottom-end.offset.4="$refs.rowDropdown{{ action_id }}" class="bg-white border flex flex-col leading-none py-1 rounded shadow-lg text-sm top-7 z-50 w-48 dark:bg-base-800 dark:border-base-700" x-cloak x-show="openActionsId{{ action_id }}" x-transition x-on:click.outside="openActionsId{{ action_id }} = false">
13
13
  {% for action in actions %}
14
14
  <a href="{% url action.raw_path instance_pk %}" class="mx-1 px-3 py-2 rounded truncate hover:bg-base-100 dark:hover:bg-base-700 dark:hover:text-base-200"{% for attr_name, attr_value in action.attrs.items %} {{ attr_name }}="{{ attr_value }}"{% endfor %}>
15
15
  {{ action.title }}
@@ -10,8 +10,8 @@
10
10
  {% preserve_filters %}
11
11
  {% endif %}
12
12
 
13
- <div class="flex flex-col grow gap-4 overflow-auto *:mb-0" {% if cl.model_admin.list_filter_sheet %}data-simplebar data-simplebar-direction='rtl'{% endif %}>
14
- <div class="flex flex-col gap-4 {% if cl.model_admin.list_filter_sheet %}px-3 py-2.5{% endif %} *:mb-0">
13
+ <div class="flex flex-col grow gap-4 overflow-auto *:mb-0" data-simplebar data-simplebar-direction="rtl">
14
+ <div class="flex flex-col gap-4 mx-1 px-2 py-2.5 {% if not cl.model_admin.list_filter_sheet %} 2xl:px-0 2xl:py-0{% endif %} *:mb-0">
15
15
  {% for spec in cl.filter_specs %}
16
16
  {% admin_list_filter cl spec %}
17
17
  {% endfor %}
@@ -1,6 +1,6 @@
1
1
  {% load i18n %}
2
2
 
3
- <div class="{% if cl.model_admin.list_filter_sheet %}bg-white border-t border-base-200 p-3 py-2.5 dark:bg-base-800 dark:border-base-700{% else %}mt-6{% endif %}">
3
+ <div class="bg-white border-t border-base-200 p-3 py-2.5 dark:bg-base-800 dark:border-base-700{% if not cl.model_admin.list_filter_sheet %} 2xl:!border-t-0 2xl:!bg-transparent 2xl:px-0{% endif %}">
4
4
  {% if cl.model_admin.list_filter_submit %}
5
5
  <button type="submit" class="bg-primary-600 block border border-transparent font-medium px-3 py-2 rounded text-white w-full">
6
6
  {% trans "Apply Filters" %}
@@ -2,9 +2,9 @@
2
2
 
3
3
  {% if show_back_button %}
4
4
  {% if opts and adminform %}
5
- {% url opts|admin_urlname:'changelist' as link %}
5
+ {% url opts|admin_urlname:'changelist' as changelist_url %}
6
6
 
7
- <a href="{{ link }}" class="block h-4.5 mr-3" title="{% trans "Go back" %}">
7
+ <a href="{% add_preserved_filters changelist_url %}" class="block h-4.5 mr-3" title="{% trans "Go back" %}">
8
8
  <span class="material-symbols-outlined">
9
9
  arrow_back
10
10
  </span>
@@ -10,7 +10,7 @@
10
10
  <span class="material-symbols-outlined">translate</span>
11
11
  </a>
12
12
 
13
- <div class="absolute bg-white border flex flex-col leading-none py-1 -right-2 rounded shadow-lg top-7 w-52 z-50 dark:bg-base-800 dark:border-base-700" x-cloak x-show="openLanguageLinks" @click.outside="openLanguageLinks = false">
13
+ <div class="absolute bg-white border flex flex-col leading-none py-1 -right-2 rounded shadow-lg top-7 w-52 z-50 dark:bg-base-800 dark:border-base-700" x-cloak x-show="openLanguageLinks" x-transition x-on:click.outside="openLanguageLinks = false">
14
14
  {% for language in languages %}
15
15
  <form action="{% url 'set_language' %}" method="post" class="flex w-full">
16
16
  {% csrf_token %}
@@ -1,13 +1,23 @@
1
- <div class="border-b border-base-200 mb-5 py-3 dark:border-base-800">
2
- <div class="flex font-semibold gap-3 h-10 items-center px-6">
1
+ <div class="border-b border-base-200 flex gap-3 items-center h-[65px] mb-5 dark:border-base-800 px-3" {% if site_dropdown %}x-data="{ openDropdown: false }"{% endif %}>
2
+ <div class="bg-transparent border border-transparent flex font-semibold gap-3 grow -mx-px h-[48px] items-center px-3 {% if site_dropdown %}cursor-pointer rounded transition-all hover:bg-white hover:border-base-200 hover:shadow-sm hover:dark:bg-base-800 hover:dark:border-base-700{% endif %}"
3
+ x-on:click="openDropdown = !openDropdown"
4
+ x-bind:class="{'bg-white border-base-200 shadow-sm dark:bg-base-800 dark:border-base-700': openDropdown, 'bg-transparent border-transparent': !openDropdown}">
3
5
  {% if site_logo %}
4
6
  {% include "unfold/helpers/site_logo.html" %}
5
7
  {% elif branding %}
6
8
  {% include "unfold/helpers/site_icon.html" %}
7
9
  {% endif %}
8
10
 
9
- <div class="block cursor-pointer h-4.5 ml-auto xl:!hidden hover:text-base-700 dark:hover:text-base-200" x-on:click="sidebarMobileOpen = !sidebarMobileOpen">
10
- <span class="material-symbols-outlined">close</span>
11
- </div>
11
+ {% if site_dropdown %}
12
+ <span class="material-symbols-outlined ml-auto select-none">
13
+ unfold_more
14
+ </span>
15
+ {% endif %}
12
16
  </div>
17
+
18
+ <span class="material-symbols-outlined block cursor-pointer h-4.5 xl:!hidden hover:text-base-700 dark:hover:text-base-200" x-on:click="sidebarMobileOpen = !sidebarMobileOpen">
19
+ close
20
+ </span>
21
+
22
+ {% include "unfold/helpers/site_dropdown.html" %}
13
23
  </div>
@@ -0,0 +1,9 @@
1
+ <h1 id="site-name">
2
+ {% if site_dropdown %}
3
+ {{ site_header|default:_('Django administration') }}
4
+ {% else %}
5
+ <a href="{% url 'admin:index' %}">
6
+ {{ site_header|default:_('Django administration') }}
7
+ </a>
8
+ {% endif %}
9
+ </h1>
@@ -0,0 +1,19 @@
1
+ {% load i18n %}
2
+
3
+ {% if site_dropdown %}
4
+ <div class="absolute bg-white border flex flex-col left-3 py-1 rounded shadow-lg top-[73px] w-[264px] z-50 dark:bg-base-800 dark:border-base-700" x-cloak x-show="openDropdown" x-transition x-on:click.outside="openDropdown = false">
5
+ {% for item in site_dropdown %}
6
+ <a href="{{ item.link }}" class="flex items-center gap-3 max-h-[30px] mx-1 px-2 py-2 rounded hover:bg-base-100 hover:text-base-700 dark:hover:bg-base-700 dark:hover:text-base-200">
7
+ {% if item.icon %}
8
+ <span class="material-symbols-outlined">
9
+ {{ item.icon }}
10
+ </span>
11
+ {% endif %}
12
+
13
+ <span class="grow truncate">
14
+ {{ item.title }}
15
+ </span>
16
+ </a>
17
+ {% endfor %}
18
+ </div>
19
+ {% endif %}
@@ -16,6 +16,14 @@
16
16
  </a>
17
17
  {% endif %}
18
18
 
19
- <div class="text-font-important-light tracking-tight dark:text-font-important-dark xl:text-base">
20
- {{ branding }}
19
+ <div class="flex flex-col gap-1">
20
+ <div class="text-font-important-light leading-none tracking-tight dark:text-font-important-dark *:leading-none {% if site_subheader %}xl:text-sm{% else %}xl:text-base{% endif %}">
21
+ {{ branding }}
22
+ </div>
23
+
24
+ {% if site_subheader %}
25
+ <div class="font-normal leading-none text-font-subtle-light text-xs dark:text-font-subtle-dark">
26
+ {{ site_subheader }}
27
+ </div>
28
+ {% endif %}
21
29
  </div>
@@ -8,7 +8,13 @@
8
8
  {% for item in tabs_list %}
9
9
  {% if item.has_permission %}
10
10
  <li class="border-b last:border-b-0 md:border-b-0 md:mr-8 dark:border-base-800">
11
- <a href="{% if item.link_callback %}{{ item.link_callback }}{% else %}{{ item.link }}{% endif %}" class="block px-3 py-2 md:py-4 md:px-0 dark:border-base-800 {% if item.active %} border-b font-semibold -mb-px text-primary-600 hover:text-primary-600 dark:text-primary-500 dark:hover:text-primary-500 md:border-primary-500 dark:md:!border-primary-600{% else %}font-medium hover:text-primary-600 dark:hover:text-primary-500{% endif %}">
11
+ <a href="{% if item.link_callback %}{{ item.link_callback }}{% else %}{{ item.link }}{% endif %}"
12
+ class="block px-3 py-2 md:py-4 md:px-0 dark:border-base-800 {% if item.active and not item.inline %} border-b font-semibold -mb-px text-primary-600 hover:text-primary-600 dark:text-primary-500 dark:hover:text-primary-500 md:border-primary-500 dark:md:!border-primary-600{% else %}font-medium hover:text-primary-600 dark:hover:text-primary-500{% endif %}"
13
+ {% if item.inline %}
14
+ x-on:click="activeTab = '{{ item.inline }}'"
15
+ x-bind:class="{'border-b border-base-200 dark:border-base-800 md:border-primary-500 dark:md:!border-primary-600 font-semibold -mb-px text-primary-600 dark:text-primary-500': activeTab == '{{ item.inline }}'}"
16
+ {% endif %}
17
+ >
12
18
  {{ item.title }}
13
19
  </a>
14
20
  </li>
@@ -7,7 +7,7 @@
7
7
  </span>
8
8
  </a>
9
9
 
10
- <nav class="absolute bg-white border flex flex-col leading-none py-1 -right-2 rounded shadow-lg top-7 w-40 z-50 dark:bg-base-800 dark:border-base-700" x-cloak x-show="openTheme" @click.outside="openTheme = false">
10
+ <nav class="absolute bg-white border flex flex-col leading-none py-1 -right-2 rounded 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
11
  <a class="cursor-pointer flex flex-row leading-none mx-1 px-3 py-1.5 rounded hover:bg-base-100 hover:text-base-700 dark:hover:bg-base-700 dark:hover:text-base-200"
12
12
  x-on:click="adminTheme = 'dark'"
13
13
  x-bind:class="adminTheme == 'dark' && 'text-primary-600 dark:text-primary-500 dark:hover:!text-primary-500 hover:!text-primary-600'">
@@ -1,11 +1,7 @@
1
1
  {% extends "unfold/layouts/base_simple.html" %}
2
2
 
3
3
  {% block branding %}
4
- <h1 id="site-name">
5
- <a href="{% url 'admin:index' %}">
6
- {{ site_header|default:_('Django administration') }}
7
- </a>
8
- </h1>
4
+ {% include "unfold/helpers/site_branding.html" %}
9
5
  {% endblock %}
10
6
 
11
7
  {% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
@@ -59,7 +59,7 @@
59
59
  {% endblock %}
60
60
  </head>
61
61
 
62
- <body class="antialiased bg-white font-sans text-font-default-light text-sm dark:bg-base-900 dark:text-font-default-dark {% if is_popup %}popup {% endif %}{% block bodyclass %}{% endblock %}" data-admin-utc-offset="{% now "Z" %}" x-data="{ mainWidth: 0, activeTab: 'general', sidebarMobileOpen: false, sidebarDesktopOpen: {% if request.session.toggle_sidebar == False %}false{% else %}true{% endif %} }" x-init="activeTab = window.location.hash?.replace('#', '') || 'general'">
62
+ <body class="antialiased bg-white font-sans text-font-default-light text-sm dark:bg-base-900 dark:text-font-default-dark {% if is_popup %}popup {% endif %}{% block bodyclass %}{% endblock %}" data-admin-utc-offset="{% now "Z" %}" x-data="{ mainWidth: 0, {% if opts %}activeTab: 'general',{% endif %} sidebarMobileOpen: false, sidebarDesktopOpen: {% if request.session.toggle_sidebar == False %}false{% else %}true{% endif %} }" x-init="activeTab = {% if opts %}window.location.hash?.replace('#', '') || 'general'{% else %}''{% endif %}">
63
63
  {% if colors %}
64
64
  <style id="unfold-theme-colors">
65
65
  :root {
@@ -1,8 +1,10 @@
1
- from typing import Any, Dict, List, Mapping, Optional, Set, Union
1
+ from collections.abc import Mapping
2
+ from typing import Any, Optional, Union
2
3
 
3
4
  from django import template
4
5
  from django.contrib.admin.helpers import AdminForm, Fieldset
5
6
  from django.contrib.admin.views.main import ChangeList
7
+ from django.db.models.options import Options
6
8
  from django.forms import Field
7
9
  from django.http import HttpRequest
8
10
  from django.template import Context, Library, Node, RequestContext, TemplateSyntaxError
@@ -15,9 +17,44 @@ from unfold.components import ComponentRegistry
15
17
  register = Library()
16
18
 
17
19
 
18
- @register.simple_tag(name="tab_list", takes_context=True)
19
- def tab_list(context, page, opts) -> str:
20
+ def _get_tabs_list(
21
+ context: RequestContext, page: str, opts: Optional[Options] = None
22
+ ) -> list:
20
23
  tabs_list = []
24
+ page_id = None
25
+
26
+ if page not in ["changeform", "changelist"]:
27
+ page_id = page
28
+
29
+ for tab in context.get("tab_list", []):
30
+ if page_id:
31
+ if tab.get("page") == page_id:
32
+ tabs_list = tab["items"]
33
+ break
34
+
35
+ continue
36
+
37
+ if "models" not in tab:
38
+ continue
39
+
40
+ for tab_model in tab["models"]:
41
+ if isinstance(tab_model, str):
42
+ if str(opts) == tab_model and page == "changelist":
43
+ tabs_list = tab["items"]
44
+ break
45
+ elif isinstance(tab_model, dict) and str(opts) == tab_model["name"]:
46
+ is_detail = tab_model.get("detail", False)
47
+
48
+ if (page == "changeform" and is_detail) or (
49
+ page == "changelist" and not is_detail
50
+ ):
51
+ tabs_list = tab["items"]
52
+ break
53
+ return tabs_list
54
+
55
+
56
+ @register.simple_tag(name="tab_list", takes_context=True)
57
+ def tab_list(context: RequestContext, page: str, opts: Optional[Options] = None) -> str:
21
58
  inlines_list = []
22
59
 
23
60
  data = {
@@ -26,22 +63,18 @@ def tab_list(context, page, opts) -> str:
26
63
  "actions_list": context.get("actions_list"),
27
64
  "actions_items": context.get("actions_items"),
28
65
  "is_popup": context.get("is_popup"),
66
+ "tabs_list": _get_tabs_list(context, page, opts),
29
67
  }
30
68
 
31
- for tab in context.get("tab_list", []):
32
- if str(opts) in tab["models"]:
33
- tabs_list = tab["items"]
34
- break
35
-
36
- if page == "changelist":
37
- data["tabs_list"] = tabs_list
38
-
39
- for inline in context.get("inline_admin_formsets", []):
40
- if hasattr(inline.opts, "tab"):
41
- inlines_list.append(inline)
69
+ # If the changeform is rendered and there are no custom tab navigation
70
+ # specified, check for inlines to put into tabs
71
+ if page == "changeform" and len(data["tabs_list"]) == 0:
72
+ for inline in context.get("inline_admin_formsets", []):
73
+ if opts and hasattr(inline.opts, "tab"):
74
+ inlines_list.append(inline)
42
75
 
43
- if page == "changeform" and len(inlines_list) > 0:
44
- data["inlines_list"] = inlines_list
76
+ if len(inlines_list) > 0:
77
+ data["inlines_list"] = inlines_list
45
78
 
46
79
  return render_to_string(
47
80
  "unfold/helpers/tab_list.html",
@@ -70,7 +103,7 @@ def index(indexable: Mapping[int, Any], i: int) -> Any:
70
103
 
71
104
 
72
105
  @register.filter
73
- def tabs(adminform: AdminForm) -> List[Fieldset]:
106
+ def tabs(adminform: AdminForm) -> list[Fieldset]:
74
107
  result = []
75
108
 
76
109
  for fieldset in adminform:
@@ -86,7 +119,7 @@ class CaptureNode(Node):
86
119
  self.varname = varname
87
120
  self.silent = silent
88
121
 
89
- def render(self, context: Dict[str, Any]) -> Union[str, SafeText]:
122
+ def render(self, context: dict[str, Any]) -> Union[str, SafeText]:
90
123
  output = self.nodelist.render(context)
91
124
  context[self.varname] = output
92
125
  if self.silent:
@@ -155,7 +188,7 @@ class RenderComponentNode(template.Node):
155
188
  self,
156
189
  template_name: str,
157
190
  nodelist: NodeList,
158
- extra_context: Optional[Dict] = None,
191
+ extra_context: Optional[dict] = None,
159
192
  include_context: bool = False,
160
193
  *args,
161
194
  **kwargs,
@@ -252,7 +285,7 @@ def add_css_class(field: Field, classes: Union[list, tuple]) -> Field:
252
285
  takes_context=True,
253
286
  name="preserve_filters",
254
287
  )
255
- def preserve_changelist_filters(context: Context) -> Dict[str, Dict[str, str]]:
288
+ def preserve_changelist_filters(context: Context) -> dict[str, dict[str, str]]:
256
289
  """
257
290
  Generate hidden input fields to preserve filters for POST forms.
258
291
  """
@@ -262,10 +295,10 @@ def preserve_changelist_filters(context: Context) -> Dict[str, Dict[str, str]]:
262
295
  if not request or not changelist:
263
296
  return {"params": {}}
264
297
 
265
- used_params: Set[str] = {
298
+ used_params: set[str] = {
266
299
  param for spec in changelist.filter_specs for param in spec.used_parameters
267
300
  }
268
- preserved_params: Dict[str, str] = {
301
+ preserved_params: dict[str, str] = {
269
302
  param: value for param, value in request.GET.items() if param not in used_params
270
303
  }
271
304
 
@@ -1,5 +1,5 @@
1
1
  import datetime
2
- from typing import Any, Dict, Optional, Union
2
+ from typing import Any, Optional, Union
3
3
 
4
4
  from django.contrib.admin.templatetags.admin_list import (
5
5
  ResultList,
@@ -355,7 +355,7 @@ def results(cl: ChangeList):
355
355
  yield UnfoldResultList(pk_value, None, items_for_result(cl, res, None))
356
356
 
357
357
 
358
- def result_list(context: Dict[str, Any], cl: ChangeList) -> Dict[str, Any]:
358
+ def result_list(context: dict[str, Any], cl: ChangeList) -> dict[str, Any]:
359
359
  """
360
360
  Display the headers and data list together.
361
361
  """
unfold/typing.py CHANGED
@@ -1,4 +1,5 @@
1
- from typing import Any, Dict, Iterable, List, Protocol, Tuple, Union
1
+ from collections.abc import Iterable
2
+ from typing import Any, Protocol, Union
2
3
 
3
4
 
4
5
  class ActionFunction(Protocol):
@@ -11,13 +12,13 @@ class ActionFunction(Protocol):
11
12
  allowed_permissions: Iterable[str]
12
13
  short_description: str
13
14
  url_path: str
14
- attrs: Dict[str, Any]
15
+ attrs: dict[str, Any]
15
16
 
16
17
  def __call__(self, *args, **kwargs):
17
18
  pass
18
19
 
19
20
 
20
21
  FieldsetsType = Union[
21
- List[Tuple[Union[str, None], Dict[str, Any]]],
22
- Tuple[Tuple[Union[str, None], Dict[str, Any]]],
22
+ list[tuple[Union[str, None], dict[str, Any]]],
23
+ tuple[tuple[Union[str, None], dict[str, Any]]],
23
24
  ]
unfold/utils.py CHANGED
@@ -1,7 +1,8 @@
1
1
  import datetime
2
2
  import decimal
3
3
  import json
4
- from typing import Any, Iterable, List, Optional
4
+ from collections.abc import Iterable
5
+ from typing import Any, Optional
5
6
 
6
7
  from django.conf import settings
7
8
  from django.db import models
@@ -114,7 +115,7 @@ def display_for_field(value: Any, field: Any, empty_value_display: str) -> str:
114
115
  return display_for_value(value, empty_value_display)
115
116
 
116
117
 
117
- def hex_to_rgb(hex_color: str) -> List[int]:
118
+ def hex_to_rgb(hex_color: str) -> list[int]:
118
119
  hex_color = hex_color.lstrip("#")
119
120
 
120
121
  r = int(hex_color[0:2], 16)
unfold/views.py CHANGED
@@ -1,4 +1,4 @@
1
- from typing import Any, Dict
1
+ from typing import Any
2
2
 
3
3
  from django.contrib.auth.mixins import PermissionRequiredMixin
4
4
 
@@ -16,7 +16,7 @@ class UnfoldModelAdminViewMixin(PermissionRequiredMixin):
16
16
  self.model_admin = model_admin
17
17
  super().__init__(**kwargs)
18
18
 
19
- def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
19
+ def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
20
20
  if not hasattr(self, "model_admin"):
21
21
  raise UnfoldException(
22
22
  "UnfoldModelAdminViewMixin was not provided with 'model_admin' argument"