Skip to content

Commit d2fe80f

Browse files
authored
feat: compressed changeform mode (#450)
1 parent b34cc16 commit d2fe80f

File tree

11 files changed

+280
-243
lines changed

11 files changed

+280
-243
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Did you decide to start using Unfold but you don't have time to make the switch
3434
- **Model tabs:** define custom tab navigations for models
3535
- **Fieldset tabs:** merge several fielsets into tabs in change form
3636
- **Colors:** possibility to override default color scheme
37+
- **Changeform modes:** display fields in changeform in compressed mode
3738
- **Third party packages:** default support for multiple popular applications
3839
- **Environment label**: distinguish between environments by displaying a label
3940
- **Nonrelated inlines**: displays nonrelated model as inline in changeform
@@ -306,6 +307,9 @@ from unfold.contrib.forms.widgets import ArrayWidget, WysiwygWidget
306307

307308
@admin.register(MyModel)
308309
class CustomAdminClass(ModelAdmin):
310+
# Display fields in changeform in compressed mode
311+
compressed_fields = True # Default: False
312+
309313
# Preprocess content of readonly fields before render
310314
readonly_preprocess_fields = {
311315
"model_field_name": "html.unescape",

src/unfold/admin.py

Lines changed: 6 additions & 181 deletions
Original file line numberDiff line numberDiff line change
@@ -7,50 +7,30 @@
77
from django.contrib.admin import StackedInline as BaseStackedInline
88
from django.contrib.admin import TabularInline as BaseTabularInline
99
from django.contrib.admin import display, helpers
10-
from django.contrib.admin.utils import lookup_field, quote
1110
from django.contrib.admin.widgets import RelatedFieldWidgetWrapper
12-
from django.core.exceptions import ObjectDoesNotExist
1311
from django.db import models
14-
from django.db.models import (
15-
BLANK_CHOICE_DASH,
16-
ForeignObjectRel,
17-
JSONField,
18-
ManyToManyRel,
19-
Model,
20-
OneToOneField,
21-
)
12+
from django.db.models import BLANK_CHOICE_DASH, Model
2213
from django.db.models.fields import Field
2314
from django.db.models.fields.related import ForeignKey, ManyToManyField
2415
from django.forms import Form
2516
from django.forms.fields import TypedChoiceField
26-
from django.forms.models import (
27-
ModelChoiceField,
28-
ModelMultipleChoiceField,
29-
)
30-
from django.forms.utils import flatatt
17+
from django.forms.models import ModelChoiceField, ModelMultipleChoiceField
3118
from django.forms.widgets import SelectMultiple
3219
from django.http import HttpRequest, HttpResponse
3320
from django.shortcuts import redirect
34-
from django.template.defaultfilters import linebreaksbr
3521
from django.template.response import TemplateResponse
36-
from django.urls import NoReverseMatch, URLPattern, path, reverse
37-
from django.utils.html import conditional_escape, format_html
38-
from django.utils.module_loading import import_string
39-
from django.utils.safestring import SafeText, mark_safe
40-
from django.utils.text import capfirst
22+
from django.urls import URLPattern, path, reverse
23+
from django.utils.safestring import mark_safe
4124
from django.utils.translation import gettext_lazy as _
4225
from django.views import View
4326

4427
from .checks import UnfoldModelAdminChecks
4528
from .dataclasses import UnfoldAction
4629
from .exceptions import UnfoldException
30+
from .fields import UnfoldAdminField, UnfoldAdminReadonlyField
4731
from .forms import ActionForm
48-
from .settings import get_config
4932
from .typing import FieldsetsType
50-
from .utils import display_for_field
5133
from .widgets import (
52-
CHECKBOX_LABEL_CLASSES,
53-
LABEL_CLASSES,
5434
SELECT_CLASSES,
5535
UnfoldAdminBigIntegerFieldWidget,
5636
UnfoldAdminDecimalFieldWidget,
@@ -90,8 +70,6 @@
9070
except ImportError:
9171
HAS_MONEY = False
9272

93-
checkbox = UnfoldBooleanWidget({"class": "action-select"}, lambda value: False)
94-
9573
FORMFIELD_OVERRIDES = {
9674
models.DateTimeField: {
9775
"form_class": forms.SplitDateTimeField,
@@ -141,163 +119,10 @@
141119
}
142120
)
143121

144-
145-
class UnfoldAdminField(helpers.AdminField):
146-
def label_tag(self) -> SafeText:
147-
classes = []
148-
if not self.field.field.widget.__class__.__name__.startswith(
149-
"Unfold"
150-
) and not self.field.field.widget.template_name.startswith("unfold"):
151-
return super().label_tag()
152-
153-
# TODO load config from current AdminSite (override Fieldline.__iter__ method)
154-
for lang, flag in get_config()["EXTENSIONS"]["modeltranslation"][
155-
"flags"
156-
].items():
157-
if f"[{lang}]" in self.field.label:
158-
self.field.label = self.field.label.replace(f"[{lang}]", flag)
159-
break
160-
161-
contents = conditional_escape(self.field.label)
162-
163-
if self.is_checkbox:
164-
classes.append(" ".join(CHECKBOX_LABEL_CLASSES))
165-
else:
166-
classes.append(" ".join(LABEL_CLASSES))
167-
168-
if self.field.field.required:
169-
classes.append("required")
170-
171-
attrs = {"class": " ".join(classes)} if classes else {}
172-
required = mark_safe(' <span class="text-red-600">*</span>')
173-
174-
return self.field.label_tag(
175-
contents=mark_safe(contents),
176-
attrs=attrs,
177-
label_suffix=required if self.field.field.required else "",
178-
)
179-
122+
checkbox = UnfoldBooleanWidget({"class": "action-select"}, lambda value: False)
180123

181124
helpers.AdminField = UnfoldAdminField
182125

183-
184-
class UnfoldAdminReadonlyField(helpers.AdminReadonlyField):
185-
def label_tag(self) -> SafeText:
186-
if not isinstance(self.model_admin, ModelAdmin) and not isinstance(
187-
self.model_admin, ModelAdminMixin
188-
):
189-
return super().label_tag()
190-
191-
attrs = {
192-
"class": " ".join(LABEL_CLASSES + ["mb-2"]),
193-
}
194-
195-
label = self.field["label"]
196-
197-
return format_html(
198-
"<label{}>{}{}</label>",
199-
flatatt(attrs),
200-
capfirst(label),
201-
self.form.label_suffix,
202-
)
203-
204-
def is_json(self) -> bool:
205-
field, obj, model_admin = (
206-
self.field["field"],
207-
self.form.instance,
208-
self.model_admin,
209-
)
210-
211-
try:
212-
f, attr, value = lookup_field(field, obj, model_admin)
213-
except (AttributeError, ValueError, ObjectDoesNotExist):
214-
return False
215-
216-
return isinstance(f, JSONField)
217-
218-
def contents(self) -> str:
219-
contents = self._get_contents()
220-
contents = self._preprocess_field(contents)
221-
return contents
222-
223-
def get_admin_url(self, remote_field, remote_obj):
224-
url_name = f"admin:{remote_field.model._meta.app_label}_{remote_field.model._meta.model_name}_change"
225-
try:
226-
url = reverse(
227-
url_name,
228-
args=[quote(remote_obj.pk)],
229-
current_app=self.model_admin.admin_site.name,
230-
)
231-
return format_html(
232-
'<a href="{}" class="text-primary-600 underline">{}</a>',
233-
url,
234-
remote_obj,
235-
)
236-
except NoReverseMatch:
237-
return str(remote_obj)
238-
239-
def _get_contents(self) -> str:
240-
from django.contrib.admin.templatetags.admin_list import _boolean_icon
241-
242-
field, obj, model_admin = (
243-
self.field["field"],
244-
self.form.instance,
245-
self.model_admin,
246-
)
247-
try:
248-
f, attr, value = lookup_field(field, obj, model_admin)
249-
except (AttributeError, ValueError, ObjectDoesNotExist):
250-
result_repr = self.empty_value_display
251-
else:
252-
if field in self.form.fields:
253-
widget = self.form[field].field.widget
254-
# This isn't elegant but suffices for contrib.auth's
255-
# ReadOnlyPasswordHashWidget.
256-
if getattr(widget, "read_only", False):
257-
return widget.render(field, value)
258-
259-
if f is None:
260-
if getattr(attr, "boolean", False):
261-
result_repr = _boolean_icon(value)
262-
else:
263-
if hasattr(value, "__html__"):
264-
result_repr = value
265-
else:
266-
result_repr = linebreaksbr(value)
267-
else:
268-
if isinstance(f.remote_field, ManyToManyRel) and value is not None:
269-
result_repr = ", ".join(map(str, value.all()))
270-
elif (
271-
isinstance(f.remote_field, (ForeignObjectRel, OneToOneField))
272-
and value is not None
273-
):
274-
result_repr = self.get_admin_url(f.remote_field, value)
275-
elif isinstance(f, models.URLField):
276-
return format_html(
277-
'<a href="{}" class="text-primary-600 underline">{}</a>',
278-
value,
279-
value,
280-
)
281-
else:
282-
result_repr = display_for_field(value, f, self.empty_value_display)
283-
return conditional_escape(result_repr)
284-
result_repr = linebreaksbr(result_repr)
285-
return conditional_escape(result_repr)
286-
287-
def _preprocess_field(self, contents: str) -> str:
288-
if (
289-
hasattr(self.model_admin, "readonly_preprocess_fields")
290-
and self.field["field"] in self.model_admin.readonly_preprocess_fields
291-
):
292-
func = self.model_admin.readonly_preprocess_fields[self.field["field"]]
293-
if isinstance(func, str):
294-
contents = import_string(func)(contents)
295-
elif callable(func):
296-
contents = func(contents)
297-
298-
return contents
299-
300-
301126
helpers.AdminReadonlyField = UnfoldAdminReadonlyField
302127

303128

src/unfold/decorators.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def display(
5858
function: Optional[Callable[[Model], Any]] = None,
5959
*,
6060
boolean: Optional[bool] = None,
61+
image: Optional[bool] = None,
6162
ordering: Optional[Union[str, Combinable, BaseExpression]] = None,
6263
description: Optional[str] = None,
6364
empty_value: Optional[str] = None,
@@ -72,6 +73,8 @@ def decorator(func: Callable[[Model], Any]) -> Callable:
7273
)
7374
if boolean is not None:
7475
func.boolean = boolean
76+
if image is not None:
77+
func.image = image
7578
if ordering is not None:
7679
func.admin_order_field = ordering
7780
if description is not None:

0 commit comments

Comments
 (0)