diff --git a/docs/reference/contrib/modeladmin/migrating_to_snippets.md b/docs/reference/contrib/modeladmin/migrating_to_snippets.md index 69d0208b0f5a..4b7c6f02f659 100644 --- a/docs/reference/contrib/modeladmin/migrating_to_snippets.md +++ b/docs/reference/contrib/modeladmin/migrating_to_snippets.md @@ -53,6 +53,24 @@ There are a few attributes of `ModelAdmin` that need to be renamed/adjusted for | `form_fields_exclude` | {attr}`~wagtail.admin.viewsets.model.ModelViewSet.exclude_form_fields` | Same value, but different attribute name to better align with `ModelViewSet`. | | - | {attr}`~SnippetViewSet.template_prefix` | New attribute. Set to the name of a template directory to override the `"wagtailsnippets/snippets/"` default. If set to `"modeladmin/"`, the template directory structure will be equal to what ModelAdmin uses. Make sure any custom templates are placed in the correct directory according to this prefix. See [](wagtailsnippets_templates) for more details. | +### Boolean properties in `list_display` + +In ModelAdmin, boolean fields in `list_display` are rendered as tick/cross icons. To achieve the same behavior in SnippetViewSet, you need to use a `wagtail.admin.ui.tables.BooleanColumn` instance in `SnippetViewSet.list_display`: + +```python +from wagtail.admin.ui.tables import BooleanColumn + + +class MySnippetViewSet(SnippetViewSet): + list_display = ("title", BooleanColumn("is_active")) +``` + +The `BooleanColumn` class works with both model fields and custom properties that return booleans. + +```{versionadded} 5.1.1 +The `BooleanColumn` class was added. +``` + ## Convert `ModelAdminGroup` class to `SnippetViewSetGroup` The {class}`~SnippetViewSetGroup` class is the snippets-equivalent to the `ModelAdminGroup` class. To migrate a `ModelAdminGroup` class to a `SnippetViewSetGroup` class, follow these instructions. diff --git a/wagtail/admin/templates/wagtailadmin/tables/boolean_cell.html b/wagtail/admin/templates/wagtailadmin/tables/boolean_cell.html new file mode 100644 index 000000000000..c42a8ce6b88d --- /dev/null +++ b/wagtail/admin/templates/wagtailadmin/tables/boolean_cell.html @@ -0,0 +1,10 @@ +{% load wagtailadmin_tags %} + + {% if value is None %} + {% icon title=value|stringformat:'s' name="help" classname="default" %} + {% elif value %} + {% icon title=value|stringformat:'s' name="success" classname="default w-text-positive-100" %} + {% else %} + {% icon title=value|stringformat:'s' name="error" classname="default w-text-critical-100" %} + {% endif %} + diff --git a/wagtail/admin/ui/tables/__init__.py b/wagtail/admin/ui/tables/__init__.py index d630df80d1c7..fb2f75944b1a 100644 --- a/wagtail/admin/ui/tables/__init__.py +++ b/wagtail/admin/ui/tables/__init__.py @@ -243,6 +243,12 @@ def get_cell_context_data(self, instance, parent_context): return context +class BooleanColumn(Column): + """Represents a True/False/None value as a tick/cross/question icon""" + + cell_template_name = "wagtailadmin/tables/boolean_cell.html" + + class LiveStatusTagColumn(StatusTagColumn): """Represents a live/draft status tag""" diff --git a/wagtail/snippets/tests/test_viewset.py b/wagtail/snippets/tests/test_viewset.py index 8e3b0fc880eb..13a1c200636b 100644 --- a/wagtail/snippets/tests/test_viewset.py +++ b/wagtail/snippets/tests/test_viewset.py @@ -667,6 +667,7 @@ def test_custom_columns(self): self.assertContains(response, "Custom FOO column") self.assertContains(response, "Updated") self.assertContains(response, "Modulo two") + self.assertContains(response, "Tristate") self.assertContains(response, "Foo UK") @@ -678,14 +679,57 @@ def test_custom_columns(self): html = response.content.decode() - # The bulk actions column plus 5 columns defined in FullFeaturedSnippetViewSet - self.assertTagInHTML("", html, count=6, allow_extra_attrs=True) + # The bulk actions column plus 6 columns defined in FullFeaturedSnippetViewSet + self.assertTagInHTML("", html, count=7, allow_extra_attrs=True) def test_falsy_value(self): # https://github.com/wagtail/wagtail/issues/10765 response = self.get() self.assertContains(response, "0", html=True, count=1) + def test_boolean_column(self): + self.model.objects.create(text="Another one") + response = self.get() + self.assertContains( + response, + """ + + + True + + """, + html=True, + count=1, + ) + self.assertContains( + response, + """ + + + False + + """, + html=True, + count=1, + ) + self.assertContains( + response, + """ + + + None + + """, + html=True, + count=1, + ) + class TestListExport(BaseSnippetViewSetTests): model = FullFeaturedSnippet diff --git a/wagtail/test/testapp/models.py b/wagtail/test/testapp/models.py index afc7ecd53293..6353ff41e62a 100644 --- a/wagtail/test/testapp/models.py +++ b/wagtail/test/testapp/models.py @@ -1132,6 +1132,9 @@ def __str__(self): def modulo_two(self): return self.pk % 2 + def tristate(self): + return (None, True, False)[self.pk % 3] + def get_preview_template(self, request, mode_name): return "tests/previewable_model.html" diff --git a/wagtail/test/testapp/wagtail_hooks.py b/wagtail/test/testapp/wagtail_hooks.py index 53988fd98855..cd0e29137741 100644 --- a/wagtail/test/testapp/wagtail_hooks.py +++ b/wagtail/test/testapp/wagtail_hooks.py @@ -17,7 +17,7 @@ from wagtail.admin.search import SearchArea from wagtail.admin.site_summary import SummaryItem from wagtail.admin.ui.components import Component -from wagtail.admin.ui.tables import UpdatedAtColumn +from wagtail.admin.ui.tables import BooleanColumn, UpdatedAtColumn from wagtail.admin.views.account import BaseSettingsPanel from wagtail.admin.widgets import Button from wagtail.snippets.models import register_snippet @@ -263,6 +263,7 @@ class FullFeaturedSnippetViewSet(SnippetViewSet): "get_foo_country_code", UpdatedAtColumn(), "modulo_two", + BooleanColumn("tristate"), ] list_export = [ "text",