Skip to content

Commit

Permalink
Merge pull request #2381 from uktrade/uat
Browse files Browse the repository at this point in the history
prod release
  • Loading branch information
saruniitr authored Jan 21, 2025
2 parents 397e2b9 + 58d738e commit 7690f3d
Show file tree
Hide file tree
Showing 21 changed files with 994 additions and 18 deletions.
25 changes: 25 additions & 0 deletions api/applications/serializers/advice.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,3 +303,28 @@ def get_flags(self, instance):
return list(instance.flags.filter(status=FlagStatuses.ACTIVE).values("id", "name", "colour", "label"))
else:
return list(instance.flags.values("id", "name", "colour", "label"))


class BulkApprovalAdviceSerializer(serializers.ModelSerializer):
user = serializers.PrimaryKeyRelatedField(queryset=GovUser.objects.filter(status=UserStatuses.ACTIVE))
case = serializers.PrimaryKeyRelatedField(queryset=Case.objects.all())

class Meta:
model = Advice
fields = (
"case",
"user",
"type",
"text",
"proviso",
"note",
"level",
"footnote",
"footnote_required",
"good",
"consignee",
"end_user",
"ultimate_end_user",
"third_party",
"country",
)
1 change: 1 addition & 0 deletions api/audit_trail/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ class AuditType(LiteEnum):
AMENDMENT_CREATED = autostr()
DEVELOPER_INTERVENTION = autostr()
ADD_EXPORTER_USER_TO_ORGANISATION = autostr()
CREATE_BULK_APPROVAL_RECOMMENDATION = autostr()

def human_readable(self):
"""
Expand Down
4 changes: 4 additions & 0 deletions api/audit_trail/formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,3 +351,7 @@ def exporter_submitted_amendment(**payload):

def amendment_created(**payload):
return f"created the case to supersede {payload['superseded_case']['reference_code']}."


def create_bulk_approval_recommendation(**payload):
return "added a recommendation using the Approve button in the queue."
288 changes: 288 additions & 0 deletions api/audit_trail/migrations/0029_add_create_bulk_approval_audit_verb.py

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions api/audit_trail/payload.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,4 +164,5 @@ def format_payload(audit_type, payload):
AuditType.AMENDMENT_CREATED: formatters.amendment_created,
AuditType.DEVELOPER_INTERVENTION: "updated application information",
AuditType.ADD_EXPORTER_USER_TO_ORGANISATION: " added exporter {exporter_email} to sites {site_names}",
AuditType.CREATE_BULK_APPROVAL_RECOMMENDATION: formatters.create_bulk_approval_recommendation,
}
5 changes: 3 additions & 2 deletions api/cases/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,9 @@ def with_submitted_range(self, submitted_from, submitted_to):
def with_finalised_range(self, finalised_from, finalised_to):
qs = self.filter(status__status=CaseStatusEnum.FINALISED)
if finalised_from:
qs = qs.filter(advice__level=AdviceLevel.FINAL, advice__created_at__date__gte=finalised_from)
qs = qs.filter(licence_decisions__created_at__date__gte=finalised_from)
if finalised_to:
qs = qs.filter(advice__level=AdviceLevel.FINAL, advice__created_at__date__lte=finalised_to)
qs = qs.filter(licence_decisions__created_at__date__lte=finalised_to)
return qs

def with_party_name(self, party_name):
Expand Down Expand Up @@ -320,6 +320,7 @@ def search( # noqa
"case_assignments__user__baseuser_ptr",
"case_assignments__user__team",
"case_assignments__queue",
"licence_decisions",
"queues",
"queues__team",
"baseapplication__licences",
Expand Down
22 changes: 22 additions & 0 deletions api/cases/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,28 @@ def no_licence_required(self):

notify_exporter_no_licence_required(self)

def move_case_forward(self, queue, user):
from api.audit_trail import service as audit_trail_service
from api.workflow.user_queue_assignment import user_queue_assignment_workflow

assignments = (
CaseAssignment.objects.select_related("queue").filter(case=self, queue=queue).order_by("queue__name")
)

# Unassign existing case advisors to be able to move forward
if assignments:
assignments.delete()

# Run routing rules and move the case forward
user_queue_assignment_workflow([queue], self)

audit_trail_service.create(
actor=user,
verb=AuditType.UNASSIGNED_QUEUES,
target=self,
payload={"queues": [queue.name], "additional_text": ""},
)

@transaction.atomic
def finalise(self, user, decisions, note):
from api.audit_trail import service as audit_trail_service
Expand Down
25 changes: 12 additions & 13 deletions api/cases/tests/test_case_search_advanced.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
)
from api.cases.enums import AdviceType
from api.cases.models import Case
from api.cases.tests.factories import TeamAdviceFactory, FinalAdviceFactory
from api.cases.tests.factories import LicenceDecisionFactory, TeamAdviceFactory, FinalAdviceFactory
from api.flags.tests.factories import FlagFactory
from api.goods.tests.factories import GoodFactory
from api.parties.tests.factories import PartyFactory
Expand Down Expand Up @@ -289,18 +289,17 @@ def test_filter_by_finalised_date_range(self):
application_1 = StandardApplicationFactory()
application_1.status = get_case_status_by_status(CaseStatusEnum.FINALISED)
application_1.save()
good = GoodFactory(organisation=application_1.organisation)
FinalAdviceFactory(
user=self.gov_user, team=self.team, case=application_1, good=good, type=AdviceType.APPROVE, created_at=day_2
)
LicenceDecisionFactory(case=application_1, created_at=day_2)

application_2 = StandardApplicationFactory()
application_2.status = get_case_status_by_status(CaseStatusEnum.FINALISED)
application_2.save()
good = GoodFactory(organisation=application_2.organisation)
FinalAdviceFactory(
user=self.gov_user, team=self.team, case=application_2, good=good, type=AdviceType.APPROVE, created_at=day_4
)
LicenceDecisionFactory(case=application_2, created_at=day_4)

application_3 = StandardApplicationFactory()
application_3.status = get_case_status_by_status(CaseStatusEnum.FINALISED)
application_3.save()
LicenceDecisionFactory(case=application_3, created_at=day_5)

qs_1 = Case.objects.search(finalised_from=day_1, finalised_to=day_3)
qs_2 = Case.objects.search(finalised_from=day_3, finalised_to=day_5)
Expand All @@ -310,11 +309,11 @@ def test_filter_by_finalised_date_range(self):
qs_6 = Case.objects.search(finalised_from=day_5)

self.assertEqual(qs_1.count(), 1)
self.assertEqual(qs_2.count(), 1)
self.assertEqual(qs_3.count(), 2)
self.assertEqual(qs_4.count(), 2)
self.assertEqual(qs_2.count(), 2)
self.assertEqual(qs_3.count(), 3)
self.assertEqual(qs_4.count(), 3)
self.assertEqual(qs_5.count(), 0)
self.assertEqual(qs_6.count(), 0)
self.assertEqual(qs_6.count(), 1)
self.assertEqual(qs_1.first().pk, application_1.pk)
self.assertEqual(qs_2.first().pk, application_2.pk)

Expand Down
1 change: 1 addition & 0 deletions api/conf/caseworker_urls.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.urls import path, include

urlpatterns = [
path("queues/", include("api.queues.caseworker.urls")),
path("applications/", include("api.applications.caseworker.urls")),
path("organisations/", include("api.organisations.caseworker.urls")),
path("static/", include("api.staticdata.caseworker.urls")),
Expand Down
17 changes: 17 additions & 0 deletions api/core/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@
from api.organisations.models import Organisation
from api.users.models import GovUser

from lite_routing.routing_rules_internal.enums import QueuesEnum

BULK_APPROVE_ALLOWED_QUEUES = {
"MOD_CAPPROT": QueuesEnum.MOD_CAPPROT,
"MOD_DI_DIRECT": QueuesEnum.MOD_DI_DIRECT,
"MOD_DI_INDIRECT": QueuesEnum.MOD_DI_INDIRECT,
"MOD_DSR": QueuesEnum.MOD_DSR,
"MOD_DSTL": QueuesEnum.MOD_DSTL,
"NCSC": QueuesEnum.NCSC,
}


def assert_user_has_permission(user, permission, organisation: Organisation = None):
if isinstance(user, GovUser):
Expand Down Expand Up @@ -52,3 +63,9 @@ def has_permission(self, request, view):
class CanCaseworkersIssueLicence(permissions.BasePermission):
def has_permission(self, request, view):
return check_user_has_permission(request.user.govuser, GovPermissions.MANAGE_LICENCE_FINAL_ADVICE)


class CanCaseworkerBulkApprove(permissions.BasePermission):
def has_permission(self, request, view):
queue_pk = view.kwargs["pk"]
return str(queue_pk) in BULK_APPROVE_ALLOWED_QUEUES.values()
28 changes: 27 additions & 1 deletion api/core/tests/test_permissions.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from unittest import mock
from parameterized import parameterized

from api.core.permissions import CaseInCaseworkerOperableStatus

from api.core.permissions import BULK_APPROVE_ALLOWED_QUEUES, CanCaseworkerBulkApprove, CaseInCaseworkerOperableStatus
from api.applications.tests.factories import StandardApplicationFactory
from api.queues.caseworker.views.bulk_approval import BulkApprovalCreateView
from api.staticdata.statuses.enums import CaseStatusEnum
from api.staticdata.statuses.models import CaseStatus
from lite_routing.routing_rules_internal.enums import QueuesEnum

from test_helpers.clients import DataTestClient

Expand All @@ -28,3 +31,26 @@ def test_has_permission_caseworker_inoperable(self, status):
mock_view.get_case.return_value = application.get_case()
permission_obj = CaseInCaseworkerOperableStatus()
assert permission_obj.has_permission(None, mock_view) is False


class TestCanCaseworkerBulkApprove(DataTestClient):

@parameterized.expand(
[
(BULK_APPROVE_ALLOWED_QUEUES["MOD_CAPPROT"], True),
(BULK_APPROVE_ALLOWED_QUEUES["MOD_DI_DIRECT"], True),
(BULK_APPROVE_ALLOWED_QUEUES["MOD_DI_INDIRECT"], True),
(BULK_APPROVE_ALLOWED_QUEUES["MOD_DSR"], True),
(BULK_APPROVE_ALLOWED_QUEUES["MOD_DSTL"], True),
(BULK_APPROVE_ALLOWED_QUEUES["NCSC"], True),
(QueuesEnum.FCDO, False),
(QueuesEnum.FCDO_COUNTER_SIGNING, False),
(QueuesEnum.DESNZ_CHEMICAL, False),
(QueuesEnum.DESNZ_NUCLEAR, False),
]
)
def test_has_permission_caseworker_bulk_approve(self, queue_id, expected):
view = BulkApprovalCreateView()
view.kwargs = {"pk": queue_id}
permission_obj = CanCaseworkerBulkApprove()
assert permission_obj.has_permission(None, view) is expected
4 changes: 2 additions & 2 deletions api/licences/tests/test_managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ class LicenceTests(DataTestClient):
def test_manager_filter_non_draft_licences(self):
application = StandardApplicationFactory()
case = application.case_ptr
draft_licence = StandardLicenceFactory(status=LicenceStatus.DRAFT, case=case)
StandardLicenceFactory(status=LicenceStatus.DRAFT, case=case)
issued_licence = StandardLicenceFactory(status=LicenceStatus.ISSUED, case=case)
cancelled_licence = StandardLicenceFactory(status=LicenceStatus.CANCELLED, case=case)
assert list(Licence.objects.filter_non_draft_licences(application=application)) == [
assert list(Licence.objects.filter_non_draft_licences(application=application).order_by("reference_code")) == [
issued_licence,
cancelled_licence,
]
Empty file.
60 changes: 60 additions & 0 deletions api/queues/caseworker/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from rest_framework import serializers

from api.applications.serializers.advice import BulkApprovalAdviceSerializer
from api.cases.enums import AdviceLevel, AdviceType
from api.cases.models import Case
from api.core.serializers import PrimaryKeyRelatedField
from api.teams.models import Team


class BulkApprovalAdviceDataSerializer(serializers.Serializer):
text = serializers.CharField()
proviso = serializers.CharField(allow_blank=True, allow_null=True)
note = serializers.CharField(allow_blank=True, allow_null=True)
footnote_required = serializers.BooleanField(allow_null=True)
footnote = serializers.CharField(allow_blank=True, allow_null=True)
team = PrimaryKeyRelatedField(queryset=Team.objects.filter(is_ogd=True))


class BulkApprovalSerializer(serializers.Serializer):
cases = PrimaryKeyRelatedField(many=True, queryset=Case.objects.all())
advice = BulkApprovalAdviceDataSerializer()

def get_advice_data(self, application, advice_fields):
user = self.context["user"]
subjects = [("good", good_on_application.good.id) for good_on_application in application.goods.all()] + [
(poa.party.type, poa.party.id) for poa in application.parties.all()
]
proviso = advice_fields.get("proviso", "")
advice_type = AdviceType.PROVISO if proviso else AdviceType.APPROVE
return [
{
"level": AdviceLevel.USER,
"type": advice_type,
"case": str(application.id),
"user": user.govuser,
subject_name: str(subject_id),
"denial_reasons": [],
**advice_fields,
}
for subject_name, subject_id in subjects
]

def build_instances_data(self, validated_data):
data = validated_data.copy()
cases = data.get("cases", [])
advice_fields = data.get("advice", {})
instances_data = []
for case in cases:
advice_data = self.get_advice_data(case.baseapplication, advice_fields)
instances_data.extend(advice_data)

return instances_data

def create(self, validated_data):
data = self.build_instances_data(validated_data)
advice_serializer = BulkApprovalAdviceSerializer(data=data, many=True)
advice_serializer.is_valid(raise_exception=True)
instances = advice_serializer.save()

return instances
Empty file.
Loading

0 comments on commit 7690f3d

Please sign in to comment.