2020-02-19 15:46:46 +00:00
=========================
How to add new Task types
=========================
2020-02-27 11:59:46 +00:00
The Workflow system allows users to create tasks, which represent stages of moderation.
2020-02-19 15:46:46 +00:00
Wagtail provides one built in task type: `` GroupApprovalTask `` , which allows any user in specific groups to approve or reject moderation.
2020-02-27 12:07:58 +00:00
However, it is possible to add your own task types in code. Instances of your custom task can then be created in the `` Tasks `` section of the Wagtail Admin.
2020-02-19 15:46:46 +00:00
Task models
~~~~~~~~~~~
All custom tasks must be models inheriting from `` wagtailcore.Task `` . In this set of examples, we'll set up a task which can be approved by only one specific user.
.. code-block :: python
# <project>/models.py
from wagtail.core.models import Task
class UserApprovalTask(Task):
pass
Subclassed Tasks follow the same approach as Pages: they are concrete models, with the specific subclass instance accessible by calling `` Task.specific() `` .
You can now add any custom fields. To make these editable in the admin, they must also be added as panels.
For example:
.. code-block :: python
# <project>/models.py
from django.conf import settings
from django.db import models
from wagtail.admin.edit_handlers import FieldPanel
from wagtail.core.models import Task
class UserApprovalTask(Task):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=False)
panels = Task.panels + [FieldPanel('user')]
Any fields that shouldn't be edited after task creation - for example, anything that would fundamentally change the meaning of the task in any history logs -
2020-02-27 12:07:58 +00:00
can be added to `` exclude_on_edit `` . For example:
2020-02-19 15:46:46 +00:00
.. code-block :: python
# <project>/models.py
from django.conf import settings
from django.db import models
from wagtail.admin.edit_handlers import FieldPanel
from wagtail.core.models import Task
class UserApprovalTask(Task):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=False)
panels = Task.panels + [FieldPanel('user')]
# prevent editing of `` user `` after the task is created
exclude_on_edit = {'user'}
Custom TaskState models
~~~~~~~~~~~~~~~~~~~~~~~
2020-02-27 12:07:58 +00:00
You might also need to store custom state information for the task: for example, a comment left by an approving user.
Normally, this is done on an instance of `` TaskState `` , which is created when a page starts the task. However, this can
also be subclassed equivalently to `` Task `` :
2020-02-19 15:46:46 +00:00
.. code-block :: python
# <project>/models.py
from wagtail.core.models import TaskState
class UserApprovalTaskState(TaskState):
pass
Your custom task must then be instructed to generate an instance of your custom task state on start instead of a plain `` TaskState `` instance:
.. code-block :: python
# <project>/models.py
from django.conf import settings
from django.db import models
from wagtail.admin.edit_handlers import FieldPanel
from wagtail.core.models import Task, TaskState
class UserApprovalTaskState(TaskState):
pass
class UserApprovalTask(Task):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=False)
panels = Task.panels + [FieldPanel('user')]
2020-02-24 15:18:06 +00:00
task_state_class = UserApprovalTaskState
2020-02-19 15:46:46 +00:00
# prevent editing of `` user `` after the task is created
exclude_on_edit = {'user'}
Customising behaviour
~~~~~~~~~~~~~~~~~~~~~
Both `` Task `` and `` TaskState `` have a number of methods which can be overridden to implement custom behaviour. Here are some of the most useful:
`` Task.user_can_access_editor(page, user) `` , `` Task.user_can_lock(page, user) `` , `` Task.user_can_unlock(page, user) `` :
These methods determine if users usually without permissions can access the editor, lock, or unlock the page, by returning True or False.
Note that returning `` False `` will not prevent users who would normally be able to perform those actions. For example, for our `` UserApprovalTask `` :
.. code-block :: python
def user_can_access_editor(self, page, user):
return user == self.user
`` Task.get_actions(page, user) `` :
This returns a list of `` (action_name, action_verbose_name) `` tuples, corresponding to the actions available for the task in the edit view menu.
For example:
.. code-block :: python
def get_actions(self, page, user):
if user == self.user:
return [
('approve', "Approve"),
('reject', "Reject"),
('cancel', "Cancel"),
]
else:
return []
`` Task.on_action(task_state, user, action_name) `` :
This performs the actions specified in `` Task.get_actions(page, user) `` : it is passed an action name, eg `` approve `` , and the relevant task state. By default,
it calls `` approve `` and `` reject `` methods on the task state when the corresponding action names are passed through.
For example, let's say we wanted to add an additional option: cancelling the entire workflow:
.. code-block :: python
def on_action(self, task_state, user, action_name):
if action_name == 'cancel':
return task_state.workflow_state.cancel(user=user)
else:
return super().on_action(task_state, user, workflow_state)
`` Task.get_task_states_user_can_moderate(user, **kwargs) `` :
This returns a QuerySet of `` TaskStates `` (or subclasses) the given user can moderate - this is currently used to select pages to display on the user's dashboard.
For example:
.. code-block :: python
def get_task_states_user_can_moderate(self, user, **kwargs):
if user == self.user:
# get all task states linked to the (base class of) current task
return TaskState.objects.filter(status=TaskState.STATUS_IN_PROGRESS, task=self.task_ptr)
else:
return TaskState.objects.none()
Adding notifications
~~~~~~~~~~~~~~~~~~~~
Wagtail's notifications are sent by `` wagtail.admin.mail.Notifier `` subclasses: callables intended to be connected to a signal.
By default, email notifications are sent upon workflow submission, approval and rejection, and upon submission to a group approval task.
As an example, we'll add email notifications for when our new task is started.
.. code-block :: python
# <project>/mail.py
from wagtail.admin.mail import EmailNotifier
from wagtail.core.models import TaskState
from .models import UserApprovalTaskState
class BaseUserApprovalTaskStateEmailNotifier(EmailNotifier):
"""A base EmailNotifier to send updates for UserApprovalTask events"""
def __init__(self):
# Allow UserApprovalTaskState and TaskState to send notifications
super().__init__((UserApprovalTaskState, TaskState))
def can_handle(self, instance, **kwargs):
if super().can_handle(instance, **kwargs) and isinstance(instance.task.specific, UserApprovalTask):
# Don't send notifications if a Task has been cancelled and then resumed - ie page was updated to a new revision
return not TaskState.objects.filter(workflow_state=instance.workflow_state, task=instance.task, status=TaskState.STATUS_CANCELLED).exists()
return False
def get_context(self, task_state, **kwargs):
context = super().get_context(task_state, **kwargs)
context['page'] = task_state.workflow_state.page
context['task'] = task_state.task.specific
return context
def get_recipient_users(self, task_state, **kwargs):
# Send emails to the user assigned to the task
approving_user = task_state.task.specific.user
recipients = {approving_user}
return recipients
def get_template_base_prefix(self, instance, **kwargs):
# Get the template base prefix for TaskState, so use the `` wagtailadmin/notifications/task_state_ `` set of notification templates
return super().get_template_base_prefix(self, instance.task_state_ptr, **kwargs)
class UserApprovalTaskStateSubmissionEmailNotifier(BaseUserApprovalTaskStateEmailNotifier):
"""An EmailNotifier to send updates for UserApprovalTask submission events"""
notification = 'submitted'
Similarly, you could define notifier subclasses for approval and rejection notifications.
Next, you need to instantiate the notifier, and connect it to the `` task_submitted `` signal.
.. code-block :: python
# <project>/signal_handlers.py
from wagtail.core.signals import task_submitted
from .mail import UserApprovalTaskStateSubmissionEmailNotifier
task_submission_email_notifier = UserApprovalTaskStateSubmissionEmailNotifier()
def register_signal_handlers():
task_submitted.connect(user_approval_task_submission_email_notifier, dispatch_uid='user_approval_task_submitted_email_notification')
`` register_signal_handlers() `` should then be run on loading the app: for example, by adding it to the `` ready() `` method in your `` AppConfig ``
(and making sure you set this config is set as `` default_app_config `` in `` <project>/__init__.py `` ).
.. code-block :: python
# <project>/apps.py
from django.apps import AppConfig
class MyAppConfig(AppConfig):
name = 'myappname'
label = 'myapplabel'
verbose_name = 'My verbose app name'
def ready(self):
from .signal_handlers import register_signal_handlers
register_signal_handlers()