Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tp2000 1471 task workflow #1298

Draft
wants to merge 78 commits into
base: master
Choose a base branch
from
Draft
Changes from 1 commit
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
340439c
App initial commit.
paulpepper-trade Aug 29, 2024
301644a
TP2000-1472 Revise task-related models (#1288)
dalecannon Sep 19, 2024
ee4871d
TP2000-1473 Register task-related models in admin site (#1289)
dalecannon Sep 20, 2024
9faac8b
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Sep 20, 2024
0595097
Merge branch 'master' into TP2000-1471--task-workflow
dalecannon Oct 29, 2024
de2733c
Implement prototype UI for tasks (#1308)
dalecannon Oct 31, 2024
ddfea35
TP2000-1487: Create Subtask Form (#1319)
marya-shariq Nov 11, 2024
ead85bc
TP2000-1540, TP2000-1541, TP2000-1546 & TP2000-1553 queues, workflows…
paulpepper-trade Nov 12, 2024
4d7dda9
TP2000-1543 Prototype workflow template detail view (#1322)
dalecannon Nov 12, 2024
77760e5
PR template tweaks
paulpepper-trade Nov 12, 2024
80c0b80
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Nov 12, 2024
5ec8f38
Black formatting
paulpepper-trade Nov 12, 2024
d1d18ed
TP2000-1545 Add task template reordering capability to workflow temp…
dalecannon Nov 14, 2024
84e3c16
TP2000-48 & TP2000-1549 Task template create and detail views (#1326)
paulpepper-trade Nov 15, 2024
f2bead0
TP2000-1551 Task template update views and forms (#1327)
paulpepper-trade Nov 15, 2024
d103846
Fix typo in macro import
paulpepper-trade Nov 15, 2024
d0466d4
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Nov 15, 2024
01db672
TP2000-1569 Embellish create subtask journey (#1325)
LaurenMullally Nov 18, 2024
baf7f61
Add support view support for task workflow deletion (#1329)
paulpepper-trade Nov 18, 2024
f4f351c
TP2000-1544 Prototype workflow template create & update views (#1328)
dalecannon Nov 18, 2024
bc787f8
Fix double quotes lint error from create subtask form pr
LaurenMullally Nov 18, 2024
a28e7bd
TP2000-1573 Implement workflow template delete view (#1330)
dalecannon Nov 19, 2024
2ff7858
Tp 2000 1489 delete subtask (#1332)
marya-shariq Nov 21, 2024
e9a4546
deleted comments
marya-shariq Nov 21, 2024
993ec64
TP2000-1488 Edit Subtasks (#1334)
LaurenMullally Nov 25, 2024
f55394a
Tp2000 1589 - temporary list views tile (#1338)
marya-shariq Nov 25, 2024
5884cfc
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Nov 25, 2024
7dfe059
TP2000-1590 TaskWorkflow summary info support (#1337)
paulpepper-trade Nov 25, 2024
997d36a
Update TaskWorkflowTemplate.create_task_workflow() method and unit te…
dalecannon Nov 28, 2024
4540d83
TP2000-1556 Implement workflow create view (#1340)
dalecannon Nov 28, 2024
2ea9f5c
TP2000-1555 Implement workflow detail view (#1341)
dalecannon Nov 28, 2024
708944c
TP2000-1602 Implement workflow delete view (#1342)
dalecannon Nov 28, 2024
02044ba
Factor out workflow item management helper functions into view mixin …
dalecannon Dec 4, 2024
2bafa60
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Dec 5, 2024
442fd3e
Tp2000 1592 workflow template list view (#1348)
marya-shariq Dec 5, 2024
363bced
TP2000-1582 Enable customisable queue field name on QueueItem subcla…
dalecannon Dec 6, 2024
ec10b14
Include user param in form.save() call (#1350)
paulpepper-trade Dec 6, 2024
3e67a87
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Dec 9, 2024
821d5e8
TP2000-1557 Implement workflow edit view (#1347)
dalecannon Dec 10, 2024
d9937d1
TP2000-1580 Task workflow combined list view pt2 (#1353)
paulpepper-trade Dec 10, 2024
31cafe9
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Dec 10, 2024
206cc7d
TP2000-1615 Various errors (#1357)
paulpepper-trade Dec 17, 2024
ab3854f
Tp2000 1594 ordering of workflow templates list view (#1356)
marya-shariq Dec 17, 2024
10f14de
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Dec 17, 2024
dd663ac
Add migrations.
paulpepper-trade Dec 17, 2024
5824196
Fix failing test (#1362)
paulpepper-trade Dec 17, 2024
4f054b6
Fix UI bug (#1364)
paulpepper-trade Dec 18, 2024
d1c3505
TP2000-1600 Add Subtask filter to Task admin view (#1344)
LaurenMullally Dec 19, 2024
1f94a81
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Dec 23, 2024
82a1f69
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Jan 2, 2025
9b99475
Add simple TaskWorkflowAdmin view (#1373)
LaurenMullally Jan 3, 2025
755fac6
Tp2000 1542 workflow template admin support (#1374)
marya-shariq Jan 9, 2025
69e65e5
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Jan 10, 2025
c6c0a43
TP2000-1652 Enhance workflow, workflow template admin views (#1381)
dalecannon Jan 15, 2025
b60a3c9
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Jan 15, 2025
6d9e368
TP2000-1658 Correct usage of select_for_update() in QueueItem instan…
dalecannon Jan 16, 2025
676edc5
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Jan 23, 2025
12f4297
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Feb 5, 2025
64a4db5
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Feb 5, 2025
fab1fab
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Feb 11, 2025
2064d09
TP2000-1583 Add task assignee update support (#1407)
paulpepper-trade Feb 11, 2025
6590627
Renumber clashing migration
paulpepper-trade Feb 11, 2025
50d8c6a
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Feb 11, 2025
761d076
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Feb 11, 2025
16ba584
Tp2000 1616 workflow task create view (#1363)
marya-shariq Feb 12, 2025
b1401a8
TP2000-1653 Enhance AutoCompleteField to support a broader range of …
dalecannon Feb 12, 2025
56f1d4b
Restrict factory to workbasket assignees
paulpepper-trade Feb 12, 2025
6f75f4f
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Feb 12, 2025
280ee81
TP2000-1651 Fix sorting on list views (#1380)
dalecannon Feb 13, 2025
562a3c1
Pre-sort objects in view sorting tests to guarantee consistent ordering
dalecannon Feb 13, 2025
380d87b
UI text changes for new 'tickets' tile on homepage (#1414)
marya-shariq Feb 20, 2025
48ef119
references to subtasks removed from UI
marya-shariq Feb 20, 2025
b64597d
Revert "references to subtasks removed from UI"
marya-shariq Feb 20, 2025
32e04c9
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Feb 26, 2025
fe6675a
TP2000-1725 Redesign ticket detail view (#1417)
dalecannon Feb 28, 2025
7ca53f0
Tp2000 1726 delete ticket view (#1420)
marya-shariq Feb 28, 2025
7ebc168
TP200-1722 Create New Ticket (#1419)
LaurenMullally Mar 3, 2025
57bbf8e
TP2000-1724 Redesign ticket edit view (#1421)
dalecannon Mar 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
TP2000-1582 Enable customisable queue field name on QueueItem subclas…
…ses (#1343)

* Amend Queue model docstring

* Enable customisable queue field name of QueueItem concrete subclasses

* Add QueueItem metaclass validation tests

* Add type hint to QueueItem.queue_field and update its docstring

* Get field_name post-init to include inherited attributes from parent class

* Rename queue field on TaskItem and TaskItemTemplate models

* Update references to named queue

* Recommend QueueItem subclasses redefine model Meta class following queue field renaming
  • Loading branch information
dalecannon authored Dec 6, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
commit 363bced4e0a5bcde5fbe7904d631c95a7c7610a4
2 changes: 1 addition & 1 deletion common/util.py
Original file line number Diff line number Diff line change
@@ -796,7 +796,7 @@ def get_related_names(instance, related_model) -> list[str]:

If a reverse foreign-key relationship exists but no related name has been
defined, a default name in the format `relatedmodel_set` will be returned.
If no such relationships exist, an empty list if returned.
If no such relationships exist, an empty list is returned.
"""
related_names = []
for field in instance._meta.get_fields():
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Generated by Django 4.2.15 on 2024-12-05 15:46

import django.db.models.deletion
from django.db import migrations
from django.db import models


class Migration(migrations.Migration):

dependencies = [
("tasks", "0016_taskworkflowtemplate_creator"),
]

operations = [
migrations.RenameField(
model_name="taskitem",
old_name="queue",
new_name="workflow",
),
migrations.AlterField(
model_name="taskitem",
name="workflow",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="workflow_items",
to="tasks.taskworkflow",
),
),
migrations.AlterModelOptions(
name="taskitem",
options={"ordering": ["workflow", "position"]},
),
migrations.RenameField(
model_name="taskitemtemplate",
old_name="queue",
new_name="workflow_template",
),
migrations.AlterField(
model_name="taskitemtemplate",
name="workflow_template",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="workflow_template_items",
to="tasks.taskworkflowtemplate",
),
),
migrations.AlterModelOptions(
name="taskitemtemplate",
options={"ordering": ["workflow_template", "position"]},
),
]
90 changes: 67 additions & 23 deletions tasks/models/queue.py
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@

from typing import Self

from django.core.exceptions import FieldDoesNotExist
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.db.transaction import atomic
@@ -18,11 +19,8 @@ class Queue(models.Model):
"""
A (FIFO) queue.

Note: This abstract class only supports a single reverse foreign-key relationship
to `QueueItem` for each instance. As such, all instance methods in this class use
the first-returned related name to access related `QueueItem` objects. This means that
if there are multiple relationships, only the first one will be considered when
retrieving items from the queue,
Note: This abstract class only supports a single, reverse foreign-key relationship
to `QueueItem` for each instance (i.e `QueueItem` instances are assumed to belong only to a single `Queue` instance).
"""

class Meta:
@@ -65,18 +63,31 @@ class QueueItemMetaClass(models.base.ModelBase):
def __new__(cls, name, bases, attrs):
new_class = super().__new__(cls, name, bases, attrs)

if (
"QueueItem" in [base.__name__ for base in bases]
and not new_class._meta.abstract
):
queue_field = attrs.get("queue", None)
if not queue_field or not isinstance(queue_field, models.ForeignKey):
raise RequiredFieldError(
f"{name} must have a 'queue' ForeignKey field.",
)
if not new_class._meta.abstract:
queue_field_name = getattr(new_class, "queue_field", None)
cls.validate_queue_field(new_class, queue_field_name)

return new_class

@staticmethod
def validate_queue_field(new_class: type[Self], queue_field_name: str) -> None:
"""Validate that concrete subclasses of `QueueItem` have a ForeignKey
field to a subclass of `Queue` model."""
try:
queue_field = new_class._meta.get_field(queue_field_name)
except FieldDoesNotExist:
queue_field = None

if not queue_field or not isinstance(queue_field, models.ForeignKey):
raise RequiredFieldError(
f"{new_class.__name__} must have a 'queue' ForeignKey field. The name of the field must match the value given to the 'queue_field' attribute on the model.",
)

if not issubclass(queue_field.remote_field.model, Queue):
raise RequiredFieldError(
f"{queue_field} must be a ForeignKey field to a subclass of 'Queue' model.",
)


class QueueItemManager(models.Manager):
@atomic
@@ -85,7 +96,8 @@ def create(self, **kwargs) -> QueueItem:
param, and place it in last position."""

with TableLock(self.model, lock=TableLock.EXCLUSIVE):
queue = kwargs.pop("queue")
queue_field = self.model.queue_field
queue = kwargs.pop(queue_field)
position = kwargs.pop("position", (queue.get_items().count() + 1))

if position <= 0:
@@ -94,8 +106,8 @@ def create(self, **kwargs) -> QueueItem:
)

return super().create(
queue=queue,
position=position,
**{queue_field: queue},
**kwargs,
)

@@ -107,6 +119,18 @@ class Meta:
abstract = True
ordering = ["queue", "position"]

queue_field: str = "queue"
"""
The name of the required ForeignKey field relating this instance to a Queue
instance.

The value of this attribute can be inherited as is or overridden in
subclasses to reflect the specific purpose or role of the queue.

If this value is overridden, the subclass must redefine the `Meta` class
to maintain ordering based on the queue field.
"""

position = models.PositiveSmallIntegerField(
db_index=True,
editable=False,
@@ -117,6 +141,14 @@ class Meta:

objects = QueueItemManager()

def get_queue_field(self) -> str:
"""Return the queue field name on this instance."""
return self.__class__.queue_field

def get_queue(self) -> type[Queue]:
"""Return the queue instance related to this instance."""
return getattr(self, self.get_queue_field())

@atomic
def delete(self):
"""Remove and delete instance from its queue, shuffling all successive
@@ -125,7 +157,7 @@ def delete(self):

self.__class__.objects.select_for_update(nowait=True).filter(
position__gt=instance.position,
queue=instance.queue,
**{self.get_queue_field(): self.get_queue()},
).update(position=models.F("position") - 1)

return super().delete()
@@ -146,7 +178,7 @@ def promote(self) -> Self:

item_to_demote = self.__class__.objects.select_for_update(nowait=True).get(
position=instance.position - 1,
queue=instance.queue,
**{self.get_queue_field(): self.get_queue()},
)
item_to_demote.position += 1
instance.position -= 1
@@ -166,12 +198,18 @@ def demote(self) -> Self:
"""
instance = self.__class__.objects.select_for_update(nowait=True).get(pk=self.pk)

if instance.position == self.queue.max_position:
queue_field = self.get_queue_field()
queue = self.get_queue()
queue_kwarg = {
queue_field: queue,
}

if instance.position == queue.max_position:
return instance

item_to_promote = self.__class__.objects.select_for_update(nowait=True).get(
position=instance.position + 1,
queue=instance.queue,
**queue_kwarg,
)
item_to_promote.position -= 1
instance.position += 1
@@ -198,7 +236,7 @@ def promote_to_first(self) -> Self:

self.__class__.objects.select_for_update(nowait=True).filter(
position__lt=instance.position,
queue=instance.queue,
**{self.get_queue_field(): self.get_queue()},
).update(position=models.F("position") + 1)

instance.position = 1
@@ -219,13 +257,19 @@ def demote_to_last(self) -> Self:
"""
instance = self.__class__.objects.select_for_update(nowait=True).get(pk=self.pk)

last_place = self.queue.max_position
queue_field = self.get_queue_field()
queue = self.get_queue()
queue_kwarg = {
queue_field: queue,
}

last_place = queue.max_position
if instance.position == last_place:
return instance

self.__class__.objects.select_for_update(nowait=True).filter(
position__gt=instance.position,
queue=instance.queue,
**queue_kwarg,
).update(position=models.F("position") - 1)

instance.position = last_place
32 changes: 23 additions & 9 deletions tasks/models/workflow.py
Original file line number Diff line number Diff line change
@@ -48,7 +48,9 @@ def get_tasks(self) -> models.QuerySet:
"""Get a QuerySet of the Tasks associated through their TaskItem
instances to this TaskWorkflow, ordered by the position of the
TaskItem."""
return Task.objects.filter(taskitem__queue=self).order_by("taskitem__position")
return Task.objects.filter(taskitem__workflow=self).order_by(
"taskitem__position",
)

def get_url(self, action: str = "detail"):
if action == "detail":
@@ -73,9 +75,11 @@ class TaskItem(QueueItem):
"""Task item queue management for Task instances (these should always be
subtasks)."""

queue = models.ForeignKey(
queue_field = "workflow"

workflow = models.ForeignKey(
TaskWorkflow,
related_name="queue_items",
related_name="workflow_items",
on_delete=models.CASCADE,
)
task = models.OneToOneField(
@@ -84,6 +88,9 @@ class TaskItem(QueueItem):
)
"""The Task instance managed by this TaskItem."""

class Meta:
ordering = ["workflow", "position"]


# ----------------------------------------
# - Template workflows and template tasks.
@@ -129,7 +136,9 @@ def get_task_templates(self) -> models.QuerySet:
"""Get a QuerySet of the TaskTemplates associated through their
TaskItemTemplate instances to this TaskWorkflowTemplate, ordered by the
position of the TaskItemTemplate."""
return TaskTemplate.objects.filter(taskitemtemplate__queue=self).order_by(
return TaskTemplate.objects.filter(
taskitemtemplate__workflow_template=self,
).order_by(
"taskitemtemplate__position",
)

@@ -155,7 +164,7 @@ def create_task_workflow(

task_item_templates = TaskItemTemplate.objects.select_related(
"task_template",
).filter(queue=self)
).filter(workflow_template=self)
for task_item_template in task_item_templates:
task_template = task_item_template.task_template
task = Task.objects.create(
@@ -165,7 +174,7 @@ def create_task_workflow(
)
TaskItem.objects.create(
position=task_item_template.position,
queue=task_workflow,
workflow=task_workflow,
task=task,
)

@@ -202,16 +211,21 @@ def get_url(self, action: str = "detail"):
class TaskItemTemplate(QueueItem):
"""Queue item management for TaskTemplate instances."""

queue = models.ForeignKey(
queue_field = "workflow_template"

workflow_template = models.ForeignKey(
TaskWorkflowTemplate,
related_name="queue_items",
related_name="workflow_template_items",
on_delete=models.CASCADE,
)
task_template = models.OneToOneField(
"tasks.TaskTemplate",
on_delete=models.CASCADE,
)

class Meta:
ordering = ["workflow_template", "position"]


class TaskTemplate(TaskBase):
"""Template used to create Task instances from within a template
@@ -226,7 +240,7 @@ def get_url(self, action: str = "detail"):
return reverse(
"workflow:task-template-ui-delete",
kwargs={
"workflow_template_pk": self.taskitemtemplate.queue.pk,
"workflow_template_pk": self.taskitemtemplate.workflow_template.pk,
"pk": self.pk,
},
)
12 changes: 6 additions & 6 deletions tasks/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -69,7 +69,7 @@ def task_workflow_template_single_task_template_item(

task_template = factories.TaskTemplateFactory.create()
factories.TaskItemTemplateFactory.create(
queue=task_workflow_template,
workflow_template=task_workflow_template,
task_template=task_template,
)

@@ -89,15 +89,15 @@ def task_workflow_template_three_task_template_items(
for _ in range(3):
task_template = factories.TaskTemplateFactory.create()
task_item_template = factories.TaskItemTemplateFactory.create(
queue=task_workflow_template,
workflow_template=task_workflow_template,
task_template=task_template,
)
task_item_templates.append(task_item_template)

assert task_workflow_template.get_items().count() == 3
assert (
TaskItemTemplate.objects.filter(
queue=task_workflow_template,
workflow_template=task_workflow_template,
).count()
== 3
)
@@ -123,7 +123,7 @@ def task_workflow_single_task_item(task_workflow) -> TaskWorkflow:
associated Task instance."""

task_item = factories.TaskItemFactory.create(
queue=task_workflow,
workflow=task_workflow,
)

assert task_workflow.get_items().count() == 1
@@ -143,11 +143,11 @@ def task_workflow_three_task_items(

task_items = factories.TaskItemFactory.create_batch(
expected_count,
queue=task_workflow,
workflow=task_workflow,
)

assert task_workflow.get_items().count() == expected_count
assert TaskItem.objects.filter(queue=task_workflow).count() == expected_count
assert TaskItem.objects.filter(workflow=task_workflow).count() == expected_count
assert (
Task.objects.filter(taskitem__in=[item.pk for item in task_items]).count()
== expected_count
Loading
Loading