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
@@ -54,7 +54,7 @@
54
54
 
55
55
  <fieldset class="border border-gray-200 mb-4 rounded-md pt-3 px-3 shadow-sm dark:border-gray-800">
56
56
  {% for field in form.visible_fields %}
57
- <div {% if field.field.is_selectable_field %}class="selectable-field-export-row" resource-index="{{ field.field.resource_index }}"{% else %}class="form-row aligned"{% endif %}>
57
+ <div {% if field.field.is_selectable_field %}class="selectable-field-export-row" resource-index="{{ field.field.resource_index }}"{% endif %}>
58
58
  {% if field.field.initial_field %}
59
59
  <p class="block font-medium mb-2 text-gray-900 text-sm dark:text-gray-200">
60
60
  {% trans "This exporter will export the following fields" %}
unfold/dataclasses.py CHANGED
@@ -1,4 +1,5 @@
1
1
  from dataclasses import dataclass
2
+ from typing import Dict, Optional
2
3
 
3
4
  from .typing import ActionFunction
4
5
 
@@ -9,3 +10,4 @@ class UnfoldAction:
9
10
  method: ActionFunction
10
11
  description: str
11
12
  path: str
13
+ attrs: Optional[Dict] = None
unfold/decorators.py CHANGED
@@ -58,6 +58,7 @@ def display(
58
58
  function: Optional[Callable[[Model], Any]] = None,
59
59
  *,
60
60
  boolean: Optional[bool] = None,
61
+ image: Optional[bool] = None,
61
62
  ordering: Optional[Union[str, Combinable, BaseExpression]] = None,
62
63
  description: Optional[str] = None,
63
64
  empty_value: Optional[str] = None,
@@ -72,6 +73,8 @@ def display(
72
73
  )
73
74
  if boolean is not None:
74
75
  func.boolean = boolean
76
+ if image is not None:
77
+ func.image = image
75
78
  if ordering is not None:
76
79
  func.admin_order_field = ordering
77
80
  if description is not None:
unfold/fields.py ADDED
@@ -0,0 +1,208 @@
1
+ from django.contrib.admin import helpers
2
+ from django.contrib.admin.utils import lookup_field, quote
3
+ from django.core.exceptions import ObjectDoesNotExist
4
+ from django.db import models
5
+ from django.db.models import (
6
+ ForeignObjectRel,
7
+ ImageField,
8
+ JSONField,
9
+ ManyToManyRel,
10
+ OneToOneField,
11
+ )
12
+ from django.forms.utils import flatatt
13
+ from django.template.defaultfilters import linebreaksbr
14
+ from django.urls import NoReverseMatch, reverse
15
+ from django.utils.html import conditional_escape, format_html
16
+ from django.utils.module_loading import import_string
17
+ from django.utils.safestring import SafeText, mark_safe
18
+ from django.utils.text import capfirst
19
+
20
+ from .settings import get_config
21
+ from .utils import display_for_field, prettify_json
22
+ from .widgets import CHECKBOX_LABEL_CLASSES, LABEL_CLASSES
23
+
24
+
25
+ class UnfoldAdminReadonlyField(helpers.AdminReadonlyField):
26
+ def label_tag(self) -> SafeText:
27
+ from .admin import ModelAdmin, ModelAdminMixin
28
+
29
+ if not isinstance(self.model_admin, ModelAdmin) and not isinstance(
30
+ self.model_admin, ModelAdminMixin
31
+ ):
32
+ return super().label_tag()
33
+
34
+ attrs = {
35
+ "class": " ".join(LABEL_CLASSES + ["mb-2"]),
36
+ }
37
+
38
+ label = self.field["label"]
39
+
40
+ return format_html(
41
+ "<label{}>{}{}</label>",
42
+ flatatt(attrs),
43
+ capfirst(label),
44
+ self.form.label_suffix,
45
+ )
46
+
47
+ def is_json(self) -> bool:
48
+ field, obj, model_admin = (
49
+ self.field["field"],
50
+ self.form.instance,
51
+ self.model_admin,
52
+ )
53
+
54
+ try:
55
+ f, attr, value = lookup_field(field, obj, model_admin)
56
+ except (AttributeError, ValueError, ObjectDoesNotExist):
57
+ return False
58
+
59
+ return isinstance(f, JSONField)
60
+
61
+ def is_image(self) -> bool:
62
+ field, obj, model_admin = (
63
+ self.field["field"],
64
+ self.form.instance,
65
+ self.model_admin,
66
+ )
67
+
68
+ try:
69
+ f, attr, value = lookup_field(field, obj, model_admin)
70
+ except (AttributeError, ValueError, ObjectDoesNotExist):
71
+ return False
72
+
73
+ if hasattr(attr, "image"):
74
+ return attr.image
75
+ elif (
76
+ isinstance(attr, property)
77
+ and hasattr(attr, "fget")
78
+ and hasattr(attr.fget, "image")
79
+ ):
80
+ return attr.fget.image
81
+
82
+ return isinstance(f, ImageField)
83
+
84
+ def contents(self) -> str:
85
+ contents = self._get_contents()
86
+ contents = self._preprocess_field(contents)
87
+ return contents
88
+
89
+ def get_admin_url(self, remote_field, remote_obj):
90
+ url_name = f"admin:{remote_field.model._meta.app_label}_{remote_field.model._meta.model_name}_change"
91
+ try:
92
+ url = reverse(
93
+ url_name,
94
+ args=[quote(remote_obj.pk)],
95
+ current_app=self.model_admin.admin_site.name,
96
+ )
97
+ return format_html(
98
+ '<a href="{}" class="text-primary-600 underline whitespace-nowrap">{}</a>',
99
+ url,
100
+ remote_obj,
101
+ )
102
+ except NoReverseMatch:
103
+ return str(remote_obj)
104
+
105
+ def _get_contents(self) -> str:
106
+ from django.contrib.admin.templatetags.admin_list import _boolean_icon
107
+
108
+ field, obj, model_admin = (
109
+ self.field["field"],
110
+ self.form.instance,
111
+ self.model_admin,
112
+ )
113
+ try:
114
+ f, attr, value = lookup_field(field, obj, model_admin)
115
+ except (AttributeError, ValueError, ObjectDoesNotExist):
116
+ result_repr = self.empty_value_display
117
+ else:
118
+ if field in self.form.fields:
119
+ widget = self.form[field].field.widget
120
+ # This isn't elegant but suffices for contrib.auth's
121
+ # ReadOnlyPasswordHashWidget.
122
+ if getattr(widget, "read_only", False):
123
+ return widget.render(field, value)
124
+
125
+ if f is None:
126
+ if getattr(attr, "boolean", False):
127
+ result_repr = _boolean_icon(value)
128
+ else:
129
+ if hasattr(value, "__html__"):
130
+ result_repr = value
131
+ else:
132
+ result_repr = linebreaksbr(value)
133
+ else:
134
+ if isinstance(f.remote_field, ManyToManyRel) and value is not None:
135
+ result_repr = ", ".join(map(str, value.all()))
136
+ elif (
137
+ isinstance(f.remote_field, (ForeignObjectRel, OneToOneField))
138
+ and value is not None
139
+ ):
140
+ result_repr = self.get_admin_url(f.remote_field, value)
141
+ elif isinstance(f, models.JSONField):
142
+ formatted_output = prettify_json(value)
143
+
144
+ if formatted_output:
145
+ return formatted_output
146
+
147
+ result_repr = display_for_field(value, f, self.empty_value_display)
148
+ return conditional_escape(result_repr)
149
+ elif isinstance(f, models.URLField):
150
+ return format_html(
151
+ '<a href="{}" class="text-primary-600 underline whitespace-nowrap">{}</a>',
152
+ value,
153
+ value,
154
+ )
155
+ else:
156
+ result_repr = display_for_field(value, f, self.empty_value_display)
157
+ return conditional_escape(result_repr)
158
+ result_repr = linebreaksbr(result_repr)
159
+ return conditional_escape(result_repr)
160
+
161
+ def _preprocess_field(self, contents: str) -> str:
162
+ if (
163
+ hasattr(self.model_admin, "readonly_preprocess_fields")
164
+ and self.field["field"] in self.model_admin.readonly_preprocess_fields
165
+ ):
166
+ func = self.model_admin.readonly_preprocess_fields[self.field["field"]]
167
+ if isinstance(func, str):
168
+ contents = import_string(func)(contents)
169
+ elif callable(func):
170
+ contents = func(contents)
171
+
172
+ return contents
173
+
174
+
175
+ class UnfoldAdminField(helpers.AdminField):
176
+ def label_tag(self) -> SafeText:
177
+ classes = []
178
+ if not self.field.field.widget.__class__.__name__.startswith(
179
+ "Unfold"
180
+ ) and not self.field.field.widget.template_name.startswith("unfold"):
181
+ return super().label_tag()
182
+
183
+ # TODO load config from current AdminSite (override Fieldline.__iter__ method)
184
+ for lang, flag in get_config()["EXTENSIONS"]["modeltranslation"][
185
+ "flags"
186
+ ].items():
187
+ if f"[{lang}]" in self.field.label:
188
+ self.field.label = self.field.label.replace(f"[{lang}]", flag)
189
+ break
190
+
191
+ contents = conditional_escape(self.field.label)
192
+
193
+ if self.is_checkbox:
194
+ classes.append(" ".join(CHECKBOX_LABEL_CLASSES))
195
+ else:
196
+ classes.append(" ".join(LABEL_CLASSES))
197
+
198
+ if self.field.field.required:
199
+ classes.append("required")
200
+
201
+ attrs = {"class": " ".join(classes)} if classes else {}
202
+ required = mark_safe(' <span class="text-red-600">*</span>')
203
+
204
+ return self.field.label_tag(
205
+ contents=mark_safe(contents),
206
+ attrs=attrs,
207
+ label_suffix=required if self.field.field.required else "",
208
+ )