django-unfold 0.46.0__py3-none-any.whl → 0.48.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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"