Allow workflows to be disabled (#7218)

Introduce a WAGTAIL_WORKFLOW_ENABLED setting; when false, the workflow report and settings menus are hidden, permissions are not registered, moderation-related dashboard panels on the homepage are skipped, workflow actions on add/edit page are hidden, and model methods such as page.current_workflow_state return None / False immediately without any db queries.
pull/7222/head
Matt Westcott 2021-05-26 11:35:53 +01:00
rodzic 4dc68550bc
commit de9588590b
12 zmienionych plików z 149 dodań i 33 usunięć

Wyświetl plik

@ -9,6 +9,7 @@ Changelog
* Added support for customising group management views (Jan Seifert)
* Added `full_url` property to image renditions (Shreyash Srivastava)
* Added locale selector when choosing translatable snippets (Karl Hobley)
* Added `WAGTAIL_WORKFLOW_ENABLED` setting for enabling / disabling moderation workflows globally (Matt Westcott)
* Fix: Invalid filter values for foreign key fields in the API now give an error instead of crashing (Tidjani Dia)
* Fix: Ordering specified in `construct_explorer_page_queryset` hook is now taken into account again by the page explorer API (Andre Fonseca)
* Fix: Deleting a page from its listing view no longer results in a 404 error (Tidjani Dia)

Wyświetl plik

@ -712,6 +712,12 @@ When true, HTML tags in form field help text will be rendered unescaped (default
Workflow
========
.. code-block:: python
WAGTAIL_WORKFLOW_ENABLED = False
Specifies whether moderation workflows are enabled (default: True). When disabled, editors will no longer be given the option to submit pages to a workflow, and the settings areas for admins to configure workflows and tasks will be unavailable.
.. code-block:: python
WAGTAIL_WORKFLOW_REQUIRE_REAPPROVAL_ON_EDIT = True

Wyświetl plik

@ -17,6 +17,7 @@ Other features
* Added support for customising group management views. See :ref:`customising_group_views`. (Jan Seifert)
* Added ``full_url`` property to image renditions (Shreyash Srivastava)
* Added locale selector when choosing translatable snippets (Karl Hobley)
* Added ``WAGTAIL_WORKFLOW_ENABLED`` setting for enabling / disabling moderation workflows globally (Matt Westcott)
Bug fixes
~~~~~~~~~

Wyświetl plik

@ -116,6 +116,18 @@ class TestPageCreation(TestCase, WagtailTestUtils):
self.assertContains(response, 'testapp/js/siren.js')
# test construct_page_action_menu hook
self.assertContains(response, '<button type="submit" name="action-relax" value="Relax." class="button">Relax.</button>')
# test that workflow actions are shown
self.assertContains(
response, '<button type="submit" name="action-submit" value="Submit for moderation" class="button">'
)
@override_settings(WAGTAIL_WORKFLOW_ENABLED=False)
def test_workflow_buttons_not_shown_when_workflow_disabled(self):
response = self.client.get(reverse('wagtailadmin_pages:add', args=('tests', 'simplepage', self.root_page.id)))
self.assertEqual(response.status_code, 200)
self.assertNotContains(
response, 'value="Submit for moderation"'
)
def test_create_multipart(self):
"""

Wyświetl plik

@ -103,6 +103,19 @@ class TestPageEdit(TestCase, WagtailTestUtils):
self.assertContains(response,
'<button type="submit" name="action-relax" value="Relax." class="button">Relax.</button>')
# test that workflow actions are shown
self.assertContains(
response, '<button type="submit" name="action-submit" value="Submit to Moderators approval" class="button">'
)
@override_settings(WAGTAIL_WORKFLOW_ENABLED=False)
def test_workflow_buttons_not_shown_when_workflow_disabled(self):
response = self.client.get(reverse('wagtailadmin_pages:edit', args=(self.event_page.id, )))
self.assertEqual(response.status_code, 200)
self.assertNotContains(
response, 'value="Submit to Moderators approval"'
)
def test_edit_draft_page_with_no_revisions(self):
# Tests that the edit page loads
response = self.client.get(reverse('wagtailadmin_pages:edit', args=(self.unpublished_page.id, )))

Wyświetl plik

@ -26,6 +26,42 @@ def delete_existing_workflows():
WorkflowTask.objects.all().delete()
class TestWorkflowMenus(TestCase, WagtailTestUtils):
def setUp(self):
self.login()
self.editor = self.create_user(
username='editor',
email='editor@email.com',
password='password',
)
editors = Group.objects.get(name='Editors')
editors.user_set.add(self.editor)
def test_workflow_settings_and_reports_menus_are_shown_to_admin(self):
response = self.client.get('/admin/')
self.assertContains(response, 'href="/admin/workflows/list/"')
self.assertContains(response, 'href="/admin/workflows/tasks/index/"')
self.assertContains(response, 'href="/admin/reports/workflow/"')
self.assertContains(response, 'href="/admin/reports/workflow_tasks/"')
def test_workflow_settings_menus_are_not_shown_to_editor(self):
self.login(user=self.editor)
response = self.client.get('/admin/')
self.assertNotContains(response, 'href="/admin/workflows/list/"')
self.assertNotContains(response, 'href="/admin/workflows/tasks/index/"')
self.assertContains(response, 'href="/admin/reports/workflow/"')
self.assertContains(response, 'href="/admin/reports/workflow_tasks/"')
@override_settings(WAGTAIL_WORKFLOW_ENABLED=False)
def test_workflow_menus_are_hidden_when_workflows_are_disabled(self):
response = self.client.get('/admin/')
self.assertNotContains(response, 'href="/admin/workflows/list/"')
self.assertNotContains(response, 'href="/admin/workflows/tasks/index/"')
self.assertNotContains(response, 'href="/admin/reports/workflow/"')
self.assertNotContains(response, 'href="/admin/reports/workflow_tasks/"')
class TestWorkflowsIndexView(TestCase, WagtailTestUtils):
def setUp(self):

Wyświetl plik

@ -57,15 +57,18 @@ class UserPagesInWorkflowModerationPanel:
def __init__(self, request):
self.request = request
# Find in progress workflow states which are either requested by the user or on pages owned by the user
self.workflow_states = (
WorkflowState.objects.active()
.filter(Q(page__owner=request.user) | Q(requested_by=request.user))
.select_related(
'page', 'current_task_state', 'current_task_state__task', 'current_task_state__page_revision'
if getattr(settings, 'WAGTAIL_WORKFLOW_ENABLED', True):
# Find in progress workflow states which are either requested by the user or on pages owned by the user
self.workflow_states = (
WorkflowState.objects.active()
.filter(Q(page__owner=request.user) | Q(requested_by=request.user))
.select_related(
'page', 'current_task_state', 'current_task_state__task', 'current_task_state__page_revision'
)
.order_by('-current_task_state__started_at')
)
.order_by('-current_task_state__started_at')
)
else:
self.workflow_states = WorkflowState.objects.none()
def render(self):
return render_to_string('wagtailadmin/home/user_pages_in_workflow_moderation.html', {
@ -79,15 +82,18 @@ class WorkflowPagesToModeratePanel:
def __init__(self, request):
self.request = request
states = (
TaskState.objects.reviewable_by(request.user)
.select_related('page_revision', 'task', 'page_revision__page')
.order_by('-started_at')
)
self.states = [
(state, state.task.specific.get_actions(page=state.page_revision.page, user=request.user), state.workflow_state.all_tasks_with_status())
for state in states
]
if getattr(settings, 'WAGTAIL_WORKFLOW_ENABLED', True):
states = (
TaskState.objects.reviewable_by(request.user)
.select_related('page_revision', 'task', 'page_revision__page')
.order_by('-started_at')
)
self.states = [
(state, state.task.specific.get_actions(page=state.page_revision.page, user=request.user), state.workflow_state.all_tasks_with_status())
for state in states
]
else:
self.states = []
def render(self):
return render_to_string('wagtailadmin/home/workflow_pages_to_moderate.html', {

Wyświetl plik

@ -311,8 +311,12 @@ class EditView(TemplateResponseMixin, ContextMixin, HookResponseMixin, View):
self.edit_handler = self.edit_handler.bind_to(instance=self.page, request=self.request)
self.form_class = self.edit_handler.get_form_class()
# Retrieve current workflow state if set, default to last workflow state
self.workflow_state = self.page.current_workflow_state or self.page.workflow_states.order_by('created_at').last()
if getattr(settings, 'WAGTAIL_WORKFLOW_ENABLED', True):
# Retrieve current workflow state if set, default to last workflow state
self.workflow_state = self.page.current_workflow_state or self.page.workflow_states.order_by('created_at').last()
else:
self.workflow_state = None
if self.workflow_state:
self.workflow_tasks = self.workflow_state.all_tasks_with_status()
else:

Wyświetl plik

@ -1,3 +1,4 @@
from django.conf import settings
from django.contrib.auth.models import Permission
from django.urls import reverse
from django.utils.http import urlencode
@ -116,6 +117,9 @@ def register_collections_menu_item():
class WorkflowsMenuItem(MenuItem):
def is_shown(self, request):
if not getattr(settings, 'WAGTAIL_WORKFLOW_ENABLED', True):
return False
return workflow_permission_policy.user_has_any_permission(
request.user, ['add', 'change', 'delete']
)
@ -123,6 +127,9 @@ class WorkflowsMenuItem(MenuItem):
class WorkflowTasksMenuItem(MenuItem):
def is_shown(self, request):
if not getattr(settings, 'WAGTAIL_WORKFLOW_ENABLED', True):
return False
return task_permission_policy.user_has_any_permission(
request.user, ['add', 'change', 'delete']
)
@ -617,7 +624,7 @@ class LockedPagesMenuItem(MenuItem):
class WorkflowReportMenuItem(MenuItem):
def is_shown(self, request):
return True
return getattr(settings, 'WAGTAIL_WORKFLOW_ENABLED', True)
class SiteHistoryReportMenuItem(MenuItem):

Wyświetl plik

@ -2835,10 +2835,15 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
@property
def has_workflow(self):
"""Returns True if the page or an ancestor has an active workflow assigned, otherwise False"""
if not getattr(settings, 'WAGTAIL_WORKFLOW_ENABLED', True):
return False
return self.get_ancestors(inclusive=True).filter(workflowpage__isnull=False).filter(workflowpage__workflow__active=True).exists()
def get_workflow(self):
"""Returns the active workflow assigned to the page or its nearest ancestor"""
if not getattr(settings, 'WAGTAIL_WORKFLOW_ENABLED', True):
return None
if hasattr(self, 'workflowpage') and self.workflowpage.workflow.active:
return self.workflowpage.workflow
else:
@ -2852,11 +2857,15 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
@property
def workflow_in_progress(self):
"""Returns True if a workflow is in progress on the current page, otherwise False"""
if not getattr(settings, 'WAGTAIL_WORKFLOW_ENABLED', True):
return False
return WorkflowState.objects.filter(page=self, status=WorkflowState.STATUS_IN_PROGRESS).exists()
@property
def current_workflow_state(self):
"""Returns the in progress or needs changes workflow state on this page, if it exists"""
if not getattr(settings, 'WAGTAIL_WORKFLOW_ENABLED', True):
return None
try:
return WorkflowState.objects.active().select_related("current_task_state__task").get(page=self)
except WorkflowState.DoesNotExist:

Wyświetl plik

@ -102,6 +102,27 @@ class TestWorkflows(TestCase):
self.assertTrue(workflow_2.all_pages().filter(id=hello_page.id).exists())
self.assertTrue(workflow_2.all_pages().filter(id=goodbye_page.id).exists())
@override_settings(WAGTAIL_WORKFLOW_ENABLED=False)
def test_workflow_methods_generate_no_queries_when_disabled(self):
homepage = Page.objects.get(url_path='/home/')
with self.assertNumQueries(0):
self.assertEqual(homepage.has_workflow, False)
with self.assertNumQueries(0):
self.assertEqual(homepage.get_workflow(), None)
with self.assertNumQueries(0):
self.assertEqual(homepage.workflow_in_progress, False)
with self.assertNumQueries(0):
self.assertEqual(homepage.current_workflow_state, None)
with self.assertNumQueries(0):
self.assertEqual(homepage.current_workflow_task_state, None)
with self.assertNumQueries(0):
self.assertEqual(homepage.current_workflow_task, None)
@freeze_time("2017-01-01 12:00:00")
def test_start_workflow_on_page(self):
# test the first WorkflowState and TaskState models are set up correctly when Workflow.start(page) is used.

Wyświetl plik

@ -64,20 +64,20 @@ def register_collection_permissions():
)
@hooks.register('register_permissions')
def register_workflow_permissions():
return Permission.objects.filter(
content_type__app_label='wagtailcore',
codename__in=['add_workflow', 'change_workflow', 'delete_workflow']
)
if getattr(settings, 'WAGTAIL_WORKFLOW_ENABLED', True):
@hooks.register('register_permissions')
def register_workflow_permissions():
return Permission.objects.filter(
content_type__app_label='wagtailcore',
codename__in=['add_workflow', 'change_workflow', 'delete_workflow']
)
@hooks.register('register_permissions')
def register_task_permissions():
return Permission.objects.filter(
content_type__app_label='wagtailcore',
codename__in=['add_task', 'change_task', 'delete_task']
)
@hooks.register('register_permissions')
def register_task_permissions():
return Permission.objects.filter(
content_type__app_label='wagtailcore',
codename__in=['add_task', 'change_task', 'delete_task']
)
@hooks.register('describe_collection_contents')