django-unfold 0.41.0__py3-none-any.whl → 0.43.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 (63) hide show
  1. {django_unfold-0.41.0.dist-info → django_unfold-0.43.0.dist-info}/METADATA +1 -1
  2. {django_unfold-0.41.0.dist-info → django_unfold-0.43.0.dist-info}/RECORD +63 -55
  3. unfold/admin.py +8 -1
  4. unfold/components.py +47 -0
  5. unfold/contrib/filters/admin.py +23 -33
  6. unfold/contrib/guardian/templates/admin/guardian/model/obj_perms_manage.html +1 -1
  7. unfold/contrib/guardian/templates/admin/guardian/model/obj_perms_manage_group.html +1 -1
  8. unfold/contrib/guardian/templates/admin/guardian/model/obj_perms_manage_user.html +1 -1
  9. unfold/contrib/import_export/templates/admin/import_export/export.html +1 -1
  10. unfold/contrib/import_export/templates/admin/import_export/import.html +1 -1
  11. unfold/contrib/import_export/templates/admin/import_export/import_validation.html +26 -21
  12. unfold/contrib/import_export/templates/admin/import_export/resource_fields_list.html +5 -3
  13. unfold/contrib/simple_history/templates/simple_history/object_history_form.html +1 -1
  14. unfold/contrib/simple_history/templates/simple_history/submit_line.html +1 -1
  15. unfold/settings.py +1 -0
  16. unfold/sites.py +25 -5
  17. unfold/static/unfold/css/styles.css +1 -1
  18. unfold/static/unfold/js/app.js +21 -15
  19. unfold/styles.css +14 -1
  20. unfold/templates/admin/base.html +3 -3
  21. unfold/templates/admin/change_form.html +9 -1
  22. unfold/templates/admin/change_list.html +1 -1
  23. unfold/templates/admin/date_hierarchy.html +5 -5
  24. unfold/templates/admin/delete_confirmation.html +1 -1
  25. unfold/templates/admin/delete_selected_confirmation.html +1 -1
  26. unfold/templates/admin/edit_inline/stacked.html +8 -2
  27. unfold/templates/admin/edit_inline/tabular.html +8 -2
  28. unfold/templates/admin/nav_sidebar.html +1 -10
  29. unfold/templates/admin/object_history.html +1 -1
  30. unfold/templates/admin/submit_line.html +2 -2
  31. unfold/templates/unfold/change_list_filter.html +5 -13
  32. unfold/templates/unfold/components/card.html +1 -1
  33. unfold/templates/unfold/components/chart/cohort.html +59 -0
  34. unfold/templates/unfold/components/navigation.html +1 -1
  35. unfold/templates/unfold/components/tracker.html +5 -0
  36. unfold/templates/unfold/helpers/account_links.html +1 -1
  37. unfold/templates/unfold/helpers/actions_row.html +9 -7
  38. unfold/templates/unfold/helpers/field_readonly.html +1 -1
  39. unfold/templates/unfold/helpers/form_label.html +1 -1
  40. unfold/templates/unfold/helpers/header.html +1 -1
  41. unfold/templates/unfold/helpers/label.html +1 -1
  42. unfold/templates/unfold/helpers/language_switch.html +27 -0
  43. unfold/templates/unfold/helpers/messages/debug.html +3 -0
  44. unfold/templates/unfold/helpers/messages/error.html +5 -3
  45. unfold/templates/unfold/helpers/messages/info.html +2 -2
  46. unfold/templates/unfold/helpers/messages/success.html +3 -0
  47. unfold/templates/unfold/helpers/messages/warning.html +3 -0
  48. unfold/templates/unfold/helpers/messages.html +13 -14
  49. unfold/templates/unfold/helpers/tab_list.html +3 -3
  50. unfold/templates/unfold/helpers/theme_switch.html +1 -1
  51. unfold/templates/unfold/helpers/userlinks.html +4 -0
  52. unfold/templates/unfold/helpers/welcomemsg.html +9 -1
  53. unfold/templates/unfold/layouts/base_simple.html +1 -1
  54. unfold/templates/unfold/layouts/skeleton.html +1 -1
  55. unfold/templates/unfold/templatetags/preserve_changelist_filters.html +3 -0
  56. unfold/templates/unfold/widgets/clearable_file_input.html +1 -1
  57. unfold/templates/unfold/widgets/clearable_file_input_small.html +1 -1
  58. unfold/templates/unfold/widgets/split_datetime_vertical.html +1 -1
  59. unfold/templatetags/unfold.py +55 -9
  60. unfold/utils.py +17 -0
  61. unfold/widgets.py +6 -2
  62. {django_unfold-0.41.0.dist-info → django_unfold-0.43.0.dist-info}/LICENSE.md +0 -0
  63. {django_unfold-0.41.0.dist-info → django_unfold-0.43.0.dist-info}/WHEEL +0 -0
@@ -8,7 +8,7 @@
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-gray-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-gray-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-gray-700 dark:hover:text-gray-200{% endif %}">
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-gray-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 %}">
12
12
  {{ item.title }}
13
13
  </a>
14
14
  </li>
@@ -20,7 +20,7 @@
20
20
  <a class="block cursor-pointer font-medium px-3 py-2 md:py-4 md:px-0"
21
21
  href="#general"
22
22
  x-on:click="activeTab = 'general'"
23
- x-bind:class="{'border-b border-gray-200 dark:border-gray-800 md:border-primary-500 dark:md:!border-primary-600 font-semibold -mb-px text-primary-600 dark:text-primary-500': activeTab == 'general', 'hover:text-gray-700 dark:hover:text-gray-200 dark:border-gray-800': activeTab != 'general'}">
23
+ x-bind:class="{'border-b border-gray-200 dark:border-gray-800 md:border-primary-500 dark:md:!border-primary-600 font-semibold -mb-px text-primary-600 dark:text-primary-500': activeTab == 'general', 'hover:text-primary-600 dark:hover:text-primary-500 dark:border-gray-800': activeTab != 'general'}">
24
24
  {% trans "General" %}
25
25
  </a>
26
26
  </li>
@@ -30,7 +30,7 @@
30
30
  <a class="block cursor-pointer font-medium px-3 py-2 md:py-4 md:px-0"
31
31
  href="#{{ inline.opts.verbose_name|slugify }}"
32
32
  x-on:click="activeTab = '{{ inline.opts.verbose_name|slugify }}'"
33
- x-bind:class="{'border-b border-gray-200 dark:border-gray-800 md:border-primary-500 dark:md:!border-primary-600 font-semibold -mb-px text-primary-600 dark:text-primary-500': activeTab == '{{ inline.opts.verbose_name|slugify }}', 'hover:text-gray-700 dark:hover:text-gray-200 dark:border-gray-800': activeTab != '{{ inline.opts.verbose_name|slugify }}'}">
33
+ x-bind:class="{'border-b border-gray-200 dark:border-gray-800 md:border-primary-500 dark:md:!border-primary-600 font-semibold -mb-px text-primary-600 dark:text-primary-500': activeTab == '{{ inline.opts.verbose_name|slugify }}', 'hover:text-primary-600 dark:hover:text-primary-500 dark:border-gray-800': activeTab != '{{ inline.opts.verbose_name|slugify }}'}">
34
34
  {% if inline.formset.max_num == 1 %}
35
35
  {{ inline.opts.verbose_name|capfirst }}
36
36
  {% else %}
@@ -7,7 +7,7 @@
7
7
  </span>
8
8
  </a>
9
9
 
10
- <nav class="absolute bg-white border flex flex-col leading-none overflow-hidden py-1 -right-2 rounded shadow-lg top-7 w-40 z-50 dark:bg-gray-800 dark:border-gray-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-gray-800 dark:border-gray-700" x-cloak x-show="openTheme" @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-gray-100 hover:text-gray-700 dark:hover:bg-gray-700 dark:hover:text-gray-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'">
@@ -7,6 +7,10 @@
7
7
  {% include "unfold/helpers/label.html" with text=environment.0 type=environment.1 %}
8
8
  {% endif %}
9
9
 
10
+ {% if show_languages %}
11
+ {% include "unfold/helpers/language_switch.html" %}
12
+ {% endif %}
13
+
10
14
  {% if not theme %}
11
15
  {% include "unfold/helpers/theme_switch.html" %}
12
16
  {% endif %}
@@ -1,6 +1,14 @@
1
1
  {% load i18n %}
2
2
 
3
- <div class="flex-grow font-semibold min-w-0 mr-3">
3
+ <div class="flex flex-row flex-grow font-semibold items-center min-w-0 mr-3">
4
+ <span class="cursor-pointer hidden flex-row items-center text-sm xl:flex">
5
+ <span class="material-symbols-outlined" hx-get="{% url "admin:toggle_sidebar" %}" hx-swap="none" x-on:click="sidebarDesktopOpen = !sidebarDesktopOpen">
6
+ dock_to_right
7
+ </span>
8
+ </span>
9
+
10
+ <span class="hidden bg-gray-200 h-5 mx-3 w-px xl:block dark:bg-gray-700"></span>
11
+
4
12
  <h1 class="overflow-hidden text-ellipsis text-sm whitespace-nowrap xl:text-base text-font-important-light dark:text-font-important-dark">
5
13
  {% if content_title %}
6
14
  <span class="tracking-tight">
@@ -21,7 +21,7 @@
21
21
  {% endspaceless %}
22
22
  {% endif %}
23
23
 
24
- <div class="px-4 lg:px-12">
24
+ <div class="px-4 lg:px-8">
25
25
  <div id="content" class="container mx-auto {% block coltype %}colM{% endblock %}">
26
26
  {% block content %}
27
27
  {% block object-tools %}{% endblock %}
@@ -58,7 +58,7 @@
58
58
 
59
59
  {% if colors %}
60
60
  <style>
61
- html {
61
+ :root {
62
62
  {% for name, weights in colors.items %}
63
63
  {% for weight, value in weights.items %}
64
64
  --color-{{ name }}-{{ weight }}: {{ value }};
@@ -0,0 +1,3 @@
1
+ {% for key, value in params.items %}
2
+ <input type="hidden" name="{{ key }}" value="{{ value }}">
3
+ {% endfor %}
@@ -21,7 +21,7 @@
21
21
  </div>
22
22
  {% endif %}
23
23
 
24
- <input type="text" aria-label="{% trans 'Choose file to upload' %}" value="{% if widget.value %}{{ widget.value.url }}{% else %}{% trans 'Choose file to upload' %}{% endif %}" disabled class="bg-white flex-grow font-medium px-3 py-2 text-ellipsis dark:bg-gray-900">
24
+ <input type="text" aria-label="{% trans 'Choose file to upload' %}" value="{% if widget.value %}{{ widget.value.url }}{% else %}{% trans 'Choose file to upload' %}{% endif %}" disabled class="bg-white flex-grow font-medium min-w-0 px-3 py-2 text-ellipsis dark:bg-gray-900">
25
25
 
26
26
  <div class="flex flex-none items-center leading-none self-stretch">
27
27
  <div class="hidden">
@@ -14,7 +14,7 @@
14
14
  </div>
15
15
  {% endif %}
16
16
 
17
- <input type="text" aria-label="{% trans 'Choose file to upload' %}" value="{% if widget.value %}{{ widget.value.url }}{% else %}{% trans 'Choose file to upload' %}{% endif %}" disabled class="bg-white flex-grow font-medium px-3 py-2 text-ellipsis dark:bg-gray-900">
17
+ <input type="text" aria-label="{% trans 'Choose file to upload' %}" value="{% if widget.value %}{{ widget.value.url }}{% else %}{% trans 'Choose file to upload' %}{% endif %}" disabled class="bg-white flex-grow font-medium min-w-0 px-3 py-2 text-ellipsis dark:bg-gray-900">
18
18
 
19
19
  <div class="flex flex-none items-center leading-none self-stretch">
20
20
  <div class="hidden">
@@ -1,4 +1,4 @@
1
- <div class="datetime flex flex-col max-w-2xl">
1
+ <div class="datetime flex flex-col max-w-2xl w-full">
2
2
  <div class="flex flex-col flex-wrap mb-2">
3
3
  {% if date_label %}
4
4
  <div class="mb-2">
@@ -1,13 +1,17 @@
1
- from typing import Any, Dict, List, Mapping, Optional, Union
1
+ from typing import Any, Dict, List, Mapping, Optional, Set, Union
2
2
 
3
3
  from django import template
4
4
  from django.contrib.admin.helpers import AdminForm, Fieldset
5
+ from django.contrib.admin.views.main import ChangeList
5
6
  from django.forms import Field
6
- from django.template import Library, Node, RequestContext, TemplateSyntaxError
7
+ from django.http import HttpRequest
8
+ from django.template import Context, Library, Node, RequestContext, TemplateSyntaxError
7
9
  from django.template.base import NodeList, Parser, Token, token_kwargs
8
10
  from django.template.loader import render_to_string
9
11
  from django.utils.safestring import SafeText
10
12
 
13
+ from unfold.components import ComponentRegistry
14
+
11
15
  register = Library()
12
16
 
13
17
 
@@ -152,24 +156,37 @@ class RenderComponentNode(template.Node):
152
156
  template_name: str,
153
157
  nodelist: NodeList,
154
158
  extra_context: Optional[Dict] = None,
159
+ include_context: bool = False,
155
160
  *args,
156
161
  **kwargs,
157
162
  ):
158
163
  self.template_name = template_name
159
164
  self.nodelist = nodelist
160
165
  self.extra_context = extra_context or {}
166
+ self.include_context = include_context
161
167
  super().__init__(*args, **kwargs)
162
168
 
163
169
  def render(self, context: RequestContext) -> str:
164
- result = self.nodelist.render(context)
170
+ values = {
171
+ name: var.resolve(context) for name, var in self.extra_context.items()
172
+ }
173
+
174
+ values.update(
175
+ {
176
+ "children": self.nodelist.render(context),
177
+ }
178
+ )
165
179
 
166
- ctx = {name: var.resolve(context) for name, var in self.extra_context.items()}
167
- ctx.update({"children": result})
180
+ if "component_class" in values:
181
+ values = ComponentRegistry.create_instance(
182
+ values["component_class"], request=context.request
183
+ ).get_context_data(**values)
184
+
185
+ if self.include_context:
186
+ values.update(context.flatten())
168
187
 
169
188
  return render_to_string(
170
- self.template_name,
171
- request=context.request,
172
- context=ctx,
189
+ self.template_name, request=context.request, context=values
173
190
  )
174
191
 
175
192
 
@@ -200,17 +217,21 @@ def do_component(parser: Parser, token: Token) -> str:
200
217
  raise TemplateSyntaxError(
201
218
  '"with" in {bits[0]} tag needs at least one keyword argument.'
202
219
  )
220
+ elif option == "include_context":
221
+ value = True
203
222
  else:
204
223
  raise TemplateSyntaxError(f"Unknown argument for {bits[0]} tag: {option}.")
205
224
 
206
225
  options[option] = value
207
226
 
227
+ include_context = options.get("include_context", False)
208
228
  nodelist = parser.parse(("endcomponent",))
209
229
  template_name = bits[1][1:-1]
230
+
210
231
  extra_context = options.get("with", {})
211
232
  parser.next_token()
212
233
 
213
- return RenderComponentNode(template_name, nodelist, extra_context)
234
+ return RenderComponentNode(template_name, nodelist, extra_context, include_context)
214
235
 
215
236
 
216
237
  @register.filter
@@ -224,3 +245,28 @@ def add_css_class(field: Field, classes: Union[list, tuple]) -> Field:
224
245
  field.field.widget.attrs["class"] = classes
225
246
 
226
247
  return field
248
+
249
+
250
+ @register.inclusion_tag(
251
+ "unfold/templatetags/preserve_changelist_filters.html",
252
+ takes_context=True,
253
+ name="preserve_filters",
254
+ )
255
+ def preserve_changelist_filters(context: Context) -> Dict[str, Dict[str, str]]:
256
+ """
257
+ Generate hidden input fields to preserve filters for POST forms.
258
+ """
259
+ request: Optional[HttpRequest] = context.get("request")
260
+ changelist: Optional[ChangeList] = context.get("cl")
261
+
262
+ if not request or not changelist:
263
+ return {"params": {}}
264
+
265
+ used_params: Set[str] = {
266
+ param for spec in changelist.filter_specs for param in spec.used_parameters
267
+ }
268
+ preserved_params: Dict[str, str] = {
269
+ param: value for param, value in request.GET.items() if param not in used_params
270
+ }
271
+
272
+ return {"params": preserved_params}
unfold/utils.py CHANGED
@@ -3,6 +3,7 @@ import decimal
3
3
  import json
4
4
  from typing import Any, Iterable, List, Optional
5
5
 
6
+ from django.conf import settings
6
7
  from django.db import models
7
8
  from django.template.loader import render_to_string
8
9
  from django.utils import formats, timezone
@@ -146,3 +147,19 @@ def prettify_json(data: Any) -> Optional[str]:
146
147
  f'<div class="block dark:hidden">{format_response(response, "colorful")}</div>'
147
148
  f'<div class="hidden dark:block">{format_response(response, "monokai")}</div>'
148
149
  )
150
+
151
+
152
+ def parse_date_str(value: str) -> Optional[datetime.date]:
153
+ for format in settings.DATE_INPUT_FORMATS:
154
+ try:
155
+ return datetime.datetime.strptime(value, format).date()
156
+ except (ValueError, TypeError):
157
+ continue
158
+
159
+
160
+ def parse_datetime_str(value: str) -> Optional[datetime.datetime]:
161
+ for format in settings.DATETIME_INPUT_FORMATS:
162
+ try:
163
+ return datetime.datetime.strptime(value, format)
164
+ except (ValueError, TypeError):
165
+ continue
unfold/widgets.py CHANGED
@@ -35,7 +35,7 @@ from .exceptions import UnfoldException
35
35
 
36
36
  LABEL_CLASSES = [
37
37
  "block",
38
- "font-medium",
38
+ "font-semibold",
39
39
  "mb-2",
40
40
  "text-font-important-light",
41
41
  "text-sm",
@@ -43,7 +43,7 @@ LABEL_CLASSES = [
43
43
  ]
44
44
 
45
45
  CHECKBOX_LABEL_CLASSES = [
46
- "font-medium",
46
+ "font-semibold",
47
47
  "ml-2",
48
48
  "text-sm",
49
49
  "text-font-important-light",
@@ -332,6 +332,8 @@ class UnfoldAdminImageSmallFieldWidget(FileFieldMixin, AdminFileWidget):
332
332
 
333
333
 
334
334
  class UnfoldAdminDateWidget(AdminDateWidget):
335
+ template_name = "unfold/widgets/date.html"
336
+
335
337
  def __init__(
336
338
  self, attrs: Optional[Dict[str, Any]] = None, format: Optional[str] = None
337
339
  ) -> None:
@@ -358,6 +360,8 @@ class UnfoldAdminSingleDateWidget(AdminDateWidget):
358
360
 
359
361
 
360
362
  class UnfoldAdminTimeWidget(AdminTimeWidget):
363
+ template_name = "unfold/widgets/time.html"
364
+
361
365
  def __init__(
362
366
  self, attrs: Optional[Dict[str, Any]] = None, format: Optional[str] = None
363
367
  ) -> None: