kopia lustrzana https://github.com/wagtail/wagtail
Add developer guide to new Task types
rodzic
b8c70163ca
commit
b508001336
|
@ -1,3 +1,267 @@
|
|||
============================
|
||||
How to add custom Task types
|
||||
============================
|
||||
=========================
|
||||
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()
|
Ładowanie…
Reference in New Issue