Skip to content

Commit

Permalink
feat(api): extract code for cancelling cases and add a python API
Browse files Browse the repository at this point in the history
  • Loading branch information
anehx committed Jun 19, 2020
1 parent ab1bd71 commit ce74d06
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 25 deletions.
21 changes: 21 additions & 0 deletions caluma/caluma_workflow/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,24 @@ def skip_work_item(work_item: models.WorkItem, user: BaseUser) -> models.WorkIte
domain_logic.SkipWorkItemLogic.post_skip(work_item, user)

return work_item


def cancel_case(case: models.Case, user: BaseUser) -> models.Case:
"""
Cancel a case and its pending work items (just like `CancelCase`).
>>> cancel_case(
... case=models.Case.first(),
... user=AnonymousUser()
... )
<Case: Case object (some-uuid)>
"""
domain_logic.CancelCaseLogic.validate_for_cancel(case)

validated_data = domain_logic.CancelCaseLogic.pre_cancel({}, user)

models.Case.objects.filter(pk=case.pk).update(**validated_data)

domain_logic.CancelCaseLogic.post_cancel(case, user)

return case
54 changes: 54 additions & 0 deletions caluma/caluma_workflow/domain_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,57 @@ def post_skip(work_item, user):
)

return work_item


class CancelCaseLogic:
"""
Shared domain logic for cancelling cases.
Used in the `cancelCase` mutation and in the `cancel_case` API. The logic
for case cancellation is split in three parts (`validate_for_cancel`,
`pre_cancel` and `post_cancel`) so that in between the appropriate update
method can be called (`super().update(...)` for the serializer and
`Case.objects.update(...) for the python API`).
"""

@staticmethod
def validate_for_cancel(case):
if case.status != models.Case.STATUS_RUNNING:
raise ValidationError("Only running cases can be canceled.")

@staticmethod
def pre_cancel(validated_data, user):
validated_data["status"] = models.Case.STATUS_CANCELED
validated_data["closed_at"] = timezone.now()
validated_data["closed_by_user"] = user.username
validated_data["closed_by_group"] = user.group

return validated_data

@staticmethod
def post_cancel(case, user):
work_items = case.work_items.exclude(
status__in=[
models.WorkItem.STATUS_COMPLETED,
models.WorkItem.STATUS_CANCELED,
]
)

for work_item in work_items:
work_item.status = models.WorkItem.STATUS_CANCELED
work_item.closed_at = timezone.now()
work_item.closed_by_user = user.username
work_item.closed_by_group = user.group
work_item.save()

# send events in separate loop in order to be sure all operations are finished
for work_item in work_items:
send_event(
events.cancelled_work_item,
sender="post_cancel_case",
work_item=work_item,
)

send_event(events.cancelled_case, sender="post_cancel_case", case=case)

return case
32 changes: 7 additions & 25 deletions caluma/caluma_workflow/serializers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from django.core.exceptions import ValidationError
from django.db import transaction
from django.utils import timezone
from rest_framework import exceptions
from rest_framework.serializers import CharField, ListField
from simple_history.utils import bulk_create_with_history
Expand Down Expand Up @@ -221,40 +220,23 @@ class Meta:
fields = ("id",)

def validate(self, data):
if self.instance.status != models.Case.STATUS_RUNNING:
raise exceptions.ValidationError("Only running cases can be canceled.")
try:
domain_logic.CancelCaseLogic.validate_for_cancel(self.instance)
except ValidationError as e:
raise exceptions.ValidationError(str(e))

user = self.context["request"].user
data["status"] = models.Case.STATUS_CANCELED
data["closed_at"] = timezone.now()
data["closed_by_user"] = user.username
data["closed_by_group"] = user.group
return super().validate(data)

@transaction.atomic
def update(self, instance, validated_data):
instance = super().update(instance, validated_data)
user = self.context["request"].user

work_items = instance.work_items.exclude(
status__in=[
models.WorkItem.STATUS_COMPLETED,
models.WorkItem.STATUS_CANCELED,
]
super().update(
instance, domain_logic.CancelCaseLogic.pre_cancel(validated_data, user)
)

for work_item in work_items:
work_item.status = models.WorkItem.STATUS_CANCELED
work_item.closed_at = timezone.now()
work_item.closed_by_user = user.username
work_item.closed_by_group = user.group
work_item.save()

# send events in separate loop in order to be sure all operations are finished
for work_item in work_items:
self.send_event(events.cancelled_work_item, work_item=work_item)
domain_logic.CancelCaseLogic.post_cancel(instance, user)

self.send_event(events.cancelled_case, case=instance)
return instance


Expand Down
14 changes: 14 additions & 0 deletions caluma/caluma_workflow/tests/test_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -588,3 +588,17 @@ def test_api_start_case(
assert case.document.form == form
assert work_item.document.form == form
assert work_item.status == models.WorkItem.STATUS_READY


@pytest.mark.parametrize(
"case__status,work_item__status",
[(models.Case.STATUS_RUNNING, models.WorkItem.STATUS_READY)],
)
def test_api_cancel_case(db, case, work_item, admin_user):
api.cancel_case(case=case, user=admin_user)

case.refresh_from_db()
work_item.refresh_from_db()

assert case.status == models.Case.STATUS_CANCELED
assert work_item.status == models.WorkItem.STATUS_CANCELED
1 change: 1 addition & 0 deletions docs/interfaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,3 +320,4 @@ in your application that installs Caluma as a django app:
- `caluma_workflow.api.start_case` To start a new case of a given workflow
- `caluma_workflow.api.complete_work_item` To complete a work item
- `caluma_workflow.api.skip_work_item` To skip a work item
- `caluma_workflow.api.cancel_case` To cancel a case

0 comments on commit ce74d06

Please sign in to comment.