Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Did you decide to start using Unfold but you don't have time to make the switch
- **Colors:** possibility to override default color scheme
- **Third party packages:** default support for multiple popular applications
- **Environment label**: distinguish between environments by displaying a label
- **Parallel admin**: support for having default admin in parallel with Unfold
- **VS Code**: project configuration and development container is included

## Table of contents <!-- omit from toc -->
Expand Down
49 changes: 28 additions & 21 deletions src/unfold/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from django.contrib.admin import TabularInline as BaseTabularInline
from django.contrib.admin import display, helpers
from django.contrib.admin.utils import lookup_field
from django.contrib.admin.widgets import RelatedFieldWidgetWrapper
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.db.models import (
Expand Down Expand Up @@ -39,17 +40,16 @@
from django.utils.text import capfirst
from django.utils.translation import gettext_lazy as _
from django.views import View
from unfold.utils import display_for_field

from .checks import UnfoldModelAdminChecks
from .dataclasses import UnfoldAction
from .exceptions import UnfoldException
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,
INPUT_CLASSES,
LABEL_CLASSES,
SELECT_CLASSES,
UnfoldAdminBigIntegerFieldWidget,
Expand All @@ -62,6 +62,7 @@
UnfoldAdminMoneyWidget,
UnfoldAdminNullBooleanSelectWidget,
UnfoldAdminRadioSelectWidget,
UnfoldAdminSelect,
UnfoldAdminSingleDateWidget,
UnfoldAdminSingleTimeWidget,
UnfoldAdminSplitDateTimeWidget,
Expand Down Expand Up @@ -142,6 +143,11 @@ 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"
Expand Down Expand Up @@ -175,6 +181,9 @@ def label_tag(self) -> SafeText:

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

attrs = {
"class": " ".join(LABEL_CLASSES + ["mb-2"]),
}
Expand Down Expand Up @@ -285,9 +294,7 @@ def formfield_for_choice_field(
radio_style=self.radio_fields[db_field.name]
)
else:
kwargs["widget"] = forms.Select(
attrs={"class": " ".join(SELECT_CLASSES)}
)
kwargs["widget"] = UnfoldAdminSelect()

kwargs["choices"] = db_field.get_choices(
include_blank=db_field.blank, blank_choice=[("", _("Select value"))]
Expand All @@ -301,16 +308,12 @@ def formfield_for_foreignkey(
# Overrides widgets for all related fields
if "widget" not in kwargs:
if db_field.name in self.raw_id_fields:
kwargs["widget"] = forms.TextInput(
attrs={"class": " ".join(INPUT_CLASSES)}
)
kwargs["widget"] = UnfoldAdminTextInputWidget()
elif (
db_field.name not in self.get_autocomplete_fields(request)
and db_field.name not in self.radio_fields
):
kwargs["widget"] = forms.Select(
attrs={"class": " ".join(SELECT_CLASSES)}
)
kwargs["widget"] = UnfoldAdminSelect()
kwargs["empty_label"] = _("Select value")

return super().formfield_for_foreignkey(db_field, request, **kwargs)
Expand All @@ -323,9 +326,7 @@ def formfield_for_manytomany(
) -> ModelMultipleChoiceField:
if "widget" not in kwargs:
if db_field.name in self.raw_id_fields:
kwargs["widget"] = forms.TextInput(
attrs={"class": " ".join(INPUT_CLASSES)}
)
kwargs["widget"] = UnfoldAdminTextInputWidget()

form_field = super().formfield_for_manytomany(db_field, request, **kwargs)

Expand All @@ -342,9 +343,7 @@ def formfield_for_nullboolean_field(
self, db_field: Field, request: HttpRequest, **kwargs
) -> Optional[Field]:
if "widget" not in kwargs:
kwargs["widget"] = forms.NullBooleanSelect(
attrs={"class": " ".join(SELECT_CLASSES)}
)
kwargs["widget"] = UnfoldAdminNullBooleanSelectWidget()

return db_field.formfield(**kwargs)

Expand All @@ -354,7 +353,14 @@ def formfield_for_dbfield(
if isinstance(db_field, models.BooleanField) and db_field.null is True:
return self.formfield_for_nullboolean_field(db_field, request, **kwargs)

return super().formfield_for_dbfield(db_field, request, **kwargs)
formfield = super().formfield_for_dbfield(db_field, request, **kwargs)

if formfield and isinstance(formfield.widget, RelatedFieldWidgetWrapper):
formfield.widget.template_name = (
"unfold/widgets/related_widget_wrapper.html"
)

return formfield


class ModelAdmin(ModelAdminMixin, BaseModelAdmin):
Expand Down Expand Up @@ -546,7 +552,8 @@ def changeform_view(
"title": action.description,
"attrs": action.method.attrs,
"path": reverse(
f"admin:{action.action_name}", args=(object_id,)
f"{self.admin_site.name}:{action.action_name}",
args=(object_id,),
),
}
)
Expand All @@ -570,7 +577,7 @@ def changelist_view(
{
"title": action.description,
"attrs": action.method.attrs,
"path": reverse(f"admin:{action.action_name}"),
"path": reverse(f"{self.admin_site.name}:{action.action_name}"),
}
for action in self.get_actions_list(request)
]
Expand All @@ -579,7 +586,7 @@ def changelist_view(
{
"title": action.description,
"attrs": action.method.attrs,
"raw_path": f"admin:{action.action_name}",
"raw_path": f"{self.admin_site.name}:{action.action_name}",
}
for action in self.get_actions_row(request)
]
Expand Down
5 changes: 5 additions & 0 deletions src/unfold/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@

class DefaultAppConfig(AppConfig):
name = "unfold"
default = True

def ready(self):
site = UnfoldAdminSite()

admin.site = site
sites.site = site


class BasicAppConfig(AppConfig):
name = "unfold"
6 changes: 3 additions & 3 deletions src/unfold/sites.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ def password_change(

from .forms import AdminOwnPasswordChangeForm

url = reverse("admin:password_change_done", current_app=self.name)
url = reverse(f"{self.name}:password_change_done", current_app=self.name)
defaults = {
"form_class": AdminOwnPasswordChangeForm,
"success_url": url,
Expand All @@ -224,9 +224,9 @@ def _get_is_active(link: str) -> bool:
if not isinstance(link, str):
link = str(link)

if link in request.path and link != reverse_lazy("admin:index"):
if link in request.path and link != reverse_lazy(f"{self.name}:index"):
return True
elif link == request.path == reverse_lazy("admin:index"):
elif link == request.path == reverse_lazy(f"{self.name}:index"):
return True

return False
Expand Down
2 changes: 1 addition & 1 deletion src/unfold/static/unfold/css/styles.css

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions src/unfold/templates/admin/change_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
{% csrf_token %}

{% if actions_detail %}
<div class="bg-gray-50 flex justify-end -mt-6 mb-4 p-3 rounded-md dark:bg-gray-800">
<div class="bg-gray-50 flex justify-end mb-4 p-3 rounded-md dark:bg-gray-800">
{% for action in actions_detail %}
<a href="{{ action.path }}" class="bg-white text-gray-500 border cursor-pointer flex font-medium items-center px-3 py-2 mr-3 rounded-md shadow-sm text-sm dark:bg-gray-900 dark:border dark:border-gray-700 dark:text-gray-400"
{% for attr_name, attr_value in action.attrs.items %}
Expand All @@ -89,7 +89,6 @@
<input type="hidden" name="{{ to_field_var }}" value="{{ to_field }}">
{% endif %}


{% include "unfold/helpers/messages/errornote.html" with errors=errors %}
{% include "unfold/helpers/messages/error.html" with errors=adminform.form.non_field_errors %}

Expand Down
6 changes: 3 additions & 3 deletions src/unfold/templates/admin/edit_inline/tabular.html
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,11 @@ <h2 class="bg-gray-100 border border-transparent font-semibold mb-6 px-4 py-3 ro
{% with is_last_col=forloop.last %}
{% for field in line %}
{% if field.is_readonly or not field.field.is_hidden %}
<td{% if field.field.name %} class="field-{{ field.field.name }}{% if field.field.errors|length > 0 %} errors{% endif %}{% if inline_admin_form.original %} p-3 lg:py-3{% else %} py-3{% endif %}{% if field.is_checkbox %} align-middle{% else %} align-top{% endif %} {% if is_last_row and not inline_admin_formset.has_add_permission %}{% if is_last_col %}border-0 {% else %}border-b lg:border-0{% endif %}{% else %}border-b{% endif %} border-gray-200 flex items-center before:capitalize before:content-[attr(data-label)] before:mr-auto before:text-gray-500 before:w-72 lg:before:hidden font-normal px-3 text-left text-sm lg:table-cell dark:border-gray-800"{% endif %} data-label="{{ field.field.label }}">
<td{% if field.field.name %} class="field-{{ field.field.name }}{% if field.field.errors|length > 0 %} errors{% endif %}{% if inline_admin_form.original %} p-3 lg:py-3{% else %} py-3{% endif %}{% if field.is_checkbox %} align-middle{% else %} align-top{% endif %} {% if is_last_row and not inline_admin_formset.has_add_permission %}{% if is_last_col %}border-0 {% else %}border-b lg:border-0{% endif %}{% else %}border-b{% endif %} border-gray-200 flex items-center before:capitalize before:content-[attr(data-label)] before:mr-auto before:text-gray-500 before:w-72 lg:before:hidden font-normal px-3 text-left text-sm lg:table-cell dark:border-gray-800 {% if field.field.is_hidden %} !hidden{% endif %}"{% endif %} data-label="{{ field.field.label }}">
{% if field.is_readonly %}
<p class="bg-gray-50 border font-medium max-w-lg px-3 py-2 rounded-md shadow-sm text-gray-500 text-sm truncate whitespace-nowrap dark:border-gray-700 dark:text-gray-400 dark:bg-gray-800">
<div class="bg-gray-50 border font-medium max-w-lg px-3 py-2 rounded-md shadow-sm text-gray-500 text-sm truncate whitespace-nowrap dark:border-gray-700 dark:text-gray-400 dark:bg-gray-800">
{{ field.contents }}
</p>
</div>
{% else %}
{{ field.field }}

Expand Down
19 changes: 16 additions & 3 deletions src/unfold/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
AdminTextInputWidget,
AdminTimeWidget,
AdminUUIDInputWidget,
RelatedFieldWidgetWrapper,
)
from django.forms import (
CheckboxInput,
Expand Down Expand Up @@ -291,7 +292,7 @@ def get_context(self, name, value, attrs):


class UnfoldAdminImageFieldWidget(FileFieldMixin, AdminFileWidget):
pass
template_name = "unfold/widgets/clearable_file_input.html"


class UnfoldAdminFileFieldWidget(FileFieldMixin, AdminFileWidget):
Expand Down Expand Up @@ -374,6 +375,8 @@ def __init__(self, attrs: Optional[Dict[str, Any]] = None) -> None:


class UnfoldAdminSplitDateTimeWidget(AdminSplitDateTime):
template_name = "unfold/widgets/split_datetime.html"

def __init__(self, attrs: Optional[Dict[str, Any]] = None) -> None:
widgets = [UnfoldAdminDateWidget, UnfoldAdminTimeWidget]
MultiWidget.__init__(self, widgets, attrs)
Expand Down Expand Up @@ -433,7 +436,12 @@ def __init__(self, attrs: Optional[Dict[str, Any]] = None) -> None:


class UnfoldAdminNullBooleanSelectWidget(NullBooleanSelect):
pass
def __init__(self, attrs=None):
if attrs is None:
attrs = {}

attrs["class"] = " ".join(SELECT_CLASSES)
super().__init__(attrs)


class UnfoldAdminSelect(Select):
Expand All @@ -446,7 +454,8 @@ def __init__(self, attrs=None, choices=()):


class UnfoldAdminRadioSelectWidget(AdminRadioSelect):
option_template_name = "admin/widgets/radio_option.html"
template_name = "unfold/widgets/radio.html"
option_template_name = "unfold/widgets/radio_option.html"

def __init__(self, radio_style: Optional[int] = None, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand Down Expand Up @@ -506,3 +515,7 @@ def __init__(
return super().__init__(
attrs={"class": " ".join(SWITCH_CLASSES), **(attrs or {})}, check_test=None
)


class UnfoldRelatedFieldWidgetWrapper(RelatedFieldWidgetWrapper):
template_name = "unfold/widgets/related_widget_wrapper.html"