diff --git a/.circleci/config.yml b/.circleci/config.yml index 73eac67d2a..ff1e6e59ba 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -114,7 +114,7 @@ jobs: - run: name: Run tests command: | - pipenv run pytest --circleci-parallelize --cov=. --cov-report xml --cov-config=.coveragerc --ignore lite_routing -k "not seeding and not elasticsearch and not performance and not migration" + pipenv run pytest --circleci-parallelize --cov=. --cov-report xml --cov-config=.coveragerc --ignore lite_routing --ignore api/anonymised_db_dumps -k "not seeding and not elasticsearch and not performance and not migration and not db_anonymiser" - upload_code_coverage: alias: tests @@ -136,6 +136,24 @@ jobs: - upload_code_coverage: alias: seeding_tests + anonymised_db_dump_tests: + docker: + - <<: *image_python + - <<: *image_postgres + - <<: *image_elasticsearch + working_directory: ~/lite-api + environment: + <<: *common_env_vars + LITE_API_ENABLE_ES: True + steps: + - setup + - run: + name: Run anonymised DB dump tests + command: | + pipenv run pytest --cov=. --cov-report xml --cov-config=.coveragerc api/anonymised_db_dumps + - upload_code_coverage: + alias: anonymised_db_dumps + migration_tests: docker: - <<: *image_python @@ -400,3 +418,4 @@ workflows: - lite_routing_tests - check-lite-routing-sha - e2e_tests + - anonymised_db_dump_tests diff --git a/.coveragerc b/.coveragerc index fe7ee3c453..2ddad5a0a1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -16,6 +16,7 @@ omit = ./api/staticdata/management/* ./runtime.txt ./lite_routing/management/commands/generate_rules_docs.py + ./django_db_anonymiser/* branch = True [report] diff --git a/.gitmodules b/.gitmodules index 5063452fa3..ab0e8c0676 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,3 +6,6 @@ path = lite_routing url = git@github.com:uktrade/lite-routing.git branch = main +[submodule "django_db_anonymiser"] + path = django_db_anonymiser + url = git@github.com:uktrade/django-db-anonymiser.git diff --git a/.prospector.yaml b/.prospector.yaml index 03398b4eb0..3304d477e1 100644 --- a/.prospector.yaml +++ b/.prospector.yaml @@ -1,5 +1,6 @@ ignore-paths: - separatedvaluesfield + - django_db_anonymiser uses: - django diff --git a/Dockerfile b/Dockerfile index 981c01c63f..5122a39ecf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,10 @@ RUN apt-get install -y libpq-dev gcc curl \ python3-cffi libcairo2 libpango-1.0-0 libpangocairo-1.0-0 libgdk-pixbuf2.0-0 \ libffi-dev shared-mime-info swig git imagemagick poppler-utils openssl libsqlite3-dev RUN curl https://pyenv.run | bash +RUN curl --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add +RUN sh -c 'echo "deb https://apt.postgresql.org/pub/repos/apt jammy-pgdg main" > /etc/apt/sources.list.d/pgdg.list' +RUN apt-get update +RUN apt-get install -y postgresql-client-12 ENV HOME /root ENV PYENV_ROOT $HOME/.pyenv ENV PATH $PYENV_ROOT/bin:$PATH diff --git a/Pipfile b/Pipfile index 55695998d4..6b26d9fd2c 100644 --- a/Pipfile +++ b/Pipfile @@ -72,6 +72,7 @@ django-test-migrations = "~=1.2.0" django-silk = "~=5.0.3" django = "~=4.2.10" django-queryable-properties = "~=1.9.1" +database-sanitizer = ">=1.1.0" [requires] python_version = "3.8" diff --git a/Pipfile.lock b/Pipfile.lock index 862383b0f4..a68dab2413 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "12d67e9acb9f3d93c6c6dbd810ce146c572181edcfa40c57871d1f32836c6cbd" + "sha256": "3c66c92729feb5352fb58a417bd4a4ddec7e29bb4041674f64d0cde084e825c5" }, "pipfile-spec": 6, "requires": { @@ -371,6 +371,14 @@ "markers": "python_version >= '3.7'", "version": "==0.7.0" }, + "database-sanitizer": { + "hashes": [ + "sha256:14d93f6eefcb08a4a96d5a075ba6e5a5e3e3ac2b8c57374114b6be889b5ea97a", + "sha256:f717ed4e9f64b193f580d0d744c96ec2f95c0e853b69b5bccdb85e5807e9bbca" + ], + "index": "pypi", + "version": "==1.1.0" + }, "dataclasses": { "hashes": [ "sha256:454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f", @@ -1203,6 +1211,63 @@ ], "version": "==2024.1" }, + "pyyaml": { + "hashes": [ + "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", + "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", + "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", + "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", + "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", + "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", + "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", + "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", + "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", + "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", + "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", + "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", + "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", + "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", + "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", + "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", + "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", + "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", + "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", + "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", + "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", + "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", + "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", + "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", + "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", + "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", + "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", + "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", + "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", + "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef", + "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", + "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", + "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", + "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", + "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", + "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", + "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", + "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", + "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", + "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", + "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", + "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", + "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", + "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", + "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", + "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", + "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", + "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", + "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", + "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", + "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" + ], + "markers": "python_version >= '3.6'", + "version": "==6.0.1" + }, "redis": { "hashes": [ "sha256:68226f7ede928db8302f29ab088a157f41061fa946b7ae865452b6d7838bbffb", @@ -1255,11 +1320,11 @@ }, "setuptools": { "hashes": [ - "sha256:850894c4195f09c4ed30dba56213bf7c3f21d86ed6bdaafb5df5972593bfc401", - "sha256:c054629b81b946d63a9c6e732bc8b2513a7c3ea645f11d0139a2191d735c60c6" + "sha256:02fa291a0471b3a18b2b2481ed902af520c69e8ae0919c13da936542754b4c56", + "sha256:5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8" ], "markers": "python_version >= '3.8'", - "version": "==69.1.0" + "version": "==69.1.1" }, "six": { "hashes": [ @@ -2432,11 +2497,11 @@ }, "setuptools": { "hashes": [ - "sha256:850894c4195f09c4ed30dba56213bf7c3f21d86ed6bdaafb5df5972593bfc401", - "sha256:c054629b81b946d63a9c6e732bc8b2513a7c3ea645f11d0139a2191d735c60c6" + "sha256:02fa291a0471b3a18b2b2481ed902af520c69e8ae0919c13da936542754b4c56", + "sha256:5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8" ], "markers": "python_version >= '3.8'", - "version": "==69.1.0" + "version": "==69.1.1" }, "six": { "hashes": [ @@ -2463,11 +2528,11 @@ }, "stevedore": { "hashes": [ - "sha256:8cc040628f3cea5d7128f2e76cf486b2251a4e543c7b938f58d9a377f6694a2d", - "sha256:a54534acf9b89bc7ed264807013b505bf07f74dbe4bcfa37d32bd063870b087c" + "sha256:1c15d95766ca0569cad14cb6272d4d31dae66b011a929d7c18219c176ea1b5c9", + "sha256:46b93ca40e1114cea93d738a6c1e365396981bb6bb78c27045b7587c9473544d" ], "markers": "python_version >= '3.8'", - "version": "==5.1.0" + "version": "==5.2.0" }, "tomli": { "hashes": [ diff --git a/api/anonymised_db_dumps/__init__.py b/api/anonymised_db_dumps/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/anonymised_db_dumps/tests/__init__.py b/api/anonymised_db_dumps/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/anonymised_db_dumps/tests/test_anonymised_dumps.py b/api/anonymised_db_dumps/tests/test_anonymised_dumps.py new file mode 100644 index 0000000000..21175cd5b9 --- /dev/null +++ b/api/anonymised_db_dumps/tests/test_anonymised_dumps.py @@ -0,0 +1,148 @@ +import os +from datetime import datetime + +from django.conf import settings +from django.core.management import call_command +from django.test import TransactionTestCase + +from api.document_data.models import DocumentData +from api.organisations.tests.factories import SiteFactory, OrganisationFactory +from api.addresses.tests.factories import AddressFactory +from api.staticdata.countries.models import Country +from api.queries.end_user_advisories.tests.factories import EndUserAdvisoryQueryFactory +from api.external_data.tests.factories import DenialFactory +from api.parties.tests.factories import PartyFactory +from api.users.tests.factories import BaseUserFactory + + +class TestAnonymiseDumps(TransactionTestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.create_test_data() + cls.dump_location = f"/tmp/{settings.DB_ANONYMISER_DUMP_FILE_NAME}" + try: + os.remove(cls.dump_location) + except FileNotFoundError: + pass + call_command("dump_and_anonymise", keep_local_dumpfile=True, skip_s3_upload=True) + with open(cls.dump_location, "r") as f: + cls.anonymised_sql = f.read() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + cls.delete_test_data() + + @classmethod + def create_test_data(cls): + cls.document_data = DocumentData.objects.create( + s3_key="somefile.txt", content_type="csv", last_modified=datetime.now() + ) + cls.site = SiteFactory(name="some site") + cls.organisation = OrganisationFactory( + name="some org", + phone_number="+4466019250102", + website="someexample.net", + eori_number="some_eori", + sic_number="some_sic", + vat_number="some_vat", + registration_number="some_reg", + ) + cls.address = AddressFactory( + address_line_1="my address line 1", + address_line_2="my address line 2", + region="my region", + postcode="my postc", + city="my city", + country__name="France", + ) + cls.end_user_advisory_query = EndUserAdvisoryQueryFactory( + contact_name="EUA name", contact_telephone="+4499919250102", contact_email="email@example.net" + ) + cls.denial = DenialFactory( + name="denial name", + address="denial address", + consignee_name="denial consignee name", + ) + cls.party = PartyFactory( + name="party name", + address="party address", + website="party.website", + email="party@email.net", + phone_number="+44party_no", + signatory_name_euu="party signatory", + details="party details", + ) + cls.base_user = BaseUserFactory( + first_name="base user first", + last_name="base user last", + email="base@user.email", + phone_number="+44baseuser", + ) + + @classmethod + def delete_test_data(cls): + cls.document_data.delete() + cls.site.delete() + cls.organisation.delete() + cls.address.delete() + cls.end_user_advisory_query.delete() + cls.denial.delete() + cls.party.delete() + cls.base_user.delete() + + def test_users_baseuser_anonymised(self): + assert str(self.base_user.id) in self.anonymised_sql + assert str(self.base_user.first_name) not in self.anonymised_sql + assert str(self.base_user.last_name) not in self.anonymised_sql + assert str(self.base_user.email) not in self.anonymised_sql + assert str(self.base_user.phone_number) not in self.anonymised_sql + + def test_party_anonymised(self): + assert str(self.party.id) in self.anonymised_sql + assert str(self.party.name) not in self.anonymised_sql + assert str(self.party.address) not in self.anonymised_sql + assert str(self.party.website) not in self.anonymised_sql + assert str(self.party.email) not in self.anonymised_sql + assert str(self.party.phone_number) not in self.anonymised_sql + assert str(self.party.signatory_name_euu) not in self.anonymised_sql + assert str(self.party.details) not in self.anonymised_sql + + def test_address_anonymised(self): + assert str(self.address.id) in self.anonymised_sql + assert self.address.address_line_1 not in self.anonymised_sql + assert self.address.address_line_2 not in self.anonymised_sql + assert self.address.region not in self.anonymised_sql + assert self.address.postcode not in self.anonymised_sql + assert self.address.city not in self.anonymised_sql + + def test_external_data_denial_anonymised(self): + assert str(self.denial.id) in self.anonymised_sql + assert self.denial.name not in self.anonymised_sql + assert self.denial.address not in self.anonymised_sql + assert self.denial.consignee_name not in self.anonymised_sql + + def test_organisation_anonymised(self): + assert str(self.organisation.id) in self.anonymised_sql + assert self.organisation.name not in self.anonymised_sql + assert str(self.organisation.phone_number) not in self.anonymised_sql + assert self.organisation.website not in self.anonymised_sql + assert self.organisation.eori_number not in self.anonymised_sql + assert self.organisation.sic_number not in self.anonymised_sql + assert self.organisation.vat_number not in self.anonymised_sql + assert self.organisation.registration_number not in self.anonymised_sql + + def test_enduser_advisory_query_anonymised(self): + assert str(self.end_user_advisory_query.id) in self.anonymised_sql + assert self.end_user_advisory_query.contact_name not in self.anonymised_sql + assert self.end_user_advisory_query.contact_telephone not in self.anonymised_sql + assert self.end_user_advisory_query.contact_email not in self.anonymised_sql + + def test_site_anonymised(self): + assert str(self.site.id) in self.anonymised_sql + assert self.site.name not in self.anonymised_sql + + def test_document_data_excluded(self): + assert self.document_data.s3_key not in self.anonymised_sql + assert str(self.document_data.id) not in self.anonymised_sql diff --git a/api/applications/helpers.py b/api/applications/helpers.py index 0321688637..b16de31a1a 100644 --- a/api/applications/helpers.py +++ b/api/applications/helpers.py @@ -14,21 +14,6 @@ OpenEndUseDetailsUpdateSerializer, StandardEndUseDetailsUpdateSerializer, ) -from api.applications.serializers.exhibition_clearance import ( - ExhibitionClearanceCreateSerializer, - ExhibitionClearanceViewSerializer, - ExhibitionClearanceUpdateSerializer, -) -from api.applications.serializers.f680_clearance import ( - F680ClearanceCreateSerializer, - F680ClearanceViewSerializer, - F680ClearanceUpdateSerializer, -) -from api.applications.serializers.gifting_clearance import ( - GiftingClearanceCreateSerializer, - GiftingClearanceViewSerializer, - GiftingClearanceUpdateSerializer, -) from api.applications.serializers.open_application import ( OpenApplicationCreateSerializer, OpenApplicationUpdateSerializer, @@ -56,12 +41,6 @@ def get_application_view_serializer(application: BaseApplication): return StandardApplicationViewSerializer elif application.case_type.sub_type == CaseTypeSubTypeEnum.OPEN: return OpenApplicationViewSerializer - elif application.case_type.sub_type == CaseTypeSubTypeEnum.EXHIBITION: - return ExhibitionClearanceViewSerializer - elif application.case_type.sub_type == CaseTypeSubTypeEnum.GIFTING: - return GiftingClearanceViewSerializer - elif application.case_type.sub_type == CaseTypeSubTypeEnum.F680: - return F680ClearanceViewSerializer else: raise BadRequestError( { @@ -78,12 +57,6 @@ def get_application_create_serializer(case_type): return StandardApplicationCreateSerializer elif sub_type == CaseTypeSubTypeEnum.OPEN: return OpenApplicationCreateSerializer - elif sub_type == CaseTypeSubTypeEnum.EXHIBITION: - return ExhibitionClearanceCreateSerializer - elif sub_type == CaseTypeSubTypeEnum.GIFTING: - return GiftingClearanceCreateSerializer - elif sub_type == CaseTypeSubTypeEnum.F680: - return F680ClearanceCreateSerializer else: raise BadRequestError({"application_type": [strings.Applications.Generic.SELECT_A_LICENCE_TYPE]}) @@ -93,12 +66,6 @@ def get_application_update_serializer(application: BaseApplication): return StandardApplicationUpdateSerializer elif application.case_type.sub_type == CaseTypeSubTypeEnum.OPEN: return OpenApplicationUpdateSerializer - elif application.case_type.sub_type == CaseTypeSubTypeEnum.EXHIBITION: - return ExhibitionClearanceUpdateSerializer - elif application.case_type.sub_type == CaseTypeSubTypeEnum.GIFTING: - return GiftingClearanceUpdateSerializer - elif application.case_type.sub_type == CaseTypeSubTypeEnum.F680: - return F680ClearanceUpdateSerializer else: raise BadRequestError( { diff --git a/api/applications/libraries/application_helpers.py b/api/applications/libraries/application_helpers.py index d61ff7589f..79251f2c1b 100644 --- a/api/applications/libraries/application_helpers.py +++ b/api/applications/libraries/application_helpers.py @@ -10,7 +10,6 @@ from api.core.constants import GovPermissions from api.core.permissions import assert_user_has_permission from api.staticdata.statuses.enums import CaseStatusEnum -from api.applications.models import HmrcQuery from api.organisations.libraries.get_organisation import get_request_user_organisation_id from api.users.models import GovUser from lite_content.lite_api import strings @@ -69,7 +68,7 @@ def can_status_be_set_by_gov_user(user: GovUser, original_status: str, new_statu return True -def create_submitted_audit(request: Request, application: HmrcQuery, old_status: str) -> None: +def create_submitted_audit(request: Request, application, old_status: str) -> None: audit_trail_service.create( actor=request.user, verb=AuditType.UPDATED_STATUS, diff --git a/api/applications/libraries/get_applications.py b/api/applications/libraries/get_applications.py index e5bb40d503..336ba7f19a 100644 --- a/api/applications/libraries/get_applications.py +++ b/api/applications/libraries/get_applications.py @@ -1,10 +1,7 @@ from api.applications.models import ( BaseApplication, - F680ClearanceApplication, - GiftingClearanceApplication, OpenApplication, StandardApplication, - ExhibitionClearanceApplication, ) from api.cases.enums import CaseTypeSubTypeEnum from api.core.exceptions import NotFoundError @@ -76,12 +73,6 @@ def get_application(pk, organisation_id=None): return obj elif application_type == CaseTypeSubTypeEnum.OPEN: return OpenApplication.objects.get(pk=pk, **kwargs) - elif application_type == CaseTypeSubTypeEnum.EXHIBITION: - return ExhibitionClearanceApplication.objects.get(pk=pk) - elif application_type == CaseTypeSubTypeEnum.GIFTING: - return GiftingClearanceApplication.objects.get(pk=pk) - elif application_type == CaseTypeSubTypeEnum.F680: - return F680ClearanceApplication.objects.get(pk=pk) else: raise NotImplementedError(f"get_application does not support this application type: {application_type}") except ( diff --git a/api/applications/managers.py b/api/applications/managers.py index c74f4cc989..0367c87bb3 100644 --- a/api/applications/managers.py +++ b/api/applications/managers.py @@ -12,18 +12,3 @@ def drafts(self, organisation): def submitted(self, organisation): draft = get_case_status_by_status(CaseStatusEnum.DRAFT) return self.get_queryset().filter(organisation=organisation).exclude(status=draft).order_by("-submitted_at") - - -class HmrcQueryManager(InheritanceManager): - def drafts(self, hmrc_organisation): - draft = get_case_status_by_status(CaseStatusEnum.DRAFT) - return self.get_queryset().filter(status=draft, hmrc_organisation=hmrc_organisation).order_by("-created_at") - - def submitted(self, hmrc_organisation): - draft = get_case_status_by_status(CaseStatusEnum.DRAFT) - return ( - self.get_queryset() - .filter(hmrc_organisation=hmrc_organisation) - .exclude(status=draft) - .order_by("-submitted_at") - ) diff --git a/api/applications/migrations/0078_remove_f680clearanceapplication_baseapplication_ptr_and_more.py b/api/applications/migrations/0078_remove_f680clearanceapplication_baseapplication_ptr_and_more.py new file mode 100644 index 0000000000..d21f6098d3 --- /dev/null +++ b/api/applications/migrations/0078_remove_f680clearanceapplication_baseapplication_ptr_and_more.py @@ -0,0 +1,45 @@ +# Generated by Django 4.2.9 on 2024-02-29 16:40 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("applications", "0077_back_populate_product_report_summary_prefix_and_suffix"), + ] + + operations = [ + migrations.RemoveField( + model_name="f680clearanceapplication", + name="baseapplication_ptr", + ), + migrations.RemoveField( + model_name="f680clearanceapplication", + name="types", + ), + migrations.RemoveField( + model_name="giftingclearanceapplication", + name="baseapplication_ptr", + ), + migrations.RemoveField( + model_name="hmrcquery", + name="baseapplication_ptr", + ), + migrations.RemoveField( + model_name="hmrcquery", + name="hmrc_organisation", + ), + migrations.DeleteModel( + name="ExhibitionClearanceApplication", + ), + migrations.DeleteModel( + name="F680ClearanceApplication", + ), + migrations.DeleteModel( + name="GiftingClearanceApplication", + ), + migrations.DeleteModel( + name="HmrcQuery", + ), + ] diff --git a/api/applications/models.py b/api/applications/models.py index 650c5fb9d0..48b9248242 100644 --- a/api/applications/models.py +++ b/api/applications/models.py @@ -11,8 +11,6 @@ from api.applications.enums import ( ApplicationExportType, ApplicationExportLicenceOfficialType, - ServiceEquipmentType, - MTCRAnswers, GoodsTypeCategory, ContractType, SecurityClassifiedApprovalsType, @@ -20,7 +18,7 @@ ) from api.appeals.models import Appeal -from api.applications.managers import BaseApplicationManager, HmrcQueryManager +from api.applications.managers import BaseApplicationManager from api.audit_trail.models import ( Audit, AuditType, @@ -36,14 +34,13 @@ from api.goods.enums import ItemType, PvGrading from api.goods.models import Good from api.organisations.enums import OrganisationDocumentType -from api.organisations.models import Organisation, Site, ExternalLocation +from api.organisations.models import Site, ExternalLocation from api.parties.enums import PartyType from api.parties.models import Party from api.queues.models import Queue 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.f680_clearance_types.models import F680ClearanceType from api.staticdata.regimes.models import RegimeEntry from api.staticdata.report_summaries.models import ReportSummaryPrefix, ReportSummarySubject from api.staticdata.statuses.enums import ( @@ -335,53 +332,6 @@ class OpenApplication(BaseApplication): contains_firearm_goods = models.BooleanField(blank=True, default=None, null=True) -# MOD Clearances Applications -# Exhibition includes End User, Consignee, Ultimate end users & Third parties -class ExhibitionClearanceApplication(BaseApplication): - title = models.CharField(blank=False, null=True, max_length=255) - first_exhibition_date = models.DateField(blank=False, null=True) - required_by_date = models.DateField(blank=False, null=True) - reason_for_clearance = models.TextField(default=None, blank=True, null=True, max_length=2000) - - -# Gifting includes End User & Third parties -class GiftingClearanceApplication(BaseApplication): - pass - - -# F680 includes End User & Third parties -class F680ClearanceApplication(BaseApplication): - types = models.ManyToManyField(F680ClearanceType, related_name="f680_clearance_application") - - expedited = models.BooleanField(default=None, null=True) - expedited_date = models.DateField(null=True, default=None) - - foreign_technology = models.BooleanField(default=None, null=True) - foreign_technology_description = models.CharField(max_length=2200, null=True) - - locally_manufactured = models.BooleanField(blank=True, default=None, null=True) - locally_manufactured_description = models.CharField(max_length=2200, null=True) - - mtcr_type = models.CharField(choices=MTCRAnswers.choices, null=True, max_length=50) - - electronic_warfare_requirement = models.BooleanField(default=None, null=True) - - uk_service_equipment = models.BooleanField(default=None, null=True) - uk_service_equipment_description = models.CharField(max_length=2200, null=True) - uk_service_equipment_type = models.CharField(choices=ServiceEquipmentType.choices, null=True, max_length=50) - - prospect_value = models.DecimalField(max_digits=15, decimal_places=2, null=True) - - -# Queries -class HmrcQuery(BaseApplication): - hmrc_organisation = models.ForeignKey(Organisation, default=None, on_delete=models.PROTECT) - reasoning = models.CharField(default=None, blank=True, null=True, max_length=1000) - have_goods_departed = models.BooleanField(default=False) # Signal in signals.py - - objects = HmrcQueryManager() - - class ApplicationDocument(Document): application = models.ForeignKey(BaseApplication, on_delete=models.CASCADE) description = models.TextField(default=None, blank=True, null=True) diff --git a/api/applications/serializers/exhibition_clearance.py b/api/applications/serializers/exhibition_clearance.py deleted file mode 100644 index 9ef6f90243..0000000000 --- a/api/applications/serializers/exhibition_clearance.py +++ /dev/null @@ -1,124 +0,0 @@ -from django.utils import timezone -from rest_framework import serializers -from rest_framework.exceptions import ValidationError - -from api.applications.mixins.serializers import PartiesSerializerMixin -from api.applications.models import ExhibitionClearanceApplication -from api.applications.serializers.generic_application import ( - GenericApplicationCreateSerializer, - GenericApplicationViewSerializer, - GenericApplicationUpdateSerializer, -) -from api.applications.serializers.good import GoodOnApplicationViewSerializer -from lite_content.lite_api import strings - - -class ExhibitionClearanceViewSerializer(PartiesSerializerMixin, GenericApplicationViewSerializer): - goods = GoodOnApplicationViewSerializer(many=True, read_only=True) - destinations = serializers.SerializerMethodField() - additional_documents = serializers.SerializerMethodField() - - class Meta: - model = ExhibitionClearanceApplication - fields = GenericApplicationViewSerializer.Meta.fields + ( - "goods", - "activity", - "usage", - "destinations", - "additional_documents", - "title", - "first_exhibition_date", - "required_by_date", - "reason_for_clearance", - ) - - -class ExhibitionClearanceCreateSerializer(GenericApplicationCreateSerializer): - class Meta: - model = ExhibitionClearanceApplication - fields = ( - "id", - "name", - "case_type", - "organisation", - "status", - ) - - -class ExhibitionClearanceUpdateSerializer(GenericApplicationUpdateSerializer): - title = serializers.CharField(required=True, max_length=255) - first_exhibition_date = serializers.DateField(required=True) - required_by_date = serializers.DateField(required=True) - reason_for_clearance = serializers.CharField(max_length=2000) - - class Meta: - model = ExhibitionClearanceApplication - fields = GenericApplicationUpdateSerializer.Meta.fields + ( - "title", - "first_exhibition_date", - "required_by_date", - "reason_for_clearance", - ) - - -class ExhibitionClearanceDetailSerializer(serializers.ModelSerializer): - title = serializers.CharField( - max_length=255, - allow_null=False, - error_messages={ - "blank": strings.Applications.Exhibition.Error.NO_EXHIBITION_NAME, - "required": strings.Applications.Exhibition.Error.NO_EXHIBITION_NAME, - "null": strings.Applications.Exhibition.Error.NO_EXHIBITION_NAME, - }, - ) - first_exhibition_date = serializers.DateField( - allow_null=False, - error_messages={ - "invalid": strings.Applications.Exhibition.Error.BLANK_EXHIBITION_START_DATE, - "required": strings.Applications.Exhibition.Error.NO_EXHIBITION_START_DATE, - "null": strings.Applications.Exhibition.Error.NO_EXHIBITION_START_DATE, - }, - ) - required_by_date = serializers.DateField( - allow_null=False, - error_messages={ - "invalid": strings.Applications.Exhibition.Error.BLANK_REQUIRED_BY_DATE, - "required": strings.Applications.Exhibition.Error.NO_REQUIRED_BY_DATE, - "null": strings.Applications.Exhibition.Error.NO_REQUIRED_BY_DATE, - }, - ) - reason_for_clearance = serializers.CharField(required=False, allow_blank=True, allow_null=True, max_length=2000) - - class Meta: - model = ExhibitionClearanceApplication - fields = ( - "title", - "first_exhibition_date", - "required_by_date", - "reason_for_clearance", - ) - - def validate(self, data): - required_by_date_errors = [] - first_exhibition_date_errors = [] - - today = timezone.now().date() - if data["required_by_date"] < today: - required_by_date_errors.append(strings.Applications.Exhibition.Error.REQUIRED_BY_DATE_FUTURE) - if data["first_exhibition_date"] < today: - first_exhibition_date_errors.append(strings.Applications.Exhibition.Error.FIRST_EXHIBITION_DATE_FUTURE) - - if not first_exhibition_date_errors and data["required_by_date"] > data["first_exhibition_date"]: - first_exhibition_date_errors.append( - strings.Applications.Exhibition.Error.REQUIRED_BY_BEFORE_FIRST_EXHIBITION_DATE - ) - - errors = {} - if first_exhibition_date_errors: - errors.update({"first_exhibition_date": first_exhibition_date_errors}) - if required_by_date_errors: - errors.update({"required_by_date": required_by_date_errors}) - if errors: - raise ValidationError(errors) - - return data diff --git a/api/applications/serializers/f680_clearance.py b/api/applications/serializers/f680_clearance.py deleted file mode 100644 index 6d99de5996..0000000000 --- a/api/applications/serializers/f680_clearance.py +++ /dev/null @@ -1,202 +0,0 @@ -from datetime import timedelta - -from django.utils import timezone -from rest_framework import serializers -from rest_framework.fields import CharField - -from api.applications import constants -from api.applications.enums import MTCRAnswers, ServiceEquipmentType -from api.applications.mixins.serializers import PartiesSerializerMixin -from api.applications.models import F680ClearanceApplication -from api.applications.serializers.generic_application import ( - GenericApplicationCreateSerializer, - GenericApplicationUpdateSerializer, - GenericApplicationViewSerializer, -) -from api.applications.serializers.good import GoodOnApplicationViewSerializer -from api.core.serializers import KeyValueChoiceField, PrimaryKeyRelatedSerializerField -from api.goods.enums import PvGrading -from lite_content.lite_api import strings -from api.staticdata.f680_clearance_types.enums import F680ClearanceTypeEnum -from api.staticdata.f680_clearance_types.models import F680ClearanceType - - -class F680ClearanceTypeSerializer(serializers.ModelSerializer): - name = KeyValueChoiceField(choices=F680ClearanceTypeEnum.choices) - - class Meta: - model = F680ClearanceType - fields = ("name",) - - -class F680ClearanceViewSerializer(PartiesSerializerMixin, GenericApplicationViewSerializer): - goods = GoodOnApplicationViewSerializer(many=True, read_only=True) - destinations = serializers.SerializerMethodField() - additional_documents = serializers.SerializerMethodField() - types = F680ClearanceTypeSerializer(read_only=True, many=True) - clearance_level = KeyValueChoiceField(choices=PvGrading.choices, allow_null=True, required=False, allow_blank=True) - - expedited = serializers.BooleanField(required=False) - expedited_date = serializers.DateField(required=False) - - foreign_technology = serializers.BooleanField(required=False) - foreign_technology_description = serializers.CharField(max_length=2000, allow_blank=True, required=False) - - locally_manufactured = serializers.BooleanField(required=False) - locally_manufactured_description = serializers.CharField(max_length=2000, allow_blank=True, required=False) - - mtcr_type = KeyValueChoiceField(choices=MTCRAnswers.choices, allow_blank=True, required=False) - - electronic_warfare_requirement = serializers.BooleanField(required=False) - - uk_service_equipment = serializers.BooleanField(required=False) - uk_service_equipment_description = serializers.CharField(max_length=2000, allow_blank=True, required=False) - uk_service_equipment_type = KeyValueChoiceField( - choices=ServiceEquipmentType.choices, allow_blank=True, required=False - ) - - prospect_value = serializers.DecimalField(required=False, allow_null=True, max_digits=15, decimal_places=2) - - class Meta: - model = F680ClearanceApplication - fields = ( - GenericApplicationViewSerializer.Meta.fields - + constants.F680.ADDITIONAL_INFORMATION_FIELDS - + ( - "case_officer", - "end_user", - "third_parties", - "goods", - "activity", - "usage", - "destinations", - "additional_documents", - "types", - "clearance_level", - "intended_end_use", - ) - ) - - -class F680ClearanceCreateSerializer(GenericApplicationCreateSerializer): - class Meta: - model = F680ClearanceApplication - fields = ( - "id", - "name", - "case_type", - "organisation", - "status", - "clearance_level", - ) - - -class F680ClearanceUpdateSerializer(GenericApplicationUpdateSerializer): - name = CharField( - max_length=100, - required=True, - allow_blank=False, - allow_null=False, - error_messages={"blank": strings.Applications.Generic.MISSING_REFERENCE_NAME_ERROR}, - ) - types = PrimaryKeyRelatedSerializerField( - queryset=F680ClearanceType.objects.all(), - serializer=F680ClearanceTypeSerializer, - error_messages={"required": strings.Applications.F680.NO_CLEARANCE_TYPE}, - many=True, - ) - clearance_level = serializers.ChoiceField(choices=PvGrading.choices, allow_null=True) - - expedited = serializers.BooleanField(required=False, allow_null=True) - expedited_date = serializers.DateField( - required=False, - error_messages={"invalid": strings.Applications.F680.AdditionalInformation.Errors.EXPEDITED_DATE_RANGE}, - ) - - foreign_technology = serializers.BooleanField(required=False, allow_null=True) - foreign_technology_description = serializers.CharField(max_length=2000, allow_blank=True, required=False) - - locally_manufactured = serializers.BooleanField(required=False, allow_null=True) - locally_manufactured_description = serializers.CharField(max_length=2000, allow_blank=True, required=False) - - mtcr_type = KeyValueChoiceField(choices=MTCRAnswers.choices, allow_blank=True, required=False) - - electronic_warfare_requirement = serializers.BooleanField(required=False, allow_null=True) - - uk_service_equipment = serializers.BooleanField(required=False, allow_null=True) - uk_service_equipment_description = serializers.CharField(max_length=2000, allow_blank=True, required=False) - uk_service_equipment_type = KeyValueChoiceField( - choices=ServiceEquipmentType.choices, allow_blank=True, required=False - ) - - prospect_value = serializers.DecimalField( - required=False, - allow_null=True, - max_digits=15, - decimal_places=2, - error_messages={"invalid": strings.Applications.F680.AdditionalInformation.Errors.PROSPECT_VALUE}, - ) - - class Meta: - model = F680ClearanceApplication - fields = ( - GenericApplicationUpdateSerializer.Meta.fields - + constants.F680.ADDITIONAL_INFORMATION_FIELDS - + ( - "types", - "clearance_level", - ) - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - if "types" in self.initial_data: - self.initial_data["types"] = [ - F680ClearanceTypeEnum.ids.get(clearance_type) for clearance_type in self.initial_data.get("types", []) - ] - - def validate(self, data): - error_strings = strings.Applications.F680.AdditionalInformation.Errors - error_messages = { - "expedited": error_strings.EXPEDITED, - "expedited_date": error_strings.EXPEDITED_DATE, - "foreign_technology": error_strings.FOREIGN_TECHNOLOGY, - "foreign_technology_description": error_strings.FOREIGN_TECHNOLOGY_DESCRIPTION, - "locally_manufactured": error_strings.LOCALLY_MANUFACTURED, - "locally_manufactured_description": error_strings.LOCALLY_MANUFACTURED_DESCRIPTION, - "mtcr_type": error_strings.MTCR_TYPE, - "electronic_warfare_requirement": error_strings.ELECTRONIC_WARFARE_REQUIREMENT, - "uk_service_equipment": error_strings.UK_SERVICE_EQUIPMENT, - "uk_service_equipment_type": error_strings.UK_SERVICE_EQUIPMENT_TYPE, - "prospect_value": error_strings.PROSPECT_VALUE, - } - for field in constants.F680.REQUIRED_FIELDS: - if field in self.initial_data: - if self.initial_data[field] is None or self.initial_data[field] == "": - raise serializers.ValidationError({field: [error_messages[field]]}) - if self.initial_data[field] is True or self.initial_data[field] == "True": - secondary_field = constants.F680.REQUIRED_SECONDARY_FIELDS.get(field, False) - if secondary_field and not self.initial_data.get(secondary_field): - raise serializers.ValidationError({secondary_field: [error_messages[secondary_field]]}) - - validated_data = super().validate(data) - - if "types" in self.initial_data and not validated_data.get("types"): - raise serializers.ValidationError({"types": strings.Applications.F680.NO_CLEARANCE_TYPE}) - - if validated_data.get("expedited"): - today = timezone.now().date() - limit = (timezone.now() + timedelta(days=30)).date() - if today > validated_data["expedited_date"] or validated_data["expedited_date"] > limit: - raise serializers.ValidationError({"expedited_date": [error_strings.EXPEDITED_DATE_RANGE]}) - - validated_data["expedited_date"] = str(validated_data["expedited_date"]) - - return validated_data - - def update(self, instance, validated_data): - if "types" in validated_data: - validated_data["types"] = validated_data.get("types") - - instance = super().update(instance, validated_data) - return instance diff --git a/api/applications/serializers/gifting_clearance.py b/api/applications/serializers/gifting_clearance.py deleted file mode 100644 index d9a15d3c89..0000000000 --- a/api/applications/serializers/gifting_clearance.py +++ /dev/null @@ -1,57 +0,0 @@ -from rest_framework import serializers -from rest_framework.fields import CharField - -from api.applications.mixins.serializers import PartiesSerializerMixin -from api.applications.models import GiftingClearanceApplication -from api.applications.serializers.generic_application import ( - GenericApplicationCreateSerializer, - GenericApplicationUpdateSerializer, - GenericApplicationViewSerializer, -) -from api.applications.serializers.good import GoodOnApplicationViewSerializer -from lite_content.lite_api import strings - - -class GiftingClearanceViewSerializer(PartiesSerializerMixin, GenericApplicationViewSerializer): - goods = GoodOnApplicationViewSerializer(many=True, read_only=True) - destinations = serializers.SerializerMethodField() - additional_documents = serializers.SerializerMethodField() - - class Meta: - model = GiftingClearanceApplication - fields = GenericApplicationViewSerializer.Meta.fields + ( - "case_officer", - "end_user", - "third_parties", - "goods", - "activity", - "usage", - "destinations", - "additional_documents", - ) - - -class GiftingClearanceCreateSerializer(GenericApplicationCreateSerializer): - class Meta: - model = GiftingClearanceApplication - fields = ( - "id", - "name", - "case_type", - "organisation", - "status", - ) - - -class GiftingClearanceUpdateSerializer(GenericApplicationUpdateSerializer): - name = CharField( - max_length=100, - required=True, - allow_blank=False, - allow_null=False, - error_messages={"blank": strings.Applications.Generic.MISSING_REFERENCE_NAME_ERROR}, - ) - - class Meta: - model = GiftingClearanceApplication - fields = GenericApplicationUpdateSerializer.Meta.fields diff --git a/api/applications/tests/test_adding_goods.py b/api/applications/tests/test_adding_goods.py index a8f30fc52d..a4c79d18ff 100644 --- a/api/applications/tests/test_adding_goods.py +++ b/api/applications/tests/test_adding_goods.py @@ -6,11 +6,8 @@ from api.applications.models import GoodOnApplication from api.audit_trail.models import Audit -from api.cases.enums import CaseTypeEnum -from api.goods.enums import ItemType from api.goods.tests.factories import GoodFactory from api.staticdata.units.enums import Units -from lite_content.lite_api import strings from test_helpers.clients import DataTestClient from test_helpers.decorators import none_param_tester @@ -404,141 +401,3 @@ def test_add_a_good_to_a_draft_with_firearms_details_is_covered_by_section_5_val response = self.client.post(url, data, **self.exporter_headers) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - -class AddingGoodsOnApplicationExhibitionTests(DataTestClient): - def setUp(self): - super().setUp() - self.draft = self.create_mod_clearance_application(self.organisation, CaseTypeEnum.EXHIBITION) - self.good = GoodFactory(organisation=self.organisation) - - def test_add_a_good_to_a_exhibition_draft_choice(self): - self.create_good_document( - self.good, - user=self.exporter_user, - organisation=self.organisation, - name="doc1", - s3_key="doc3", - ) - - data = {"good_id": self.good.id, "item_type": ItemType.VIDEO} - - url = reverse("applications:application_goods", kwargs={"pk": self.draft.id}) - - response = self.client.post(url, data, **self.exporter_headers) - - response_data = response.json()["good"] - - self.assertIsNone(response_data["value"]) - self.assertIsNone(response_data["quantity"]) - self.assertIsNone(response_data["unit"]) - self.assertIsNone(response_data["is_good_incorporated"]) - self.assertEqual(response_data["good"], str(self.good.id)) - self.assertEqual(response_data["item_type"], str(ItemType.VIDEO)) - # we expect other item type to be None as it should not be set unless ItemType is Other - self.assertIsNone(response_data["other_item_type"]) - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - # check application - url = reverse("applications:application_goods", kwargs={"pk": self.draft.id}) - response = self.client.get(url, **self.exporter_headers) - response_data = response.json() - audit_qs = Audit.objects.all() - - # The standard draft comes with one good pre-added, plus the good added in this test makes 2 - self.assertEqual(len(response_data["goods"]), 2) - # No audit created for drafts. - self.assertEqual(audit_qs.count(), 0) - - def test_add_a_good_to_a_exhibition_other(self): - self.create_good_document( - self.good, - user=self.exporter_user, - organisation=self.organisation, - name="doc1", - s3_key="doc3", - ) - other_value = "abcde" - data = {"good_id": self.good.id, "item_type": ItemType.OTHER, "other_item_type": other_value} - - url = reverse("applications:application_goods", kwargs={"pk": self.draft.id}) - - response = self.client.post(url, data, **self.exporter_headers) - - response_data = response.json()["good"] - - self.assertIsNone(response_data["value"]) - self.assertIsNone(response_data["quantity"]) - self.assertIsNone(response_data["unit"]) - self.assertIsNone(response_data["is_good_incorporated"]) - self.assertEqual(response_data["good"], str(self.good.id)) - self.assertEqual(response_data["item_type"], str(ItemType.OTHER)) - self.assertEqual(response_data["other_item_type"], other_value) - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - # check application - url = reverse("applications:application_goods", kwargs={"pk": self.draft.id}) - response = self.client.get(url, **self.exporter_headers) - response_data = response.json() - audit_qs = Audit.objects.all() - - # The standard draft comes with one good pre-added, plus the good added in this test makes 2 - self.assertEqual(len(response_data["goods"]), 2) - # No audit created for drafts. - self.assertEqual(audit_qs.count(), 0) - - def test_add_a_good_to_a_exhibition_other_blank_failure(self): - self.create_good_document( - self.good, - user=self.exporter_user, - organisation=self.organisation, - name="doc1", - s3_key="doc3", - ) - - data = {"good_id": self.good.id, "item_type": ItemType.OTHER, "other_item_type": ""} - - url = reverse("applications:application_goods", kwargs={"pk": self.draft.id}) - - response = self.client.post(url, data, **self.exporter_headers) - - errors = response.json()["errors"] - - self.assertEqual(errors["other_item_type"][0], strings.Goods.OTHER_ITEM_TYPE) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - url = reverse("applications:application_goods", kwargs={"pk": self.draft.id}) - response = self.client.get(url, **self.exporter_headers) - response_data = response.json() - - # Still one good as test failed - self.assertEqual(len(response_data["goods"]), 1) - - def test_add_a_good_to_a_exhibition_no_data_failure(self): - self.create_good_document( - self.good, - user=self.exporter_user, - organisation=self.organisation, - name="doc1", - s3_key="doc3", - ) - - data = {"good_id": self.good.id} - - url = reverse("applications:application_goods", kwargs={"pk": self.draft.id}) - - response = self.client.post(url, data, **self.exporter_headers) - - errors = response.json()["errors"] - - self.assertEqual(errors["item_type"][0], strings.Goods.ITEM_TYPE) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - url = reverse("applications:application_goods", kwargs={"pk": self.draft.id}) - response = self.client.get(url, **self.exporter_headers) - response_data = response.json() - - # Still one good as test failed - self.assertEqual(len(response_data["goods"]), 1) diff --git a/api/applications/tests/test_adding_route_of_goods.py b/api/applications/tests/test_adding_route_of_goods.py index 189c047d49..69c4c3e963 100644 --- a/api/applications/tests/test_adding_route_of_goods.py +++ b/api/applications/tests/test_adding_route_of_goods.py @@ -2,7 +2,7 @@ from parameterized import parameterized from rest_framework import status -from api.cases.enums import CaseTypeEnum, CaseTypeSubTypeEnum +from api.cases.enums import CaseTypeSubTypeEnum from lite_content.lite_api import strings from test_helpers.clients import DataTestClient @@ -17,17 +17,6 @@ def setUp(self): self.non_waybill_or_lading_route_details_field = "non_waybill_or_lading_route_details" self.data = {self.is_shipped_waybill_or_lading_field: "True"} - @parameterized.expand([[CaseTypeEnum.F680], [CaseTypeEnum.EXHIBITION], [CaseTypeEnum.GIFTING]]) - def test_non_open_or_standard_applications_failure(self, case_type): - case = self.create_mod_clearance_application(self.organisation, case_type=case_type) - url = reverse("applications:route_of_goods", kwargs={"pk": case.id}) - response = self.client.put(url, self.data, **self.exporter_headers) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual( - response.json()["errors"], ["This operation can only be used on applications of type: open, standard"] - ) - @parameterized.expand([CaseTypeSubTypeEnum.OPEN, CaseTypeSubTypeEnum.STANDARD]) def test_edit_standard_and_open_applications_success(self, case_type): if case_type == CaseTypeSubTypeEnum.OPEN: diff --git a/api/applications/tests/test_application_questions.py b/api/applications/tests/test_application_questions.py deleted file mode 100644 index eed5916582..0000000000 --- a/api/applications/tests/test_application_questions.py +++ /dev/null @@ -1,81 +0,0 @@ -from django.urls import reverse -from django.utils import timezone -from rest_framework import status - -from api.applications.enums import ServiceEquipmentType -from api.cases.enums import CaseTypeEnum -from api.staticdata.statuses.enums import CaseStatusEnum -from api.staticdata.statuses.libraries.get_case_status import get_case_status_by_status -from test_helpers.clients import DataTestClient - - -class ApplicationQuestionsTest(DataTestClient): - def setUp(self): - super().setUp() - self.draft = self.create_mod_clearance_application( - self.organisation, CaseTypeEnum.F680, additional_information=False - ) - self.url = reverse("applications:application", kwargs={"pk": self.draft.id}) - self.exporter_user.set_role(self.organisation, self.exporter_super_user_role) - - def test_update_f680_questions_success(self): - data = {"foreign_technology": True, "foreign_technology_description": "This is going to Norway."} - - response = self.client.put(self.url, data, **self.exporter_headers) - self.draft.refresh_from_db() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(self.draft.foreign_technology, data["foreign_technology"]) - self.assertEqual(self.draft.foreign_technology_description, data["foreign_technology_description"]) - - def test_update_questions_minor_edit_fail(self): - self.draft.status = get_case_status_by_status(CaseStatusEnum.SUBMITTED) - self.draft.save() - - data = {"foreign_technology": True, "foreign_technology_description": "This is going to Norway."} - - response = self.client.put(self.url, data, **self.exporter_headers) - self.draft.refresh_from_db() - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.json(), {"errors": {"Additional details": ["This isn't possible on a minor edit"]}}) - - def test_update_f680_questions_bad_data_failure(self): - data = {"foreign_technology": "FAKE DATA"} - - response = self.client.put(self.url, data, **self.exporter_headers) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.json(), {"errors": {"foreign_technology": ["Must be a valid boolean."]}}) - - def test_update_f680_questions_without_conditional_fail(self): - data = { - "expedited": True, - } - - response = self.client.put(self.url, data, **self.exporter_headers) - - self.draft.refresh_from_db() - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.json(), {"errors": {"expedited_date": ["Enter the date you need the clearance"]}}) - - def test_update_f680_questions_with_conditional_success(self): - date = timezone.now().date() - data = { - "expedited": True, - "expedited_date": f"{date.year}-{str(date.month).zfill(2)}-{str(date.day).zfill(2)}", - } - - response = self.client.put(self.url, data, **self.exporter_headers) - self.draft.refresh_from_db() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(self.draft.expedited, data["expedited"]) - self.assertEqual(str(self.draft.expedited_date), data["expedited_date"]) - - def test_update_f680_questions_enum_success_type(self): - data = { - "uk_service_equipment": True, - "uk_service_equipment_type": ServiceEquipmentType.MOD_FUNDED, - } - - response = self.client.put(self.url, data, **self.exporter_headers) - - self.draft.refresh_from_db() - self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/api/applications/tests/test_copy_application.py b/api/applications/tests/test_copy_application.py index 6785248a35..afff4772a3 100644 --- a/api/applications/tests/test_copy_application.py +++ b/api/applications/tests/test_copy_application.py @@ -10,13 +10,10 @@ GoodOnApplication, CountryOnApplication, SiteOnApplication, - ExhibitionClearanceApplication, - GiftingClearanceApplication, - F680ClearanceApplication, ) from api.cases.enums import CaseTypeEnum, CaseTypeSubTypeEnum from api.goodstype.models import GoodsType -from api.parties.models import Party, PartyDocument +from api.parties.models import PartyDocument from api.staticdata.statuses.enums import CaseStatusEnum from api.staticdata.statuses.libraries.get_case_status import get_case_status_by_status from api.staticdata.trade_control.enums import TradeControlProductCategory, TradeControlActivity @@ -282,129 +279,6 @@ def test_copy_submitted_open_application_successful(self): self._validate_open_application() - def test_copy_draft_exhibition_application_successful(self): - """ - Ensure we can copy an exhibition application that is a draft - """ - self.original_application = self.create_mod_clearance_application(self.organisation, CaseTypeEnum.EXHIBITION) - - self.url = reverse_lazy("applications:copy", kwargs={"pk": self.original_application.id}) - - self.data = {"name": "New application"} - - self.response = self.client.post(self.url, self.data, **self.exporter_headers) - self.response_data = self.response.json()["data"] - - self.assertEqual(self.response.status_code, status.HTTP_201_CREATED) - self.assertNotEqual(self.response_data, self.original_application.id) - - self.copied_application = ExhibitionClearanceApplication.objects.get(id=self.response_data) - - self._validate_exhibition_application() - - def test_copy_submitted_exhibition_application_successful(self): - """ - Ensure we can copy an exhibition application that is submitted (ongoing or otherwise) - """ - self.original_application = self.create_mod_clearance_application(self.organisation, CaseTypeEnum.EXHIBITION) - self.submit_application(self.original_application) - - self.url = reverse_lazy("applications:copy", kwargs={"pk": self.original_application.id}) - - self.data = {"name": "New application"} - - self.response = self.client.post(self.url, self.data, **self.exporter_headers) - self.response_data = self.response.json()["data"] - - self.assertEqual(self.response.status_code, status.HTTP_201_CREATED) - self.assertNotEqual(self.response_data, self.original_application.id) - - self.copied_application = ExhibitionClearanceApplication.objects.get(id=self.response_data) - - self._validate_exhibition_application() - - def test_copy_draft_gifting_application_successful(self): - """ - Ensure we can copy an exhibition application that is a draft - """ - self.original_application = self.create_mod_clearance_application(self.organisation, CaseTypeEnum.GIFTING) - - self.url = reverse_lazy("applications:copy", kwargs={"pk": self.original_application.id}) - - self.data = {"name": "New application"} - - self.response = self.client.post(self.url, self.data, **self.exporter_headers) - self.response_data = self.response.json()["data"] - - self.assertEqual(self.response.status_code, status.HTTP_201_CREATED) - self.assertNotEqual(self.response_data, self.original_application.id) - - self.copied_application = GiftingClearanceApplication.objects.get(id=self.response_data) - - self._validate_gifting_application() - - def test_copy_submitted_gifting_application_successful(self): - """ - Ensure we can copy an exhibition application that is submitted (ongoing or otherwise) - """ - self.original_application = self.create_mod_clearance_application(self.organisation, CaseTypeEnum.GIFTING) - self.submit_application(self.original_application) - - self.url = reverse_lazy("applications:copy", kwargs={"pk": self.original_application.id}) - - self.data = {"name": "New application"} - - self.response = self.client.post(self.url, self.data, **self.exporter_headers) - self.response_data = self.response.json()["data"] - - self.assertEqual(self.response.status_code, status.HTTP_201_CREATED) - self.assertNotEqual(self.response_data, self.original_application.id) - - self.copied_application = GiftingClearanceApplication.objects.get(id=self.response_data) - - self._validate_gifting_application() - - def test_copy_draft_F680_application_successful(self): - """ - Ensure we can copy an f680 application that is a draft - """ - self.original_application = self.create_mod_clearance_application(self.organisation, CaseTypeEnum.F680) - - self.url = reverse_lazy("applications:copy", kwargs={"pk": self.original_application.id}) - - self.data = {"name": "New application"} - - self.response = self.client.post(self.url, self.data, **self.exporter_headers) - self.response_data = self.response.json()["data"] - - self.assertEqual(self.response.status_code, status.HTTP_201_CREATED) - self.assertNotEqual(self.response_data, self.original_application.id) - - self.copied_application = F680ClearanceApplication.objects.get(id=self.response_data) - - self._validate_F680_application() - - def test_copy_submitted_F680_application_successful(self): - """ - Ensure we can copy an f680 application that is submitted (ongoing or otherwise) - """ - self.original_application = self.create_mod_clearance_application(self.organisation, CaseTypeEnum.F680) - self.submit_application(self.original_application) - - self.url = reverse_lazy("applications:copy", kwargs={"pk": self.original_application.id}) - - self.data = {"name": "New application"} - - self.response = self.client.post(self.url, self.data, **self.exporter_headers) - self.response_data = self.response.json()["data"] - - self.assertEqual(self.response.status_code, status.HTTP_201_CREATED) - self.assertNotEqual(self.response_data, self.original_application.id) - - self.copied_application = F680ClearanceApplication.objects.get(id=self.response_data) - - self._validate_F680_application() - def _validate_standard_application(self): self._validate_reset_data() self._validate_end_use_details() @@ -439,56 +313,6 @@ def _validate_trade_control_details(self): self.copied_application.trade_control_product_categories, ) - def _validate_exhibition_application(self): - self._validate_reset_data() - - self.assertEqual(self.original_application.title, self.copied_application.title) - self.assertEqual(self.original_application.first_exhibition_date, self.copied_application.first_exhibition_date) - self.assertEqual(self.original_application.required_by_date, self.copied_application.required_by_date) - self.assertEqual(self.original_application.reason_for_clearance, self.copied_application.reason_for_clearance) - - self._validate_good_on_application() - - self._validate_case_data() - - def _validate_gifting_application(self): - self._validate_reset_data() - - self._validate_good_on_application() - - self._validate_end_user() - self._validate_third_party() - - self._validate_case_data() - - def _validate_F680_application(self): - self._validate_reset_data() - - self._validate_f680_clearance_types() - - self._validate_end_use_details(self.copied_application.case_type.sub_type) - - self._validate_good_on_application() - - self._validate_end_user() - self._validate_third_party() - - self._validate_case_data() - - def _validate_hmrc_enquiry(self): - self._validate_reset_data() - self.assertEqual(self.original_application.reasoning, self.copied_application.reasoning) - self.assertEqual(self.original_application.have_goods_departed, self.copied_application.have_goods_departed) - - self._validate_goodstype() - - self._validate_end_user() - self._validate_consignee() - self._validate_ultimate_end_user() - self._validate_third_party() - - self._validate_case_data() - def _validate_reset_data(self): self.assertNotEqual(self.copied_application.id, self.original_application.id) self.assertEqual(self.copied_application.copy_of.id, self.original_application.id) diff --git a/api/applications/tests/test_create_application.py b/api/applications/tests/test_create_application.py index 3a27230ba4..3131630c0f 100644 --- a/api/applications/tests/test_create_application.py +++ b/api/applications/tests/test_create_application.py @@ -11,9 +11,6 @@ StandardApplication, OpenApplication, BaseApplication, - ExhibitionClearanceApplication, - GiftingClearanceApplication, - F680ClearanceApplication, ) from api.cases.enums import CaseTypeEnum, CaseTypeReferenceEnum from lite_content.lite_api import strings @@ -63,60 +60,6 @@ def test_create_draft_standard_individual_export_application_empty_export_type_s self.assertEqual(response_data["id"], str(standard_application.id)) self.assertEqual(StandardApplication.objects.count(), 1) - def test_create_draft_exhibition_clearance_application_successful(self): - """ - Ensure we can create a new Exhibition Clearance draft object - """ - self.assertEqual(ExhibitionClearanceApplication.objects.count(), 0) - - data = { - "name": "Test", - "application_type": CaseTypeReferenceEnum.EXHC, - } - - response = self.client.post(self.url, data, **self.exporter_headers) - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(ExhibitionClearanceApplication.objects.count(), 1) - - def test_create_draft_gifting_clearance_application_successful(self): - """ - Ensure we can create a new Gifting Clearance draft object - """ - self.assertEqual(GiftingClearanceApplication.objects.count(), 0) - - data = { - "name": "Test", - "application_type": CaseTypeReferenceEnum.GIFT, - } - - response = self.client.post(self.url, data, **self.exporter_headers) - application = GiftingClearanceApplication.objects.get() - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(GiftingClearanceApplication.objects.count(), 1) - self.assertEqual(application.name, data["name"]) - self.assertEqual(application.case_type.id, CaseTypeEnum.GIFTING.id) - - def test_create_draft_f680_clearance_application_successful(self): - """ - Ensure we can create a new F680 Clearance draft object - """ - self.assertEqual(F680ClearanceApplication.objects.count(), 0) - - data = { - "name": "Test", - "application_type": CaseTypeReferenceEnum.F680, - } - - response = self.client.post(self.url, data, **self.exporter_headers) - application = F680ClearanceApplication.objects.get() - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(F680ClearanceApplication.objects.count(), 1) - self.assertEqual(application.name, data["name"]) - self.assertEqual(application.case_type.id, CaseTypeEnum.F680.id) - def test_create_draft_open_application_successful(self): """ Ensure we can create a new open application draft object diff --git a/api/applications/tests/test_delete_application.py b/api/applications/tests/test_delete_application.py index 4eb6a1979b..8924a1e383 100644 --- a/api/applications/tests/test_delete_application.py +++ b/api/applications/tests/test_delete_application.py @@ -1,96 +1,52 @@ from django.urls import reverse -from parameterized import parameterized from rest_framework.status import HTTP_200_OK, HTTP_400_BAD_REQUEST, HTTP_403_FORBIDDEN from api.applications.models import BaseApplication -from api.cases.enums import CaseTypeSubTypeEnum, CaseTypeEnum -from lite_content.lite_api import strings from test_helpers.clients import DataTestClient class DeleteApplication(DataTestClient): def setUp(self): super().setUp() - self.applications = { - CaseTypeSubTypeEnum.STANDARD: self.create_draft_standard_application(self.organisation), - CaseTypeSubTypeEnum.EXHIBITION: self.create_mod_clearance_application( - self.organisation, case_type=CaseTypeEnum.EXHIBITION - ), - CaseTypeSubTypeEnum.GIFTING: self.create_mod_clearance_application( - self.organisation, case_type=CaseTypeEnum.GIFTING - ), - CaseTypeSubTypeEnum.F680: self.create_mod_clearance_application( - self.organisation, case_type=CaseTypeEnum.F680 - ), - } - self.users = {"EXPORTER": self.exporter_headers, "GOV": self.gov_headers, "HMRC": self.hmrc_exporter_headers} + self.draft = self.create_draft_standard_application(self.organisation) - @parameterized.expand( - [ - (CaseTypeSubTypeEnum.STANDARD, "EXPORTER"), - (CaseTypeSubTypeEnum.EXHIBITION, "EXPORTER"), - (CaseTypeSubTypeEnum.GIFTING, "EXPORTER"), - (CaseTypeSubTypeEnum.F680, "EXPORTER"), - ] - ) - def test_delete_draft_application_as_valid_user_success(self, application_type, user): + def test_delete_draft_application_as_valid_user_success(self): """ Tests that applications can be deleted by their owners when in a draft state """ - draft = self.applications[application_type] - headers = self.users[user] number_of_applications = BaseApplication.objects.all().count() - url = reverse("applications:application", kwargs={"pk": draft.id}) + url = reverse("applications:application", kwargs={"pk": self.draft.id}) - response = self.client.delete(url, **headers) + response = self.client.delete(url, **self.exporter_headers) self.assertEqual(response.status_code, HTTP_200_OK) - self.assertEqual(response.json()["status"], strings.Applications.Generic.DELETE_DRAFT_APPLICATION) + self.assertEqual(response.json()["status"], "Draft application deleted") self.assertEqual(number_of_applications - 1, BaseApplication.objects.all().count()) - self.assertTrue(draft not in BaseApplication.objects.all()) + self.assertNotIn(self.draft, BaseApplication.objects.all()) - @parameterized.expand( - [ - (CaseTypeSubTypeEnum.STANDARD, "GOV"), - (CaseTypeSubTypeEnum.EXHIBITION, "GOV"), - (CaseTypeSubTypeEnum.GIFTING, "GOV"), - (CaseTypeSubTypeEnum.F680, "GOV"), - ] - ) - def test_delete_draft_application_as_invalid_user_failure(self, application_type, user): + def test_delete_draft_application_as_invalid_user_failure(self): """ Tests that applications cannot be deleted by users who do not own the application """ - draft = self.applications[application_type] - headers = self.users[user] number_of_applications = BaseApplication.objects.all().count() - url = reverse("applications:application", kwargs={"pk": draft.id}) + url = reverse("applications:application", kwargs={"pk": self.draft.id}) - response = self.client.delete(url, **headers) + response = self.client.delete(url, **self.gov_headers) self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) self.assertEqual(number_of_applications, BaseApplication.objects.all().count()) - @parameterized.expand( - [ - (CaseTypeSubTypeEnum.STANDARD, "EXPORTER"), - (CaseTypeSubTypeEnum.EXHIBITION, "EXPORTER"), - (CaseTypeSubTypeEnum.GIFTING, "EXPORTER"), - (CaseTypeSubTypeEnum.F680, "EXPORTER"), - ] - ) - def test_delete_submitted_application_failure(self, application_type, user): + def test_delete_submitted_application_failure(self): """ Tests that applications cannot be deleted after they have been submitted """ - application = self.applications[application_type] - headers = self.users[user] + application = self.draft self.submit_application(application) number_of_applications = BaseApplication.objects.all().count() url = reverse("applications:application", kwargs={"pk": application.id}) - response = self.client.delete(url, **headers) + response = self.client.delete(url, **self.exporter_headers) self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) - self.assertEqual(response.json()["errors"], strings.Applications.Generic.DELETE_SUBMITTED_APPLICATION_ERROR) + self.assertEqual(response.json()["errors"], "Only draft applications can be deleted") self.assertEqual(number_of_applications, BaseApplication.objects.all().count()) diff --git a/api/applications/tests/test_edit_application.py b/api/applications/tests/test_edit_application.py index 9b052ebb3d..bfc5b40929 100644 --- a/api/applications/tests/test_edit_application.py +++ b/api/applications/tests/test_edit_application.py @@ -1,17 +1,10 @@ -import datetime - from django.urls import reverse -from parameterized import parameterized, parameterized_class +from parameterized import parameterized from rest_framework import status from api.applications.libraries.case_status_helpers import get_case_statuses from api.audit_trail.enums import AuditType from api.audit_trail.models import Audit -from api.cases.enums import CaseTypeEnum -from api.goods.enums import PvGrading -from lite_content.lite_api import strings -from api.parties.enums import PartyType, SubType -from api.staticdata.f680_clearance_types.enums import F680ClearanceTypeEnum from api.staticdata.statuses.enums import CaseStatusEnum from api.staticdata.statuses.libraries.get_case_status import get_case_status_by_status from test_helpers.clients import DataTestClient @@ -166,491 +159,3 @@ def test_edit_submitted_application_reference_number(self): self.assertEqual(response.status_code, status.HTTP_200_OK) audit = Audit.objects.get(verb=AuditType.REMOVED_APPLICATION_LETTER_REFERENCE) self.assertEqual(audit.payload, {"old_ref_number": "no reference"}) - - -@parameterized_class( - "case_type", - [ - (CaseTypeEnum.EXHIBITION,), - (CaseTypeEnum.GIFTING,), - (CaseTypeEnum.F680,), - ], -) -class EditMODClearanceApplicationsTests(DataTestClient): - def setUp(self): - super().setUp() - self.application = self.create_mod_clearance_application(self.organisation, case_type=self.case_type) - self.url = reverse("applications:application", kwargs={"pk": self.application.id}) - self.data = {"name": "abc"} - - def test_edit_unsubmitted_application_name_success(self): - updated_at = self.application.updated_at - - response = self.client.put(self.url, self.data, **self.exporter_headers) - - self.application.refresh_from_db() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(self.application.name, self.data["name"]) - self.assertNotEqual(self.application.updated_at, updated_at) - # Unsubmitted (draft) applications should not create audit entries when edited - self.assertEqual(Audit.objects.count(), 0) - - @parameterized.expand(get_case_statuses(read_only=False)) - def test_edit_application_name_in_editable_status_success(self, editable_status): - old_name = self.application.name - self.submit_application(self.application) - self.application.status = get_case_status_by_status(editable_status) - self.application.save() - updated_at = self.application.updated_at - - response = self.client.put(self.url, self.data, **self.exporter_headers) - self.application.refresh_from_db() - audit_object = Audit.objects.get(verb=AuditType.UPDATED_APPLICATION_NAME) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(self.application.name, self.data["name"]) - self.assertNotEqual(self.application.updated_at, updated_at) - self.assertEqual(audit_object.payload, {"new_name": self.data["name"], "old_name": old_name}) - - @parameterized.expand(get_case_statuses(read_only=True)) - def test_edit_application_name_in_read_only_status_failure(self, read_only_status): - self.submit_application(self.application) - self.application.status = get_case_status_by_status(read_only_status) - self.application.save() - - response = self.client.put(self.url, self.data, **self.exporter_headers) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - -class EditF680ApplicationsTests(DataTestClient): - def setUp(self): - super().setUp() - self.application = self.create_mod_clearance_application(self.organisation, case_type=CaseTypeEnum.F680) - self.url = reverse("applications:application", kwargs={"pk": self.application.id}) - - @parameterized.expand(["", "1", "2", "clearance"]) - def test_add_clearance_level_invalid_inputs(self, level): - data = {"clearance_level": level} - - response = self.client.put(self.url, data=data, **self.exporter_headers) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - @parameterized.expand([p[0] for p in PvGrading.choices]) - def test_add_clearance_level_success(self, level): - data = {"clearance_level": level} - - response = self.client.put(self.url, data=data, **self.exporter_headers) - self.application.refresh_from_db() - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(self.application.clearance_level, level) - - def test_edit_submitted_application_clearance_level_minor_fail(self): - """Test successful editing of an application's reference number when the application's status - is non read-only. - """ - application = self.create_mod_clearance_application(self.organisation, CaseTypeEnum.F680) - url = reverse("applications:application", kwargs={"pk": application.id}) - self.submit_application(application) - - data = {"clearance_level": PvGrading.NATO_CONFIDENTIAL} - - response = self.client.put(url, data=data, **self.exporter_headers) - self.application.refresh_from_db() - self.assertEqual( - response.json()["errors"], {"clearance_level": [strings.Applications.Generic.NOT_POSSIBLE_ON_MINOR_EDIT]} - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - def test_edit_submitted_application_clearance_level_major_success(self): - """Test successful editing of an application's reference number when the application's status - is non read-only. - """ - application = self.create_mod_clearance_application(self.organisation, CaseTypeEnum.F680) - url = reverse("applications:application", kwargs={"pk": application.id}) - self.submit_application(application) - application.status = get_case_status_by_status(CaseStatusEnum.APPLICANT_EDITING) - application.save() - - data = {"clearance_level": PvGrading.NATO_CONFIDENTIAL} - - response = self.client.put(url, data=data, **self.exporter_headers) - application.refresh_from_db() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(application.clearance_level, data["clearance_level"]) - - def test_edit_submitted_application_clearance_type_minor_fail(self): - application = self.create_mod_clearance_application(self.organisation, CaseTypeEnum.F680) - url = reverse("applications:application", kwargs={"pk": application.id}) - self.submit_application(application) - - data = {"types": [F680ClearanceTypeEnum.MARKET_SURVEY]} - response = self.client.put(url, data=data, **self.exporter_headers) - - self.application.refresh_from_db() - self.assertEqual( - response.json()["errors"], {"types": [strings.Applications.Generic.NOT_POSSIBLE_ON_MINOR_EDIT]} - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - def test_edit_submitted_application_clearance_type_major_success(self): - application = self.create_mod_clearance_application(self.organisation, CaseTypeEnum.F680) - url = reverse("applications:application", kwargs={"pk": application.id}) - self.submit_application(application) - application.status = get_case_status_by_status(CaseStatusEnum.APPLICANT_EDITING) - application.save() - - data = {"types": [F680ClearanceTypeEnum.DEMONSTRATION_IN_THE_UK_TO_OVERSEAS_CUSTOMERS]} - response = self.client.put(url, data=data, **self.exporter_headers) - - application.refresh_from_db() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual( - application.types.get().name, F680ClearanceTypeEnum.DEMONSTRATION_IN_THE_UK_TO_OVERSEAS_CUSTOMERS - ) - - # Check add audit - audit = Audit.objects.get(verb=AuditType.UPDATE_APPLICATION_F680_CLEARANCE_TYPES) - self.assertEqual( - audit.payload, - { - "old_types": [F680ClearanceTypeEnum.get_text(F680ClearanceTypeEnum.MARKET_SURVEY)], - "new_types": [F680ClearanceTypeEnum.get_text(type) for type in data["types"]], - }, - ) - - def test_edit_submitted_application_clearance_type_no_data_failure(self): - application = self.create_mod_clearance_application(self.organisation, CaseTypeEnum.F680) - url = reverse("applications:application", kwargs={"pk": application.id}) - self.submit_application(application) - application.status = get_case_status_by_status(CaseStatusEnum.APPLICANT_EDITING) - application.save() - - data = {"types": []} - response = self.client.put(url, data=data, **self.exporter_headers) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual( - response.json()["errors"], - {"types": [strings.Applications.F680.NO_CLEARANCE_TYPE]}, - ) - - def test_add_party_to_f680_success(self): - party = { - "type": PartyType.THIRD_PARTY, - "name": "Government of Paraguay", - "address": "Asuncion", - "country": "PY", - "sub_type": SubType.GOVERNMENT, - "website": "https://www.gov.py", - "role": "agent", - "clearance_level": PvGrading.UK_OFFICIAL, - } - url = reverse("applications:parties", kwargs={"pk": self.application.id}) - response = self.client.post(url, data=party, **self.exporter_headers) - - self.application.refresh_from_db() - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - def test_add_party_no_clearance_to_f680_failure(self): - party = { - "type": PartyType.THIRD_PARTY, - "name": "Government of Paraguay", - "address": "Asuncion", - "country": "PY", - "sub_type": "government", - "website": "https://www.gov.py", - "role": "agent", - } - url = reverse("applications:parties", kwargs={"pk": self.application.id}) - response = self.client.post(url, data=party, **self.exporter_headers) - - self.application.refresh_from_db() - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.json()["errors"], {"clearance_level": ["This field is required."]}) - - -class EditExhibitionApplicationsTests(DataTestClient): - def setUp(self): - super().setUp() - self.application = self.create_mod_clearance_application(self.organisation, case_type=CaseTypeEnum.EXHIBITION) - self.exhibition_url = reverse("applications:exhibition", kwargs={"pk": self.application.id}) - - def test_edit_exhibition_title_in_draft_success(self): - data = { - "title": "new_title", - "required_by_date": self.application.required_by_date, - "first_exhibition_date": self.application.first_exhibition_date, - } - - response = self.client.post(self.exhibition_url, data=data, **self.exporter_headers) - - response_data = response.json()["application"] - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response_data["title"], data["title"]) - - def test_edit_exhibition_title_in_draft_failure_blank(self): - data = { - "title": "", - "required_by_date": self.application.required_by_date, - "first_exhibition_date": self.application.first_exhibition_date, - } - - response = self.client.post(self.exhibition_url, data=data, **self.exporter_headers) - - response_data = response.json()["errors"] - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response_data["title"][0], strings.Applications.Exhibition.Error.NO_EXHIBITION_NAME) - - def test_edit_exhibition_title_in_draft_failure_none(self): - data = { - "title": None, - "required_by_date": self.application.required_by_date, - "first_exhibition_date": self.application.first_exhibition_date, - } - - response = self.client.post(self.exhibition_url, data=data, **self.exporter_headers) - - response_data = response.json()["errors"] - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response_data["title"][0], strings.Applications.Exhibition.Error.NO_EXHIBITION_NAME) - - def test_edit_exhibition_title_in_draft_failure_not_given(self): - data = { - "required_by_date": self.application.required_by_date, - "first_exhibition_date": self.application.first_exhibition_date, - } - - response = self.client.post(self.exhibition_url, data=data, **self.exporter_headers) - response_data = response.json()["errors"] - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response_data["title"][0], strings.Applications.Exhibition.Error.NO_EXHIBITION_NAME) - - def test_edit_exhibition_required_by_date_draft_success(self): - required_by_date = datetime.date.today() + datetime.timedelta(days=5) - required_by_date = required_by_date.isoformat() - - data = { - "title": self.application.title, - "required_by_date": required_by_date, - "first_exhibition_date": self.application.first_exhibition_date, - } - - response = self.client.post(self.exhibition_url, data=data, **self.exporter_headers) - response_data = response.json()["application"] - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response_data["required_by_date"], required_by_date) - - def test_edit_exhibition_required_by_date_later_than_first_exhibition_date_draft_failure(self): - data = { - "title": self.application.title, - "required_by_date": "2220-05-15", - "first_exhibition_date": self.application.first_exhibition_date, - } - - response = self.client.post(self.exhibition_url, data=data, **self.exporter_headers) - - response_data = response.json()["errors"] - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual( - response_data["first_exhibition_date"][0], - strings.Applications.Exhibition.Error.REQUIRED_BY_BEFORE_FIRST_EXHIBITION_DATE, - ) - - def test_edit_exhibition_required_by_date_draft_failure_blank(self): - data = { - "title": self.application.title, - "required_by_date": "", - "first_exhibition_date": self.application.first_exhibition_date, - } - - response = self.client.post(self.exhibition_url, data=data, **self.exporter_headers) - - response_data = response.json()["errors"] - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual( - response_data["required_by_date"][0], - strings.Applications.Exhibition.Error.BLANK_REQUIRED_BY_DATE, - ) - - def test_edit_exhibition_required_by_date_draft_failure_not_given(self): - data = { - "title": self.application.title, - "first_exhibition_date": self.application.first_exhibition_date, - } - - response = self.client.post(self.exhibition_url, data=data, **self.exporter_headers) - - response_data = response.json()["errors"] - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual( - response_data["required_by_date"][0], - strings.Applications.Exhibition.Error.NO_REQUIRED_BY_DATE, - ) - - def test_edit_exhibition_required_by_date_draft_failure_none(self): - data = { - "title": self.application.title, - "first_exhibition_date": self.application.first_exhibition_date, - "required_by_date": None, - } - - response = self.client.post(self.exhibition_url, data=data, **self.exporter_headers) - - response_data = response.json()["errors"] - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual( - response_data["required_by_date"][0], - strings.Applications.Exhibition.Error.NO_REQUIRED_BY_DATE, - ) - - def test_edit_exhibition_first_exhibition_date_draft_success(self): - data = { - "title": self.application.title, - "required_by_date": self.application.required_by_date, - "first_exhibition_date": "2030-08-03", - } - - response = self.client.post(self.exhibition_url, data=data, **self.exporter_headers) - - response_data = response.json()["application"] - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response_data["first_exhibition_date"], data["first_exhibition_date"]) - - def test_edit_exhibition_first_exhibition_date_draft_failure_before_today(self): - data = { - "title": self.application.title, - "required_by_date": self.application.required_by_date, - "first_exhibition_date": "2018-05-03", - } - - response = self.client.post(self.exhibition_url, data=data, **self.exporter_headers) - - response_data = response.json()["errors"] - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual( - response_data["first_exhibition_date"][0], - strings.Applications.Exhibition.Error.FIRST_EXHIBITION_DATE_FUTURE, - ) - - def test_can_not_edit_exhibition_details_in_minor_edit(self): - self.submit_application(self.application) - # same data as success - data = { - "title": "new_title", - "required_by_date": self.application.required_by_date, - "first_exhibition_date": self.application.first_exhibition_date, - } - - response = self.client.post(self.exhibition_url, data=data, **self.exporter_headers) - - response_data = response.json()["errors"]["non_field_errors"] - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual( - response_data, - [strings.Applications.Generic.INVALID_OPERATION_FOR_NON_DRAFT_OR_MAJOR_EDIT_CASE_ERROR], - ) - - def test_can_edit_exhibition_details_in_major_edit(self): - self.submit_application(self.application) - self.application.status = get_case_status_by_status(CaseStatusEnum.APPLICANT_EDITING) - self.application.save() - # same data as success - data = { - "title": "new_title", - "required_by_date": self.application.required_by_date, - "first_exhibition_date": self.application.first_exhibition_date, - } - - response = self.client.post(self.exhibition_url, data=data, **self.exporter_headers) - - response_data = response.json()["application"] - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response_data["title"], data["title"]) - - def test_add_third_party_exhibition_clearance_failure(self): - party = { - "type": PartyType.THIRD_PARTY, - "name": "Government of Paraguay", - "address": "Asuncion", - "country": "PY", - "sub_type": "government", - "website": "https://www.gov.py", - "role": "agent", - } - url = reverse("applications:parties", kwargs={"pk": self.application.id}) - response = self.client.post(url, data=party, **self.exporter_headers) - - self.application.refresh_from_db() - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.json()["errors"], {"bad_request": strings.PartyErrors.BAD_CASE_TYPE}) - - def test_add_consignee_exhibition_clearance_failure(self): - party = { - "type": PartyType.CONSIGNEE, - "name": "Government of Paraguay", - "address": "Asuncion", - "country": "PY", - "sub_type": "government", - "website": "https://www.gov.py", - "role": "agent", - } - url = reverse("applications:parties", kwargs={"pk": self.application.id}) - response = self.client.post(url, data=party, **self.exporter_headers) - - self.application.refresh_from_db() - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.json()["errors"], {"bad_request": strings.PartyErrors.BAD_CASE_TYPE}) - - def test_add_end_user_exhibition_clearance_failure(self): - party = { - "type": PartyType.END_USER, - "name": "Government of Paraguay", - "address": "Asuncion", - "signatory_name_euu": "Government of Paraguay", - "country": "PY", - "sub_type": "government", - "website": "https://www.gov.py", - "role": "agent", - } - url = reverse("applications:parties", kwargs={"pk": self.application.id}) - response = self.client.post(url, data=party, **self.exporter_headers) - - self.application.refresh_from_db() - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.json()["errors"], {"bad_request": strings.PartyErrors.BAD_CASE_TYPE}) - - def test_add_ultimate_end_user_exhibition_clearance_failure(self): - party = { - "type": PartyType.ULTIMATE_END_USER, - "name": "Government of Paraguay", - "address": "Asuncion", - "country": "PY", - "sub_type": "government", - "website": "https://www.gov.py", - "role": "agent", - } - url = reverse("applications:parties", kwargs={"pk": self.application.id}) - response = self.client.post(url, data=party, **self.exporter_headers) - - self.application.refresh_from_db() - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.json()["errors"], {"bad_request": strings.PartyErrors.BAD_CASE_TYPE}) diff --git a/api/applications/tests/test_edit_end_use_details.py b/api/applications/tests/test_edit_end_use_details.py index 167d148d9d..1a85c87f6b 100644 --- a/api/applications/tests/test_edit_end_use_details.py +++ b/api/applications/tests/test_edit_end_use_details.py @@ -2,9 +2,7 @@ from parameterized import parameterized from rest_framework import status -from api.audit_trail.enums import AuditType from api.audit_trail.models import Audit -from api.cases.enums import CaseTypeEnum from lite_content.lite_api import strings from api.staticdata.statuses.enums import CaseStatusEnum from api.staticdata.statuses.libraries.get_case_status import get_case_status_by_status @@ -516,44 +514,3 @@ def test_edit_open_application_end_use_details_intended_end_use_is_empty(self): response.json()["errors"]["intended_end_use"], [strings.Applications.Generic.EndUseDetails.Error.INTENDED_END_USE], ) - - -class EditF680ApplicationTests(DataTestClient): - def setUp(self): - super().setUp() - self.application = self.create_mod_clearance_application(self.organisation, case_type=CaseTypeEnum.F680) - self.url = reverse("applications:end_use_details", kwargs={"pk": self.application.id}) - - def test_edit_f680_application_end_use_details_intended_end_use(self): - self.application.status = get_case_status_by_status(CaseStatusEnum.APPLICANT_EDITING) - self.application.save() - - data = { - "intended_end_use": "this is the intended end use", - } - - response = self.client.put(self.url, data, **self.exporter_headers) - - self.application.refresh_from_db() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(self.application.intended_end_use, data["intended_end_use"]) - self.assertEqual(Audit.objects.count(), 1) - - def test_edit_f680_application_end_use_details_intended_end_use_is_empty_failure(self): - self.submit_application(self.application) - self.application.status = get_case_status_by_status(CaseStatusEnum.APPLICANT_EDITING) - self.application.save() - - data = { - "intended_end_use": "", - } - - response = self.client.put(self.url, data, **self.exporter_headers) - - self.application.refresh_from_db() - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(len(response.json()["errors"]), 1) - self.assertEqual( - response.json()["errors"]["intended_end_use"], - [strings.Applications.Generic.EndUseDetails.Error.INTENDED_END_USE], - ) diff --git a/api/applications/tests/test_edit_temporary_export_details.py b/api/applications/tests/test_edit_temporary_export_details.py index c94ab4193e..8329645ca2 100644 --- a/api/applications/tests/test_edit_temporary_export_details.py +++ b/api/applications/tests/test_edit_temporary_export_details.py @@ -6,7 +6,6 @@ from api.applications.enums import ApplicationExportType from lite_content.lite_api import strings from api.audit_trail.models import Audit -from api.cases.enums import CaseTypeEnum from api.staticdata.statuses.enums import CaseStatusEnum from api.staticdata.statuses.libraries.get_case_status import get_case_status_by_status from test_helpers.clients import DataTestClient @@ -31,18 +30,6 @@ def test_perform_action_on_non_temporary_export_type_standard_applications_failu {"temp_export_details": ["Cannot update temporary export details for a permanent export type"]}, ) - def test_perform_action_on_non_open_or_standard_applications_failure(self): - permanent_application = self.create_mod_clearance_application( - self.organisation, case_type=CaseTypeEnum.EXHIBITION - ) - url = reverse("applications:temporary_export_details", kwargs={"pk": permanent_application.id}) - response = self.client.put(url, {}, **self.exporter_headers) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual( - response.json()["errors"], ["This operation can only be used on applications of type: open, standard"] - ) - def test_edit_unsubmitted_standard_application_all_temporary_export_details_success(self): date = timezone.now().date() + timezone.timedelta(days=2) diff --git a/api/applications/tests/test_finalise_application.py b/api/applications/tests/test_finalise_application.py index 8e37413b6b..30a5ca95a6 100644 --- a/api/applications/tests/test_finalise_application.py +++ b/api/applications/tests/test_finalise_application.py @@ -13,7 +13,7 @@ from api.audit_trail.enums import AuditType from api.audit_trail.models import Audit from api.audit_trail.serializers import AuditSerializer -from api.cases.enums import AdviceType, CaseTypeEnum, AdviceLevel, CountersignOrder +from api.cases.enums import AdviceType, AdviceLevel, CountersignOrder from api.cases.models import Advice, Case from api.cases.tests.factories import CountersignAdviceFactory, FinalAdviceFactory, UserAdviceFactory from api.core.constants import GovPermissions @@ -157,28 +157,6 @@ def test_approve_application_blocking_flags_failure(self): self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response_data["errors"], [f"{strings.Applications.Finalise.Error.BLOCKING_FLAGS}{flag.name}"]) - def test_finalise_clearance_application_success(self): - clearance_application = self.create_mod_clearance_application_case( - self.organisation, case_type=CaseTypeEnum.EXHIBITION - ) - self._set_user_permission( - [GovPermissions.MANAGE_CLEARANCE_FINAL_ADVICE, GovPermissions.MANAGE_LICENCE_DURATION] - ) - data = {"action": AdviceType.APPROVE, "duration": 13} - data.update(self.post_date) - - url = reverse("applications:finalise", kwargs={"pk": clearance_application.pk}) - response = self.client.put(url, data=data, **self.gov_headers) - response_data = response.json() - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response_data["case"], str(clearance_application.id)) - self.assertEqual(response_data["reference_code"], f"{clearance_application.reference_code}") - self.assertEqual(response_data["start_date"], self.date.strftime("%Y-%m-%d")) - self.assertEqual(response_data["duration"], data["duration"]) - self.assertEqual(response_data["status"], LicenceStatus.DRAFT) - self.assertTrue(Licence.objects.filter(case=clearance_application, status=LicenceStatus.DRAFT).exists()) - def test_set_duration_permission_denied(self): self._set_user_permission([GovPermissions.MANAGE_LICENCE_FINAL_ADVICE]) data = {"action": AdviceType.APPROVE, "duration": 13} diff --git a/api/applications/tests/test_mod_clearance_submit.py b/api/applications/tests/test_mod_clearance_submit.py deleted file mode 100644 index d0875d1e72..0000000000 --- a/api/applications/tests/test_mod_clearance_submit.py +++ /dev/null @@ -1,446 +0,0 @@ -from unittest import mock - -from django.urls import reverse -from parameterized import parameterized_class -from rest_framework import status - -from api.applications.models import ( - SiteOnApplication, - GoodOnApplication, - ExhibitionClearanceApplication, - F680ClearanceApplication, - GiftingClearanceApplication, -) -from api.cases.enums import CaseTypeEnum, CaseDocumentState -from api.cases.models import CaseDocument -from api.core.constants import AutoGeneratedDocuments -from lite_content.lite_api import strings -from api.parties.enums import PartyType -from api.parties.models import PartyDocument -from api.staticdata.statuses.enums import CaseStatusEnum -from test_helpers.clients import DataTestClient - - -@parameterized_class( - "case_type", - [ - (CaseTypeEnum.EXHIBITION,), - (CaseTypeEnum.GIFTING,), - (CaseTypeEnum.F680,), - ], -) -class MODClearanceTests(DataTestClient): - """ - Shared MOD clearance tests. - Covers elements MOD clearances have in common like the requirement - for goods & locations. - """ - - def setUp(self): - super().setUp() - self.draft = self.create_mod_clearance_application(self.organisation, case_type=self.case_type) - self.url = reverse("applications:application_submit", kwargs={"pk": self.draft.id}) - self.exporter_user.set_role(self.organisation, self.exporter_super_user_role) - - def test_submit_MOD_clearance_success(self): - response = self.client.put(self.url, **self.exporter_headers) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.json()["application"]["name"], self.draft.name) - - def test_submit_MOD_clearance_without_goods_failure(self): - GoodOnApplication.objects.get(application=self.draft).delete() - - response = self.client.put(self.url, **self.exporter_headers) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.json()["errors"]["goods"], [strings.Applications.Standard.NO_GOODS_SET]) - - -class ExhibitionClearanceTests(DataTestClient): - def setUp(self): - super().setUp() - self.draft = self.create_mod_clearance_application(self.organisation, case_type=CaseTypeEnum.EXHIBITION) - self.url = reverse("applications:application_submit", kwargs={"pk": self.draft.id}) - self.exporter_user.set_role(self.organisation, self.exporter_super_user_role) - - def test_submit_exhibition_clearance_success(self): - response = self.client.put(self.url, **self.exporter_headers) - application = ExhibitionClearanceApplication.objects.get() - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.json()["application"]["name"], self.draft.name) - self.assertEqual(ExhibitionClearanceApplication.objects.count(), 1) - self.assertEqual(list(application.third_parties.all()), []) - self.assertIsNone(application.end_user) - self.assertIsNone(application.consignee) - self.assertIsNone(application.submitted_at) - self.assertEqual(application.status.status, CaseStatusEnum.DRAFT) - self.assertIsNotNone(application.goods.get()) - - def test_submit_exhibition_clearance_without_location_failure(self): - SiteOnApplication.objects.get(application=self.draft).delete() - - response = self.client.put(self.url, **self.exporter_headers) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.json()["errors"]["location"], [strings.Applications.Generic.NO_LOCATION_SET]) - - def test_submit_exhibition_clearance_without_details_failure(self): - self.draft.title, self.draft.first_exhibition_date, self.draft.required_by_date = None, None, None - self.draft.save() - - response = self.client.put(self.url, **self.exporter_headers) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.json()["errors"]["details"], [strings.Applications.Exhibition.Error.NO_DETAILS]) - - def test_submit_exhibition_clearance_without_goods_failure(self): - GoodOnApplication.objects.get(application=self.draft).delete() - - response = self.client.put(self.url, **self.exporter_headers) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.json()["errors"]["goods"], [strings.Applications.Standard.NO_GOODS_SET]) - - @mock.patch("api.documents.libraries.s3_operations.upload_bytes_file") - @mock.patch("api.cases.generated_documents.helpers.html_to_pdf") - def test_exhibition_clearance_declaration_submit_success(self, upload_bytes_file_func, html_to_pdf_func): - upload_bytes_file_func.return_value = None - html_to_pdf_func.return_value = None - - data = { - "submit_declaration": True, - "agreed_to_declaration": True, - "agreed_to_foi": True, - "foi_reason": "Because", - "agreed_to_declaration_text": "I Agree", - } - application = ExhibitionClearanceApplication.objects.get() - self.assertEqual(application.status.status, CaseStatusEnum.DRAFT) - - url = reverse("applications:application_submit", kwargs={"pk": self.draft.id}) - response = self.client.put(url, data, **self.exporter_headers) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - - application.refresh_from_db() - self.assertIsNotNone(application.submitted_at) - self.assertNotEqual(application.status.status, CaseStatusEnum.DRAFT) - self.assertEqual(application.agreed_to_foi, True) - self.assertEqual(application.submitted_by, self.exporter_user) - # Asserting that the 'Application Form' has been autogenerated on submission of the application - html_to_pdf_func.assert_called_once() - upload_bytes_file_func.assert_called_once() - self.assertEqual( - CaseDocument.objects.filter( - name__contains=AutoGeneratedDocuments.APPLICATION_FORM, - type=CaseDocumentState.AUTO_GENERATED, - safe=True, - case=application, - visible_to_exporter=False, - ).count(), - 1, - ) - - def test_exhibition_clearance_declaration_submit_tcs_false_failure(self): - data = { - "submit_declaration": True, - "agreed_to_declaration": False, - "agreed_to_foi": True, - "foi_reason": "Because", - } - - url = reverse("applications:application_submit", kwargs={"pk": self.draft.id}) - response = self.client.put(url, data, **self.exporter_headers) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - errors = response.json()["errors"] - self.assertEqual( - errors["agreed_to_declaration_text"], - ["To submit the application, you must confirm that you agree by typing “I AGREE”"], - ) - - -class GiftingClearanceTests(DataTestClient): - def setUp(self): - super().setUp() - self.draft = self.create_mod_clearance_application(self.organisation, case_type=CaseTypeEnum.GIFTING) - self.url = reverse("applications:application_submit", kwargs={"pk": self.draft.id}) - self.exporter_user.set_role(self.organisation, self.exporter_super_user_role) - - def test_submit_gifting_clearance_success(self): - response = self.client.put(self.url, **self.exporter_headers) - application = GiftingClearanceApplication.objects.get() - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.json()["application"]["name"], self.draft.name) - self.assertEqual(GiftingClearanceApplication.objects.count(), 1) - self.assertIsNotNone(application.third_parties.get()) - self.assertIsNotNone(application.end_user) - self.assertIsNone(application.submitted_at) - self.assertEqual(application.status.status, CaseStatusEnum.DRAFT) - self.assertIsNotNone(application.goods.get()) - - def test_submit_gifting_clearance_without_end_user_failure(self): - self.draft.delete_party(self.draft.end_user) - - response = self.client.put(self.url, **self.exporter_headers) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.json()["errors"]["end_user"], [strings.Applications.Standard.NO_END_USER_SET]) - - def test_submit_gifting_clearance_without_end_user_document_success(self): - PartyDocument.objects.filter(party=self.draft.end_user.party).delete() - - response = self.client.put(self.url, **self.exporter_headers) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.json()["application"]["name"], self.draft.name) - - def test_submit_gifting_with_consignee_failure(self): - self.create_party("Consignee", self.organisation, PartyType.CONSIGNEE, self.draft) - - response = self.client.put(self.url, **self.exporter_headers) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.json()["errors"]["consignee"], [strings.Applications.Gifting.CONSIGNEE]) - - def test_submit_gifting_with_ultimate_end_user_failure(self): - self.create_party("Ultimate End User", self.organisation, PartyType.ULTIMATE_END_USER, self.draft) - - response = self.client.put(self.url, **self.exporter_headers) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual( - response.json()["errors"]["ultimate_end_users"], [strings.Applications.Gifting.ULTIMATE_END_USERS] - ) - - def test_submit_gifting_clearance_with_location_failure(self): - SiteOnApplication(site=self.organisation.primary_site, application=self.draft).save() - - response = self.client.put(self.url, **self.exporter_headers) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.json()["errors"]["location"], [strings.Applications.Gifting.LOCATIONS]) - - @mock.patch("api.documents.libraries.s3_operations.upload_bytes_file") - @mock.patch("api.cases.generated_documents.helpers.html_to_pdf") - def test_gifting_clearance_declaration_submit_success(self, upload_bytes_file_func, html_to_pdf_func): - upload_bytes_file_func.return_value = None - html_to_pdf_func.return_value = None - - data = { - "submit_declaration": True, - "agreed_to_declaration": True, - "agreed_to_foi": True, - "foi_reason": "Because", - "agreed_to_declaration_text": "I Agree", - } - application = GiftingClearanceApplication.objects.get() - self.assertEqual(application.status.status, CaseStatusEnum.DRAFT) - - url = reverse("applications:application_submit", kwargs={"pk": self.draft.id}) - response = self.client.put(url, data, **self.exporter_headers) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - - application.refresh_from_db() - self.assertIsNotNone(application.submitted_at) - self.assertNotEqual(application.status.status, CaseStatusEnum.DRAFT) - self.assertEqual(application.agreed_to_foi, True) - self.assertEqual(application.submitted_by, self.exporter_user) - # Asserting that the 'Application Form' has been autogenerated on submission of the application - html_to_pdf_func.assert_called_once() - upload_bytes_file_func.assert_called_once() - self.assertEqual( - CaseDocument.objects.filter( - name__contains=AutoGeneratedDocuments.APPLICATION_FORM, - type=CaseDocumentState.AUTO_GENERATED, - safe=True, - case=application, - visible_to_exporter=False, - ).count(), - 1, - ) - - def test_gifting_clearance_declaration_submit_tcs_false_failure(self): - data = { - "submit_declaration": True, - "agreed_to_declaration": False, - "agreed_to_foi": True, - "foi_reason": "Because", - } - - url = reverse("applications:application_submit", kwargs={"pk": self.draft.id}) - response = self.client.put(url, data, **self.exporter_headers) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - errors = response.json()["errors"] - self.assertEqual( - errors["agreed_to_declaration_text"], - ["To submit the application, you must confirm that you agree by typing “I AGREE”"], - ) - - -class F680ClearanceTests(DataTestClient): - def setUp(self): - super().setUp() - self.draft = self.create_mod_clearance_application(self.organisation, case_type=CaseTypeEnum.F680) - self.url = reverse("applications:application_submit", kwargs={"pk": self.draft.id}) - self.exporter_user.set_role(self.organisation, self.exporter_super_user_role) - - def test_submit_F680_clearance_success(self): - response = self.client.put(self.url, **self.exporter_headers) - application = F680ClearanceApplication.objects.get() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.json()["application"]["name"], self.draft.name) - self.assertEqual(F680ClearanceApplication.objects.count(), 1) - self.assertIsNotNone(application.third_parties.get()) - self.assertIsNotNone(application.end_user) - self.assertIsNone(application.submitted_at) - self.assertEqual(application.status.status, CaseStatusEnum.DRAFT) - self.assertIsNotNone(application.goods.get()) - self.assertIsNotNone(application.intended_end_use) - - def test_submit_F680_with_end_user_and_without_third_party_success(self): - self.draft.third_parties.all().delete() - - response = self.client.put(self.url, **self.exporter_headers) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.json()["application"]["name"], self.draft.name) - - def test_submit_F680_without_end_user_and_with_third_party_success(self): - self.draft.delete_party(self.draft.end_user) - - response = self.client.put(self.url, **self.exporter_headers) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.json()["application"]["name"], self.draft.name) - - def test_submit_F680_without_end_user_or_third_party_failure(self): - self.draft.delete_party(self.draft.end_user) - self.draft.third_parties.all().delete() - self.draft.save() - - response = self.client.put(self.url, **self.exporter_headers) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.json()["errors"]["party"], [strings.Applications.F680.NO_END_USER_OR_THIRD_PARTY]) - - def test_submit_F680_without_end_user_document_success(self): - PartyDocument.objects.filter(party=self.draft.end_user.party).delete() - - response = self.client.put(self.url, **self.exporter_headers) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.json()["application"]["name"], self.draft.name) - - def test_submit_F680_with_consignee_failure(self): - self.create_party("Consignee", self.organisation, PartyType.CONSIGNEE, self.draft) - - response = self.client.put(self.url, **self.exporter_headers) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.json()["errors"]["consignee"], [strings.Applications.F680.CONSIGNEE]) - - def test_submit_F680_with_ultimate_end_user_failure(self): - self.create_party("Ultimate End User", self.organisation, PartyType.ULTIMATE_END_USER, self.draft) - - response = self.client.put(self.url, **self.exporter_headers) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual( - response.json()["errors"]["ultimate_end_users"], [strings.Applications.F680.ULTIMATE_END_USERS] - ) - - def test_submit_F680_clearance_with_location_failure(self): - SiteOnApplication(site=self.organisation.primary_site, application=self.draft).save() - - response = self.client.put(self.url, **self.exporter_headers) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.json()["errors"]["location"], [strings.Applications.F680.LOCATIONS]) - - def test_submit_F680_clearance_without_details_failure(self): - self.draft.types.clear() - - response = self.client.put(self.url, **self.exporter_headers) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.json()["errors"]["types"], [strings.Applications.F680.NO_CLEARANCE_TYPE]) - - def test_submit_F680_clearance_without_end_use_details_failure(self): - self.draft.intended_end_use = "" - self.draft.save() - - response = self.client.put(self.url, **self.exporter_headers) - - self.draft.refresh_from_db() - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(len(response.json()["errors"]), 1) - self.assertEqual( - response.json()["errors"]["end_use_details"], [strings.Applications.Generic.NO_END_USE_DETAILS] - ) - - @mock.patch("api.documents.libraries.s3_operations.upload_bytes_file") - @mock.patch("api.cases.generated_documents.helpers.html_to_pdf") - def test_f680_clearance_declaration_submit_success(self, upload_bytes_file_func, html_to_pdf_func): - upload_bytes_file_func.return_value = None - html_to_pdf_func.return_value = None - - data = { - "submit_declaration": True, - "agreed_to_declaration": True, - "agreed_to_foi": True, - "foi_reason": "Because", - "agreed_to_declaration_text": "I Agree", - } - application = F680ClearanceApplication.objects.get() - self.assertEqual(application.status.status, CaseStatusEnum.DRAFT) - - url = reverse("applications:application_submit", kwargs={"pk": self.draft.id}) - response = self.client.put(url, data, **self.exporter_headers) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - - application.refresh_from_db() - self.assertIsNotNone(application.submitted_at) - self.assertNotEqual(application.status.status, CaseStatusEnum.DRAFT) - self.assertEqual(application.agreed_to_foi, True) - self.assertEqual(application.submitted_by, self.exporter_user) - # Asserting that the 'Application Form' has been autogenerated on submission of the application - html_to_pdf_func.assert_called_once() - upload_bytes_file_func.assert_called_once() - self.assertEqual( - CaseDocument.objects.filter( - name__contains=AutoGeneratedDocuments.APPLICATION_FORM, - type=CaseDocumentState.AUTO_GENERATED, - safe=True, - case=application, - visible_to_exporter=False, - ).count(), - 1, - ) - - def test_f680_clearance_declaration_submit_tcs_false_failure(self): - data = { - "submit_declaration": True, - "agreed_to_declaration": False, - "agreed_to_foi": True, - "foi_reason": "Because", - } - - url = reverse("applications:application_submit", kwargs={"pk": self.draft.id}) - response = self.client.put(url, data, **self.exporter_headers) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - errors = response.json()["errors"] - self.assertEqual( - errors["agreed_to_declaration_text"], - ["To submit the application, you must confirm that you agree by typing “I AGREE”"], - ) diff --git a/api/applications/tests/test_view_application.py b/api/applications/tests/test_view_application.py index 48edbad130..df0a2128e5 100644 --- a/api/applications/tests/test_view_application.py +++ b/api/applications/tests/test_view_application.py @@ -121,102 +121,6 @@ def test_view_draft_standard_application_as_exporter_success(self): str(standard_application.third_parties.get().party.id), ) - @parameterized.expand([(CaseTypeEnum.EXHIBITION,), (CaseTypeEnum.GIFTING,), (CaseTypeEnum.F680,)]) - def test_view_draft_MOD_clearances_list_as_exporter_success(self, type): - self.exporter_user.set_role(self.organisation, self.exporter_super_user_role) - application = self.create_mod_clearance_application(self.organisation, case_type=type) - - response = self.client.get(self.url, **self.exporter_headers) - response_data = response.json()["results"] - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response_data), 1) - self.assertEqual(response_data[0]["name"], application.name) - self.assertEqual( - response_data[0]["case_type"]["sub_type"]["key"], - application.case_type.sub_type, - ) - self.assertIsNotNone(response_data[0]["updated_at"]) - self.assertEqual(response_data[0]["status"]["key"], CaseStatusEnum.DRAFT) - - def test_view_draft_exhibition_clearance_as_exporter_success(self): - application = self.create_mod_clearance_application(self.organisation, case_type=CaseTypeEnum.EXHIBITION) - - url = reverse("applications:application", kwargs={"pk": application.id}) - - response = self.client.get(url, **self.exporter_headers) - retrieved_application = response.json() - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(retrieved_application["name"], application.name) - self.assertEqual(retrieved_application["case_type"]["reference"]["key"], application.case_type.reference) - self.assertIsNotNone(retrieved_application["created_at"]) - self.assertIsNotNone(retrieved_application["updated_at"]) - self.assertIsNone(retrieved_application["submitted_at"]) - self.assertEqual(retrieved_application["title"], application.title) - self.assertEqual(retrieved_application["first_exhibition_date"], str(application.first_exhibition_date)) - self.assertEqual(retrieved_application["required_by_date"], str(application.required_by_date)) - self.assertEqual(retrieved_application["reason_for_clearance"], application.reason_for_clearance) - - self.assertEqual(retrieved_application["status"]["key"], CaseStatusEnum.DRAFT) - self.assertEqual(GoodOnApplication.objects.filter(application__id=application.id).count(), 1) - - def test_view_draft_gifting_clearance_as_exporter_success(self): - application = self.create_mod_clearance_application(self.organisation, case_type=CaseTypeEnum.GIFTING) - - url = reverse("applications:application", kwargs={"pk": application.id}) - - response = self.client.get(url, **self.exporter_headers) - retrieved_application = response.json() - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(retrieved_application["name"], application.name) - self.assertEqual( - retrieved_application["case_type"]["reference"]["key"], - application.case_type.reference, - ) - self.assertIsNotNone(retrieved_application["created_at"]) - self.assertIsNotNone(retrieved_application["updated_at"]) - self.assertIsNone(retrieved_application["submitted_at"]) - self.assertEqual(retrieved_application["status"]["key"], CaseStatusEnum.DRAFT) - self.assertEqual(GoodOnApplication.objects.filter(application__id=application.id).count(), 1) - self.assertEqual( - retrieved_application["end_user"]["id"], - str(application.end_user.party.id), - ) - self.assertEqual( - retrieved_application["third_parties"][0]["id"], - str(application.third_parties.get().party.id), - ) - - def test_view_draft_f680_clearance_as_exporter_success(self): - application = self.create_mod_clearance_application(self.organisation, case_type=CaseTypeEnum.F680) - - url = reverse("applications:application", kwargs={"pk": application.id}) - - response = self.client.get(url, **self.exporter_headers) - retrieved_application = response.json() - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(retrieved_application["name"], application.name) - self.assertEqual( - retrieved_application["case_type"]["reference"]["key"], - application.case_type.reference, - ) - self.assertIsNotNone(retrieved_application["created_at"]) - self.assertIsNotNone(retrieved_application["updated_at"]) - self.assertIsNone(retrieved_application["submitted_at"]) - self.assertEqual(retrieved_application["status"]["key"], CaseStatusEnum.DRAFT) - self.assertEqual(GoodOnApplication.objects.filter(application__id=application.id).count(), 1) - self.assertEqual( - retrieved_application["end_user"]["id"], - str(application.end_user.party.id), - ) - self.assertEqual( - retrieved_application["third_parties"][0]["id"], - str(application.third_parties.get().party.id), - ) - def test_view_draft_open_application_as_exporter_success(self): open_application = self.create_draft_open_application(self.organisation) diff --git a/api/applications/urls.py b/api/applications/urls.py index fce70bbc1f..565e9eebc7 100644 --- a/api/applications/urls.py +++ b/api/applications/urls.py @@ -132,7 +132,6 @@ ), # Existing parties path("/existing-parties/", existing_parties.ExistingParties.as_view(), name="existing_parties"), - path("/exhibition-details/", applications.ExhibitionDetails.as_view(), name="exhibition"), # Denial matches path( "/denial-matches/", diff --git a/api/applications/views/applications.py b/api/applications/views/applications.py index e22d667103..c5a3f2b9bf 100644 --- a/api/applications/views/applications.py +++ b/api/applications/views/applications.py @@ -53,10 +53,8 @@ ExternalLocationOnApplication, PartyOnApplication, StandardApplication, - F680ClearanceApplication, ) from api.applications.notify import notify_exporter_case_opened_for_editing -from api.applications.serializers.exhibition_clearance import ExhibitionClearanceDetailSerializer from api.applications.serializers.generic_application import ( GenericApplicationListSerializer, GenericApplicationCopySerializer, @@ -79,7 +77,7 @@ authorised_to_view_application, allowed_application_types, ) -from api.core.helpers import convert_date_to_string, str_to_bool +from api.core.helpers import str_to_bool from api.core.permissions import ( assert_user_has_permission, IsExporterInOrganisation, @@ -97,7 +95,6 @@ from lite_content.lite_api import strings from api.organisations.libraries.get_organisation import get_request_user_organisation, get_request_user_organisation_id from api.organisations.models import Site -from api.staticdata.f680_clearance_types.enums import F680ClearanceTypeEnum from api.staticdata.statuses.enums import CaseStatusEnum from api.staticdata.statuses.libraries.case_status_validate import is_case_status_draft from api.staticdata.statuses.libraries.get_case_status import get_case_status_by_status @@ -255,22 +252,6 @@ def put(self, request, pk): status=status.HTTP_400_BAD_REQUEST, ) - # Prevent minor edits of the f680 clearance types - if not application.is_major_editable() and request.data.get("types"): - return JsonResponse( - data={"errors": {"types": [strings.Applications.Generic.NOT_POSSIBLE_ON_MINOR_EDIT]}}, - status=status.HTTP_400_BAD_REQUEST, - ) - - # Prevent minor edits of additional_information - if not application.is_major_editable() and any( - [request.data.get(field) for field in constants.F680.ADDITIONAL_INFORMATION_FIELDS] - ): - return JsonResponse( - data={"errors": {"Additional details": [strings.Applications.Generic.NOT_POSSIBLE_ON_MINOR_EDIT]}}, - status=status.HTTP_400_BAD_REQUEST, - ) - if not serializer.is_valid(): return JsonResponse(data={"errors": serializer.errors}, status=status.HTTP_400_BAD_REQUEST) @@ -296,26 +277,6 @@ def put(self, request, pk): serializer.save() return JsonResponse(data={}, status=status.HTTP_200_OK) - # Audit block - if application.case_type.sub_type == CaseTypeSubTypeEnum.F680: - if request.data.get("types"): - old_types = [ - F680ClearanceTypeEnum.get_text(type) for type in application.types.values_list("name", flat=True) - ] - new_types = [F680ClearanceTypeEnum.get_text(type) for type in request.data.get("types")] - serializer.save() - - if set(old_types) != set(new_types): - audit_trail_service.create( - actor=request.user, - verb=AuditType.UPDATE_APPLICATION_F680_CLEARANCE_TYPES, - target=case, - payload={"old_types": old_types, "new_types": new_types}, - ) - return JsonResponse(data={}, status=status.HTTP_200_OK) - else: - serializer.save() - if application.case_type.sub_type == CaseTypeSubTypeEnum.STANDARD: save_and_audit_have_you_been_informed_ref(request, application, serializer) serializer.save() @@ -787,9 +748,6 @@ def post(self, request, pk): # Get all parties connected to the application and produce a copy (and replace reference for each one) self.duplicate_parties_on_new_application() - # Get all f680 clearance types - self.duplicate_f680_clearance_types() - # Remove usage & licenced quantity/ value self.new_application.goods_type.update(usage=0) @@ -919,86 +877,6 @@ def duplicate_goodstypes_for_new_application(self): good.flags.set(old_good_flags) good.control_list_entries.set(old_good_control_list_entries) - def duplicate_f680_clearance_types(self): - if self.new_application.case_type.sub_type == CaseTypeSubTypeEnum.F680: - self.new_application.types.set( - list( - F680ClearanceApplication.objects.get(id=self.old_application_id).types.values_list("id", flat=True) - ) - ) - - -class ExhibitionDetails(ListCreateAPIView): - authentication_classes = (ExporterAuthentication,) - queryset = BaseApplication.objects.all() - serializer = ExhibitionClearanceDetailSerializer - - @application_in_state(is_major_editable=True) - @authorised_to_view_application(ExporterUser) - def post(self, request, pk): - application = get_application(pk) - serializer = self.serializer(instance=application, data=request.data) - if serializer.is_valid(): - old_title = application.title - old_first_exhibition_date = application.first_exhibition_date - old_required_by_date = application.required_by_date - old_reason_for_clearance = application.reason_for_clearance - case = application.get_case() - serializer.save() - validated_data = serializer.validated_data - - if validated_data["title"] != old_title: - audit_trail_service.create( - actor=request.user, - verb=AuditType.UPDATED_EXHIBITION_DETAILS_TITLE, - target=case, - payload={ - "old_title": old_title, - "new_title": validated_data["title"], - }, - ) - - if validated_data["first_exhibition_date"] != old_first_exhibition_date: - audit_trail_service.create( - actor=request.user, - verb=AuditType.UPDATED_EXHIBITION_DETAILS_START_DATE, - target=application.get_case(), - payload={ - "old_first_exhibition_date": convert_date_to_string(old_first_exhibition_date) - if old_first_exhibition_date - else "", - "new_first_exhibition_date": convert_date_to_string(validated_data["first_exhibition_date"]), - }, - ) - - if validated_data["required_by_date"] != old_required_by_date: - audit_trail_service.create( - actor=request.user, - verb=AuditType.UPDATED_EXHIBITION_DETAILS_REQUIRED_BY_DATE, - target=application.get_case(), - payload={ - "old_required_by_date": convert_date_to_string(old_required_by_date) - if old_required_by_date - else "", - "new_required_by_date": convert_date_to_string(validated_data["required_by_date"]), - }, - ) - - if validated_data.get("reason_for_clearance") != old_reason_for_clearance: - audit_trail_service.create( - actor=request.user, - verb=AuditType.UPDATED_EXHIBITION_DETAILS_REASON_FOR_CLEARANCE, - target=application.get_case(), - payload={ - "old_reason_for_clearance": old_reason_for_clearance, - "new_reason_for_clearance": validated_data["reason_for_clearance"], - }, - ) - - return JsonResponse(data={"application": serializer.data}, status=status.HTTP_200_OK) - - return JsonResponse(data={"errors": serializer.errors}, status=status.HTTP_400_BAD_REQUEST) - class ApplicationRouteOfGoods(UpdateAPIView): authentication_classes = (ExporterAuthentication,) diff --git a/api/audit_trail/payload.py b/api/audit_trail/payload.py index e3b0e71742..5184caff6c 100644 --- a/api/audit_trail/payload.py +++ b/api/audit_trail/payload.py @@ -1,3 +1,4 @@ +from operator import itemgetter from api.audit_trail.enums import AuditType from api.audit_trail import formatters from lite_content.lite_api import strings @@ -12,6 +13,13 @@ def format_payload(audit_type, payload): return text +ADDITIONAL_TEXT_FORMATTERS = { + AuditType.ECJU_QUERY: itemgetter("ecju_query"), + AuditType.ECJU_QUERY_RESPONSE: itemgetter("ecju_response"), + AuditType.ECJU_QUERY_MANUALLY_CLOSED: itemgetter("ecju_response"), +} + + audit_type_format = { AuditType.OGL_CREATED: strings.Audit.CREATED_OGL, AuditType.OGL_FIELD_EDITED: strings.Audit.UPDATED_OGL, @@ -49,9 +57,9 @@ def format_payload(audit_type, payload): AuditType.PV_GRADING_RESPONSE: strings.Audit.PV_GRADING_RESPONSE, AuditType.CREATED_CASE_NOTE: strings.Audit.CREATED_CASE_NOTE, AuditType.CREATED_CASE_NOTE_WITH_MENTIONS: " mentioned ", - AuditType.ECJU_QUERY: " added an ECJU Query: {ecju_query}", - AuditType.ECJU_QUERY_RESPONSE: " responded to an ECJU Query: {ecju_response}", - AuditType.ECJU_QUERY_MANUALLY_CLOSED: " manually closed a query: {ecju_response}", + AuditType.ECJU_QUERY: " added an ECJU Query.", + AuditType.ECJU_QUERY_RESPONSE: " responded to an ECJU Query.", + AuditType.ECJU_QUERY_MANUALLY_CLOSED: " manually closed a query.", AuditType.UPDATED_STATUS: formatters.get_updated_status, AuditType.UPDATED_SUB_STATUS: formatters.get_updated_sub_status, AuditType.UPDATED_APPLICATION_NAME: strings.Audit.UPDATED_APPLICATION_NAME, diff --git a/api/audit_trail/serializers.py b/api/audit_trail/serializers.py index f2f94de6d9..588f51bf37 100644 --- a/api/audit_trail/serializers.py +++ b/api/audit_trail/serializers.py @@ -5,7 +5,7 @@ from api.audit_trail.models import Audit from api.audit_trail.enums import AuditType -from api.audit_trail.payload import format_payload +from api.audit_trail.payload import format_payload, ADDITIONAL_TEXT_FORMATTERS from api.users.enums import UserType @@ -66,4 +66,8 @@ def get_text(self, instance): return format_payload(verb, payload) def get_additional_text(self, instance): - return instance.payload.get("additional_text", "") + verb = AuditType(instance.verb) + return ADDITIONAL_TEXT_FORMATTERS.get( + verb, + lambda payload: payload.get("additional_text", ""), + )(instance.payload) diff --git a/api/cases/managers.py b/api/cases/managers.py index e54766e07b..d0e282f874 100644 --- a/api/cases/managers.py +++ b/api/cases/managers.py @@ -3,14 +3,7 @@ from django.apps import apps from django.db import models, transaction -from django.db.models import ( - BinaryField, - Case, - Prefetch, - Q, - Sum, - When, -) +from django.db.models import Prefetch, Q, Sum from django.utils import timezone from api.cases.enums import AdviceLevel, CaseTypeEnum @@ -464,15 +457,7 @@ def search( # noqa case_qs = case_qs.with_report_summary_subject_or_prefix(report_summary) if is_work_queue: - case_qs = case_qs.annotate( - case_order=Case( - When(baseapplication__hmrcquery__have_goods_departed=False, then=0), - default=1, - output_field=BinaryField(), - ) - ) - - case_qs = case_qs.order_by("case_order", "submitted_at") + case_qs = case_qs.order_by("submitted_at") else: case_qs = case_qs.order_by_date() diff --git a/api/cases/tests/test_case_ecju_queries.py b/api/cases/tests/test_case_ecju_queries.py index cfc99b835a..c0ad0fcd84 100644 --- a/api/cases/tests/test_case_ecju_queries.py +++ b/api/cases/tests/test_case_ecju_queries.py @@ -491,7 +491,9 @@ def _test_exporter_responds_to_query(self, add_documents, query_type): self.assertTrue(query_response_audit.exists()) audit_obj = query_response_audit.first() audit_text = AuditSerializer(audit_obj).data["text"] - self.assertEqual(audit_text, " responded to an ECJU Query: Attached the requested documents.") + audit_additional_text = AuditSerializer(audit_obj).data["additional_text"] + self.assertEqual(audit_text, " responded to an ECJU Query.") + self.assertEqual(audit_additional_text, "Attached the requested documents") self.assertEqual(audit_obj.target.id, case.id) if add_documents: @@ -521,7 +523,10 @@ def test_caseworker_manually_closes_query(self): self.assertTrue(query_response_audit.exists()) audit_obj = query_response_audit.first() audit_text = AuditSerializer(audit_obj).data["text"] - self.assertEqual(audit_text, " manually closed a query: exporter provided details.") + audit_additional_text = AuditSerializer(audit_obj).data["additional_text"] + + self.assertEqual(audit_text, " manually closed a query.") + self.assertEqual(audit_additional_text, "exporter provided details") self.assertEqual(audit_obj.target.id, case.id) self.assertEqual(0, BaseNotification.objects.filter(object_id=ecju_query.id).count()) @@ -942,3 +947,4 @@ def test_exporter_responding_to_query_creates_case_note_mention_for_caseworker(s self.assertEqual(case_note_mentions.user, expected_gov_user) self.assertEqual(case_note.text, expected_case_note_text) self.assertEqual(audit_object.payload, expected_audit_payload) + self.assertEqual(audit_object.payload["additional_text"], expected_case_note_text) diff --git a/api/cases/tests/test_grant_licence.py b/api/cases/tests/test_grant_licence.py index f34de69ae0..347e0eb66c 100644 --- a/api/cases/tests/test_grant_licence.py +++ b/api/cases/tests/test_grant_licence.py @@ -5,7 +5,7 @@ from api.licences.enums import LicenceStatus from api.licences.models import Licence from api.audit_trail.models import Audit -from api.cases.enums import AdviceType, CaseTypeEnum, AdviceLevel +from api.cases.enums import AdviceType, CaseTypeEnum from api.cases.generated_documents.models import GeneratedCaseDocument from api.cases.tests.factories import FinalAdviceFactory from api.core.constants import GovPermissions @@ -82,54 +82,6 @@ def test_missing_advice_document_failure(self): self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.json(), {"errors": {"decision-approve": [Cases.Licence.MISSING_DOCUMENTS]}}) - @mock.patch("api.cases.views.views.notify_exporter_licence_issued") - @mock.patch("api.cases.generated_documents.models.GeneratedCaseDocument.send_exporter_notifications") - def test_grant_clearance_success(self, send_exporter_notifications_func, mock_notify): - clearance_case = self.create_mod_clearance_application(self.organisation, CaseTypeEnum.EXHIBITION) - self.submit_application(clearance_case) - FinalAdviceFactory(user=self.gov_user, case=clearance_case, type=AdviceType.APPROVE) - self.url = reverse("cases:finalise", kwargs={"pk": clearance_case.id}) - - self.gov_user.role.permissions.set([GovPermissions.MANAGE_CLEARANCE_FINAL_ADVICE.name]) - licence = StandardLicenceFactory(case=clearance_case, status=LicenceStatus.DRAFT) - self.create_generated_case_document( - clearance_case, self.template, advice_type=AdviceType.APPROVE, licence=licence - ) - - response = self.client.put(self.url, data={}, **self.gov_headers) - clearance_case.refresh_from_db() - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(response.json()["licence"], str(licence.id)) - self.assertEqual( - Licence.objects.filter( - case=clearance_case, - status=LicenceStatus.ISSUED, - decisions__exact=Decision.objects.get(name=AdviceType.APPROVE), - ).count(), - 1, - ) - self.assertEqual(clearance_case.status, CaseStatus.objects.get(status=CaseStatusEnum.FINALISED)) - for document in GeneratedCaseDocument.objects.filter(advice_type__isnull=False): - self.assertTrue(document.visible_to_exporter) - self.assertEqual(Audit.objects.count(), 7) - send_exporter_notifications_func.assert_called() - mock_notify.assert_called_with(clearance_case.get_case()) - - def test_grant_clearance_wrong_permission_failure(self): - clearance_case = self.create_mod_clearance_application(self.organisation, CaseTypeEnum.EXHIBITION) - self.submit_application(clearance_case) - self.url = reverse("cases:finalise", kwargs={"pk": clearance_case.id}) - - self.gov_user.role.permissions.set([GovPermissions.MANAGE_LICENCE_FINAL_ADVICE.name]) - StandardLicenceFactory(case=clearance_case, status=LicenceStatus.DRAFT) - self.create_generated_case_document(clearance_case, self.template, advice_type=AdviceType.APPROVE) - - response = self.client.put(self.url, data={}, **self.gov_headers) - - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(response.json(), {"errors": {"error": PermissionDeniedError.default_detail}}) - def test_finalise_case_without_licence_success(self): self.gov_user.role.permissions.set([GovPermissions.MANAGE_LICENCE_FINAL_ADVICE.name]) self.create_generated_case_document(self.standard_case, self.template, advice_type=AdviceType.APPROVE) diff --git a/api/cases/tests/test_reference_code.py b/api/cases/tests/test_reference_code.py index 415af9fbbd..544ba77239 100644 --- a/api/cases/tests/test_reference_code.py +++ b/api/cases/tests/test_reference_code.py @@ -55,26 +55,6 @@ def test_open_application_reference_code(self): ) self.assertEqual(open_application.reference_code, expected_reference) - def test_exhibition_clearance_reference_code(self): - exhibition_clearance = self.create_mod_clearance_application( - self.organisation, case_type=CaseTypeEnum.EXHIBITION - ) - exhibition_clearance = self.submit_application(exhibition_clearance) - expected_reference = build_expected_reference(CaseTypeEnum.EXHIBITION.reference) - self.assertEqual(exhibition_clearance.reference_code, expected_reference) - - def test_f680_clearance_reference_code(self): - f680_clearance = self.create_mod_clearance_application(self.organisation, case_type=CaseTypeEnum.F680) - f680_clearance = self.submit_application(f680_clearance) - expected_reference = build_expected_reference(CaseTypeEnum.F680.reference) - self.assertEqual(f680_clearance.reference_code, expected_reference) - - def test_gifting_clearance_reference_code(self): - gifting_clearance = self.create_mod_clearance_application(self.organisation, case_type=CaseTypeEnum.GIFTING) - gifting_clearance = self.submit_application(gifting_clearance) - expected_reference = build_expected_reference(CaseTypeEnum.GIFTING.reference) - self.assertEqual(gifting_clearance.reference_code, expected_reference) - def test_end_user_advisory_reference_code(self): end_user_advisory_query = self.create_end_user_advisory_case("", "", self.organisation) expected_reference = build_expected_reference(CaseTypeEnum.EUA.reference) diff --git a/api/cases/tests/test_sla.py b/api/cases/tests/test_sla.py index 0832758383..d541eff2d7 100644 --- a/api/cases/tests/test_sla.py +++ b/api/cases/tests/test_sla.py @@ -7,16 +7,15 @@ from parameterized import parameterized from pytz import timezone as tz -from api.cases.enums import CaseTypeEnum, CaseTypeSubTypeEnum +from api.cases.enums import CaseTypeSubTypeEnum from api.cases.models import Case, CaseQueue, EcjuQuery from api.cases.celery_tasks import ( update_cases_sla, STANDARD_APPLICATION_TARGET_DAYS, OPEN_APPLICATION_TARGET_DAYS, - MOD_CLEARANCE_TARGET_DAYS, SLA_UPDATE_CUTOFF_TIME, ) -from api.cases.models import CaseAssignmentSLA, CaseQueue, DepartmentSLA +from api.cases.models import CaseQueue, DepartmentSLA from api.staticdata.statuses.enums import CaseStatusEnum from api.staticdata.statuses.libraries.get_case_status import get_case_status_by_status from api.teams.models import Department @@ -44,11 +43,6 @@ def setUp(self): self.case_types = { CaseTypeSubTypeEnum.STANDARD: self.create_draft_standard_application(self.organisation), CaseTypeSubTypeEnum.OPEN: self.create_draft_open_application(self.organisation), - CaseTypeSubTypeEnum.EXHIBITION: self.create_mod_clearance_application( - self.organisation, CaseTypeEnum.EXHIBITION - ), - CaseTypeSubTypeEnum.F680: self.create_mod_clearance_application(self.organisation, CaseTypeEnum.F680), - CaseTypeSubTypeEnum.GIFTING: self.create_mod_clearance_application(self.organisation, CaseTypeEnum.GIFTING), CaseTypeSubTypeEnum.GOODS: self.create_clc_query("abc", self.organisation), CaseTypeSubTypeEnum.EUA: self.create_end_user_advisory("abc", "abc", self.organisation), } @@ -98,78 +92,6 @@ def test_sla_update_open_application( self.assertEqual(case.sla_days, 1) self.assertEqual(case.sla_remaining_days, target - 1) - @mock.patch("api.cases.celery_tasks.is_weekend") - @mock.patch("api.cases.celery_tasks.is_bank_holiday") - def test_sla_update_exhibition_mod( - self, - mock_is_weekend, - mock_is_bank_holiday, - application_type=CaseTypeSubTypeEnum.EXHIBITION, - target=MOD_CLEARANCE_TARGET_DAYS, - ): - mock_is_weekend.return_value = False - mock_is_bank_holiday.return_value = False - application = self.case_types[application_type] - case = self.submit_application(application) - _set_submitted_at(case, HOUR_BEFORE_CUTOFF) - CaseQueue.objects.create(case=application.case_ptr, queue=self.queue) - results = run_update_cases_sla_task() - sla = CaseAssignmentSLA.objects.get() - case.refresh_from_db() - - self.assertEqual(sla.sla_days, 1) - self.assertEqual(results, 1) - self.assertEqual(case.sla_days, 1) - self.assertEqual(case.sla_remaining_days, target - 1) - - @mock.patch("api.cases.celery_tasks.is_weekend") - @mock.patch("api.cases.celery_tasks.is_bank_holiday") - def test_sla_update_F680_mod( - self, - mock_is_weekend, - mock_is_bank_holiday, - application_type=CaseTypeSubTypeEnum.F680, - target=MOD_CLEARANCE_TARGET_DAYS, - ): - mock_is_weekend.return_value = False - mock_is_bank_holiday.return_value = False - application = self.case_types[application_type] - case = self.submit_application(application) - _set_submitted_at(case, HOUR_BEFORE_CUTOFF) - CaseQueue.objects.create(queue=self.queue, case=application.case_ptr) - sla = CaseAssignmentSLA.objects.create(sla_days=4, queue=self.queue, case=application.case_ptr) - results = run_update_cases_sla_task() - - case.refresh_from_db() - sla.refresh_from_db() - - self.assertEqual(sla.sla_days, 5) - self.assertEqual(results, 1) - self.assertEqual(case.sla_days, 1) - self.assertEqual(case.sla_remaining_days, target - 1) - - @mock.patch("api.cases.celery_tasks.is_weekend") - @mock.patch("api.cases.celery_tasks.is_bank_holiday") - def test_sla_update_gifting_mod( - self, - mock_is_weekend, - mock_is_bank_holiday, - application_type=CaseTypeSubTypeEnum.GIFTING, - target=MOD_CLEARANCE_TARGET_DAYS, - ): - mock_is_weekend.return_value = False - mock_is_bank_holiday.return_value = False - application = self.case_types[application_type] - case = self.submit_application(application) - _set_submitted_at(case, HOUR_BEFORE_CUTOFF) - - results = run_update_cases_sla_task() - case.refresh_from_db() - - self.assertEqual(results, 1) - self.assertEqual(case.sla_days, 1) - self.assertEqual(case.sla_remaining_days, target - 1) - @parameterized.expand([(CaseTypeSubTypeEnum.GOODS,), (CaseTypeSubTypeEnum.EUA,)]) def test_sla_doesnt_update_queries(self, query_type): query = self.case_types[query_type] diff --git a/api/conf/anonymise_model_config.yaml b/api/conf/anonymise_model_config.yaml new file mode 100644 index 0000000000..ccef37bf3b --- /dev/null +++ b/api/conf/anonymise_model_config.yaml @@ -0,0 +1,46 @@ +config: + addons: + - django_db_anonymiser.db_anonymiser + extra_parameters: + pg_dump: + - "--exclude-table-data=document_data_documentdata" + +strategy: + users_baseuser: + first_name: faker.first_name + last_name: faker.last_name + email: faker.email + phone_number: faker.phone_number + parties_party: + name: faker.name + address: faker.address + website: faker.website + email: faker.email + phone_number: faker.phone_number + signatory_name_euu: faker.name + details: faker.text + address: + address_line_1: faker.street_address + address_line_2: faker.city + region: faker.city + postcode: faker.postcode + city: faker.city + address: faker.address + external_data_denial: + name: faker.name + address: faker.address + consignee_name: faker.name + organisation: + name: faker.company_name + phone_number: faker.phone_number + website: faker.website + eori_number: faker.eori_number + sic_number: faker.sic_number + vat_number: faker.vat_number + registration_number: faker.registration_number + end_user_advisories_enduseradvisoryquery: + contact_name: faker.name + contact_email: faker.email + contact_telephone: faker.phone_number + site: + name: faker.name diff --git a/api/conf/celery.py b/api/conf/celery.py index d5d9663438..2f476b2bf1 100644 --- a/api/conf/celery.py +++ b/api/conf/celery.py @@ -17,6 +17,10 @@ # Load celery_tasks.py modules from all registered Django apps. app.autodiscover_tasks(related_name="celery_tasks") + +BACKUP_DOCUMENT_DATA_SCHEDULE_NAME = "backup document data 2am" + + # Define any regular scheduled tasks app.conf.beat_schedule = { "update sanction search index at 7am, 7pm": { @@ -27,7 +31,7 @@ "task": "api.cases.celery_tasks.update_cases_sla", "schedule": crontab(hour=22, minute=30), }, - "backup document data 2am": { + BACKUP_DOCUMENT_DATA_SCHEDULE_NAME: { "task": "api.document_data.celery_tasks.backup_document_data", "schedule": crontab(hour=2, minute=0), }, diff --git a/api/conf/settings.py b/api/conf/settings.py index c4c55f6bfe..45988f0227 100644 --- a/api/conf/settings.py +++ b/api/conf/settings.py @@ -121,6 +121,7 @@ "api.assessments", "api.document_data", "api.survey", + "django_db_anonymiser.db_anonymiser", ] MOCK_VIRUS_SCAN_ACTIVATE_ENDPOINTS = env("MOCK_VIRUS_SCAN_ACTIVATE_ENDPOINTS") @@ -244,16 +245,20 @@ # AWS VCAP_SERVICES = env.json("VCAP_SERVICES", {}) +S3_BUCKET_TAG_FILE_UPLOADS = "file-uploads" + if VCAP_SERVICES: if "aws-s3-bucket" not in VCAP_SERVICES: raise Exception("S3 Bucket not bound to environment") - aws_credentials = VCAP_SERVICES["aws-s3-bucket"][0]["credentials"] - AWS_ENDPOINT_URL = None - AWS_ACCESS_KEY_ID = aws_credentials["aws_access_key_id"] - AWS_SECRET_ACCESS_KEY = aws_credentials["aws_secret_access_key"] - AWS_REGION = aws_credentials["aws_region"] - AWS_STORAGE_BUCKET_NAME = aws_credentials["bucket_name"] + for bucket_details in VCAP_SERVICES["aws-s3-bucket"]: + if S3_BUCKET_TAG_FILE_UPLOADS in bucket_details["tags"]: + aws_credentials = bucket_details["credentials"] + AWS_ENDPOINT_URL = None + AWS_ACCESS_KEY_ID = aws_credentials["aws_access_key_id"] + AWS_SECRET_ACCESS_KEY = aws_credentials["aws_secret_access_key"] + AWS_REGION = aws_credentials["aws_region"] + AWS_STORAGE_BUCKET_NAME = aws_credentials["bucket_name"] else: AWS_ENDPOINT_URL = env("AWS_ENDPOINT_URL", default=None) AWS_ACCESS_KEY_ID = env("AWS_ACCESS_KEY_ID") @@ -285,6 +290,7 @@ def _build_redis_url(base_url, db_number, **query_args): CELERY_TASK_ALWAYS_EAGER = env.bool("CELERY_TASK_ALWAYS_EAGER", False) CELERY_TASK_STORE_EAGER_RESULT = env.bool("CELERY_TASK_STORE_EAGER_RESULT", False) CELERY_TASK_SEND_SENT_EVENT = env.bool("CELERY_TASK_SEND_SENT_EVENT", True) +CELERY_TASK_TRACK_STARTED = env.bool("CELERY_TASK_TRACK_STARTED", True) S3_CONNECT_TIMEOUT = 60 # Maximum time, in seconds, to wait for an initial connection @@ -496,3 +502,28 @@ def _build_redis_url(base_url, db_number, **query_args): CONTENT_DATA_MIGRATION_DIR = Path(BASE_DIR).parent / "lite_content/lite_api/migrations" BACKUP_DOCUMENT_DATA_TO_DB = env("BACKUP_DOCUMENT_DATA_TO_DB", default=True) + + +S3_BUCKET_TAG_ANONYMISER_DESTINATION = "anonymiser" + +if VCAP_SERVICES: + if "aws-s3-bucket" not in VCAP_SERVICES: + raise Exception("S3 Bucket not bound to environment") + + for bucket_details in VCAP_SERVICES["aws-s3-bucket"]: + if S3_BUCKET_TAG_ANONYMISER_DESTINATION in bucket_details["tags"]: + aws_credentials = bucket_details["credentials"] + DB_ANONYMISER_AWS_ENDPOINT_URL = None + DB_ANONYMISER_AWS_ACCESS_KEY_ID = aws_credentials["aws_access_key_id"] + DB_ANONYMISER_AWS_SECRET_ACCESS_KEY = aws_credentials["aws_secret_access_key"] + DB_ANONYMISER_AWS_REGION = aws_credentials["aws_region"] + DB_ANONYMISER_AWS_STORAGE_BUCKET_NAME = aws_credentials["bucket_name"] +else: + DB_ANONYMISER_AWS_ENDPOINT_URL = AWS_ENDPOINT_URL + DB_ANONYMISER_AWS_ACCESS_KEY_ID = env("DB_ANONYMISER_AWS_ACCESS_KEY_ID", default=None) + DB_ANONYMISER_AWS_SECRET_ACCESS_KEY = env("DB_ANONYMISER_AWS_SECRET_ACCESS_KEY", default=None) + DB_ANONYMISER_AWS_REGION = env("DB_ANONYMISER_AWS_REGION", default=None) + DB_ANONYMISER_AWS_STORAGE_BUCKET_NAME = env("DB_ANONYMISER_AWS_STORAGE_BUCKET_NAME", default=None) + +DB_ANONYMISER_CONFIG_LOCATION = Path(BASE_DIR) / "conf" / "anonymise_model_config.yaml" +DB_ANONYMISER_DUMP_FILE_NAME = env.str("DB_ANONYMISER_DUMP_FILE_NAME", "anonymised.sql") diff --git a/api/conf/settings_test.py b/api/conf/settings_test.py index 30b35d95be..502121952f 100644 --- a/api/conf/settings_test.py +++ b/api/conf/settings_test.py @@ -12,3 +12,10 @@ INSTALLED_APPS += [ "api.core.tests.apps.CoreTestsConfig", ] + + +DB_ANONYMISER_AWS_ACCESS_KEY_ID = "fakekey" +DB_ANONYMISER_AWS_SECRET_ACCESS_KEY = "fakesecret" +DB_ANONYMISER_AWS_REGION = "eu-west-2" +DB_ANONYMISER_AWS_STORAGE_BUCKET_NAME = "anonymiser-bucket" +DB_ANONYMISER_AWS_ENDPOINT_URL = None diff --git a/api/document_data/apps.py b/api/document_data/apps.py new file mode 100644 index 0000000000..4b9ef44725 --- /dev/null +++ b/api/document_data/apps.py @@ -0,0 +1,12 @@ +from django.apps import AppConfig + +from health_check.plugins import plugin_dir + + +class DocumentDataConfig(AppConfig): + name = "api.document_data" + + def ready(self): + from .health_checks import BackupDocumentDataHealthCheckBackend + + plugin_dir.register(BackupDocumentDataHealthCheckBackend) diff --git a/api/document_data/celery_tasks.py b/api/document_data/celery_tasks.py index a995db066d..8971b1e393 100644 --- a/api/document_data/celery_tasks.py +++ b/api/document_data/celery_tasks.py @@ -3,10 +3,14 @@ from celery.utils.log import get_task_logger from django.conf import settings +from django.utils import timezone from api.documents.libraries.s3_operations import get_object from api.documents.models import Document -from api.document_data.models import DocumentData +from api.document_data.models import ( + BackupLog, + DocumentData, +) logger = get_task_logger(__name__) @@ -18,10 +22,11 @@ @shared_task( autoretry_for=(Exception,), + bind=True, max_retries=MAX_ATTEMPTS, retry_backoff=RETRY_BACKOFF, ) -def backup_document_data(): +def backup_document_data(self): """Backup document data into the database.""" # When running this command by hand it's best to set the logging as follows: @@ -38,6 +43,8 @@ def backup_document_data(): logger.info("Skipping backup document data to db") return + backup_log = BackupLog.objects.create(task_id=self.request.id) + safe_documents = Document.objects.filter(safe=True) count = safe_documents.count() logger.debug( @@ -106,4 +113,7 @@ def backup_document_data(): document_id, ) + backup_log.ended_at = timezone.now() + backup_log.save() + logger.debug("Completed backing up documents") diff --git a/api/document_data/health_checks.py b/api/document_data/health_checks.py new file mode 100644 index 0000000000..cc735fde9a --- /dev/null +++ b/api/document_data/health_checks.py @@ -0,0 +1,84 @@ +import celery + +from django.conf import settings +from django.utils import timezone + +from api.conf.celery import ( + app, + BACKUP_DOCUMENT_DATA_SCHEDULE_NAME, +) + +from health_check.backends import BaseHealthCheckBackend +from health_check.exceptions import HealthCheckException + +from api.documents.models import Document +from api.document_data.models import BackupLog, DocumentData + + +class BackupDocumentDataHealthCheckException(HealthCheckException): + message_type = "backup documents error" + + +class BackupDocumentDataHealthCheckBackend(BaseHealthCheckBackend): + # This isn't critical because ideally we don't want to get a P1 alert as + # people outside of our team will presume this means that the system is down + # which it isn't, however we want to receive alerts via Sentry to let us + # know this has failed. + critical_service = False + + def check_status(self): + if not settings.BACKUP_DOCUMENT_DATA_TO_DB: + return + + if not BackupLog.objects.exists(): + # This will only occur before our first run. + # Treat this as a very short lived edge case that isn't a sign of an + # error. + return + + latest_backup_log = BackupLog.objects.latest() + if not latest_backup_log.ended_at: + # If we find that we have a backup log without an end date then we + # can assume that the task is either running or has completely + # failed in some way. + async_result = latest_backup_log.get_async_result() + if async_result.status in [ + celery.states.STARTED, + celery.states.RETRY, + celery.states.PENDING, + ]: + # If the task has pending, started or is retrying then we should + # just wait until it's finished to check everything else so wait + # until the next healthcheck to roll around. + return + + # If it's not running then we can presume that some kind of error + # occurred and the task bailed out + raise BackupDocumentDataHealthCheckException( + f"Latest backup ended with status {async_result.status}", + ) + + # If we have an end date then we can ask celery when we think it's next + # going to run. + # If the date is in the past then it means the task that should have + # run previously hasn't for some reason. + backup_schedule = app.conf.beat_schedule[BACKUP_DOCUMENT_DATA_SCHEDULE_NAME]["schedule"] + next_run_delta = backup_schedule.remaining_estimate(latest_backup_log.ended_at) + now = timezone.now() + next_run = now + next_run_delta + if next_run < timezone.now(): + raise BackupDocumentDataHealthCheckException("Backup not run today") + + # If we manage to get here we know that the task was run recently and + # now we need to check to make sure that our backed up files match those + # that are in the main documents. + backed_up_s3_keys = DocumentData.objects.values_list("s3_key", flat=True) + not_backed_up = Document.objects.filter( + created_at__lte=latest_backup_log.ended_at, + safe=True, + ).exclude(s3_key__in=backed_up_s3_keys) + if not_backed_up.exists(): + raise BackupDocumentDataHealthCheckException(f"{(not_backed_up.count())} files missing from backup") + + def identifier(self): # pragma: no cover + return self.__class__.__name__ diff --git a/api/document_data/migrations/0002_backuplog.py b/api/document_data/migrations/0002_backuplog.py new file mode 100644 index 0000000000..a517b2a8a8 --- /dev/null +++ b/api/document_data/migrations/0002_backuplog.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.9 on 2024-02-16 14:33 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ("document_data", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="BackupLog", + fields=[ + ("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ("started_at", models.DateTimeField(auto_now_add=True)), + ("ended_at", models.DateTimeField(null=True)), + ("task_id", models.UUIDField()), + ], + ), + ] diff --git a/api/document_data/migrations/0003_alter_backuplog_options.py b/api/document_data/migrations/0003_alter_backuplog_options.py new file mode 100644 index 0000000000..64853fd7a5 --- /dev/null +++ b/api/document_data/migrations/0003_alter_backuplog_options.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.9 on 2024-02-16 14:44 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("document_data", "0002_backuplog"), + ] + + operations = [ + migrations.AlterModelOptions( + name="backuplog", + options={"ordering": ["-started_at"]}, + ), + ] diff --git a/api/document_data/migrations/0004_alter_backuplog_options.py b/api/document_data/migrations/0004_alter_backuplog_options.py new file mode 100644 index 0000000000..0afab0ea04 --- /dev/null +++ b/api/document_data/migrations/0004_alter_backuplog_options.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.9 on 2024-02-16 14:50 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("document_data", "0003_alter_backuplog_options"), + ] + + operations = [ + migrations.AlterModelOptions( + name="backuplog", + options={"get_latest_by": ["started_at"], "ordering": ["started_at"]}, + ), + ] diff --git a/api/document_data/models.py b/api/document_data/models.py index adfe94a465..22f6ec84e3 100644 --- a/api/document_data/models.py +++ b/api/document_data/models.py @@ -2,6 +2,8 @@ from django.db import models +from celery.result import AsyncResult + from api.common.models import TimestampableModel @@ -11,3 +13,17 @@ class DocumentData(TimestampableModel): data = models.BinaryField() last_modified = models.DateTimeField() content_type = models.CharField(max_length=255) + + +class BackupLog(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + started_at = models.DateTimeField(auto_now_add=True) + ended_at = models.DateTimeField(null=True) + task_id = models.UUIDField() + + class Meta: + get_latest_by = ["started_at"] + ordering = ["started_at"] + + def get_async_result(self): + return AsyncResult(str(self.task_id)) diff --git a/api/document_data/tests/test_celery_tasks.py b/api/document_data/tests/test_celery_tasks.py index 89bfafe72e..719ebce46f 100644 --- a/api/document_data/tests/test_celery_tasks.py +++ b/api/document_data/tests/test_celery_tasks.py @@ -33,7 +33,7 @@ def test_backup_new_document_data(self): 0, ) - backup_document_data() + backup_document_data.apply() self.assertEqual( DocumentData.objects.count(), @@ -69,7 +69,7 @@ def test_update_existing_document_data(self): 0, ) - backup_document_data() + backup_document_data.apply() self.assertEqual( DocumentData.objects.count(), 1, @@ -80,7 +80,7 @@ def test_update_existing_document_data(self): document_data.save() self.put_object_in_default_bucket("thisisakey", b"new contents") - backup_document_data() + backup_document_data.apply() self.assertEqual( DocumentData.objects.count(), @@ -116,7 +116,7 @@ def test_leave_existing_document_data(self): 0, ) - backup_document_data() + backup_document_data.apply() self.assertEqual( DocumentData.objects.count(), 1, @@ -127,7 +127,7 @@ def test_leave_existing_document_data(self): document_data.save() self.put_object_in_default_bucket("thisisakey", b"new contents") - backup_document_data() + backup_document_data.apply() self.assertEqual( DocumentData.objects.count(), @@ -161,7 +161,7 @@ def test_ignore_client_error(self, mock_get_object): 0, ) - backup_document_data() + backup_document_data.apply() self.assertEqual( DocumentData.objects.count(), 0, @@ -181,7 +181,7 @@ def test_ignore_get_object_returning_none(self, mock_get_object): 0, ) - backup_document_data() + backup_document_data.apply() self.assertEqual( DocumentData.objects.count(), 0, @@ -201,7 +201,7 @@ def test_stop_backup_new_document_data(self): 0, ) - backup_document_data() + backup_document_data.apply() self.assertEqual( DocumentData.objects.count(), diff --git a/api/document_data/tests/test_health_checks.py b/api/document_data/tests/test_health_checks.py new file mode 100644 index 0000000000..5756d6f753 --- /dev/null +++ b/api/document_data/tests/test_health_checks.py @@ -0,0 +1,216 @@ +import celery +import uuid + +from unittest.mock import patch + +from django.test import override_settings +from django.utils import timezone + +from rest_framework.test import APITestCase + +from freezegun import freeze_time +from parameterized import parameterized + +from api.conf.celery import BACKUP_DOCUMENT_DATA_SCHEDULE_NAME +from api.documents.tests.factories import DocumentFactory +from api.document_data.health_checks import ( + BackupDocumentDataHealthCheckBackend, + BackupDocumentDataHealthCheckException, +) +from api.document_data.models import ( + BackupLog, + DocumentData, +) + + +@override_settings(BACKUP_DOCUMENT_DATA_TO_DB=True) +class TestBackupDocumentDataHealthcheckBackend(APITestCase): + def setUp(self): + super().setUp() + + self.backend = BackupDocumentDataHealthCheckBackend() + + @override_settings(BACKUP_DOCUMENT_DATA_TO_DB=False) + def test_backup_document_data_healthcheck_backup_off(self): + self.assertIsNone(self.backend.check_status()) + + def test_backup_document_data_healthcheck_no_backup_log(self): + self.assertEqual(BackupLog.objects.count(), 0) + self.assertIsNone(self.backend.check_status()) + + @parameterized.expand( + [ + celery.states.STARTED, + celery.states.RETRY, + celery.states.PENDING, + ] + ) + @patch("api.document_data.models.AsyncResult") + def test_backup_document_data_task_running_state(self, running_state, mock_AsyncResult): + task_id = uuid.uuid4() + BackupLog.objects.create(task_id=task_id) + mock_AsyncResult().status = running_state + self.assertIsNone(self.backend.check_status()) + mock_AsyncResult.assert_called_with(str(task_id)) + + @parameterized.expand( + [ + celery.states.FAILURE, + celery.states.REVOKED, + ] + ) + @patch("api.document_data.models.AsyncResult") + def test_backup_document_data_task_failure_state(self, failure_state, mock_AsyncResult): + task_id = uuid.uuid4() + BackupLog.objects.create(task_id=task_id) + mock_AsyncResult().status = failure_state + with self.assertRaises(BackupDocumentDataHealthCheckException): + self.backend.check_status() + mock_AsyncResult.assert_called_with(str(task_id)) + + @patch("api.document_data.health_checks.app") + def test_backup_document_data_not_run_today(self, mock_app): + task_id = uuid.uuid4() + ended_at = timezone.datetime( + 2024, + 2, + 26, + 2, + 0, + 0, + tzinfo=timezone.timezone.utc, + ) + BackupLog.objects.create( + ended_at=ended_at, + task_id=task_id, + ) + mock_remaining_estimate = mock_app.conf.beat_schedule[BACKUP_DOCUMENT_DATA_SCHEDULE_NAME][ + "schedule" + ].remaining_estimate + mock_remaining_estimate.return_value = timezone.timedelta(hours=-8) + with freeze_time("2024-02-26 10:00:00"), self.assertRaises(BackupDocumentDataHealthCheckException): + self.backend.check_status() + mock_remaining_estimate.assert_called_with(ended_at) + + @patch("api.document_data.health_checks.app") + def test_backup_document_data_missing_files_in_backup(self, mock_app): + task_id = uuid.uuid4() + ended_at = timezone.datetime( + 2024, + 2, + 26, + 2, + 0, + 0, + tzinfo=timezone.timezone.utc, + ) + BackupLog.objects.create( + ended_at=ended_at, + task_id=task_id, + ) + DocumentFactory.create( + created_at=timezone.datetime( + 2024, + 2, + 25, + 14, + 0, + 0, + ), + safe=True, + ) + mock_remaining_estimate = mock_app.conf.beat_schedule[BACKUP_DOCUMENT_DATA_SCHEDULE_NAME][ + "schedule" + ].remaining_estimate + mock_remaining_estimate.return_value = timezone.timedelta(hours=16) + with freeze_time("2024-02-26 10:00:00"), self.assertRaises(BackupDocumentDataHealthCheckException): + self.backend.check_status() + + @patch("api.document_data.health_checks.app") + def test_backup_document_data_unsafe_files_ignored(self, mock_app): + task_id = uuid.uuid4() + ended_at = timezone.datetime( + 2024, + 2, + 26, + 2, + 0, + 0, + tzinfo=timezone.timezone.utc, + ) + BackupLog.objects.create( + ended_at=ended_at, + task_id=task_id, + ) + DocumentFactory.create( + created_at=timezone.datetime( + 2024, + 2, + 25, + 14, + 0, + 0, + ), + safe=False, + ) + mock_remaining_estimate = mock_app.conf.beat_schedule[BACKUP_DOCUMENT_DATA_SCHEDULE_NAME][ + "schedule" + ].remaining_estimate + mock_remaining_estimate.return_value = timezone.timedelta(hours=16) + self.assertIsNone(self.backend.check_status()) + + @patch("api.document_data.health_checks.app") + def test_backup_document_data_files_created_after_last_backup_ignored(self, mock_app): + task_id = uuid.uuid4() + ended_at = timezone.datetime( + 2024, + 2, + 26, + 2, + 0, + 0, + tzinfo=timezone.timezone.utc, + ) + BackupLog.objects.create( + ended_at=ended_at, + task_id=task_id, + ) + DocumentFactory.create( + created_at=ended_at + timezone.timedelta(hours=1), + safe=True, + ) + mock_remaining_estimate = mock_app.conf.beat_schedule[BACKUP_DOCUMENT_DATA_SCHEDULE_NAME][ + "schedule" + ].remaining_estimate + mock_remaining_estimate.return_value = timezone.timedelta(hours=16) + self.assertIsNone(self.backend.check_status()) + + @patch("api.document_data.health_checks.app") + def test_backup_document_data_files_all_files_backed_up(self, mock_app): + task_id = uuid.uuid4() + ended_at = timezone.datetime( + 2024, + 2, + 26, + 2, + 0, + 0, + tzinfo=timezone.timezone.utc, + ) + BackupLog.objects.create( + ended_at=ended_at, + task_id=task_id, + ) + document = DocumentFactory.create( + created_at=ended_at - timezone.timedelta(hours=5), + safe=True, + ) + DocumentData.objects.create( + last_modified=timezone.now(), + s3_key=document.s3_key, + ) + mock_remaining_estimate = mock_app.conf.beat_schedule[BACKUP_DOCUMENT_DATA_SCHEDULE_NAME][ + "schedule" + ].remaining_estimate + mock_remaining_estimate.return_value = timezone.timedelta(hours=16) + self.assertIsNone(self.backend.check_status()) diff --git a/api/external_data/tests/factories.py b/api/external_data/tests/factories.py new file mode 100644 index 0000000000..79b15f09a9 --- /dev/null +++ b/api/external_data/tests/factories.py @@ -0,0 +1,8 @@ +import factory + +from api.external_data.models import Denial + + +class DenialFactory(factory.django.DjangoModelFactory): + class Meta: + model = Denial diff --git a/api/letter_templates/context_generator.py b/api/letter_templates/context_generator.py index 41caab3243..925e406a86 100644 --- a/api/letter_templates/context_generator.py +++ b/api/letter_templates/context_generator.py @@ -8,13 +8,12 @@ from rest_framework import serializers from api.appeals.constants import APPEAL_DAYS -from api.applications.enums import GoodsTypeCategory, MTCRAnswers, ServiceEquipmentType +from api.applications.enums import GoodsTypeCategory from api.cases.enums import AdviceLevel, AdviceType, CaseTypeSubTypeEnum from api.compliance.enums import ComplianceVisitTypes, ComplianceRiskValues from api.licences.enums import LicenceStatus from api.parties.enums import PartyRole, PartyType, SubType from api.staticdata.denial_reasons.serializers import DenialReasonSerializer -from api.staticdata.f680_clearance_types.enums import F680ClearanceTypeEnum from api.staticdata.units.enums import Units from api.goods.enums import ( PvGrading, @@ -30,8 +29,6 @@ ApplicationDocument, StandardApplication, OpenApplication, - ExhibitionClearanceApplication, - F680ClearanceApplication, CountryOnApplication, GoodOnApplication, ) @@ -309,65 +306,6 @@ class Meta: compliant_limitations_eu_reference = serializers.CharField(source="compliant_limitations_eu_ref") -class F680ClearanceApplicationSerializer(serializers.ModelSerializer): - class Meta: - model = F680ClearanceApplication - fields = [ - "expedited", - "expedited_date", - "foreign_technology", - "foreign_technology_description", - "locally_manufactured", - "locally_manufactured_description", - "mtcr_type", - "electronic_warfare_requirement", - "uk_service_equipment", - "uk_service_equipment_description", - "uk_service_equipment_type", - "prospect_value", - "clearance_level", - "clearance_types", - ] - - expedited_date = serializers.DateField(format=DATE_FORMAT, input_formats=None) - expedited = FriendlyBooleanField() - foreign_technology = FriendlyBooleanField() - locally_manufactured = FriendlyBooleanField() - electronic_warfare_requirement = FriendlyBooleanField() - uk_service_equipment = FriendlyBooleanField() - clearance_types = serializers.SerializerMethodField() - mtcr_type = serializers.SerializerMethodField() - uk_service_equipment_type = serializers.SerializerMethodField() - clearance_level = serializers.SerializerMethodField() - - def get_uk_service_equipment_type(self, obj): - return ServiceEquipmentType.to_str(obj.uk_service_equipment_type) if obj.uk_service_equipment_type else None - - def get_mtcr_type(self, obj): - return MTCRAnswers.to_str(obj.mtcr_type) if obj.mtcr_type else None - - def get_clearance_types(self, obj): - return [F680ClearanceTypeEnum.get_text(f680_type.name) for f680_type in obj.types.all()] - - def get_clearance_level(self, obj): - return PvGrading.to_str(obj.clearance_level) - - -class FlattenedF680ClearanceApplicationSerializer(serializers.ModelSerializer): - class Meta: - model = Case - fields = ["baseapplication"] - - baseapplication = BaseApplicationSerializer() - - def to_representation(self, obj): - ret = super().to_representation(obj) - f680 = F680ClearanceApplication.objects.get(id=obj.pk) - f680_data = F680ClearanceApplicationSerializer(f680).data - serialized = {**ret["baseapplication"], **f680_data} - return serialized - - class TemporaryExportDetailsSerializer(serializers.Serializer): """ Serializes both OpenApplication and StandardApplication @@ -463,31 +401,6 @@ def to_representation(self, obj): return serialized -class ExhibitionClearanceApplicationSerializer(serializers.ModelSerializer): - class Meta: - model = ExhibitionClearanceApplication - fields = ["exhibition_title", "first_exhibition_date", "required_by_date", "reason_for_clearance"] - - exhibition_title = serializers.CharField(source="title") - first_exhibition_date = serializers.DateField(format=DATE_FORMAT, input_formats=None) - required_by_date = serializers.DateField(format=DATE_FORMAT, input_formats=None) - - -class FlattenedExhibitionClearanceApplicationSerializer(serializers.ModelSerializer): - class Meta: - model = Case - fields = ["baseapplication"] - - baseapplication = BaseApplicationSerializer() - - def to_representation(self, obj): - ret = super().to_representation(obj) - exhibition_clearance = ExhibitionClearanceApplication.objects.get(id=obj.pk) - exhibition_clearance_data = ExhibitionClearanceApplicationSerializer(exhibition_clearance).data - serialized = {**ret["baseapplication"], **exhibition_clearance_data} - return serialized - - class EndUserAdvisoryQuerySerializer(serializers.ModelSerializer): class Meta: model = EndUserAdvisoryQuery @@ -924,9 +837,6 @@ def get_document_context(case, addressee=None): SERIALIZER_MAPPING = { CaseTypeSubTypeEnum.STANDARD: FlattenedStandardApplicationSerializer, CaseTypeSubTypeEnum.OPEN: FlattenedOpenApplicationSerializer, - CaseTypeSubTypeEnum.EXHIBITION: FlattenedExhibitionClearanceApplicationSerializer, - CaseTypeSubTypeEnum.F680: FlattenedF680ClearanceApplicationSerializer, - CaseTypeSubTypeEnum.GIFTING: BaseApplicationSerializer, CaseTypeSubTypeEnum.EUA: EndUserAdvisoryQuerySerializer, CaseTypeSubTypeEnum.GOODS: GoodsQuerySerializer, CaseTypeSubTypeEnum.COMP_SITE: FlattenedComplianceSiteWithVisitReportsSerializer, diff --git a/api/letter_templates/tests/test_context_generation.py b/api/letter_templates/tests/test_context_generation.py index b1bf962e35..9d44f4d9d9 100644 --- a/api/letter_templates/tests/test_context_generation.py +++ b/api/letter_templates/tests/test_context_generation.py @@ -1,20 +1,17 @@ -from datetime import date - -from django.template.loader import render_to_string import pytest +from datetime import date +from django.template.loader import render_to_string from parameterized import parameterized from api.applications.enums import ( ApplicationExportType, ApplicationExportLicenceOfficialType, GoodsTypeCategory, - MTCRAnswers, - ServiceEquipmentType, ) from api.applications.models import ExternalLocationOnApplication, CountryOnApplication, GoodOnApplication from api.applications.tests.factories import GoodOnApplicationFactory -from api.cases.enums import AdviceType, CaseTypeEnum +from api.cases.enums import AdviceType from api.licences.tests.factories import StandardLicenceFactory from api.letter_templates.context_generator import EcjuQuerySerializer from api.cases.tests.factories import GoodCountryDecisionFactory, FinalAdviceFactory @@ -27,7 +24,6 @@ from api.core.helpers import add_months, DATE_FORMAT, TIME_FORMAT, friendly_boolean, get_value_from_enum from api.goods.enums import ( PvGrading, - ItemType, MilitaryUse, Component, ItemCategory, @@ -43,7 +39,6 @@ from api.parties.enums import PartyType, SubType from api.parties.models import Party from api.staticdata.countries.models import Country -from api.staticdata.f680_clearance_types.enums import F680ClearanceTypeEnum from api.staticdata.statuses.enums import CaseStatusEnum from api.staticdata.statuses.libraries.get_case_status import get_case_status_by_status from api.staticdata.trade_control.enums import TradeControlActivity, TradeControlProductCategory @@ -327,29 +322,6 @@ def _assert_exhibition_clearance_details(self, context, case): self.assertEqual(context["required_by_date"], case.required_by_date.strftime(DATE_FORMAT)) self.assertEqual(context["reason_for_clearance"], case.reason_for_clearance) - def _assert_f680_clearance_details(self, context, case): - self.assertEqual( - context["clearance_types"], - [F680ClearanceTypeEnum.get_text(f680_type.name) for f680_type in case.types.all()], - ) - self.assertEqual(context["expedited"], friendly_boolean(case.expedited)) - self.assertEqual(context["expedited_date"], case.expedited_date.strftime(DATE_FORMAT)) - self.assertEqual(context["foreign_technology"], friendly_boolean(case.foreign_technology)) - self.assertEqual(context["foreign_technology_description"], case.foreign_technology_description) - self.assertEqual(context["locally_manufactured"], friendly_boolean(case.locally_manufactured)) - self.assertEqual(context["locally_manufactured_description"], case.locally_manufactured_description) - self.assertEqual(context["mtcr_type"], MTCRAnswers.to_str(case.mtcr_type)) - self.assertEqual( - context["electronic_warfare_requirement"], friendly_boolean(case.electronic_warfare_requirement) - ) - self.assertEqual(context["uk_service_equipment"], friendly_boolean(case.uk_service_equipment)) - self.assertEqual(context["uk_service_equipment_description"], case.uk_service_equipment_description) - self.assertEqual( - context["uk_service_equipment_type"], ServiceEquipmentType.to_str(case.uk_service_equipment_type) - ) - self.assertEqual(context["prospect_value"], "{:.2f}".format(case.prospect_value)) - self.assertEqual(context["clearance_level"], PvGrading.to_str(case.clearance_level)) - def _assert_end_user_advisory_details(self, context, case): self.assertEqual(context["note"], case.note) self.assertEqual(context["query_reason"], case.reasoning) @@ -821,58 +793,6 @@ def test_generate_context_with_open_application_details(self): self._assert_open_application_details(context["details"], case) self._assert_destination_details(context["destinations"][0], destination) - def test_generate_context_with_exhibition_clearance_details(self): - case = self.create_mod_clearance_application(self.organisation, case_type=CaseTypeEnum.EXHIBITION) - case.reason_for_clearance = "abc" - good = case.goods.first() - good.item_type = ItemType.BROCHURE - good.other_item_type = "abc" - good.save() - case.save() - - context = get_document_context(case) - render_to_string(template_name="letter_templates/case_context_test.html", context=context) - - self.assertEqual(context["case_reference"], case.reference_code) - self.assertEqual(context["case_officer_name"], case.get_case_officer_name()) - self._assert_case_type_details(context["case_type"], case) - self._assert_exhibition_clearance_details(context["details"], case) - self._assert_good(context["goods"]["all"][0], good) - - def test_generate_context_with_f680_clearance_details(self): - case = self.create_mod_clearance_application(self.organisation, case_type=CaseTypeEnum.F680) - case.expedited = True - case.expedited_date = date(year=2020, month=1, day=1) - case.foreign_technology = False - case.foreign_technology_description = "abc" - case.locally_manufactured = True - case.locally_manufactured_description = "def" - case.mtcr_type = MTCRAnswers.CATEGORY_1 - case.electronic_warfare_requirement = None - case.uk_service_equipment = False - case.uk_service_equipment_description = "ghi" - case.uk_service_equipment_type = ServiceEquipmentType.MOD_FUNDED - case.prospect_value = 500.50 - case.save() - - context = get_document_context(case) - render_to_string(template_name="letter_templates/case_context_test.html", context=context) - - self.assertEqual(context["case_reference"], case.reference_code) - self.assertEqual(context["case_officer_name"], case.get_case_officer_name()) - self._assert_case_type_details(context["case_type"], case) - self._assert_f680_clearance_details(context["details"], case) - - def test_generate_context_with_gifting_clearance_details(self): - case = self.create_mod_clearance_application(self.organisation, case_type=CaseTypeEnum.GIFTING) - - context = get_document_context(case) - render_to_string(template_name="letter_templates/case_context_test.html", context=context) - - self.assertEqual(context["case_reference"], case.reference_code) - self.assertEqual(context["case_officer_name"], case.get_case_officer_name()) - self._assert_case_type_details(context["case_type"], case) - def test_generate_context_with_end_user_advisory_query_details(self): case = self.create_end_user_advisory(note="abc", reasoning="def", organisation=self.organisation) diff --git a/api/licences/tests/test_get_licence.py b/api/licences/tests/test_get_licence.py index fecfddd847..6d347a8dff 100644 --- a/api/licences/tests/test_get_licence.py +++ b/api/licences/tests/test_get_licence.py @@ -69,18 +69,12 @@ def test_get_licence_gov_view(self): def test_get_licence_exporter_view(self): applications = [ self.create_standard_application_case(self.organisation), - self.create_mod_clearance_application_case(self.organisation, CaseTypeEnum.F680), - self.create_mod_clearance_application_case(self.organisation, CaseTypeEnum.GIFTING), - self.create_mod_clearance_application_case(self.organisation, CaseTypeEnum.EXHIBITION), self.create_open_application_case(self.organisation), ] template = self.create_letter_template( case_types=[ CaseTypeEnum.SIEL.id, CaseTypeEnum.OIEL.id, - CaseTypeEnum.F680.id, - CaseTypeEnum.GIFTING.id, - CaseTypeEnum.EXHIBITION.id, ] ) licences = { diff --git a/api/licences/tests/test_get_licences.py b/api/licences/tests/test_get_licences.py index 5a1469b9a2..48717e97a7 100644 --- a/api/licences/tests/test_get_licences.py +++ b/api/licences/tests/test_get_licences.py @@ -23,26 +23,15 @@ def setUp(self): super().setUp() self.url = reverse("licences:licences") self.standard_application = self.create_standard_application_case(self.organisation) - self.f680_application = self.create_mod_clearance_application_case(self.organisation, CaseTypeEnum.F680) - self.gifting_application = self.create_mod_clearance_application_case(self.organisation, CaseTypeEnum.GIFTING) - self.exhibition_application = self.create_mod_clearance_application_case( - self.organisation, CaseTypeEnum.EXHIBITION - ) self.open_application = self.create_open_application_case(self.organisation) self.open_application.goods_type.first().countries.set([Country.objects.first()]) self.applications = [ self.standard_application, - self.f680_application, - self.gifting_application, - self.exhibition_application, self.open_application, ] self.template = self.create_letter_template( case_types=[ CaseTypeEnum.SIEL.id, - CaseTypeEnum.F680.id, - CaseTypeEnum.GIFTING.id, - CaseTypeEnum.EXHIBITION.id, CaseTypeEnum.OIEL.id, ] ) @@ -123,17 +112,6 @@ def test_get_standard_licences_only(self): self.assertTrue(str(self.licences[self.standard_application].id) in ids) self.assertTrue(str(self.licences[self.open_application].id) in ids) - def test_get_clearance_licences_only(self): - response = self.client.get(self.url + "?licence_type=" + LicenceType.CLEARANCE, **self.exporter_headers) - response_data = response.json()["results"] - ids = [licence["id"] for licence in response_data] - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response_data), 3) - self.assertTrue(str(self.licences[self.exhibition_application].id) in ids) - self.assertTrue(str(self.licences[self.f680_application].id) in ids) - self.assertTrue(str(self.licences[self.gifting_application].id) in ids) - def test_draft_licences_are_not_included(self): draft_licence = StandardLicenceFactory(case=self.standard_application, status=LicenceStatus.DRAFT) diff --git a/api/licences/tests/test_get_nlrs.py b/api/licences/tests/test_get_nlrs.py index 4df3c27139..19458b59d7 100644 --- a/api/licences/tests/test_get_nlrs.py +++ b/api/licences/tests/test_get_nlrs.py @@ -10,14 +10,8 @@ def setUp(self): super().setUp() self.url = reverse("licences:nlrs") self.standard_application = self.create_standard_application_case(self.organisation) - self.f680_application = self.create_mod_clearance_application(self.organisation, CaseTypeEnum.F680) - self.gifting_application = self.create_mod_clearance_application(self.organisation, CaseTypeEnum.GIFTING) - self.exhibition_application = self.create_mod_clearance_application(self.organisation, CaseTypeEnum.EXHIBITION) self.applications = [ self.standard_application, - self.f680_application, - self.gifting_application, - self.exhibition_application, ] self.template = self.create_letter_template( case_types=[ diff --git a/api/licences/tests/test_hmrc_integration_to_api.py b/api/licences/tests/test_hmrc_integration_to_api.py index 813a32a277..3a78719059 100644 --- a/api/licences/tests/test_hmrc_integration_to_api.py +++ b/api/licences/tests/test_hmrc_integration_to_api.py @@ -32,27 +32,6 @@ def create_siel_licence(self): self._create_good_on_licence(licence, standard_application.goods.first()) return licence - def create_f680_licence(self): - f680_application = self.create_mod_clearance_application_case(self.organisation, CaseTypeEnum.F680) - self.create_advice(self.gov_user, f680_application, "good", AdviceType.APPROVE, AdviceLevel.FINAL) - licence = self.create_licence(f680_application, status=LicenceStatus.ISSUED) - self._create_good_on_licence(licence, f680_application.goods.first()) - return licence - - def create_gifting_licence(self): - gifting_application = self.create_mod_clearance_application_case(self.organisation, CaseTypeEnum.GIFTING) - self.create_advice(self.gov_user, gifting_application, "good", AdviceType.APPROVE, AdviceLevel.FINAL) - licence = self.create_licence(gifting_application, status=LicenceStatus.ISSUED) - self._create_good_on_licence(licence, gifting_application.goods.first()) - return licence - - def create_exhibition_licence(self): - exhibition_application = self.create_mod_clearance_application_case(self.organisation, CaseTypeEnum.EXHIBITION) - self.create_advice(self.gov_user, exhibition_application, "good", AdviceType.APPROVE, AdviceLevel.FINAL) - licence = self.create_licence(exhibition_application, status=LicenceStatus.ISSUED) - self._create_good_on_licence(licence, exhibition_application.goods.first()) - return licence - def create_ogl_licence(self): open_general_licence = OpenGeneralLicenceFactory(case_type=CaseType.objects.get(id=CaseTypeEnum.OGEL.id)) open_general_licence_case = OpenGeneralLicenceCaseFactory( @@ -86,9 +65,7 @@ def _create_good_on_licence(self, licence, good_on_application): value=good_on_application.value, ) - @parameterized.expand( - [[create_siel_licence], [create_f680_licence], [create_gifting_licence], [create_exhibition_licence]] - ) + @parameterized.expand([[create_siel_licence]]) def test_update_usages_accepted_licence_standard_applications(self, create_licence): licence = create_licence(self) gol_first = licence.goods.first() @@ -159,9 +136,7 @@ def test_update_usages_accepted_licence_open_application(self): ).exists() ) - @parameterized.expand( - [[create_siel_licence], [create_f680_licence], [create_gifting_licence], [create_exhibition_licence]] - ) + @parameterized.expand([[create_siel_licence]]) def test_update_usages_all_goods_exhausted_on_licence(self, create_licence): licence = create_licence(self) gol = licence.goods.first() @@ -221,9 +196,7 @@ def test_update_usages_all_goods_exhausted_when_action_is_open_does_inform_hmrc_ self.assertTrue(HMRCIntegrationUsageData.objects.filter(id=usage_data_id, licences=licence).exists()) self.assertEqual(licence.status, LicenceStatus.ISSUED) - @parameterized.expand( - [[create_siel_licence], [create_f680_licence], [create_gifting_licence], [create_exhibition_licence]] - ) + @parameterized.expand([[create_siel_licence]]) def test_update_usages_usage_data_id_already_reported(self, create_licence): licence = create_licence(self) usage_data_id = str(uuid.uuid4()) @@ -242,9 +215,7 @@ def test_update_usages_usage_data_id_already_reported(self, create_licence): self.assertEqual(licence.goods.first().usage, original_usage) self.assertTrue(HMRCIntegrationUsageData.objects.filter(id=usage_data_id).exists()) - @parameterized.expand( - [[create_siel_licence], [create_f680_licence], [create_gifting_licence], [create_exhibition_licence]] - ) + @parameterized.expand([[create_siel_licence]]) def test_update_usages_no_usage_data_id_bad_request(self, create_licence): licence = create_licence(self) original_usage = licence.goods.first().usage @@ -271,9 +242,7 @@ def test_update_usages_no_usage_data_id_bad_request(self, create_licence): self.assertEqual(licence.goods.first().usage, original_usage) self.assertFalse(HMRCIntegrationUsageData.objects.filter(licences=licence).exists()) - @parameterized.expand( - [[create_siel_licence], [create_f680_licence], [create_gifting_licence], [create_exhibition_licence]] - ) + @parameterized.expand([[create_siel_licence]]) def test_update_usages_no_licences_bad_request(self, create_licence): licence = create_licence(self) original_usage = licence.goods.first().usage @@ -289,9 +258,7 @@ def test_update_usages_no_licences_bad_request(self, create_licence): self.assertEqual(licence.goods.first().usage, original_usage) self.assertFalse(HMRCIntegrationUsageData.objects.filter(id=usage_data_id).exists()) - @parameterized.expand( - [[create_siel_licence], [create_f680_licence], [create_gifting_licence], [create_exhibition_licence]] - ) + @parameterized.expand([[create_siel_licence]]) def test_update_usages_no_licence_id_rejected_licence(self, create_licence): licence = create_licence(self) usage_data_id = str(uuid.uuid4()) @@ -311,9 +278,7 @@ def test_update_usages_no_licence_id_rejected_licence(self, create_licence): ) self.assertFalse(HMRCIntegrationUsageData.objects.filter(id=usage_data_id).exists()) - @parameterized.expand( - [[create_siel_licence], [create_f680_licence], [create_gifting_licence], [create_exhibition_licence]] - ) + @parameterized.expand([[create_siel_licence]]) def test_update_usages_invalid_licence_id_rejected_licence(self, create_licence): licence = create_licence(self) usage_data_id = str(uuid.uuid4()) @@ -340,9 +305,7 @@ def test_update_usages_invalid_licence_id_rejected_licence(self, create_licence) ) self.assertFalse(HMRCIntegrationUsageData.objects.filter(id=usage_data_id).exists()) - @parameterized.expand( - [[create_siel_licence], [create_f680_licence], [create_gifting_licence], [create_exhibition_licence]] - ) + @parameterized.expand([[create_siel_licence]]) def test_update_usages_no_action_rejected_licence(self, create_licence): licence = create_licence(self) usage_data_id = str(uuid.uuid4()) @@ -368,9 +331,7 @@ def test_update_usages_no_action_rejected_licence(self, create_licence): ) self.assertFalse(HMRCIntegrationUsageData.objects.filter(id=usage_data_id).exists()) - @parameterized.expand( - [[create_siel_licence], [create_f680_licence], [create_gifting_licence], [create_exhibition_licence]] - ) + @parameterized.expand([[create_siel_licence]]) def test_update_usages_invalid_action_rejected_licence(self, create_licence): licence = create_licence(self) usage_data_id = str(uuid.uuid4()) @@ -397,9 +358,7 @@ def test_update_usages_invalid_action_rejected_licence(self, create_licence): ) self.assertFalse(HMRCIntegrationUsageData.objects.filter(id=usage_data_id).exists()) - @parameterized.expand( - [[create_siel_licence], [create_f680_licence], [create_gifting_licence], [create_exhibition_licence]] - ) + @parameterized.expand([[create_siel_licence]]) def test_update_usages_no_goods_rejected_licence(self, create_licence): licence = create_licence(self) usage_data_id = str(uuid.uuid4()) @@ -420,9 +379,7 @@ def test_update_usages_no_goods_rejected_licence(self, create_licence): ) self.assertFalse(HMRCIntegrationUsageData.objects.filter(id=usage_data_id).exists()) - @parameterized.expand( - [[create_siel_licence], [create_f680_licence], [create_gifting_licence], [create_exhibition_licence]] - ) + @parameterized.expand([[create_siel_licence]]) def test_update_usages_no_good_id_rejected_licence(self, create_licence): licence = create_licence(self) usage_data_id = str(uuid.uuid4()) @@ -445,9 +402,7 @@ def test_update_usages_no_good_id_rejected_licence(self, create_licence): ) self.assertFalse(HMRCIntegrationUsageData.objects.filter(id=usage_data_id).exists()) - @parameterized.expand( - [[create_siel_licence], [create_f680_licence], [create_gifting_licence], [create_exhibition_licence]] - ) + @parameterized.expand([[create_siel_licence]]) def test_update_usages_invalid_good_id_rejected_licence(self, create_licence): licence = create_licence(self) usage_data_id = str(uuid.uuid4()) @@ -474,9 +429,7 @@ def test_update_usages_invalid_good_id_rejected_licence(self, create_licence): ) self.assertFalse(HMRCIntegrationUsageData.objects.filter(id=usage_data_id).exists()) - @parameterized.expand( - [[create_siel_licence], [create_f680_licence], [create_gifting_licence], [create_exhibition_licence]] - ) + @parameterized.expand([[create_siel_licence]]) def test_update_usages_no_good_usage_rejected_licence(self, create_licence): licence = create_licence(self) original_usage = licence.goods.first().usage @@ -505,9 +458,7 @@ def test_update_usages_no_good_usage_rejected_licence(self, create_licence): self.assertEqual(licence.goods.first().usage, original_usage) self.assertFalse(HMRCIntegrationUsageData.objects.filter(id=usage_data_id).exists()) - @parameterized.expand( - [[create_siel_licence], [create_f680_licence], [create_gifting_licence], [create_exhibition_licence]] - ) + @parameterized.expand([[create_siel_licence]]) def test_update_usages_multiple_licences_invalid_licence_id_rejected_licence(self, create_licence): licence = create_licence(self) original_usage = licence.goods.first().usage @@ -541,9 +492,7 @@ def test_update_usages_multiple_licences_invalid_licence_id_rejected_licence(sel HMRCIntegrationUsageData.objects.filter(id=usage_data_id, licences=invalid_licence_id).exists() ) - @parameterized.expand( - [[create_siel_licence], [create_f680_licence], [create_gifting_licence], [create_exhibition_licence]] - ) + @parameterized.expand([[create_siel_licence]]) def test_update_usages_multiple_licences_invalid_good_id_rejected_licence(self, create_licence): licence_1 = create_licence(self) licence_1_original_usage = licence_1.goods.first().usage @@ -578,9 +527,7 @@ def test_update_usages_multiple_licences_invalid_good_id_rejected_licence(self, self.assertTrue(HMRCIntegrationUsageData.objects.filter(id=usage_data_id, licences=licence_1).exists()) self.assertFalse(HMRCIntegrationUsageData.objects.filter(id=usage_data_id, licences=licence_2).exists()) - @parameterized.expand( - [[create_siel_licence], [create_f680_licence], [create_gifting_licence], [create_exhibition_licence]] - ) + @parameterized.expand([[create_siel_licence]]) def test_update_usages_multiple_goods_invalid_good_id_rejected_licence(self, create_licence): licence = create_licence(self) original_usage = licence.goods.first().usage @@ -616,9 +563,7 @@ def test_update_usages_multiple_goods_invalid_good_id_rejected_licence(self, cre self.assertEqual(licence.goods.first().usage, original_usage) self.assertFalse(HMRCIntegrationUsageData.objects.filter(id=usage_data_id).exists()) - @parameterized.expand( - [[create_siel_licence], [create_f680_licence], [create_gifting_licence], [create_exhibition_licence]] - ) + @parameterized.expand([[create_siel_licence]]) def test_update_usages_multiple_licences_and_goods_invalid_good_id_rejected_licence(self, create_licence): usage_data_id = str(uuid.uuid4()) usage_data = 10 diff --git a/api/queries/end_user_advisories/tests/factories.py b/api/queries/end_user_advisories/tests/factories.py new file mode 100644 index 0000000000..5ecaa23284 --- /dev/null +++ b/api/queries/end_user_advisories/tests/factories.py @@ -0,0 +1,10 @@ +import factory +from api.queries.end_user_advisories.models import EndUserAdvisoryQuery +from api.cases.tests.factories import CaseFactory + + +class EndUserAdvisoryQueryFactory(CaseFactory): + end_user = factory.SubFactory("api.parties.tests.factories.PartyFactory") + + class Meta: + model = EndUserAdvisoryQuery diff --git a/api/workflow/tests/test_flagging_rules.py b/api/workflow/tests/test_flagging_rules.py index 7a85455eec..3967e69bd0 100644 --- a/api/workflow/tests/test_flagging_rules.py +++ b/api/workflow/tests/test_flagging_rules.py @@ -1,11 +1,9 @@ import unittest -from django.urls import reverse_lazy from parameterized import parameterized from api.applications.models import GoodOnApplication, PartyOnApplication, CountryOnApplication from api.applications.tests.factories import PartyOnApplicationFactory -from api.cases.enums import CaseTypeEnum from api.flags.enums import FlagLevels, FlagStatuses from api.flags.models import Flag, FlaggingRule from api.goods.enums import GoodStatus @@ -27,145 +25,6 @@ class FlaggingRulesAutomation(DataTestClient): - def test_adding_case_type_flag(self): - flag = self.create_flag(name="case flag", level=FlagLevels.CASE, team=self.team) - self.create_flagging_rule( - level=FlagLevels.CASE, team=self.team, flag=flag, matching_values=[CaseTypeEnum.EXHIBITION.reference] - ) - - case = self.create_mod_clearance_application(self.organisation, CaseTypeEnum.EXHIBITION) - self.submit_application(case) - - apply_flagging_rules_to_case(case) - - case.refresh_from_db() - - self.assertTrue(flag in list(case.flags.all())) - - def test_adding_goods_type_flag_from_case(self): - flag = self.create_flag(name="good flag", level=FlagLevels.GOOD, team=self.team) - case = self.create_mod_clearance_application(self.organisation, CaseTypeEnum.EXHIBITION) - self.submit_application(case) - good = GoodOnApplication.objects.filter(application_id=case.id).first().good - self.create_flagging_rule( - level=FlagLevels.GOOD, team=self.team, flag=flag, matching_values=[good.control_list_entries.first().rating] - ) - - apply_flagging_rules_to_case(case) - - good_flags = list(good.flags.all()) - - self.assertTrue(flag in good_flags) - - def test_adding_goods_type_flag_with_exclusion_entries(self): - flag = self.create_flag(name="good flag", level=FlagLevels.GOOD, team=self.team) - case = self.create_mod_clearance_application(self.organisation, CaseTypeEnum.EXHIBITION) - self.submit_application(case) - good = GoodOnApplication.objects.filter(application_id=case.id).first().good - self.create_flagging_rule( - level=FlagLevels.GOOD, - team=self.team, - flag=flag, - matching_values=[good.control_list_entries.first().rating], - matching_groups=["ML5"], - excluded_values=[good.control_list_entries.first().rating], - ) - - apply_flagging_rules_to_case(case) - - good.flags.count() >= 1 - - def test_adding_goods_type_flag_from_case_with_verified_only_rule_failure(self): - """Test flag not applied to good when flagging rule is for verified goods only.""" - flag = self.create_flag(name="for verified good flag", level=FlagLevels.GOOD, team=self.team) - case = self.create_mod_clearance_application(self.organisation, CaseTypeEnum.EXHIBITION) - self.submit_application(case) - good = GoodOnApplication.objects.filter(application_id=case.id).first().good - - self.create_flagging_rule( - level=FlagLevels.GOOD, - team=self.team, - flag=flag, - matching_values=[good.control_list_entries.first().rating], - is_for_verified_goods_only=True, - ) - - apply_flagging_rules_to_case(case) - - good_flags = list(good.flags.all()) - self.assertFalse(flag in good_flags) - - def test_adding_goods_type_flag_from_case_with_verified_only_rule_success(self): - """Test flag is applied to verified good when the flagging rule is applicable to only verified goods.""" - flag = self.create_flag(name="for verified good flag", level=FlagLevels.GOOD, team=self.team) - case = self.create_mod_clearance_application(self.organisation, CaseTypeEnum.EXHIBITION) - self.submit_application(case) - good = GoodOnApplication.objects.filter(application_id=case.id).first().good - good.status = GoodStatus.VERIFIED - good.save() - - self.create_flagging_rule( - level=FlagLevels.GOOD, - team=self.team, - flag=flag, - matching_values=[good.control_list_entries.first().rating], - is_for_verified_goods_only=True, - ) - - apply_flagging_rules_to_case(case) - - good_flags = list(good.flags.all()) - self.assertTrue(flag in good_flags) - - def test_adding_destination_type_flag_from_case(self): - flag = self.create_flag(name="good flag", level=FlagLevels.DESTINATION, team=self.team) - case = self.create_mod_clearance_application(self.organisation, CaseTypeEnum.F680) - self.submit_application(case) - party = PartyOnApplication.objects.filter(application_id=case.id).first().party - self.create_flagging_rule( - level=FlagLevels.DESTINATION, team=self.team, flag=flag, matching_values=[party.country_id] - ) - - apply_flagging_rules_to_case(case) - - party_flags = list(party.flags.all()) - - self.assertTrue(flag in party_flags) - - def test_case_dont_add_deactivated_flag(self): - flag = self.create_flag(name="case flag", level=FlagLevels.CASE, team=self.team) - self.create_flagging_rule( - level=FlagLevels.CASE, team=self.team, flag=flag, matching_values=[CaseTypeEnum.EXHIBITION.reference] - ) - flag.status = FlagStatuses.DEACTIVATED - flag.save() - - case = self.create_mod_clearance_application(self.organisation, CaseTypeEnum.EXHIBITION) - - apply_flagging_rules_to_case(case) - - case.refresh_from_db() - - self.assertTrue(flag not in list(case.flags.all())) - - def test_case_dont_add_deactivated_flagging_rule(self): - flag = self.create_flag(name="case flag", level=FlagLevels.CASE, team=self.team) - self.create_flagging_rule( - level=FlagLevels.CASE, - team=self.team, - flag=flag, - matching_values=[CaseTypeEnum.EXHIBITION.reference], - status=FlagStatuses.DEACTIVATED, - ) - - case = self.create_mod_clearance_application(self.organisation, CaseTypeEnum.EXHIBITION) - - apply_flagging_rules_to_case(case) - - case.refresh_from_db() - - self.assertTrue(flag not in list(case.flags.all())) - def test_get_active_flagging_rules_goods(self): active_flag = self.create_flag(name="good flag", level=FlagLevels.GOOD, team=self.team) self.create_flagging_rule(level=FlagLevels.GOOD, team=self.team, flag=active_flag, matching_values=["abc"]) @@ -452,61 +311,6 @@ def test_standard_application(self): self.assertIn(good_flag, good.flags.all()) self.assertIn(destination_flag, party.flags.all()) - def test_F680_application(self): - application = self.create_mod_clearance_application(self.organisation, CaseTypeEnum.F680) - - case_flag = self.create_flag("case flag", FlagLevels.CASE, self.team) - self.create_flagging_rule( - FlagLevels.CASE, self.team, flag=case_flag, matching_values=[application.case_type.reference] - ) - - good = GoodOnApplication.objects.filter(application_id=application.id).first().good - good_flag = self.create_flag("good flag", FlagLevels.GOOD, self.team) - self.create_flagging_rule( - FlagLevels.GOOD, self.team, flag=good_flag, matching_values=[good.control_list_entries.first().rating] - ) - - party = PartyOnApplication.objects.filter(application_id=application.id).first().party - destination_flag = self.create_flag("dest flag", FlagLevels.DESTINATION, self.team) - self.create_flagging_rule( - FlagLevels.DESTINATION, self.team, flag=destination_flag, matching_values=[party.country_id] - ) - - self.submit_application(application) - apply_flagging_rules_to_case(application) - - application.refresh_from_db() - good.refresh_from_db() - party.refresh_from_db() - - self.assertIn(case_flag, application.flags.all()) - self.assertIn(good_flag, good.flags.all()) - self.assertIn(destination_flag, party.flags.all()) - - def test_exhibition_application(self): - application = self.create_mod_clearance_application(self.organisation, CaseTypeEnum.EXHIBITION) - self.submit_application(application) - - case_flag = self.create_flag("case flag", FlagLevels.CASE, self.team) - self.create_flagging_rule( - FlagLevels.CASE, self.team, flag=case_flag, matching_values=[application.case_type.reference] - ) - - good = GoodOnApplication.objects.filter(application_id=application.id).first().good - good_flag = self.create_flag("good flag", FlagLevels.GOOD, self.team) - self.create_flagging_rule( - FlagLevels.GOOD, self.team, flag=good_flag, matching_values=[good.control_list_entries.first().rating] - ) - - self.submit_application(application) - apply_flagging_rules_to_case(application) - - application.refresh_from_db() - good.refresh_from_db() - - self.assertIn(case_flag, application.flags.all()) - self.assertIn(good_flag, good.flags.all()) - def test_goods_query_application(self): query = self.create_clc_query("query", self.organisation) diff --git a/apt.yml b/apt.yml index 95b7f7cf5b..1c98617d1c 100644 --- a/apt.yml +++ b/apt.yml @@ -1,3 +1,8 @@ +keys: +- https://www.postgresql.org/media/keys/ACCC4CF8.asc +repos: +- deb http://apt.postgresql.org/pub/repos/apt jammy-pgdg main packages: - swig - swig4.0 +- postgresql-client-12 diff --git a/bin/dump_and_anonymise.sh b/bin/dump_and_anonymise.sh new file mode 100755 index 0000000000..1d2b1a30c4 --- /dev/null +++ b/bin/dump_and_anonymise.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# The symbolic links to pg_dump and psql are not correctly setup for some reason +rm /home/vcap/deps/0/bin/pg_dump +ln -s /home/vcap/deps/0/apt/usr/lib/postgresql/12/bin/pg_dump /home/vcap/deps/0/bin/pg_dump + +rm /home/vcap/deps/0/bin/psql +ln -s /home/vcap/deps/0/apt/usr/lib/postgresql/12/bin/psql /home/vcap/deps/0/bin/psql + +python manage.py dump_and_anonymise diff --git a/django_db_anonymiser b/django_db_anonymiser new file mode 160000 index 0000000000..e450a97e06 --- /dev/null +++ b/django_db_anonymiser @@ -0,0 +1 @@ +Subproject commit e450a97e0622f8ed3195ab0ae32852d0290aaa2e diff --git a/docker-compose.yml b/docker-compose.yml index ab4d4dd12e..4851cb4e3d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -104,7 +104,7 @@ services: - 9000:9000 - 9001:9001 entrypoint: sh - command: -c 'mkdir -p /buckets/uploads && minio server /buckets --console-address ":9001"' + command: -c 'mkdir -p /buckets/uploads && mkdir -p /buckets/anonymiser && minio server /buckets --console-address ":9001"' environment: - MINIO_ROOT_USER=minio_username - MINIO_ROOT_PASSWORD=minio_password diff --git a/local.env b/local.env index 72b0f94014..eb2178b64c 100644 --- a/local.env +++ b/local.env @@ -38,6 +38,12 @@ AWS_SECRET_ACCESS_KEY=minio_password AWS_STORAGE_BUCKET_NAME=uploads AWS_REGION=eu-west-2 +# DB anonymiser AWS +DB_ANONYMISER_AWS_ACCESS_KEY_ID=minio_username +DB_ANONYMISER_AWS_SECRET_ACCESS_KEY=minio_password +DB_ANONYMISER_AWS_STORAGE_BUCKET_NAME=anonymiser +DB_ANONYMISER_AWS_REGION=eu-west-2 + # AV AV_SERVICE_URL=http://localhost:8100/mock_virus_scan/scan AV_SERVICE_USERNAME=DUMMY diff --git a/pyproject.toml b/pyproject.toml index 43327b08ca..ae737a1e3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,5 +21,6 @@ exclude = ''' | dist | separatedvaluesfield | migrations + | django_db_anonymiser )/ ''' diff --git a/pytest.ini b/pytest.ini index 10bff84c3e..76fc6e4346 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,7 +1,7 @@ [pytest] DJANGO_SETTINGS_MODULE = api.conf.settings_test addopts = - -k "not seeding and not elasticsearch and not performance" -p no:warnings -p no:logging -s + -k "not seeding and not elasticsearch and not performance" -p no:warnings -p no:logging -s --ignore=django_db_anonymiser env = ELASTICSEARCH_SANCTION_INDEX_ALIAS=sanctions-alias-test ELASTICSEARCH_DENIALS_INDEX_ALIAS=denials-alias-test diff --git a/test_helpers/clients.py b/test_helpers/clients.py index 9d8c917a48..1504527953 100644 --- a/test_helpers/clients.py +++ b/test_helpers/clients.py @@ -24,9 +24,6 @@ StandardApplication, OpenApplication, ApplicationDocument, - ExhibitionClearanceApplication, - GiftingClearanceApplication, - F680ClearanceApplication, ) from api.applications.tests.factories import ( GoodOnApplicationFactory, @@ -675,80 +672,6 @@ def create_draft_standard_application( return application - def create_mod_clearance_application( - self, - organisation, - case_type, - reference_name="MOD Clearance Draft", - safe_document=True, - additional_information=True, - ): - if case_type == CaseTypeEnum.F680: - model = F680ClearanceApplication - elif case_type == CaseTypeEnum.GIFTING: - model = GiftingClearanceApplication - elif case_type == CaseTypeEnum.EXHIBITION: - model = ExhibitionClearanceApplication - else: - raise BaseException("Invalid case type when creating test MOD Clearance application") - - application = model.objects.create( - name=reference_name, - activity="Trade", - usage="Trade", - organisation=organisation, - case_type_id=case_type.id, - status=get_case_status_by_status(CaseStatusEnum.DRAFT), - clearance_level=PvGrading.UK_UNCLASSIFIED if case_type == CaseTypeEnum.F680 else None, - submitted_by=self.exporter_user, - ) - - if case_type == CaseTypeEnum.EXHIBITION: - application.title = "title" - application.required_by_date = "2030-07-20" - application.first_exhibition_date = "2030-07-20" - application.save() - # must be refreshed to return data in same format as database call - application.refresh_from_db() - elif case_type == CaseTypeEnum.F680: - application.types.add(F680ClearanceType.objects.first()) - self.create_party("End User", organisation, PartyType.END_USER, application) - self.create_party("Third party", organisation, PartyType.THIRD_PARTY, application) - self.add_party_documents(application, safe_document, consignee=case_type == CaseTypeEnum.EXHIBITION) - if additional_information: - self.add_additional_information(application) - application.intended_end_use = "intended end use here" - application.save() - else: - self.create_party("End User", organisation, PartyType.END_USER, application) - self.create_party("Third party", organisation, PartyType.THIRD_PARTY, application) - self.add_party_documents(application, safe_document, consignee=case_type == CaseTypeEnum.EXHIBITION) - - if case_type not in [CaseTypeEnum.F680, CaseTypeEnum.EXHIBITION, CaseTypeEnum.GIFTING]: - self.create_party("Consignee", organisation, PartyType.CONSIGNEE, application) - - # Add a good to the standard application - self.good_on_application = GoodOnApplication.objects.create( - good=GoodFactory(organisation=organisation, is_good_controlled=True), - application=application, - quantity=10, - unit=Units.NAR, - value=500, - ) - - self.create_application_document(application) - - if case_type == CaseTypeEnum.EXHIBITION: - # Add a site to the application - SiteOnApplication(site=organisation.primary_site, application=application).save() - - return application - - def create_mod_clearance_application_case(self, organisation, case_type): - draft = self.create_mod_clearance_application(organisation, case_type) - - return self.submit_application(draft, self.exporter_user) - def create_incorporated_good_and_ultimate_end_user_on_application(self, organisation, application): good = Good.objects.create( is_good_controlled=True, organisation=self.organisation, description="a good", part_number="123456"