Skip to content

Commit

Permalink
Merge pull request #118 from maykinmedia/feature/111-process-of-assig…
Browse files Browse the repository at this point in the history
…nement

[#111] Part 1 - Assignement flow
  • Loading branch information
SilviaAmAm authored Jun 18, 2024
2 parents 299872e + 343dad3 commit a6ace71
Show file tree
Hide file tree
Showing 12 changed files with 440 additions and 32 deletions.
6 changes: 6 additions & 0 deletions backend/src/openarchiefbeheer/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from rest_framework import routers

from openarchiefbeheer.accounts.api.views import ReviewersView, WhoAmIView
from openarchiefbeheer.destruction.api.views import ListStatusesListView
from openarchiefbeheer.destruction.api.viewsets import (
DestructionListItemsViewSet,
DestructionListReviewViewSet,
Expand Down Expand Up @@ -67,6 +68,11 @@
[
path("reviewers/", ReviewersView.as_view(), name="reviewers"),
path("whoami/", WhoAmIView.as_view(), name="whoami"),
path(
"destruction-list-statuses/",
ListStatusesListView.as_view(),
name="destruction-list-statuses",
),
path(
"_retrieve_zaken/", CacheZakenView.as_view(), name="retrieve-zaken"
),
Expand Down
34 changes: 34 additions & 0 deletions backend/src/openarchiefbeheer/destruction/api/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from rest_framework import permissions

from ..constants import ListStatus


class CanStartDestructionPermission(permissions.BasePermission):
message = _("You are not allowed to create a destruction list.")
Expand All @@ -15,3 +17,35 @@ class CanReviewPermission(permissions.BasePermission):

def has_permission(self, request, view):
return request.user.role and request.user.role.can_review_destruction


class CanUpdateDestructionList(permissions.BasePermission):
message = _(
"You are either not allowed to update this destruction list or "
"the destruction list can currently not be updated."
)

def has_permission(self, request, view):
return request.user.role and request.user.role.can_start_destruction

def has_object_permission(self, request, view, destruction_list):
return (
request.user == destruction_list.author
and destruction_list.status == ListStatus.new
)


class CanMakeRequestedChanges(permissions.BasePermission):
message = _(
"You are either not allowed to update this destruction list or "
"the destruction list can currently not be updated."
)

def has_permission(self, request, view):
return request.user.role and request.user.role.can_start_destruction

def has_object_permission(self, request, view, destruction_list):
return (
request.user == destruction_list.author
and destruction_list.status == ListStatus.changes_requested
)
32 changes: 25 additions & 7 deletions backend/src/openarchiefbeheer/destruction/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from openarchiefbeheer.logging import logevent
from openarchiefbeheer.zaken.api.serializers import ZaakSerializer

from ..constants import ListItemStatus, ReviewDecisionChoices
from ..constants import ListItemStatus, ListRole, ListStatus, ReviewDecisionChoices
from ..models import (
DestructionList,
DestructionListAssignee,
Expand Down Expand Up @@ -107,10 +107,16 @@ def __init__(self, *args, **kwargs):
def validate_assignees(
self, assignees: list[DestructionListAssignee]
) -> list[DestructionListAssignee]:
if len(assignees) != len(set([assignee["user"].pk for assignee in assignees])):
assignees_pks = [assignee["user"].pk for assignee in assignees]
if len(assignees) != len(set(assignees_pks)):
raise ValidationError(
_("The same user should not be selected as a reviewer more than once.")
)

author = self.context["request"].user
if author.pk in assignees_pks:
raise ValidationError(_("The author of a list cannot also be a reviewer."))

return assignees

def create(self, validated_data: dict) -> DestructionList:
Expand All @@ -119,12 +125,17 @@ def create(self, validated_data: dict) -> DestructionList:

author = self.context["request"].user
validated_data["author"] = author
validated_data["status"] = ListStatus.ready_to_review
destruction_list = DestructionList.objects.create(**validated_data)

destruction_list.bulk_create_items(items_data)
assignees = destruction_list.bulk_create_assignees(assignees_data)

destruction_list.assign(assignees[0])
# Create an assignee also for the author
DestructionListAssignee.objects.create(
user=author, destruction_list=destruction_list, role=ListRole.author
)
reviewers = destruction_list.bulk_create_reviewers(assignees_data)

destruction_list.assign(reviewers[0])

logevent.destruction_list_created(destruction_list, author)

Expand All @@ -146,8 +157,8 @@ def update(
instance.bulk_create_items(items_data)

if assignees_data is not None:
instance.assignees.all().delete()
instance.bulk_create_assignees(assignees_data)
instance.assignees.filter(role=ListRole.reviewer).delete()
instance.bulk_create_reviewers(assignees_data)

instance.save()

Expand Down Expand Up @@ -299,4 +310,11 @@ def create(self, validated_data: dict) -> DestructionListReview:
]
DestructionListItemReview.objects.bulk_create(review_items_data)

destruction_list = validated_data["destruction_list"]
if review.decision == ReviewDecisionChoices.accepted:
destruction_list.assign_next()
else:
destruction_list.set_status(ListStatus.changes_requested)
destruction_list.get_author().assign()

return review
28 changes: 28 additions & 0 deletions backend/src/openarchiefbeheer/destruction/api/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from django.utils.translation import gettext_lazy as _

from drf_spectacular.plumbing import build_array_type, build_basic_type
from drf_spectacular.utils import OpenApiExample, OpenApiTypes, extend_schema
from rest_framework.response import Response
from rest_framework.views import APIView

from ..constants import ListStatus


@extend_schema(
summary=_("List destruction list statuses"),
description=_("List the possible statuses that a destruction lists can have."),
tags=["statuses"],
responses={
200: build_array_type(build_array_type(build_basic_type(OpenApiTypes.STR)))
},
examples=[
OpenApiExample(
name="Example response",
response_only=True,
value=[["key1", "label1"], ["key2", "label2"]],
)
],
)
class ListStatusesListView(APIView):
def get(self, request):
return Response(ListStatus.choices)
19 changes: 18 additions & 1 deletion backend/src/openarchiefbeheer/destruction/api/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,22 @@
from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.utils import OpenApiExample, extend_schema, extend_schema_view
from rest_framework import mixins, viewsets
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response

from ..models import DestructionList, DestructionListItem, DestructionListReview
from .filtersets import (
DestructionListFilterset,
DestructionListItemFilterset,
DestructionListReviewFilterset,
)
from .permissions import CanReviewPermission, CanStartDestructionPermission
from .permissions import (
CanMakeRequestedChanges,
CanReviewPermission,
CanStartDestructionPermission,
CanUpdateDestructionList,
)
from .serializers import (
DestructionListItemSerializer,
DestructionListResponseSerializer,
Expand Down Expand Up @@ -128,6 +135,10 @@ class DestructionListViewSet(
def get_permissions(self):
if self.action == "create":
permission_classes = [IsAuthenticated & CanStartDestructionPermission]
elif self.action == "update":
permission_classes = [IsAuthenticated & CanUpdateDestructionList]
elif self.action == "make_requested_changes":
permission_classes = [IsAuthenticated & CanMakeRequestedChanges]
else:
permission_classes = [IsAuthenticated]
return [permission() for permission in permission_classes]
Expand All @@ -145,6 +156,12 @@ def create(self, request, *args, **kwargs):
def update(self, request, *args, **kwargs):
return super().update(request, *args, **kwargs)

@action(detail=True, methods=["patch"], name="make-requested-changes")
def make_requested_changes(self, request, *args, **kwargs):
# Triggers the object permissions check
self.get_object()
return Response("Not implemented!")


@extend_schema_view(
list=extend_schema(
Expand Down
13 changes: 10 additions & 3 deletions backend/src/openarchiefbeheer/destruction/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@


class ListStatus(models.TextChoices):
in_progress = "in_progress", _("in progress")
processing = "processing", _("processing")
completed = "completed", _("completed")
new = "new", _("new")
ready_to_review = "ready_to_review", _("ready to review")
changes_requested = "changes_requested", _("changes requested")
ready_to_delete = "ready_to_delete", _("ready to delete")
deleted = "deleted", _("deleted")


class ListItemStatus(models.TextChoices):
Expand All @@ -19,3 +21,8 @@ class ListItemStatus(models.TextChoices):
class ReviewDecisionChoices(models.TextChoices):
accepted = "accepted", _("accepted")
rejected = "rejected", _("rejected")


class ListRole(models.TextChoices):
reviewer = "reviewer", _("Reviewer")
author = "author", _("Author")
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Generated by Django 4.2.11 on 2024-06-17 13:03

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("destruction", "0007_destructionlistreview_destructionlistitemreview"),
]

operations = [
migrations.AddField(
model_name="destructionlistassignee",
name="role",
field=models.CharField(
choices=[("reviewer", "Reviewer"), ("author", "Author")],
default="reviewer",
max_length=80,
verbose_name="role",
),
),
migrations.AlterField(
model_name="destructionlist",
name="status",
field=models.CharField(
choices=[
("new", "new"),
("ready_to_review", "ready to review"),
("changes_requested", "changes requested"),
("ready_to_delete", "ready to delete"),
("deleted", "deleted"),
],
default="new",
max_length=80,
verbose_name="status",
),
),
]
30 changes: 27 additions & 3 deletions backend/src/openarchiefbeheer/destruction/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from openarchiefbeheer.destruction.constants import (
ListItemStatus,
ListRole,
ListStatus,
ReviewDecisionChoices,
)
Expand Down Expand Up @@ -58,7 +59,7 @@ class DestructionList(models.Model):
)
status = models.CharField(
_("status"),
default=ListStatus.in_progress,
default=ListStatus.new,
choices=ListStatus.choices,
max_length=80,
)
Expand Down Expand Up @@ -92,12 +93,14 @@ def set_status(self, status: str) -> None:
self.status_changed = timezone.now()
self.save()

def bulk_create_assignees(
def bulk_create_reviewers(
self, assignees_data: dict
) -> list["DestructionListAssignee"]:
return DestructionListAssignee.objects.bulk_create(
[
DestructionListAssignee(**{**assignee, "destruction_list": self})
DestructionListAssignee(
**{**assignee, "role": ListRole.reviewer, "destruction_list": self}
)
for assignee in assignees_data
]
)
Expand All @@ -110,6 +113,21 @@ def bulk_create_items(self, items_data: dict) -> list["DestructionListItem"]:
]
)

def get_author(self) -> "DestructionListAssignee":
return self.assignees.get(role=ListRole.author)

def assign_next(self) -> None:
reviewers = self.assignees.filter(role=ListRole.reviewer).order_by("order")

# All reviews are completed
if self.assignee == reviewers.last().user:
self.get_author().assign()
self.set_status(ListStatus.ready_to_delete)
return

next_reviewer = reviewers[self.assignee.order]
next_reviewer.assign()


class DestructionListItem(models.Model):
destruction_list = models.ForeignKey(
Expand Down Expand Up @@ -178,6 +196,12 @@ class DestructionListAssignee(OrderedModel):
help_text=_("The user assigned to the destruction list."),
)
assigned_on = models.DateTimeField(_("assigned on"), blank=True, null=True)
role = models.CharField(
_("role"),
default=ListRole.reviewer,
choices=ListRole.choices,
max_length=80,
)

class Meta(OrderedModel.Meta):
verbose_name = _("destruction list assignee")
Expand Down
7 changes: 7 additions & 0 deletions backend/src/openarchiefbeheer/destruction/tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ class Meta:
model = "destruction.DestructionList"


class DestructionListAssigneeFactory(factory.django.DjangoModelFactory):
destruction_list = factory.SubFactory(DestructionListFactory)

class Meta:
model = "destruction.DestructionListAssignee"


class DestructionListItemFactory(factory.django.DjangoModelFactory):
destruction_list = factory.SubFactory(DestructionListFactory)
zaak = factory.Faker("url")
Expand Down
Loading

0 comments on commit a6ace71

Please sign in to comment.