Skip to content

Commit

Permalink
Merge pull request #1785 from uktrade/dev
Browse files Browse the repository at this point in the history
UAT deployment
  • Loading branch information
hnryjmes authored Jan 22, 2024
2 parents 1b01248 + 996ddc6 commit a34f452
Show file tree
Hide file tree
Showing 20 changed files with 285 additions and 264 deletions.
4 changes: 3 additions & 1 deletion Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ faker = "~=4.18.0"
boto3 = "~=1.26.17"
django-activity-stream = "~=0.10.0"
django-allow-cidr = "~=0.5.0"
django-background-tasks = "~=1.2.5"
django-elasticsearch-dsl = "~=7.2.2"
django-elasticsearch-dsl-drf = "~=0.22.5"
django-environ = "~=0.9.0"
Expand Down Expand Up @@ -71,7 +70,10 @@ redis = "~=4.4.4"
psycopg2-binary = "~=2.9.3"
django-test-migrations = "~=1.2.0"
django-silk = "~=5.0.3"
django-queryable-properties = "~=1.9.1"
django = "~=3.2.23"
django-compat = "~=1.0.15"


[requires]
python_version = "3.8"
Expand Down
383 changes: 201 additions & 182 deletions Pipfile.lock

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion Procfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
web: SWIG_LIB=/home/vcap/deps/0/apt/usr/share/swig4.0 CFLAGS=-I/home/vcap/deps/1/python/include/python3.8.18m pip3 install endesive==1.5.9 && python manage.py migrate && gunicorn --worker-class gevent -c api/conf/gconfig.py -b 0.0.0.0:$PORT api.conf.wsgi
worker: python manage.py process_tasks
celeryworker: celery -A api.conf worker -l info
celeryscheduler: celery -A api.conf beat
2 changes: 1 addition & 1 deletion api/cases/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ def search( # noqa
CaseReviewDate = get_model("cases", "casereviewdate")

case_qs = case_qs.exclude(
id__in=EcjuQuery.objects.filter(raised_by_user__team_id=user.team.id, responded_at__isnull=True)
id__in=EcjuQuery.objects.filter(raised_by_user__team_id=user.team.id, is_query_closed=False)
.values("case_id")
.distinct()
)
Expand Down
17 changes: 17 additions & 0 deletions api/cases/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@
from django.conf import settings
from django.contrib.contenttypes.fields import GenericRelation
from django.db import models
from django.db.models import Q
from django.utils import timezone

from rest_framework.exceptions import ValidationError
from queryable_properties.managers import QueryablePropertiesManager
from queryable_properties.properties import queryable_property


from api.audit_trail.enums import AuditType
from api.cases.enums import (
Expand Down Expand Up @@ -588,6 +593,9 @@ class EcjuQuery(TimestampableModel):
Query from ECJU to exporters
"""

# Allows the properies to be queryable, allowing filters
objects = QueryablePropertiesManager()

id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
question = models.CharField(null=False, blank=False, max_length=5000)
response = models.CharField(null=True, blank=False, max_length=2200)
Expand All @@ -612,6 +620,15 @@ class EcjuQuery(TimestampableModel):
choices=ECJUQueryType.choices, max_length=50, default=ECJUQueryType.ECJU, null=False, blank=False
)

@queryable_property
def is_query_closed(self):
return self.responded_at is not None

# This method allows the above propery to be used in filtering objects. Similar to db fields.
@is_query_closed.filter(lookups=("exact",))
def is_query_closed(self, lookup, value):
return ~Q(responded_at__isnull=value)

notifications = GenericRelation(ExporterNotification, related_query_name="ecju_query")

def save(self, *args, **kwargs):
Expand Down
3 changes: 3 additions & 0 deletions api/cases/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ class ECJUQuerySummarySerializer(serializers.Serializer):
raised_by_user = serializers.SerializerMethodField()
responded_by_user = serializers.SerializerMethodField()
query_type = serializers.CharField()
is_query_closed = serializers.BooleanField()

def _user_name(self, user):
if not user:
Expand Down Expand Up @@ -573,6 +574,7 @@ class Meta:
"responded_at",
"query_type",
"documents",
"is_query_closed",
)

def get_raised_by_user_name(self, instance):
Expand Down Expand Up @@ -610,6 +612,7 @@ class Meta:
"created_at",
"responded_at",
"documents",
"is_query_closed",
)

def get_responded_by_user(self, instance):
Expand Down
51 changes: 18 additions & 33 deletions api/cases/tests/test_case_ecju_queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,15 @@ def test_correct_ecju_query_details_are_returned_to_exporter_user(self):
self.assertEqual(returned_ecju_query_1.get("question"), "ECJU Query 1")
self.assertEqual(returned_ecju_query_1.get("response"), None)
self.assertEqual(returned_ecju_query_1.get("team")["name"], self.ecju_query_1.team.name)
self.assertEqual(returned_ecju_query_1.get("is_query_closed"), self.ecju_query_1.is_query_closed)
# We can't predict exactly when the query is created so we settle for the fact that its set
self.assertIsNotNone(returned_ecju_query_1.get("created_at"))

returned_ecju_query_2 = response_json.get("ecju_queries")[1]
self.assertEqual(returned_ecju_query_2.get("question"), "ECJU Query 2")
self.assertEqual(returned_ecju_query_2.get("response"), "I have a response")
self.assertEqual(returned_ecju_query_2.get("team")["name"], self.ecju_query_2.team.name)
self.assertEqual(returned_ecju_query_2.get("is_query_closed"), self.ecju_query_2.is_query_closed)
# We can't predict exactly when the query is created so we settle for the fact that its set
self.assertIsNotNone(returned_ecju_query_1.get("created_at"))

Expand Down Expand Up @@ -166,6 +168,7 @@ def test_gov_user_can_get_an_individual_ecju_query(self):
self.assertEqual(str(ecju_query.question), response_data["ecju_query"]["question"])
self.assertEqual(ecju_query.response, None)
self.assertEqual(str(ecju_query.case.id), response_data["ecju_query"]["case"])
self.assertEqual(ecju_query.is_query_closed, response_data["ecju_query"]["is_query_closed"])

def test_ecju_query_open_query_count(self):
"""
Expand All @@ -174,64 +177,45 @@ def test_ecju_query_open_query_count(self):
Then the request is successful we return the number of open ECJUQueries
"""
case = self.create_standard_application_case(self.organisation)
EcjuQueryFactory(question="open", case=case, raised_by_user=self.gov_user, response=None)
EcjuQueryFactory(question="open query 1", case=case, raised_by_user=self.gov_user, response=None)

url = reverse("cases:case_ecju_query_open_count", kwargs={"pk": case.id})

# Act
response = self.client.get(url, **self.gov_headers)

# Assert
response_data = response.json()

self.assertEqual(status.HTTP_200_OK, response.status_code)
self.assertEqual(1, response_data["count"])

EcjuQueryFactory(
question="open query 2",
case=case,
responded_by_user=self.exporter_user.baseuser_ptr,
response="I have a response only",
)

response = self.client.get(url, **self.gov_headers)
self.assertEqual(status.HTTP_200_OK, response.status_code)
self.assertEqual(1, response_data["count"])

def test_ecju_query_open_query_count_responded_return_zero(self):
"""
Given an ECJU query
When a user request no of open queries on a case
Then the request is successful we return 0 as open queries
"""
case = self.create_standard_application_case(self.organisation)
case_2 = self.create_standard_application_case(self.organisation)

ecju_query = EcjuQueryFactory(
question="open",
case=case,
responded_by_user=self.exporter_user.baseuser_ptr,
responded_at=timezone.now(),
)

EcjuQueryFactory(
question="open",
question="closed query",
case=case,
responded_by_user=self.exporter_user.baseuser_ptr,
response="I have a response only",
responded_at=timezone.now(),
)

url = reverse("cases:case_ecju_query_open_count", kwargs={"pk": case.id})

# Act
response = self.client.get(url, **self.gov_headers)
response_2 = self.client.get(
reverse("cases:case_ecju_query_open_count", kwargs={"pk": case_2.id}), **self.gov_headers
)

# Assert
response_data = response.json()

self.assertEqual(status.HTTP_200_OK, response.status_code)
self.assertEqual(0, response_data["count"])

response_data_2 = response.json()

self.assertEqual(status.HTTP_200_OK, response.status_code)
self.assertEqual(0, response_data_2["count"])

ecju_query.response = "I have a response now"
ecju_query.save()

response = self.client.get(url, **self.gov_headers)

response_data = response.json()
Expand Down Expand Up @@ -260,6 +244,7 @@ def test_gov_user_can_create_ecju_queries(self, query_type, mock_notify):
self.assertEqual(status.HTTP_201_CREATED, response.status_code)
self.assertEqual(response_data["ecju_query_id"], str(ecju_query.id))
self.assertEqual("Test ECJU Query question?", ecju_query.question)
self.assertEqual(False, ecju_query.is_query_closed)

mock_notify.assert_called_with(case.id)

Expand Down
1 change: 1 addition & 0 deletions api/cases/tests/test_case_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -1118,6 +1118,7 @@ def test_api_success(self):
"raised_by_user": f"{self.ecju_query.raised_by_user.first_name} {self.ecju_query.raised_by_user.last_name}",
"responded_by_user": f"{self.ecju_query.responded_by_user.first_name} {self.ecju_query.responded_by_user.last_name}",
"query_type": self.ecju_query.query_type,
"is_query_closed": self.ecju_query.is_query_closed,
}
],
)
Expand Down
2 changes: 1 addition & 1 deletion api/cases/views/search/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def get(self, request, *args, **kwargs):
),
has_open_queries=Exists(
EcjuQuery.objects.filter(
case=OuterRef("pk"), raised_by_user__team_id=user.team.id, responded_at__isnull=True
case=OuterRef("pk"), raised_by_user__team_id=user.team.id, is_query_closed=False
)
),
)
Expand Down
6 changes: 1 addition & 5 deletions api/cases/views/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -590,11 +590,7 @@ class ECJUQueriesOpenCount(APIView):

def get(self, request, pk):
"""Gets count of all open queries."""
qs = EcjuQuery.objects.filter(
case__pk=pk,
responded_at__isnull=True,
response__isnull=True,
)
qs = EcjuQuery.objects.filter(case__pk=pk, is_query_closed=False)
return JsonResponse(data={"count": qs.count()})


Expand Down
8 changes: 0 additions & 8 deletions api/conf/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
ALLOWED_HOSTS=(str, ""),
DEBUG=(bool, False),
LOG_LEVEL=(str, "INFO"),
BACKGROUND_TASK_ENABLED=(bool, False),
SUPPRESS_TEST_OUTPUT=(bool, False),
HAWK_AUTHENTICATION_ENABLED=(bool, True),
LITE_HMRC_INTEGRATION_ENABLED=(bool, False),
Expand Down Expand Up @@ -58,7 +57,6 @@
"api.applications",
"api.audit_trail",
"api.bookmarks",
"background_task",
"api.cases",
"api.cases.generated_documents",
"api.compliance",
Expand Down Expand Up @@ -232,12 +230,6 @@

DEFAULT_AUTO_FIELD = "django.db.models.AutoField"

# Background tasks
BACKGROUND_TASK_ENABLED = env("BACKGROUND_TASK_ENABLED")
BACKGROUND_TASK_RUN_ASYNC = True
# Number of times a task is retried given a failure occurs with exponential back-off = ((current_attempt ** 4) + 5)
MAX_ATTEMPTS = 7 # e.g. 7th attempt occurs approx 40 minutes after 1st attempt (assuming instantaneous failures)

# AWS
VCAP_SERVICES = env.json("VCAP_SERVICES", {})

Expand Down
2 changes: 1 addition & 1 deletion api/documents/celery_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def scan_document_for_viruses(self, document_id):

try:
document.scan_for_viruses()
except Exception as exc: # noqa
except Exception:
logger.exception("Document virus scan failed")
raise

Expand Down
8 changes: 5 additions & 3 deletions api/documents/libraries/process_document.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@

from api.documents.celery_tasks import scan_document_for_viruses, delete_document_from_s3

logger = logging.getLogger(__name__)


def process_document(document):
try:
document_id = str(document.id)
scan_document_for_viruses.apply_async(args=(document_id,), link_error=delete_document_from_s3.si(document_id))
except Exception as e:
logging.error(e)
raise serializers.ValidationError({"document": e})
except Exception:
logger.exception("Error scanning document with id %s for viruses", document_id)
raise serializers.ValidationError({"document": "Error scanning document for viruses"})
16 changes: 8 additions & 8 deletions api/letter_templates/templates/letter_templates/siel.html
Original file line number Diff line number Diff line change
Expand Up @@ -606,19 +606,19 @@ <h1>Standard Individual Export Licence</h1>
the Customs and Excise Management Act 1979, or the legislation under which this licence was issued.
</div>
<div class="terms-and-conditions-paragraph">
<strong>Understanding the Control List Number (Rating Classification)</strong>
<strong>Understanding the control list entry</strong>
</div>
<div class="terms-and-conditions-paragraph">
Control List No.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Relevant Control List<br>
ML*&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;UK Military List
The control list entry is part of the UK Strategic Export Control List.
</div>
<div class="terms-and-conditions-paragraph">
For further details on the Control List number (rating), read the UK Strategic Export Control Lists guidance on GOV.UK:
For further details on the control list entry and how you can use this information, read the guidance on GOV.UK:
</div>

<a href="https://www.gov.uk/guidance/uk-strategic-export-control-lists-the-consolidated-list-of-strategic-military-and-dual-use-items">
https://www.gov.uk/guidance/uk-strategic-export-control-lists-the-consolidated-list-of-strategic-military-and-dual-use-items
</a>
<div class="terms-and-conditions-paragraph">
<a href="https://www.gov.uk/government/publications/uk-strategic-export-control-lists-the-consolidated-list-of-strategic-military-and-dual-use-items-that-require-export-authorisation">
https://www.gov.uk/government/publications/uk-strategic-export-control-lists-the-consolidated-list-of-strategic-military-and-dual-use-items-that-require-export-authorisation
</a>
</div>
</td>
</tr>
</table>
Expand Down
6 changes: 3 additions & 3 deletions api/licences/tests/test_api_to_hmrc_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from api.cases.tests.factories import GoodCountryDecisionFactory
from api.core.constants import GovPermissions
from api.core.helpers import add_months
from api.conf.settings import MAX_ATTEMPTS, LITE_HMRC_REQUEST_TIMEOUT
from api.conf.settings import LITE_HMRC_REQUEST_TIMEOUT
from api.licences.enums import LicenceStatus, HMRCIntegrationActionEnum, licence_status_to_hmrc_integration_action
from api.licences.helpers import get_approved_goods_types, get_approved_countries
from api.licences.libraries.hmrc_integration_operations import (
Expand Down Expand Up @@ -504,15 +504,15 @@ def test_send_licence_to_hmrc_integration_failure(self, send_licence):
send_licence.assert_called_with(self.standard_licence, self.hmrc_integration_status)

@mock.patch("api.licences.celery_tasks.send_licence")
def test_send_licence_to_hmrc_integration_with_background_task_success(self, send_licence):
def test_send_licence_to_hmrc_integration_task_success(self, send_licence):
send_licence.return_value = None

send_licence_details_to_lite_hmrc.delay(str(self.standard_licence.id), self.hmrc_integration_status)

send_licence.assert_called_once()

@mock.patch("api.licences.celery_tasks.send_licence")
def test_send_licence_to_hmrc_integration_with_background_task_failure(self, send_licence):
def test_send_licence_to_hmrc_integration_task_failure(self, send_licence):
send_licence.side_effect = HMRCIntegrationException("Received an unexpected response")

with self.assertRaises(HMRCIntegrationException) as error:
Expand Down
3 changes: 1 addition & 2 deletions api/search/signals.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from background_task.models import Task
from django.core.exceptions import ObjectDoesNotExist
from django.conf import settings
from django.db.models.signals import post_save
Expand All @@ -11,7 +10,7 @@

@receiver(post_save)
def update_search_documents(sender, **kwargs):
if not settings.LITE_API_ENABLE_ES or issubclass(sender, Task):
if not settings.LITE_API_ENABLE_ES:
return

app_label = sender._meta.app_label
Expand Down
Loading

0 comments on commit a34f452

Please sign in to comment.