<commit_before>============================
How to add custom Task types
============================<commit_msg>Add developer guide to new Task types<commit_after>=========================
How to add new Task types
=========================

The Workflow system introduced in Wagtail 2.9 allows users to create tasks, which represent stages of moderation.

Wagtail provides one built in task type: ``GroupApprovalTask``, which allows any user in specific groups to approve or reject moderation.

However, it is possible to add your task types in code. Instances of your custom task can then be created in the ``Tasks`` section of the Wagtail Admin.

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 - 
can be added to ``exlude_on_edit``. 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')]

        # prevent editing of ``user`` after the task is created
        exclude_on_edit = {'user'}


Custom TaskState models
~~~~~~~~~~~~~~~~~~~~~~~

You might also need to store custom state information for the task. 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``:

.. 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')]

        # 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()
{'fraction_non_alphanumeric': 0.09812216260833677, 'fraction_numerical': 0.0002063557573256294, 'mean_word_length': 3.6201143946615826, 'pattern_counts': {'":': 0, '<': 12, '<?xml version=': 0, '>': 12, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3936833', 'n_tokens_mistral': 2646, 'n_tokens_neox': 2525, 'n_words': 945}