Skip to content

Commit

Permalink
Merge pull request #1824 from uktrade/dev
Browse files Browse the repository at this point in the history
UAT release
  • Loading branch information
kevincarrogan authored Feb 14, 2024
2 parents 926e8d1 + 438e721 commit 68e9236
Show file tree
Hide file tree
Showing 32 changed files with 933 additions and 53 deletions.
25 changes: 24 additions & 1 deletion api/cases/helpers.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from datetime import timedelta

from api.audit_trail.enums import AuditType
from api.common.dates import is_bank_holiday, is_weekend
from api.cases.enums import CaseTypeReferenceEnum
from api.staticdata.statuses.enums import CaseStatusEnum
from api.users.models import GovUser, GovNotification
from api.users.models import BaseUser, GovUser, GovNotification
from api.users.enums import SystemUser


def get_assigned_to_user_case_ids(user: GovUser, queue_id=None):
Expand Down Expand Up @@ -81,3 +83,24 @@ def can_set_status(case, status):
def working_days_in_range(start_date, end_date):
dates_in_range = [start_date + timedelta(n) for n in range((end_date - start_date).days)]
return len([date for date in dates_in_range if (not is_bank_holiday(date) and not is_weekend(date))])


def create_system_mention(case, case_note_text, mention_user):
"""
Create a LITE system mention e.g. exporter responded to an ECJU query
"""
# to avoid circular import ImportError these must be imported here
from api.cases.models import CaseNote, CaseNoteMentions
from api.audit_trail import service as audit_trail_service

case_note = CaseNote(text=case_note_text, case=case, user=BaseUser.objects.get(id=SystemUser.id))
case_note.save()
case_note_mentions = CaseNoteMentions(user=mention_user, case_note=case_note)
case_note_mentions.save()
audit_payload = {
"mention_users": [f"{mention_user.full_name} ({mention_user.team.name})"],
"additional_text": case_note_text,
}
audit_trail_service.create_system_user_audit(
verb=AuditType.CREATED_CASE_NOTE_WITH_MENTIONS, action_object=case_note, target=case, payload=audit_payload
)
1 change: 0 additions & 1 deletion api/cases/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
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 (
AdviceType,
Expand Down
65 changes: 57 additions & 8 deletions api/cases/tests/test_case_ecju_queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from api.audit_trail.serializers import AuditSerializer
from api.cases.enums import ECJUQueryType
from api.cases.models import EcjuQuery
from api.core.exceptions import NotFoundError
from api.compliance.tests.factories import ComplianceSiteCaseFactory
from api.licences.enums import LicenceStatus
from api.licences.tests.factories import StandardLicenceFactory
Expand All @@ -23,6 +24,7 @@
from api.staticdata.statuses.libraries.get_case_status import get_case_status_by_status
from test_helpers.clients import DataTestClient
from api.users.tests.factories import ExporterUserFactory
from api.cases.models import CaseNoteMentions

faker = Faker()

Expand Down Expand Up @@ -465,7 +467,7 @@ def _test_exporter_responds_to_query(self, add_documents, query_type):
query_response_url = reverse("cases:case_ecju_query", kwargs={"pk": case.id, "ecju_pk": ecju_query.id})
data = {"response": "Attached the requested documents"}
response = self.client.put(query_response_url, data, **self.exporter_headers)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.status_code, status.HTTP_200_OK)
response = response.json()["ecju_query"]
self.assertEqual(response["response"], data["response"])

Expand Down Expand Up @@ -498,7 +500,7 @@ def test_caseworker_manually_closes_query(self):
self.assertEqual(1, BaseNotification.objects.filter(object_id=ecju_query.id).count())

response = self.client.put(query_response_url, data, **self.gov_headers)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.status_code, status.HTTP_200_OK)
response = response.json()["ecju_query"]
self.assertEqual(response["response"], data["response"])

Expand All @@ -520,7 +522,7 @@ def test_close_query_has_optional_response_exporter(self):

data = {"response": ""}
response = self.client.put(query_response_url, data, **self.exporter_headers)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.status_code, status.HTTP_200_OK)

response_ecju_query = response.json()["ecju_query"]
self.assertIsNone(response_ecju_query["response"])
Expand Down Expand Up @@ -550,7 +552,7 @@ def test_caseworker_manually_closes_query_exporter_responds_raises_error(self):
data = {"response": "exporter provided details"}

response = self.client.put(query_response_url, data, **self.gov_headers)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.status_code, status.HTTP_200_OK)
response = response.json()["ecju_query"]
self.assertEqual(response["response"], data["response"])

Expand All @@ -567,7 +569,7 @@ def test_caseworker_manually_closes_query_already_closed_raises_error(self):
data = {"response": "exporter provided details"}

response = self.client.put(query_response_url, data, **self.exporter_headers)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.status_code, status.HTTP_200_OK)
response = response.json()["ecju_query"]
self.assertEqual(response["response"], data["response"])

Expand All @@ -594,7 +596,7 @@ def test_exporter_cannot_respond_to_same_ecju_query_twice(self):
url = reverse("cases:case_ecju_query", kwargs={"pk": case.id, "ecju_pk": ecju_query.id})
data = {"response": "Additional details included"}
response = self.client.put(url, data, **self.exporter_headers)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.status_code, status.HTTP_200_OK)
response = response.json()["ecju_query"]
self.assertEqual(response["response"], data["response"])

Expand Down Expand Up @@ -630,7 +632,7 @@ def test_exporter_cannot_add_documents_to_closed_query(self):
query_response_url = reverse("cases:case_ecju_query", kwargs={"pk": case.id, "ecju_pk": ecju_query.id})
data = {"response": "Attached the requested documents"}
response = self.client.put(query_response_url, data, **self.exporter_headers)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.status_code, status.HTTP_200_OK)
response = response.json()["ecju_query"]
self.assertEqual(response["response"], data["response"])
self.assertEqual(len(response["documents"]), 1)
Expand Down Expand Up @@ -659,7 +661,7 @@ def test_exporter_cannot_delete_documents_of_closed_query(self):
url = reverse("cases:case_ecju_query", kwargs={"pk": case.id, "ecju_pk": ecju_query.id})
data = {"response": "Additional details included"}
response = self.client.put(url, data, **self.exporter_headers)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.status_code, status.HTTP_200_OK)
response = response.json()["ecju_query"]
self.assertEqual(response["response"], data["response"])
self.assertEqual(len(response["documents"]), 1)
Expand All @@ -675,3 +677,50 @@ def test_exporter_cannot_delete_documents_of_closed_query(self):
self.assertEqual(response.status_code, status.HTTP_200_OK)
response = response.json()
self.assertIsNotNone(response["document"]["id"])

@parameterized.expand(["this is some response text", ""])
def test_exporter_responding_to_query_creates_case_note_mention_for_caseworker(self, response_text):
case = self.create_standard_application_case(self.organisation)

# caseworker raises a query
url = reverse("cases:case_ecju_queries", kwargs={"pk": case.id})
question_text = "this is the question text"
data = {"question": question_text, "query_type": ECJUQueryType.ECJU}

response = self.client.post(url, data, **self.gov_headers)
response_data = response.json()
ecju_query = EcjuQuery.objects.get(case=case)

self.assertFalse(ecju_query.is_query_closed)
self.assertEqual(status.HTTP_201_CREATED, response.status_code)
self.assertEqual(response_data["ecju_query_id"], str(ecju_query.id))
self.assertEqual(question_text, ecju_query.question)
self.assertIsNone(ecju_query.response)

# exporter responds to the query
url = reverse("cases:case_ecju_query", kwargs={"pk": case.id, "ecju_pk": ecju_query.id})
data = {"response": response_text}

response = self.client.put(url, data, **self.exporter_headers)
ecju_query = EcjuQuery.objects.get(case=case)

self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(ecju_query.is_query_closed)

# check case note mention is created
case_note_mentions = CaseNoteMentions.objects.first()
case_note = case_note_mentions.case_note
audit_object = Audit.objects.first()

expected_gov_user = ecju_query.raised_by_user
expected_exporter_user = ecju_query.responded_by_user
expected_mention_users_text = f"{expected_gov_user.full_name} ({expected_gov_user.team.name})"
expected_case_note_text = f"{expected_exporter_user.get_full_name()} has responded to a query."
expected_audit_payload = {
"mention_users": [expected_mention_users_text],
"additional_text": expected_case_note_text,
}

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)
55 changes: 30 additions & 25 deletions api/cases/views/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from rest_framework.views import APIView

from api.applications.models import GoodOnApplication
from api.users.models import BaseNotification
from api.users.models import BaseNotification, ExporterUser
from api.applications.serializers.advice import (
CountersignAdviceSerializer,
CountryWithFlagsSerializer,
Expand All @@ -27,6 +27,7 @@
)
from api.cases.generated_documents.models import GeneratedCaseDocument
from api.cases.generated_documents.serializers import AdviceDocumentGovSerializer
from api.cases.helpers import create_system_mention
from api.cases.libraries.advice import group_advice
from api.cases.libraries.finalise import get_required_decision_document_types
from api.cases.libraries.get_case import get_case, get_case_document
Expand Down Expand Up @@ -606,52 +607,56 @@ def get(self, request, pk, ecju_pk):

def put(self, request, pk, ecju_pk):
"""
If not validate only Will update the ecju query instance, with a response, and return the data details.
If validate only, this will return if the data is acceptable or not.
Update an ECJU query to be closed
"""

ecju_query = get_ecju_query(ecju_pk)
if ecju_query.response:
return JsonResponse(
data={"errors": f"Responding to closed {ecju_query.get_query_type_display()} is not allowed"},
status=status.HTTP_400_BAD_REQUEST,
)

is_govuser = hasattr(request.user, "govuser")
is_govuser_request = hasattr(request.user, "govuser")
is_blank_response = not bool(request.data.get("response"))

# response is required only when a govuser closes a query
if is_govuser and is_blank_response:
if is_govuser_request and is_blank_response:
return JsonResponse(
data={"errors": "Enter a reason why you are closing the query"}, status=status.HTTP_400_BAD_REQUEST
)

data = {"responded_by_user": str(request.user.pk)}

if request.data.get("response"):
data.update({"response": request.data["response"]})

serializer = EcjuQueryUserResponseSerializer(instance=ecju_query, data=data, partial=True)

if serializer.is_valid():
if "validate_only" not in request.data or not request.data["validate_only"]:
serializer.save()
# Delete any notifications against this query
ecju_query_type = ContentType.objects.get_for_model(EcjuQuery)
BaseNotification.objects.filter(object_id=ecju_pk, content_type=ecju_query_type).delete()
serializer.save()

# If the user is a Govuser query is manually being closed by a caseworker
query_verb = AuditType.ECJU_QUERY_MANUALLY_CLOSED if is_govuser else AuditType.ECJU_QUERY_RESPONSE
audit_trail_service.create(
actor=request.user,
verb=query_verb,
action_object=serializer.instance,
target=serializer.instance.case,
payload={"ecju_response": data.get("response")},
# Delete any notifications against this query
ecju_query_type = ContentType.objects.get_for_model(EcjuQuery)
BaseNotification.objects.filter(object_id=ecju_pk, content_type=ecju_query_type).delete()

# If the user is a govuser then query is manually being closed by a case worker
query_verb = AuditType.ECJU_QUERY_MANUALLY_CLOSED if is_govuser_request else AuditType.ECJU_QUERY_RESPONSE
audit_trail_service.create(
actor=request.user,
verb=query_verb,
action_object=serializer.instance,
target=serializer.instance.case,
payload={"ecju_response": data.get("response")},
)

# If an exporter responds to a query, create a mention notification
# for the case worker that lets them know the query has been responded to
if not is_govuser_request:
exporter_user_full_name = ExporterUser.objects.get(baseuser_ptr_id=request.user.pk).full_name
create_system_mention(
case=ecju_query.case,
case_note_text=f"{exporter_user_full_name} has responded to a query.",
mention_user=ecju_query.raised_by_user,
)
return JsonResponse(data={"ecju_query": serializer.data}, status=status.HTTP_201_CREATED)
else:
return JsonResponse(data={}, status=status.HTTP_200_OK)

return JsonResponse(data={"ecju_query": serializer.data}, status=status.HTTP_200_OK)

return JsonResponse(data={"errors": serializer.errors}, status=status.HTTP_400_BAD_REQUEST)

Expand Down
4 changes: 4 additions & 0 deletions api/conf/celery.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,8 @@
"task": "api.cases.celery_tasks.update_cases_sla",
"schedule": crontab(hour=22, minute=30),
},
"backup document data 2am": {
"task": "api.document_data.celery_tasks.backup_document_data",
"schedule": crontab(hour=2, minute=0),
},
}
5 changes: 4 additions & 1 deletion api/conf/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
"lite_routing",
"api.appeals",
"api.assessments",
"api.document_data",
]

MIDDLEWARE = [
Expand Down Expand Up @@ -456,7 +457,7 @@ def _build_redis_url(base_url, db_number, **query_args):
{
"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/government/uploads/system/uploads/attachment_data/file/1129559/UK_Sanctions_List.xml",
"uk_sanctions_file": "https://assets.publishing.service.gov.uk/media/65ca02639c5b7f0012951caf/UK_Sanctions_List.xml",
},
)
LITE_INTERNAL_NOTIFICATION_EMAILS = env.json("LITE_INTERNAL_NOTIFICATION_EMAILS", {})
Expand All @@ -483,3 +484,5 @@ 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)
4 changes: 4 additions & 0 deletions api/conf/settings_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@
SUPPRESS_TEST_OUTPUT = True

AWS_ENDPOINT_URL = None

INSTALLED_APPS += [
"api.core.tests.apps.CoreTestsConfig",
]
17 changes: 17 additions & 0 deletions api/core/filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from rest_framework import filters

from django.core.exceptions import ImproperlyConfigured


class ParentFilter(filters.BaseFilterBackend):
def filter_queryset(self, request, queryset, view):
parent_filter_id_lookup_field = getattr(view, "parent_filter_id_lookup_field", None)
if not parent_filter_id_lookup_field:
raise ImproperlyConfigured(
f"Cannot use {self.__class__.__name__} on a view which does not have a parent_filter_id_lookup_field attribute"
)

lookup = {
parent_filter_id_lookup_field: view.kwargs["pk"],
}
return queryset.filter(**lookup)
6 changes: 6 additions & 0 deletions api/core/tests/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class CoreTestsConfig(AppConfig):
name = "api.core.tests"
label = "api_core_tests"
32 changes: 32 additions & 0 deletions api/core/tests/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Generated by Django 4.2.9 on 2024-02-09 14:48

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

initial = True

dependencies = []

operations = [
migrations.CreateModel(
name="ParentModel",
fields=[
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("name", models.CharField(max_length=255)),
],
),
migrations.CreateModel(
name="ChildModel",
fields=[
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("name", models.CharField(max_length=255)),
(
"parent",
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="api_core_tests.parentmodel"),
),
],
),
]
Empty file.
10 changes: 10 additions & 0 deletions api/core/tests/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from django.db import models


class ParentModel(models.Model):
name = models.CharField(max_length=255)


class ChildModel(models.Model):
name = models.CharField(max_length=255)
parent = models.ForeignKey(ParentModel, on_delete=models.CASCADE)
Loading

0 comments on commit 68e9236

Please sign in to comment.