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

UAT release #2380

Merged
merged 39 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
6838673
Add endpoint that creates approval recommendations in bulk
saruniitr Dec 23, 2024
2b2c20b
Add serializer for creating advice records during bulk approval
saruniitr Dec 24, 2024
8b3b53d
Create advice records for multiple cases
saruniitr Dec 24, 2024
ddbf563
Move the cases forward as per the process after bulk approval
saruniitr Dec 24, 2024
ca00c96
Include number of cases approved in response
saruniitr Dec 27, 2024
31694c5
Add permission classes for Bulk approval view
saruniitr Jan 6, 2025
3dd0125
Add new audit event when multiple cases are approved in bulk
saruniitr Jan 6, 2025
9cff9da
Refactor Case activity view to use filter backends
saruniitr Jan 7, 2025
7e9b4ec
Combine bulk approval audit events to the case audit events
saruniitr Jan 7, 2025
70913ef
Add unit tests for Bulk approval
saruniitr Jan 7, 2025
257fa11
Include number of cases approved at once in audit event
saruniitr Jan 7, 2025
bdbcc24
Merge branch 'dev' into LTD-5763-bulk-approval
saruniitr Jan 7, 2025
7106a9b
Fix duplicate role error in tests
saruniitr Jan 7, 2025
f388a2e
Update bulk approve allowed filter to check for allowed queues
saruniitr Jan 7, 2025
910d466
Add bulk approval audit event for each case
saruniitr Jan 8, 2025
ab26b40
Re-home bulk approval into a caseworker specific endpoint
saruniitr Jan 8, 2025
0823d0f
Validate input data using serializer before creating Advice records
saruniitr Jan 9, 2025
9246b36
Refactor moving case forward as a method in the model
saruniitr Jan 9, 2025
590b0d1
Revert "Combine bulk approval audit events to the case audit events"
saruniitr Jan 9, 2025
8b45c61
Revert "Refactor Case activity view to use filter backends"
saruniitr Jan 9, 2025
bab1530
Remove temporary debug code
saruniitr Jan 9, 2025
6aefc88
Add missing conftest file
saruniitr Jan 9, 2025
f6a17d4
Add unit test for the move case forward function
saruniitr Jan 10, 2025
0f58087
Allow bulk approve on NCSC queue as well
saruniitr Jan 10, 2025
c27eb86
Update bulk approval audit event content
saruniitr Jan 12, 2025
7cd2215
Add more bulk approval unit tests
saruniitr Jan 13, 2025
3036b2e
Fix role already exists error
saruniitr Jan 13, 2025
8e3d3eb
Merge branch 'dev' into LTD-5763-bulk-approval
saruniitr Jan 13, 2025
f5db0c5
Remove unused fixtures
saruniitr Jan 13, 2025
6e23135
Fix order of results in the assert
saruniitr Jan 13, 2025
47624d4
Fix test by sorting the results based on reference code
saruniitr Jan 13, 2025
d1be859
Fix coverage issue
saruniitr Jan 13, 2025
4c5b1f8
Merge branch 'dev' into LTD-5763-bulk-approval
saruniitr Jan 14, 2025
eeb6f74
Merge pull request #2362 from uktrade/LTD-5763-bulk-approval
saruniitr Jan 14, 2025
cb98464
fix search
depsiatwal Jan 10, 2025
a21fb99
Merge branch 'dev' into LTD-5582-fix-case-search
depsiatwal Jan 16, 2025
1cf1064
Merge pull request #2367 from uktrade/LTD-5582-fix-case-search
depsiatwal Jan 16, 2025
57d19cd
Allow good creation to be skipped when creating a test application
kevincarrogan Jan 16, 2025
f9ac492
Merge pull request #2377 from uktrade/allow-good-creation-to-be-skipp…
kevincarrogan Jan 17, 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
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
Loading