Skip to content

Commit

Permalink
Implement prototype UI for tasks (#1308)
Browse files Browse the repository at this point in the history
* Add initial TaskCreate view & form

* Add initial Task detail view

* Add initial Task edit form & view

* Add initial Task delete view & form

* Add initial Task list view

* Try pre_save signals for TaskLog creation

* Tidy templates

* Allow Task deletion

* Register TaskLog model in admin

* Add test for TaskLog creation

* Rename user group in conftest

* Set up pre_save signal for TaskAssignee to create TaskLog entries

* Persist TaskLog entries after Task deletion

* Add mixins to override Manager and QuerySet methods for pre_save signal handling

* Fix Task delete view

* Move WithSignalManagerMixin and WithSignalQuerysetMixin to common/models/mixins

* Set initial pagination limit for TaskListView
  • Loading branch information
dalecannon authored Oct 31, 2024
1 parent 0595097 commit de2733c
Show file tree
Hide file tree
Showing 25 changed files with 872 additions and 34 deletions.
35 changes: 35 additions & 0 deletions common/models/mixins/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Mixins for models."""

from django.db import models
from django.db.models import signals


class TimestampedMixin(models.Model):
Expand All @@ -11,3 +12,37 @@ class TimestampedMixin(models.Model):

class Meta:
abstract = True


class WithSignalManagerMixin:
"""A mixin that overrides default Manager methods to send pre_save signals
for model instances."""

def bulk_create(self, objs, **kwargs) -> list:
for obj in objs:
signals.pre_save.send(sender=self.model, instance=obj)
return super().bulk_create(objs, **kwargs)


class WithSignalQuerysetMixin:
"""A mixin that overrides default QuerySet methods to send pre_save signals
for model instances."""

def update(self, **kwargs) -> int:
old_instances_map = {}
pk_list = []
for old_instance in self.iterator():
old_instances_map[old_instance.pk] = old_instance
pk_list.append(old_instance.pk)

rows_updated = super().update(**kwargs)

for new_instance in self.model.objects.filter(pk__in=pk_list):
old_instance = old_instances_map.get(new_instance.pk)
signals.pre_save.send(
sender=self.model,
instance=new_instance,
old_instance=old_instance,
)

return rows_updated
2 changes: 2 additions & 0 deletions common/tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import factory
from django.contrib.auth import get_user_model
from django.db.models import signals
from factory.fuzzy import FuzzyChoice
from factory.fuzzy import FuzzyText
from faker import Faker
Expand Down Expand Up @@ -1565,6 +1566,7 @@ class SubTaskFactory(TaskFactory):
parent_task = factory.SubFactory(TaskFactory)


@factory.django.mute_signals(signals.pre_save)
class TaskAssigneeFactory(factory.django.DjangoModelFactory):
user = factory.SubFactory(UserFactory)
assignment_type = FuzzyChoice(TaskAssignee.AssignmentType.values)
Expand Down
5 changes: 4 additions & 1 deletion conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ def api_client() -> APIClient:

@pytest.fixture
def policy_group(db) -> Group:
group = factories.UserGroupFactory.create(name="Policy")
group = factories.UserGroupFactory.create(name="Tariff Managers")

for app_label, codename in [
("common", "add_trackedmodel"),
Expand All @@ -301,6 +301,9 @@ def policy_group(db) -> Group:
("publishing", "consume_from_packaging_queue"),
("publishing", "manage_packaging_queue"),
("publishing", "view_envelope"),
("tasks", "add_task"),
("tasks", "change_task"),
("tasks", "delete_task"),
("tasks", "add_taskassignee"),
("tasks", "change_taskassignee"),
("tasks", "add_comment"),
Expand Down
16 changes: 16 additions & 0 deletions tasks/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from tasks.models import ProgressState
from tasks.models import Task
from tasks.models import TaskAssignee
from tasks.models import TaskLog


class TaskAdminMixin:
Expand Down Expand Up @@ -77,10 +78,25 @@ def task_id(self, obj):
return self.link_to_task(obj.task)


class TaskLogAdmin(admin.ModelAdmin):
list_display = ["task", "action", "created_at"]
list_filter = ["action"]
readonly_fields = [
"id",
"action",
"description",
"task",
"instigator",
"created_at",
]


admin.site.register(Task, TaskAdmin)

admin.site.register(Category, CategoryAdmin)

admin.site.register(ProgressState, ProgressStateAdmin)

admin.site.register(TaskAssignee, TaskAssigneeAdmin)

admin.site.register(TaskLog, TaskLogAdmin)
28 changes: 28 additions & 0 deletions tasks/filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from django.forms import CheckboxSelectMultiple
from django.urls import reverse_lazy
from django_filters import ModelMultipleChoiceFilter

from common.filters import TamatoFilter
from tasks.models import ProgressState
from tasks.models import Task


class TaskFilter(TamatoFilter):

search_fields = (
"id",
"title",
"description",
)
clear_url = reverse_lazy("workflow:task-ui-list")

progress_state = ModelMultipleChoiceFilter(
label="Status",
help_text="Select all that apply",
queryset=ProgressState.objects.all(),
widget=CheckboxSelectMultiple,
)

class Meta:
model = Task
fields = ["search", "category", "progress_state"]
70 changes: 70 additions & 0 deletions tasks/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from crispy_forms_gds.helper import FormHelper
from crispy_forms_gds.layout import Layout
from crispy_forms_gds.layout import Size
from crispy_forms_gds.layout import Submit
from django.forms import ModelForm

from common.forms import delete_form_for
from tasks.models import Task
from workbaskets.models import WorkBasket


class TaskBaseForm(ModelForm):
class Meta:
model = Task
exclude = ["parent_task", "creator"]

error_messages = {
"title": {
"required": "Enter a title",
},
"description": {
"required": "Enter a description",
},
"progress_state": {
"required": "Select a status",
},
}

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.init_fields()
self.init_layout()

def init_fields(self):
self.fields["progress_state"].label = "Status"
self.fields["workbasket"].queryset = WorkBasket.objects.editable()

def init_layout(self):
self.helper = FormHelper(self)
self.helper.label_size = Size.SMALL
self.helper.legend_size = Size.SMALL
self.helper.layout = Layout(
"title",
"description",
"category",
"progress_state",
"workbasket",
Submit(
"submit",
"Save",
data_module="govuk-button",
data_prevent_double_click="true",
),
)


class TaskCreateForm(TaskBaseForm):
def save(self, user, commit=True):
instance = super().save(commit=False)
instance.creator = user
if commit:
instance.save()
return instance


class TaskUpdateForm(TaskBaseForm):
pass


TaskDeleteForm = delete_form_for(Task)
42 changes: 42 additions & 0 deletions tasks/jinja2/tasks/confirm_create.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{% extends "common/confirm_create.jinja" %}

{% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %}
{% from "components/panel/macro.njk" import govukPanel %}
{% from "components/button/macro.njk" import govukButton %}

{% set page_title = "Task created" %}

{% block breadcrumb %}
{{ govukBreadcrumbs({
"items": [
{"text": "Home", "href": url("home")},
{"text": "Create a new task", "href": url("workflow:task-ui-create") },
{"text": "Task created"}
]
}) }}
{% endblock %}

{% block panel %}
{{ govukPanel({
"titleText": "Task: " ~ object.title,
"text": "You have created a new task",
"classes": "govuk-!-margin-bottom-7"
}) }}
{% endblock %}

{% block button_group %}
{{ govukButton({
"text": "View task",
"href": url('workflow:task-ui-detail', kwargs={"pk": object.pk}),
"classes": "govuk-button"
}) }}
{{ govukButton({
"text": "Return to homepage",
"href": url("home"),
"classes": "govuk-button--secondary"
}) }}
{% endblock %}

{% block actions %}
<li><a href="{{ url("workflow:task-ui-list") }}">Find and view tasks</a></li>
{% endblock %}
41 changes: 41 additions & 0 deletions tasks/jinja2/tasks/confirm_delete.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{% extends "layouts/confirm.jinja" %}

{% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %}
{% from "components/panel/macro.njk" import govukPanel %}
{% from "components/button/macro.njk" import govukButton %}

{% set page_title = "Task deleted" %}

{% block breadcrumb %}
{{ govukBreadcrumbs({
"items": [
{"text": "Find and view tasks", "href": url("workflow:task-ui-list")},
{"text": page_title}
]
}) }}
{% endblock %}

{% block panel %}
{{ govukPanel({
"titleText": "Task ID: " ~ deleted_pk,
"text": "Task has been deleted",
"classes": "govuk-!-margin-bottom-7"
}) }}
{% endblock %}

{% block button_group %}
{{ govukButton({
"text": "Find and view tasks",
"href": url("workflow:task-ui-list"),
"classes": "govuk-button"
}) }}
{{ govukButton({
"text": "Return to homepage",
"href": url("home"),
"classes": "govuk-button--secondary"
}) }}
{% endblock %}

{% block actions %}
<li><a href="{{ url("workflow:task-ui-create") }}">Create a task</a></li>
{% endblock %}
41 changes: 41 additions & 0 deletions tasks/jinja2/tasks/confirm_update.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{% extends "common/confirm_update.jinja" %}

{% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %}
{% from "components/panel/macro.njk" import govukPanel %}
{% from "components/button/macro.njk" import govukButton %}

{% set page_title = "Task updated" %}

{% block breadcrumb %}
{{ govukBreadcrumbs({
"items": [
{"text": "Task: " ~ object.title, "href": url("workflow:task-ui-detail", kwargs={"pk": object.pk})},
{"text": page_title}
]
}) }}
{% endblock %}

{% block panel %}
{{ govukPanel({
"titleText": "Task: " ~ object.title,
"text": "Task has been updated",
"classes": "govuk-!-margin-bottom-7"
}) }}
{% endblock %}

{% block button_group %}
{{ govukButton({
"text": "View task",
"href": url('workflow:task-ui-detail', kwargs={"pk": object.pk}),
"classes": "govuk-button"
}) }}
{{ govukButton({
"text": "Return to homepage",
"href": url("home"),
"classes": "govuk-button--secondary"
}) }}
{% endblock %}

{% block actions %}
<li><a href="{{ url("workflow:task-ui-list") }}">Find and view tasks</a></li>
{% endblock %}
26 changes: 26 additions & 0 deletions tasks/jinja2/tasks/delete.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{% extends "common/delete.jinja" %}

{% from "components/breadcrumbs.jinja" import breadcrumbs %}
{% from "components/warning-text/macro.njk" import govukWarningText %}
{% from "components/button/macro.njk" import govukButton %}

{% set page_title = "Delete Task:" ~ object.title %}

{% block breadcrumb %}
{{ breadcrumbs(request, [
{"text": "Find and view tasks", "href": url("workflow:task-ui-list")},
{"text": "Task: " ~ object.title, "href": url("workflow:task-ui-detail", kwargs={"pk": object.pk})},
{"text": page_title}
])
}}
{% endblock %}

{% block form %}
{{ govukWarningText({
"text": "Are you sure you want to delete this task?"
}) }}

{% call django_form(action=url("workflow:task-ui-delete", kwargs={"pk": object.pk})) %}
{{ crispy(form) }}
{% endcall %}
{% endblock %}
Loading

0 comments on commit de2733c

Please sign in to comment.