From 09e4651f82cd86ffa4b25d013e2715e6ed1db137 Mon Sep 17 00:00:00 2001 From: Kevin Carrogan Date: Mon, 25 Nov 2024 15:16:55 +0000 Subject: [PATCH 1/4] 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 2/4] 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 3/4] 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 78f96e340702e47c8b21bbce1b6a23c18dd797ae Mon Sep 17 00:00:00 2001 From: Arun Siluvery Date: Thu, 28 Nov 2024 16:23:05 +0000 Subject: [PATCH 4/4] 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"