Skip to content

Commit 2dd8713

Browse files
authored
feat: switch widget for boolean values (#237)
1 parent dd852b2 commit 2dd8713

File tree

9 files changed

+269
-64
lines changed

9 files changed

+269
-64
lines changed

src/unfold/admin.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@
6868
UnfoldAdminTextareaWidget,
6969
UnfoldAdminTextInputWidget,
7070
UnfoldAdminUUIDInputWidget,
71+
UnfoldBooleanSwitchWidget,
72+
UnfoldBooleanWidget,
7173
)
7274

7375
try:
@@ -85,7 +87,7 @@
8587
except ImportError:
8688
HAS_MONEY = False
8789

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

9092
FORMFIELD_OVERRIDES = {
9193
models.DateTimeField: {
@@ -101,6 +103,7 @@
101103
models.UUIDField: {"widget": UnfoldAdminUUIDInputWidget},
102104
models.TextField: {"widget": UnfoldAdminTextareaWidget},
103105
models.NullBooleanField: {"widget": UnfoldAdminNullBooleanSelectWidget},
106+
models.BooleanField: {"widget": UnfoldBooleanWidget},
104107
models.IntegerField: {"widget": UnfoldAdminIntegerFieldWidget},
105108
models.BigIntegerField: {"widget": UnfoldAdminBigIntegerFieldWidget},
106109
models.DecimalField: {"widget": UnfoldAdminDecimalFieldWidget},
@@ -126,6 +129,11 @@
126129
}
127130
)
128131

132+
CHANGE_FORM_FORMFIELD_OVERRIDES = copy.deepcopy(FORMFIELD_OVERRIDES)
133+
CHANGE_FORM_FORMFIELD_OVERRIDES.update(
134+
{models.BooleanField: {"widget": UnfoldBooleanSwitchWidget}}
135+
)
136+
129137
FORMFIELD_OVERRIDES_INLINE = copy.deepcopy(FORMFIELD_OVERRIDES)
130138

131139
FORMFIELD_OVERRIDES_INLINE.update(
@@ -528,6 +536,8 @@ def changeform_view(
528536
if extra_context is None:
529537
extra_context = {}
530538

539+
self.formfield_overrides = CHANGE_FORM_FORMFIELD_OVERRIDES
540+
531541
actions = []
532542
if object_id:
533543
for action in self.get_actions_detail(request):
@@ -640,7 +650,7 @@ def get_action_choices(
640650
default_choices = [("", _("Select action"))]
641651
return super().get_action_choices(request, default_choices)
642652

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

src/unfold/sites.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from .settings import get_config
1414
from .utils import hex_to_rgb
15-
from .widgets import INPUT_CLASSES
15+
from .widgets import CHECKBOX_CLASSES, INPUT_CLASSES
1616

1717

1818
class UnfoldAdminSite(AdminSite):
@@ -55,6 +55,7 @@ def each_context(self, request: HttpRequest) -> Dict[str, Any]:
5555
{
5656
"form_classes": {
5757
"text_input": INPUT_CLASSES,
58+
"checkbox": CHECKBOX_CLASSES,
5859
},
5960
"site_logo": self._get_mode_images(
6061
get_config(self.settings_name)["SITE_LOGO"], request

src/unfold/static/unfold/css/styles.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/unfold/styles.css

Lines changed: 0 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -86,57 +86,6 @@ select:after {
8686
display: block;
8787
}
8888

89-
/*******************************************************
90-
Checkbox
91-
*******************************************************/
92-
#page input[type="checkbox"] {
93-
@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;
94-
@apply focus:outline focus:outline-1 focus:outline-offset-2 focus:outline-primary-500;
95-
}
96-
97-
#page input[type="checkbox"]:after {
98-
@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;
99-
100-
content: "done";
101-
font-family: "Material Symbols Outlined";
102-
}
103-
104-
#page input[type="checkbox"]:checked {
105-
@apply bg-primary-600 border-primary-600 transition-all;
106-
}
107-
108-
#page input[type="checkbox"]:checked:after {
109-
@apply text-white;
110-
}
111-
112-
#page input[type="checkbox"].hidden {
113-
display: none;
114-
}
115-
116-
/*******************************************************
117-
Radio
118-
*******************************************************/
119-
#page input[type="radio"] {
120-
@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;
121-
@apply focus:outline focus:outline-1 focus:outline-offset-2 focus:outline-primary-500;
122-
}
123-
124-
#page input[type="radio"]:after {
125-
@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;
126-
}
127-
128-
#page input[type="radio"]:checked {
129-
@apply bg-primary-600 border-primary-600 transition-all;
130-
}
131-
132-
#page input[type="radio"]:checked:after {
133-
@apply bg-white dark:bg-gray-200;
134-
}
135-
136-
#page input[type="radio"].hidden {
137-
display: none;
138-
}
139-
14089
/*******************************************************
14190
Table
14291
*******************************************************/

src/unfold/templates/admin/change_list_results.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<thead class="hidden lg:table-header-group">
1212
<tr>
1313
{% for header in result_headers %}
14-
<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 }}>
14+
<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">
1515
<div class="flex items-center">
1616
<div class="text">
1717
{% if header.sortable %}

src/unfold/templates/admin/edit_inline/stacked.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{% load i18n admin_urls %}
1+
{% load admin_urls i18n unfold %}
22

33
<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 }}">
44
<fieldset class="module {{ inline_admin_formset.classes }}">
@@ -43,7 +43,7 @@ <h3 class="border-b {% if not forloop.first %}border-t{% endif %} border-gray-20
4343

4444
{% if inline_admin_formset.formset.can_delete and inline_admin_formset.has_delete_permission and inline_admin_form.original %}
4545
<span class="delete flex items-center ml-auto text-gray-500">
46-
{{ inline_admin_form.deletion_field.field }} {{ inline_admin_form.deletion_field.label_tag }}
46+
{{ inline_admin_form.deletion_field.field|add_css_class:form_classes.checkbox }} {{ inline_admin_form.deletion_field.label_tag }}
4747
</span>
4848
{% endif %}
4949
</h3>

src/unfold/templates/admin/edit_inline/tabular.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{% load i18n admin_urls static admin_modify %}
1+
{% load admin_modify admin_urls i18n static unfold %}
22

33
<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 }}">
44
<div class="tabular inline-related {% if forloop.last %}last-related{% endif %}">
@@ -137,7 +137,7 @@ <h2 class="bg-gray-100 border border-transparent font-semibold mb-6 px-4 py-3 ro
137137
{% if inline_admin_form.original %}
138138
<div class="flex flex-row lg:mt-3">
139139
<div class="ml-auto">
140-
{{ inline_admin_form.deletion_field.field }}
140+
{{ inline_admin_form.deletion_field.field|add_css_class:form_classes.checkbox }}
141141
</div>
142142
</div>
143143
{% endif %}

src/unfold/templatetags/unfold_list.py

Lines changed: 107 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@
44
from django.contrib.admin.templatetags.admin_list import (
55
ResultList,
66
_coerce_field_name,
7-
result_headers,
87
result_hidden_fields,
98
)
109
from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
1110
from django.contrib.admin.templatetags.base import InclusionAdminNode
12-
from django.contrib.admin.utils import lookup_field
13-
from django.contrib.admin.views.main import PAGE_VAR, ChangeList
11+
from django.contrib.admin.utils import label_for_field, lookup_field
12+
from django.contrib.admin.views.main import (
13+
ORDER_VAR,
14+
PAGE_VAR,
15+
ChangeList,
16+
)
1417
from django.core.exceptions import ObjectDoesNotExist
1518
from django.db import models
1619
from django.forms import Form
@@ -21,19 +24,120 @@
2124
from django.urls import NoReverseMatch
2225
from django.utils.html import format_html
2326
from django.utils.safestring import SafeText, mark_safe
27+
from django.utils.translation import gettext_lazy as _
2428

2529
from ..utils import (
2630
display_for_field,
2731
display_for_header,
2832
display_for_label,
2933
display_for_value,
3034
)
35+
from ..widgets import UnfoldBooleanWidget
3136

3237
register = Library()
3338

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

3641

42+
def result_headers(cl):
43+
"""
44+
Generate the list column headers.
45+
"""
46+
ordering_field_columns = cl.get_ordering_field_columns()
47+
for i, field_name in enumerate(cl.list_display):
48+
text, attr = label_for_field(
49+
field_name, cl.model, model_admin=cl.model_admin, return_attr=True
50+
)
51+
is_field_sortable = cl.sortable_by is None or field_name in cl.sortable_by
52+
if attr:
53+
field_name = _coerce_field_name(field_name, i)
54+
# Potentially not sortable
55+
56+
# if the field is the action checkbox: no sorting and special class
57+
if field_name == "action_checkbox":
58+
yield {
59+
"text": UnfoldBooleanWidget(
60+
{
61+
"id": "action-toggle",
62+
"aria-label": _(
63+
"Select all objects on this page for an action"
64+
),
65+
}
66+
).render("action-toggle", False),
67+
"class_attrib": mark_safe("action-checkbox-column"),
68+
"sortable": False,
69+
}
70+
continue
71+
72+
admin_order_field = getattr(attr, "admin_order_field", None)
73+
# Set ordering for attr that is a property, if defined.
74+
if isinstance(attr, property) and hasattr(attr, "fget"):
75+
admin_order_field = getattr(attr.fget, "admin_order_field", None)
76+
if not admin_order_field:
77+
is_field_sortable = False
78+
79+
if not is_field_sortable:
80+
# Not sortable
81+
yield {
82+
"text": text,
83+
"class_attrib": format_html("column-{}", field_name),
84+
"sortable": False,
85+
}
86+
continue
87+
88+
# OK, it is sortable if we got this far
89+
th_classes = ["sortable", f"column-{field_name}"]
90+
order_type = ""
91+
new_order_type = "asc"
92+
sort_priority = 0
93+
# Is it currently being sorted on?
94+
is_sorted = i in ordering_field_columns
95+
if is_sorted:
96+
order_type = ordering_field_columns.get(i).lower()
97+
sort_priority = list(ordering_field_columns).index(i) + 1
98+
th_classes.append("sorted %sending" % order_type)
99+
new_order_type = {"asc": "desc", "desc": "asc"}[order_type]
100+
101+
# build new ordering param
102+
o_list_primary = [] # URL for making this field the primary sort
103+
o_list_remove = [] # URL for removing this field from sort
104+
o_list_toggle = [] # URL for toggling order type for this field
105+
106+
def make_qs_param(t, n):
107+
return ("-" if t == "desc" else "") + str(n)
108+
109+
for j, ot in ordering_field_columns.items():
110+
if j == i: # Same column
111+
param = make_qs_param(new_order_type, j)
112+
# We want clicking on this header to bring the ordering to the
113+
# front
114+
o_list_primary.insert(0, param)
115+
o_list_toggle.append(param)
116+
# o_list_remove - omit
117+
else:
118+
param = make_qs_param(ot, j)
119+
o_list_primary.append(param)
120+
o_list_toggle.append(param)
121+
o_list_remove.append(param)
122+
123+
if i not in ordering_field_columns:
124+
o_list_primary.insert(0, make_qs_param(new_order_type, i))
125+
126+
yield {
127+
"text": text,
128+
"sortable": True,
129+
"sorted": is_sorted,
130+
"ascending": order_type == "asc",
131+
"sort_priority": sort_priority,
132+
"url_primary": cl.get_query_string({ORDER_VAR: ".".join(o_list_primary)}),
133+
"url_remove": cl.get_query_string({ORDER_VAR: ".".join(o_list_remove)}),
134+
"url_toggle": cl.get_query_string({ORDER_VAR: ".".join(o_list_toggle)}),
135+
"class_attrib": format_html(' class="{}"', " ".join(th_classes))
136+
if th_classes
137+
else "",
138+
}
139+
140+
37141
def items_for_result(cl: ChangeList, result: HttpRequest, form) -> SafeText:
38142
"""
39143
Generate the actual list of data.

0 commit comments

Comments
 (0)