990 lines
34 KiB
Python
990 lines
34 KiB
Python
|
|
import django_filters
|
||
|
|
from django import forms
|
||
|
|
from django.contrib.contenttypes.models import ContentType
|
||
|
|
from django.core.exceptions import PermissionDenied
|
||
|
|
from django.core.paginator import Paginator
|
||
|
|
from django.db import transaction
|
||
|
|
from django.db.models import Count, OuterRef, Prefetch
|
||
|
|
from django.db.models.functions import Lower
|
||
|
|
from django.http import Http404, HttpResponseBadRequest
|
||
|
|
from django.shortcuts import get_object_or_404, redirect, render
|
||
|
|
from django.template.loader import render_to_string
|
||
|
|
from django.urls import reverse
|
||
|
|
from django.utils.functional import cached_property
|
||
|
|
from django.utils.http import url_has_allowed_host_and_scheme
|
||
|
|
from django.utils.text import capfirst
|
||
|
|
from django.utils.translation import gettext_lazy as _
|
||
|
|
from django.utils.translation import ngettext
|
||
|
|
from django.views.decorators.http import require_POST
|
||
|
|
from django.views.generic import TemplateView
|
||
|
|
|
||
|
|
from wagtail.admin import messages
|
||
|
|
from wagtail.admin.auth import PermissionPolicyChecker
|
||
|
|
from wagtail.admin.filters import MultipleContentTypeFilter, WagtailFilterSet
|
||
|
|
from wagtail.admin.forms.workflows import (
|
||
|
|
TaskChooserSearchForm,
|
||
|
|
WorkflowContentTypeForm,
|
||
|
|
WorkflowPagesFormSet,
|
||
|
|
get_task_form_class,
|
||
|
|
get_workflow_edit_handler,
|
||
|
|
)
|
||
|
|
from wagtail.admin.modal_workflow import render_modal_workflow
|
||
|
|
from wagtail.admin.ui.tables import BaseColumn, Column, TitleColumn
|
||
|
|
from wagtail.admin.views.generic import CreateView, DeleteView, EditView, IndexView
|
||
|
|
from wagtail.coreutils import resolve_model_string
|
||
|
|
from wagtail.models import (
|
||
|
|
Page,
|
||
|
|
Task,
|
||
|
|
TaskState,
|
||
|
|
Workflow,
|
||
|
|
WorkflowContentType,
|
||
|
|
WorkflowState,
|
||
|
|
WorkflowTask,
|
||
|
|
)
|
||
|
|
from wagtail.permissions import (
|
||
|
|
page_permission_policy,
|
||
|
|
task_permission_policy,
|
||
|
|
workflow_permission_policy,
|
||
|
|
)
|
||
|
|
from wagtail.snippets.models import get_workflow_enabled_models
|
||
|
|
from wagtail.workflows import get_task_types
|
||
|
|
|
||
|
|
task_permission_checker = PermissionPolicyChecker(task_permission_policy)
|
||
|
|
|
||
|
|
|
||
|
|
class WorkflowTitleColumn(TitleColumn):
|
||
|
|
cell_template_name = "wagtailadmin/workflows/includes/workflow_title_cell.html"
|
||
|
|
|
||
|
|
|
||
|
|
class WorkflowUsedByColumn(TitleColumn):
|
||
|
|
cell_template_name = "wagtailadmin/workflows/includes/workflow_used_by_cell.html"
|
||
|
|
|
||
|
|
def get_cell_context_data(self, instance, parent_context):
|
||
|
|
context = super().get_cell_context_data(instance, parent_context)
|
||
|
|
context["workflow_enabled_models"] = get_workflow_enabled_models()
|
||
|
|
return context
|
||
|
|
|
||
|
|
|
||
|
|
class WorkflowTasksColumn(BaseColumn):
|
||
|
|
cell_template_name = "wagtailadmin/workflows/includes/workflow_tasks_cell.html"
|
||
|
|
num_tasks = 5
|
||
|
|
|
||
|
|
def get_cell_context_data(self, instance, parent_context):
|
||
|
|
context = super().get_cell_context_data(instance, parent_context)
|
||
|
|
context["tasks"] = instance.workflow_tasks.all()[: self.num_tasks]
|
||
|
|
context["extra_count"] = instance.workflow_tasks.count() - self.num_tasks
|
||
|
|
return context
|
||
|
|
|
||
|
|
|
||
|
|
class BaseWorkflowFilterSet(WagtailFilterSet):
|
||
|
|
show_disabled = django_filters.ChoiceFilter(
|
||
|
|
label=_("Show disabled"),
|
||
|
|
method="filter_show_disabled",
|
||
|
|
choices=(("true", _("Yes")), ("false", _("No"))),
|
||
|
|
widget=forms.RadioSelect,
|
||
|
|
empty_label=None,
|
||
|
|
initial="false",
|
||
|
|
)
|
||
|
|
|
||
|
|
def __init__(self, data=None, queryset=None, *, request=None, prefix=None):
|
||
|
|
if data is not None:
|
||
|
|
if data.get("show_disabled") is None:
|
||
|
|
filter = self.base_filters["show_disabled"]
|
||
|
|
data = data.copy()
|
||
|
|
data["show_disabled"] = filter.extra["initial"]
|
||
|
|
super().__init__(data, queryset, request=request, prefix=prefix)
|
||
|
|
|
||
|
|
def filter_show_disabled(self, queryset, name, value):
|
||
|
|
if value == "true":
|
||
|
|
return queryset
|
||
|
|
return queryset.filter(active=True)
|
||
|
|
|
||
|
|
|
||
|
|
class WorkflowFilterSet(BaseWorkflowFilterSet):
|
||
|
|
class Meta:
|
||
|
|
model = Workflow
|
||
|
|
fields = []
|
||
|
|
|
||
|
|
|
||
|
|
class Index(IndexView):
|
||
|
|
permission_policy = workflow_permission_policy
|
||
|
|
model = Workflow
|
||
|
|
context_object_name = "workflows"
|
||
|
|
template_name = "wagtailadmin/workflows/index.html"
|
||
|
|
results_template_name = "wagtailadmin/workflows/index_results.html"
|
||
|
|
add_url_name = "wagtailadmin_workflows:add"
|
||
|
|
edit_url_name = "wagtailadmin_workflows:edit"
|
||
|
|
index_url_name = "wagtailadmin_workflows:index"
|
||
|
|
index_results_url_name = "wagtailadmin_workflows:index_results"
|
||
|
|
page_title = _("Workflows")
|
||
|
|
add_item_label = _("Add a workflow")
|
||
|
|
header_icon = "tasks"
|
||
|
|
columns = [
|
||
|
|
WorkflowTitleColumn(
|
||
|
|
"name",
|
||
|
|
label=_("Name"),
|
||
|
|
url_name="wagtailadmin_workflows:edit",
|
||
|
|
width="25%",
|
||
|
|
sort_key="name",
|
||
|
|
),
|
||
|
|
WorkflowUsedByColumn(
|
||
|
|
"usage",
|
||
|
|
label=_("Used by"),
|
||
|
|
url_name="wagtailadmin_workflows:usage",
|
||
|
|
width="15%",
|
||
|
|
),
|
||
|
|
WorkflowTasksColumn("tasks", label=_("Tasks")),
|
||
|
|
]
|
||
|
|
default_ordering = "name"
|
||
|
|
search_fields = ["name"]
|
||
|
|
filterset_class = WorkflowFilterSet
|
||
|
|
_show_breadcrumbs = True
|
||
|
|
paginate_by = 20
|
||
|
|
|
||
|
|
def show_disabled(self):
|
||
|
|
return self.filters.form.cleaned_data.get("show_disabled") == "true"
|
||
|
|
|
||
|
|
def get_base_queryset(self):
|
||
|
|
queryset = super().get_base_queryset()
|
||
|
|
content_types = WorkflowContentType.objects.filter(
|
||
|
|
workflow=OuterRef("pk")
|
||
|
|
).values_list("pk", flat=True)
|
||
|
|
queryset = queryset.annotate(content_types=Count(content_types))
|
||
|
|
return queryset.prefetch_related(
|
||
|
|
"workflow_pages",
|
||
|
|
"workflow_pages__page",
|
||
|
|
"workflow_tasks",
|
||
|
|
"workflow_tasks__task",
|
||
|
|
)
|
||
|
|
|
||
|
|
def get_context_data(self, **kwargs):
|
||
|
|
context = super().get_context_data(**kwargs)
|
||
|
|
context["showing_disabled"] = self.show_disabled()
|
||
|
|
return context
|
||
|
|
|
||
|
|
|
||
|
|
class Create(CreateView):
|
||
|
|
permission_policy = workflow_permission_policy
|
||
|
|
model = Workflow
|
||
|
|
page_title = _("New workflow")
|
||
|
|
template_name = "wagtailadmin/workflows/create.html"
|
||
|
|
success_message = _("Workflow '%(object)s' created.")
|
||
|
|
add_url_name = "wagtailadmin_workflows:add"
|
||
|
|
edit_url_name = "wagtailadmin_workflows:edit"
|
||
|
|
index_url_name = "wagtailadmin_workflows:index"
|
||
|
|
header_icon = "tasks"
|
||
|
|
edit_handler = None
|
||
|
|
_show_breadcrumbs = True
|
||
|
|
|
||
|
|
def get_edit_handler(self):
|
||
|
|
if not self.edit_handler:
|
||
|
|
self.edit_handler = get_workflow_edit_handler()
|
||
|
|
return self.edit_handler
|
||
|
|
|
||
|
|
def get_form_class(self):
|
||
|
|
return self.get_edit_handler().get_form_class()
|
||
|
|
|
||
|
|
def get_pages_formset(self):
|
||
|
|
if self.request.method == "POST":
|
||
|
|
return WorkflowPagesFormSet(
|
||
|
|
self.request.POST, instance=self.object, prefix="pages"
|
||
|
|
)
|
||
|
|
else:
|
||
|
|
return WorkflowPagesFormSet(instance=self.object, prefix="pages")
|
||
|
|
|
||
|
|
def get_content_type_form(self):
|
||
|
|
if self.request.method == "POST":
|
||
|
|
return WorkflowContentTypeForm(self.request.POST, workflow=self.object)
|
||
|
|
else:
|
||
|
|
return WorkflowContentTypeForm(workflow=self.object)
|
||
|
|
|
||
|
|
def get_context_data(self, **kwargs):
|
||
|
|
context = super().get_context_data(**kwargs)
|
||
|
|
form = context["form"]
|
||
|
|
bound_panel = self.edit_handler.get_bound_panel(
|
||
|
|
form=form, instance=form.instance, request=self.request
|
||
|
|
)
|
||
|
|
pages_formset = self.get_pages_formset()
|
||
|
|
|
||
|
|
context["edit_handler"] = bound_panel
|
||
|
|
context["pages_formset"] = pages_formset
|
||
|
|
context["has_workflow_enabled_models"] = bool(get_workflow_enabled_models())
|
||
|
|
context["content_type_form"] = self.get_content_type_form()
|
||
|
|
context["media"] = form.media + bound_panel.media + pages_formset.media
|
||
|
|
return context
|
||
|
|
|
||
|
|
def form_valid(self, form):
|
||
|
|
self.form = form
|
||
|
|
|
||
|
|
with transaction.atomic():
|
||
|
|
self.object = self.save_instance()
|
||
|
|
|
||
|
|
pages_formset = self.get_pages_formset()
|
||
|
|
content_type_form = self.get_content_type_form()
|
||
|
|
if pages_formset.is_valid() and content_type_form.is_valid():
|
||
|
|
pages_formset.save()
|
||
|
|
content_type_form.save()
|
||
|
|
|
||
|
|
success_message = self.get_success_message(self.object)
|
||
|
|
if success_message is not None:
|
||
|
|
messages.success(
|
||
|
|
self.request,
|
||
|
|
success_message,
|
||
|
|
buttons=[
|
||
|
|
messages.button(
|
||
|
|
reverse(self.edit_url_name, args=(self.object.id,)),
|
||
|
|
_("Edit"),
|
||
|
|
)
|
||
|
|
],
|
||
|
|
)
|
||
|
|
return redirect(self.get_success_url())
|
||
|
|
|
||
|
|
else:
|
||
|
|
transaction.set_rollback(True)
|
||
|
|
|
||
|
|
return self.form_invalid(form)
|
||
|
|
|
||
|
|
|
||
|
|
class Edit(EditView):
|
||
|
|
permission_policy = workflow_permission_policy
|
||
|
|
model = Workflow
|
||
|
|
page_title = _("Editing workflow")
|
||
|
|
template_name = "wagtailadmin/workflows/edit.html"
|
||
|
|
success_message = _("Workflow '%(object)s' updated.")
|
||
|
|
add_url_name = "wagtailadmin_workflows:add"
|
||
|
|
edit_url_name = "wagtailadmin_workflows:edit"
|
||
|
|
delete_url_name = "wagtailadmin_workflows:disable"
|
||
|
|
delete_item_label = _("Disable")
|
||
|
|
index_url_name = "wagtailadmin_workflows:index"
|
||
|
|
enable_item_label = _("Enable")
|
||
|
|
enable_url_name = "wagtailadmin_workflows:enable"
|
||
|
|
header_icon = "tasks"
|
||
|
|
edit_handler = None
|
||
|
|
MAX_PAGES = 5
|
||
|
|
_show_breadcrumbs = True
|
||
|
|
|
||
|
|
def get_edit_handler(self):
|
||
|
|
if not self.edit_handler:
|
||
|
|
self.edit_handler = get_workflow_edit_handler()
|
||
|
|
return self.edit_handler
|
||
|
|
|
||
|
|
def get_form_class(self):
|
||
|
|
return self.get_edit_handler().get_form_class()
|
||
|
|
|
||
|
|
def get_pages_formset(self):
|
||
|
|
if self.request.method == "POST":
|
||
|
|
return WorkflowPagesFormSet(
|
||
|
|
self.request.POST, instance=self.get_object(), prefix="pages"
|
||
|
|
)
|
||
|
|
else:
|
||
|
|
return WorkflowPagesFormSet(instance=self.get_object(), prefix="pages")
|
||
|
|
|
||
|
|
def get_content_type_form(self):
|
||
|
|
if self.request.method == "POST":
|
||
|
|
return WorkflowContentTypeForm(self.request.POST, workflow=self.object)
|
||
|
|
else:
|
||
|
|
return WorkflowContentTypeForm(workflow=self.object)
|
||
|
|
|
||
|
|
def get_paginated_pages(self):
|
||
|
|
# Get the (paginated) list of Pages to which this Workflow is assigned.
|
||
|
|
pages = Page.objects.filter(workflowpage__workflow=self.get_object())
|
||
|
|
pages.paginator = Paginator(pages, self.MAX_PAGES)
|
||
|
|
page_number = int(self.request.GET.get("p", 1))
|
||
|
|
paginated_pages = pages.paginator.page(page_number)
|
||
|
|
return paginated_pages
|
||
|
|
|
||
|
|
def get_context_data(self, **kwargs):
|
||
|
|
context = super().get_context_data(**kwargs)
|
||
|
|
form = context["form"]
|
||
|
|
bound_panel = self.edit_handler.get_bound_panel(
|
||
|
|
form=form, instance=form.instance, request=self.request
|
||
|
|
)
|
||
|
|
pages_formset = self.get_pages_formset()
|
||
|
|
context["edit_handler"] = bound_panel
|
||
|
|
context["pages"] = self.get_paginated_pages()
|
||
|
|
context["pages_formset"] = pages_formset
|
||
|
|
context["has_workflow_enabled_models"] = bool(get_workflow_enabled_models())
|
||
|
|
context["content_type_form"] = self.get_content_type_form()
|
||
|
|
context["can_disable"] = (
|
||
|
|
self.permission_policy is None
|
||
|
|
or self.permission_policy.user_has_permission(self.request.user, "delete")
|
||
|
|
) and self.object.active
|
||
|
|
context["can_enable"] = (
|
||
|
|
self.permission_policy is None
|
||
|
|
or self.permission_policy.user_has_permission(self.request.user, "add")
|
||
|
|
) and not self.object.active
|
||
|
|
context["media"] = bound_panel.media + form.media + pages_formset.media
|
||
|
|
return context
|
||
|
|
|
||
|
|
@property
|
||
|
|
def get_enable_url(self):
|
||
|
|
return reverse(self.enable_url_name, args=(self.object.pk,))
|
||
|
|
|
||
|
|
@transaction.atomic()
|
||
|
|
def form_valid(self, form):
|
||
|
|
self.form = form
|
||
|
|
|
||
|
|
with transaction.atomic():
|
||
|
|
self.object = self.save_instance()
|
||
|
|
successful = True
|
||
|
|
|
||
|
|
# Save pages formset and content type form
|
||
|
|
# Note: These are hidden when the workflow is inactive
|
||
|
|
if self.object.active:
|
||
|
|
pages_formset = self.get_pages_formset()
|
||
|
|
content_type_form = self.get_content_type_form()
|
||
|
|
if pages_formset.is_valid() and content_type_form.is_valid():
|
||
|
|
pages_formset.save()
|
||
|
|
content_type_form.save()
|
||
|
|
else:
|
||
|
|
transaction.set_rollback(True)
|
||
|
|
successful = False
|
||
|
|
|
||
|
|
if successful:
|
||
|
|
success_message = self.get_success_message()
|
||
|
|
if success_message is not None:
|
||
|
|
messages.success(
|
||
|
|
self.request,
|
||
|
|
success_message,
|
||
|
|
buttons=[
|
||
|
|
messages.button(
|
||
|
|
reverse(self.edit_url_name, args=(self.object.id,)),
|
||
|
|
_("Edit"),
|
||
|
|
)
|
||
|
|
],
|
||
|
|
)
|
||
|
|
return redirect(self.get_success_url())
|
||
|
|
|
||
|
|
return self.form_invalid(form)
|
||
|
|
|
||
|
|
|
||
|
|
class Disable(DeleteView):
|
||
|
|
permission_policy = workflow_permission_policy
|
||
|
|
model = Workflow
|
||
|
|
page_title = _("Disable workflow")
|
||
|
|
template_name = "wagtailadmin/workflows/confirm_disable.html"
|
||
|
|
success_message = _("Workflow '%(object)s' disabled.")
|
||
|
|
add_url_name = "wagtailadmin_workflows:add"
|
||
|
|
edit_url_name = "wagtailadmin_workflows:edit"
|
||
|
|
delete_url_name = "wagtailadmin_workflows:disable"
|
||
|
|
index_url_name = "wagtailadmin_workflows:index"
|
||
|
|
header_icon = "tasks"
|
||
|
|
|
||
|
|
@property
|
||
|
|
def get_edit_url(self):
|
||
|
|
return reverse(self.edit_url_name, args=(self.kwargs["pk"],))
|
||
|
|
|
||
|
|
def get_context_data(self, **kwargs):
|
||
|
|
context = super().get_context_data(**kwargs)
|
||
|
|
states_in_progress = WorkflowState.objects.filter(
|
||
|
|
workflow=self.object, status=WorkflowState.STATUS_IN_PROGRESS
|
||
|
|
).count()
|
||
|
|
if states_in_progress:
|
||
|
|
context["warning_message"] = ngettext(
|
||
|
|
"This workflow is in progress on %(states_in_progress)d page/snippet. Disabling this workflow will cancel moderation on this page/snippet.",
|
||
|
|
"This workflow is in progress on %(states_in_progress)d pages/snippets. Disabling this workflow will cancel moderation on these pages/snippets.",
|
||
|
|
states_in_progress,
|
||
|
|
) % {
|
||
|
|
"states_in_progress": states_in_progress,
|
||
|
|
}
|
||
|
|
return context
|
||
|
|
|
||
|
|
def delete_action(self):
|
||
|
|
self.object.deactivate(user=self.request.user)
|
||
|
|
|
||
|
|
|
||
|
|
def usage(request, pk):
|
||
|
|
workflow = get_object_or_404(Workflow, id=pk)
|
||
|
|
|
||
|
|
editable_pages = page_permission_policy.instances_user_has_permission_for(
|
||
|
|
request.user, "change"
|
||
|
|
)
|
||
|
|
pages = workflow.all_pages() & editable_pages
|
||
|
|
paginator = Paginator(pages, per_page=10)
|
||
|
|
pages = paginator.get_page(request.GET.get("p"))
|
||
|
|
|
||
|
|
return render(
|
||
|
|
request,
|
||
|
|
"wagtailadmin/workflows/usage.html",
|
||
|
|
{
|
||
|
|
"workflow": workflow,
|
||
|
|
"used_by": pages,
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@require_POST
|
||
|
|
def enable_workflow(request, pk):
|
||
|
|
# Reactivate an inactive workflow
|
||
|
|
workflow = get_object_or_404(Workflow, id=pk)
|
||
|
|
|
||
|
|
# Check permissions
|
||
|
|
if not workflow_permission_policy.user_has_permission(request.user, "add"):
|
||
|
|
raise PermissionDenied
|
||
|
|
|
||
|
|
# Set workflow to active if inactive
|
||
|
|
if not workflow.active:
|
||
|
|
workflow.active = True
|
||
|
|
workflow.save()
|
||
|
|
messages.success(
|
||
|
|
request,
|
||
|
|
_("Workflow '%(workflow_name)s' enabled.")
|
||
|
|
% {"workflow_name": workflow.name},
|
||
|
|
)
|
||
|
|
|
||
|
|
# Redirect
|
||
|
|
redirect_to = request.POST.get("next", None)
|
||
|
|
if redirect_to and url_has_allowed_host_and_scheme(
|
||
|
|
url=redirect_to, allowed_hosts={request.get_host()}
|
||
|
|
):
|
||
|
|
return redirect(redirect_to)
|
||
|
|
else:
|
||
|
|
return redirect("wagtailadmin_workflows:edit", workflow.id)
|
||
|
|
|
||
|
|
|
||
|
|
@require_POST
|
||
|
|
def remove_workflow(request, page_pk, workflow_pk=None):
|
||
|
|
# Remove a workflow from a page (specifically a single workflow if workflow_pk is set)
|
||
|
|
# Get the page
|
||
|
|
page = get_object_or_404(Page, id=page_pk)
|
||
|
|
|
||
|
|
# Check permissions
|
||
|
|
if not workflow_permission_policy.user_has_permission(request.user, "change"):
|
||
|
|
raise PermissionDenied
|
||
|
|
|
||
|
|
if hasattr(page, "workflowpage"):
|
||
|
|
# If workflow_pk is set, this will only remove the workflow if it its pk matches - this prevents accidental
|
||
|
|
# removal of the wrong workflow via a workflow edit page if the page listing is out of date
|
||
|
|
if not workflow_pk or workflow_pk == page.workflowpage.workflow.pk:
|
||
|
|
page.workflowpage.delete()
|
||
|
|
messages.success(
|
||
|
|
request,
|
||
|
|
_("Workflow removed from Page '%(page_title)s'.")
|
||
|
|
% {"page_title": page.get_admin_display_title()},
|
||
|
|
)
|
||
|
|
|
||
|
|
# Redirect
|
||
|
|
redirect_to = request.POST.get("next", None)
|
||
|
|
if redirect_to and url_has_allowed_host_and_scheme(
|
||
|
|
url=redirect_to, allowed_hosts={request.get_host()}
|
||
|
|
):
|
||
|
|
return redirect(redirect_to)
|
||
|
|
else:
|
||
|
|
return redirect("wagtailadmin_explore", page.id)
|
||
|
|
|
||
|
|
|
||
|
|
class TaskTitleColumn(TitleColumn):
|
||
|
|
cell_template_name = "wagtailadmin/workflows/includes/task_title_cell.html"
|
||
|
|
|
||
|
|
|
||
|
|
class TaskUsageColumn(Column):
|
||
|
|
cell_template_name = "wagtailadmin/workflows/includes/task_usage_cell.html"
|
||
|
|
|
||
|
|
|
||
|
|
class TaskFilterSet(BaseWorkflowFilterSet):
|
||
|
|
def __init__(self, data=None, queryset=None, *, request=None, prefix=None):
|
||
|
|
super().__init__(data, queryset, request=request, prefix=prefix)
|
||
|
|
task_types = get_task_types()
|
||
|
|
ct_ids = [
|
||
|
|
ct.id for ct in ContentType.objects.get_for_models(*task_types).values()
|
||
|
|
]
|
||
|
|
if len(task_types) > 1:
|
||
|
|
self.filters["content_type"] = MultipleContentTypeFilter(
|
||
|
|
label=_("Type"),
|
||
|
|
widget=forms.CheckboxSelectMultiple,
|
||
|
|
queryset=lambda request: ContentType.objects.filter(pk__in=ct_ids),
|
||
|
|
field_name="content_type",
|
||
|
|
)
|
||
|
|
|
||
|
|
class Meta:
|
||
|
|
model = Task
|
||
|
|
fields = []
|
||
|
|
|
||
|
|
|
||
|
|
class TaskIndex(IndexView):
|
||
|
|
permission_policy = task_permission_policy
|
||
|
|
model = Task
|
||
|
|
context_object_name = "tasks"
|
||
|
|
template_name = "wagtailadmin/workflows/task_index.html"
|
||
|
|
results_template_name = "wagtailadmin/workflows/task_index_results.html"
|
||
|
|
add_url_name = "wagtailadmin_workflows:select_task_type"
|
||
|
|
edit_url_name = "wagtailadmin_workflows:edit_task"
|
||
|
|
index_url_name = "wagtailadmin_workflows:task_index"
|
||
|
|
index_results_url_name = "wagtailadmin_workflows:task_index_results"
|
||
|
|
page_title = _("Workflow tasks")
|
||
|
|
add_item_label = _("New workflow task")
|
||
|
|
header_icon = "thumbtack"
|
||
|
|
columns = [
|
||
|
|
TaskTitleColumn(
|
||
|
|
"name",
|
||
|
|
label=_("Name"),
|
||
|
|
url_name="wagtailadmin_workflows:edit_task",
|
||
|
|
sort_key="name",
|
||
|
|
),
|
||
|
|
Column("type", label=_("Type"), accessor="get_verbose_name", width="25%"),
|
||
|
|
TaskUsageColumn(
|
||
|
|
"usage", label=_("Used on"), accessor="_active_workflows", width="25%"
|
||
|
|
),
|
||
|
|
]
|
||
|
|
default_ordering = "name"
|
||
|
|
search_fields = ["name"]
|
||
|
|
filterset_class = TaskFilterSet
|
||
|
|
_show_breadcrumbs = True
|
||
|
|
paginate_by = 50
|
||
|
|
|
||
|
|
def show_disabled(self):
|
||
|
|
return self.filters.form.cleaned_data.get("show_disabled") == "true"
|
||
|
|
|
||
|
|
def get_queryset(self):
|
||
|
|
return (
|
||
|
|
super()
|
||
|
|
.get_queryset()
|
||
|
|
.specific()
|
||
|
|
.prefetch_related(
|
||
|
|
Prefetch(
|
||
|
|
"workflow_tasks",
|
||
|
|
queryset=WorkflowTask.objects.filter(
|
||
|
|
workflow__active=True
|
||
|
|
).select_related("workflow"),
|
||
|
|
to_attr="_active_workflows",
|
||
|
|
)
|
||
|
|
)
|
||
|
|
)
|
||
|
|
|
||
|
|
def get_context_data(self, **kwargs):
|
||
|
|
context = super().get_context_data(**kwargs)
|
||
|
|
context["showing_disabled"] = self.show_disabled()
|
||
|
|
return context
|
||
|
|
|
||
|
|
|
||
|
|
def select_task_type(request):
|
||
|
|
if not task_permission_policy.user_has_permission(request.user, "add"):
|
||
|
|
raise PermissionDenied
|
||
|
|
|
||
|
|
task_types = [
|
||
|
|
(
|
||
|
|
model.get_verbose_name(),
|
||
|
|
model._meta.app_label,
|
||
|
|
model._meta.model_name,
|
||
|
|
model.get_description(),
|
||
|
|
)
|
||
|
|
for model in get_task_types()
|
||
|
|
]
|
||
|
|
# sort by lower-cased version of verbose name
|
||
|
|
task_types.sort(key=lambda task_type: task_type[0].lower())
|
||
|
|
|
||
|
|
if len(task_types) == 1:
|
||
|
|
# Only one task type is available - redirect straight to the create form rather than
|
||
|
|
# making the user choose
|
||
|
|
verbose_name, app_label, model_name, description = task_types[0]
|
||
|
|
return redirect("wagtailadmin_workflows:add_task", app_label, model_name)
|
||
|
|
|
||
|
|
return render(
|
||
|
|
request,
|
||
|
|
"wagtailadmin/workflows/select_task_type.html",
|
||
|
|
{
|
||
|
|
"task_types": task_types,
|
||
|
|
"icon": "thumbtack",
|
||
|
|
"title": _("Workflows"),
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
class CreateTask(CreateView):
|
||
|
|
permission_policy = task_permission_policy
|
||
|
|
model = None
|
||
|
|
page_title = _("New workflow task")
|
||
|
|
template_name = "wagtailadmin/workflows/create_task.html"
|
||
|
|
success_message = _("Task '%(object)s' created.")
|
||
|
|
add_url_name = "wagtailadmin_workflows:add_task"
|
||
|
|
edit_url_name = "wagtailadmin_workflows:edit_task"
|
||
|
|
index_url_name = "wagtailadmin_workflows:task_index"
|
||
|
|
header_icon = "thumbtack"
|
||
|
|
_show_breadcrumbs = True
|
||
|
|
|
||
|
|
@cached_property
|
||
|
|
def model(self):
|
||
|
|
try:
|
||
|
|
content_type = ContentType.objects.get_by_natural_key(
|
||
|
|
self.kwargs["app_label"], self.kwargs["model_name"]
|
||
|
|
)
|
||
|
|
except (ContentType.DoesNotExist, AttributeError):
|
||
|
|
raise Http404
|
||
|
|
|
||
|
|
# Get class
|
||
|
|
model = content_type.model_class()
|
||
|
|
|
||
|
|
# Make sure the class is a descendant of Task
|
||
|
|
if not issubclass(model, Task) or model is Task:
|
||
|
|
raise Http404
|
||
|
|
|
||
|
|
return model
|
||
|
|
|
||
|
|
def get_form_class(self):
|
||
|
|
return get_task_form_class(self.model)
|
||
|
|
|
||
|
|
def get_add_url(self):
|
||
|
|
return reverse(
|
||
|
|
self.add_url_name,
|
||
|
|
kwargs={
|
||
|
|
"app_label": self.kwargs.get("app_label"),
|
||
|
|
"model_name": self.kwargs.get("model_name"),
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
def get_breadcrumbs_items(self):
|
||
|
|
# Use the base Task class instead of the specific class for the index view
|
||
|
|
items = [
|
||
|
|
{
|
||
|
|
"url": reverse(self.index_url_name),
|
||
|
|
"label": capfirst(Task._meta.verbose_name_plural),
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"url": "",
|
||
|
|
"label": _("New: %(model_name)s")
|
||
|
|
% {"model_name": capfirst(self.model._meta.verbose_name)},
|
||
|
|
},
|
||
|
|
]
|
||
|
|
return self.breadcrumbs_items + items
|
||
|
|
|
||
|
|
|
||
|
|
class EditTask(EditView):
|
||
|
|
permission_policy = task_permission_policy
|
||
|
|
model = None
|
||
|
|
page_title = _("Editing workflow task")
|
||
|
|
template_name = "wagtailadmin/workflows/edit_task.html"
|
||
|
|
success_message = _("Task '%(object)s' updated.")
|
||
|
|
add_url_name = "wagtailadmin_workflows:select_task_type"
|
||
|
|
edit_url_name = "wagtailadmin_workflows:edit_task"
|
||
|
|
delete_url_name = "wagtailadmin_workflows:disable_task"
|
||
|
|
index_url_name = "wagtailadmin_workflows:task_index"
|
||
|
|
delete_item_label = _("Disable")
|
||
|
|
enable_item_label = _("Enable")
|
||
|
|
enable_url_name = "wagtailadmin_workflows:enable_task"
|
||
|
|
header_icon = "thumbtack"
|
||
|
|
_show_breadcrumbs = True
|
||
|
|
|
||
|
|
@cached_property
|
||
|
|
def model(self):
|
||
|
|
return type(self.get_object())
|
||
|
|
|
||
|
|
@cached_property
|
||
|
|
def page_title(self):
|
||
|
|
return _("Editing %(task_type)s") % {
|
||
|
|
"task_type": self.get_object().content_type.name
|
||
|
|
}
|
||
|
|
|
||
|
|
def get_queryset(self):
|
||
|
|
if self.queryset is None:
|
||
|
|
return Task.objects.all()
|
||
|
|
|
||
|
|
def get_object(self, queryset=None):
|
||
|
|
return super().get_object().specific
|
||
|
|
|
||
|
|
def get_form_class(self):
|
||
|
|
return get_task_form_class(self.model, for_edit=True)
|
||
|
|
|
||
|
|
def get_breadcrumbs_items(self):
|
||
|
|
# Use the base Task class instead of the specific class
|
||
|
|
items = [
|
||
|
|
{
|
||
|
|
"url": reverse(self.index_url_name),
|
||
|
|
"label": capfirst(Task._meta.verbose_name_plural),
|
||
|
|
},
|
||
|
|
{"url": "", "label": str(self.object)},
|
||
|
|
]
|
||
|
|
return self.breadcrumbs_items + items
|
||
|
|
|
||
|
|
def get_context_data(self, **kwargs):
|
||
|
|
context = super().get_context_data(**kwargs)
|
||
|
|
context["can_disable"] = (
|
||
|
|
self.permission_policy is None
|
||
|
|
or self.permission_policy.user_has_permission(self.request.user, "delete")
|
||
|
|
) and self.object.active
|
||
|
|
context["can_enable"] = (
|
||
|
|
self.permission_policy is None
|
||
|
|
or self.permission_policy.user_has_permission(self.request.user, "add")
|
||
|
|
) and not self.object.active
|
||
|
|
|
||
|
|
# TODO: add warning msg when there are pages/snippets currently on this task in a workflow, add interaction like resetting task state when saved
|
||
|
|
return context
|
||
|
|
|
||
|
|
@property
|
||
|
|
def get_enable_url(self):
|
||
|
|
return reverse(self.enable_url_name, args=(self.object.pk,))
|
||
|
|
|
||
|
|
|
||
|
|
class DisableTask(DeleteView):
|
||
|
|
permission_policy = task_permission_policy
|
||
|
|
model = Task
|
||
|
|
page_title = _("Disable task")
|
||
|
|
template_name = "wagtailadmin/workflows/confirm_disable_task.html"
|
||
|
|
success_message = _("Task '%(object)s' disabled.")
|
||
|
|
add_url_name = "wagtailadmin_workflows:add_task"
|
||
|
|
edit_url_name = "wagtailadmin_workflows:edit_task"
|
||
|
|
delete_url_name = "wagtailadmin_workflows:disable_task"
|
||
|
|
index_url_name = "wagtailadmin_workflows:task_index"
|
||
|
|
header_icon = "thumbtack"
|
||
|
|
|
||
|
|
def get_context_data(self, **kwargs):
|
||
|
|
context = super().get_context_data(**kwargs)
|
||
|
|
states_in_progress = TaskState.objects.filter(
|
||
|
|
status=TaskState.STATUS_IN_PROGRESS, task=self.get_object().pk
|
||
|
|
).count()
|
||
|
|
if states_in_progress:
|
||
|
|
context["warning_message"] = ngettext(
|
||
|
|
"This task is in progress on %(states_in_progress)d page/snippet. Disabling this task will cause it to be skipped in the moderation workflow and not be listed for selection when editing a workflow.",
|
||
|
|
"This task is in progress on %(states_in_progress)d pages/snippets. Disabling this task will cause it to be skipped in the moderation workflow and not be listed for selection when editing a workflow.",
|
||
|
|
states_in_progress,
|
||
|
|
) % {
|
||
|
|
"states_in_progress": states_in_progress,
|
||
|
|
}
|
||
|
|
return context
|
||
|
|
|
||
|
|
@property
|
||
|
|
def get_edit_url(self):
|
||
|
|
return reverse(self.edit_url_name, args=(self.kwargs["pk"],))
|
||
|
|
|
||
|
|
def delete_action(self):
|
||
|
|
self.object.deactivate(user=self.request.user)
|
||
|
|
|
||
|
|
|
||
|
|
@require_POST
|
||
|
|
def enable_task(request, pk):
|
||
|
|
# Reactivate an inactive task
|
||
|
|
task = get_object_or_404(Task, id=pk)
|
||
|
|
|
||
|
|
# Check permissions
|
||
|
|
if not task_permission_policy.user_has_permission(request.user, "add"):
|
||
|
|
raise PermissionDenied
|
||
|
|
|
||
|
|
# Set workflow to active if inactive
|
||
|
|
if not task.active:
|
||
|
|
task.active = True
|
||
|
|
task.save()
|
||
|
|
messages.success(
|
||
|
|
request, _("Task '%(task_name)s' enabled.") % {"task_name": task.name}
|
||
|
|
)
|
||
|
|
|
||
|
|
# Redirect
|
||
|
|
redirect_to = request.POST.get("next", None)
|
||
|
|
if redirect_to and url_has_allowed_host_and_scheme(
|
||
|
|
url=redirect_to, allowed_hosts={request.get_host()}
|
||
|
|
):
|
||
|
|
return redirect(redirect_to)
|
||
|
|
else:
|
||
|
|
return redirect("wagtailadmin_workflows:edit_task", task.id)
|
||
|
|
|
||
|
|
|
||
|
|
def get_task_chosen_response(request, task):
|
||
|
|
"""
|
||
|
|
helper function: given a task, return the response indicating that it has been chosen
|
||
|
|
"""
|
||
|
|
result_data = {
|
||
|
|
"id": task.id,
|
||
|
|
"name": task.name,
|
||
|
|
"edit_url": reverse("wagtailadmin_workflows:edit_task", args=[task.id]),
|
||
|
|
}
|
||
|
|
return render_modal_workflow(
|
||
|
|
request,
|
||
|
|
None,
|
||
|
|
None,
|
||
|
|
None,
|
||
|
|
json_data={"step": "task_chosen", "result": result_data},
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
class BaseTaskChooserView(TemplateView):
|
||
|
|
def dispatch(self, request):
|
||
|
|
self.task_models = get_task_types()
|
||
|
|
self.can_create = (
|
||
|
|
task_permission_policy.user_has_permission(request.user, "add")
|
||
|
|
and len(self.task_models) != 0
|
||
|
|
)
|
||
|
|
return super().dispatch(request)
|
||
|
|
|
||
|
|
def get_create_model(self):
|
||
|
|
"""
|
||
|
|
To be called after dispatch(); returns the model to use for a new task if one is known
|
||
|
|
(either from being the only available task mode, or from being specified in the URL as create_model)
|
||
|
|
"""
|
||
|
|
if self.can_create:
|
||
|
|
if len(self.task_models) == 1:
|
||
|
|
return self.task_models[0]
|
||
|
|
|
||
|
|
elif "create_model" in self.request.GET:
|
||
|
|
create_model = resolve_model_string(self.request.GET["create_model"])
|
||
|
|
|
||
|
|
if create_model not in self.task_models:
|
||
|
|
raise Http404
|
||
|
|
|
||
|
|
return create_model
|
||
|
|
|
||
|
|
def get_create_form_class(self):
|
||
|
|
"""
|
||
|
|
To be called after dispatch(); returns the form class for creating a new task
|
||
|
|
"""
|
||
|
|
self.create_model = self.get_create_model()
|
||
|
|
if self.create_model:
|
||
|
|
return get_task_form_class(self.create_model)
|
||
|
|
else:
|
||
|
|
return None
|
||
|
|
|
||
|
|
def get_create_form(self):
|
||
|
|
"""
|
||
|
|
To be called after dispatch(); returns a blank create form, or None if not available
|
||
|
|
"""
|
||
|
|
create_form_class = self.get_create_form_class()
|
||
|
|
if create_form_class:
|
||
|
|
return create_form_class(prefix="create-task")
|
||
|
|
|
||
|
|
def get_task_type_options(self):
|
||
|
|
"""
|
||
|
|
To be called after dispatch(); returns the task types list for the "select task type" view
|
||
|
|
"""
|
||
|
|
task_types = [
|
||
|
|
(
|
||
|
|
model.get_verbose_name(),
|
||
|
|
model._meta.app_label,
|
||
|
|
model._meta.model_name,
|
||
|
|
model.get_description(),
|
||
|
|
)
|
||
|
|
for model in self.task_models
|
||
|
|
]
|
||
|
|
# sort by lower-cased version of verbose name
|
||
|
|
task_types.sort(key=lambda task_type: task_type[0].lower())
|
||
|
|
|
||
|
|
return task_types
|
||
|
|
|
||
|
|
def get_task_type_filter_choices(self):
|
||
|
|
"""
|
||
|
|
To be called after dispatch(); returns the list of task type choices for filter on "existing task" tab
|
||
|
|
"""
|
||
|
|
task_type_choices = [
|
||
|
|
(model, model.get_verbose_name()) for model in self.task_models
|
||
|
|
]
|
||
|
|
task_type_choices.sort(key=lambda task_type: task_type[1].lower())
|
||
|
|
return task_type_choices
|
||
|
|
|
||
|
|
def get_form_js_context(self):
|
||
|
|
return {}
|
||
|
|
|
||
|
|
def get_task_listing_context_data(self):
|
||
|
|
search_form = TaskChooserSearchForm(
|
||
|
|
self.request.GET, task_type_choices=self.get_task_type_filter_choices()
|
||
|
|
)
|
||
|
|
tasks = all_tasks = search_form.task_model.objects.filter(active=True).order_by(
|
||
|
|
Lower("name")
|
||
|
|
)
|
||
|
|
q = ""
|
||
|
|
|
||
|
|
if search_form.is_searching():
|
||
|
|
# Note: I decided not to use wagtailsearch here. This is because
|
||
|
|
# wagtailsearch creates a new index for each model you make
|
||
|
|
# searchable and this might affect someone's quota. I doubt there
|
||
|
|
# would ever be enough tasks to require using anything more than
|
||
|
|
# an icontains anyway.
|
||
|
|
q = search_form.cleaned_data["q"]
|
||
|
|
tasks = tasks.filter(name__icontains=q)
|
||
|
|
|
||
|
|
# Pagination
|
||
|
|
paginator = Paginator(tasks, per_page=10)
|
||
|
|
tasks = paginator.get_page(self.request.GET.get("p"))
|
||
|
|
|
||
|
|
return {
|
||
|
|
"search_form": search_form,
|
||
|
|
"tasks": tasks,
|
||
|
|
"all_tasks": all_tasks,
|
||
|
|
"query_string": q,
|
||
|
|
"can_create": self.can_create,
|
||
|
|
}
|
||
|
|
|
||
|
|
def get_create_tab_context_data(self):
|
||
|
|
return {
|
||
|
|
"create_form": self.create_form,
|
||
|
|
"add_url": reverse("wagtailadmin_workflows:task_chooser_create")
|
||
|
|
+ "?"
|
||
|
|
+ self.request.GET.urlencode()
|
||
|
|
if self.create_model
|
||
|
|
else None,
|
||
|
|
"task_types": self.get_task_type_options(),
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
class TaskChooserView(BaseTaskChooserView):
|
||
|
|
def get(self, request):
|
||
|
|
self.create_form = self.get_create_form()
|
||
|
|
return super().get(request)
|
||
|
|
|
||
|
|
def get_context_data(self, **kwargs):
|
||
|
|
context = {
|
||
|
|
"can_create": self.can_create,
|
||
|
|
}
|
||
|
|
context.update(self.get_task_listing_context_data())
|
||
|
|
context.update(self.get_create_tab_context_data())
|
||
|
|
return context
|
||
|
|
|
||
|
|
def render_to_response(self, context):
|
||
|
|
js_context = self.get_form_js_context()
|
||
|
|
js_context["step"] = "chooser"
|
||
|
|
|
||
|
|
return render_modal_workflow(
|
||
|
|
self.request,
|
||
|
|
"wagtailadmin/workflows/task_chooser/chooser.html",
|
||
|
|
None,
|
||
|
|
context,
|
||
|
|
json_data=js_context,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
class TaskChooserCreateView(BaseTaskChooserView):
|
||
|
|
def get(self, request):
|
||
|
|
self.create_form = self.get_create_form()
|
||
|
|
return super().get(request)
|
||
|
|
|
||
|
|
def post(self, request):
|
||
|
|
create_form_class = self.get_create_form_class()
|
||
|
|
if not create_form_class:
|
||
|
|
return HttpResponseBadRequest()
|
||
|
|
|
||
|
|
self.create_form = create_form_class(
|
||
|
|
request.POST, request.FILES, prefix="create-task"
|
||
|
|
)
|
||
|
|
|
||
|
|
if self.create_form.is_valid():
|
||
|
|
task = self.create_form.save()
|
||
|
|
return get_task_chosen_response(request, task)
|
||
|
|
else:
|
||
|
|
context = self.get_context_data()
|
||
|
|
return self.render_to_response(context)
|
||
|
|
|
||
|
|
def get_context_data(self, **kwargs):
|
||
|
|
return self.get_create_tab_context_data()
|
||
|
|
|
||
|
|
def render_to_response(self, context):
|
||
|
|
tab_html = render_to_string(
|
||
|
|
"wagtailadmin/workflows/task_chooser/includes/create_tab.html",
|
||
|
|
context,
|
||
|
|
self.request,
|
||
|
|
)
|
||
|
|
|
||
|
|
js_context = self.get_form_js_context()
|
||
|
|
js_context["step"] = "reshow_create_tab"
|
||
|
|
js_context["htmlFragment"] = tab_html
|
||
|
|
|
||
|
|
return render_modal_workflow(
|
||
|
|
self.request, None, None, None, json_data=js_context
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
class TaskChooserResultsView(BaseTaskChooserView):
|
||
|
|
template_name = "wagtailadmin/workflows/task_chooser/includes/results.html"
|
||
|
|
|
||
|
|
def get_context_data(self, **kwargs):
|
||
|
|
return self.get_task_listing_context_data()
|
||
|
|
|
||
|
|
|
||
|
|
def task_chosen(request, task_id):
|
||
|
|
task = get_object_or_404(Task, id=task_id)
|
||
|
|
return get_task_chosen_response(request, task)
|