django-unfold 0.26.0__py3-none-any.whl → 0.28.0__py3-none-any.whl

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