From 09e4651f82cd86ffa4b25d013e2715e6ed1db137 Mon Sep 17 00:00:00 2001 From: Kevin Carrogan Date: Mon, 25 Nov 2024 15:16:55 +0000 Subject: [PATCH 01/22] Add endpoint for goods descriptions --- api/assessments/views.py | 1 - api/data_workspace/v2/serializers.py | 13 +++ api/data_workspace/v2/tests/bdd/conftest.py | 84 ++++++++++++++++++- .../bdd/scenarios/goods_descriptions.feature | 33 ++++++++ .../v2/tests/bdd/test_goods_descriptions.py | 80 ++++++++++++++++++ api/data_workspace/v2/urls.py | 1 + api/data_workspace/v2/views.py | 15 ++++ 7 files changed, 223 insertions(+), 4 deletions(-) create mode 100644 api/data_workspace/v2/tests/bdd/scenarios/goods_descriptions.feature create mode 100644 api/data_workspace/v2/tests/bdd/test_goods_descriptions.py diff --git a/api/assessments/views.py b/api/assessments/views.py index 453e51b3e8..4962adcb45 100644 --- a/api/assessments/views.py +++ b/api/assessments/views.py @@ -83,7 +83,6 @@ def update(self, request, *args, **kwargs): def validate_ids(data): - ids = [record["id"] for record in data] duplicate_ids = [goa_id for goa_id, count in Counter(ids).items() if count > 1] diff --git a/api/data_workspace/v2/serializers.py b/api/data_workspace/v2/serializers.py index 1e2643002a..99122c6482 100644 --- a/api/data_workspace/v2/serializers.py +++ b/api/data_workspace/v2/serializers.py @@ -11,6 +11,7 @@ from api.cases.enums import LicenceDecisionType from api.cases.models import Case from api.staticdata.countries.models import Country +from api.staticdata.report_summaries.models import ReportSummary class LicenceDecisionSerializer(serializers.ModelSerializer): @@ -82,6 +83,18 @@ class Meta: ) +class GoodDescriptionSerializer(serializers.ModelSerializer): + description = serializers.CharField(source="name") + good_id = serializers.UUIDField() + + class Meta: + model = ReportSummary + fields = ( + "description", + "good_id", + ) + + class ApplicationSerializer(serializers.ModelSerializer): licence_type = serializers.CharField(source="case_type.reference") status = serializers.CharField(source="status.status") diff --git a/api/data_workspace/v2/tests/bdd/conftest.py b/api/data_workspace/v2/tests/bdd/conftest.py index 02b5dcbaba..4cda8b3ede 100644 --- a/api/data_workspace/v2/tests/bdd/conftest.py +++ b/api/data_workspace/v2/tests/bdd/conftest.py @@ -3,21 +3,27 @@ import pytest import pytz +from moto import mock_aws + from rest_framework import status from rest_framework.test import APIClient from pytest_bdd import ( + given, parsers, then, + when, ) +from django.conf import settings from django.urls import reverse +from api.applications.enums import ApplicationExportType from api.applications.tests.factories import ( + DraftStandardApplicationFactory, GoodOnApplicationFactory, PartyOnApplicationFactory, StandardApplicationFactory, - DraftStandardApplicationFactory, ) from api.cases.enums import CaseTypeEnum from api.cases.models import CaseType @@ -27,15 +33,23 @@ Roles, ) from api.goods.tests.factories import GoodFactory +from api.documents.libraries.s3_operations import init_s3_client from api.letter_templates.models import LetterTemplate +from api.parties.tests.factories import PartyDocumentFactory from api.organisations.tests.factories import OrganisationFactory from api.staticdata.letter_layouts.models import LetterLayout from api.staticdata.statuses.enums import CaseStatusEnum from api.staticdata.statuses.models import CaseStatus from api.staticdata.units.enums import Units from api.users.libraries.user_to_token import user_to_token -from api.users.enums import SystemUser, UserType -from api.users.models import BaseUser, Permission +from api.users.enums import ( + SystemUser, + UserType, +) +from api.users.models import ( + BaseUser, + Permission, +) from api.users.tests.factories import ( BaseUserFactory, ExporterUserFactory, @@ -50,6 +64,19 @@ def load_json(filename): return json.load(f) +@pytest.fixture(autouse=True) +def mock_s3(): + with mock_aws(): + s3 = init_s3_client() + s3.create_bucket( + Bucket=settings.AWS_STORAGE_BUCKET_NAME, + CreateBucketConfiguration={ + "LocationConstraint": settings.AWS_REGION, + }, + ) + yield + + @pytest.fixture() def seed_layouts(): layouts = load_json("api/data_workspace/v2/tests/bdd/initial_data/letter_layouts.json") @@ -217,6 +244,57 @@ def draft_application(): return draft_application +@pytest.fixture +def submit_application(api_client, exporter_headers, mocker): + def _submit_application(draft_application): + type_code = "T" if draft_application.export_type == ApplicationExportType.TEMPORARY else "P" + reference_code = f"GBSIEL/2024/0000001/{type_code}" + mocker.patch("api.cases.models.generate_reference_code", return_value=reference_code) + + response = api_client.put( + reverse( + "applications:application_submit", + kwargs={ + "pk": draft_application.pk, + }, + ), + data={ + "submit_declaration": True, + "agreed_to_declaration_text": "i agree", + }, + **exporter_headers, + ) + assert response.status_code == 200, response.json()["errors"] + + draft_application.refresh_from_db() + return draft_application + + return _submit_application + + +@given("a draft standard application", target_fixture="draft_standard_application") +def given_draft_standard_application(organisation): + application = DraftStandardApplicationFactory( + organisation=organisation, + ) + + PartyDocumentFactory( + party=application.end_user.party, + s3_key="party-document", + safe=True, + ) + + return application + + +@when( + "the application is submitted", + target_fixture="submitted_standard_application", +) +def when_the_application_is_submitted(submit_application, draft_standard_application): + return submit_application(draft_standard_application) + + @then(parsers.parse("the `{table_name}` table is empty")) def empty_table(client, unpage_data, table_name): metadata_url = reverse("data_workspace:v2:table-metadata") diff --git a/api/data_workspace/v2/tests/bdd/scenarios/goods_descriptions.feature b/api/data_workspace/v2/tests/bdd/scenarios/goods_descriptions.feature new file mode 100644 index 0000000000..5e2197f00d --- /dev/null +++ b/api/data_workspace/v2/tests/bdd/scenarios/goods_descriptions.feature @@ -0,0 +1,33 @@ +@db +Feature: goods_descriptions Table + +Scenario: Draft application + Given a draft standard application + Then the `goods_descriptions` table is empty + +Scenario: Submitted application + Given a draft standard application + And the application has the following goods: + | id |name | + | 8fa8dc3c-c103-42f5-ba94-2d9098b8821d | A controlled good | + | 4dad5dc6-38ef-4bf7-99fd-0c6bc5d86048 | An NLR good | + When the application is submitted + Then the `goods_descriptions` table is empty + +Scenario: Assess application + Given a draft standard application + And the application has the following goods: + | id | name | + | 8fa8dc3c-c103-42f5-ba94-2d9098b8821d | A controlled good | + | 118a003c-7191-4a2c-97e9-be243722cbb2 | Another controlled good | + | 4dad5dc6-38ef-4bf7-99fd-0c6bc5d86048 | An NLR good | + When the application is submitted + And the goods are assessed by TAU as: + | id | Control list entry | Report summary prefix | Report summary subject | + | 8fa8dc3c-c103-42f5-ba94-2d9098b8821d | ML1 | accessories for | composite laminates | + | 118a003c-7191-4a2c-97e9-be243722cbb2 | ML1 | | composite laminates | + | 4dad5dc6-38ef-4bf7-99fd-0c6bc5d86048 | NLR | | | + Then the `goods_descriptions` table has the following rows: + | good_id | description | + | 8fa8dc3c-c103-42f5-ba94-2d9098b8821d | accessories for composite laminates | + | 118a003c-7191-4a2c-97e9-be243722cbb2 | composite laminates | diff --git a/api/data_workspace/v2/tests/bdd/test_goods_descriptions.py b/api/data_workspace/v2/tests/bdd/test_goods_descriptions.py new file mode 100644 index 0000000000..88083a1278 --- /dev/null +++ b/api/data_workspace/v2/tests/bdd/test_goods_descriptions.py @@ -0,0 +1,80 @@ +from pytest_bdd import ( + given, + parsers, + scenarios, + when, +) + +from django.urls import reverse + +from api.applications.tests.factories import GoodOnApplicationFactory +from api.staticdata.report_summaries.models import ( + ReportSummaryPrefix, + ReportSummarySubject, +) + + +scenarios("./scenarios/goods_descriptions.feature") + + +@given(parsers.parse("the application has the following goods:{goods}")) +def given_the_application_has_the_following_goods(parse_table, draft_standard_application, goods): + draft_standard_application.goods.all().delete() + good_attributes = parse_table(goods)[1:] + for id, name in good_attributes: + GoodOnApplicationFactory( + application=draft_standard_application, + id=id, + good__name=name, + ) + + +@when(parsers.parse("the goods are assessed by TAU as:{assessments}")) +def when_the_goods_are_assessed_by_tau( + parse_table, + submitted_standard_application, + assessments, + api_client, + lu_case_officer, + gov_headers, +): + assessments = parse_table(assessments)[1:] + url = reverse("assessments:make_assessments", kwargs={"case_pk": submitted_standard_application.pk}) + + assessment_payload = [] + for good_on_application_id, control_list_entry, report_summary_prefix, report_summary_subject in assessments: + data = { + "id": good_on_application_id, + "comment": "Some comment", + } + + if control_list_entry == "NLR": + data.update( + { + "control_list_entries": [], + "is_good_controlled": False, + } + ) + else: + if report_summary_prefix: + prefix = ReportSummaryPrefix.objects.get(name=report_summary_prefix) + else: + prefix = None + subject = ReportSummarySubject.objects.get(name=report_summary_subject) + data.update( + { + "control_list_entries": [control_list_entry], + "report_summary_prefix": prefix.pk if prefix else None, + "report_summary_subject": subject.pk, + "is_good_controlled": True, + "regime_entries": [], + } + ) + assessment_payload.append(data) + + response = api_client.put( + url, + assessment_payload, + **gov_headers, + ) + assert response.status_code == 200, response.content diff --git a/api/data_workspace/v2/urls.py b/api/data_workspace/v2/urls.py index f806142c87..68e22fcaa1 100644 --- a/api/data_workspace/v2/urls.py +++ b/api/data_workspace/v2/urls.py @@ -7,4 +7,5 @@ router_v2.register(views.CountryViewSet) router_v2.register(views.DestinationViewSet) router_v2.register(views.GoodViewSet) +router_v2.register(views.GoodDescriptionViewSet) router_v2.register(views.ApplicationViewSet) diff --git a/api/data_workspace/v2/views.py b/api/data_workspace/v2/views.py index c572a76239..21a6fcbf87 100644 --- a/api/data_workspace/v2/views.py +++ b/api/data_workspace/v2/views.py @@ -32,11 +32,13 @@ ApplicationSerializer, CountrySerializer, DestinationSerializer, + GoodDescriptionSerializer, GoodSerializer, LicenceDecisionSerializer, LicenceDecisionType, ) from api.staticdata.countries.models import Country +from api.staticdata.report_summaries.models import ReportSummary from api.staticdata.statuses.enums import CaseStatusEnum @@ -116,6 +118,19 @@ class DataWorkspace: table_name = "goods" +class GoodDescriptionViewSet(BaseViewSet): + serializer_class = GoodDescriptionSerializer + queryset = ( + ReportSummary.objects.select_related("prefix", "subject") + .prefetch_related("goods_on_application") + .exclude(goods_on_application__isnull=True) + .annotate(good_id=F("goods_on_application__id")) + ) + + class DataWorkspace: + table_name = "goods_descriptions" + + def get_closed_statuses(): status_map = dict(CaseStatusEnum.choices) return list( From f7082be7277666eefd6b6b644135bf4a4fcd8861 Mon Sep 17 00:00:00 2001 From: Kevin Carrogan Date: Mon, 11 Nov 2024 16:36:00 +0000 Subject: [PATCH 02/22] Add an assessments table --- api/data_workspace/v2/serializers.py | 11 +++++++++++ api/data_workspace/v2/urls.py | 1 + api/data_workspace/v2/views.py | 14 ++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/api/data_workspace/v2/serializers.py b/api/data_workspace/v2/serializers.py index 99122c6482..464472b6ed 100644 --- a/api/data_workspace/v2/serializers.py +++ b/api/data_workspace/v2/serializers.py @@ -10,6 +10,7 @@ ) from api.cases.enums import LicenceDecisionType from api.cases.models import Case +from api.staticdata.control_list_entries.models import ControlListEntry from api.staticdata.countries.models import Country from api.staticdata.report_summaries.models import ReportSummary @@ -128,3 +129,13 @@ def get_first_closed_at(self, application) -> typing.Optional[datetime.datetime] return application.baseapplication_ptr.case_ptr.closed_status_updates[0].created_at return None + +class AssessmentSerializer(serializers.ModelSerializer): + good_id = serializers.UUIDField() + + class Meta: + model = ControlListEntry + fields = ( + "good_id", + "rating", + ) diff --git a/api/data_workspace/v2/urls.py b/api/data_workspace/v2/urls.py index 68e22fcaa1..4d2dd24b9e 100644 --- a/api/data_workspace/v2/urls.py +++ b/api/data_workspace/v2/urls.py @@ -9,3 +9,4 @@ router_v2.register(views.GoodViewSet) router_v2.register(views.GoodDescriptionViewSet) router_v2.register(views.ApplicationViewSet) +router_v2.register(views.AssessmentViewSet) diff --git a/api/data_workspace/v2/views.py b/api/data_workspace/v2/views.py index 21a6fcbf87..50a1410a08 100644 --- a/api/data_workspace/v2/views.py +++ b/api/data_workspace/v2/views.py @@ -30,6 +30,7 @@ from api.core.helpers import str_to_bool from api.data_workspace.v2.serializers import ( ApplicationSerializer, + AssessmentSerializer, CountrySerializer, DestinationSerializer, GoodDescriptionSerializer, @@ -37,6 +38,7 @@ LicenceDecisionSerializer, LicenceDecisionType, ) +from api.staticdata.control_list_entries.models import ControlListEntry from api.staticdata.countries.models import Country from api.staticdata.report_summaries.models import ReportSummary from api.staticdata.statuses.enums import CaseStatusEnum @@ -162,3 +164,15 @@ class ApplicationViewSet(BaseViewSet): class DataWorkspace: table_name = "applications" + + +class AssessmentViewSet(BaseViewSet): + serializer_class = AssessmentSerializer + + def get_queryset(self): + return ControlListEntry.objects.annotate( + good_id=F("goodonapplication__id"), + ).exclude(good_id__isnull=True) + + class DataWorkspace: + table_name = "goods_ratings" From cbc2fd13ed80f2b276d630c726baa18cb61c9c2c Mon Sep 17 00:00:00 2001 From: Arun Siluvery Date: Mon, 25 Nov 2024 17:57:09 +0000 Subject: [PATCH 03/22] Add bdd tests for goods ratings endpoint --- api/data_workspace/v2/tests/bdd/conftest.py | 70 +++++++++++++++++ .../tests/bdd/scenarios/goods_ratings.feature | 33 ++++++++ .../v2/tests/bdd/test_goods_descriptions.py | 78 +------------------ .../v2/tests/bdd/test_goods_ratings.py | 4 + 4 files changed, 108 insertions(+), 77 deletions(-) create mode 100644 api/data_workspace/v2/tests/bdd/scenarios/goods_ratings.feature create mode 100644 api/data_workspace/v2/tests/bdd/test_goods_ratings.py diff --git a/api/data_workspace/v2/tests/bdd/conftest.py b/api/data_workspace/v2/tests/bdd/conftest.py index 4cda8b3ede..2134e40953 100644 --- a/api/data_workspace/v2/tests/bdd/conftest.py +++ b/api/data_workspace/v2/tests/bdd/conftest.py @@ -38,6 +38,10 @@ from api.parties.tests.factories import PartyDocumentFactory from api.organisations.tests.factories import OrganisationFactory from api.staticdata.letter_layouts.models import LetterLayout +from api.staticdata.report_summaries.models import ( + ReportSummaryPrefix, + ReportSummarySubject, +) from api.staticdata.statuses.enums import CaseStatusEnum from api.staticdata.statuses.models import CaseStatus from api.staticdata.units.enums import Units @@ -363,4 +367,70 @@ def check_rows(client, parse_table, unpage_data, table_name, rows): for row in parsed_rows[1:]: expected_data.append({key: value for key, value in zip(keys, row)}) expected_data = cast_to_types(expected_data, table_metadata["fields"]) + + actual_data = sorted(actual_data, key=lambda d, key=keys[0]: d[key]) + expected_data = sorted(expected_data, key=lambda d, key=keys[0]: d[key]) assert actual_data == expected_data + + +@given(parsers.parse("the application has the following goods:{goods}")) +def given_the_application_has_the_following_goods(parse_table, draft_standard_application, goods): + draft_standard_application.goods.all().delete() + good_attributes = parse_table(goods)[1:] + for id, name in good_attributes: + GoodOnApplicationFactory( + application=draft_standard_application, + id=id, + good__name=name, + ) + + +@when(parsers.parse("the goods are assessed by TAU as:{assessments}")) +def when_the_goods_are_assessed_by_tau( + parse_table, + submitted_standard_application, + assessments, + api_client, + lu_case_officer, + gov_headers, +): + assessments = parse_table(assessments)[1:] + url = reverse("assessments:make_assessments", kwargs={"case_pk": submitted_standard_application.pk}) + + assessment_payload = [] + for good_on_application_id, control_list_entry, report_summary_prefix, report_summary_subject in assessments: + data = { + "id": good_on_application_id, + "comment": "Some comment", + } + + if control_list_entry == "NLR": + data.update( + { + "control_list_entries": [], + "is_good_controlled": False, + } + ) + else: + if report_summary_prefix: + prefix = ReportSummaryPrefix.objects.get(name=report_summary_prefix) + else: + prefix = None + subject = ReportSummarySubject.objects.get(name=report_summary_subject) + data.update( + { + "control_list_entries": [control_list_entry], + "report_summary_prefix": prefix.pk if prefix else None, + "report_summary_subject": subject.pk, + "is_good_controlled": True, + "regime_entries": [], + } + ) + assessment_payload.append(data) + + response = api_client.put( + url, + assessment_payload, + **gov_headers, + ) + assert response.status_code == 200, response.content diff --git a/api/data_workspace/v2/tests/bdd/scenarios/goods_ratings.feature b/api/data_workspace/v2/tests/bdd/scenarios/goods_ratings.feature new file mode 100644 index 0000000000..df0202b1a5 --- /dev/null +++ b/api/data_workspace/v2/tests/bdd/scenarios/goods_ratings.feature @@ -0,0 +1,33 @@ +@db +Feature: goods_ratings Table + +Scenario: Draft application + Given a draft standard application + Then the `goods_ratings` table is empty + +Scenario: Submitted application + Given a draft standard application + And the application has the following goods: + | id | name | + | 8fa8dc3c-c103-42f5-ba94-2d9098b8821d | A controlled good | + | 4dad5dc6-38ef-4bf7-99fd-0c6bc5d86048 | An NLR good | + When the application is submitted + Then the `goods_ratings` table is empty + +Scenario: Assess application + Given a draft standard application + And the application has the following goods: + | id | name | + | 8fa8dc3c-c103-42f5-ba94-2d9098b8821d | A controlled good | + | 118a003c-7191-4a2c-97e9-be243722cbb2 | Another controlled good | + | 4dad5dc6-38ef-4bf7-99fd-0c6bc5d86048 | An NLR good | + When the application is submitted + And the goods are assessed by TAU as: + | id | Control list entry | Report summary prefix | Report summary subject | + | 8fa8dc3c-c103-42f5-ba94-2d9098b8821d | ML22a | accessories for | composite laminates | + | 118a003c-7191-4a2c-97e9-be243722cbb2 | PL9010 | | composite laminates | + | 4dad5dc6-38ef-4bf7-99fd-0c6bc5d86048 | NLR | | | + Then the `goods_ratings` table has the following rows: + | good_id | rating | + | 8fa8dc3c-c103-42f5-ba94-2d9098b8821d | ML22a | + | 118a003c-7191-4a2c-97e9-be243722cbb2 | PL9010 | diff --git a/api/data_workspace/v2/tests/bdd/test_goods_descriptions.py b/api/data_workspace/v2/tests/bdd/test_goods_descriptions.py index 88083a1278..8128fdd7f4 100644 --- a/api/data_workspace/v2/tests/bdd/test_goods_descriptions.py +++ b/api/data_workspace/v2/tests/bdd/test_goods_descriptions.py @@ -1,80 +1,4 @@ -from pytest_bdd import ( - given, - parsers, - scenarios, - when, -) - -from django.urls import reverse - -from api.applications.tests.factories import GoodOnApplicationFactory -from api.staticdata.report_summaries.models import ( - ReportSummaryPrefix, - ReportSummarySubject, -) +from pytest_bdd import scenarios scenarios("./scenarios/goods_descriptions.feature") - - -@given(parsers.parse("the application has the following goods:{goods}")) -def given_the_application_has_the_following_goods(parse_table, draft_standard_application, goods): - draft_standard_application.goods.all().delete() - good_attributes = parse_table(goods)[1:] - for id, name in good_attributes: - GoodOnApplicationFactory( - application=draft_standard_application, - id=id, - good__name=name, - ) - - -@when(parsers.parse("the goods are assessed by TAU as:{assessments}")) -def when_the_goods_are_assessed_by_tau( - parse_table, - submitted_standard_application, - assessments, - api_client, - lu_case_officer, - gov_headers, -): - assessments = parse_table(assessments)[1:] - url = reverse("assessments:make_assessments", kwargs={"case_pk": submitted_standard_application.pk}) - - assessment_payload = [] - for good_on_application_id, control_list_entry, report_summary_prefix, report_summary_subject in assessments: - data = { - "id": good_on_application_id, - "comment": "Some comment", - } - - if control_list_entry == "NLR": - data.update( - { - "control_list_entries": [], - "is_good_controlled": False, - } - ) - else: - if report_summary_prefix: - prefix = ReportSummaryPrefix.objects.get(name=report_summary_prefix) - else: - prefix = None - subject = ReportSummarySubject.objects.get(name=report_summary_subject) - data.update( - { - "control_list_entries": [control_list_entry], - "report_summary_prefix": prefix.pk if prefix else None, - "report_summary_subject": subject.pk, - "is_good_controlled": True, - "regime_entries": [], - } - ) - assessment_payload.append(data) - - response = api_client.put( - url, - assessment_payload, - **gov_headers, - ) - assert response.status_code == 200, response.content diff --git a/api/data_workspace/v2/tests/bdd/test_goods_ratings.py b/api/data_workspace/v2/tests/bdd/test_goods_ratings.py new file mode 100644 index 0000000000..e1f3e17865 --- /dev/null +++ b/api/data_workspace/v2/tests/bdd/test_goods_ratings.py @@ -0,0 +1,4 @@ +from pytest_bdd import scenarios + + +scenarios("./scenarios/goods_ratings.feature") From e8492c8dd0e336abab8d7322a6d5ad6ae7763760 Mon Sep 17 00:00:00 2001 From: Kevin Carrogan Date: Mon, 18 Nov 2024 13:48:18 +0000 Subject: [PATCH 04/22] Add footnotes table --- api/data_workspace/v2/serializers.py | 7 +++++++ api/data_workspace/v2/urls.py | 1 + api/data_workspace/v2/views.py | 16 +++++++++++++++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/api/data_workspace/v2/serializers.py b/api/data_workspace/v2/serializers.py index 0eab2882f6..8f01866efa 100644 --- a/api/data_workspace/v2/serializers.py +++ b/api/data_workspace/v2/serializers.py @@ -129,3 +129,10 @@ def get_first_closed_at(self, application) -> typing.Optional[datetime.datetime] return application.baseapplication_ptr.case_ptr.closed_status_updates[0].created_at return None + + +class FootnoteSerializer(serializers.Serializer): + footnote = serializers.CharField() + team_name = serializers.CharField(source="team__name") + application_id = serializers.CharField(source="case__pk") + type = serializers.CharField() diff --git a/api/data_workspace/v2/urls.py b/api/data_workspace/v2/urls.py index 68e22fcaa1..05817076f4 100644 --- a/api/data_workspace/v2/urls.py +++ b/api/data_workspace/v2/urls.py @@ -9,3 +9,4 @@ router_v2.register(views.GoodViewSet) router_v2.register(views.GoodDescriptionViewSet) router_v2.register(views.ApplicationViewSet) +router_v2.register(views.FootnoteViewSet) diff --git a/api/data_workspace/v2/views.py b/api/data_workspace/v2/views.py index ed96d178af..086e455892 100644 --- a/api/data_workspace/v2/views.py +++ b/api/data_workspace/v2/views.py @@ -24,13 +24,14 @@ ) from api.audit_trail.enums import AuditType from api.audit_trail.models import Audit -from api.cases.models import LicenceDecision +from api.cases.models import Advice, LicenceDecision from api.core.authentication import DataWorkspaceOnlyAuthentication from api.core.helpers import str_to_bool from api.data_workspace.v2.serializers import ( ApplicationSerializer, CountrySerializer, DestinationSerializer, + FootnoteSerializer, GoodDescriptionSerializer, GoodSerializer, LicenceDecisionSerializer, @@ -141,3 +142,16 @@ class ApplicationViewSet(BaseViewSet): class DataWorkspace: table_name = "applications" + + +class FootnoteViewSet(BaseViewSet): + serializer_class = FootnoteSerializer + queryset = ( + Advice.objects.exclude(Q(footnote="") | Q(footnote__isnull=True)) + .values("footnote", "team__name", "case__pk", "type") + .order_by("case__pk") + .distinct() + ) + + class DataWorkspace: + table_name = "footnotes" From 26ade2319c8192db03a62c354acc2cdcaeb52026 Mon Sep 17 00:00:00 2001 From: Arun Siluvery Date: Wed, 27 Nov 2024 16:57:25 +0000 Subject: [PATCH 05/22] Add bdd tests for footnotes endpoint --- api/data_workspace/v2/tests/bdd/conftest.py | 48 +++++++++++++++ .../v2/tests/bdd/scenarios/footnotes.feature | 33 ++++++++++ .../v2/tests/bdd/test_applications.py | 31 ---------- .../v2/tests/bdd/test_footnotes.py | 61 +++++++++++++++++++ 4 files changed, 142 insertions(+), 31 deletions(-) create mode 100644 api/data_workspace/v2/tests/bdd/scenarios/footnotes.feature create mode 100644 api/data_workspace/v2/tests/bdd/test_footnotes.py diff --git a/api/data_workspace/v2/tests/bdd/conftest.py b/api/data_workspace/v2/tests/bdd/conftest.py index 2c1bace8ad..687a4b3e42 100644 --- a/api/data_workspace/v2/tests/bdd/conftest.py +++ b/api/data_workspace/v2/tests/bdd/conftest.py @@ -130,6 +130,18 @@ def gov_user_permissions(): Permission.objects.get_or_create(id=permission.name, name=permission.value, type=UserType.INTERNAL.value) +@pytest.fixture() +def ogd_advisor(gov_user, gov_user_permissions): + gov_user.role = RoleFactory(name="OGD Advisor", type=UserType.INTERNAL) + gov_user.role.permissions.set( + [ + GovPermissions.MAINTAIN_FOOTNOTES.name, + ] + ) + gov_user.save() + return gov_user + + @pytest.fixture() def lu_case_officer(gov_user, gov_user_permissions): gov_user.role = RoleFactory(name="Case officer", type=UserType.INTERNAL) @@ -161,6 +173,11 @@ def gov_headers(gov_user): return {"HTTP_GOV_USER_TOKEN": user_to_token(gov_user.baseuser_ptr)} +@pytest.fixture() +def ogd_advisor_headers(ogd_advisor): + return {"HTTP_GOV_USER_TOKEN": user_to_token(ogd_advisor.baseuser_ptr)} + + @pytest.fixture() def lu_case_officer_headers(lu_case_officer): return {"HTTP_GOV_USER_TOKEN": user_to_token(lu_case_officer.baseuser_ptr)} @@ -364,3 +381,34 @@ def check_rows(client, parse_table, unpage_data, table_name, rows): expected_data.append({key: value for key, value in zip(keys, row)}) expected_data = cast_to_types(expected_data, table_metadata["fields"]) assert actual_data == expected_data + + +@pytest.fixture() +def parse_attributes(parse_table): + def _parse_attributes(attributes): + kwargs = {} + table_data = parse_table(attributes) + for key, value in table_data[1:]: + kwargs[key] = value + return kwargs + + return _parse_attributes + + +@given( + parsers.parse("a draft standard application with attributes:{attributes}"), + target_fixture="draft_standard_application", +) +def given_a_draft_standard_application_with_attributes(organisation, parse_attributes, attributes): + application = DraftStandardApplicationFactory( + organisation=organisation, + **parse_attributes(attributes), + ) + + PartyDocumentFactory( + party=application.end_user.party, + s3_key="party-document", + safe=True, + ) + + return application diff --git a/api/data_workspace/v2/tests/bdd/scenarios/footnotes.feature b/api/data_workspace/v2/tests/bdd/scenarios/footnotes.feature new file mode 100644 index 0000000000..9662a59456 --- /dev/null +++ b/api/data_workspace/v2/tests/bdd/scenarios/footnotes.feature @@ -0,0 +1,33 @@ +@db +Feature: applications Table + +Scenario: Draft application + Given a draft standard application + Then the `footnotes` table is empty + +Scenario: Submit an application + Given a draft standard application + When the application is submitted + Then the `footnotes` table is empty + +Scenario: Approving an application with footnotes + Given a draft standard application with attributes: + | name | value | + | id | 03fb08eb-1564-4b68-9336-3ca8906543f9 | + When the application is submitted + And a recommendation is added with footnotes: + | team | footnotes | + | FCDO | Commercial end user | + Then the `footnotes` table has the following rows: + | application_id | team_name | type | footnote | + | 03fb08eb-1564-4b68-9336-3ca8906543f9 | FCDO | approve | Commercial end user | + +Scenario: Approving an application with no footnotes + Given a draft standard application with attributes: + | name | value | + | id | 03fb08eb-1564-4b68-9336-3ca8906543f9 | + When the application is submitted + And a recommendation is added with footnotes: + | team | footnotes | + | FCDO | | + Then the `footnotes` table is empty diff --git a/api/data_workspace/v2/tests/bdd/test_applications.py b/api/data_workspace/v2/tests/bdd/test_applications.py index 1a2c1ac20c..bad695de0f 100644 --- a/api/data_workspace/v2/tests/bdd/test_applications.py +++ b/api/data_workspace/v2/tests/bdd/test_applications.py @@ -41,18 +41,6 @@ scenarios("./scenarios/applications.feature") -@pytest.fixture() -def parse_attributes(parse_table): - def _parse_attributes(attributes): - kwargs = {} - table_data = parse_table(attributes) - for key, value in table_data[1:]: - kwargs[key] = value - return kwargs - - return _parse_attributes - - def run_processing_time_task(start, up_to): processing_time_task_run_date_time = start.replace(hour=22, minute=30) up_to = pytz.utc.localize(datetime.datetime.fromisoformat(up_to)) @@ -204,25 +192,6 @@ def given_draft_standard_application(organisation): return application -@given( - parsers.parse("a draft standard application with attributes:{attributes}"), - target_fixture="draft_standard_application", -) -def given_a_draft_standard_application_with_attributes(organisation, parse_attributes, attributes): - application = DraftStandardApplicationFactory( - organisation=organisation, - **parse_attributes(attributes), - ) - - PartyDocumentFactory( - party=application.end_user.party, - s3_key="party-document", - safe=True, - ) - - return application - - @given( parsers.parse("a draft temporary standard application with attributes:{attributes}"), target_fixture="draft_standard_application", diff --git a/api/data_workspace/v2/tests/bdd/test_footnotes.py b/api/data_workspace/v2/tests/bdd/test_footnotes.py new file mode 100644 index 0000000000..b3b3ceb470 --- /dev/null +++ b/api/data_workspace/v2/tests/bdd/test_footnotes.py @@ -0,0 +1,61 @@ +import pytest + +from pytest_bdd import ( + parsers, + scenarios, + when, +) + +from django.urls import reverse + +from api.staticdata.statuses.enums import CaseStatusEnum +from api.teams.models import Team + + +scenarios("./scenarios/footnotes.feature") + + +@pytest.fixture() +def add_recommendation( + api_client, + ogd_advisor, + ogd_advisor_headers, +): + def _add_recommendation(application, footnotes_data): + + url = reverse("caseworker_applications:change_status", kwargs={"pk": str(application.pk)}) + response = api_client.post(url, data={"status": CaseStatusEnum.OGD_ADVICE}, **ogd_advisor_headers) + assert response.status_code == 200 + + ogd_advisor.team = Team.objects.get(name=footnotes_data[0]["team"]) + ogd_advisor.save() + + subjects = [("good_id", 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() + ] + + data = [ + { + "type": "approve", + "text": "Recommend issuing licence", + "proviso": "", + "footnote_required": True, + "footnote": footnotes_data[0]["footnotes"], + subject_name: str(subject_id), + "denial_reasons": [], + } + for subject_name, subject_id in subjects + ] + url = reverse("cases:user_advice", kwargs={"pk": str(application.pk)}) + response = api_client.post(url, data=data, **ogd_advisor_headers) + assert response.status_code == 201 + + return _add_recommendation + + +@when(parsers.parse("a recommendation is added with footnotes:{footnotes}")) +def when_a_recommendation_is_added_with_footnotes( + submitted_standard_application, add_recommendation, parse_table, footnotes +): + footnotes_data = [{"team": team, "footnotes": text} for team, text in parse_table(footnotes)[1:]] + add_recommendation(submitted_standard_application, footnotes_data) From 288b3554ef7919f8d8d68d17c899c24fd3fe8054 Mon Sep 17 00:00:00 2001 From: Kevin Carrogan Date: Tue, 19 Nov 2024 13:13:14 +0000 Subject: [PATCH 06/22] Add unit viewset --- api/data_workspace/v2/serializers.py | 5 +++++ api/data_workspace/v2/urls.py | 1 + api/data_workspace/v2/views.py | 22 ++++++++++++++++++++++ 3 files changed, 28 insertions(+) diff --git a/api/data_workspace/v2/serializers.py b/api/data_workspace/v2/serializers.py index 0eab2882f6..4bc90815e6 100644 --- a/api/data_workspace/v2/serializers.py +++ b/api/data_workspace/v2/serializers.py @@ -129,3 +129,8 @@ def get_first_closed_at(self, application) -> typing.Optional[datetime.datetime] return application.baseapplication_ptr.case_ptr.closed_status_updates[0].created_at return None + + +class UnitSerializer(serializers.Serializer): + code = serializers.CharField() + description = serializers.CharField() diff --git a/api/data_workspace/v2/urls.py b/api/data_workspace/v2/urls.py index 68e22fcaa1..1f077d4931 100644 --- a/api/data_workspace/v2/urls.py +++ b/api/data_workspace/v2/urls.py @@ -9,3 +9,4 @@ router_v2.register(views.GoodViewSet) router_v2.register(views.GoodDescriptionViewSet) router_v2.register(views.ApplicationViewSet) +router_v2.register(views.UnitViewSet) diff --git a/api/data_workspace/v2/views.py b/api/data_workspace/v2/views.py index ed96d178af..81e28b76ce 100644 --- a/api/data_workspace/v2/views.py +++ b/api/data_workspace/v2/views.py @@ -2,6 +2,7 @@ from rest_framework import viewsets from rest_framework.pagination import LimitOffsetPagination +from rest_framework.response import Response from rest_framework.settings import api_settings from rest_framework_csv.renderers import PaginatedCSVRenderer @@ -34,10 +35,12 @@ GoodDescriptionSerializer, GoodSerializer, LicenceDecisionSerializer, + UnitSerializer, ) from api.staticdata.countries.models import Country from api.staticdata.report_summaries.models import ReportSummary from api.staticdata.statuses.enums import CaseStatusEnum +from api.staticdata.units.enums import Units class DisableableLimitOffsetPagination(LimitOffsetPagination): @@ -141,3 +144,22 @@ class ApplicationViewSet(BaseViewSet): class DataWorkspace: table_name = "applications" + + + +class UnitViewSet(viewsets.ViewSet): + authentication_classes = (DataWorkspaceOnlyAuthentication,) + pagination_class = DisableableLimitOffsetPagination + renderer_classes = tuple(api_settings.DEFAULT_RENDERER_CLASSES) + (PaginatedCSVRenderer,) + + def list(self, request): + units = [{"code": code, "description": description} for code, description in Units.choices] + return Response(UnitSerializer(units, many=True).data) + + def retrieve(self, request, pk): + units = dict(Units.choices) + description = units[pk] + return Response(UnitSerializer({"code": pk, "description": description}).data) + + class DataWorkspace: + table_name = "units" From 574744b7c78b9d210518d6f8ae975ed6e28a1dfb Mon Sep 17 00:00:00 2001 From: Kevin Carrogan Date: Tue, 19 Nov 2024 13:18:04 +0000 Subject: [PATCH 07/22] Add proper 404 for unfound unit --- api/data_workspace/v2/views.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/api/data_workspace/v2/views.py b/api/data_workspace/v2/views.py index 81e28b76ce..ce6ec18f02 100644 --- a/api/data_workspace/v2/views.py +++ b/api/data_workspace/v2/views.py @@ -17,6 +17,7 @@ Min, ) from django.db.models.lookups import GreaterThan +from django.http import Http404 from api.applications.models import ( GoodOnApplication, @@ -146,7 +147,6 @@ class DataWorkspace: table_name = "applications" - class UnitViewSet(viewsets.ViewSet): authentication_classes = (DataWorkspaceOnlyAuthentication,) pagination_class = DisableableLimitOffsetPagination @@ -158,7 +158,10 @@ def list(self, request): def retrieve(self, request, pk): units = dict(Units.choices) - description = units[pk] + try: + description = units[pk] + except KeyError: + raise Http404() return Response(UnitSerializer({"code": pk, "description": description}).data) class DataWorkspace: From 923291eefcf367f40923b07706df483d1bc6fca7 Mon Sep 17 00:00:00 2001 From: Arun Siluvery Date: Wed, 27 Nov 2024 18:54:13 +0000 Subject: [PATCH 08/22] Add bdd tests for units endpoint --- api/data_workspace/v2/tests/bdd/conftest.py | 31 +++++++++++++++++++ .../v2/tests/bdd/scenarios/units.feature | 19 ++++++++++++ api/data_workspace/v2/tests/bdd/test_units.py | 4 +++ 3 files changed, 54 insertions(+) create mode 100644 api/data_workspace/v2/tests/bdd/scenarios/units.feature create mode 100644 api/data_workspace/v2/tests/bdd/test_units.py diff --git a/api/data_workspace/v2/tests/bdd/conftest.py b/api/data_workspace/v2/tests/bdd/conftest.py index 2c1bace8ad..c87c5c9afe 100644 --- a/api/data_workspace/v2/tests/bdd/conftest.py +++ b/api/data_workspace/v2/tests/bdd/conftest.py @@ -364,3 +364,34 @@ def check_rows(client, parse_table, unpage_data, table_name, rows): expected_data.append({key: value for key, value in zip(keys, row)}) expected_data = cast_to_types(expected_data, table_metadata["fields"]) assert actual_data == expected_data + + +@given(parsers.parse("endpoint exists for exporting `{table_name}`")) +def given_endpoint_exists(client, table_name): + metadata_url = reverse("data_workspace:v2:table-metadata") + response = client.get(metadata_url) + assert table_name in [t["table_name"] for t in response.json()["tables"]] + + +@then(parsers.parse("the `{table_name}` table should contain the following rows:{rows}")) +def check_rows(client, parse_table, unpage_data, table_name, rows): + metadata_url = reverse("data_workspace:v2:table-metadata") + response = client.get(metadata_url) + tables_metadata = response.json()["tables"] + for m in tables_metadata: + if m["table_name"] == table_name: + table_metadata = m + break + else: + pytest.fail(f"No table called {table_name} found") + + response = client.get(table_metadata["endpoint"]) + assert response.status_code == status.HTTP_200_OK + actual_data = response.data + + parsed_rows = parse_table(rows) + keys = parsed_rows[0] + expected_data = [] + for row in parsed_rows[1:]: + expected_data.append({key: value for key, value in zip(keys, row)}) + assert actual_data == expected_data diff --git a/api/data_workspace/v2/tests/bdd/scenarios/units.feature b/api/data_workspace/v2/tests/bdd/scenarios/units.feature new file mode 100644 index 0000000000..303816a1d9 --- /dev/null +++ b/api/data_workspace/v2/tests/bdd/scenarios/units.feature @@ -0,0 +1,19 @@ +@db +Feature: units Table + +Scenario: Units endpoint + Given endpoint exists for exporting `units` + Then the `units` table should contain the following rows: + | code | description | + | NAR | Items | + | TON | Tonnes | + | KGM | Kilograms | + | GRM | Grams | + | MGM | Milligrams | + | MCG | Micrograms | + | MTR | Metres | + | MTK | Square metres | + | MTQ | Cubic metres | + | LTR | Litres | + | MLT | Millilitres | + | MCL | Microlitres | diff --git a/api/data_workspace/v2/tests/bdd/test_units.py b/api/data_workspace/v2/tests/bdd/test_units.py new file mode 100644 index 0000000000..2cb318f5ae --- /dev/null +++ b/api/data_workspace/v2/tests/bdd/test_units.py @@ -0,0 +1,4 @@ +from pytest_bdd import scenarios + + +scenarios("./scenarios/units.feature") From 78f96e340702e47c8b21bbce1b6a23c18dd797ae Mon Sep 17 00:00:00 2001 From: Arun Siluvery Date: Thu, 28 Nov 2024 16:23:05 +0000 Subject: [PATCH 09/22] Order goods_rating table by the rating --- api/data_workspace/v2/views.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/api/data_workspace/v2/views.py b/api/data_workspace/v2/views.py index af37046589..eed9f4055c 100644 --- a/api/data_workspace/v2/views.py +++ b/api/data_workspace/v2/views.py @@ -149,9 +149,13 @@ class AssessmentViewSet(BaseViewSet): serializer_class = AssessmentSerializer def get_queryset(self): - return ControlListEntry.objects.annotate( - good_id=F("goodonapplication__id"), - ).exclude(good_id__isnull=True) + return ( + ControlListEntry.objects.annotate( + good_id=F("goodonapplication__id"), + ) + .exclude(good_id__isnull=True) + .order_by("rating") + ) class DataWorkspace: table_name = "goods_ratings" From fb4ef213329509357aad0134c44ea8fe3fd19158 Mon Sep 17 00:00:00 2001 From: Henry Cooksley Date: Mon, 25 Nov 2024 16:40:44 +0000 Subject: [PATCH 10/22] Add goods on licences endpoint initial commit --- api/data_workspace/v2/serializers.py | 6 ++++++ api/data_workspace/v2/urls.py | 4 ++++ api/data_workspace/v2/views.py | 12 ++++++++++++ lite_routing | 2 +- 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/api/data_workspace/v2/serializers.py b/api/data_workspace/v2/serializers.py index 0eab2882f6..c0a4f4029a 100644 --- a/api/data_workspace/v2/serializers.py +++ b/api/data_workspace/v2/serializers.py @@ -11,6 +11,8 @@ ) from api.cases.enums import LicenceDecisionType from api.cases.models import LicenceDecision +from api.cases.models import Case +from api.licences.models import GoodOnLicence from api.staticdata.countries.models import Country from api.staticdata.report_summaries.models import ReportSummary @@ -94,6 +96,10 @@ class Meta: "description", "good_id", ) +class GoodOnLicenceSerializer(serializers.ModelSerializer): + class Meta: + model = GoodOnLicence + fields = ("id", "good_id", "licence_id") class ApplicationSerializer(serializers.ModelSerializer): diff --git a/api/data_workspace/v2/urls.py b/api/data_workspace/v2/urls.py index 68e22fcaa1..25304d1985 100644 --- a/api/data_workspace/v2/urls.py +++ b/api/data_workspace/v2/urls.py @@ -7,5 +7,9 @@ router_v2.register(views.CountryViewSet) router_v2.register(views.DestinationViewSet) router_v2.register(views.GoodViewSet) +<<<<<<< HEAD router_v2.register(views.GoodDescriptionViewSet) +======= +router_v2.register(views.GoodOnLicenceViewSet) +>>>>>>> ea66a2a2 (Add goods on licences endpoint initial commit) router_v2.register(views.ApplicationViewSet) diff --git a/api/data_workspace/v2/views.py b/api/data_workspace/v2/views.py index ed96d178af..bfbfd4b2f0 100644 --- a/api/data_workspace/v2/views.py +++ b/api/data_workspace/v2/views.py @@ -34,7 +34,11 @@ GoodDescriptionSerializer, GoodSerializer, LicenceDecisionSerializer, + LicenceDecisionType, + GoodOnLicenceSerializer, ) +from api.licences.enums import LicenceStatus +from api.licences.models import GoodOnLicence from api.staticdata.countries.models import Country from api.staticdata.report_summaries.models import ReportSummary from api.staticdata.statuses.enums import CaseStatusEnum @@ -117,6 +121,14 @@ def get_closed_statuses(): ) +class GoodOnLicenceViewSet(BaseViewSet): + serializer_class = GoodOnLicenceSerializer + queryset = GoodOnLicence.objects.exclude( + licence__case__status__=CaseStatusEnum.DRAFT, + licence__status=LicenceStatus.DRAFT, + ) + + class ApplicationViewSet(BaseViewSet): serializer_class = ApplicationSerializer queryset = ( diff --git a/lite_routing b/lite_routing index 514fdb9de4..fbad92cd75 160000 --- a/lite_routing +++ b/lite_routing @@ -1 +1 @@ -Subproject commit 514fdb9de41b40f812ef9d461bac405cc01405d8 +Subproject commit fbad92cd75b1195ad3c2262e6a95c6b7e8c925ef From 47924e0ee58ffae5bef3cd26838f5b3777fb95c7 Mon Sep 17 00:00:00 2001 From: Henry Cooksley Date: Mon, 25 Nov 2024 16:43:06 +0000 Subject: [PATCH 11/22] Add table_name --- api/data_workspace/v2/views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/data_workspace/v2/views.py b/api/data_workspace/v2/views.py index bfbfd4b2f0..067154e5e3 100644 --- a/api/data_workspace/v2/views.py +++ b/api/data_workspace/v2/views.py @@ -128,6 +128,9 @@ class GoodOnLicenceViewSet(BaseViewSet): licence__status=LicenceStatus.DRAFT, ) + class DataWorkspace: + table_name = "goods_on_licences" + class ApplicationViewSet(BaseViewSet): serializer_class = ApplicationSerializer From 2170459f21cb203611865870bf38b5d39c5424e7 Mon Sep 17 00:00:00 2001 From: Henry Cooksley Date: Tue, 26 Nov 2024 12:16:22 +0000 Subject: [PATCH 12/22] Add test for issuing a licence --- api/data_workspace/v2/serializers.py | 8 +- api/data_workspace/v2/tests/bdd/conftest.py | 62 ++++++++++++++++ .../bdd/scenarios/goods_on_licences.feature | 14 ++++ .../v2/tests/bdd/test_applications.py | 74 ------------------- .../v2/tests/bdd/test_goods_on_licences.py | 35 +++++++++ api/data_workspace/v2/urls.py | 3 - api/data_workspace/v2/views.py | 3 +- lite_routing | 2 +- pii-secret-exclude.txt | 1 + 9 files changed, 120 insertions(+), 82 deletions(-) create mode 100644 api/data_workspace/v2/tests/bdd/scenarios/goods_on_licences.feature create mode 100644 api/data_workspace/v2/tests/bdd/test_goods_on_licences.py diff --git a/api/data_workspace/v2/serializers.py b/api/data_workspace/v2/serializers.py index c0a4f4029a..944c270de9 100644 --- a/api/data_workspace/v2/serializers.py +++ b/api/data_workspace/v2/serializers.py @@ -11,7 +11,6 @@ ) from api.cases.enums import LicenceDecisionType from api.cases.models import LicenceDecision -from api.cases.models import Case from api.licences.models import GoodOnLicence from api.staticdata.countries.models import Country from api.staticdata.report_summaries.models import ReportSummary @@ -96,10 +95,15 @@ class Meta: "description", "good_id", ) + + class GoodOnLicenceSerializer(serializers.ModelSerializer): + good_id = serializers.UUIDField() + licence_id = serializers.UUIDField() + class Meta: model = GoodOnLicence - fields = ("id", "good_id", "licence_id") + fields = ("good_id", "licence_id") class ApplicationSerializer(serializers.ModelSerializer): diff --git a/api/data_workspace/v2/tests/bdd/conftest.py b/api/data_workspace/v2/tests/bdd/conftest.py index 2c1bace8ad..ffc4710d75 100644 --- a/api/data_workspace/v2/tests/bdd/conftest.py +++ b/api/data_workspace/v2/tests/bdd/conftest.py @@ -25,6 +25,10 @@ PartyOnApplicationFactory, StandardApplicationFactory, ) +from api.cases.enums import ( + AdviceType, +) +from api.cases.tests.factories import FinalAdviceFactory from api.cases.enums import CaseTypeEnum from api.cases.models import CaseType from api.core.constants import ( @@ -33,6 +37,7 @@ Roles, ) from api.goods.tests.factories import GoodFactory +from api.flags.enums import SystemFlags from api.documents.libraries.s3_operations import init_s3_client from api.letter_templates.models import LetterTemplate from api.parties.tests.factories import PartyDocumentFactory @@ -364,3 +369,60 @@ def check_rows(client, parse_table, unpage_data, table_name, rows): expected_data.append({key: value for key, value in zip(keys, row)}) expected_data = cast_to_types(expected_data, table_metadata["fields"]) assert actual_data == expected_data + + +@pytest.fixture() +def parse_attributes(parse_table): + def _parse_attributes(attributes): + kwargs = {} + table_data = parse_table(attributes) + for key, value in table_data[1:]: + kwargs[key] = value + return kwargs + + return _parse_attributes + + +@pytest.fixture() +def issue_licence(api_client, lu_case_officer, gov_headers, siel_template): + def _issue_licence(application): + data = {"action": AdviceType.APPROVE, "duration": 24} + for good_on_app in application.goods.all(): + good_on_app.quantity = 100 + good_on_app.value = 10000 + good_on_app.save() + data[f"quantity-{good_on_app.id}"] = str(good_on_app.quantity) + data[f"value-{good_on_app.id}"] = str(good_on_app.value) + FinalAdviceFactory(user=lu_case_officer, case=application, good=good_on_app.good) + + issue_date = datetime.datetime.now() + data.update({"year": issue_date.year, "month": issue_date.month, "day": issue_date.day}) + + application.flags.remove(SystemFlags.ENFORCEMENT_CHECK_REQUIRED) + + url = reverse("applications:finalise", kwargs={"pk": application.pk}) + response = api_client.put(url, data=data, **gov_headers) + assert response.status_code == 200, response.content + response = response.json() + + data = { + "template": str(siel_template.id), + "text": "", + "visible_to_exporter": False, + "advice_type": AdviceType.APPROVE, + } + url = reverse( + "cases:generated_documents:generated_documents", + kwargs={"pk": str(application.pk)}, + ) + response = api_client.post(url, data=data, **gov_headers) + assert response.status_code == 201, response.content + + url = reverse( + "cases:finalise", + kwargs={"pk": str(application.pk)}, + ) + response = api_client.put(url, data={}, **gov_headers) + assert response.status_code == 201 + + return _issue_licence diff --git a/api/data_workspace/v2/tests/bdd/scenarios/goods_on_licences.feature b/api/data_workspace/v2/tests/bdd/scenarios/goods_on_licences.feature new file mode 100644 index 0000000000..1e78d22ed9 --- /dev/null +++ b/api/data_workspace/v2/tests/bdd/scenarios/goods_on_licences.feature @@ -0,0 +1,14 @@ +@db +Feature: Goods On Licences + +Scenario: Issue a licence + Given a standard application with the following goods: + | id | name | + | 61d193bd-a4d8-4f7d-8c07-1ac5e03ea2c7 | A controlled good | + And a draft licence with attributes: + | name | value | + | id | 962b4948-b87a-42fe-9c2b-61bdefd9cd21 | + When the licence is issued + Then the `goods_on_licences` table has the following rows: + | good_id | licence_id | + | 61d193bd-a4d8-4f7d-8c07-1ac5e03ea2c7 | 962b4948-b87a-42fe-9c2b-61bdefd9cd21 | diff --git a/api/data_workspace/v2/tests/bdd/test_applications.py b/api/data_workspace/v2/tests/bdd/test_applications.py index 1a2c1ac20c..63bce3634b 100644 --- a/api/data_workspace/v2/tests/bdd/test_applications.py +++ b/api/data_workspace/v2/tests/bdd/test_applications.py @@ -4,8 +4,6 @@ from freezegun import freeze_time -from moto import mock_aws - from pytest_bdd import ( given, parsers, @@ -13,7 +11,6 @@ when, ) -from django.conf import settings from django.urls import reverse from api.applications.enums import ApplicationExportType @@ -28,7 +25,6 @@ AdviceType, ) from api.cases.tests.factories import FinalAdviceFactory -from api.documents.libraries.s3_operations import init_s3_client from api.flags.enums import SystemFlags from api.licences.enums import LicenceStatus from api.parties.tests.factories import ( @@ -41,18 +37,6 @@ scenarios("./scenarios/applications.feature") -@pytest.fixture() -def parse_attributes(parse_table): - def _parse_attributes(attributes): - kwargs = {} - table_data = parse_table(attributes) - for key, value in table_data[1:]: - kwargs[key] = value - return kwargs - - return _parse_attributes - - def run_processing_time_task(start, up_to): processing_time_task_run_date_time = start.replace(hour=22, minute=30) up_to = pytz.utc.localize(datetime.datetime.fromisoformat(up_to)) @@ -62,19 +46,6 @@ def run_processing_time_task(start, up_to): processing_time_task_run_date_time = processing_time_task_run_date_time + datetime.timedelta(days=1) -@pytest.fixture(autouse=True) -def mock_s3(): - with mock_aws(): - s3 = init_s3_client() - s3.create_bucket( - Bucket=settings.AWS_STORAGE_BUCKET_NAME, - CreateBucketConfiguration={ - "LocationConstraint": settings.AWS_REGION, - }, - ) - yield - - @pytest.fixture def submit_application(api_client, exporter_headers, mocker): def _submit_application(draft_application): @@ -103,51 +74,6 @@ def _submit_application(draft_application): return _submit_application -@pytest.fixture() -def issue_licence(api_client, lu_case_officer, gov_headers, siel_template): - def _issue_licence(application): - data = {"action": AdviceType.APPROVE, "duration": 24} - for good_on_app in application.goods.all(): - good_on_app.quantity = 100 - good_on_app.value = 10000 - good_on_app.save() - data[f"quantity-{good_on_app.id}"] = str(good_on_app.quantity) - data[f"value-{good_on_app.id}"] = str(good_on_app.value) - FinalAdviceFactory(user=lu_case_officer, case=application, good=good_on_app.good) - - issue_date = datetime.datetime.now() - data.update({"year": issue_date.year, "month": issue_date.month, "day": issue_date.day}) - - application.flags.remove(SystemFlags.ENFORCEMENT_CHECK_REQUIRED) - - url = reverse("applications:finalise", kwargs={"pk": application.pk}) - response = api_client.put(url, data=data, **gov_headers) - assert response.status_code == 200, response.content - response = response.json() - - data = { - "template": str(siel_template.id), - "text": "", - "visible_to_exporter": False, - "advice_type": AdviceType.APPROVE, - } - url = reverse( - "cases:generated_documents:generated_documents", - kwargs={"pk": str(application.pk)}, - ) - response = api_client.post(url, data=data, **gov_headers) - assert response.status_code == 201, response.content - - url = reverse( - "cases:finalise", - kwargs={"pk": str(application.pk)}, - ) - response = api_client.put(url, data={}, **gov_headers) - assert response.status_code == 201 - - return _issue_licence - - @pytest.fixture() def caseworker_change_status(api_client, lu_case_officer, lu_case_officer_headers): def _caseworker_change_status(application, status): diff --git a/api/data_workspace/v2/tests/bdd/test_goods_on_licences.py b/api/data_workspace/v2/tests/bdd/test_goods_on_licences.py new file mode 100644 index 0000000000..b63dc856bb --- /dev/null +++ b/api/data_workspace/v2/tests/bdd/test_goods_on_licences.py @@ -0,0 +1,35 @@ +from pytest_bdd import given, parsers, scenarios, when + +from api.applications.tests.factories import GoodOnApplicationFactory +from api.licences.tests.factories import StandardLicenceFactory +from api.licences.enums import LicenceStatus + +scenarios("./scenarios/goods_on_licences.feature") + + +@given(parsers.parse("a standard application with the following goods:{goods}"), target_fixture="standard_application") +def standard_application_with_following_goods(parse_table, goods, standard_application): + standard_application.goods.all().delete() + good_attributes = parse_table(goods)[1:] + for id, name in good_attributes: + GoodOnApplicationFactory( + application=standard_application, + id=id, + good__name=name, + ) + return standard_application + + +@given(parsers.parse("a draft licence with attributes:{attributes}"), target_fixture="draft_licence") +def draft_licence_with_attributes(parse_attributes, attributes, standard_application): + draft_licence = StandardLicenceFactory( + case=standard_application, status=LicenceStatus.DRAFT, **parse_attributes(attributes) + ) + return draft_licence + + +@when("the licence is issued") +def licence_is_issued(standard_application, issue_licence): + issue_licence(standard_application) + issued_application = standard_application.refresh_from_db() + return issued_application diff --git a/api/data_workspace/v2/urls.py b/api/data_workspace/v2/urls.py index 25304d1985..99ad9f58e8 100644 --- a/api/data_workspace/v2/urls.py +++ b/api/data_workspace/v2/urls.py @@ -7,9 +7,6 @@ router_v2.register(views.CountryViewSet) router_v2.register(views.DestinationViewSet) router_v2.register(views.GoodViewSet) -<<<<<<< HEAD router_v2.register(views.GoodDescriptionViewSet) -======= router_v2.register(views.GoodOnLicenceViewSet) ->>>>>>> ea66a2a2 (Add goods on licences endpoint initial commit) router_v2.register(views.ApplicationViewSet) diff --git a/api/data_workspace/v2/views.py b/api/data_workspace/v2/views.py index 067154e5e3..107e7ac629 100644 --- a/api/data_workspace/v2/views.py +++ b/api/data_workspace/v2/views.py @@ -34,7 +34,6 @@ GoodDescriptionSerializer, GoodSerializer, LicenceDecisionSerializer, - LicenceDecisionType, GoodOnLicenceSerializer, ) from api.licences.enums import LicenceStatus @@ -124,7 +123,7 @@ def get_closed_statuses(): class GoodOnLicenceViewSet(BaseViewSet): serializer_class = GoodOnLicenceSerializer queryset = GoodOnLicence.objects.exclude( - licence__case__status__=CaseStatusEnum.DRAFT, + licence__case__status__status=CaseStatusEnum.DRAFT, licence__status=LicenceStatus.DRAFT, ) diff --git a/lite_routing b/lite_routing index fbad92cd75..514fdb9de4 160000 --- a/lite_routing +++ b/lite_routing @@ -1 +1 @@ -Subproject commit fbad92cd75b1195ad3c2262e6a95c6b7e8c925ef +Subproject commit 514fdb9de41b40f812ef9d461bac405cc01405d8 diff --git a/pii-secret-exclude.txt b/pii-secret-exclude.txt index ecf8253680..03d87ef58b 100644 --- a/pii-secret-exclude.txt +++ b/pii-secret-exclude.txt @@ -117,3 +117,4 @@ api/staticdata/report_summaries/migrations/data/0002_add_report_summaries/report api/cases/generated_documents/tests/data/dummy.pdf api/cases/generated_documents/tests/data/signed.pdf api/data_workspace/v2/tests/bdd/scenarios/applications.feature +api/data_workspace/v2/tests/bdd/scenarios/goods_on_licences.feature From cb1c1e2fcf3ff4285ca2dd6923c1cd6f622ef914 Mon Sep 17 00:00:00 2001 From: Henry Cooksley Date: Thu, 28 Nov 2024 19:26:30 +0000 Subject: [PATCH 13/22] Add test for NLR goods not appearing in goods on licences table --- api/data_workspace/v2/tests/bdd/conftest.py | 3 + .../bdd/scenarios/goods_on_licences.feature | 17 ++++++ .../v2/tests/bdd/test_goods_on_licences.py | 60 ++++++++++++++++++- 3 files changed, 78 insertions(+), 2 deletions(-) diff --git a/api/data_workspace/v2/tests/bdd/conftest.py b/api/data_workspace/v2/tests/bdd/conftest.py index ffc4710d75..fc4f49568c 100644 --- a/api/data_workspace/v2/tests/bdd/conftest.py +++ b/api/data_workspace/v2/tests/bdd/conftest.py @@ -393,6 +393,9 @@ def _issue_licence(application): good_on_app.save() data[f"quantity-{good_on_app.id}"] = str(good_on_app.quantity) data[f"value-{good_on_app.id}"] = str(good_on_app.value) + # create final advice for controlled goods; skip NLR goods + if good_on_app.is_good_controlled == False: + continue FinalAdviceFactory(user=lu_case_officer, case=application, good=good_on_app.good) issue_date = datetime.datetime.now() diff --git a/api/data_workspace/v2/tests/bdd/scenarios/goods_on_licences.feature b/api/data_workspace/v2/tests/bdd/scenarios/goods_on_licences.feature index 1e78d22ed9..ce3c4a9e4e 100644 --- a/api/data_workspace/v2/tests/bdd/scenarios/goods_on_licences.feature +++ b/api/data_workspace/v2/tests/bdd/scenarios/goods_on_licences.feature @@ -12,3 +12,20 @@ Scenario: Issue a licence Then the `goods_on_licences` table has the following rows: | good_id | licence_id | | 61d193bd-a4d8-4f7d-8c07-1ac5e03ea2c7 | 962b4948-b87a-42fe-9c2b-61bdefd9cd21 | + +Scenario: NLR goods not on licence + Given a standard application with the following goods: + | id | name | + | aa9736f9-48f5-4d44-ace9-e4b8738591a5 | Another controlled good | + | 56f562f6-b554-4bb3-923b-8695ab15afca | An NLR good | + And the goods are assessed by TAU as: + | id | Control list entry | Report summary prefix | Report summary subject | + | aa9736f9-48f5-4d44-ace9-e4b8738591a5 | ML5b | accessories for | network analysers | + | 56f562f6-b554-4bb3-923b-8695ab15afca | NLR | | | + And a draft licence with attributes: + | name | value | + | id | 847a9a03-c35f-4036-ab8c-8b58d13482ab | + When the licence is issued + Then the `goods_on_licences` table has the following rows: + | good_id | licence_id | + | aa9736f9-48f5-4d44-ace9-e4b8738591a5 | 847a9a03-c35f-4036-ab8c-8b58d13482ab | diff --git a/api/data_workspace/v2/tests/bdd/test_goods_on_licences.py b/api/data_workspace/v2/tests/bdd/test_goods_on_licences.py index b63dc856bb..f545efbfda 100644 --- a/api/data_workspace/v2/tests/bdd/test_goods_on_licences.py +++ b/api/data_workspace/v2/tests/bdd/test_goods_on_licences.py @@ -1,8 +1,13 @@ +from django.urls import reverse from pytest_bdd import given, parsers, scenarios, when from api.applications.tests.factories import GoodOnApplicationFactory from api.licences.tests.factories import StandardLicenceFactory from api.licences.enums import LicenceStatus +from api.staticdata.report_summaries.models import ( + ReportSummaryPrefix, + ReportSummarySubject, +) scenarios("./scenarios/goods_on_licences.feature") @@ -31,5 +36,56 @@ def draft_licence_with_attributes(parse_attributes, attributes, standard_applica @when("the licence is issued") def licence_is_issued(standard_application, issue_licence): issue_licence(standard_application) - issued_application = standard_application.refresh_from_db() - return issued_application + standard_application.refresh_from_db() + return standard_application + + +@given(parsers.parse("the goods are assessed by TAU as:{assessments}")) +def the_goods_are_assessed_by_tau_as( + parse_table, + standard_application, + assessments, + api_client, + lu_case_officer, + gov_headers, +): + assessments = parse_table(assessments)[1:] + url = reverse("assessments:make_assessments", kwargs={"case_pk": standard_application.pk}) + + assessment_payload = [] + for good_on_application_id, control_list_entry, report_summary_prefix, report_summary_subject in assessments: + data = { + "id": good_on_application_id, + "comment": "Some comment", + } + + if control_list_entry == "NLR": + data.update( + { + "control_list_entries": [], + "is_good_controlled": False, + } + ) + else: + if report_summary_prefix: + prefix = ReportSummaryPrefix.objects.get(name=report_summary_prefix) + else: + prefix = None + subject = ReportSummarySubject.objects.get(name=report_summary_subject) + data.update( + { + "control_list_entries": [control_list_entry], + "report_summary_prefix": prefix.pk if prefix else None, + "report_summary_subject": subject.pk, + "is_good_controlled": True, + "regime_entries": [], + } + ) + assessment_payload.append(data) + + response = api_client.put( + url, + assessment_payload, + **gov_headers, + ) + assert response.status_code == 200, response.content From 807d0a0140eca9c5373a564f45c09b94c36f458f Mon Sep 17 00:00:00 2001 From: Henry Cooksley Date: Thu, 28 Nov 2024 19:40:29 +0000 Subject: [PATCH 14/22] Add test for draft licences and draft applications --- .../bdd/scenarios/goods_on_licences.feature | 18 ++++++++++++++++++ .../v2/tests/bdd/test_goods_on_licences.py | 15 +++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/api/data_workspace/v2/tests/bdd/scenarios/goods_on_licences.feature b/api/data_workspace/v2/tests/bdd/scenarios/goods_on_licences.feature index ce3c4a9e4e..3b67b6cded 100644 --- a/api/data_workspace/v2/tests/bdd/scenarios/goods_on_licences.feature +++ b/api/data_workspace/v2/tests/bdd/scenarios/goods_on_licences.feature @@ -29,3 +29,21 @@ Scenario: NLR goods not on licence Then the `goods_on_licences` table has the following rows: | good_id | licence_id | | aa9736f9-48f5-4d44-ace9-e4b8738591a5 | 847a9a03-c35f-4036-ab8c-8b58d13482ab | + +Scenario: Draft licences + Given a standard application with the following goods: + | id | name | + | f7c674b1-cd5e-4a6d-a1f5-d6ab58149d05 | A controlled good 2 | + And a draft licence with attributes: + | name | value | + | id | 297e89b9-fc93-4f38-be46-c2ab38914007 | + Then the `goods_on_licences` table is empty + +Scenario: Draft applications + Given a draft standard application with the following goods: + | id | name | + | 8262dcf7-d932-4a33-978d-b5aa8a7878ee | A controlled good 3 | + And a draft licence with attributes: + | name | value | + | id | 2078827b-6d67-406c-becc-41c423720cfc | + Then the `goods_on_licences` table is empty diff --git a/api/data_workspace/v2/tests/bdd/test_goods_on_licences.py b/api/data_workspace/v2/tests/bdd/test_goods_on_licences.py index f545efbfda..9c51e66aa4 100644 --- a/api/data_workspace/v2/tests/bdd/test_goods_on_licences.py +++ b/api/data_workspace/v2/tests/bdd/test_goods_on_licences.py @@ -89,3 +89,18 @@ def the_goods_are_assessed_by_tau_as( **gov_headers, ) assert response.status_code == 200, response.content + + +@given( + parsers.parse("a draft standard application with the following goods:{goods}"), target_fixture="draft_application" +) +def draft_standard_application_with_following_goods(parse_table, goods, draft_application): + draft_application.goods.all().delete() + good_attributes = parse_table(goods)[1:] + for id, name in good_attributes: + GoodOnApplicationFactory( + application=draft_application, + id=id, + good__name=name, + ) + return draft_application From 415390d21f1b06083f2bf2e27eae2c759e2201f2 Mon Sep 17 00:00:00 2001 From: Arun Siluvery Date: Thu, 28 Nov 2024 22:51:19 +0000 Subject: [PATCH 15/22] Update units endpoint to return the response similar to other endpoints Units is not actually a model but we want the response to be paginated similar to other endpoints so that metadata endpoint can correctly expose this to DW. --- api/data_workspace/v2/tests/bdd/conftest.py | 26 +------------------ .../v2/tests/bdd/scenarios/units.feature | 4 +-- api/data_workspace/v2/views.py | 19 +++----------- 3 files changed, 6 insertions(+), 43 deletions(-) diff --git a/api/data_workspace/v2/tests/bdd/conftest.py b/api/data_workspace/v2/tests/bdd/conftest.py index c87c5c9afe..2e4f5941cf 100644 --- a/api/data_workspace/v2/tests/bdd/conftest.py +++ b/api/data_workspace/v2/tests/bdd/conftest.py @@ -366,32 +366,8 @@ def check_rows(client, parse_table, unpage_data, table_name, rows): assert actual_data == expected_data -@given(parsers.parse("endpoint exists for exporting `{table_name}`")) +@given(parsers.parse("LITE exports `{table_name}` data to DW")) def given_endpoint_exists(client, table_name): metadata_url = reverse("data_workspace:v2:table-metadata") response = client.get(metadata_url) assert table_name in [t["table_name"] for t in response.json()["tables"]] - - -@then(parsers.parse("the `{table_name}` table should contain the following rows:{rows}")) -def check_rows(client, parse_table, unpage_data, table_name, rows): - metadata_url = reverse("data_workspace:v2:table-metadata") - response = client.get(metadata_url) - tables_metadata = response.json()["tables"] - for m in tables_metadata: - if m["table_name"] == table_name: - table_metadata = m - break - else: - pytest.fail(f"No table called {table_name} found") - - response = client.get(table_metadata["endpoint"]) - assert response.status_code == status.HTTP_200_OK - actual_data = response.data - - parsed_rows = parse_table(rows) - keys = parsed_rows[0] - expected_data = [] - for row in parsed_rows[1:]: - expected_data.append({key: value for key, value in zip(keys, row)}) - assert actual_data == expected_data diff --git a/api/data_workspace/v2/tests/bdd/scenarios/units.feature b/api/data_workspace/v2/tests/bdd/scenarios/units.feature index 303816a1d9..7a54ce948a 100644 --- a/api/data_workspace/v2/tests/bdd/scenarios/units.feature +++ b/api/data_workspace/v2/tests/bdd/scenarios/units.feature @@ -2,8 +2,8 @@ Feature: units Table Scenario: Units endpoint - Given endpoint exists for exporting `units` - Then the `units` table should contain the following rows: + Given LITE exports `units` data to DW + Then the `units` table has the following rows: | code | description | | NAR | Items | | TON | Tonnes | diff --git a/api/data_workspace/v2/views.py b/api/data_workspace/v2/views.py index ce6ec18f02..ec403eebdc 100644 --- a/api/data_workspace/v2/views.py +++ b/api/data_workspace/v2/views.py @@ -147,22 +147,9 @@ class DataWorkspace: table_name = "applications" -class UnitViewSet(viewsets.ViewSet): - authentication_classes = (DataWorkspaceOnlyAuthentication,) - pagination_class = DisableableLimitOffsetPagination - renderer_classes = tuple(api_settings.DEFAULT_RENDERER_CLASSES) + (PaginatedCSVRenderer,) - - def list(self, request): - units = [{"code": code, "description": description} for code, description in Units.choices] - return Response(UnitSerializer(units, many=True).data) - - def retrieve(self, request, pk): - units = dict(Units.choices) - try: - description = units[pk] - except KeyError: - raise Http404() - return Response(UnitSerializer({"code": pk, "description": description}).data) +class UnitViewSet(BaseViewSet): + serializer_class = UnitSerializer + queryset = [{"code": code, "description": description} for code, description in Units.choices] class DataWorkspace: table_name = "units" From 5d9e50ee1376678730ec4886a268ab90d52637ad Mon Sep 17 00:00:00 2001 From: Arun Siluvery Date: Fri, 29 Nov 2024 08:35:19 +0000 Subject: [PATCH 16/22] Remove unused imports --- api/data_workspace/v2/views.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/api/data_workspace/v2/views.py b/api/data_workspace/v2/views.py index ec403eebdc..472ca5682c 100644 --- a/api/data_workspace/v2/views.py +++ b/api/data_workspace/v2/views.py @@ -2,7 +2,6 @@ from rest_framework import viewsets from rest_framework.pagination import LimitOffsetPagination -from rest_framework.response import Response from rest_framework.settings import api_settings from rest_framework_csv.renderers import PaginatedCSVRenderer @@ -17,7 +16,6 @@ Min, ) from django.db.models.lookups import GreaterThan -from django.http import Http404 from api.applications.models import ( GoodOnApplication, From f80613b99374d01819731f4517024c45bc4c59b0 Mon Sep 17 00:00:00 2001 From: Kevin Carrogan Date: Mon, 25 Nov 2024 19:22:08 +0000 Subject: [PATCH 17/22] Add denial reasons to licence decision --- .../0069_licencedecision_denial_reasons.py | 19 +++++++++++++++ .../migrations/0073_merge_20241126_1335.py | 13 +++++++++++ api/cases/models.py | 14 ++++++++++- api/cases/tests/test_finalise_advice.py | 23 +++++++++++++++---- .../v2/tests/bdd/licences/conftest.py | 12 ++++++++-- .../bdd/licences/test_licence_decisions.py | 12 ++++++---- 6 files changed, 81 insertions(+), 12 deletions(-) create mode 100644 api/cases/migrations/0069_licencedecision_denial_reasons.py create mode 100644 api/cases/migrations/0073_merge_20241126_1335.py diff --git a/api/cases/migrations/0069_licencedecision_denial_reasons.py b/api/cases/migrations/0069_licencedecision_denial_reasons.py new file mode 100644 index 0000000000..b4ea6bebe8 --- /dev/null +++ b/api/cases/migrations/0069_licencedecision_denial_reasons.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.16 on 2024-11-25 19:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("denial_reasons", "0006_populate_uuid_field"), + ("cases", "0068_populate_licence_decisions"), + ] + + operations = [ + migrations.AddField( + model_name="licencedecision", + name="denial_reasons", + field=models.ManyToManyField(to="denial_reasons.denialreason"), + ), + ] diff --git a/api/cases/migrations/0073_merge_20241126_1335.py b/api/cases/migrations/0073_merge_20241126_1335.py new file mode 100644 index 0000000000..ef8d638682 --- /dev/null +++ b/api/cases/migrations/0073_merge_20241126_1335.py @@ -0,0 +1,13 @@ +# Generated by Django 4.2.16 on 2024-11-26 13:35 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("cases", "0069_licencedecision_denial_reasons"), + ("cases", "0072_alter_decision_populate_issued_on_appeal"), + ] + + operations = [] diff --git a/api/cases/models.py b/api/cases/models.py index 309e410757..edf390ec84 100644 --- a/api/cases/models.py +++ b/api/cases/models.py @@ -410,12 +410,23 @@ def finalise(self, request, decisions): if previous_licence_decision.decision == current_decision: previous_decision = previous_licence_decision - LicenceDecision.objects.create( + licence_decision = LicenceDecision.objects.create( case=self, decision=current_decision, licence=licence, previous_decision=previous_decision, ) + if advice_type == AdviceType.REFUSE: + denial_reasons = ( + self.advice.filter( + level=AdviceLevel.FINAL, + type=AdviceType.REFUSE, + ) + .only("denial_reasons__id") + .distinct() + .values_list("denial_reasons__id", flat=True) + ) + licence_decision.denial_reasons.set(denial_reasons) licence_reference = licence.reference_code if licence and advice_type == AdviceType.APPROVE else "" audit_trail_service.create( @@ -846,6 +857,7 @@ class LicenceDecision(TimestampableModel): licence = models.ForeignKey( "licences.Licence", on_delete=models.DO_NOTHING, related_name="licence_decisions", null=True, blank=True ) + denial_reasons = models.ManyToManyField(DenialReason) excluded_from_statistics_reason = models.TextField(default=None, blank=True, null=True) previous_decision = models.ForeignKey( "self", related_name="previous_decisions", default=None, null=True, on_delete=models.DO_NOTHING diff --git a/api/cases/tests/test_finalise_advice.py b/api/cases/tests/test_finalise_advice.py index c342705acc..c2335233ea 100644 --- a/api/cases/tests/test_finalise_advice.py +++ b/api/cases/tests/test_finalise_advice.py @@ -6,8 +6,12 @@ from api.audit_trail.enums import AuditType from api.audit_trail.models import Audit from api.audit_trail import service as audit_trail_service -from api.cases.enums import AdviceType, CaseTypeEnum, LicenceDecisionType -from api.cases.models import LicenceDecision +from api.cases.enums import ( + AdviceLevel, + AdviceType, + CaseTypeEnum, + LicenceDecisionType, +) from api.cases.tests.factories import FinalAdviceFactory from api.cases.libraries.get_case import get_case from api.cases.generated_documents.models import GeneratedCaseDocument @@ -65,8 +69,19 @@ def test_refuse_standard_application_success(self, send_exporter_notifications_f payload__decision=AdviceType.REFUSE, ).exists() ) - self.assertTrue( - LicenceDecision.objects.filter(case=self.application, decision=LicenceDecisionType.REFUSED).exists() + + licence_decision = case.licence_decisions.get(decision=LicenceDecisionType.REFUSED) + + advice = case.advice.get( + level=AdviceLevel.FINAL, + type=AdviceType.REFUSE, + ) + + self.assertTrue(licence_decision.denial_reasons.exists()) + + self.assertQuerySetEqual( + licence_decision.denial_reasons.all(), + advice.denial_reasons.all(), ) @mock.patch("api.cases.notify.notify_exporter_licence_refused") diff --git a/api/data_workspace/v2/tests/bdd/licences/conftest.py b/api/data_workspace/v2/tests/bdd/licences/conftest.py index 5c666f5e03..9396dd8bae 100644 --- a/api/data_workspace/v2/tests/bdd/licences/conftest.py +++ b/api/data_workspace/v2/tests/bdd/licences/conftest.py @@ -7,7 +7,10 @@ PartyOnApplicationFactory, DraftStandardApplicationFactory, ) -from api.cases.enums import AdviceType +from api.cases.enums import ( + AdviceLevel, + AdviceType, +) from api.cases.tests.factories import FinalAdviceFactory from api.goods.tests.factories import GoodFactory from api.licences.enums import LicenceStatus @@ -75,7 +78,12 @@ def standard_case_with_final_advice(lu_case_officer): @pytest.fixture() def standard_case_with_refused_advice(lu_case_officer, standard_case_with_final_advice): - standard_case_with_final_advice.advice.update(type=AdviceType.REFUSE) + final_advice = standard_case_with_final_advice.advice.filter(level=AdviceLevel.FINAL) + for advice in final_advice: + advice.type = AdviceType.REFUSE + advice.text = "refusing licence" + advice.denial_reasons.set(["1a", "1b", "1c"]) + advice.save() return standard_case_with_final_advice diff --git a/api/data_workspace/v2/tests/bdd/licences/test_licence_decisions.py b/api/data_workspace/v2/tests/bdd/licences/test_licence_decisions.py index e03491faf0..adf8f1c9ad 100644 --- a/api/data_workspace/v2/tests/bdd/licences/test_licence_decisions.py +++ b/api/data_workspace/v2/tests/bdd/licences/test_licence_decisions.py @@ -187,7 +187,7 @@ def case_officer_refuses_licence(client, gov_headers, case_with_refused_advice): kwargs={"pk": str(case_with_refused_advice.pk)}, ) response = client.put(url, {}, content_type="application/json", **gov_headers) - assert response.status_code == 201 + assert response.status_code == 201, response.content case_with_refused_advice.refresh_from_db() assert case_with_refused_advice.status == CaseStatus.objects.get(status=CaseStatusEnum.FINALISED) @@ -297,10 +297,12 @@ def case_ready_to_be_finalised_after_refusing_licence(client, lu_case_officer_he case_with_final_advice = issued_licence.case assert case_with_final_advice.status == CaseStatus.objects.get(status=CaseStatusEnum.FINALISED) - case_with_final_advice.advice.filter(level=AdviceLevel.FINAL).update( - type=AdviceType.REFUSE, - text="refusing licence", - ) + final_advice = case_with_final_advice.advice.filter(level=AdviceLevel.FINAL) + for advice in final_advice: + advice.type = AdviceType.REFUSE + advice.text = "refusing licence" + advice.denial_reasons.set(["1a", "1b", "1c"]) + advice.save() case_with_final_advice = case_reopen_prepare_to_finalise(client, lu_case_officer_headers, case_with_final_advice) From d952e002d13525508d9ceb478199f4201774118e Mon Sep 17 00:00:00 2001 From: Kevin Carrogan Date: Mon, 25 Nov 2024 19:25:13 +0000 Subject: [PATCH 18/22] Stop passing through request to case model in finalise --- api/cases/models.py | 10 +++++----- api/cases/views/views.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/api/cases/models.py b/api/cases/models.py index edf390ec84..48765c8c05 100644 --- a/api/cases/models.py +++ b/api/cases/models.py @@ -342,7 +342,7 @@ def no_licence_required(self): notify_exporter_no_licence_required(self) @transaction.atomic - def finalise(self, request, decisions): + def finalise(self, user, decisions, note): from api.audit_trail import service as audit_trail_service from api.cases.libraries.finalise import remove_flags_on_finalisation, remove_flags_from_audit_trail from api.licences.models import Licence @@ -361,7 +361,7 @@ def finalise(self, request, decisions): if Licence.objects.filter(case=self).count() > 1: audit_trail_service.create( - actor=request.user, + actor=user, verb=AuditType.REINSTATED_APPLICATION, target=self, payload={ @@ -376,12 +376,12 @@ def finalise(self, request, decisions): self.save() audit_trail_service.create( - actor=request.user, + actor=user, verb=AuditType.UPDATED_STATUS, target=self, payload={ "status": {"new": self.status.status, "old": old_status}, - "additional_text": request.data.get("note"), + "additional_text": note, }, ) logging.info("Case is now finalised") @@ -430,7 +430,7 @@ def finalise(self, request, decisions): licence_reference = licence.reference_code if licence and advice_type == AdviceType.APPROVE else "" audit_trail_service.create( - actor=request.user, + actor=user, verb=AuditType.CREATED_FINAL_RECOMMENDATION, target=self, payload={ diff --git a/api/cases/views/views.py b/api/cases/views/views.py index c1f4a1d7bb..f295fbbcb6 100644 --- a/api/cases/views/views.py +++ b/api/cases/views/views.py @@ -894,7 +894,7 @@ def put(self, request, pk): ) # finalises case, grants licence and publishes decision documents - licence_id = case.finalise(request, required_decisions) + licence_id = case.finalise(request.user, required_decisions, request.data.get("note")) return JsonResponse({"case": pk, "licence": licence_id}, status=status.HTTP_201_CREATED) From 9b4a53759ef5f33f140d7dff468fff9ba633dd81 Mon Sep 17 00:00:00 2001 From: Kevin Carrogan Date: Thu, 28 Nov 2024 11:07:47 +0000 Subject: [PATCH 19/22] Remove merge migration --- ...ns.py => 0073_licencedecision_denial_reasons.py} | 4 ++-- api/cases/migrations/0073_merge_20241126_1335.py | 13 ------------- 2 files changed, 2 insertions(+), 15 deletions(-) rename api/cases/migrations/{0069_licencedecision_denial_reasons.py => 0073_licencedecision_denial_reasons.py} (76%) delete mode 100644 api/cases/migrations/0073_merge_20241126_1335.py diff --git a/api/cases/migrations/0069_licencedecision_denial_reasons.py b/api/cases/migrations/0073_licencedecision_denial_reasons.py similarity index 76% rename from api/cases/migrations/0069_licencedecision_denial_reasons.py rename to api/cases/migrations/0073_licencedecision_denial_reasons.py index b4ea6bebe8..f04f1800dd 100644 --- a/api/cases/migrations/0069_licencedecision_denial_reasons.py +++ b/api/cases/migrations/0073_licencedecision_denial_reasons.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.16 on 2024-11-25 19:13 +# Generated by Django 4.2.16 on 2024-11-28 11:07 from django.db import migrations, models @@ -7,7 +7,7 @@ class Migration(migrations.Migration): dependencies = [ ("denial_reasons", "0006_populate_uuid_field"), - ("cases", "0068_populate_licence_decisions"), + ("cases", "0072_alter_decision_populate_issued_on_appeal"), ] operations = [ diff --git a/api/cases/migrations/0073_merge_20241126_1335.py b/api/cases/migrations/0073_merge_20241126_1335.py deleted file mode 100644 index ef8d638682..0000000000 --- a/api/cases/migrations/0073_merge_20241126_1335.py +++ /dev/null @@ -1,13 +0,0 @@ -# Generated by Django 4.2.16 on 2024-11-26 13:35 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ("cases", "0069_licencedecision_denial_reasons"), - ("cases", "0072_alter_decision_populate_issued_on_appeal"), - ] - - operations = [] From 18ec33ae2fc3d2b4058747660cfc49e5b6c9358b Mon Sep 17 00:00:00 2001 From: Kevin Carrogan Date: Mon, 25 Nov 2024 19:50:21 +0000 Subject: [PATCH 20/22] Add licence refusal criteria endpoint --- api/data_workspace/v2/serializers.py | 10 +++ api/data_workspace/v2/tests/bdd/conftest.py | 68 ++++++++++++++-- .../licence_refusal_criteria.feature | 28 +++++++ .../v2/tests/bdd/test_applications.py | 77 ++----------------- .../bdd/test_licence_refusal_criteria.py | 39 ++++++++++ api/data_workspace/v2/urls.py | 1 + api/data_workspace/v2/views.py | 12 +++ pii-secret-exclude.txt | 1 + 8 files changed, 159 insertions(+), 77 deletions(-) create mode 100644 api/data_workspace/v2/tests/bdd/scenarios/licence_refusal_criteria.feature create mode 100644 api/data_workspace/v2/tests/bdd/test_licence_refusal_criteria.py diff --git a/api/data_workspace/v2/serializers.py b/api/data_workspace/v2/serializers.py index 4afd09bd3c..eda988064a 100644 --- a/api/data_workspace/v2/serializers.py +++ b/api/data_workspace/v2/serializers.py @@ -14,6 +14,7 @@ from api.licences.models import GoodOnLicence from api.staticdata.control_list_entries.models import ControlListEntry from api.staticdata.countries.models import Country +from api.staticdata.denial_reasons.models import DenialReason from api.staticdata.report_summaries.models import ReportSummary @@ -163,3 +164,12 @@ class Meta: "good_id", "rating", ) + + +class LicenceRefusalCriteriaSerializer(serializers.ModelSerializer): + criteria = serializers.CharField(source="display_value") + licence_decision_id = serializers.UUIDField(source="licence_decisions_id") + + class Meta: + model = DenialReason + fields = ("criteria", "licence_decision_id") diff --git a/api/data_workspace/v2/tests/bdd/conftest.py b/api/data_workspace/v2/tests/bdd/conftest.py index 92b824deaa..8c38d25e1f 100644 --- a/api/data_workspace/v2/tests/bdd/conftest.py +++ b/api/data_workspace/v2/tests/bdd/conftest.py @@ -27,21 +27,21 @@ ) from api.cases.enums import ( AdviceType, + CaseTypeEnum, ) -from api.cases.tests.factories import FinalAdviceFactory -from api.cases.enums import CaseTypeEnum from api.cases.models import CaseType +from api.cases.tests.factories import FinalAdviceFactory from api.core.constants import ( ExporterPermissions, GovPermissions, Roles, ) -from api.goods.tests.factories import GoodFactory -from api.flags.enums import SystemFlags from api.documents.libraries.s3_operations import init_s3_client +from api.flags.enums import SystemFlags +from api.goods.tests.factories import GoodFactory from api.letter_templates.models import LetterTemplate -from api.parties.tests.factories import PartyDocumentFactory from api.organisations.tests.factories import OrganisationFactory +from api.parties.tests.factories import PartyDocumentFactory from api.staticdata.letter_layouts.models import LetterLayout from api.staticdata.report_summaries.models import ( ReportSummaryPrefix, @@ -542,3 +542,61 @@ def _issue_licence(application): assert response.status_code == 201 return _issue_licence + + +@pytest.fixture() +def refuse_application( + api_client, + lu_case_officer, + siel_refusal_template, + gov_headers, +): + def _refuse_application(application, denial_reasons=None): + if not denial_reasons: + denial_reasons = ["1a", "1b", "1c"] + + data = {"action": AdviceType.REFUSE} + for good_on_app in application.goods.all(): + good_on_app.quantity = 100 + good_on_app.value = 10000 + good_on_app.save() + data[f"quantity-{good_on_app.id}"] = str(good_on_app.quantity) + data[f"value-{good_on_app.id}"] = str(good_on_app.value) + FinalAdviceFactory( + user=lu_case_officer, + case=application, + good=good_on_app.good, + type=AdviceType.REFUSE, + denial_reasons=denial_reasons, + ) + + application.flags.remove(SystemFlags.ENFORCEMENT_CHECK_REQUIRED) + + url = reverse("applications:finalise", kwargs={"pk": application.pk}) + response = api_client.put(url, data=data, **gov_headers) + assert response.status_code == 200, response.content + response = response.json() + + data = { + "template": str(siel_refusal_template.id), + "text": "", + "visible_to_exporter": False, + "advice_type": AdviceType.REFUSE, + } + url = reverse( + "cases:generated_documents:generated_documents", + kwargs={"pk": str(application.pk)}, + ) + response = api_client.post(url, data=data, **gov_headers) + assert response.status_code == 201, response.content + + url = reverse( + "cases:finalise", + kwargs={"pk": str(application.pk)}, + ) + response = api_client.put(url, data={}, **gov_headers) + assert response.status_code == 201, response.content + + application.refresh_from_db() + + return _refuse_application diff --git a/api/data_workspace/v2/tests/bdd/scenarios/licence_refusal_criteria.feature b/api/data_workspace/v2/tests/bdd/scenarios/licence_refusal_criteria.feature new file mode 100644 index 0000000000..a57c6aeab5 --- /dev/null +++ b/api/data_workspace/v2/tests/bdd/scenarios/licence_refusal_criteria.feature @@ -0,0 +1,28 @@ +@db +Feature: licence_refusal_criteria Table + +Scenario: Draft application + Given a draft standard application + Then the `licence_refusal_criteria` table is empty + +Scenario: Submit an application + Given a draft standard application + When the application is submitted + Then the `licence_refusal_criteria` table is empty + +Scenario: Issuing an application + Given a draft standard application + When the application is submitted + And the application is issued + Then the `licence_refusal_criteria` table is empty + +Scenario: Refusing an application + Given a draft standard application + When the application is submitted + And the application is refused with criteria: + | 1 | + | 2c | + Then the `licence_refusal_criteria` table has the following rows: + | licence_decision_id | criteria | + | 03fb08eb-1564-4b68-9336-3ca8906543f9 | 1 | + | 03fb08eb-1564-4b68-9336-3ca8906543f9 | 2c | diff --git a/api/data_workspace/v2/tests/bdd/test_applications.py b/api/data_workspace/v2/tests/bdd/test_applications.py index fc2f6fef25..f96d994753 100644 --- a/api/data_workspace/v2/tests/bdd/test_applications.py +++ b/api/data_workspace/v2/tests/bdd/test_applications.py @@ -24,8 +24,6 @@ AdviceLevel, AdviceType, ) -from api.cases.tests.factories import FinalAdviceFactory -from api.flags.enums import SystemFlags from api.licences.enums import LicenceStatus from api.parties.tests.factories import ( PartyDocumentFactory, @@ -115,21 +113,6 @@ def _exporter_change_status(application, status): return _exporter_change_status -@given("a draft standard application", target_fixture="draft_standard_application") -def given_draft_standard_application(organisation): - application = DraftStandardApplicationFactory( - organisation=organisation, - ) - - PartyDocumentFactory( - party=application.end_user.party, - s3_key="party-document", - safe=True, - ) - - return application - - @given( parsers.parse("a draft temporary standard application with attributes:{attributes}"), target_fixture="draft_standard_application", @@ -177,14 +160,6 @@ def given_a_good_is_onward_incorporated(draft_standard_application): ) -@when( - "the application is submitted", - target_fixture="submitted_standard_application", -) -def when_the_application_is_submitted(submit_application, draft_standard_application): - return submit_application(draft_standard_application) - - @when( parsers.parse("the application is submitted at {submission_time}"), target_fixture="submitted_standard_application", @@ -213,61 +188,19 @@ def when_the_application_is_issued_at( @when(parsers.parse("the application is refused at {timestamp}"), target_fixture="refused_application") def when_the_application_is_refused_at( - api_client, - lu_case_officer, - siel_refusal_template, - gov_headers, submitted_standard_application, + refuse_application, timestamp, ): run_processing_time_task(submitted_standard_application.submitted_at, timestamp) with freeze_time(timestamp): - data = {"action": AdviceType.REFUSE} - for good_on_app in submitted_standard_application.goods.all(): - good_on_app.quantity = 100 - good_on_app.value = 10000 - good_on_app.save() - data[f"quantity-{good_on_app.id}"] = str(good_on_app.quantity) - data[f"value-{good_on_app.id}"] = str(good_on_app.value) - FinalAdviceFactory( - user=lu_case_officer, - case=submitted_standard_application, - good=good_on_app.good, - type=AdviceType.REFUSE, - ) - - submitted_standard_application.flags.remove(SystemFlags.ENFORCEMENT_CHECK_REQUIRED) - - url = reverse("applications:finalise", kwargs={"pk": submitted_standard_application.pk}) - response = api_client.put(url, data=data, **gov_headers) - assert response.status_code == 200, response.content - response = response.json() - - data = { - "template": str(siel_refusal_template.id), - "text": "", - "visible_to_exporter": False, - "advice_type": AdviceType.REFUSE, - } - url = reverse( - "cases:generated_documents:generated_documents", - kwargs={"pk": str(submitted_standard_application.pk)}, - ) - response = api_client.post(url, data=data, **gov_headers) - assert response.status_code == 201, response.content + refuse_application(submitted_standard_application) - url = reverse( - "cases:finalise", - kwargs={"pk": str(submitted_standard_application.pk)}, - ) - response = api_client.put(url, data={}, **gov_headers) - assert response.status_code == 201, response.content - - submitted_standard_application.refresh_from_db() - refused_application = submitted_standard_application + submitted_standard_application.refresh_from_db() + refused_application = submitted_standard_application - return refused_application + return refused_application @when(parsers.parse("the application is appealed at {timestamp}"), target_fixture="appealed_application") diff --git a/api/data_workspace/v2/tests/bdd/test_licence_refusal_criteria.py b/api/data_workspace/v2/tests/bdd/test_licence_refusal_criteria.py new file mode 100644 index 0000000000..6637981aa1 --- /dev/null +++ b/api/data_workspace/v2/tests/bdd/test_licence_refusal_criteria.py @@ -0,0 +1,39 @@ +import uuid + +from pytest_bdd import ( + parsers, + scenarios, + when, +) + + +scenarios("./scenarios/licence_refusal_criteria.feature") + + +@when(parsers.parse("the application is refused with criteria:{criteria}")) +def when_the_application_is_refused_with_criteria( + submitted_standard_application, refuse_application, parse_table, criteria +): + criteria = [c[0] for c in parse_table(criteria)] + refuse_application(submitted_standard_application, criteria) + refused_application = submitted_standard_application + licence_decision = refused_application.licence_decisions.get() + old_pk = licence_decision.pk + licence_decision.pk = uuid.UUID("03fb08eb-1564-4b68-9336-3ca8906543f9") # /PS-IGNORE + licence_decision.save() + licence_decision.denial_reasons.through.objects.filter(licencedecision_id=old_pk).update( + licencedecision_id=licence_decision.pk + ) + + +@when("the application is issued") +def when_the_application_is_issued( + issue_licence, + submitted_standard_application, +): + issue_licence(submitted_standard_application) + + submitted_standard_application.refresh_from_db() + issued_application = submitted_standard_application + + return issued_application diff --git a/api/data_workspace/v2/urls.py b/api/data_workspace/v2/urls.py index 39dafcc438..08121a86ad 100644 --- a/api/data_workspace/v2/urls.py +++ b/api/data_workspace/v2/urls.py @@ -13,3 +13,4 @@ router_v2.register(views.UnitViewSet) router_v2.register(views.FootnoteViewSet) router_v2.register(views.AssessmentViewSet) +router_v2.register(views.LicenceRefusalCriteriaViewSet) diff --git a/api/data_workspace/v2/views.py b/api/data_workspace/v2/views.py index 6d75cf7825..b798a6d3a5 100644 --- a/api/data_workspace/v2/views.py +++ b/api/data_workspace/v2/views.py @@ -38,12 +38,14 @@ LicenceDecisionSerializer, UnitSerializer, GoodOnLicenceSerializer, + LicenceRefusalCriteriaSerializer, ) from api.licences.enums import LicenceStatus from api.licences.models import GoodOnLicence from api.staticdata.control_list_entries.models import ControlListEntry from api.staticdata.countries.models import Country from api.staticdata.report_summaries.models import ReportSummary +from api.staticdata.denial_reasons.models import DenialReason from api.staticdata.statuses.enums import CaseStatusEnum from api.staticdata.units.enums import Units @@ -197,3 +199,13 @@ def get_queryset(self): class DataWorkspace: table_name = "goods_ratings" + + +class LicenceRefusalCriteriaViewSet(BaseViewSet): + serializer_class = LicenceRefusalCriteriaSerializer + queryset = DenialReason.objects.exclude(licencedecision__denial_reasons__isnull=True).annotate( + licence_decisions_id=F("licencedecision__id") + ) + + class DataWorkspace: + table_name = "licence_refusal_criteria" diff --git a/pii-secret-exclude.txt b/pii-secret-exclude.txt index 03d87ef58b..98c9152fc0 100644 --- a/pii-secret-exclude.txt +++ b/pii-secret-exclude.txt @@ -118,3 +118,4 @@ api/cases/generated_documents/tests/data/dummy.pdf api/cases/generated_documents/tests/data/signed.pdf api/data_workspace/v2/tests/bdd/scenarios/applications.feature api/data_workspace/v2/tests/bdd/scenarios/goods_on_licences.feature +api/data_workspace/v2/tests/bdd/scenarios/licence_refusal_criteria.feature From b5224e1dd2535f5327f41321fe9f5fe90e68069f Mon Sep 17 00:00:00 2001 From: Kevin Carrogan Date: Mon, 2 Dec 2024 10:34:52 +0000 Subject: [PATCH 21/22] fixup --- api/data_workspace/v2/serializers.py | 2 +- api/data_workspace/v2/views.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/data_workspace/v2/serializers.py b/api/data_workspace/v2/serializers.py index eda988064a..f1aed13de4 100644 --- a/api/data_workspace/v2/serializers.py +++ b/api/data_workspace/v2/serializers.py @@ -168,7 +168,7 @@ class Meta: class LicenceRefusalCriteriaSerializer(serializers.ModelSerializer): criteria = serializers.CharField(source="display_value") - licence_decision_id = serializers.UUIDField(source="licence_decisions_id") + licence_decision_id = serializers.UUIDField(source="licence_decision_id") class Meta: model = DenialReason diff --git a/api/data_workspace/v2/views.py b/api/data_workspace/v2/views.py index b798a6d3a5..d1ffe94ea7 100644 --- a/api/data_workspace/v2/views.py +++ b/api/data_workspace/v2/views.py @@ -204,7 +204,7 @@ class DataWorkspace: class LicenceRefusalCriteriaViewSet(BaseViewSet): serializer_class = LicenceRefusalCriteriaSerializer queryset = DenialReason.objects.exclude(licencedecision__denial_reasons__isnull=True).annotate( - licence_decisions_id=F("licencedecision__id") + licence_decision_id=F("licencedecision__id") ) class DataWorkspace: From fabe60d56c68250504c95fd8acd7c3e952398a99 Mon Sep 17 00:00:00 2001 From: Kevin Carrogan Date: Mon, 2 Dec 2024 10:48:20 +0000 Subject: [PATCH 22/22] fixup --- api/data_workspace/v2/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/data_workspace/v2/serializers.py b/api/data_workspace/v2/serializers.py index f1aed13de4..6806c98f15 100644 --- a/api/data_workspace/v2/serializers.py +++ b/api/data_workspace/v2/serializers.py @@ -168,7 +168,7 @@ class Meta: class LicenceRefusalCriteriaSerializer(serializers.ModelSerializer): criteria = serializers.CharField(source="display_value") - licence_decision_id = serializers.UUIDField(source="licence_decision_id") + licence_decision_id = serializers.UUIDField() class Meta: model = DenialReason