Skip to content

Commit

Permalink
Merge pull request #2397 from uktrade/uat
Browse files Browse the repository at this point in the history
Production release
  • Loading branch information
kevincarrogan authored Jan 30, 2025
2 parents 9f43d7d + 2e66528 commit d5a0472
Show file tree
Hide file tree
Showing 16 changed files with 490 additions and 217 deletions.
50 changes: 49 additions & 1 deletion api/applications/exporter/serializers.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,57 @@
from api.cases.models import Case
from api.staticdata.statuses.libraries.get_case_status import get_status_value_from_case_status_enum
from api.staticdata.statuses.models import CaseStatus
from rest_framework import serializers

from api.staticdata.statuses.enums import CaseStatusEnum


class ApplicationChangeStatusSerializer(serializers.Serializer):

status = serializers.ChoiceField(choices=CaseStatusEnum.all())
note = serializers.CharField(required=False, allow_blank=True, allow_null=True, max_length=2000)


class ApplicationStatusSerializer(serializers.ModelSerializer):
status_display = serializers.SerializerMethodField()

class Meta:
model = CaseStatus
fields = ("status", "status_display")

def get_status_display(self, obj):
return get_status_value_from_case_status_enum(obj.status)


class CaseAmendmentSerializer(serializers.Serializer):
status = serializers.SerializerMethodField()
ecju_query_count = serializers.SerializerMethodField()
reference_code = serializers.CharField()
submitted_at = serializers.DateTimeField()
id = serializers.UUIDField()
status = ApplicationStatusSerializer()

def get_ecju_query_count(self, instance):
return instance.case_ecju_query.all().count()


class ApplicationHistorySerializer(serializers.ModelSerializer):
amendment_history = serializers.SerializerMethodField()

def get_amendment_history(self, instance):
amendments = []
case_amended = instance

# Go backwards through amendment chain until we find the original case
while case_amended.superseded_by:
case_amended = case_amended.superseded_by

# Travel forwards in amendment chain to find the latest amended case
while case_amended:
case_amended_data = CaseAmendmentSerializer(case_amended).data
amendments.append(case_amended_data)
case_amended = case_amended.amendment_of
return amendments

class Meta:
model = Case
fields = ("id", "reference_code", "amendment_history")
1 change: 1 addition & 0 deletions api/applications/exporter/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@

urlpatterns = [
path("<uuid:pk>/status/", applications.ApplicationChangeStatus.as_view(), name="change_status"),
path("<uuid:pk>/history/", applications.ApplicationHistory.as_view(), name="history"),
]
36 changes: 13 additions & 23 deletions api/applications/exporter/views/applications.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,24 @@
from django.core.exceptions import ObjectDoesNotExist
from django.http import Http404, JsonResponse
from django.http import JsonResponse
from django.db import transaction
from rest_framework.generics import GenericAPIView
from api.applications.exporter.views.mixins import ExporterApplicationMixin
from api.cases.models import Case
from rest_framework.generics import GenericAPIView, RetrieveAPIView
from rest_framework import status

from api.applications.exporter.permissions import CaseStatusExporterChangeable
from api.applications.exporter.serializers import ApplicationChangeStatusSerializer
from api.applications.exporter.serializers import ApplicationChangeStatusSerializer, ApplicationHistorySerializer
from api.applications.helpers import get_application_view_serializer
from api.applications.libraries.get_applications import get_application
from api.core.authentication import ExporterAuthentication
from api.core.exceptions import NotFoundError
from api.core.permissions import IsExporterInOrganisation
from api.staticdata.statuses.libraries.get_case_status import get_case_status_by_status


class ApplicationChangeStatus(GenericAPIView):
authentication_classes = (ExporterAuthentication,)
class ApplicationChangeStatus(ExporterApplicationMixin, GenericAPIView):
permission_classes = [
IsExporterInOrganisation,
CaseStatusExporterChangeable,
]
serializer_class = ApplicationChangeStatusSerializer

def setup(self, request, *args, **kwargs):
super().setup(request, *args, **kwargs)
try:
self.application = get_application(self.kwargs["pk"])
except (ObjectDoesNotExist, NotFoundError):
raise Http404()

def get_object(self):
self.check_object_permissions(self.request, self.application)
return self.application

def get_organisation(self):
return self.application.organisation

@transaction.atomic
def post(self, request, pk):
application = self.get_object()
Expand All @@ -49,3 +32,10 @@ def post(self, request, pk):
).data

return JsonResponse(data=response_data, status=status.HTTP_200_OK)


class ApplicationHistory(ExporterApplicationMixin, RetrieveAPIView):

lookup_field = "pk"
queryset = Case.objects.all()
serializer_class = ApplicationHistorySerializer
31 changes: 31 additions & 0 deletions api/applications/exporter/views/mixins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from django.core.exceptions import ObjectDoesNotExist
from django.http import Http404
from api.core.authentication import ExporterAuthentication
from api.core.exceptions import NotFoundError

from api.applications.libraries.get_applications import get_application
from api.core.permissions import IsExporterInOrganisation


class ExporterApplicationMixin:
# Mixin for views which checks the exporter is within same organisation as the application
# Checks Exporter is authenticated

authentication_classes = (ExporterAuthentication,)
permission_classes = [
IsExporterInOrganisation,
]

def setup(self, request, *args, **kwargs):
super().setup(request, *args, **kwargs)
try:
self.application = get_application(self.kwargs["pk"])
except (ObjectDoesNotExist, NotFoundError):
raise Http404()

def get_object(self):
self.check_object_permissions(self.request, self.application)
return self.application

def get_organisation(self):
return self.application.organisation
96 changes: 96 additions & 0 deletions api/applications/exporter/views/tests/test_applications.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import uuid
from django.utils import timezone
from pytz import timezone as tz

from api.cases.tests.factories import EcjuQueryFactory
from parameterized import parameterized

from django.urls import reverse
Expand All @@ -6,6 +11,7 @@
from api.applications.tests.factories import StandardApplicationFactory
from api.staticdata.statuses.enums import CaseStatusEnum
from api.staticdata.statuses.models import CaseStatus
from api.cases.models import Case, Queue
from api.organisations.tests.factories import OrganisationFactory

from test_helpers.clients import DataTestClient
Expand Down Expand Up @@ -77,3 +83,93 @@ def test_change_status_application_wrong_organisation(self):
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.application.refresh_from_db()
self.assertEqual(self.application.status.status, original_status)


class TestApplicationHistory(DataTestClient):

def setUp(self):
super().setUp()

self.amendment_1 = StandardApplicationFactory(
organisation=self.exporter_user.organisation,
status=CaseStatus.objects.get(status=CaseStatusEnum.SUBMITTED),
submitted_at=timezone.now(),
)
self.amendment_1.queues.add(Queue.objects.first())
self.amendment_1.save()
EcjuQueryFactory(
question="ECJU Query 1", case=self.amendment_1, raised_by_user=self.gov_user, responded_at=timezone.now()
)
EcjuQueryFactory(question="ECJU Query 2", case=self.amendment_1, raised_by_user=self.gov_user, response=None)

self.amendment_2 = self.amendment_1.create_amendment(self.exporter_user)
self.amendment_1.refresh_from_db()
self.amendment_2.submitted_at = timezone.now()
self.amendment_2.status = CaseStatus.objects.get(status=CaseStatusEnum.SUBMITTED)
self.amendment_2.reference_code = "GBSIEL/2025/0000002/P"
self.amendment_2.save()
EcjuQueryFactory(case=self.amendment_2, raised_by_user=self.gov_user)

self.latest_case = self.amendment_2.create_amendment(self.exporter_user)
self.amendment_2.refresh_from_db()
self.latest_case.submitted_at = timezone.now()
self.latest_case.reference_code = "GBSIEL/2025/0000003/P"
self.latest_case.status = CaseStatus.objects.get(status=CaseStatusEnum.SUBMITTED)
self.latest_case.save()
self.latest_case.refresh_from_db()

@parameterized.expand(["GBSIEL/2025/0000001/P", "GBSIEL/2025/0000002/P", "GBSIEL/2025/0000003/P"])
def test_get_amendment_history(self, case_ref):

case = Case.objects.get(reference_code=case_ref)
url = reverse(
"exporter_applications:history",
kwargs={
"pk": str(case.pk),
},
)

response = self.client.get(url, **self.exporter_headers)

self.assertEqual(response.status_code, status.HTTP_200_OK)

expected_json = {
"id": str(case.id),
"reference_code": case.reference_code,
"amendment_history": [
{
"id": str(c.id),
"reference_code": c.reference_code,
"submitted_at": c.submitted_at.astimezone(tz("UTC")).strftime("%Y-%m-%dT%H:%M:%S.%f") + "Z",
"status": {"status": c.status.status, "status_display": CaseStatusEnum.get_text(c.status.status)},
"ecju_query_count": c.case_ecju_query.all().count(),
}
for c in [self.latest_case, self.amendment_2, self.amendment_1]
],
}
self.assertEqual(response.json(), expected_json)

def test_get_history_application_not_found(self):

url = reverse(
"exporter_applications:history",
kwargs={
"pk": str(uuid.uuid4()),
},
)
response = self.client.get(url, **self.exporter_headers)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

def test_get_history_application_wrong_organisation(self):
self.latest_case.organisation = OrganisationFactory()
self.latest_case.save()

url = reverse(
"exporter_applications:history",
kwargs={
"pk": str(self.latest_case.pk),
},
)
response = self.client.get(url, **self.exporter_headers)

self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
53 changes: 35 additions & 18 deletions api/applications/serializers/good.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
from api.goods.serializers import (
GoodSerializerInternal,
FirearmDetailsSerializer,
GoodSerializerInternalIncludingPrecedents,
)
from api.gov_users.serializers import GovUserSimpleSerializer
from api.licences.models import GoodOnLicence
Expand Down Expand Up @@ -172,28 +171,46 @@ def update(self, instance, validated_data):
return super().update(instance, validated_data)


class GoodOnApplicationDataWorkspaceSerializer(GoodOnApplicationViewSerializer):
good = GoodSerializerInternalIncludingPrecedents(read_only=True)
good_application_documents = serializers.SerializerMethodField()
good_application_internal_documents = serializers.SerializerMethodField()
class GoodOnApplicationDataWorkspaceSerializer(serializers.ModelSerializer):
unit = serializers.SerializerMethodField()
good = serializers.SerializerMethodField()
firearm_details = serializers.SerializerMethodField()
is_good_controlled = serializers.SerializerMethodField()

class Meta:
model = GoodOnApplication
base_fields = list(GoodOnApplicationViewSerializer.Meta.fields)
fields = base_fields + [
"good_application_documents",
"good_application_internal_documents",
]

def get_good_application_documents(self, instance):
documents = GoodOnApplicationDocument.objects.filter(
application=instance.application, good=instance.good, safe=True
fields = (
"id",
"created_at",
"updated_at",
"quantity",
"unit",
"value",
"is_good_incorporated",
"application_id",
"comment",
"report_summary",
"end_use_control",
"is_precedent",
"good",
"firearm_details",
"is_good_controlled",
"is_trigger_list_guidelines_applicable",
"is_nca_applicable",
"nsg_assessment_note",
)
return GoodOnApplicationDocumentViewSerializer(documents, many=True).data

def get_good_application_internal_documents(self, instance):
documents = GoodOnApplicationInternalDocument.objects.filter(good_on_application=instance.id, safe=True)
return GoodOnApplicationInternalDocumentViewSerializer(documents, many=True).data
def get_unit(self, instance):
return {"key": instance.unit}

def get_good(self, instance):
return {"id": instance.good_id}

def get_firearm_details(self, instance):
return {"id": instance.firearm_details_id}

def get_is_good_controlled(self, instance):
return {"key": instance.is_good_controlled}


class GoodOnApplicationCreateSerializer(serializers.ModelSerializer):
Expand Down
Loading

0 comments on commit d5a0472

Please sign in to comment.