diff --git a/docs/reference/viewsets.md b/docs/reference/viewsets.md index 9c9a1606d1..b9d2e9330f 100644 --- a/docs/reference/viewsets.md +++ b/docs/reference/viewsets.md @@ -79,6 +79,7 @@ Viewsets are Wagtail's mechanism for defining a group of related admin views wit ```{eval-rst} .. autoclass:: wagtail.snippets.views.snippets.SnippetViewSet + .. autoattribute:: icon .. autoattribute:: list_display .. autoattribute:: filterset_class .. autoattribute:: index_view_class diff --git a/docs/topics/snippets.md b/docs/topics/snippets.md index 103d011a40..da3ed1b438 100644 --- a/docs/topics/snippets.md +++ b/docs/topics/snippets.md @@ -567,7 +567,11 @@ class MemberFilterSet(WagtailFilterSet): fields = ["shirt_size"] ``` -You can define a {attr}`~wagtail.snippets.views.snippets.SnippetViewSet.list_display` attribute to specify the columns shown on the listing view. You can also add the ability to filter the listing view by defining a {attr}`~wagtail.snippets.views.snippets.SnippetViewSet.filterset_class` attribute on a subclass of `SnippetViewSet`. For example: +You can define a {attr}`~wagtail.snippets.views.snippets.SnippetViewSet.icon` attribute to specify the icon that is used across the admin for this snippet type. The `icon` needs to be [registered in the Wagtail icon library](../../advanced_topics/icons). If `icon` is not set, the default `"snippet"` icon is used. + +The {attr}`~wagtail.snippets.views.snippets.SnippetViewSet.list_display` attribute can be set to specify the columns shown on the listing view. You can also add the ability to filter the listing view by defining a {attr}`~wagtail.snippets.views.snippets.SnippetViewSet.filterset_class` attribute on a subclass of `SnippetViewSet`. + +For example: ```python # views.py @@ -578,6 +582,7 @@ from myapp.models import MemberFilterSet class MemberViewSet(SnippetViewSet): + icon = "user" list_display = ["name", "shirt_size", "get_shirt_size_display", UpdatedAtColumn()] filterset_class = MemberFilterSet ``` diff --git a/wagtail/snippets/tests/test_bulk_actions/test_bulk_delete.py b/wagtail/snippets/tests/test_bulk_actions/test_bulk_delete.py index a62a87bf43..50ecfedfb9 100644 --- a/wagtail/snippets/tests/test_bulk_actions/test_bulk_delete.py +++ b/wagtail/snippets/tests/test_bulk_actions/test_bulk_delete.py @@ -4,13 +4,13 @@ from django.test import TestCase from django.urls import reverse from wagtail.snippets.bulk_actions.delete import DeleteBulkAction -from wagtail.test.snippets.models import StandardSnippet +from wagtail.test.testapp.models import FullFeaturedSnippet from wagtail.test.utils import WagtailTestUtils class TestSnippetDeleteView(WagtailTestUtils, TestCase): def setUp(self): - self.snippet_model = StandardSnippet + self.snippet_model = FullFeaturedSnippet # create a set of test snippets self.test_snippets = [ @@ -41,6 +41,9 @@ class TestSnippetDeleteView(WagtailTestUtils, TestCase): self.assertTemplateUsed( response, "wagtailsnippets/bulk_actions/confirm_bulk_delete.html" ) + self.assertTemplateUsed(response, "wagtailadmin/shared/header.html") + self.assertEqual(response.context["header_icon"], "cog") + self.assertContains(response, "icon icon-cog", count=1) def test_bulk_delete(self): response = self.client.post(self.url) @@ -66,7 +69,7 @@ class TestSnippetDeleteView(WagtailTestUtils, TestCase): html = response.content.decode() self.assertInHTML( - "

You don't have permission to delete these standard snippets

", + "

You don't have permission to delete these full-featured snippets

", html, ) diff --git a/wagtail/snippets/tests/test_snippets.py b/wagtail/snippets/tests/test_snippets.py index 75c4134c00..1c32c1acc2 100644 --- a/wagtail/snippets/tests/test_snippets.py +++ b/wagtail/snippets/tests/test_snippets.py @@ -111,6 +111,7 @@ class TestSnippetListView(WagtailTestUtils, TestCase): response = self.get() self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "wagtailsnippets/snippets/type_index.html") + self.assertEqual(response.context["header_icon"], "snippet") def get_with_limited_permissions(self): self.user.is_superuser = False @@ -3783,6 +3784,7 @@ class TestSnippetChooserPanel(WagtailTestUtils, TestCase): self.assertIn(self.advert_text, field_html) self.assertIn("Choose advert", field_html) self.assertIn("Choose another advert", field_html) + self.assertIn("icon icon-snippet icon", field_html) def test_render_as_empty_field(self): test_snippet = SnippetChooserModel() @@ -4683,6 +4685,7 @@ class TestAddOnlyPermissions(WagtailTestUtils, TestCase): response = self.client.get(reverse("wagtailsnippets_tests_advert:add")) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "wagtailsnippets/snippets/create.html") + self.assertEqual(response.context["header_icon"], "snippet") def test_get_edit(self): response = self.client.get( @@ -4746,6 +4749,7 @@ class TestEditOnlyPermissions(WagtailTestUtils, TestCase): ) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "wagtailsnippets/snippets/edit.html") + self.assertEqual(response.context["header_icon"], "snippet") def test_get_delete(self): response = self.client.get( @@ -4807,6 +4811,7 @@ class TestDeleteOnlyPermissions(WagtailTestUtils, TestCase): ) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "wagtailadmin/generic/confirm_delete.html") + self.assertEqual(response.context["header_icon"], "snippet") class TestSnippetEditHandlers(WagtailTestUtils, TestCase): @@ -5276,6 +5281,8 @@ class TestSnippetChooseWithCustomPrimaryKey(WagtailTestUtils, TestCase): def test_simple(self): response = self.get() self.assertTemplateUsed(response, "wagtailadmin/generic/chooser/chooser.html") + self.assertEqual(response.context["header_icon"], "snippet") + self.assertEqual(response.context["icon"], "snippet") def test_ordering(self): """ diff --git a/wagtail/snippets/tests/test_viewset.py b/wagtail/snippets/tests/test_viewset.py new file mode 100644 index 0000000000..120236062c --- /dev/null +++ b/wagtail/snippets/tests/test_viewset.py @@ -0,0 +1,155 @@ +from django.contrib.admin.utils import quote +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase +from django.urls import reverse + +from wagtail.admin.panels import get_edit_handler +from wagtail.coreutils import get_dummy_request +from wagtail.models import Workflow, WorkflowContentType +from wagtail.test.testapp.models import Advert, FullFeaturedSnippet, SnippetChooserModel +from wagtail.test.utils import WagtailTestUtils + + +class TestCustomIcon(WagtailTestUtils, TestCase): + def setUp(self): + self.user = self.login() + self.object = FullFeaturedSnippet.objects.create( + text="test snippet with custom icon" + ) + self.revision_1 = self.object.save_revision() + self.revision_1.publish() + self.object.text = "test snippet with custom icon (updated)" + self.revision_2 = self.object.save_revision() + + def get_url(self, url_name, args=()): + app_label = self.object._meta.app_label + model_name = self.object._meta.model_name + view_name = f"wagtailsnippets_{app_label}_{model_name}:{url_name}" + return reverse(view_name, args=args) + + def test_get_views(self): + pk = quote(self.object.pk) + views = [ + ("list", []), + ("add", []), + ("edit", [pk]), + ("delete", [pk]), + ("usage", [pk]), + ("unpublish", [pk]), + ("workflow_history", [pk]), + ("revisions_revert", [pk, self.revision_1.id]), + ("revisions_compare", [pk, self.revision_1.id, self.revision_2.id]), + ("revisions_unschedule", [pk, self.revision_2.id]), + ] + for view_name, args in views: + with self.subTest(view_name=view_name): + response = self.client.get(self.get_url(view_name, args)) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context["header_icon"], "cog") + self.assertContains(response, "icon icon-cog", count=1) + # TODO: Make the list view use the shared header template + if view_name != "list": + self.assertTemplateUsed(response, "wagtailadmin/shared/header.html") + + def test_get_history(self): + response = self.client.get(self.get_url("history", [quote(self.object.pk)])) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "wagtailadmin/shared/header.html") + # History view icon is not configurable for consistency with pages + self.assertEqual(response.context["header_icon"], "history") + self.assertContains(response, "icon icon-history") + self.assertNotContains(response, "icon icon-cog") + + def test_get_workflow_history_detail(self): + # Assign default workflow to the snippet model + self.content_type = ContentType.objects.get_for_model(type(self.object)) + self.workflow = Workflow.objects.first() + WorkflowContentType.objects.create( + content_type=self.content_type, + workflow=self.workflow, + ) + self.object.text = "Edited!" + self.object.save_revision() + workflow_state = self.workflow.start(self.object, self.user) + response = self.client.get( + self.get_url( + "workflow_history_detail", [quote(self.object.pk), workflow_state.id] + ) + ) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "wagtailadmin/shared/header.html") + # The icon is not displayed in the header, + # but it is displayed in the main content + self.assertEqual(response.context["header_icon"], "list-ul") + self.assertContains(response, "icon icon-list-ul") + self.assertContains(response, "icon icon-cog") + + +class TestSnippetChooserPanelWithIcon(WagtailTestUtils, TestCase): + def setUp(self): + self.user = self.login() + self.request = get_dummy_request() + self.request.user = self.user + self.text = "Test full-featured snippet with icon text" + test_snippet = SnippetChooserModel.objects.create( + advert=Advert.objects.create(text="foo"), + full_featured=FullFeaturedSnippet.objects.create(text=self.text), + ) + + self.edit_handler = get_edit_handler(SnippetChooserModel) + self.form_class = self.edit_handler.get_form_class() + form = self.form_class(instance=test_snippet) + edit_handler = self.edit_handler.get_bound_panel( + instance=test_snippet, form=form, request=self.request + ) + + self.object_chooser_panel = [ + panel + for panel in edit_handler.children + if getattr(panel, "field_name", None) == "full_featured" + ][0] + + def test_render_html(self): + field_html = self.object_chooser_panel.render_html() + self.assertIn(self.text, field_html) + self.assertIn("Choose full-featured snippet", field_html) + self.assertIn("Choose another full-featured snippet", field_html) + self.assertIn("icon icon-cog icon", field_html) + + # make sure no snippet icons remain + self.assertNotIn("icon-snippet", field_html) + + def test_render_as_empty_field(self): + test_snippet = SnippetChooserModel() + form = self.form_class(instance=test_snippet) + edit_handler = self.edit_handler.get_bound_panel( + instance=test_snippet, form=form, request=self.request + ) + + snippet_chooser_panel = [ + panel + for panel in edit_handler.children + if getattr(panel, "field_name", None) == "full_featured" + ][0] + + field_html = snippet_chooser_panel.render_html() + self.assertIn("Choose full-featured snippet", field_html) + self.assertIn("Choose another full-featured snippet", field_html) + self.assertIn("icon icon-cog icon", field_html) + + # make sure no snippet icons remain + self.assertNotIn("icon-snippet", field_html) + + def test_chooser_popup(self): + response = self.client.get( + reverse("wagtailsnippetchoosers_tests_fullfeaturedsnippet:choose") + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context["header_icon"], "cog") + self.assertContains(response, "icon icon-cog", count=1) + self.assertEqual(response.context["icon"], "cog") + + # make sure no snippet icons remain + for key in response.context.keys(): + if "icon" in key: + self.assertNotIn("snippet", response.context[key]) diff --git a/wagtail/test/testapp/migrations/0023_snippetchoosermodel_full_featured.py b/wagtail/test/testapp/migrations/0023_snippetchoosermodel_full_featured.py new file mode 100644 index 0000000000..ab607bc229 --- /dev/null +++ b/wagtail/test/testapp/migrations/0023_snippetchoosermodel_full_featured.py @@ -0,0 +1,24 @@ +# Generated by Django 4.1.7 on 2023-03-13 16:44 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("tests", "0022_variousondeletemodel"), + ] + + operations = [ + migrations.AddField( + model_name="snippetchoosermodel", + name="full_featured", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="tests.fullfeaturedsnippet", + ), + ), + ] diff --git a/wagtail/test/testapp/models.py b/wagtail/test/testapp/models.py index be9c7f2838..85fa18696f 100644 --- a/wagtail/test/testapp/models.py +++ b/wagtail/test/testapp/models.py @@ -1135,9 +1135,6 @@ class FullFeaturedSnippet( verbose_name_plural = "full-featured snippets" -register_snippet(FullFeaturedSnippet) - - def get_default_advert(): return Advert.objects.first() @@ -1343,9 +1340,13 @@ class EventPageChooserModel(models.Model): class SnippetChooserModel(models.Model): advert = models.ForeignKey(Advert, help_text="help text", on_delete=models.CASCADE) + full_featured = models.ForeignKey( + FullFeaturedSnippet, on_delete=models.CASCADE, null=True, blank=True + ) panels = [ FieldPanel("advert"), + FieldPanel("full_featured"), ] diff --git a/wagtail/test/testapp/wagtail_hooks.py b/wagtail/test/testapp/wagtail_hooks.py index 7a2b3c69d6..33b2d9f3d6 100644 --- a/wagtail/test/testapp/wagtail_hooks.py +++ b/wagtail/test/testapp/wagtail_hooks.py @@ -12,8 +12,10 @@ from wagtail.admin.ui.components import Component from wagtail.admin.views.account import BaseSettingsPanel from wagtail.admin.widgets import Button from wagtail.snippets.models import register_snippet +from wagtail.snippets.views.snippets import SnippetViewSet from wagtail.test.snippets.models import FilterableSnippet from wagtail.test.snippets.views import FilterableSnippetViewSet +from wagtail.test.testapp.models import FullFeaturedSnippet from .forms import FavouriteColourForm @@ -226,3 +228,10 @@ def add_broken_links_summary_item(request, items): register_snippet(FilterableSnippet, viewset=FilterableSnippetViewSet) + + +class FullFeaturedSnippetViewSet(SnippetViewSet): + icon = "cog" + + +register_snippet(FullFeaturedSnippet, viewset=FullFeaturedSnippetViewSet)