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

Production release #2397

Merged
merged 14 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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