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"