Skip to content

Commit

Permalink
Merge pull request #2139 from uktrade/uat
Browse files Browse the repository at this point in the history
prod release
  • Loading branch information
saruniitr authored Aug 15, 2024
2 parents beedd06 + 79d7c41 commit 741a131
Show file tree
Hide file tree
Showing 27 changed files with 313 additions and 76 deletions.
2 changes: 1 addition & 1 deletion Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ celery = "~=5.3.0"
redis = "~=4.4.4"
django-test-migrations = "~=1.2.0"
django-silk = "~=5.0.3"
django = "~=4.2.14"
django = "~=4.2.15"
django-queryable-properties = "~=1.9.1"
database-sanitizer = ">=1.1.0"
django-reversion = ">=5.0.12"
Expand Down
8 changes: 4 additions & 4 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.13 on 2024-08-12 10:46

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("applications", "0083_amend_existing_goodonapplication_unit_legacy_codes"),
]

operations = [
migrations.AddField(
model_name="standardapplication",
name="subject_to_itar_controls",
field=models.BooleanField(blank=True, default=None, null=True),
),
]
1 change: 1 addition & 0 deletions api/applications/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ class StandardApplication(BaseApplication, Clonable):
f1686_reference_number = models.CharField(default=None, blank=True, null=True, max_length=100)
f1686_approval_date = models.DateField(blank=False, null=True)
other_security_approval_details = models.TextField(default=None, blank=True, null=True)
subject_to_itar_controls = models.BooleanField(blank=True, default=None, null=True)

clone_exclusions = [
"appeal",
Expand Down
4 changes: 4 additions & 0 deletions api/applications/serializers/generic_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class GenericApplicationListSerializer(serializers.Serializer):
case_type = TinyCaseTypeSerializer()
status = CaseStatusField()
submitted_at = serializers.DateTimeField()
submitted_by = serializers.SerializerMethodField()
updated_at = serializers.DateTimeField()
reference_code = serializers.CharField()
export_type = serializers.SerializerMethodField()
Expand All @@ -56,6 +57,9 @@ def get_export_type(self, instance):
"value": get_value_from_enum(instance.export_type, ApplicationExportType),
}

def get_submitted_by(self, instance):
return f"{instance.submitted_by.full_name}" if instance.submitted_by else ""


class GenericApplicationViewSerializer(serializers.ModelSerializer):
name = CharField(
Expand Down
2 changes: 2 additions & 0 deletions api/applications/serializers/standard_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ class Meta:
"appeal_deadline",
"appeal",
"sub_status",
"subject_to_itar_controls",
)
)

Expand Down Expand Up @@ -241,6 +242,7 @@ class Meta:
"f1686_reference_number",
"f1686_approval_date",
"other_security_approval_details",
"subject_to_itar_controls",
)

def __init__(self, *args, **kwargs):
Expand Down
2 changes: 2 additions & 0 deletions api/applications/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ def test_clone(self):
proposed_return_date=timezone.now(),
reference_number_on_information_form="some ref",
security_approvals=["some approvals"],
subject_to_itar_controls=False,
sla_days=10,
sla_remaining_days=20,
sla_updated_at=timezone.now(),
Expand Down Expand Up @@ -227,6 +228,7 @@ def test_clone(self):
"queues": [],
"reference_number_on_information_form": "some ref",
"security_approvals": ["some approvals"],
"subject_to_itar_controls": False,
"sla_days": 0,
"sla_remaining_days": None,
"sla_updated_at": None,
Expand Down
65 changes: 65 additions & 0 deletions api/applications/tests/test_security_approvals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from rest_framework import status
from rest_framework.reverse import reverse

from api.applications.enums import SecurityClassifiedApprovalsType
from api.applications.tests.factories import DraftStandardApplicationFactory
from api.parties.enums import PartyType
from api.parties.tests.factories import PartyDocumentFactory
from test_helpers.clients import DataTestClient


class ApplicationsSecurityApprovalsTests(DataTestClient):

def setUp(self):
super().setUp()
self.draft = DraftStandardApplicationFactory(organisation=self.organisation)
end_user = self.draft.parties.filter(party__type=PartyType.END_USER).first()
PartyDocumentFactory(party=end_user.party, s3_key="some secret key", safe=True)

self.url = reverse("applications:application_submit", kwargs={"pk": self.draft.id})

def test_application_submit_with_security_approvals_success(self):
self.draft.is_mod_security_approved = True
self.draft.security_approvals = [SecurityClassifiedApprovalsType.F680]
self.draft.subject_to_itar_controls = False
self.draft.save()

response = self.client.put(self.url, **self.exporter_headers)
self.assertEqual(response.status_code, status.HTTP_200_OK)

response = response.json()["application"]
self.assertEqual(response["is_mod_security_approved"], True)
self.assertEqual(response["security_approvals"], [SecurityClassifiedApprovalsType.F680])
self.assertEqual(response["subject_to_itar_controls"], False)

def test_application_submit_fail_without_security_approvals(self):
self.draft.is_mod_security_approved = None
self.draft.save()

response = self.client.put(self.url, **self.exporter_headers)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

response = response.json()["errors"]
self.assertEqual(
response["security_approvals"],
["To submit the application, complete the 'Do you have a security approval?' section"],
)

def test_edit_itar_controls_status(self):
self.draft.is_mod_security_approved = True
self.draft.security_approvals = [SecurityClassifiedApprovalsType.OTHER]
self.draft.subject_to_itar_controls = False
self.draft.save()

url = reverse("applications:application", kwargs={"pk": self.draft.id})
data = {
"security_approvals": [SecurityClassifiedApprovalsType.F680],
"subject_to_itar_controls": True,
}
response = self.client.put(url, data=data, **self.exporter_headers)
self.assertEqual(response.status_code, status.HTTP_200_OK)

self.draft.refresh_from_db()
self.assertEqual(self.draft.is_mod_security_approved, True)
self.assertEqual(self.draft.security_approvals, [SecurityClassifiedApprovalsType.F680])
self.assertEqual(self.draft.subject_to_itar_controls, True)
2 changes: 2 additions & 0 deletions api/applications/tests/test_view_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,8 @@ def test_view_submitted_applications(self):
response = self.client.get(url, **self.exporter_headers)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.json()["count"], 1)
application_data = response.json()["results"][0]
self.assertEqual(application_data["submitted_by"], self.exporter_user.full_name)

def test_organisation_has_existing_applications(self):
url = reverse("applications:existing")
Expand Down
4 changes: 3 additions & 1 deletion api/audit_trail/formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,9 +228,11 @@ def product_reviewed(**payload):
def licence_status_updated(**payload):
status = payload["status"].lower()
licence = payload["licence"]
previous_status = payload.get("previous_status")
if status == LicenceStatus.EXHAUSTED:
return f"The products for licence {licence} were exported and the status set to '{status}'."

if previous_status:
return f"set the licence status of {licence} from {previous_status} to {status}."
return f"{status} licence {licence}."


Expand Down
23 changes: 1 addition & 22 deletions api/audit_trail/signals.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import logging

from django.db.models.signals import post_save, pre_save
from django.db.models.signals import post_save
from django.dispatch import receiver

from api.audit_trail import service as audit_trail_service
from api.audit_trail.enums import AuditType
from api.audit_trail.models import Audit
from api.audit_trail.serializers import AuditSerializer
from api.licences.models import Licence


logger = logging.getLogger(__name__)
Expand All @@ -19,21 +16,3 @@ def emit_audit_log(sender, instance, **kwargs):
text = str(instance)
extra = AuditSerializer(instance).data
logger.info(text, extra={"audit": extra})


@receiver(pre_save, sender=Licence)
def audit_licence_status_change(sender, instance, **kwargs):
"""Audit a licence status change."""
try:
Licence.objects.get(id=instance.id, status=instance.status)
except Licence.DoesNotExist:
# The `pre_save` signal is called *before* the save() method is run and
# the `instance` is for the object that is about to be saved. In this case,
# if a `License` with the given parameters does not exist, it implies
# a change in status.
audit_trail_service.create_system_user_audit(
verb=AuditType.LICENCE_UPDATED_STATUS,
action_object=instance,
target=instance.case.get_case(),
payload={"licence": instance.reference_code, "status": instance.status},
)
4 changes: 4 additions & 0 deletions api/audit_trail/tests/test_formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,10 @@ def test_remove_party_audit_message(self, payload, expected_result):
({"status": "expired", "licence": "1"}, "expired licence 1."),
({"status": "draft", "licence": "1"}, "draft licence 1."),
({"status": "expired", "licence": "1"}, "expired licence 1."),
(
{"status": "suspended", "licence": "1", "previous_status": "issued"},
"set the licence status of 1 from issued to suspended.",
),
]
)
def test_licence_status_updated(self, payload, expected_result):
Expand Down
4 changes: 2 additions & 2 deletions api/core/tests/test_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,9 +266,9 @@ def a_view(request, *args, **kwargs):
[LicenceStatus.CANCELLED, CaseStatusEnum.FINALISED, "The licence status is not editable."],
]
)
def test_licence_is_editable_failure(self, license_status, case_status, error_msg):
def test_licence_is_editable_failure(self, licence_status, case_status, error_msg):
application = self.create_standard_application_case(self.organisation)
licence = StandardLicenceFactory(case=application, status=license_status)
licence = StandardLicenceFactory(case=application, status=licence_status)
application.status = get_case_status_by_status(case_status)
application.save()

Expand Down
4 changes: 2 additions & 2 deletions api/letter_templates/templates/letter_templates/siel.html
Original file line number Diff line number Diff line change
Expand Up @@ -263,8 +263,8 @@ <h1>Standard Individual Export Licence</h1>
</tr>
<tr>
<td class="border-black" colspan="6">
<span class="cell__heading">2b. Applicant reference</span>
<span class="cell__uppercase">{{ details.user_reference }}</span>
<div class="cell__heading">2b. Applicant reference</div>
<div class="cell__uppercase max-width">{{ details.user_reference }}</div>
</td>
</tr>
<tr>
Expand Down
69 changes: 49 additions & 20 deletions api/licences/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import uuid
from datetime import datetime
from api.audit_trail.enums import AuditType
import reversion

from django.db import models
Expand All @@ -14,6 +15,7 @@
from api.licences.managers import LicenceManager
from api.staticdata.decisions.models import Decision
from api.cases.notify import notify_exporter_licence_suspended, notify_exporter_licence_revoked
from api.audit_trail import service as audit_trail_service


class HMRCIntegrationUsageData(TimestampableModel):
Expand Down Expand Up @@ -50,6 +52,14 @@ class Licence(TimestampableModel):
def __str__(self):
return self.reference_code

def _set_status(self, status, user=None, send_status_change_to_hmrc=True):
original_status = self.status
self.status = status
self.save(send_status_change_to_hmrc=send_status_change_to_hmrc)
status_changed = original_status != self.status
if status_changed:
self.create_licence_change_audit_log(original_status=original_status, user=user)

def hmrc_mail_status(self):
"""
Fetch mail status from HRMC-integration server
Expand All @@ -60,31 +70,46 @@ def hmrc_mail_status(self):
raise ImproperlyConfigured("Did you forget to switch on LITE_HMRC_INTEGRATION_ENABLED?")
return get_mail_status(self)

def surrender(self, send_status_change_to_hmrc=True):
self.status = LicenceStatus.SURRENDERED
self.save(send_status_change_to_hmrc=send_status_change_to_hmrc)

def suspend(self):
self.status = LicenceStatus.SUSPENDED
self.save()
def create_licence_change_audit_log(self, original_status=None, user=None):
"""
Generates a system audit log for licence changes
"""
audit_kwargs = {
"verb": AuditType.LICENCE_UPDATED_STATUS,
"action_object": self,
"target": self.case.get_case(),
"payload": {"licence": self.reference_code, "status": self.status, "previous_status": original_status},
}
if user:
audit_trail_service.create(
**audit_kwargs,
actor=user,
)
else:
audit_trail_service.create_system_user_audit(**audit_kwargs)

def surrender(self, user=None):
self._set_status(status=LicenceStatus.SURRENDERED, user=user, send_status_change_to_hmrc=True)

def suspend(self, user=None):
self._set_status(LicenceStatus.SUSPENDED, user, send_status_change_to_hmrc=False)
notify_exporter_licence_suspended(self)

def revoke(self, send_status_change_to_hmrc=True):
self.status = LicenceStatus.REVOKED
self.save(send_status_change_to_hmrc=send_status_change_to_hmrc)
def revoke(self, user=None):
self._set_status(LicenceStatus.REVOKED, user=user, send_status_change_to_hmrc=True)
notify_exporter_licence_revoked(self)

def cancel(self, send_status_change_to_hmrc=True):
self.status = LicenceStatus.CANCELLED
self.save(send_status_change_to_hmrc=send_status_change_to_hmrc)
def cancel(self, user=None, send_status_change_to_hmrc=True):
self._set_status(
status=LicenceStatus.CANCELLED, user=user, send_status_change_to_hmrc=send_status_change_to_hmrc
)

def reinstate(self):
# This supersedes the issue method as it's called explicity on the license
def reinstate(self, user=None):
# This supersedes the issue method as it's called explicity on the licence
# Hence the user explicty knows which license is being reinstated
self.status = LicenceStatus.REINSTATED
self.save()
self._set_status(status=LicenceStatus.REINSTATED, user=user, send_status_change_to_hmrc=True)

def issue(self, send_status_change_to_hmrc=True):
def issue(self, user=None, send_status_change_to_hmrc=True):
# re-issue the licence if an older version exists
status = LicenceStatus.ISSUED
old_licence = (
Expand All @@ -94,8 +119,7 @@ def issue(self, send_status_change_to_hmrc=True):
old_licence.cancel(send_status_change_to_hmrc=False)
status = LicenceStatus.REINSTATED

self.status = status
self.save(send_status_change_to_hmrc=send_status_change_to_hmrc)
self._set_status(status=status, user=user, send_status_change_to_hmrc=send_status_change_to_hmrc)

def save(self, *args, **kwargs):
end_datetime = datetime.strptime(
Expand All @@ -104,6 +128,11 @@ def save(self, *args, **kwargs):
self.end_date = end_datetime.date()

send_status_change_to_hmrc = kwargs.pop("send_status_change_to_hmrc", False)

# We only require a system log if this is a new save since this isn't user initiated

if not Licence.objects.filter(id=self.id).exists():
self.create_licence_change_audit_log()
super(Licence, self).save(*args, **kwargs)

# Immediately notify HMRC if needed
Expand Down
Loading

0 comments on commit 741a131

Please sign in to comment.