31
loading...
This website collects cookies to deliver better user experience
Page
model itself where a mix of custom code and CMS Admin editing can be combined to make something incredibly flexible and powerful.Task
model is and what we want to let the user fill out.Workflow
area has a few key models, the main being a Workflow
and a Task
.Task
in the Wagtail admin, the user is presented with what kind of Task
to use (similar to when creating a new Page
), this UI only shows if there are more than one kind of Task
models available (Wagtail will search through your models and find all those that are subclasses of the core Task
model).Task
model we are creating contains the fields that the user enters for multiple Task
s of that 'kind', which are then mapped to one or more user created Workflow
s.Task
model in that case, we do not actually need to define sets of checklists but rather a way for users to enter a set of checklist items and the simplest way to do this would be with a multi-line TextField
where each line becomes a checklist item.Task
will require ALL checklist items to be ticked when submitting, this way the checklists can be a 'suggestion' or a 'requirement' on a per Task
instance basis.Task
instance can be changed at any time, so the checklist the user views today could be different tomorrow and as such we will keep this implementation simpler by not tracking that the checklist was submitted and which items were ticked but that could be implemented as an enhancement down the road.GroupApprovalTask
.GroupApprovalTask
is that our ChecklistApprovalTask
is very similar, we want to assign a user group that can approve/reject as a Task
but we just want to allow the approve step to show extra content in the approval screen.ChecklistApprovalTask
instances. For the rest of this tutorial it would be good to have one ready to go to test with as we build out the features.ChecklistApprovalTask
that extends GroupApprovalTask
.checklist
a TextField
which will be used to generate the checklist items (per line), and a is_checklist_required
BooleanField
which will be ticked to force each checklist item to be ticked.Page
panels
, we can use the admin_form_fields
class attribute to define a List of fields that will be shown when a user creates/edits this Task
.get_description
class method and the meta verbose names to provide a user-facing description & name of this Task
type, plus we want to override the one that comes with the GroupApprovalTask
class.django-admin makemigrations
and then django-admin migrate
to apply your new model.
from django.db import models
from wagtail.core.models import GroupApprovalTask
class ChecklistApprovalTask(GroupApprovalTask):
"""
Custom task type where all the features of the GroupApprovalTask will exist but
with the ability to define a custom checklist that may be required to be checked for
Approval of this step. Checklist field will be a multi-line field, each line being
one checklist item.
"""
# Reminder: Already has 'groups' field as we are extending `GroupApprovalTask`
checklist = models.TextField(
"Checklist",
help_text="Each line will become a checklist item shown on the Approve step.",
)
is_checklist_required = models.BooleanField(
"Required",
help_text="If required, all items in the checklist must be ticked to approve.",
blank=True,
)
admin_form_fields = GroupApprovalTask.admin_form_fields + [
"checklist",
"is_checklist_required",
]
@classmethod
def get_description(cls):
return (
"Members of the chosen User Groups can approve this task with a checklist."
)
class Meta:
verbose_name = "Checklist approval task"
verbose_name_plural = "Checklist approval tasks"
GroupApprovalTask
, when in a Workflow, will give the user two options; 'Approve and Publish' and 'Approve with Comment and Publish', the difference is that the one with the comment will open a form modal when clicked where the user can fill out a comment.Task
is to ensure that the approval step can only be completed with a form modal variant, and in this form we will show the checklist.Task
has a method get_actions
which will return a list of (action_name, action_verbose_name, action_requires_additional_data_from_modal)
tuples.CrosscheckApprovalTask
built above, create a new method get_actions
, this should copy the user check from the GroupApprovalTask implementation but only return two actions.
class CrosscheckApprovalTask(GroupApprovalTask):
# ... checklist etc, from above step
def get_actions(self, page, user):
"""
Customise the actions returned to have a reject and only one approve.
The approve will have a third value as True which indicates a form is
required.
"""
if self.groups.filter(id__in=user.groups.all()).exists() or user.is_superuser:
REJECT = ("reject", "Request changes", True)
APPROVE = ("approve", "Approve", True)
return [REJECT, APPROVE]
return []
# ... @classmethod etc
GroupApprovalTask
makes, this form needs to have a MultipleChoiceField
where each of the choices
is a line in our Task
model's checklist
field.Task
model's is_checklist_required
saved value.Task
form we can add a get_form_for_action
method and when the action is 'approve'
we can provide a custom Form.TaskState
(the model that reflects each state for a Task
as it is processed).get_form_for_action
on the GroupApprovalTask
we can see that it returns a TaskStateCommentForm
which extends a Django Form
with one field, comments
.type
built-in function, passing in three args. A name, base classes tuple and a dict where each key will be used to generate dynamic attributes (fields) and methods (e.g. the clean method).get_form_for_action
returns, this way we do not need to think about what this is in the code, but know that it is the TaskStateCommentForm
above.clean
method that will remove any checklist values that are submitted (as we do not want to save these).checklist
, which we will pull out to a new class method get_checklist_field
which can return a forms.MultipleChoiceField
that has dynamic values for required
and the choices
based on the Task
instance. Note: The default widget used for this field is SelectMultiple
which is a bit cluncky, but we will enhance that in the next step.checklist
field shows before the comment
field, for that we can dynamically add a field_order
attribute.
from django import forms
from django.db import models
from django.utils.translation import gettext_lazy as _
# ... other imports
class ChecklistApprovalTask(GroupApprovalTask):
# ... checklist etc, from above step
def get_checklist_field(self):
"""
Prepare a form field that is a list of checklist boxes for each line in the
checklist on this Task instance.
"""
required = self.is_checklist_required
field = dict(label=_("Checklist"), required=required)
field["choices"] = [
(index, label) for index, label in enumerate(self.checklist.splitlines())
]
return forms.MultipleChoiceField(**field)
def get_form_for_action(self, action):
"""
If the action is 'approve', return a new class (using type) that has access
to the checklist items as a field based on this Task's instance.
"""
form_class = super().get_form_for_action(action)
if action == "approve":
def clean(form):
"""
When this form's clean method is processed (on a POST), ensure we do not pass
the 'checklist' data any further as no handling of this data is built.
"""
cleaned_data = super(form_class, form).clean()
if "checklist" in cleaned_data:
del cleaned_data["checklist"]
return cleaned_data
return type(
str("ChecklistApprovalTaskForm"),
(form_class,),
dict(
checklist=self.get_checklist_field(),
clean=clean,
field_order=["checklist", "comment"],
),
)
return form_class
# ... @classmethod etc
help_text
, validators
and the widget
of our field generated in the method get_checklist_field
.help_text
needs to be a dynamic value based on the required value we set up in the previous step.MinLengthValidator
, while this is usually used to validate string length it can be used just the same for validating the length of the list of values provided to the field (in our case it will be a list of indices). We will also pass in a custom message
kwarg to this validator so it makes sense to the user.widget
we will use the built-in CheckboxSelectMultiple
, but note in the docs that even if we set required
on the field the checkbox will not actually put required on the inputs HTML attributes so we need to pass in an extra attrs
to the widget to handle this.min_length_validator
, help_text
, validators
and widget
lines.
from django import forms
from django.core.validators import MinLengthValidator
from django.db import models
from django.utils.translation import gettext_lazy as _, ngettext_lazy
# ... other imports
class ChecklistApprovalTask(GroupApprovalTask):
# ... checklist etc, from above step
def get_checklist_field(self):
"""
Prepare a form field that is a list of checklist boxes for each line in the
checklist on this Task instance.
"""
required = self.is_checklist_required
field = dict(label=_("Checklist"), required=required)
field["choices"] = [
(index, label) for index, label in enumerate(self.checklist.splitlines())
]
min_length_validator = MinLengthValidator(
len(field["choices"]),
message=ngettext_lazy(
"Only %(show_value)d item has been checked (all %(limit_value)d are required).",
"Only %(show_value)d items have been checked (all %(limit_value)d are required).",
"show_value",
),
)
field["help_text"] = (
_("Please check all items.") if required else _("Please review all items.")
)
field["validators"] = [min_length_validator] if required else []
field["widget"] = forms.CheckboxSelectMultiple(
# required attr needed as not rendered by default (even if field required)
# https://docs.djangoproject.com/en/3.2/ref/forms/widgets/#django.forms.CheckboxSelectMultiple
attrs={"required": "required"} if required else {}
)
return forms.MultipleChoiceField(**field)
description
field to the Task
so that users can put content above the checklist that explains part of a process.