diff --git a/api/audit_trail/enums.py b/api/audit_trail/enums.py index e062e07e7..c8472cf82 100644 --- a/api/audit_trail/enums.py +++ b/api/audit_trail/enums.py @@ -117,6 +117,7 @@ class AuditType(LiteEnum): OGEL_REISSUED = autostr() LICENCE_UPDATED_STATUS = autostr() DOCUMENT_ON_ORGANISATION_CREATE = autostr() + REPORT_SUMMARY_UPDATED = autostr() def human_readable(self): """ diff --git a/api/audit_trail/migrations/0002_auto_20211007_1352.py b/api/audit_trail/migrations/0002_auto_20211007_1352.py new file mode 100644 index 000000000..960df6358 --- /dev/null +++ b/api/audit_trail/migrations/0002_auto_20211007_1352.py @@ -0,0 +1,244 @@ +# Generated by Django 3.1.8 on 2021-10-07 12:52 + +import api.audit_trail.enums +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("audit_trail", "0001_squashed_0028_merge_20210224_1641"), + ] + + operations = [ + migrations.AlterField( + model_name="audit", + name="verb", + field=models.CharField( + choices=[ + (api.audit_trail.enums.AuditType["CREATED"], "created"), + (api.audit_trail.enums.AuditType["OGL_CREATED"], "ogl_created"), + (api.audit_trail.enums.AuditType["OGL_FIELD_EDITED"], "ogl_field_edited"), + (api.audit_trail.enums.AuditType["OGL_MULTI_FIELD_EDITED"], "ogl_multi_field_edited"), + (api.audit_trail.enums.AuditType["ADD_FLAGS"], "add_flags"), + (api.audit_trail.enums.AuditType["REMOVE_FLAGS"], "remove_flags"), + (api.audit_trail.enums.AuditType["GOOD_REVIEWED"], "good_reviewed"), + (api.audit_trail.enums.AuditType["GOOD_ADD_FLAGS"], "good_add_flags"), + (api.audit_trail.enums.AuditType["GOOD_REMOVE_FLAGS"], "good_remove_flags"), + (api.audit_trail.enums.AuditType["GOOD_ADD_REMOVE_FLAGS"], "good_add_remove_flags"), + (api.audit_trail.enums.AuditType["DESTINATION_ADD_FLAGS"], "destination_add_flags"), + (api.audit_trail.enums.AuditType["DESTINATION_REMOVE_FLAGS"], "destination_remove_flags"), + (api.audit_trail.enums.AuditType["ADD_GOOD_TO_APPLICATION"], "add_good_to_application"), + (api.audit_trail.enums.AuditType["REMOVE_GOOD_FROM_APPLICATION"], "remove_good_from_application"), + (api.audit_trail.enums.AuditType["ADD_GOOD_TYPE_TO_APPLICATION"], "add_good_type_to_application"), + ( + api.audit_trail.enums.AuditType["REMOVE_GOOD_TYPE_FROM_APPLICATION"], + "remove_good_type_from_application", + ), + ( + api.audit_trail.enums.AuditType["UPDATE_APPLICATION_END_USE_DETAIL"], + "update_application_end_use_detail", + ), + ( + api.audit_trail.enums.AuditType["UPDATE_APPLICATION_TEMPORARY_EXPORT"], + "update_application_temporary_export", + ), + ( + api.audit_trail.enums.AuditType["REMOVED_SITES_FROM_APPLICATION"], + "removed_sites_from_application", + ), + (api.audit_trail.enums.AuditType["ADD_SITES_TO_APPLICATION"], "add_sites_to_application"), + ( + api.audit_trail.enums.AuditType["REMOVED_EXTERNAL_LOCATIONS_FROM_APPLICATION"], + "removed_external_locations_from_application", + ), + ( + api.audit_trail.enums.AuditType["ADD_EXTERNAL_LOCATIONS_TO_APPLICATION"], + "add_external_locations_to_application", + ), + ( + api.audit_trail.enums.AuditType["REMOVED_COUNTRIES_FROM_APPLICATION"], + "removed_countries_from_application", + ), + (api.audit_trail.enums.AuditType["ADD_COUNTRIES_TO_APPLICATION"], "add_countries_to_application"), + ( + api.audit_trail.enums.AuditType["ADD_ADDITIONAL_CONTACT_TO_CASE"], + "add_additional_contact_to_case", + ), + (api.audit_trail.enums.AuditType["MOVE_CASE"], "move_case"), + (api.audit_trail.enums.AuditType["ASSIGN_CASE"], "assign_case"), + (api.audit_trail.enums.AuditType["ASSIGN_USER_TO_CASE"], "assign_user_to_case"), + (api.audit_trail.enums.AuditType["REMOVE_CASE"], "remove_case"), + (api.audit_trail.enums.AuditType["REMOVE_CASE_FROM_ALL_QUEUES"], "remove_case_from_all_queues"), + ( + api.audit_trail.enums.AuditType["REMOVE_CASE_FROM_ALL_USER_ASSIGNMENTS"], + "remove_case_from_all_user_assignments", + ), + (api.audit_trail.enums.AuditType["CLC_RESPONSE"], "clc_response"), + (api.audit_trail.enums.AuditType["PV_GRADING_RESPONSE"], "pv_grading_response"), + (api.audit_trail.enums.AuditType["CREATED_CASE_NOTE"], "created_case_note"), + (api.audit_trail.enums.AuditType["ECJU_QUERY"], "ecju_query"), + (api.audit_trail.enums.AuditType["UPDATED_STATUS"], "updated_status"), + (api.audit_trail.enums.AuditType["UPDATED_APPLICATION_NAME"], "updated_application_name"), + ( + api.audit_trail.enums.AuditType["UPDATE_APPLICATION_LETTER_REFERENCE"], + "update_application_letter_reference", + ), + ( + api.audit_trail.enums.AuditType["UPDATE_APPLICATION_F680_CLEARANCE_TYPES"], + "update_application_f680_clearance_types", + ), + ( + api.audit_trail.enums.AuditType["ADDED_APPLICATION_LETTER_REFERENCE"], + "added_application_letter_reference", + ), + ( + api.audit_trail.enums.AuditType["REMOVED_APPLICATION_LETTER_REFERENCE"], + "removed_application_letter_reference", + ), + (api.audit_trail.enums.AuditType["ASSIGNED_COUNTRIES_TO_GOOD"], "assigned_countries_to_good"), + (api.audit_trail.enums.AuditType["REMOVED_COUNTRIES_FROM_GOOD"], "removed_countries_from_good"), + (api.audit_trail.enums.AuditType["CREATED_FINAL_ADVICE"], "created_final_advice"), + (api.audit_trail.enums.AuditType["CLEARED_FINAL_ADVICE"], "cleared_final_advice"), + (api.audit_trail.enums.AuditType["CREATED_TEAM_ADVICE"], "created_team_advice"), + (api.audit_trail.enums.AuditType["CLEARED_TEAM_ADVICE"], "cleared_team_advice"), + (api.audit_trail.enums.AuditType["CREATED_USER_ADVICE"], "created_user_advice"), + (api.audit_trail.enums.AuditType["ADD_PARTY"], "add_party"), + (api.audit_trail.enums.AuditType["REMOVE_PARTY"], "remove_party"), + (api.audit_trail.enums.AuditType["UPLOAD_PARTY_DOCUMENT"], "upload_party_document"), + (api.audit_trail.enums.AuditType["DELETE_PARTY_DOCUMENT"], "delete_party_document"), + (api.audit_trail.enums.AuditType["UPLOAD_APPLICATION_DOCUMENT"], "upload_application_document"), + (api.audit_trail.enums.AuditType["DELETE_APPLICATION_DOCUMENT"], "delete_application_document"), + (api.audit_trail.enums.AuditType["UPLOAD_CASE_DOCUMENT"], "upload_case_document"), + (api.audit_trail.enums.AuditType["GENERATE_CASE_DOCUMENT"], "generate_case_document"), + (api.audit_trail.enums.AuditType["ADD_CASE_OFFICER_TO_CASE"], "add_case_officer_to_case"), + (api.audit_trail.enums.AuditType["REMOVE_CASE_OFFICER_FROM_CASE"], "remove_case_officer_from_case"), + (api.audit_trail.enums.AuditType["GRANTED_APPLICATION"], "granted_application"), + (api.audit_trail.enums.AuditType["REINSTATED_APPLICATION"], "reinstated_application"), + (api.audit_trail.enums.AuditType["FINALISED_APPLICATION"], "finalised_application"), + (api.audit_trail.enums.AuditType["UNASSIGNED_QUEUES"], "unassigned_queues"), + (api.audit_trail.enums.AuditType["UNASSIGNED"], "unassigned"), + (api.audit_trail.enums.AuditType["CREATED_DOCUMENT_TEMPLATE"], "created_document_template"), + (api.audit_trail.enums.AuditType["UPDATED_LETTER_TEMPLATE_NAME"], "updated_letter_template_name"), + ( + api.audit_trail.enums.AuditType["ADDED_LETTER_TEMPLATE_CASE_TYPES"], + "added_letter_template_case_types", + ), + ( + api.audit_trail.enums.AuditType["UPDATED_LETTER_TEMPLATE_CASE_TYPES"], + "updated_letter_template_case_types", + ), + ( + api.audit_trail.enums.AuditType["REMOVED_LETTER_TEMPLATE_CASE_TYPES"], + "removed_letter_template_case_types", + ), + ( + api.audit_trail.enums.AuditType["ADDED_LETTER_TEMPLATE_DECISIONS"], + "added_letter_template_decisions", + ), + ( + api.audit_trail.enums.AuditType["UPDATED_LETTER_TEMPLATE_DECISIONS"], + "updated_letter_template_decisions", + ), + ( + api.audit_trail.enums.AuditType["REMOVED_LETTER_TEMPLATE_DECISIONS"], + "removed_letter_template_decisions", + ), + ( + api.audit_trail.enums.AuditType["UPDATED_LETTER_TEMPLATE_PARAGRAPHS"], + "updated_letter_template_paragraphs", + ), + ( + api.audit_trail.enums.AuditType["REMOVED_LETTER_TEMPLATE_PARAGRAPHS"], + "removed_letter_template_paragraphs", + ), + ( + api.audit_trail.enums.AuditType["ADDED_LETTER_TEMPLATE_PARAGRAPHS"], + "added_letter_template_paragraphs", + ), + ( + api.audit_trail.enums.AuditType["UPDATED_LETTER_TEMPLATE_LAYOUT"], + "updated_letter_template_layout", + ), + ( + api.audit_trail.enums.AuditType["UPDATED_LETTER_TEMPLATE_PARAGRAPHS_ORDERING"], + "updated_letter_template_paragraphs_ordering", + ), + ( + api.audit_trail.enums.AuditType["UPDATED_LETTER_TEMPLATE_INCLUDE_DIGITAL_SIGNATURE"], + "updated_letter_template_include_digital_signature", + ), + (api.audit_trail.enums.AuditType["CREATED_PICKLIST"], "created_picklist"), + (api.audit_trail.enums.AuditType["UPDATED_PICKLIST_TEXT"], "updated_picklist_text"), + (api.audit_trail.enums.AuditType["UPDATED_PICKLIST_NAME"], "updated_picklist_name"), + (api.audit_trail.enums.AuditType["DEACTIVATE_PICKLIST"], "deactivate_picklist"), + (api.audit_trail.enums.AuditType["REACTIVATE_PICKLIST"], "reactivate_picklist"), + ( + api.audit_trail.enums.AuditType["UPDATED_EXHIBITION_DETAILS_TITLE"], + "updated_exhibition_details_title", + ), + ( + api.audit_trail.enums.AuditType["UPDATED_EXHIBITION_DETAILS_START_DATE"], + "updated_exhibition_details_start_date", + ), + ( + api.audit_trail.enums.AuditType["UPDATED_EXHIBITION_DETAILS_REQUIRED_BY_DATE"], + "updated_exhibition_details_required_by_date", + ), + ( + api.audit_trail.enums.AuditType["UPDATED_EXHIBITION_DETAILS_REASON_FOR_CLEARANCE"], + "updated_exhibition_details_reason_for_clearance", + ), + (api.audit_trail.enums.AuditType["UPDATED_ROUTE_OF_GOODS"], "updated_route_of_goods"), + (api.audit_trail.enums.AuditType["UPDATED_ORGANISATION"], "updated_organisation"), + (api.audit_trail.enums.AuditType["CREATED_ORGANISATION"], "created_organisation"), + (api.audit_trail.enums.AuditType["REGISTER_ORGANISATION"], "register_organisation"), + (api.audit_trail.enums.AuditType["REJECTED_ORGANISATION"], "rejected_organisation"), + (api.audit_trail.enums.AuditType["APPROVED_ORGANISATION"], "approved_organisation"), + (api.audit_trail.enums.AuditType["REMOVED_FLAG_ON_ORGANISATION"], "removed_flag_on_organisation"), + (api.audit_trail.enums.AuditType["ADDED_FLAG_ON_ORGANISATION"], "added_flag_on_organisation"), + (api.audit_trail.enums.AuditType["RERUN_ROUTING_RULES"], "rerun_routing_rules"), + (api.audit_trail.enums.AuditType["ENFORCEMENT_CHECK"], "enforcement_check"), + (api.audit_trail.enums.AuditType["UPDATED_SITE"], "updated_site"), + (api.audit_trail.enums.AuditType["CREATED_SITE"], "created_site"), + (api.audit_trail.enums.AuditType["UPDATED_SITE_NAME"], "updated_site_name"), + (api.audit_trail.enums.AuditType["COMPLIANCE_SITE_CASE_CREATE"], "compliance_site_case_create"), + ( + api.audit_trail.enums.AuditType["COMPLIANCE_SITE_CASE_NEW_LICENCE"], + "compliance_site_case_new_licence", + ), + (api.audit_trail.enums.AuditType["ADDED_NEXT_REVIEW_DATE"], "added_next_review_date"), + (api.audit_trail.enums.AuditType["EDITED_NEXT_REVIEW_DATE"], "edited_next_review_date"), + (api.audit_trail.enums.AuditType["REMOVED_NEXT_REVIEW_DATE"], "removed_next_review_date"), + (api.audit_trail.enums.AuditType["COMPLIANCE_VISIT_CASE_CREATED"], "compliance_visit_case_created"), + (api.audit_trail.enums.AuditType["COMPLIANCE_VISIT_CASE_UPDATED"], "compliance_visit_case_updated"), + ( + api.audit_trail.enums.AuditType["COMPLIANCE_PEOPLE_PRESENT_CREATED"], + "compliance_people_present_created", + ), + ( + api.audit_trail.enums.AuditType["COMPLIANCE_PEOPLE_PRESENT_UPDATED"], + "compliance_people_present_updated", + ), + ( + api.audit_trail.enums.AuditType["COMPLIANCE_PEOPLE_PRESENT_DELETED"], + "compliance_people_present_deleted", + ), + ( + api.audit_trail.enums.AuditType["UPDATED_GOOD_ON_DESTINATION_MATRIX"], + "updated_good_on_destination_matrix", + ), + (api.audit_trail.enums.AuditType["LICENCE_UPDATED_GOOD_USAGE"], "licence_updated_good_usage"), + (api.audit_trail.enums.AuditType["OGEL_REISSUED"], "ogel_reissued"), + (api.audit_trail.enums.AuditType["LICENCE_UPDATED_STATUS"], "licence_updated_status"), + ( + api.audit_trail.enums.AuditType["DOCUMENT_ON_ORGANISATION_CREATE"], + "document_on_organisation_create", + ), + (api.audit_trail.enums.AuditType["REPORT_SUMMARY_UPDATED"], "report_summary_updated"), + ], + db_index=True, + max_length=255, + ), + ), + ] diff --git a/api/audit_trail/payload.py b/api/audit_trail/payload.py index fcd3caf36..8555430e2 100644 --- a/api/audit_trail/payload.py +++ b/api/audit_trail/payload.py @@ -125,4 +125,5 @@ def format_payload(audit_type, payload): AuditType.OGEL_REISSUED: strings.Audit.OGEL_REISSUED, AuditType.LICENCE_UPDATED_STATUS: strings.Audit.LICENCE_UPDATED_STATUS, AuditType.DOCUMENT_ON_ORGANISATION_CREATE: "added {document_type} '{file_name}' to organization", + AuditType.REPORT_SUMMARY_UPDATED: "updated ARS for {good_name} from {old_report_summary} to {report_summary}", } diff --git a/api/goods/tests/test_control_codes.py b/api/goods/tests/test_control_codes.py index 167c3ac20..31f484baf 100644 --- a/api/goods/tests/test_control_codes.py +++ b/api/goods/tests/test_control_codes.py @@ -2,6 +2,8 @@ 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.applications.models import GoodOnApplication from api.core import constants from api.flags.enums import FlagLevels @@ -56,6 +58,7 @@ def test_verify_multiple_goods(self): data = { "objects": [self.good_1.pk, self.good_2.pk], + "current_object": self.good_on_application_1.pk, "comment": "I Am Easy to Find", "report_summary": self.report_summary.text, "control_list_entries": ["ML1a"], @@ -78,6 +81,7 @@ def test_report_summary_saved_goodonapplication(self): data = { "objects": [self.good_1.pk], + "current_object": self.good_on_application_1.pk, "control_list_entries": ["ML1a"], "is_precedent": False, "is_good_controlled": True, @@ -98,6 +102,7 @@ def test_verify_multiple_goods_NLR(self): """ data = { "objects": [self.good_1.pk, self.good_2.pk], + "current_object": self.good_on_application_1.pk, "comment": "I Am Easy to Find", "report_summary": self.report_summary.text, "control_list_entries": ["ML1a"], @@ -116,6 +121,7 @@ def test_invalid_good_pk(self): # given one of the good pk is invalid data = { "objects": [self.team.pk, self.good_1.pk], + "current_object": self.good_on_application_1.pk, "comment": "I Am Easy to Find", "report_summary": self.report_summary.text, "is_good_controlled": False, @@ -162,6 +168,7 @@ def test_invalid_good_pk(self): def test_is_precedent_is_set(self, input, expected_is_precedent): defaults = { "objects": [self.good_1.pk], + "current_object": self.good_on_application_1.pk, "report_summary": self.report_summary.text, } data = {**defaults, **input} @@ -180,6 +187,7 @@ def test_standard_invalid_control_list_entries(self): """ data = { "objects": [self.good_1.pk, self.good_2.pk], + "current_object": self.good_on_application_1.pk, "comment": "I Am Easy to Find", "report_summary": self.report_summary.text, "is_good_controlled": True, @@ -199,6 +207,7 @@ def test_standard_controlled_good_empty_control_list_entries(self): """ data = { "objects": [self.good_1.pk, self.good_2.pk], + "current_object": self.good_on_application_1.pk, "comment": "I Am Easy to Find", "report_summary": self.report_summary.text, "is_good_controlled": True, @@ -237,6 +246,7 @@ def test_cannot_set_control_list_entries_when_application_in_terminal_state(self data = { "objects": self.good_1.pk, + "current_object": self.good_on_application_1.pk, "comment": "I Am Easy to Find", "report_summary": self.report_summary.text, "control_list_entries": "ML1a", @@ -246,6 +256,46 @@ def test_cannot_set_control_list_entries_when_application_in_terminal_state(self response = self.client.post(self.url, data, **self.gov_headers) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + def test_report_summary_updates_same_product_added_twice(self): + self.product_on_application1 = GoodOnApplication.objects.create( + good=self.good_1, + application=self.application, + quantity=10, + unit=Units.NAR, + value=500, + report_summary="Rifles (10)", + ) + self.product_on_application2 = GoodOnApplication.objects.create( + good=self.good_1, + application=self.application, + quantity=5, + unit=Units.NAR, + value=500, + report_summary="Rifles (5)", + ) + data = { + "objects": [self.good_1.pk], + "current_object": self.product_on_application1.pk, + "control_list_entries": ["ML1a"], + "is_precedent": False, + "is_good_controlled": True, + "end_use_control": [], + "report_summary": "Sniper rifles (10)", + "comment": "report summary update test", + } + + response = self.client.post(self.url, data, **self.gov_headers) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.product_on_application1.refresh_from_db() + + self.assertEqual(self.product_on_application1.report_summary, "Sniper rifles (10)") + self.assertEqual(self.product_on_application2.report_summary, "Rifles (5)") + audit_qs = Audit.objects.filter(verb=AuditType.REPORT_SUMMARY_UPDATED) + self.assertEqual(audit_qs.count(), 1) + audit_payload = audit_qs.first().payload + self.assertEqual(audit_payload["old_report_summary"], "Rifles (10)") + self.assertEqual(audit_payload["report_summary"], "Sniper rifles (10)") + class GoodsVerifiedTestsOpenApplication(DataTestClient): def setUp(self): @@ -276,6 +326,7 @@ def test_verify_single_good(self): """ data = { "objects": self.good_1.pk, + "current_object": self.good_1.pk, "comment": "I Am Easy to Find", "report_summary": self.report_summary.text, "is_good_controlled": True, @@ -300,6 +351,7 @@ def test_verify_only_change_comment_doesnt_remove_flags(self): self.good_1.save() data = { "objects": self.good_1.pk, + "current_object": self.good_1.pk, "comment": "I Am Easy to Find", "report_summary": self.report_summary.text, "control_list_entries": ["ML1a"], @@ -322,6 +374,7 @@ def test_invalid_control_list_entries(self): data = { "objects": [self.good_1.pk, self.good_2.pk], + "current_object": self.good_1.pk, "comment": "I Am Easy to Find", "report_summary": self.report_summary.text, "control_list_entries": ["invalid"], diff --git a/api/goods/views.py b/api/goods/views.py index 48459251a..78cc22ed3 100644 --- a/api/goods/views.py +++ b/api/goods/views.py @@ -92,11 +92,40 @@ def post(self, request, case_pk): case = get_case(case_pk) for good in self.get_queryset(): - serializer = self.get_serializer(good) + data = request.data.copy() + serializer_class = self.get_serializer_class() + if data["report_summary"] != good.report_summary and str(good.id) != data.get( + "current_object", str(good.id) + ): + data["report_summary"] = good.report_summary + + serializer = serializer_class(good, data=data) serializer.is_valid(raise_exception=True) old_control_list_entries = list(good.control_list_entries.values_list("rating", flat=True)) old_is_controlled = good.is_good_controlled - serializer.save() + old_report_summary = good.report_summary + + obj = serializer.save() + + if request.data["report_summary"] != old_report_summary: + if str(good.id) == request.data.get("current_object", str(good.id)): + if isinstance(good, GoodsType): + name = good.description + else: + name = good.name + + audit_trail_service.create( + actor=request.user, + verb=AuditType.REPORT_SUMMARY_UPDATED, + action_object=obj, + target=case, + payload={ + "good_name": name, + "old_report_summary": old_report_summary, + "report_summary": obj.report_summary, + }, + ) + if "control_list_entries" in serializer.data or "is_good_controlled" in serializer.data: new_control_list_entries = [item.rating for item in serializer.validated_data["control_list_entries"]] new_is_controlled = serializer.validated_data["is_good_controlled"] diff --git a/api/workflow/tests/test_flagging_rules.py b/api/workflow/tests/test_flagging_rules.py index c0688074b..418a41562 100644 --- a/api/workflow/tests/test_flagging_rules.py +++ b/api/workflow/tests/test_flagging_rules.py @@ -107,6 +107,7 @@ def test_goods_flag_before_and_after_reviewing_good(self): self.url = reverse_lazy("goods:control_list_entries", kwargs={"case_pk": case.id}) data = { "objects": [good.id], + "current_object": good.id, "comment": "Update rating to match with flag excluded value", "report_summary": "test report summary", "control_list_entries": ["ML8b"],