diff --git a/Pipfile b/Pipfile index 3c72de655e..2b003f57ea 100644 --- a/Pipfile +++ b/Pipfile @@ -64,12 +64,12 @@ django-log-formatter-ecs = "==0.0.5" whitenoise = "~=5.3.0" django-audit-log-middleware = "~=0.0.4" django-extensions = "~=3.2.3" -ipython = "~=7.34.0" +ipython = "~=8.10.0" celery = "~=5.3.0" redis = "~=4.4.4" django-test-migrations = "~=1.2.0" django-silk = "~=5.0.3" -django = "~=4.2.15" +django = "~=4.2.17" django-queryable-properties = "~=1.9.1" database-sanitizer = ">=1.1.0" django-reversion = ">=5.0.12" diff --git a/Pipfile.lock b/Pipfile.lock index 61dbfc301f..70a4e4f7f2 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "5b04da23ff858fb3447d3919e58d73cec97e41e8a970308baa2c7521fe83cab9" + "sha256": "3466bbe6fe8a352e4e7c997025e60e49b6723bebadd605fd74d5c8beaab08f43" }, "pipfile-spec": 6, "requires": { @@ -33,6 +33,14 @@ "markers": "python_version >= '3.8'", "version": "==3.8.1" }, + "asttokens": { + "hashes": [ + "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", + "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2" + ], + "markers": "python_version >= '3.8'", + "version": "==3.0.0" + }, "async-timeout": { "hashes": [ "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", @@ -463,12 +471,12 @@ }, "django": { "hashes": [ - "sha256:1ddc333a16fc139fd253035a1606bb24261951bbc3a6ca256717fa06cc41a898", - "sha256:6f1616c2786c408ce86ab7e10f792b8f15742f7b7b7460243929cb371e7f1dad" + "sha256:3a93350214ba25f178d4045c0786c61573e7dbfa3c509b3551374f1e11ba8de0", + "sha256:6b56d834cc94c8b21a8f4e775064896be3b4a4ca387f2612d4406a5927cd2fdc" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.2.16" + "version": "==4.2.17" }, "django-activity-stream": { "hashes": [ @@ -734,6 +742,14 @@ "markers": "python_version >= '3.8'", "version": "==2.0.0" }, + "executing": { + "hashes": [ + "sha256:8d63781349375b5ebccc3142f4b30350c0cd9c79f921cde38be2be4637e98eaf", + "sha256:8ea27ddd260da8150fa5a708269c4a10e76161e2496ec3e587da9e3c0fe4b9ab" + ], + "markers": "python_version >= '3.8'", + "version": "==2.1.0" + }, "factory-boy": { "hashes": [ "sha256:728df59b372c9588b83153facf26d3d28947fc750e8e3c95cefa9bed0e6394ee", @@ -1022,12 +1038,12 @@ }, "ipython": { "hashes": [ - "sha256:af3bdb46aa292bce5615b1b2ebc76c2080c5f77f54bda2ec72461317273e7cd6", - "sha256:c175d2440a1caff76116eb719d40538fbb316e214eda85c5515c303aacbfb23e" + "sha256:b13a1d6c1f5818bd388db53b7107d17454129a70de2b87481d555daede5eb49e", + "sha256:b38c31e8fc7eff642fc7c597061fff462537cf2314e3225a19c906b7b0d8a345" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==7.34.0" + "markers": "python_version >= '3.8'", + "version": "==8.10.0" }, "jdcal": { "hashes": [ @@ -1440,6 +1456,13 @@ ], "version": "==0.7.0" }, + "pure-eval": { + "hashes": [ + "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", + "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42" + ], + "version": "==0.2.3" + }, "pycodestyle": { "hashes": [ "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3", @@ -1458,19 +1481,20 @@ }, "pygments": { "hashes": [ - "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", - "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a" + "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", + "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c" ], "markers": "python_version >= '3.8'", - "version": "==2.18.0" + "version": "==2.19.1" }, "pyjwt": { "hashes": [ - "sha256:543b77207db656de204372350926bed5a86201c4cbff159f623f79c7bb487a15", - "sha256:7628a7eb7938959ac1b26e819a1df0fd3259505627b575e4bad6d08f76db695c" + "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", + "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb" ], + "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==2.10.0" + "version": "==2.10.1" }, "pypdf2": { "hashes": [ @@ -1743,6 +1767,13 @@ "markers": "python_version >= '3.8'", "version": "==0.5.2" }, + "stack-data": { + "hashes": [ + "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", + "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695" + ], + "version": "==0.6.3" + }, "tblib": { "hashes": [ "sha256:059bd77306ea7b419d4f76016aef6d7027cc8a0785579b5aad198803435f882c", @@ -1781,7 +1812,7 @@ "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], - "markers": "python_version >= '3.8'", + "markers": "python_version < '3.10'", "version": "==4.12.2" }, "tzdata": { @@ -2543,11 +2574,12 @@ }, "jinja2": { "hashes": [ - "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", - "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d" + "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", + "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb" ], + "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==3.1.4" + "version": "==3.1.5" }, "jmespath": { "hashes": [ diff --git a/api/cases/managers.py b/api/cases/managers.py index e777fdfc52..fb87617898 100644 --- a/api/cases/managers.py +++ b/api/cases/managers.py @@ -323,6 +323,7 @@ def search( # noqa "queues", "queues__team", "baseapplication__licences", + "flags", Prefetch( "baseapplication__parties", to_attr="end_user_parties", diff --git a/api/cases/serializers.py b/api/cases/serializers.py index 2262a2a728..3146bc8744 100644 --- a/api/cases/serializers.py +++ b/api/cases/serializers.py @@ -33,6 +33,8 @@ from api.compliance.serializers.ComplianceVisitCaseSerializers import ComplianceVisitSerializer from api.core.serializers import KeyValueChoiceField, PrimaryKeyRelatedSerializerField from api.documents.libraries.process_document import process_document +from api.flags.serializers import CaseListFlagSerializer +from api.flags.models import Flag from api.gov_users.serializers import GovUserSimpleSerializer from api.organisations.models import Organisation from api.organisations.serializers import TinyOrganisationViewSerializer @@ -122,6 +124,7 @@ class CaseListSerializer(serializers.Serializer): intended_end_use = serializers.SerializerMethodField() end_users = serializers.SerializerMethodField() sub_status = CaseSubStatusSerializer() + flags = PrimaryKeyRelatedSerializerField(many=True, queryset=Flag.objects.all(), serializer=CaseListFlagSerializer) def __init__(self, *args, **kwargs): self.team = kwargs.pop("team", None) diff --git a/api/cases/views/search/service.py b/api/cases/views/search/service.py index aa85f8c990..5f718fcbbc 100644 --- a/api/cases/views/search/service.py +++ b/api/cases/views/search/service.py @@ -16,6 +16,7 @@ from api.applications.serializers.advice import AdviceSearchViewSerializer from api.cases.models import Case, EcjuQuery, Advice from api.common.dates import working_days_in_range, number_of_days_since +from api.flags.models import Flag from api.flags.serializers import CaseListFlagSerializer from api.organisations.models import Organisation from api.staticdata.statuses.enums import CaseStatusEnum @@ -50,27 +51,7 @@ def get_advice_types_list(): return AdviceType.to_representation() -def populate_other_flags(cases: List[Dict]): - from api.flags.models import Flag - - case_ids = [case["id"] for case in cases] - - union_flags = set( - [ - *Flag.objects.filter(cases__id__in=case_ids).annotate(case_id=F("cases__id")), - *Flag.objects.filter(organisations__cases__id__in=case_ids).annotate(case_id=F("organisations__cases__id")), - ] - ) - - for case in cases: - case_id = str(case["id"]) - flags = [flag for flag in union_flags if str(flag.case_id) == case_id] - case["flags"] = CaseListFlagSerializer(flags, many=True).data - - def populate_goods_flags(cases: List[Dict]): - from api.flags.models import Flag - case_ids = [case["id"] for case in cases] qs1 = Flag.objects.filter(goods__goods_on_application__application_id__in=case_ids).annotate( case_id=F("goods__goods_on_application__application_id"), diff --git a/api/cases/views/search/tests/__init__.py b/api/cases/views/search/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/cases/tests/views/search/test_service.py b/api/cases/views/search/tests/test_service.py similarity index 100% rename from api/cases/tests/views/search/test_service.py rename to api/cases/views/search/tests/test_service.py diff --git a/api/cases/tests/test_case_search.py b/api/cases/views/search/tests/test_views.py similarity index 98% rename from api/cases/tests/test_case_search.py rename to api/cases/views/search/tests/test_views.py index bf4bc5fbca..8eda5cd6a9 100644 --- a/api/cases/tests/test_case_search.py +++ b/api/cases/views/search/tests/test_views.py @@ -910,7 +910,7 @@ def setUp(self): super().setUp() self.other_team = self.create_team("other team") - self.other_team_gov_user = self.create_gov_user("new_user@digital.trade.gov.uk", self.other_team) + self.other_team_gov_user = self.create_gov_user("new_user@digital.trade.gov.uk", self.other_team) # /PS-IGNORE self.queue = self.create_queue("my new queue", self.team) self.case = self.create_standard_application_case(self.organisation) self.case.queues.set([self.queue]) @@ -1011,7 +1011,7 @@ def test_api_success(self): self._create_data() self.advice = FinalAdviceFactory(user=self.gov_user, case=self.case, type=AdviceType.APPROVE, good=self.good) self.gov_user2 = self.create_gov_user( - team=self.create_team(name="other_team"), email="new_user2@digital.trade.gov.uk" + team=self.create_team(name="other_team"), email="new_user2@digital.trade.gov.uk" # /PS-IGNORE ) self.group_advice = FinalAdviceFactory( user=self.gov_user2, case=self.case, type=AdviceType.REFUSE, good=self.good @@ -1148,12 +1148,33 @@ def test_api_success(self): ], ) - # Reflect rest framework's way of rendering datetime objects... https://github.com/encode/django-rest-framework/blob/c9e7b68a4c1db1ac60e962053380acda549609f3/rest_framework/utils/encoders.py#L29 + # Reflect rest framework's way of rendering datetime objects... https://github.com/encode/django-rest-framework/blob/c9e7b68a4c1db1ac60e962053380acda549609f3/rest_framework/utils/encoders.py#L29 /PS-IGNORE expected_submitted_at = self.case.submitted_at.isoformat() if expected_submitted_at.endswith("+00:00"): expected_submitted_at = expected_submitted_at[:-6] + "Z" self.assertEqual(case_api_result["submitted_at"], expected_submitted_at) + def test_api_multiple_cases_flags_correct(self): + # Create two cases.. + self._create_data() + self._create_data() + # Add a case flag to each case.. + flag_alias = "REFER_TO_FCDO_MEUC_CONCERNS" + flag = Flag.objects.get(alias=flag_alias) + all_cases = Case.objects.all() + for case in all_cases: + case.flags.add(flag) + + # Perform the search + response = self.client.get(self.url, **self.gov_headers) + response_data = response.json()["results"] + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response_data["cases"]), 2) + for search_result in response_data["cases"]: + self.assertEqual(len(search_result["flags"]), 1) + self.assertEqual(search_result["flags"][0]["alias"], flag_alias) + def test_api_no_advice(self): self._create_data() response = self.client.get(self.url, **self.gov_headers) diff --git a/api/cases/views/search/views.py b/api/cases/views/search/views.py index c57c984d0f..4c838ec840 100644 --- a/api/cases/views/search/views.py +++ b/api/cases/views/search/views.py @@ -59,7 +59,6 @@ def get(self, request, *args, **kwargs): # Populate certain fields outside of the serializer for performance improvements service.populate_goods_flags(cases) service.populate_destinations_flags(cases) - service.populate_other_flags(cases) service.populate_organisation(cases) service.populate_is_recently_updated(cases) service.populate_activity_updates(case_map) diff --git a/api/conf/settings.py b/api/conf/settings.py index 4222882301..68eaf8be5f 100644 --- a/api/conf/settings.py +++ b/api/conf/settings.py @@ -404,7 +404,7 @@ { "un_sanctions_file": "https://scsanctions.un.org/resources/xml/en/consolidated.xml", "office_financial_sanctions_file": "https://ofsistorage.blob.core.windows.net/publishlive/2022format/ConList.xml", - "uk_sanctions_file": "https://assets.publishing.service.gov.uk/media/65ca02639c5b7f0012951caf/UK_Sanctions_List.xml", # /PS-IGNORE + "uk_sanctions_file": "https://assets.publishing.service.gov.uk/media/6776a7d49d03f12136308d1c/UK_Sanctions_List.xml", # /PS-IGNORE }, ) LITE_INTERNAL_NOTIFICATION_EMAILS = env.json("LITE_INTERNAL_NOTIFICATION_EMAILS", {}) diff --git a/api/organisations/serializers.py b/api/organisations/serializers.py index 6d5469c423..0beab5513f 100644 --- a/api/organisations/serializers.py +++ b/api/organisations/serializers.py @@ -287,7 +287,11 @@ def validate_registration_number(self, value): # Check for uniqueness only when creating a new Organisation if not self.instance: - if Organisation.objects.filter(registration_number=value).exists(): + if ( + Organisation.objects.filter(registration_number=value) + .exclude(status__in=[OrganisationStatus.REJECTED, OrganisationStatus.DRAFT]) + .exists() + ): raise serializers.ValidationError("This registration number is already in use.") return value @@ -502,7 +506,11 @@ class OrganisationRegistrationNumberSerializer(serializers.Serializer): def validate_registration_number(self, value): # Check for uniqueness only when creating a new Organisation if not self.instance: - if Organisation.objects.filter(registration_number=value).exists(): + if ( + Organisation.objects.filter(registration_number=value) + .exclude(status__in=[OrganisationStatus.REJECTED, OrganisationStatus.DRAFT]) + .exists() + ): raise serializers.ValidationError("This registration number is already in use.") return value diff --git a/api/organisations/tests/test_organisations.py b/api/organisations/tests/test_organisations.py index 17ef022ce6..12ea5588e7 100644 --- a/api/organisations/tests/test_organisations.py +++ b/api/organisations/tests/test_organisations.py @@ -1,7 +1,6 @@ import pytest from unittest import mock -from django.conf import settings from faker import Faker from parameterized import parameterized @@ -14,7 +13,6 @@ from api.core.authentication import EXPORTER_USER_TOKEN_HEADER from api.core.constants import Roles, GovPermissions from lite_content.lite_api.strings import Organisations -from api.organisations.constants import UK_VAT_VALIDATION_REGEX, UK_EORI_VALIDATION_REGEX from api.organisations.enums import OrganisationType, OrganisationStatus from api.organisations.tests.factories import OrganisationFactory from api.organisations.models import Organisation @@ -177,7 +175,7 @@ def test_create_commercial_organisation_as_internal_success( "region": "Hertfordshire", "postcode": "AL1 4GT", "city": "St Albans", - } + }, ], [{"address": "123", "country": "PL"}], ] @@ -248,6 +246,51 @@ def test_create_commercial_organisation_as_exporter_success( {"organisation_name": data["name"], "applicant_email": data["user"]["email"]} ) + def test_create_commercial_organisation_as_exporter_success_with_previously_rejected_or_draft_crn(self): + data = { + "name": "Lemonworld Co", + "type": OrganisationType.COMMERCIAL, + "eori_number": "GB123456789000", + "sic_number": "01110", + "vat_number": "GB123456789", + "registration_number": "98765432", + "phone_number": "+441234567895", + "website": "", + "site": { + "name": "Headquarters", + "address": { + "address_line_1": "42 Industrial Estate", + "address_line_2": "Queens Road", + "region": "Hertfordshire", + "postcode": "AL1 4GT", + "city": "St Albans", + }, + }, + "user": {"email": "trinity@bsg.com"}, + } + response = self.client.post( + self.url, data, **{EXPORTER_USER_TOKEN_HEADER: user_to_token(self.exporter_user.baseuser_ptr)} + ) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + organisation = Organisation.objects.get(id=response.json()["id"]) + organisation.status = OrganisationStatus.REJECTED + organisation.save() + response = self.client.post( + self.url, data, **{EXPORTER_USER_TOKEN_HEADER: user_to_token(self.exporter_user.baseuser_ptr)} + ) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + organisation = Organisation.objects.get(id=response.json()["id"]) + organisation.status = OrganisationStatus.DRAFT + organisation.save() + response = self.client.post( + self.url, data, **{EXPORTER_USER_TOKEN_HEADER: user_to_token(self.exporter_user.baseuser_ptr)} + ) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + def test_create_organisation_phone_number_mandatory(self): data = { "name": "Lemonworld Co", @@ -974,3 +1017,13 @@ def test_validate_registration_number_fail(self): self.assertEqual( response.json(), {"errors": {"registration_number": ["This registration number is already in use."]}} ) + + @parameterized.expand([OrganisationStatus.REJECTED, OrganisationStatus.DRAFT]) + def test_validate_registration_number_success_for_rejected_or_draft_org(self, org_status): + self.organisation.refresh_from_db() + self.organisation.status = org_status + self.organisation.save() + data = {"registration_number": self.organisation.registration_number} + response = self.client.post(self.url, data, **self.exporter_headers) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json(), data)