Skip to content

Commit b5b4105

Browse files
SilviaAmAmsvenvandescheur
authored andcommitted
✨ [#227] Reassign assignees
1 parent b56f9a4 commit b5b4105

File tree

4 files changed

+87
-20
lines changed

4 files changed

+87
-20
lines changed

backend/src/openarchiefbeheer/destruction/api/viewsets.py

+7-8
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
ReviewResponse,
2323
)
2424
from ..tasks import delete_destruction_list
25+
from ..utils import process_new_assignees
2526
from .filtersets import (
2627
DestructionListFilterset,
2728
DestructionListItemFilterset,
@@ -265,14 +266,12 @@ def reassign(self, request, *args, **kwargs):
265266
)
266267
serialiser.is_valid(raise_exception=True)
267268

268-
with transaction.atomic():
269-
destruction_list.assignees.filter(
270-
role=serialiser.validated_data["role"]
271-
).delete()
272-
new_assignees = destruction_list.bulk_create_assignees(
273-
serialiser.validated_data["assignees"],
274-
serialiser.validated_data["role"],
275-
)
269+
new_assignees = process_new_assignees(
270+
destruction_list,
271+
serialiser.validated_data["assignees"],
272+
serialiser.validated_data["role"],
273+
)
274+
destruction_list.reassign()
276275

277276
logevent.destruction_list_reassigned(
278277
destruction_list,

backend/src/openarchiefbeheer/destruction/assignment_logic.py

+39-11
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
from .constants import ListRole, ListStatus, ReviewDecisionChoices
66

77
if TYPE_CHECKING:
8-
from .models import DestructionList
8+
from .models import DestructionList, DestructionListAssignee
99

1010

1111
class State(Protocol):
1212
def assign_next(self, destruction_list: "DestructionList") -> None: ...
1313

14+
def reassign(self, destruction_list: "DestructionList") -> None: ...
15+
1416

1517
class NewList:
1618
def assign_next(self, destruction_list: "DestructionList") -> None:
@@ -23,8 +25,27 @@ def assign_next(self, destruction_list: "DestructionList") -> None:
2325
destruction_list.set_status(ListStatus.ready_to_review)
2426
destruction_list.assign(assignee)
2527

28+
def reassign(self, destruction_list: "DestructionList") -> None:
29+
# When a list is new, it is assigned to the author. No action needed.
30+
pass
31+
2632

2733
class ReadyToReview:
34+
def _deduce_next_reviewer(
35+
self, destruction_list: "DestructionList"
36+
) -> "DestructionListAssignee":
37+
# Find the reviewers who have given fewer reviews
38+
reviewers = destruction_list.assignees.filter(role=ListRole.reviewer).annotate(
39+
num_reviews=Count("user__created_reviews")
40+
)
41+
min_number_reviews = reviewers.aggregate(min_reviews=Min("num_reviews"))
42+
next_reviewer = (
43+
reviewers.filter(num_reviews=min_number_reviews["min_reviews"])
44+
.order_by("pk")
45+
.first()
46+
)
47+
return next_reviewer
48+
2849
def assign_next(self, destruction_list: "DestructionList") -> None:
2950
last_review = destruction_list.reviews.order_by("created").last()
3051
if last_review and last_review.decision == ReviewDecisionChoices.rejected:
@@ -42,16 +63,11 @@ def assign_next(self, destruction_list: "DestructionList") -> None:
4263
destruction_list.assign(destruction_list.get_author())
4364
return
4465

45-
# Find the reviewers who have given fewer reviews
46-
reviewers = destruction_list.assignees.filter(role=ListRole.reviewer).annotate(
47-
num_reviews=Count("user__created_reviews")
48-
)
49-
min_number_reviews = reviewers.aggregate(min_reviews=Min("num_reviews"))
50-
next_reviewer = (
51-
reviewers.filter(num_reviews=min_number_reviews["min_reviews"])
52-
.order_by("pk")
53-
.first()
54-
)
66+
next_reviewer = self._deduce_next_reviewer(destruction_list)
67+
destruction_list.assign(next_reviewer)
68+
69+
def reassign(self, destruction_list: "DestructionList") -> None:
70+
next_reviewer = self._deduce_next_reviewer(destruction_list)
5571
destruction_list.assign(next_reviewer)
5672

5773

@@ -63,6 +79,10 @@ def assign_next(self, destruction_list: "DestructionList") -> None:
6379
last_review = destruction_list.reviews.order_by("created").last()
6480
destruction_list.assign(destruction_list.get_assignee(last_review.author))
6581

82+
def reassign(self, destruction_list: "DestructionList") -> None:
83+
# When a list has requested changes, it is assigned to the author. No action needed.
84+
pass
85+
6686

6787
class InternallyReviewed:
6888
def assign_next(self, destruction_list: "DestructionList") -> None:
@@ -75,6 +95,10 @@ def assign_next(self, destruction_list: "DestructionList") -> None:
7595
)
7696
destruction_list.assign(archivist)
7797

98+
def reassign(self, destruction_list: "DestructionList") -> None:
99+
# When a list has been internally reviewed, it is assigned to the author. No action needed.
100+
pass
101+
78102

79103
class ReadyForArchivist:
80104
def assign_next(self, destruction_list: "DestructionList") -> None:
@@ -85,6 +109,10 @@ def assign_next(self, destruction_list: "DestructionList") -> None:
85109

86110
# TODO in the case where the archivist rejects it is not clear yet what should happen!
87111

112+
def reassign(self, destruction_list: "DestructionList") -> None:
113+
# TODO
114+
raise NotImplementedError
115+
88116

89117
STATE_MANAGER = {
90118
ListStatus.new: NewList(),

backend/src/openarchiefbeheer/destruction/models.py

+3
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,9 @@ def get_current_assignee(self) -> Optional["DestructionListAssignee"]:
147147
def assign_next(self) -> None:
148148
STATE_MANAGER[self.status].assign_next(self)
149149

150+
def reassign(self) -> None:
151+
STATE_MANAGER[self.status].reassign(self)
152+
150153
def all_reviewers_approved(self) -> bool:
151154
number_of_reviewers = self.assignees.filter(role=ListRole.reviewer).count()
152155
latest_reviews = self.reviews.order_by("created")

backend/src/openarchiefbeheer/destruction/utils.py

+38-1
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33

44
from django.conf import settings
55
from django.core.mail import send_mail
6+
from django.db import transaction
7+
from django.db.models import Q
68

79
from openarchiefbeheer.accounts.models import User
810
from openarchiefbeheer.emails.models import EmailConfig
911
from openarchiefbeheer.emails.render_backend import get_sandboxed_backend
1012

1113
from .constants import InternalStatus
12-
from .models import DestructionList
14+
from .models import DestructionList, DestructionListAssignee
1315

1416

1517
def notify(subject: str, body: str, context: dict, recipients: list[str]) -> None:
@@ -124,3 +126,38 @@ def mark_as_failed_on_error(object_with_status: ObjectWithStatus):
124126
except Exception as exc:
125127
object_with_status.set_processing_status(InternalStatus.failed)
126128
raise exc
129+
130+
131+
def process_new_assignees(
132+
destruction_list: DestructionList,
133+
assignees: list[dict],
134+
role: str,
135+
) -> list[DestructionListAssignee]:
136+
"""
137+
Remove any assignees that are not present in the new assignees and create the new ones.
138+
139+
Example:
140+
Before reassigning there are reviewerA, reviewerB and reviewerC.
141+
The record manager requests that the new reviewers are reviewerB and reviewerD.
142+
This function deletes reviewerA and reviewerC and creates reviewerD.
143+
"""
144+
users = [assignee["user"] for assignee in assignees]
145+
146+
with transaction.atomic():
147+
destruction_list.assignees.filter(~Q(user__in=users), role=role).delete()
148+
149+
existing_assignees_users = [
150+
assignee.user.pk
151+
for assignee in destruction_list.assignees.filter(role=role)
152+
]
153+
assignees_to_create = []
154+
for assignee in assignees:
155+
if assignee["user"].pk not in existing_assignees_users:
156+
assignees_to_create.append(assignee)
157+
158+
new_assignees = destruction_list.bulk_create_assignees(
159+
assignees_to_create,
160+
role,
161+
)
162+
163+
return new_assignees

0 commit comments

Comments
 (0)