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.
- {django_unfold-0.26.0.dist-info → django_unfold-0.28.0.dist-info}/METADATA +54 -5
- {django_unfold-0.26.0.dist-info → django_unfold-0.28.0.dist-info}/RECORD +33 -26
- unfold/admin.py +6 -157
- unfold/contrib/forms/templates/unfold/forms/array.html +31 -0
- unfold/contrib/forms/widgets.py +58 -3
- unfold/contrib/import_export/forms.py +21 -7
- unfold/contrib/import_export/templates/admin/import_export/change_list_export.html +5 -0
- unfold/contrib/import_export/templates/admin/import_export/export.html +1 -1
- unfold/dataclasses.py +2 -0
- unfold/decorators.py +3 -0
- unfold/fields.py +208 -0
- unfold/static/unfold/css/styles.css +1 -1
- unfold/templates/admin/change_form.html +0 -2
- unfold/templates/admin/edit_inline/tabular.html +4 -6
- unfold/templates/admin/includes/fieldset.html +2 -32
- unfold/templates/admin/login.html +4 -0
- unfold/templates/admin/submit_line.html +1 -1
- unfold/templates/unfold/helpers/attrs.html +1 -0
- unfold/templates/unfold/helpers/display_header.html +1 -1
- unfold/templates/unfold/helpers/field_readonly.html +1 -3
- unfold/templates/unfold/helpers/field_readonly_value.html +1 -0
- unfold/templates/unfold/helpers/fieldset_row.html +53 -0
- unfold/templates/unfold/helpers/search_results.html +10 -10
- unfold/templates/unfold/layouts/base.html +11 -0
- unfold/templates/unfold/layouts/base_simple.html +7 -1
- unfold/templates/unfold/widgets/clearable_file_input.html +1 -1
- unfold/templates/unfold/widgets/clearable_file_input_small.html +1 -1
- unfold/templates/unfold/widgets/foreign_key_raw_id.html +7 -13
- unfold/utils.py +21 -1
- unfold/views.py +18 -6
- unfold/widgets.py +12 -1
- {django_unfold-0.26.0.dist-info → django_unfold-0.28.0.dist-info}/LICENSE.md +0 -0
- {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 }}"{%
|
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
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
|
+
)
|