workflows.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. from django import forms
  2. from django.core.exceptions import ImproperlyConfigured, ValidationError
  3. from django.utils.functional import cached_property
  4. from django.utils.translation import gettext as _
  5. from django.utils.translation import gettext_lazy as __
  6. from wagtail.admin import widgets
  7. from wagtail.admin.edit_handlers import FieldPanel, InlinePanel, ObjectList
  8. from wagtail.admin.forms import WagtailAdminModelForm
  9. from wagtail.admin.widgets.workflows import AdminTaskChooser
  10. from wagtail.coreutils import get_model_string
  11. from wagtail.models import Page, Task, Workflow, WorkflowPage
  12. class TaskChooserSearchForm(forms.Form):
  13. q = forms.CharField(
  14. label=__("Search term"), widget=forms.TextInput(), required=False
  15. )
  16. def __init__(self, *args, task_type_choices=None, **kwargs):
  17. placeholder = kwargs.pop("placeholder", _("Search"))
  18. super().__init__(*args, **kwargs)
  19. self.fields["q"].widget.attrs = {"placeholder": placeholder}
  20. # Add task type filter if there is more than one task type option
  21. if task_type_choices and len(task_type_choices) > 1:
  22. self.fields["task_type"] = forms.ChoiceField(
  23. choices=(
  24. # Append an "All types" choice to the beginning
  25. [(None, _("All types"))]
  26. # The task type choices that are passed in use the models as values, we need
  27. # to convert these to something that can be represented in HTML
  28. + [
  29. (get_model_string(model), verbose_name)
  30. for model, verbose_name in task_type_choices
  31. ]
  32. ),
  33. required=False,
  34. )
  35. # Save a mapping of task_type values back to the model that we can reference later
  36. self.task_type_choices = {
  37. get_model_string(model): model for model, verbose_name in task_type_choices
  38. }
  39. def is_searching(self):
  40. """
  41. Returns True if the user typed a search query
  42. """
  43. return self.is_valid() and bool(self.cleaned_data.get("q"))
  44. @cached_property
  45. def task_model(self):
  46. """
  47. Returns the selected task model.
  48. This looks for the task model in the following order:
  49. 1) If there's only one task model option, return it
  50. 2) If a task model has been selected, return it
  51. 3) Return the generic Task model
  52. """
  53. models = list(self.task_type_choices.values())
  54. if len(models) == 1:
  55. return models[0]
  56. elif self.is_valid():
  57. model_name = self.cleaned_data.get("task_type")
  58. if model_name and model_name in self.task_type_choices:
  59. return self.task_type_choices[model_name]
  60. return Task
  61. def specific_task_model_selected(self):
  62. return self.task_model is not Task
  63. class WorkflowPageForm(forms.ModelForm):
  64. page = forms.ModelChoiceField(
  65. queryset=Page.objects.all(),
  66. widget=widgets.AdminPageChooser(target_models=[Page], can_choose_root=True),
  67. )
  68. class Meta:
  69. model = WorkflowPage
  70. fields = ["page"]
  71. def clean(self):
  72. page = self.cleaned_data.get("page")
  73. try:
  74. existing_workflow = page.workflowpage.workflow
  75. if not self.errors and existing_workflow != self.cleaned_data["workflow"]:
  76. # If the form has no errors, Page has an existing Workflow assigned, that Workflow is not
  77. # the selected Workflow, and overwrite_existing is not True, add a new error. This should be used to
  78. # trigger the confirmation message in the view. This is why this error is only added if there are no
  79. # other errors - confirmation should be the final step.
  80. self.add_error(
  81. "page",
  82. ValidationError(
  83. _("This page already has workflow '{0}' assigned.").format(
  84. existing_workflow
  85. ),
  86. code="existing_workflow",
  87. ),
  88. )
  89. except AttributeError:
  90. pass
  91. def save(self, commit=False):
  92. page = self.cleaned_data["page"]
  93. if commit:
  94. WorkflowPage.objects.update_or_create(
  95. page=page,
  96. defaults={"workflow": self.cleaned_data["workflow"]},
  97. )
  98. class BaseWorkflowPagesFormSet(forms.BaseInlineFormSet):
  99. def __init__(self, *args, **kwargs):
  100. super().__init__(*args, **kwargs)
  101. for form in self.forms:
  102. form.fields["DELETE"].widget = forms.HiddenInput()
  103. @property
  104. def empty_form(self):
  105. empty_form = super().empty_form
  106. empty_form.fields["DELETE"].widget = forms.HiddenInput()
  107. return empty_form
  108. def clean(self):
  109. """Checks that no two forms refer to the same page object"""
  110. if any(self.errors):
  111. # Don't bother validating the formset unless each form is valid on its own
  112. return
  113. pages = [
  114. form.cleaned_data["page"]
  115. for form in self.forms
  116. # need to check for presence of 'page' in cleaned_data,
  117. # because a completely blank form passes validation
  118. if form not in self.deleted_forms and "page" in form.cleaned_data
  119. ]
  120. if len(set(pages)) != len(pages):
  121. # pages list contains duplicates
  122. raise forms.ValidationError(
  123. _("You cannot assign this workflow to the same page multiple times.")
  124. )
  125. WorkflowPagesFormSet = forms.inlineformset_factory(
  126. Workflow,
  127. WorkflowPage,
  128. form=WorkflowPageForm,
  129. formset=BaseWorkflowPagesFormSet,
  130. extra=1,
  131. can_delete=True,
  132. fields=["page"],
  133. )
  134. class BaseTaskForm(forms.ModelForm):
  135. pass
  136. def get_task_form_class(task_model, for_edit=False):
  137. """
  138. Generates a form class for the given task model.
  139. If the form is to edit an existing task, set for_edit to True. This applies
  140. the readonly restrictions on fields defined in admin_form_readonly_on_edit_fields.
  141. """
  142. fields = task_model.admin_form_fields
  143. form_class = forms.modelform_factory(
  144. task_model,
  145. form=BaseTaskForm,
  146. fields=fields,
  147. widgets=getattr(task_model, "admin_form_widgets", {}),
  148. )
  149. if for_edit:
  150. for field_name in getattr(task_model, "admin_form_readonly_on_edit_fields", []):
  151. if field_name not in form_class.base_fields:
  152. raise ImproperlyConfigured(
  153. "`%s.admin_form_readonly_on_edit_fields` contains the field "
  154. "'%s' that doesn't exist. Did you forget to add "
  155. "it to `%s.admin_form_fields`?"
  156. % (task_model.__name__, field_name, task_model.__name__)
  157. )
  158. form_class.base_fields[field_name].disabled = True
  159. return form_class
  160. def get_workflow_edit_handler():
  161. """
  162. Returns an edit handler which provides the "name" and "tasks" fields for workflow.
  163. """
  164. # Note. It's a bit of a hack that we use edit handlers here. Ideally, it should be
  165. # made easier to reuse the inline panel templates for any formset.
  166. # Since this form is internal, we're OK with this for now. We might want to revisit
  167. # this decision later if we decide to allow custom fields on Workflows.
  168. panels = [
  169. FieldPanel(
  170. "name", heading=_("Give your workflow a name"), classname="full title"
  171. ),
  172. InlinePanel(
  173. "workflow_tasks",
  174. [
  175. FieldPanel("task", widget=AdminTaskChooser(show_clear_link=False)),
  176. ],
  177. heading=_("Add tasks to your workflow"),
  178. ),
  179. ]
  180. edit_handler = ObjectList(panels, base_form_class=WagtailAdminModelForm)
  181. return edit_handler.bind_to(model=Workflow)