Skip to content

Commit

Permalink
feat: switch widget for boolean values (#237)
Browse files Browse the repository at this point in the history
  • Loading branch information
lukasvinclav committed Dec 29, 2023
1 parent dd852b2 commit 2dd8713
Show file tree
Hide file tree
Showing 9 changed files with 269 additions and 64 deletions.
14 changes: 12 additions & 2 deletions src/unfold/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@
UnfoldAdminTextareaWidget,
UnfoldAdminTextInputWidget,
UnfoldAdminUUIDInputWidget,
UnfoldBooleanSwitchWidget,
UnfoldBooleanWidget,
)

try:
Expand All @@ -85,7 +87,7 @@
except ImportError:
HAS_MONEY = False

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

FORMFIELD_OVERRIDES = {
models.DateTimeField: {
Expand All @@ -101,6 +103,7 @@
models.UUIDField: {"widget": UnfoldAdminUUIDInputWidget},
models.TextField: {"widget": UnfoldAdminTextareaWidget},
models.NullBooleanField: {"widget": UnfoldAdminNullBooleanSelectWidget},
models.BooleanField: {"widget": UnfoldBooleanWidget},
models.IntegerField: {"widget": UnfoldAdminIntegerFieldWidget},
models.BigIntegerField: {"widget": UnfoldAdminBigIntegerFieldWidget},
models.DecimalField: {"widget": UnfoldAdminDecimalFieldWidget},
Expand All @@ -126,6 +129,11 @@
}
)

CHANGE_FORM_FORMFIELD_OVERRIDES = copy.deepcopy(FORMFIELD_OVERRIDES)
CHANGE_FORM_FORMFIELD_OVERRIDES.update(
{models.BooleanField: {"widget": UnfoldBooleanSwitchWidget}}
)

FORMFIELD_OVERRIDES_INLINE = copy.deepcopy(FORMFIELD_OVERRIDES)

FORMFIELD_OVERRIDES_INLINE.update(
Expand Down Expand Up @@ -528,6 +536,8 @@ def changeform_view(
if extra_context is None:
extra_context = {}

self.formfield_overrides = CHANGE_FORM_FORMFIELD_OVERRIDES

actions = []
if object_id:
for action in self.get_actions_detail(request):
Expand Down Expand Up @@ -640,7 +650,7 @@ def get_action_choices(
default_choices = [("", _("Select action"))]
return super().get_action_choices(request, default_choices)

@display(description=mark_safe('<input type="checkbox" id="action-toggle">'))
@display(description=mark_safe(checkbox.render("action_toggle_all", 1)))
def action_checkbox(self, obj: Model):
return checkbox.render(helpers.ACTION_CHECKBOX_NAME, str(obj.pk))

Expand Down
3 changes: 2 additions & 1 deletion src/unfold/sites.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from .settings import get_config
from .utils import hex_to_rgb
from .widgets import INPUT_CLASSES
from .widgets import CHECKBOX_CLASSES, INPUT_CLASSES


class UnfoldAdminSite(AdminSite):
Expand Down Expand Up @@ -55,6 +55,7 @@ def each_context(self, request: HttpRequest) -> Dict[str, Any]:
{
"form_classes": {
"text_input": INPUT_CLASSES,
"checkbox": CHECKBOX_CLASSES,
},
"site_logo": self._get_mode_images(
get_config(self.settings_name)["SITE_LOGO"], request
Expand Down
2 changes: 1 addition & 1 deletion src/unfold/static/unfold/css/styles.css

Large diffs are not rendered by default.

51 changes: 0 additions & 51 deletions src/unfold/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -86,57 +86,6 @@ select:after {
display: block;
}

/*******************************************************
Checkbox
*******************************************************/
#page input[type="checkbox"] {
@apply appearance-none bg-white block border border-gray-300 cursor-pointer h-4 relative rounded w-4 dark:bg-gray-700 dark:border-gray-500 hover:border-gray-400;
@apply focus:outline focus:outline-1 focus:outline-offset-2 focus:outline-primary-500;
}

#page input[type="checkbox"]:after {
@apply absolute flex h-4 items-center justify-center leading-none -ml-px -mt-px text-white transition-all text-sm w-4 dark:text-gray-700;

content: "done";
font-family: "Material Symbols Outlined";
}

#page input[type="checkbox"]:checked {
@apply bg-primary-600 border-primary-600 transition-all;
}

#page input[type="checkbox"]:checked:after {
@apply text-white;
}

#page input[type="checkbox"].hidden {
display: none;
}

/*******************************************************
Radio
*******************************************************/
#page input[type="radio"] {
@apply appearance-none bg-white block border border-gray-300 cursor-pointer h-4 relative rounded-full w-4 dark:bg-gray-700 dark:border-gray-500 hover:border-gray-400;
@apply focus:outline focus:outline-1 focus:outline-offset-2 focus:outline-primary-500;
}

#page input[type="radio"]:after {
@apply absolute bg-white content-[''] flex h-2 items-center justify-center leading-none left-1/2 rounded-full text-white top-1/2 transition-all -translate-x-1/2 -translate-y-1/2 text-sm w-2 dark:text-gray-700 dark:bg-transparent;
}

#page input[type="radio"]:checked {
@apply bg-primary-600 border-primary-600 transition-all;
}

#page input[type="radio"]:checked:after {
@apply bg-white dark:bg-gray-200;
}

#page input[type="radio"].hidden {
display: none;
}

/*******************************************************
Table
*******************************************************/
Expand Down
2 changes: 1 addition & 1 deletion src/unfold/templates/admin/change_list_results.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<thead class="hidden lg:table-header-group">
<tr>
{% for header in result_headers %}
<th class="align-middle font-medium px-3 py-2 text-left text-gray-400 text-sm {% if "action-toggle" in header.text and forloop.counter == 1 %}w-10{% endif %}" scope="col"{{ header.class_attrib }}>
<th class="align-middle font-medium px-3 py-2 text-left text-gray-400 text-sm {{ header.class_attrib }} {% if "action-toggle" in header.text and forloop.counter == 1 %}w-10{% endif %}" scope="col">
<div class="flex items-center">
<div class="text">
{% if header.sortable %}
Expand Down
4 changes: 2 additions & 2 deletions src/unfold/templates/admin/edit_inline/stacked.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% load i18n admin_urls %}
{% load admin_urls i18n unfold %}

<div class="js-inline-admin-formset inline-group" id="{{ inline_admin_formset.formset.prefix }}-group" data-inline-type="stacked" data-inline-formset="{{ inline_admin_formset.inline_formset_data }}">
<fieldset class="module {{ inline_admin_formset.classes }}">
Expand Down Expand Up @@ -43,7 +43,7 @@ <h3 class="border-b {% if not forloop.first %}border-t{% endif %} border-gray-20

{% if inline_admin_formset.formset.can_delete and inline_admin_formset.has_delete_permission and inline_admin_form.original %}
<span class="delete flex items-center ml-auto text-gray-500">
{{ inline_admin_form.deletion_field.field }} {{ inline_admin_form.deletion_field.label_tag }}
{{ inline_admin_form.deletion_field.field|add_css_class:form_classes.checkbox }} {{ inline_admin_form.deletion_field.label_tag }}
</span>
{% endif %}
</h3>
Expand Down
4 changes: 2 additions & 2 deletions src/unfold/templates/admin/edit_inline/tabular.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% load i18n admin_urls static admin_modify %}
{% load admin_modify admin_urls i18n static unfold %}

<div class="js-inline-admin-formset inline-group" id="{{ inline_admin_formset.formset.prefix }}-group" data-inline-type="tabular" data-inline-formset="{{ inline_admin_formset.inline_formset_data }}">
<div class="tabular inline-related {% if forloop.last %}last-related{% endif %}">
Expand Down Expand Up @@ -137,7 +137,7 @@ <h2 class="bg-gray-100 border border-transparent font-semibold mb-6 px-4 py-3 ro
{% if inline_admin_form.original %}
<div class="flex flex-row lg:mt-3">
<div class="ml-auto">
{{ inline_admin_form.deletion_field.field }}
{{ inline_admin_form.deletion_field.field|add_css_class:form_classes.checkbox }}
</div>
</div>
{% endif %}
Expand Down
110 changes: 107 additions & 3 deletions src/unfold/templatetags/unfold_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@
from django.contrib.admin.templatetags.admin_list import (
ResultList,
_coerce_field_name,
result_headers,
result_hidden_fields,
)
from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
from django.contrib.admin.templatetags.base import InclusionAdminNode
from django.contrib.admin.utils import lookup_field
from django.contrib.admin.views.main import PAGE_VAR, ChangeList
from django.contrib.admin.utils import label_for_field, lookup_field
from django.contrib.admin.views.main import (
ORDER_VAR,
PAGE_VAR,
ChangeList,
)
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.forms import Form
Expand All @@ -21,19 +24,120 @@
from django.urls import NoReverseMatch
from django.utils.html import format_html
from django.utils.safestring import SafeText, mark_safe
from django.utils.translation import gettext_lazy as _

from ..utils import (
display_for_field,
display_for_header,
display_for_label,
display_for_value,
)
from ..widgets import UnfoldBooleanWidget

register = Library()

LINK_CLASSES = ["text-gray-700 dark:text-gray-200"]


def result_headers(cl):
"""
Generate the list column headers.
"""
ordering_field_columns = cl.get_ordering_field_columns()
for i, field_name in enumerate(cl.list_display):
text, attr = label_for_field(
field_name, cl.model, model_admin=cl.model_admin, return_attr=True
)
is_field_sortable = cl.sortable_by is None or field_name in cl.sortable_by
if attr:
field_name = _coerce_field_name(field_name, i)
# Potentially not sortable

# if the field is the action checkbox: no sorting and special class
if field_name == "action_checkbox":
yield {
"text": UnfoldBooleanWidget(
{
"id": "action-toggle",
"aria-label": _(
"Select all objects on this page for an action"
),
}
).render("action-toggle", False),
"class_attrib": mark_safe("action-checkbox-column"),
"sortable": False,
}
continue

admin_order_field = getattr(attr, "admin_order_field", None)
# Set ordering for attr that is a property, if defined.
if isinstance(attr, property) and hasattr(attr, "fget"):
admin_order_field = getattr(attr.fget, "admin_order_field", None)
if not admin_order_field:
is_field_sortable = False

if not is_field_sortable:
# Not sortable
yield {
"text": text,
"class_attrib": format_html("column-{}", field_name),
"sortable": False,
}
continue

# OK, it is sortable if we got this far
th_classes = ["sortable", f"column-{field_name}"]
order_type = ""
new_order_type = "asc"
sort_priority = 0
# Is it currently being sorted on?
is_sorted = i in ordering_field_columns
if is_sorted:
order_type = ordering_field_columns.get(i).lower()
sort_priority = list(ordering_field_columns).index(i) + 1
th_classes.append("sorted %sending" % order_type)
new_order_type = {"asc": "desc", "desc": "asc"}[order_type]

# build new ordering param
o_list_primary = [] # URL for making this field the primary sort
o_list_remove = [] # URL for removing this field from sort
o_list_toggle = [] # URL for toggling order type for this field

def make_qs_param(t, n):
return ("-" if t == "desc" else "") + str(n)

for j, ot in ordering_field_columns.items():
if j == i: # Same column
param = make_qs_param(new_order_type, j)
# We want clicking on this header to bring the ordering to the
# front
o_list_primary.insert(0, param)
o_list_toggle.append(param)
# o_list_remove - omit
else:
param = make_qs_param(ot, j)
o_list_primary.append(param)
o_list_toggle.append(param)
o_list_remove.append(param)

if i not in ordering_field_columns:
o_list_primary.insert(0, make_qs_param(new_order_type, i))

yield {
"text": text,
"sortable": True,
"sorted": is_sorted,
"ascending": order_type == "asc",
"sort_priority": sort_priority,
"url_primary": cl.get_query_string({ORDER_VAR: ".".join(o_list_primary)}),
"url_remove": cl.get_query_string({ORDER_VAR: ".".join(o_list_remove)}),
"url_toggle": cl.get_query_string({ORDER_VAR: ".".join(o_list_toggle)}),
"class_attrib": format_html(' class="{}"', " ".join(th_classes))
if th_classes
else "",
}


def items_for_result(cl: ChangeList, result: HttpRequest, form) -> SafeText:
"""
Generate the actual list of data.
Expand Down
Loading

0 comments on commit 2dd8713

Please sign in to comment.