From f4f351c4af2c03ab19ad156233fdacabdc59bd6e Mon Sep 17 00:00:00 2001
From: Dale Cannon <118175145+dalecannon@users.noreply.github.com>
Date: Mon, 18 Nov 2024 13:59:26 +0000
Subject: [PATCH] TP2000-1544 Prototype workflow template create & update views
(#1328)
* Don't show actions for a single item in queue list
* Filter queue items by their own queue when reordering
* Support task workflow template creation
* Support workflow template editing
---
conftest.py | 3 +
tasks/forms.py | 43 ++++++++++++
tasks/jinja2/tasks/includes/task_queue.jinja | 8 ++-
.../workflows/template_confirm_create.jinja | 47 +++++++++++++
.../workflows/template_confirm_update.jinja | 49 +++++++++++++
.../tasks/workflows/template_create.jinja | 16 +++++
.../tasks/workflows/template_detail.jinja | 2 +-
.../tasks/workflows/template_edit.jinja | 19 +++++
tasks/models/queue.py | 7 +-
tasks/models/workflow.py | 14 ++++
tasks/tests/test_views.py | 70 +++++++++++++++++++
tasks/urls.py | 20 ++++++
tasks/views.py | 40 +++++++++++
13 files changed, 334 insertions(+), 4 deletions(-)
create mode 100644 tasks/jinja2/tasks/workflows/template_confirm_create.jinja
create mode 100644 tasks/jinja2/tasks/workflows/template_confirm_update.jinja
create mode 100644 tasks/jinja2/tasks/workflows/template_create.jinja
create mode 100644 tasks/jinja2/tasks/workflows/template_edit.jinja
diff --git a/conftest.py b/conftest.py
index 5670aa32c..56049cabb 100644
--- a/conftest.py
+++ b/conftest.py
@@ -310,6 +310,9 @@ def policy_group(db) -> Group:
("tasks", "view_comment"),
("tasks", "change_comment"),
("tasks", "delete_comment"),
+ ("tasks", "add_taskworkflowtemplate"),
+ ("tasks", "change_taskworkflowtemplate"),
+ ("tasks", "delete_taskworkflowtemplate"),
("tasks", "view_taskworkflowtemplate"),
("tasks", "add_tasktemplate"),
("tasks", "change_tasktemplate"),
diff --git a/tasks/forms.py b/tasks/forms.py
index 7c60325b8..8eb40723d 100644
--- a/tasks/forms.py
+++ b/tasks/forms.py
@@ -7,6 +7,7 @@
from common.forms import delete_form_for
from tasks.models import Task
from tasks.models import TaskTemplate
+from tasks.models import TaskWorkflowTemplate
from workbaskets.models import WorkBasket
@@ -81,6 +82,48 @@ def save(self, parent_task, user, commit=True):
TaskDeleteForm = delete_form_for(Task)
+class TaskWorkflowTemplateBaseForm(ModelForm):
+ class Meta:
+ model = TaskWorkflowTemplate
+ fields = ("title", "description")
+
+ error_messages = {
+ "title": {
+ "required": "Enter a title",
+ },
+ "description": {
+ "required": "Enter a description",
+ },
+ }
+
+ def __init__(self, *args, submit_title, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.helper = FormHelper(self)
+ self.helper.label_size = Size.SMALL
+ self.helper.legend_size = Size.SMALL
+ self.helper.layout = Layout(
+ "title",
+ "description",
+ Submit(
+ "submit",
+ submit_title,
+ data_module="govuk-button",
+ data_prevent_double_click="true",
+ ),
+ )
+
+
+class TaskWorkflowTemplateCreateForm(TaskWorkflowTemplateBaseForm):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, submit_title="Create", **kwargs)
+
+
+class TaskWorkflowTemplateUpdateForm(TaskWorkflowTemplateBaseForm):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, submit_title="Update", **kwargs)
+
+
class TaskTemplateFormBase(ModelForm):
class Meta:
model = TaskTemplate
diff --git a/tasks/jinja2/tasks/includes/task_queue.jinja b/tasks/jinja2/tasks/includes/task_queue.jinja
index 79e45dd16..904a3fdfc 100644
--- a/tasks/jinja2/tasks/includes/task_queue.jinja
+++ b/tasks/jinja2/tasks/includes/task_queue.jinja
@@ -5,7 +5,9 @@
{%- for obj in object_list %}
{%- set up_down_cell_content %}
- {% if loop.index == 1 %}
+ {% if object_list|length == 1 %}
+ {# No buttons needed for a single item #}
+ {% elif loop.index == 1 %}
{{ render_item_button(obj, "demote", use_icon=True) }}
{% elif loop.index == 2 %}
{{ render_item_button(obj, "promote", use_icon=True) }}
@@ -31,7 +33,9 @@
{% endset -%}
{%- set order_cell_content %}
- {% if loop.index == 1%}
+ {% if object_list|length == 1 %}
+ {# No buttons needed for a single item #}
+ {% elif loop.index == 1 %}
{{ render_item_button(obj, "demote_to_last") }}
{% elif loop.index == object_list | length %}
{{ render_item_button(obj, "promote_to_first") }}
diff --git a/tasks/jinja2/tasks/workflows/template_confirm_create.jinja b/tasks/jinja2/tasks/workflows/template_confirm_create.jinja
new file mode 100644
index 000000000..62f9ee5fe
--- /dev/null
+++ b/tasks/jinja2/tasks/workflows/template_confirm_create.jinja
@@ -0,0 +1,47 @@
+{% extends "common/confirm_create.jinja" %}
+
+{% from "components/breadcrumbs.jinja" import breadcrumbs %}
+{% from "components/panel/macro.njk" import govukPanel %}
+{% from "components/button/macro.njk" import govukButton %}
+
+{% set page_title = "Workflow template created" %}
+
+{% block breadcrumb %}
+ {{ breadcrumbs(
+ request,
+ [
+ {"text": "Find and view workflow templates", "href": "#TODO"},
+ {
+ "text": "Create a workflow template",
+ "href": url("workflow:task-workflow-template-ui-create"),
+ },
+ {"text": page_title}
+ ],
+ False,
+ ) }}
+{% endblock %}
+
+{% block panel %}
+ {{ govukPanel({
+ "titleText": "Workflow template: " ~ object.title,
+ "text": "You have created a new workflow template",
+ "classes": "govuk-!-margin-bottom-7"
+ }) }}
+{% endblock %}
+
+{% block button_group %}
+ {{ govukButton({
+ "text": "View workflow template",
+ "href": url("workflow:task-workflow-template-ui-detail", kwargs={"pk": object.pk}),
+ "classes": "govuk-button"
+ }) }}
+ {{ govukButton({
+ "text": "Create a task template",
+ "href": url("workflow:task-template-ui-create", kwargs={"workflow_template_pk": object.pk}),
+ "classes": "govuk-button--secondary"
+ }) }}
+{% endblock %}
+
+{% block actions %}
+
Find and view workflow templates
+{% endblock %}
diff --git a/tasks/jinja2/tasks/workflows/template_confirm_update.jinja b/tasks/jinja2/tasks/workflows/template_confirm_update.jinja
new file mode 100644
index 000000000..fde9d4b53
--- /dev/null
+++ b/tasks/jinja2/tasks/workflows/template_confirm_update.jinja
@@ -0,0 +1,49 @@
+{% extends "common/confirm_create.jinja" %}
+
+{% from "components/breadcrumbs.jinja" import breadcrumbs %}
+{% from "components/panel/macro.njk" import govukPanel %}
+{% from "components/button/macro.njk" import govukButton %}
+
+{% set page_title = "Workflow template updated" %}
+
+{% block breadcrumb %}
+ {{ breadcrumbs(
+ request,
+ [
+ {"text": "Find and view workflow templates", "href": "#TODO"},
+ {
+ "text": "Workflow template: " ~ object.title,
+ "href": url(
+ "workflow:task-workflow-template-ui-detail",
+ kwargs={"pk": object.pk}),
+ },
+ {"text": page_title}
+ ],
+ False,
+ ) }}
+{% endblock %}
+
+{% block panel %}
+ {{ govukPanel({
+ "titleText": "Workflow template: " ~ object.title,
+ "text": "You have updated the workflow template",
+ "classes": "govuk-!-margin-bottom-7"
+ }) }}
+{% endblock %}
+
+{% block button_group %}
+ {{ govukButton({
+ "text": "View workflow template",
+ "href": url("workflow:task-workflow-template-ui-detail", kwargs={"pk": object.pk}),
+ "classes": "govuk-button"
+ }) }}
+ {{ govukButton({
+ "text": "Create a task template",
+ "href": url("workflow:task-template-ui-create", kwargs={"workflow_template_pk": object.pk}),
+ "classes": "govuk-button--secondary"
+ }) }}
+{% endblock %}
+
+{% block actions %}
+ Find and view workflow templates
+{% endblock %}
diff --git a/tasks/jinja2/tasks/workflows/template_create.jinja b/tasks/jinja2/tasks/workflows/template_create.jinja
new file mode 100644
index 000000000..f2ec118c6
--- /dev/null
+++ b/tasks/jinja2/tasks/workflows/template_create.jinja
@@ -0,0 +1,16 @@
+{% extends "layouts/create.jinja" %}
+
+{% from "components/breadcrumbs.jinja" import breadcrumbs %}
+
+{% set page_title = "Create a workflow template" %}
+
+{% block breadcrumb %}
+ {{ breadcrumbs(
+ request,
+ [
+ {"text": "Find and view workflow templates", "href": "#TODO"},
+ {"text": page_title}
+ ],
+ False,
+ ) }}
+{% endblock %}
diff --git a/tasks/jinja2/tasks/workflows/template_detail.jinja b/tasks/jinja2/tasks/workflows/template_detail.jinja
index 86c76750c..afa1e3f9e 100644
--- a/tasks/jinja2/tasks/workflows/template_detail.jinja
+++ b/tasks/jinja2/tasks/workflows/template_detail.jinja
@@ -5,7 +5,7 @@
{% set page_title = "Workflow template: " ~ object.title %}
{% set list_include = "tasks/includes/task_queue.jinja" %}
-{% set edit_url = "#TODO" %}
+{% set edit_url = object.get_url("edit") %}
{% block breadcrumb %}
{{ breadcrumbs(request, [
diff --git a/tasks/jinja2/tasks/workflows/template_edit.jinja b/tasks/jinja2/tasks/workflows/template_edit.jinja
new file mode 100644
index 000000000..0bcfad36c
--- /dev/null
+++ b/tasks/jinja2/tasks/workflows/template_edit.jinja
@@ -0,0 +1,19 @@
+{% extends "layouts/form.jinja" %}
+{% from "components/breadcrumbs.jinja" import breadcrumbs %}
+
+{% set page_title = "Edit workflow template details" %}
+
+{% block breadcrumb %}
+ {{ breadcrumbs(request, [
+ {"text": "Find and view tasks", "href": url("workflow:task-ui-list")},
+ {"text": "Workflow template: " ~ object.title, "href": object.get_url("detail")},
+ {"text": page_title}
+ ])
+ }}
+{% endblock %}
+
+{% block form %}
+ {% call django_form() %}
+ {{ crispy(form) }}
+ {% endcall %}
+{% endblock %}
diff --git a/tasks/models/queue.py b/tasks/models/queue.py
index 0fc517f26..79aabb103 100644
--- a/tasks/models/queue.py
+++ b/tasks/models/queue.py
@@ -125,6 +125,7 @@ def delete(self):
self.__class__.objects.select_for_update(nowait=True).filter(
position__gt=instance.position,
+ queue=instance.queue,
).update(position=models.F("position") - 1)
return super().delete()
@@ -145,6 +146,7 @@ def promote(self) -> Self:
item_to_demote = self.__class__.objects.select_for_update(nowait=True).get(
position=instance.position - 1,
+ queue=instance.queue,
)
item_to_demote.position += 1
instance.position -= 1
@@ -169,6 +171,7 @@ def demote(self) -> Self:
item_to_promote = self.__class__.objects.select_for_update(nowait=True).get(
position=instance.position + 1,
+ queue=instance.queue,
)
item_to_promote.position -= 1
instance.position += 1
@@ -194,7 +197,8 @@ def promote_to_first(self) -> Self:
return instance
self.__class__.objects.select_for_update(nowait=True).filter(
- models.Q(position__lt=instance.position),
+ position__lt=instance.position,
+ queue=instance.queue,
).update(position=models.F("position") + 1)
instance.position = 1
@@ -221,6 +225,7 @@ def demote_to_last(self) -> Self:
self.__class__.objects.select_for_update(nowait=True).filter(
position__gt=instance.position,
+ queue=instance.queue,
).update(position=models.F("position") - 1)
instance.position = last_place
diff --git a/tasks/models/workflow.py b/tasks/models/workflow.py
index 57cb27804..650130675 100644
--- a/tasks/models/workflow.py
+++ b/tasks/models/workflow.py
@@ -112,6 +112,20 @@ def create_task_workflow(self) -> "TaskWorkflow":
return task_workflow
+ def get_url(self, action: str = "detail"):
+ if action == "detail":
+ return reverse(
+ "workflow:task-workflow-template-ui-detail",
+ kwargs={"pk": self.pk},
+ )
+ elif action == "edit":
+ return reverse(
+ "workflow:task-workflow-template-ui-update",
+ kwargs={"pk": self.pk},
+ )
+
+ return "#NOT-IMPLEMENTED"
+
class TaskItemTemplate(QueueItem):
"""Queue item management for TaskTemplate instances."""
diff --git a/tasks/tests/test_views.py b/tasks/tests/test_views.py
index 67a541f2d..5ac49e991 100644
--- a/tasks/tests/test_views.py
+++ b/tasks/tests/test_views.py
@@ -208,6 +208,76 @@ def convert_to_index(position: int) -> int:
assert reordered_item.id == expected_item.id
+def test_workflow_template_create_view(valid_user_client):
+ """Tests that a new workflow template can be created and that the
+ corresponding confirmation view returns a HTTP 200 response."""
+
+ assert not TaskWorkflowTemplate.objects.exists()
+
+ create_url = reverse("workflow:task-workflow-template-ui-create")
+ form_data = {
+ "title": "Test workflow template",
+ "description": "Test description",
+ }
+ create_response = valid_user_client.post(create_url, form_data)
+
+ created_workflow_template = TaskWorkflowTemplate.objects.get(
+ title=form_data["title"],
+ description=form_data["description"],
+ )
+ confirmation_url = reverse(
+ "workflow:task-workflow-template-ui-confirm-create",
+ kwargs={"pk": created_workflow_template.pk},
+ )
+ assert create_response.status_code == 302
+ assert create_response.url == confirmation_url
+
+ confirmation_response = valid_user_client.get(confirmation_url)
+
+ soup = BeautifulSoup(str(confirmation_response.content), "html.parser")
+
+ assert confirmation_response.status_code == 200
+ assert (
+ created_workflow_template.title in soup.select("h1.govuk-panel__title")[0].text
+ )
+
+
+def test_workflow_template_update_view(
+ valid_user_client,
+ task_workflow_template,
+):
+ """Tests that a workflow template can be updated and that the corresponding
+ confirmation view returns a HTTP 200 response."""
+
+ update_url = reverse(
+ "workflow:task-workflow-template-ui-update",
+ kwargs={"pk": task_workflow_template.pk},
+ )
+ form_data = {
+ "title": "Updated test title",
+ "description": "Updated test title",
+ }
+
+ update_response = valid_user_client.post(update_url, form_data)
+ assert update_response.status_code == 302
+
+ task_workflow_template.refresh_from_db()
+ assert task_workflow_template.title == form_data["title"]
+ assert task_workflow_template.description == form_data["description"]
+
+ confirmation_url = reverse(
+ "workflow:task-workflow-template-ui-confirm-update",
+ kwargs={"pk": task_workflow_template.pk},
+ )
+ assert update_response.url == confirmation_url
+
+ confirmation_response = valid_user_client.get(confirmation_url)
+ assert confirmation_response.status_code == 200
+
+ soup = BeautifulSoup(str(confirmation_response.content), "html.parser")
+ assert task_workflow_template.title in soup.select("h1.govuk-panel__title")[0].text
+
+
def test_create_task_template_view(valid_user_client, task_workflow_template):
"""Test the view for creating new TaskTemplates and the confirmation view
that a successful creation redirects to."""
diff --git a/tasks/urls.py b/tasks/urls.py
index 6620250ef..8c4967225 100644
--- a/tasks/urls.py
+++ b/tasks/urls.py
@@ -48,6 +48,26 @@
views.TaskWorkflowTemplateDetailView.as_view(),
name="task-workflow-template-ui-detail",
),
+ path(
+ "create/",
+ views.TaskWorkflowTemplateCreateView.as_view(),
+ name="task-workflow-template-ui-create",
+ ),
+ path(
+ "/confirm-create/",
+ views.TaskWorkflowTemplateConfirmCreateView.as_view(),
+ name="task-workflow-template-ui-confirm-create",
+ ),
+ path(
+ "/update/",
+ views.TaskWorkflowTemplateUpdateView.as_view(),
+ name="task-workflow-template-ui-update",
+ ),
+ path(
+ "/confirm-update/",
+ views.TaskWorkflowTemplateConfirmUpdateView.as_view(),
+ name="task-workflow-template-ui-confirm-update",
+ ),
path(
"task-templates//",
views.TaskTemplateDetailView.as_view(),
diff --git a/tasks/views.py b/tasks/views.py
index 4d92f157f..bcc02e61b 100644
--- a/tasks/views.py
+++ b/tasks/views.py
@@ -21,6 +21,8 @@
from tasks.forms import TaskTemplateDeleteForm
from tasks.forms import TaskTemplateUpdateForm
from tasks.forms import TaskUpdateForm
+from tasks.forms import TaskWorkflowTemplateCreateForm
+from tasks.forms import TaskWorkflowTemplateUpdateForm
from tasks.models import Task
from tasks.models import TaskItemTemplate
from tasks.models import TaskTemplate
@@ -247,6 +249,44 @@ def demote_to_last(self, task_template_id: int) -> None:
pass
+class TaskWorkflowTemplateCreateView(PermissionRequiredMixin, CreateView):
+ model = TaskWorkflowTemplate
+ permission_required = "tasks.add_taskworkflowtemplate"
+ template_name = "tasks/workflows/template_create.jinja"
+ form_class = TaskWorkflowTemplateCreateForm
+
+ def get_success_url(self):
+ return reverse(
+ "workflow:task-workflow-template-ui-confirm-create",
+ kwargs={"pk": self.object.pk},
+ )
+
+
+class TaskWorkflowTemplateConfirmCreateView(PermissionRequiredMixin, DetailView):
+ model = TaskWorkflowTemplate
+ template_name = "tasks/workflows/template_confirm_create.jinja"
+ permission_required = "tasks.add_taskworkflowtemplate"
+
+
+class TaskWorkflowTemplateUpdateView(PermissionRequiredMixin, UpdateView):
+ model = TaskWorkflowTemplate
+ template_name = "tasks/workflows/template_edit.jinja"
+ permission_required = "tasks.change_taskworkflowtemplate"
+ form_class = TaskWorkflowTemplateUpdateForm
+
+ def get_success_url(self):
+ return reverse(
+ "workflow:task-workflow-template-ui-confirm-update",
+ kwargs={"pk": self.object.pk},
+ )
+
+
+class TaskWorkflowTemplateConfirmUpdateView(PermissionRequiredMixin, DetailView):
+ model = TaskWorkflowTemplate
+ template_name = "tasks/workflows/template_confirm_update.jinja"
+ permission_required = "tasks.change_taskworkflowtemplate"
+
+
class TaskTemplateDetailView(PermissionRequiredMixin, DetailView):
model = TaskTemplate
template_name = "tasks/workflows/task_template_detail.jinja"