diff --git a/wagtail/admin/tests/test_workflows.py b/wagtail/admin/tests/test_workflows.py index 737e9d74d6..09bec53594 100644 --- a/wagtail/admin/tests/test_workflows.py +++ b/wagtail/admin/tests/test_workflows.py @@ -3601,11 +3601,35 @@ class TestDashboardWithPages(BasePageWorkflowTests): "Compare with previous version", ) + def test_dashboard_after_deleting_object_in_moderation(self): + # WorkflowState's content_object may point to a nonexistent object + # https://github.com/wagtail/wagtail/issues/11300 + self.login(self.submitter) + self.post("submit") + self.object.delete() + + response = self.client.get(reverse("wagtailadmin_home")) + self.assertEqual(response.status_code, 200) + self.assertNotContains(response, "Your pages and snippets in a workflow") + + self.login(self.moderator) + response = self.client.get(reverse("wagtailadmin_home")) + self.assertEqual(response.status_code, 200) + self.assertNotContains(response, "Awaiting your review") + class TestDashboardWithSnippets(TestDashboardWithPages, BaseSnippetWorkflowTests): pass +class TestDashboardWithNonLockableSnippets(TestDashboardWithSnippets): + # This model does not use LockableMixin, and it also does not have a + # GenericRelation to WorkflowState and Revision, but it should not break + # the dashboard. + # See https://github.com/wagtail/wagtail/issues/11300 for more details. + model = ModeratedModel + + class TestWorkflowStateEmailNotifier(BasePageWorkflowTests): def setUp(self): super().setUp() diff --git a/wagtail/admin/views/home.py b/wagtail/admin/views/home.py index 0d335bfb65..5829498a75 100644 --- a/wagtail/admin/views/home.py +++ b/wagtail/admin/views/home.py @@ -161,6 +161,12 @@ class UserObjectsInWorkflowModerationPanel(Component): ) .order_by("-current_task_state__started_at") ) + # Filter out workflow states where the GenericForeignKey points to + # a nonexistent object. This can happen if the model does not define + # a GenericRelation to WorkflowState and the instance is deleted. + context["workflow_states"] = [ + state for state in context["workflow_states"] if state.content_object + ] else: context["workflow_states"] = WorkflowState.objects.none() context["request"] = request @@ -198,6 +204,12 @@ class WorkflowObjectsToModeratePanel(Component): ) for state in states: obj = state.revision.content_object + # Skip task states where the revision's GenericForeignKey points to + # a nonexistent object. This can happen if the model does not define + # a GenericRelation to WorkflowState and/or Revision and the instance + # is deleted. + if not obj: + continue actions = state.task.specific.get_actions(obj, request.user) workflow_tasks = state.workflow_state.all_tasks_with_status() diff --git a/wagtail/test/testapp/models.py b/wagtail/test/testapp/models.py index 1636781846..51f88ab3d5 100644 --- a/wagtail/test/testapp/models.py +++ b/wagtail/test/testapp/models.py @@ -1123,6 +1123,22 @@ class FullFeaturedSnippet( some_attribute = "some value" + workflow_states = GenericRelation( + "wagtailcore.WorkflowState", + content_type_field="base_content_type", + object_id_field="object_id", + related_query_name="full_featured_snippet", + for_concrete_model=False, + ) + + revisions = GenericRelation( + "wagtailcore.Revision", + content_type_field="base_content_type", + object_id_field="object_id", + related_query_name="full_featured_snippet", + for_concrete_model=False, + ) + search_fields = [ index.SearchField("text"), index.AutocompleteField("text"),