From 6b8a19a331cb63a2726f53beb671c459c5f8ac31 Mon Sep 17 00:00:00 2001 From: Brendan Smith Date: Fri, 24 Jan 2025 09:24:26 +0000 Subject: [PATCH 1/5] Make security approval section driven by exporter answer API --- .../applications/views/goods/common/mixins.py | 6 +++++ .../views/security_approvals/views.py | 18 ++++++++------ exporter/exporter_answers/__init__.py | 0 exporter/exporter_answers/services.py | 24 +++++++++++++++++++ .../security-approvals-summary.html | 24 +++++++++---------- 5 files changed, 53 insertions(+), 19 deletions(-) create mode 100644 exporter/exporter_answers/__init__.py create mode 100644 exporter/exporter_answers/services.py diff --git a/exporter/applications/views/goods/common/mixins.py b/exporter/applications/views/goods/common/mixins.py index 5fa7218618..276390dc57 100644 --- a/exporter/applications/views/goods/common/mixins.py +++ b/exporter/applications/views/goods/common/mixins.py @@ -4,6 +4,7 @@ from exporter.applications.services import get_application from exporter.goods.services import get_good, get_good_on_application +from exporter.exporter_answers.services import get_exporter_answer_set class GoodOnApplicationMixin: @@ -40,4 +41,9 @@ def dispatch(self, request, *args, **kwargs): except requests.exceptions.HTTPError: raise Http404(f"Couldn't get application {kwargs['pk']}") + exporter_answers, _ = get_exporter_answer_set(request, kwargs["pk"]) + self.exporter_answers = {} + for answer_set in exporter_answers["results"]: + self.exporter_answers[answer_set["section"]] = answer_set["answers"] + return super().dispatch(request, *args, **kwargs) diff --git a/exporter/applications/views/security_approvals/views.py b/exporter/applications/views/security_approvals/views.py index e0661e7a34..88be9f283c 100644 --- a/exporter/applications/views/security_approvals/views.py +++ b/exporter/applications/views/security_approvals/views.py @@ -10,7 +10,7 @@ from core.constants import SecurityClassifiedApprovalsType from core.wizard.views import BaseSessionWizardView -from exporter.applications.services import put_application +from exporter.exporter_answers.services import post_exporter_answer_set from exporter.applications.views.goods.common.mixins import ApplicationMixin from .forms import ( @@ -76,20 +76,23 @@ def get_success_url(self): ) @expect_status( - HTTPStatus.OK, + HTTPStatus.CREATED, "Error updating export details", "Unexpected error updating export details", ) - def update_application(self, form_dict): - payload = self.get_payload(form_dict) - return put_application( + def submit_answers(self, form_dict): + answers_payload = self.get_payload(form_dict) + return post_exporter_answer_set( self.request, + "application", + "security_approvals", + "standardapplication", self.application["id"], - payload, + answers_payload, ) def done(self, form_list, form_dict, **kwargs): - self.update_application(form_dict) + self.submit_answers(form_dict) return redirect(self.get_success_url()) @@ -98,6 +101,7 @@ class SecurityApprovalsSummaryView(LoginRequiredMixin, ApplicationMixin, Templat def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) + context["security_approval_answers"] = self.exporter_answers.get("security_approvals") context["application"] = self.application context["back_link_url"] = reverse("applications:task_list", kwargs={"pk": self.kwargs["pk"]}) context["security_classified_approvals_types"] = SecurityClassifiedApprovalsType diff --git a/exporter/exporter_answers/__init__.py b/exporter/exporter_answers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exporter/exporter_answers/services.py b/exporter/exporter_answers/services.py new file mode 100644 index 0000000000..6d302a93bb --- /dev/null +++ b/exporter/exporter_answers/services.py @@ -0,0 +1,24 @@ +from django.conf import settings + + +from core import client + + +def post_exporter_answer_set(request, flow, section, target_content_type, target_object_id, answers): + data = { + "flow": flow, + "section": section, + "answers": answers, + "frontend_commit_sha": settings.GIT_COMMIT, + "target_content_type": target_content_type, + "target_object_id": target_object_id, + } + response = client.post(request, f"/exporter/exporter-answers/exporter-answer-set/", data) + return response.json(), response.status_code + + +def get_exporter_answer_set(request, target_object_id): + response = client.get( + request, f"/exporter/exporter-answers/exporter-answer-set/?target_object_id={target_object_id}&status=draft" + ) + return response.json(), response.status_code diff --git a/exporter/templates/applications/security-approvals/security-approvals-summary.html b/exporter/templates/applications/security-approvals/security-approvals-summary.html index afed25a7e1..2f1803cae3 100644 --- a/exporter/templates/applications/security-approvals/security-approvals-summary.html +++ b/exporter/templates/applications/security-approvals/security-approvals-summary.html @@ -20,16 +20,16 @@

Do you have an MOD security approval, such as F680 or F1686?
-
{{ application.is_mod_security_approved|yesno|capfirst }}
+
{{ security_approval_answers.is_mod_security_approved|yesno|capfirst }}
Change
- {% if application.is_mod_security_approved %} + {% if security_approval_answers.security_approvals %}
What type of approval do you have?
-
{{ application.security_approvals|list_to_choice_labels:security_classified_approvals_types }}
+
{{ security_approval_answers.security_approvals|list_to_choice_labels:security_classified_approvals_types }}
Change @@ -37,10 +37,10 @@

{% endif %} - {% if "F680" in application.security_approvals %} + {% if "F680" in security_approval_answers.security_approvals %}
Are any products on this application subject to ITAR controls?
-
{{ application.subject_to_itar_controls|yesno|capfirst }}
+
{{ security_approval_answers.subject_to_itar_controls|yesno|capfirst }}
Change @@ -50,7 +50,7 @@

{% endif %} - {% if "F1686" in application.security_approvals %} + {% if "F1686" in security_approval_answers.security_approvals %}
What is the F1686 reference number?
-
{{ application.f1686_reference_number }}
+
{{ security_approval_answers.f1686_reference_number }}
Change @@ -78,7 +78,7 @@

When was the F1686 approved?
-
{{ application.f1686_approval_date|str_date_only }}
+
{{ security_approval_answers.f1686_approval_date|str_date_only }}
Change @@ -86,10 +86,10 @@

{% endif %} - {% if "Other" in application.security_approvals %} + {% if "Other" in security_approval_answers.security_approvals %}
Provide details of your written approval
-
{{ application.other_security_approval_details }}
+
{{ security_approval_answers.other_security_approval_details }}
Change From 760f9be2ebf441a46237fa02d0afd9d4dc72dd4d Mon Sep 17 00:00:00 2001 From: Brendan Smith Date: Fri, 24 Jan 2025 12:23:20 +0000 Subject: [PATCH 2/5] Post form questions to ExporterAnswerSet API --- .../applications/views/goods/common/mixins.py | 2 ++ .../views/goods/common/payloads.py | 9 +++++++++ .../views/security_approvals/edit_views.py | 2 +- .../views/security_approvals/forms.py | 18 +++++++----------- .../views/security_approvals/payloads.py | 14 ++++++++++++-- .../views/security_approvals/views.py | 14 ++++++++++---- exporter/exporter_answers/services.py | 3 ++- 7 files changed, 43 insertions(+), 19 deletions(-) diff --git a/exporter/applications/views/goods/common/mixins.py b/exporter/applications/views/goods/common/mixins.py index 276390dc57..bc2bd55ae5 100644 --- a/exporter/applications/views/goods/common/mixins.py +++ b/exporter/applications/views/goods/common/mixins.py @@ -43,7 +43,9 @@ def dispatch(self, request, *args, **kwargs): exporter_answers, _ = get_exporter_answer_set(request, kwargs["pk"]) self.exporter_answers = {} + self.exporter_questions = {} for answer_set in exporter_answers["results"]: self.exporter_answers[answer_set["section"]] = answer_set["answers"] + self.exporter_questions[answer_set["section"]] = answer_set["questions"] return super().dispatch(request, *args, **kwargs) diff --git a/exporter/applications/views/goods/common/payloads.py b/exporter/applications/views/goods/common/payloads.py index 555f389335..737294a0f3 100644 --- a/exporter/applications/views/goods/common/payloads.py +++ b/exporter/applications/views/goods/common/payloads.py @@ -6,6 +6,15 @@ def get_cleaned_data(form): return form.cleaned_data +def get_questions_data(form): + if not form.cleaned_data: + return {} + questions = {} + for field_name, field in form.declared_fields.items(): + questions[field_name] = field.label + return questions + + def get_pv_grading_payload(form): return { "is_pv_graded": "yes" if form.cleaned_data["is_pv_graded"] else "no", diff --git a/exporter/applications/views/security_approvals/edit_views.py b/exporter/applications/views/security_approvals/edit_views.py index c8fb67c502..b37a87eab8 100644 --- a/exporter/applications/views/security_approvals/edit_views.py +++ b/exporter/applications/views/security_approvals/edit_views.py @@ -25,7 +25,7 @@ is_f1686_approval_changed_and_selected, is_other_approval_changed_and_selected, ) -from .payloads import get_f1686_data, SecurityApprovalStepsPayloadBuilder +from .payloads import get_f1686_data from .constants import SecurityApprovalSteps from .initial import ( get_initial_security_classified_details, diff --git a/exporter/applications/views/security_approvals/forms.py b/exporter/applications/views/security_approvals/forms.py index ab3d921185..838496e56b 100644 --- a/exporter/applications/views/security_approvals/forms.py +++ b/exporter/applications/views/security_approvals/forms.py @@ -20,11 +20,6 @@ class SecurityClassifiedDetailsForm(BaseForm): class Layout: TITLE = "If you are exporting security classified products, you may need a Ministry of Defence (MOD) approval" - label = """ - This includes the release of United States ITAR (International Traffic in Arms regulations) material.

- Do you have an MOD security approval, such as F680 or F1686? - """ - security_approvals = forms.MultipleChoiceField( choices=SecurityClassifiedApprovalsType.choices, label="What type of approval do you have?", @@ -38,7 +33,8 @@ class Layout: (False, "No"), ), coerce=coerce_str_to_bool, - label=label, + label="Do you have an MOD security approval, such as F680 or F1686?", + help_text="This includes the release of United States ITAR (International Traffic in Arms regulations) material.", error_messages={ "required": "Select no if you do not have an MOD security approval", }, @@ -75,7 +71,7 @@ class SubjectToITARControlsForm(BaseForm): class Layout: TITLE = "Are any products on this application subject to ITAR controls?" - label = """ + help_text = """ We need to know if this export involves any defence articles including technical data that are subject to controls under the United States (US) International Traffic in Arms regulations (ITAR). """ @@ -87,7 +83,8 @@ class Layout: ), coerce=coerce_str_to_bool, widget=forms.RadioSelect, - label=label, + label="Are any products on this application subject to ITAR controls?", + help_text=help_text, error_messages={ "required": "Select no if the products are not subject to ITAR controls", }, @@ -103,7 +100,7 @@ class Layout: f680_reference_number = forms.CharField( widget=forms.TextInput, - label="", + label="What is the F680 reference number?", error_messages={ "required": " Enter the F680 reference number", }, @@ -169,8 +166,7 @@ class Layout: other_security_approval_details = forms.CharField( widget=forms.Textarea(attrs={"rows": 5}), - label="", - help_text="Enter any details you have about the MOD contracting authority, reference numbers, " + label="Enter any details you have about the MOD contracting authority, reference numbers, " "the signatory of the approval, or the Project Security Instruction.", error_messages={ "required": "Enter the details of your written approval", diff --git a/exporter/applications/views/security_approvals/payloads.py b/exporter/applications/views/security_approvals/payloads.py index d422140c1c..f26c5b4ced 100644 --- a/exporter/applications/views/security_approvals/payloads.py +++ b/exporter/applications/views/security_approvals/payloads.py @@ -1,7 +1,7 @@ from core.wizard.payloads import MergingPayloadBuilder from .constants import SecurityApprovalSteps -from exporter.applications.views.goods.common.payloads import get_cleaned_data +from exporter.applications.views.goods.common.payloads import get_cleaned_data, get_questions_data def get_f1686_data(form): @@ -10,7 +10,7 @@ def get_f1686_data(form): return payload -class SecurityApprovalStepsPayloadBuilder(MergingPayloadBuilder): +class SecurityApprovalStepsAnswerPayloadBuilder(MergingPayloadBuilder): payload_dict = { SecurityApprovalSteps.SECURITY_CLASSIFIED: get_cleaned_data, SecurityApprovalSteps.SUBJECT_TO_ITAR_CONTROLS: get_cleaned_data, @@ -18,3 +18,13 @@ class SecurityApprovalStepsPayloadBuilder(MergingPayloadBuilder): SecurityApprovalSteps.F1686_DETAILS: get_f1686_data, SecurityApprovalSteps.SECURITY_OTHER_DETAILS: get_cleaned_data, } + + +class SecurityApprovalStepsQuestionPayloadBuilder(MergingPayloadBuilder): + payload_dict = { + SecurityApprovalSteps.SECURITY_CLASSIFIED: get_questions_data, + SecurityApprovalSteps.SUBJECT_TO_ITAR_CONTROLS: get_questions_data, + SecurityApprovalSteps.F680_REFERENCE_NUMBER: get_questions_data, + SecurityApprovalSteps.F1686_DETAILS: get_questions_data, + SecurityApprovalSteps.SECURITY_OTHER_DETAILS: get_questions_data, + } diff --git a/exporter/applications/views/security_approvals/views.py b/exporter/applications/views/security_approvals/views.py index 88be9f283c..ac5fdf7b97 100644 --- a/exporter/applications/views/security_approvals/views.py +++ b/exporter/applications/views/security_approvals/views.py @@ -23,7 +23,7 @@ from .constants import SecurityApprovalSteps from .conditionals import is_f680_approval, is_f1686_approval, is_other_approval -from .payloads import SecurityApprovalStepsPayloadBuilder +from .payloads import SecurityApprovalStepsAnswerPayloadBuilder, SecurityApprovalStepsQuestionPayloadBuilder logger = logging.getLogger(__name__) @@ -65,8 +65,12 @@ def get_context_data(self, form, **kwargs): return ctx - def get_payload(self, form_dict): - export_details_payload = SecurityApprovalStepsPayloadBuilder().build(form_dict) + def get_answers_payload(self, form_dict): + export_details_payload = SecurityApprovalStepsAnswerPayloadBuilder().build(form_dict) + return export_details_payload + + def get_questions_payload(self, form_dict): + export_details_payload = SecurityApprovalStepsQuestionPayloadBuilder().build(form_dict) return export_details_payload def get_success_url(self): @@ -81,7 +85,8 @@ def get_success_url(self): "Unexpected error updating export details", ) def submit_answers(self, form_dict): - answers_payload = self.get_payload(form_dict) + answers_payload = self.get_answers_payload(form_dict) + questions_payload = self.get_questions_payload(form_dict) return post_exporter_answer_set( self.request, "application", @@ -89,6 +94,7 @@ def submit_answers(self, form_dict): "standardapplication", self.application["id"], answers_payload, + questions_payload, ) def done(self, form_list, form_dict, **kwargs): diff --git a/exporter/exporter_answers/services.py b/exporter/exporter_answers/services.py index 6d302a93bb..1dde22ebc4 100644 --- a/exporter/exporter_answers/services.py +++ b/exporter/exporter_answers/services.py @@ -4,11 +4,12 @@ from core import client -def post_exporter_answer_set(request, flow, section, target_content_type, target_object_id, answers): +def post_exporter_answer_set(request, flow, section, target_content_type, target_object_id, answers, questions): data = { "flow": flow, "section": section, "answers": answers, + "questions": questions, "frontend_commit_sha": settings.GIT_COMMIT, "target_content_type": target_content_type, "target_object_id": target_object_id, From 4dfa656fb4f836d5ef4960b04a6cfc4a9231b194 Mon Sep 17 00:00:00 2001 From: Brendan Smith Date: Fri, 24 Jan 2025 12:47:07 +0000 Subject: [PATCH 3/5] Demonstrate programmatic security approval summary --- core/builtins/custom_tags.py | 11 ++++++++++- .../applications/views/security_approvals/views.py | 1 + .../security-approvals-summary.html | 13 +++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/core/builtins/custom_tags.py b/core/builtins/custom_tags.py index 8ca4783fe7..ece671ba16 100644 --- a/core/builtins/custom_tags.py +++ b/core/builtins/custom_tags.py @@ -18,7 +18,7 @@ from django import template from django.conf import settings from django.http import QueryDict -from django.template.defaultfilters import stringfilter, safe, capfirst +from django.template.defaultfilters import stringfilter, safe, capfirst, yesno from django.templatetags.tz import localtime from django.utils.html import escape from django.utils.safestring import mark_safe, SafeString @@ -1060,3 +1060,12 @@ def __init__(self, text="..."): context["paging_form_id"] = form_id return context + + +@register.filter +def to_display(value): + if type(value) == bool: + return capfirst(yesno(value)) + if type(value) == list: + return ", ".join(value) + return value diff --git a/exporter/applications/views/security_approvals/views.py b/exporter/applications/views/security_approvals/views.py index ac5fdf7b97..b623401d05 100644 --- a/exporter/applications/views/security_approvals/views.py +++ b/exporter/applications/views/security_approvals/views.py @@ -108,6 +108,7 @@ class SecurityApprovalsSummaryView(LoginRequiredMixin, ApplicationMixin, Templat def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) context["security_approval_answers"] = self.exporter_answers.get("security_approvals") + context["security_approval_questions"] = self.exporter_questions.get("security_approvals") context["application"] = self.application context["back_link_url"] = reverse("applications:task_list", kwargs={"pk": self.kwargs["pk"]}) context["security_classified_approvals_types"] = SecurityClassifiedApprovalsType diff --git a/exporter/templates/applications/security-approvals/security-approvals-summary.html b/exporter/templates/applications/security-approvals/security-approvals-summary.html index 2f1803cae3..e988a908df 100644 --- a/exporter/templates/applications/security-approvals/security-approvals-summary.html +++ b/exporter/templates/applications/security-approvals/security-approvals-summary.html @@ -17,6 +17,7 @@

Review your answers below and make any amends you need to. Click 'Save and continue' to save your progress.

+
Do you have an MOD security approval, such as F680 or F1686?
@@ -101,5 +102,17 @@

+
+ {% for field_name, field_label in security_approval_questions.items %} +
+
{{field_label}}
+
{{ security_approval_answers|getitem:field_name|to_display }}
+
+ Change +
+
+ {% endfor %} +

{% endblock %} From 2ecf5421d21974134b4d59a04885eae04066599c Mon Sep 17 00:00:00 2001 From: Brendan Smith Date: Fri, 24 Jan 2025 15:53:27 +0000 Subject: [PATCH 4/5] Fix exporter answer retrieval now draft has become active --- exporter/applications/views/security_approvals/forms.py | 5 +++-- exporter/exporter_answers/services.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/exporter/applications/views/security_approvals/forms.py b/exporter/applications/views/security_approvals/forms.py index 838496e56b..f77bd38fa3 100644 --- a/exporter/applications/views/security_approvals/forms.py +++ b/exporter/applications/views/security_approvals/forms.py @@ -125,11 +125,12 @@ class Layout: f1686_reference_number = forms.CharField( required=False, widget=forms.TextInput, - label="Reference number (optional)", + label="What is the F1686 reference number?", + help_text="Reference number (optional)", ) f1686_approval_date = CustomErrorDateInputField( - label="Approval date", + label="When was the F1686 approved?", require_all_fields=False, help_text=f"For example, 20 2 {datetime.now().year-2}", error_messages={ diff --git a/exporter/exporter_answers/services.py b/exporter/exporter_answers/services.py index 1dde22ebc4..7348fd0a58 100644 --- a/exporter/exporter_answers/services.py +++ b/exporter/exporter_answers/services.py @@ -20,6 +20,6 @@ def post_exporter_answer_set(request, flow, section, target_content_type, target def get_exporter_answer_set(request, target_object_id): response = client.get( - request, f"/exporter/exporter-answers/exporter-answer-set/?target_object_id={target_object_id}&status=draft" + request, f"/exporter/exporter-answers/exporter-answer-set/?target_object_id={target_object_id}&status=active" ) return response.json(), response.status_code From db3edce46b7d2290a18b17014d3230714baf17b1 Mon Sep 17 00:00:00 2001 From: Brendan Smith Date: Fri, 24 Jan 2025 17:59:53 +0000 Subject: [PATCH 5/5] Show exporter answers on case detail page --- caseworker/cases/helpers/case.py | 3 +++ caseworker/exporter_answers/__init__.py | 0 caseworker/exporter_answers/services.py | 8 ++++++++ .../templates/components/security-approvals.html | 12 ++++++++++++ 4 files changed, 23 insertions(+) create mode 100644 caseworker/exporter_answers/__init__.py create mode 100644 caseworker/exporter_answers/services.py diff --git a/caseworker/cases/helpers/case.py b/caseworker/cases/helpers/case.py index 4392f1f6cb..145fda8a99 100644 --- a/caseworker/cases/helpers/case.py +++ b/caseworker/cases/helpers/case.py @@ -28,6 +28,7 @@ from caseworker.core.services import get_user_permissions, get_status_properties, get_permissible_statuses from lite_content.lite_internal_frontend import cases from lite_content.lite_internal_frontend.cases import CasePage, ApplicationPage +from caseworker.exporter_answers.services import get_exporter_answer_set from caseworker.queues.services import get_queue from caseworker.users.services import get_gov_user @@ -199,6 +200,7 @@ def get_context(self): if rules.test_rule("can_licence_status_be_changed", self.request, licence): show_actions_column = True break + exporter_answers, _ = get_exporter_answer_set(self.request, self.case_id) return { **context, @@ -207,6 +209,7 @@ def get_context(self): "slices": [Slices.SUMMARY, *self.slices], "case": self.case, "queue": self.queue, + "exporter_answers": exporter_answers["results"], "is_system_queue": self.queue["is_system_queue"], "goods_summary": self.get_goods_summary(), "destination_countries": self.get_destination_countries(), diff --git a/caseworker/exporter_answers/__init__.py b/caseworker/exporter_answers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/caseworker/exporter_answers/services.py b/caseworker/exporter_answers/services.py new file mode 100644 index 0000000000..bc35152875 --- /dev/null +++ b/caseworker/exporter_answers/services.py @@ -0,0 +1,8 @@ +from core import client + + +def get_exporter_answer_set(request, target_object_id): + response = client.get( + request, f"/caseworker/exporter-answers/exporter-answer-set/?target_object_id={target_object_id}&status=active" + ) + return response.json(), response.status_code diff --git a/caseworker/templates/components/security-approvals.html b/caseworker/templates/components/security-approvals.html index 80d039698a..26fc6cc5d5 100644 --- a/caseworker/templates/components/security-approvals.html +++ b/caseworker/templates/components/security-approvals.html @@ -43,3 +43,15 @@

Security approvals

{% endif %} {% endif %}
+ +{% for exporter_answer_set in exporter_answers %} +

{{exporter_answer_set.section}}

+
+ {% for field in exporter_answer_set.answer_fields %} +
+
{{exporter_answer_set.questions|getitem:field}}
+
{{exporter_answer_set.answers|getitem:field|to_display}}
+
+ {% endfor %} +
+{% endfor %}