diff --git a/api/applications/helpers.py b/api/applications/helpers.py index 8141e9228..510bab674 100644 --- a/api/applications/helpers.py +++ b/api/applications/helpers.py @@ -24,6 +24,7 @@ from api.documents.models import Document from api.documents.libraries import s3_operations from api.external_data.models import SanctionMatch +from api.f680.exporter.serializers import F680Serializer from api.licences.models import GoodOnLicence from lite_content.lite_api import strings @@ -33,6 +34,8 @@ def get_application_view_serializer(application: BaseApplication): if application.case_type.sub_type == CaseTypeSubTypeEnum.STANDARD: return StandardApplicationViewSerializer + elif application.case_type.sub_type == CaseTypeSubTypeEnum.F680: + return F680Serializer else: raise BadRequestError( { diff --git a/api/applications/libraries/get_applications.py b/api/applications/libraries/get_applications.py index 2e7ea726a..5c682fad0 100644 --- a/api/applications/libraries/get_applications.py +++ b/api/applications/libraries/get_applications.py @@ -1,6 +1,4 @@ -from api.applications.models import BaseApplication, StandardApplication -from api.cases.enums import CaseTypeSubTypeEnum -from api.core.exceptions import NotFoundError +from api.cases.models import Case def get_application(pk, organisation_id=None): @@ -8,68 +6,7 @@ def get_application(pk, organisation_id=None): if organisation_id: kwargs["organisation_id"] = str(organisation_id) - application_type = _get_application_type(pk) - if application_type != CaseTypeSubTypeEnum.STANDARD: - raise NotImplementedError(f"get_application does not support this application type: {application_type}") + case = Case.objects.get(pk=pk, **kwargs) + application = case.get_application() - qs = StandardApplication.objects.select_related( - "baseapplication_ptr", - "baseapplication_ptr__end_user", - "baseapplication_ptr__end_user__party", - "case_officer", - "case_officer__team", - "case_type", - "organisation", - "organisation__primary_site", - "status", - "submitted_by", - "submitted_by__baseuser_ptr", - ).prefetch_related( - "goods", - "goods__control_list_entries", - "goods__good", - "goods__good__control_list_entries", - "goods__good__flags", - "goods__good__flags__team", - "goods__good__gooddocument_set", - "goods__good__firearm_details", - "goods__good__pv_grading_details", - "goods__good__goods_on_application", - "goods__good__goods_on_application__application", - "goods__good__goods_on_application__application__queues", - "goods__good__goods_on_application__good", - "goods__good__goods_on_application__good__flags", - "goods__good__goods_on_application__regime_entries", - "goods__good__goods_on_application__regime_entries__subsection", - "goods__good__goods_on_application__regime_entries__subsection__regime", - "goods__regime_entries", - "goods__regime_entries__subsection", - "goods__regime_entries__subsection__regime", - "goods__good__report_summary_prefix", - "goods__good__report_summary_subject", - "goods__goodonapplicationdocument_set", - "goods__goodonapplicationdocument_set__user", - "goods__good_on_application_internal_documents", - "goods__good_on_application_internal_documents__document", - "denial_matches", - "denial_matches__denial_entity", - "application_sites", - "application_sites__site", - "application_sites__site__address", - "application_sites__site__address__country", - "external_application_sites", - "applicationdocument_set", - "goods__report_summary_prefix", - "goods__report_summary_subject", - "goods__firearm_details", - "goods__assessed_by", - ) - obj = qs.get(pk=pk, **kwargs) - return obj - - -def _get_application_type(pk): - try: - return BaseApplication.objects.values_list("case_type__sub_type", flat=True).get(pk=pk) - except BaseApplication.DoesNotExist: - raise NotFoundError({"application_type": "Application type not found - " + str(pk)}) + return application diff --git a/api/applications/models.py b/api/applications/models.py index 4610d6f47..b762760d1 100644 --- a/api/applications/models.py +++ b/api/applications/models.py @@ -205,20 +205,6 @@ class BaseApplication(ApplicationPartyMixin, Case): class Meta: ordering = ["created_at"] - def on_submit(self, old_status): - additional_payload = {} - if self.amendment_of: - # Add an audit entry to the case that was superseded by this amendment - audit_trail_service.create_system_user_audit( - verb=AuditType.EXPORTER_SUBMITTED_AMENDMENT, - target=self.amendment_of, - payload={ - "amendment": {"reference_code": self.reference_code}, - }, - ) - additional_payload["amendment_of"] = {"reference_code": self.amendment_of.reference_code} - create_submitted_audit(self.submitted_by, self, old_status, additional_payload) - def add_to_queue(self, queue): case = self.get_case() @@ -271,9 +257,85 @@ def set_appealed(self, appeal, exporter_user): def create_amendment(self, user): raise NotImplementedError() + def on_submit(self, old_status): + additional_payload = {} + if self.amendment_of: + # Add an audit entry to the case that was superseded by this amendment + audit_trail_service.create_system_user_audit( + verb=AuditType.EXPORTER_SUBMITTED_AMENDMENT, + target=self.amendment_of, + payload={ + "amendment": {"reference_code": self.reference_code}, + }, + ) + additional_payload["amendment_of"] = {"reference_code": self.amendment_of.reference_code} + create_submitted_audit(self.submitted_by, self, old_status, additional_payload) + + +class StandardApplicationQuerySet(models.QuerySet): + def get_prepared_object(self, pk): + return ( + self.select_related( + "baseapplication_ptr", + "baseapplication_ptr__end_user", + "baseapplication_ptr__end_user__party", + "case_officer", + "case_officer__team", + "case_type", + "organisation", + "organisation__primary_site", + "status", + "submitted_by", + "submitted_by__baseuser_ptr", + ) + .prefetch_related( + "goods", + "goods__control_list_entries", + "goods__good", + "goods__good__control_list_entries", + "goods__good__flags", + "goods__good__flags__team", + "goods__good__gooddocument_set", + "goods__good__firearm_details", + "goods__good__pv_grading_details", + "goods__good__goods_on_application", + "goods__good__goods_on_application__application", + "goods__good__goods_on_application__application__queues", + "goods__good__goods_on_application__good", + "goods__good__goods_on_application__good__flags", + "goods__good__goods_on_application__regime_entries", + "goods__good__goods_on_application__regime_entries__subsection", + "goods__good__goods_on_application__regime_entries__subsection__regime", + "goods__regime_entries", + "goods__regime_entries__subsection", + "goods__regime_entries__subsection__regime", + "goods__good__report_summary_prefix", + "goods__good__report_summary_subject", + "goods__goodonapplicationdocument_set", + "goods__goodonapplicationdocument_set__user", + "goods__good_on_application_internal_documents", + "goods__good_on_application_internal_documents__document", + "denial_matches", + "denial_matches__denial_entity", + "application_sites", + "application_sites__site", + "application_sites__site__address", + "application_sites__site__address__country", + "external_application_sites", + "applicationdocument_set", + "goods__report_summary_prefix", + "goods__report_summary_subject", + "goods__firearm_details", + "goods__assessed_by", + ) + .get(pk=pk) + ) + # Licence Applications class StandardApplication(BaseApplication, Clonable): + objects = StandardApplicationQuerySet.as_manager() + GB = "GB" NI = "NI" GOODS_STARTING_POINT_CHOICES = [ @@ -422,6 +484,11 @@ def create_amendment(self, user): self.case_ptr.change_status(system_user, get_case_status_by_status(CaseStatusEnum.SUPERSEDED_BY_EXPORTER_EDIT)) return amendment_application + def validate_application_ready_for_submission(self): + from api.applications.creators import validate_application_ready_for_submission + + return validate_application_ready_for_submission(self) + class ApplicationDocument(Document, Clonable): application = models.ForeignKey(BaseApplication, on_delete=models.CASCADE) diff --git a/api/applications/views/applications.py b/api/applications/views/applications.py index bff82acea..5ccbf2088 100644 --- a/api/applications/views/applications.py +++ b/api/applications/views/applications.py @@ -22,7 +22,7 @@ from api.appeals.models import Appeal from api.appeals.serializers import AppealSerializer from api.applications import constants -from api.applications.creators import validate_application_ready_for_submission, _validate_agree_to_declaration +from api.applications.creators import _validate_agree_to_declaration from api.applications.helpers import ( get_application_create_serializer, get_application_view_serializer, @@ -61,6 +61,7 @@ from api.cases.generated_documents.models import GeneratedCaseDocument from api.cases.generated_documents.helpers import auto_generate_case_document from api.cases.libraries.get_flags import get_flags +from api.cases.models import Case from api.cases.notify import notify_exporter_appeal_acknowledgement from api.cases.serializers import ApplicationManageSubStatusSerializer from api.cases.celery_tasks import get_application_target_sla @@ -290,8 +291,7 @@ def put(self, request, pk): request.user.exporteruser, ExporterPermissions.SUBMIT_LICENCE_APPLICATION, application.organisation ) - errors = validate_application_ready_for_submission(application) - + errors = application.validate_application_ready_for_submission() if errors: return JsonResponse(data={"errors": errors}, status=status.HTTP_400_BAD_REQUEST) @@ -345,10 +345,11 @@ def put(self, request, pk): run_routing_rules(application) # Set the sites on this application as used so their name/site records located at are no longer editable - sites_on_application = SiteOnApplication.objects.filter(application=application) - Site.objects.filter(id__in=sites_on_application.values_list("site_id", flat=True)).update( - is_used_on_application=True - ) + if application.case_type.sub_type == CaseTypeSubTypeEnum.STANDARD: + sites_on_application = SiteOnApplication.objects.filter(application=application) + Site.objects.filter(id__in=sites_on_application.values_list("site_id", flat=True)).update( + is_used_on_application=True + ) if application.case_type.sub_type in [ CaseTypeSubTypeEnum.STANDARD, @@ -405,10 +406,10 @@ class ApplicationSubStatuses(ListAPIView): def setup(self, request, *args, **kwargs): super().setup(request, *args, **kwargs) - self.application = get_object_or_404(StandardApplication, pk=self.kwargs["pk"]) + self.case = get_object_or_404(Case, pk=self.kwargs["pk"]) def get_queryset(self): - return self.application.status.sub_statuses.all().order_by("order") + return self.case.status.sub_statuses.all().order_by("order") class ApplicationFinaliseView(APIView): diff --git a/api/cases/models.py b/api/cases/models.py index 936ecfb80..bda521bfb 100644 --- a/api/cases/models.py +++ b/api/cases/models.py @@ -486,6 +486,18 @@ def delete(self, *args, **kwargs): ) return super().delete(*args, **kwargs) + def get_application(self): + from api.applications.models import StandardApplication + from api.f680.models import F680Application + + application_mapping = { + CaseTypeSubTypeEnum.STANDARD: StandardApplication, + CaseTypeSubTypeEnum.F680: F680Application, + } + model_class = application_mapping[self.case_type.sub_type] + + return model_class.objects.get_prepared_object(pk=self.pk) + class CaseQueue(TimestampableModel): case = models.ForeignKey(Case, related_name="casequeues", on_delete=models.DO_NOTHING) diff --git a/api/conf/exporter_urls.py b/api/conf/exporter_urls.py index c95eee8a7..349bd6178 100644 --- a/api/conf/exporter_urls.py +++ b/api/conf/exporter_urls.py @@ -3,4 +3,5 @@ urlpatterns = [ path("applications/", include("api.applications.exporter.urls")), path("static/", include("api.staticdata.exporter.urls")), + path("f680/", include("api.f680.exporter.urls")), ] diff --git a/api/conf/settings.py b/api/conf/settings.py index 68eaf8be5..a590e0e1e 100644 --- a/api/conf/settings.py +++ b/api/conf/settings.py @@ -135,6 +135,7 @@ "api.assessments", "api.document_data", "api.survey", + "api.f680", "django_db_anonymiser.db_anonymiser", "reversion", "drf_spectacular", diff --git a/api/core/decorators.py b/api/core/decorators.py index 83754f7c3..d5a727504 100644 --- a/api/core/decorators.py +++ b/api/core/decorators.py @@ -3,10 +3,11 @@ from functools import wraps from uuid import UUID -from django.http import JsonResponse, Http404 +from django.http import JsonResponse +from django.shortcuts import get_object_or_404 from rest_framework import status -from api.applications.models import BaseApplication +from api.cases.models import Case from api.licences.enums import LicenceStatus from api.licences.models import Licence from lite_content.lite_api import strings @@ -31,11 +32,8 @@ def _get_application_id(request: APIView, kwargs): def _get_application(request: APIView, kwargs): pk = _get_application_id(request, kwargs) - result = BaseApplication.objects.filter(pk=pk) - if not result.exists(): - raise Http404 - else: - return result + case = get_object_or_404(Case, pk=pk) + return case.get_application() def allowed_application_types(application_types: List[str]) -> Callable: @@ -46,7 +44,8 @@ def allowed_application_types(application_types: List[str]) -> Callable: def decorator(func): @wraps(func) def inner(request, *args, **kwargs): - sub_type = _get_application(request, kwargs).values_list("case_type__sub_type", flat=True)[0] + application = _get_application(request, kwargs) + sub_type = application.case_type.sub_type if sub_type not in application_types: return JsonResponse( @@ -71,8 +70,8 @@ def application_in_status(status_check_func): def decorator(view_func): @wraps(view_func) def inner(request, *args, **kwargs): - application_status = _get_application(request, kwargs).values_list("status__status", flat=True)[0] - has_status, error = status_check_func(application_status) + application = _get_application(request, kwargs) + has_status, error = status_check_func(application.status.status) if has_status: return view_func(request, *args, **kwargs) return JsonResponse( @@ -148,11 +147,9 @@ def inner(request, *args, **kwargs): if user.type == UserType.EXPORTER: organisation_id = get_request_user_organisation_id(request.request) - required_application_details = _get_application(request, kwargs).values( - "case_type__sub_type", "organisation_id" - )[0] + application = _get_application(request, kwargs) - has_access = required_application_details["organisation_id"] == organisation_id + has_access = application.organisation_id == organisation_id if not has_access: return JsonResponse( data={ diff --git a/api/f680/__init__.py b/api/f680/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/api/f680/exporter/__init__.py b/api/f680/exporter/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/api/f680/exporter/serializers.py b/api/f680/exporter/serializers.py new file mode 100644 index 000000000..63f5c1dce --- /dev/null +++ b/api/f680/exporter/serializers.py @@ -0,0 +1,40 @@ +from rest_framework import serializers + +from api.applications.serializers.fields import CaseStatusField +from api.organisations.serializers import OrganisationDetailSerializer + +from api.f680.models import F680Application # /PS-IGNORE + + +class F680Serializer(serializers.ModelSerializer): # /PS-IGNORE + status = CaseStatusField(read_only=True) + organisation = OrganisationDetailSerializer(read_only=True) + name = serializers.SerializerMethodField() + submitted_at = serializers.DateTimeField(read_only=True) + submitted_by = serializers.SerializerMethodField() + + class Meta: + model = F680Application # /PS-IGNORE + fields = [ + "id", + "application", + "status", + "sub_status", + "reference_code", + "organisation", + "name", + "submitted_at", + "submitted_by", + ] + + def create(self, validated_data): + validated_data["organisation"] = self.context["organisation"] + return super().create(validated_data) + + def get_name(self, application): + return application.application["application"]["name"]["value"] + + def get_submitted_by(self, application): + if not application.submitted_by: + return "" + return f"{application.submitted_by.first_name} {application.submitted_by.last_name}" diff --git a/api/f680/exporter/urls.py b/api/f680/exporter/urls.py new file mode 100644 index 000000000..efdf584e1 --- /dev/null +++ b/api/f680/exporter/urls.py @@ -0,0 +1,14 @@ +from django.urls import path + +from api.f680.exporter.views import ( + F680ApplicationView, + F680ApplicationsView, # /PS-IGNORE +) + + +app_name = "exporter_f680" # /PS-IGNORE + +urlpatterns = [ + path("", F680ApplicationsView.as_view(), name="applications"), # /PS-IGNORE + path("/", F680ApplicationView.as_view(), name="application"), +] diff --git a/api/f680/exporter/views.py b/api/f680/exporter/views.py new file mode 100644 index 000000000..21b1d4670 --- /dev/null +++ b/api/f680/exporter/views.py @@ -0,0 +1,26 @@ +from rest_framework.generics import ( + CreateAPIView, + RetrieveAPIView, +) + +from api.organisations.libraries.get_organisation import get_request_user_organisation + +from api.f680.models import F680Application # /PS-IGNORE +from api.f680.exporter.serializers import F680Serializer # /PS-IGNORE + + +class F680ApplicationsView(CreateAPIView): # /PS-IGNORE + queryset = F680Application.objects.all() # /PS-IGNORE + serializer_class = F680Serializer # /PS-IGNORE + + def get_serializer_context(self): + serializer_context = super().get_serializer_context() + + serializer_context["organisation"] = get_request_user_organisation(self.request) + + return serializer_context + + +class F680ApplicationView(RetrieveAPIView): + queryset = F680Application.objects.all() # /PS-IGNORE + serializer_class = F680Serializer # /PS-IGNORE diff --git a/api/f680/migrations/0001_initial.py b/api/f680/migrations/0001_initial.py new file mode 100644 index 000000000..594801bf1 --- /dev/null +++ b/api/f680/migrations/0001_initial.py @@ -0,0 +1,37 @@ +# Generated by Django 4.2.17 on 2025-01-21 20:05 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("cases", "0075_update_licence_decision_denial_reasons"), + ] + + operations = [ + migrations.CreateModel( + name="F680Application", # /PS-IGNORE + fields=[ + ( + "case_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="cases.case", + ), + ), + ("application", models.JSONField()), + ], + options={ + "abstract": False, + }, + bases=("cases.case",), + ), + ] diff --git a/api/f680/migrations/__init__.py b/api/f680/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/api/f680/models.py b/api/f680/models.py new file mode 100644 index 000000000..34ddb5518 --- /dev/null +++ b/api/f680/models.py @@ -0,0 +1,43 @@ +from django.db import models + +from api.cases.enums import ( + CaseTypeSubTypeEnum, + CaseTypeTypeEnum, +) +from api.cases.models import ( + Case, + CaseType, +) +from api.staticdata.statuses.enums import CaseStatusEnum +from api.staticdata.statuses.libraries.get_case_status import get_case_status_by_status + + +class F680ApplicationQuerySet(models.QuerySet): + def get_prepared_object(self, pk): + return self.get(pk=pk) + + +class F680Application(Case): # /PS-IGNORE + application = models.JSONField() + + objects = F680ApplicationQuerySet.as_manager() + + def save(self, *args, **kwargs): + try: + _ = self.case_type + except self.__class__.case_type.RelatedObjectDoesNotExist: + self.case_type = CaseType.objects.get( + sub_type=CaseTypeSubTypeEnum.F680, + type=CaseTypeTypeEnum.APPLICATION, + ) + + if not self.status: + self.status = get_case_status_by_status(CaseStatusEnum.DRAFT) + + return super().save(*args, **kwargs) + + def validate_application_ready_for_submission(self): + return {} + + def on_submit(self, old_status): + pass diff --git a/lite_routing b/lite_routing index bac4557d6..257d6272b 160000 --- a/lite_routing +++ b/lite_routing @@ -1 +1 @@ -Subproject commit bac4557d614d503e8a09c1ae132e64a9fe72f524 +Subproject commit 257d6272beec8a0366b483f034adf45fb96f8585