Skip to content

Commit

Permalink
feat: compressed changeform mode (#450)
Browse files Browse the repository at this point in the history
  • Loading branch information
lukasvinclav authored Jun 6, 2024
1 parent b34cc16 commit d2fe80f
Show file tree
Hide file tree
Showing 11 changed files with 280 additions and 243 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Did you decide to start using Unfold but you don't have time to make the switch
- **Model tabs:** define custom tab navigations for models
- **Fieldset tabs:** merge several fielsets into tabs in change form
- **Colors:** possibility to override default color scheme
- **Changeform modes:** display fields in changeform in compressed mode
- **Third party packages:** default support for multiple popular applications
- **Environment label**: distinguish between environments by displaying a label
- **Nonrelated inlines**: displays nonrelated model as inline in changeform
Expand Down Expand Up @@ -306,6 +307,9 @@ from unfold.contrib.forms.widgets import ArrayWidget, WysiwygWidget

@admin.register(MyModel)
class CustomAdminClass(ModelAdmin):
# Display fields in changeform in compressed mode
compressed_fields = True # Default: False

# Preprocess content of readonly fields before render
readonly_preprocess_fields = {
"model_field_name": "html.unescape",
Expand Down
187 changes: 6 additions & 181 deletions src/unfold/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,50 +7,30 @@
from django.contrib.admin import StackedInline as BaseStackedInline
from django.contrib.admin import TabularInline as BaseTabularInline
from django.contrib.admin import display, helpers
from django.contrib.admin.utils import lookup_field, quote
from django.contrib.admin.widgets import RelatedFieldWidgetWrapper
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.db.models import (
BLANK_CHOICE_DASH,
ForeignObjectRel,
JSONField,
ManyToManyRel,
Model,
OneToOneField,
)
from django.db.models import BLANK_CHOICE_DASH, Model
from django.db.models.fields import Field
from django.db.models.fields.related import ForeignKey, ManyToManyField
from django.forms import Form
from django.forms.fields import TypedChoiceField
from django.forms.models import (
ModelChoiceField,
ModelMultipleChoiceField,
)
from django.forms.utils import flatatt
from django.forms.models import ModelChoiceField, ModelMultipleChoiceField
from django.forms.widgets import SelectMultiple
from django.http import HttpRequest, HttpResponse
from django.shortcuts import redirect
from django.template.defaultfilters import linebreaksbr
from django.template.response import TemplateResponse
from django.urls import NoReverseMatch, URLPattern, path, reverse
from django.utils.html import conditional_escape, format_html
from django.utils.module_loading import import_string
from django.utils.safestring import SafeText, mark_safe
from django.utils.text import capfirst
from django.urls import URLPattern, path, reverse
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from django.views import View

from .checks import UnfoldModelAdminChecks
from .dataclasses import UnfoldAction
from .exceptions import UnfoldException
from .fields import UnfoldAdminField, UnfoldAdminReadonlyField
from .forms import ActionForm
from .settings import get_config
from .typing import FieldsetsType
from .utils import display_for_field
from .widgets import (
CHECKBOX_LABEL_CLASSES,
LABEL_CLASSES,
SELECT_CLASSES,
UnfoldAdminBigIntegerFieldWidget,
UnfoldAdminDecimalFieldWidget,
Expand Down Expand Up @@ -90,8 +70,6 @@
except ImportError:
HAS_MONEY = False

checkbox = UnfoldBooleanWidget({"class": "action-select"}, lambda value: False)

FORMFIELD_OVERRIDES = {
models.DateTimeField: {
"form_class": forms.SplitDateTimeField,
Expand Down Expand Up @@ -141,163 +119,10 @@
}
)


class UnfoldAdminField(helpers.AdminField):
def label_tag(self) -> SafeText:
classes = []
if not self.field.field.widget.__class__.__name__.startswith(
"Unfold"
) and not self.field.field.widget.template_name.startswith("unfold"):
return super().label_tag()

# TODO load config from current AdminSite (override Fieldline.__iter__ method)
for lang, flag in get_config()["EXTENSIONS"]["modeltranslation"][
"flags"
].items():
if f"[{lang}]" in self.field.label:
self.field.label = self.field.label.replace(f"[{lang}]", flag)
break

contents = conditional_escape(self.field.label)

if self.is_checkbox:
classes.append(" ".join(CHECKBOX_LABEL_CLASSES))
else:
classes.append(" ".join(LABEL_CLASSES))

if self.field.field.required:
classes.append("required")

attrs = {"class": " ".join(classes)} if classes else {}
required = mark_safe(' <span class="text-red-600">*</span>')

return self.field.label_tag(
contents=mark_safe(contents),
attrs=attrs,
label_suffix=required if self.field.field.required else "",
)

checkbox = UnfoldBooleanWidget({"class": "action-select"}, lambda value: False)

helpers.AdminField = UnfoldAdminField


class UnfoldAdminReadonlyField(helpers.AdminReadonlyField):
def label_tag(self) -> SafeText:
if not isinstance(self.model_admin, ModelAdmin) and not isinstance(
self.model_admin, ModelAdminMixin
):
return super().label_tag()

attrs = {
"class": " ".join(LABEL_CLASSES + ["mb-2"]),
}

label = self.field["label"]

return format_html(
"<label{}>{}{}</label>",
flatatt(attrs),
capfirst(label),
self.form.label_suffix,
)

def is_json(self) -> bool:
field, obj, model_admin = (
self.field["field"],
self.form.instance,
self.model_admin,
)

try:
f, attr, value = lookup_field(field, obj, model_admin)
except (AttributeError, ValueError, ObjectDoesNotExist):
return False

return isinstance(f, JSONField)

def contents(self) -> str:
contents = self._get_contents()
contents = self._preprocess_field(contents)
return contents

def get_admin_url(self, remote_field, remote_obj):
url_name = f"admin:{remote_field.model._meta.app_label}_{remote_field.model._meta.model_name}_change"
try:
url = reverse(
url_name,
args=[quote(remote_obj.pk)],
current_app=self.model_admin.admin_site.name,
)
return format_html(
'<a href="{}" class="text-primary-600 underline">{}</a>',
url,
remote_obj,
)
except NoReverseMatch:
return str(remote_obj)

def _get_contents(self) -> str:
from django.contrib.admin.templatetags.admin_list import _boolean_icon

field, obj, model_admin = (
self.field["field"],
self.form.instance,
self.model_admin,
)
try:
f, attr, value = lookup_field(field, obj, model_admin)
except (AttributeError, ValueError, ObjectDoesNotExist):
result_repr = self.empty_value_display
else:
if field in self.form.fields:
widget = self.form[field].field.widget
# This isn't elegant but suffices for contrib.auth's
# ReadOnlyPasswordHashWidget.
if getattr(widget, "read_only", False):
return widget.render(field, value)

if f is None:
if getattr(attr, "boolean", False):
result_repr = _boolean_icon(value)
else:
if hasattr(value, "__html__"):
result_repr = value
else:
result_repr = linebreaksbr(value)
else:
if isinstance(f.remote_field, ManyToManyRel) and value is not None:
result_repr = ", ".join(map(str, value.all()))
elif (
isinstance(f.remote_field, (ForeignObjectRel, OneToOneField))
and value is not None
):
result_repr = self.get_admin_url(f.remote_field, value)
elif isinstance(f, models.URLField):
return format_html(
'<a href="{}" class="text-primary-600 underline">{}</a>',
value,
value,
)
else:
result_repr = display_for_field(value, f, self.empty_value_display)
return conditional_escape(result_repr)
result_repr = linebreaksbr(result_repr)
return conditional_escape(result_repr)

def _preprocess_field(self, contents: str) -> str:
if (
hasattr(self.model_admin, "readonly_preprocess_fields")
and self.field["field"] in self.model_admin.readonly_preprocess_fields
):
func = self.model_admin.readonly_preprocess_fields[self.field["field"]]
if isinstance(func, str):
contents = import_string(func)(contents)
elif callable(func):
contents = func(contents)

return contents


helpers.AdminReadonlyField = UnfoldAdminReadonlyField


Expand Down
3 changes: 3 additions & 0 deletions src/unfold/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def display(
function: Optional[Callable[[Model], Any]] = None,
*,
boolean: Optional[bool] = None,
image: Optional[bool] = None,
ordering: Optional[Union[str, Combinable, BaseExpression]] = None,
description: Optional[str] = None,
empty_value: Optional[str] = None,
Expand All @@ -72,6 +73,8 @@ def decorator(func: Callable[[Model], Any]) -> Callable:
)
if boolean is not None:
func.boolean = boolean
if image is not None:
func.image = image
if ordering is not None:
func.admin_order_field = ordering
if description is not None:
Expand Down
Loading

0 comments on commit d2fe80f

Please sign in to comment.