django-unfold 0.26.0__py3-none-any.whl → 0.28.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 (33) hide show
  1. {django_unfold-0.26.0.dist-info → django_unfold-0.28.0.dist-info}/METADATA +54 -5
  2. {django_unfold-0.26.0.dist-info → django_unfold-0.28.0.dist-info}/RECORD +33 -26
  3. unfold/admin.py +6 -157
  4. unfold/contrib/forms/templates/unfold/forms/array.html +31 -0
  5. unfold/contrib/forms/widgets.py +58 -3
  6. unfold/contrib/import_export/forms.py +21 -7
  7. unfold/contrib/import_export/templates/admin/import_export/change_list_export.html +5 -0
  8. unfold/contrib/import_export/templates/admin/import_export/export.html +1 -1
  9. unfold/dataclasses.py +2 -0
  10. unfold/decorators.py +3 -0
  11. unfold/fields.py +208 -0
  12. unfold/static/unfold/css/styles.css +1 -1
  13. unfold/templates/admin/change_form.html +0 -2
  14. unfold/templates/admin/edit_inline/tabular.html +4 -6
  15. unfold/templates/admin/includes/fieldset.html +2 -32
  16. unfold/templates/admin/login.html +4 -0
  17. unfold/templates/admin/submit_line.html +1 -1
  18. unfold/templates/unfold/helpers/attrs.html +1 -0
  19. unfold/templates/unfold/helpers/display_header.html +1 -1
  20. unfold/templates/unfold/helpers/field_readonly.html +1 -3
  21. unfold/templates/unfold/helpers/field_readonly_value.html +1 -0
  22. unfold/templates/unfold/helpers/fieldset_row.html +53 -0
  23. unfold/templates/unfold/helpers/search_results.html +10 -10
  24. unfold/templates/unfold/layouts/base.html +11 -0
  25. unfold/templates/unfold/layouts/base_simple.html +7 -1
  26. unfold/templates/unfold/widgets/clearable_file_input.html +1 -1
  27. unfold/templates/unfold/widgets/clearable_file_input_small.html +1 -1
  28. unfold/templates/unfold/widgets/foreign_key_raw_id.html +7 -13
  29. unfold/utils.py +21 -1
  30. unfold/views.py +18 -6
  31. unfold/widgets.py +12 -1
  32. {django_unfold-0.26.0.dist-info → django_unfold-0.28.0.dist-info}/LICENSE.md +0 -0
  33. {django_unfold-0.26.0.dist-info → django_unfold-0.28.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,53 @@
1
+ <div class="form-row
2
+ {% if adminform.model_admin.compressed_fields %} border-b border-gray-200 -mx-3 px-3 pt-3 first:pt-0 dark:border-gray-800 last:border-b-0{% endif %}
3
+ {% if not line.fields|length == 1 %} flex flex-row flex-wrap gap-x-8{% endif %}
4
+ {% if not line.has_visible_field %} hidden{% endif %}
5
+ {% for field in line %}{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% endfor %}">
6
+ {% for field in line %}
7
+ <div class="flex group {% if not line.fields|length == 1 and not adminform.model_admin.compressed_fields %} lg:max-w-xs flex-grow{% endif %}{% if field.errors %} errors {% endif %}{% if not forloop.parentloop.last %} {% if adminform.model_admin.compressed_fields %}mb-3{% else %}mb-6{% endif %}{% else %} pb-3{% endif %} {% if adminform.model_admin.compressed_fields %}flex-col lg:flex-row lg:gap-2 {% else %}flex-col{% endif %}">
8
+ {% if field.is_checkbox %}
9
+ <div class="flex flex-row">
10
+ {{ field.field }}
11
+
12
+ <div class="flex flex-col">
13
+ {{ field.label_tag }}
14
+
15
+ {% if field.field.help_text %}
16
+ <div class="ml-2 -mt-1">
17
+ {% include "unfold/helpers/help_text.html" with help_text=field.field.help_text %}
18
+ </div>
19
+ {% endif %}
20
+
21
+ {% if field.errors %}
22
+ <span class="mt-1 text-red-600 text-sm dark:text-red-500">
23
+ {{ field.errors }}
24
+ </span>
25
+ {% endif %}
26
+ </div>
27
+ </div>
28
+ {% else %}
29
+ <div class="{% if adminform.model_admin.compressed_fields %} min-w-48 mt-2 w-48{% endif %}">
30
+ {{ field.label_tag }}
31
+ </div>
32
+
33
+ <div class="flex-grow">
34
+ {% if field.is_readonly %}
35
+ {% include "unfold/helpers/field_readonly_value.html" %}
36
+ {% else %}
37
+ {{ field.field }}
38
+ {% endif %}
39
+
40
+ {% if field.field.help_text and not field.is_checkbox %}
41
+ {% include "unfold/helpers/help_text.html" with help_text=field.field.help_text %}
42
+ {% endif %}
43
+
44
+ {% if field.errors %}
45
+ <span class="mt-1 text-red-600 text-sm dark:text-red-500">
46
+ {{ field.errors }}
47
+ </span>
48
+ {% endif %}
49
+ </div>
50
+ {% endif %}
51
+ </div>
52
+ {% endfor %}
53
+ </div>
@@ -1,13 +1,13 @@
1
1
  {% if results %}
2
- <ul class="absolute bg-white border left-0 mt-12 right-0 rounded top-0 shadow-sm text-sm dark:bg-gray-800 dark:border-gray-700">
3
- {% for app in results %}
4
- {% for model in app.models %}
5
- <li>
6
- <a href="{{ model.admin_url }}" class="block group overflow-hidden px-3 py-2 text-gray-500 text-ellipsis whitespace-nowrap hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
7
- {{ app.name }} <span class="align-text-top material-symbols-outlined md-18 text-gray-300 group-hover:text-gray-400 dark:text-gray-600">arrow_right_alt</span> {{ model.name }}
8
- </a>
9
- </li>
2
+ <ul class="absolute bg-white border left-0 mt-12 right-0 rounded top-0 shadow-sm text-sm z-10 dark:bg-gray-800 dark:border-gray-700">
3
+ {% for app in results %}
4
+ {% for model in app.models %}
5
+ <li>
6
+ <a href="{{ model.admin_url }}" class="block group overflow-hidden px-3 py-2 text-gray-500 text-ellipsis whitespace-nowrap hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
7
+ {{ app.name }} <span class="align-text-top material-symbols-outlined md-18 text-gray-300 group-hover:text-gray-400 dark:text-gray-600">arrow_right_alt</span> {{ model.name }}
8
+ </a>
9
+ </li>
10
+ {% endfor %}
10
11
  {% endfor %}
11
- {% endfor %}
12
- </ul>
12
+ </ul>
13
13
  {% endif %}
@@ -0,0 +1,11 @@
1
+ {% extends "unfold/layouts/base_simple.html" %}
2
+
3
+ {% block branding %}
4
+ <h1 id="site-name">
5
+ <a href="{% url 'admin:index' %}">
6
+ {{ site_header|default:_('Django administration') }}
7
+ </a>
8
+ </h1>
9
+ {% endblock %}
10
+
11
+ {% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
@@ -15,7 +15,13 @@
15
15
  {% include "unfold/helpers/header.html" %}
16
16
  {% endblock %}
17
17
 
18
- <div class="p-4 lg:p-12">
18
+ {% if not is_popup %}
19
+ {% spaceless %}
20
+ {% block breadcrumbs %}{% endblock %}
21
+ {% endspaceless %}
22
+ {% endif %}
23
+
24
+ <div class="px-4 lg:px-12">
19
25
  <div id="content" class="container mx-auto {% block coltype %}colM{% endblock %}">
20
26
  {% block content %}
21
27
  {% block object-tools %}{% endblock %}
@@ -1,7 +1,7 @@
1
1
  {% load i18n %}
2
2
 
3
3
  {% if widget.attrs.accept == 'image/*' and widget.is_initial %}
4
- <div class="bg-gray-50 border flex items-center justify-center mb-4 min-h-32 p-1 rounded-md w-48 dark:bg-white/[.02] dark:border-gray-700">
4
+ <div class="mb-4 max-w-48">
5
5
  <a href="{{ widget.value.url }}" target="_blank">
6
6
  <img src="{{ widget.value.url }}" alt="{% trans 'Image preview' %}" class="block rounded" />
7
7
  </a>
@@ -17,7 +17,7 @@
17
17
  <input type="text" 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 {% if widget.value %}text-gray-500 dark:text-gray-400{% else %}text-gray-300 dark:text-gray-400{% endif %}">
18
18
 
19
19
  <div class="flex flex-none items-center leading-none self-stretch">
20
- <input id="{{ widget.name }}" type="{{ widget.type }}" name="{{ widget.name }}" class="opacity-0 pointer-events-none" {% include "django/forms/widgets/attrs.html" %} />
20
+ <input id="{{ widget.name }}" type="{{ widget.type }}" name="{{ widget.name }}" class="{{ widget.file_input_class }}" {% include "django/forms/widgets/attrs.html" %} />
21
21
 
22
22
  <label for="{{ widget.name }}" class="cursor-pointer text-gray-400 px-3 hover:text-gray-700 dark:text-gray-500 dark:hover:text-gray-200">
23
23
  <span class="material-symbols-outlined">file_upload</span>
@@ -2,20 +2,14 @@
2
2
  {% include 'django/forms/widgets/input.html' %}
3
3
 
4
4
  {% if related_url %}
5
- <a href="{{ related_url }}" class="related-lookup related-widget-wrapper-link view-related bg-white border cursor-pointer flex items-center h-9.5 justify-center ml-2 rounded shadow-sm shrink-0 text-gray-400 text-sm w-9.5 hover:text-gray-700 dark:bg-gray-900 dark:border-gray-700 dark:text-gray-500 dark:hover:text-gray-200" id="lookup_id_{{ widget.name }}" title="{{ link_title }}">
5
+ <a href="{{ related_url }}" class="related-lookup related-widget-wrapper-link view-related bg-white border cursor-pointer flex items-center h-9.5 justify-center ml-2 rounded shadow-sm shrink-0 text-gray-400 text-sm w-9.5 hover:text-gray-700 dark:bg-gray-900 dark:border-gray-700 dark:text-gray-500 dark:hover:text-gray-200" id="lookup_id_{{ widget.name }}">
6
6
  <span class="material-symbols-outlined text-sm">search</span>
7
7
  </a>
8
8
  {% endif %}
9
- </div>
10
9
 
11
- {% if link_label %}
12
- <strong class="mt-2 font-medium text-xs">
13
- {% if link_url %}
14
- <a href="{{ link_url }}" class="underline text-primary-500">
15
- {{ link_label }}
16
- </a>
17
- {% else %}
18
- {{ link_label }}
19
- {% endif %}
20
- </strong>
21
- {% endif %}
10
+ {% if link_label and link_url %}
11
+ <a href="{{ link_url }}" title="{{ link_label }}" class="bg-white border cursor-pointer flex items-center h-9.5 justify-center ml-2 rounded shadow-sm shrink-0 text-gray-400 text-sm w-9.5 hover:text-gray-700 dark:bg-gray-900 dark:border-gray-700 dark:text-gray-500 dark:hover:text-gray-200">
12
+ <span class="material-symbols-outlined text-sm">visibility</span>
13
+ </a>
14
+ {% endif %}
15
+ </div>
unfold/utils.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import datetime
2
2
  import decimal
3
3
  import json
4
- from typing import Any, Iterable, List
4
+ from typing import Any, Iterable, List, Optional
5
5
 
6
6
  from django.db import models
7
7
  from django.template.loader import render_to_string
@@ -121,3 +121,23 @@ def hex_to_rgb(hex_color: str) -> List[int]:
121
121
  b = int(hex_color[4:6], 16)
122
122
 
123
123
  return (r, g, b)
124
+
125
+
126
+ def prettify_json(data: Any) -> Optional[str]:
127
+ try:
128
+ from pygments import highlight
129
+ from pygments.formatters import HtmlFormatter
130
+ from pygments.lexers import JsonLexer
131
+ except ImportError:
132
+ return None
133
+
134
+ def format_response(response: str, theme: str) -> str:
135
+ formatter = HtmlFormatter(style=theme, noclasses=True, nobackground=True)
136
+ return highlight(response, JsonLexer(), formatter)
137
+
138
+ response = json.dumps(data, sort_keys=True, indent=4)
139
+
140
+ return mark_safe(
141
+ f'<div class="block dark:hidden">{format_response(response, "colorful")}</div>'
142
+ f'<div class="hidden dark:block">{format_response(response, "monokai")}</div>'
143
+ )
unfold/views.py CHANGED
@@ -10,13 +10,25 @@ class UnfoldModelAdminViewMixin(PermissionRequiredMixin):
10
10
  Prepares views to be displayed in admin
11
11
  """
12
12
 
13
- def get_context_data(self, **kwargs) -> Dict[str, Any]:
14
- if "model_admin" not in self.kwargs:
13
+ model_admin = None
14
+
15
+ def __init__(self, model_admin, **kwargs):
16
+ self.model_admin = model_admin
17
+ super().__init__(**kwargs)
18
+
19
+ def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
20
+ if not hasattr(self, "model_admin"):
15
21
  raise UnfoldException(
16
22
  "UnfoldModelAdminViewMixin was not provided with 'model_admin' argument"
17
23
  )
18
- model_admin = self.kwargs["model_admin"]
19
- context_data = super().get_context_data(
20
- **kwargs, **model_admin.admin_site.each_context(self.request)
24
+
25
+ if not hasattr(self, "title"):
26
+ raise UnfoldException(
27
+ "UnfoldModelAdminViewMixin was not provided with 'title' attribute"
28
+ )
29
+
30
+ return super().get_context_data(
31
+ **kwargs,
32
+ **self.model_admin.admin_site.each_context(self.request),
33
+ **{"title": self.title},
21
34
  )
22
- return context_data
unfold/widgets.py CHANGED
@@ -288,7 +288,18 @@ class FileFieldMixin:
288
288
  def get_context(self, name, value, attrs):
289
289
  widget = super().get_context(name, value, attrs)
290
290
  widget["widget"].update(
291
- {"class": " ".join([*CHECKBOX_CLASSES, *["form-check-input"]])}
291
+ {
292
+ "class": " ".join([*CHECKBOX_CLASSES, *["form-check-input"]]),
293
+ "file_input_class": " ".join(
294
+ [
295
+ self.attrs.get("class", ""),
296
+ *[
297
+ "opacity-0",
298
+ "pointer-events-none",
299
+ ],
300
+ ]
301
+ ),
302
+ }
292
303
  )
293
304
  return widget
294
305