From 880a2c5464fe40bc801f2f594fc883b4c5374ae8 Mon Sep 17 00:00:00 2001 From: Brendan Smith Date: Wed, 5 Feb 2025 14:51:26 +0000 Subject: [PATCH 01/60] Stop PII pre-commit complaining about F680 --- .pre-commit-config.yaml | 3 ++- pii-ignore-strings.txt | 1 + pii-secret-exclude.txt | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 pii-ignore-strings.txt diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c6b2d4bb26..8019f5182e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,8 @@ repos: - id: detect-aws-credentials args: ["--allow-missing-credentials"] - repo: https://github.com/uktrade/pii-secret-check-hooks - rev: 0.0.0.36 + # TODO: Switch back to a versioned release after this feature branch merges on the PII repo + rev: add-file-content-ignore-strings-option hooks: - id: pii_secret_filename files: "" diff --git a/pii-ignore-strings.txt b/pii-ignore-strings.txt new file mode 100644 index 0000000000..b6d7750223 --- /dev/null +++ b/pii-ignore-strings.txt @@ -0,0 +1 @@ +F680 diff --git a/pii-secret-exclude.txt b/pii-secret-exclude.txt index a96388c675..4fab15d495 100644 --- a/pii-secret-exclude.txt +++ b/pii-secret-exclude.txt @@ -110,3 +110,4 @@ unit_tests/caseworker/advice/views/test_trigger_list_view.py unit_tests/caseworker/advice/views/test_advice_view.py unit_tests/caseworker/advice/views/test_view_ogd_advice_countersign.py unit_tests/caseworker/queues/test_views.py +pii-ignore-strings.txt From 762751c035765c4a6ae1094f2a5fa00588fe4d62 Mon Sep 17 00:00:00 2001 From: Brendan Smith Date: Wed, 5 Feb 2025 14:56:14 +0000 Subject: [PATCH 02/60] Add initial general application details section for F680s --- .../f680/application_sections/__init__.py | 0 .../general_application_details/__init__.py | 0 .../general_application_details/constants.py | 3 ++ .../general_application_details/forms.py | 37 ++++++++++++++ .../general_application_details/urls.py | 10 ++++ .../general_application_details/views.py | 51 +++++++++++++++++++ exporter/f680/mixins.py | 0 exporter/f680/payloads.py | 12 +++++ exporter/f680/services.py | 5 ++ exporter/f680/urls.py | 12 +++-- exporter/f680/views.py | 2 +- exporter/templates/f680/summary.html | 20 ++++++-- 12 files changed, 144 insertions(+), 8 deletions(-) create mode 100644 exporter/f680/application_sections/__init__.py create mode 100644 exporter/f680/application_sections/general_application_details/__init__.py create mode 100644 exporter/f680/application_sections/general_application_details/constants.py create mode 100644 exporter/f680/application_sections/general_application_details/forms.py create mode 100644 exporter/f680/application_sections/general_application_details/urls.py create mode 100644 exporter/f680/application_sections/general_application_details/views.py create mode 100644 exporter/f680/mixins.py diff --git a/exporter/f680/application_sections/__init__.py b/exporter/f680/application_sections/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exporter/f680/application_sections/general_application_details/__init__.py b/exporter/f680/application_sections/general_application_details/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exporter/f680/application_sections/general_application_details/constants.py b/exporter/f680/application_sections/general_application_details/constants.py new file mode 100644 index 0000000000..2f0c2ea7f0 --- /dev/null +++ b/exporter/f680/application_sections/general_application_details/constants.py @@ -0,0 +1,3 @@ +class FormSteps: + APPLICATION_NAME = "APPLICATION_NAME" + EXCEPTIONAL_CIRCUMSTANCES = "EXCEPTIONAL_CIRCUMSTANCES" diff --git a/exporter/f680/application_sections/general_application_details/forms.py b/exporter/f680/application_sections/general_application_details/forms.py new file mode 100644 index 0000000000..5b0e5bfe9e --- /dev/null +++ b/exporter/f680/application_sections/general_application_details/forms.py @@ -0,0 +1,37 @@ +from django import forms + +from core.common.forms import BaseForm + + +class ApplicationNameForm(BaseForm): + class Layout: + TITLE = "Name the application" + TITLE_AS_LABEL_FOR = "name" + SUBMIT_BUTTON_TEXT = "Continue" + + name = forms.CharField( + label="", + help_text="Give the application a reference name so you can refer back to it when needed", + ) + + def get_layout_fields(self): + return ("name",) + + +class ExceptionalCircumstancesForm(BaseForm): + class Layout: + TITLE = "Do you have exceptional circumstances that mean you need F680 approval in less than 30 days?" + TITLE_AS_LABEL_FOR = "is_exceptional_circumstances" + SUBMIT_BUTTON_TEXT = "Continue" + + is_exceptional_circumstances = forms.TypedChoiceField( + choices=( + (True, "Yes"), + (False, "No"), + ), + label="", + widget=forms.RadioSelect, + ) + + def get_layout_fields(self): + return ("is_exceptional_circumstances",) diff --git a/exporter/f680/application_sections/general_application_details/urls.py b/exporter/f680/application_sections/general_application_details/urls.py new file mode 100644 index 0000000000..243d4751c1 --- /dev/null +++ b/exporter/f680/application_sections/general_application_details/urls.py @@ -0,0 +1,10 @@ +from django.urls import path + +from . import views + + +app_name = "general_application_details" + +urlpatterns = [ + path("", views.GeneralApplicationDetailsView.as_view(), name="wizard"), +] diff --git a/exporter/f680/application_sections/general_application_details/views.py b/exporter/f680/application_sections/general_application_details/views.py new file mode 100644 index 0000000000..041886a5cc --- /dev/null +++ b/exporter/f680/application_sections/general_application_details/views.py @@ -0,0 +1,51 @@ +from http import HTTPStatus +from django.shortcuts import redirect +from django.urls import reverse + +from core.auth.views import LoginRequiredMixin +from core.decorators import expect_status +from core.wizard.views import BaseSessionWizardView + +from exporter.f680.services import patch_f680_application, get_f680_application +from exporter.f680.payloads import F680PatchPayloadBuilder +from exporter.f680.views import F680FeatureRequiredMixin + +from .constants import FormSteps +from .forms import ApplicationNameForm, ExceptionalCircumstancesForm + + +class GeneralApplicationDetailsView(LoginRequiredMixin, F680FeatureRequiredMixin, BaseSessionWizardView): + form_list = [ + (FormSteps.APPLICATION_NAME, ApplicationNameForm), + (FormSteps.EXCEPTIONAL_CIRCUMSTANCES, ExceptionalCircumstancesForm), + ] + + def setup(self, request, *args, **kwargs): + super().setup(request, *args, **kwargs) + self.application = get_f680_application(request, kwargs["pk"]) + + @expect_status( + HTTPStatus.OK, + "Error updating F680 application", + "Unexpected error updating F680 application", + ) + def patch_f680_application(self, data): + return patch_f680_application(self.request, self.application["id"], data) + + def get_success_url(self, application_id): + return reverse( + "f680:summary", + kwargs={ + "pk": application_id, + }, + ) + + def get_payload(self, form_dict): + section = "general_application_details" + current_application = self.application["application"] + return F680PatchPayloadBuilder().build(section, current_application, form_dict) + + def done(self, form_list, form_dict, **kwargs): + data = self.get_payload(form_dict) + response_data, _ = self.patch_f680_application(data) + return redirect(self.get_success_url(response_data["id"])) diff --git a/exporter/f680/mixins.py b/exporter/f680/mixins.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exporter/f680/payloads.py b/exporter/f680/payloads.py index f023db6d7d..6abbb81d50 100644 --- a/exporter/f680/payloads.py +++ b/exporter/f680/payloads.py @@ -1,3 +1,5 @@ +from deepmerge import always_merger + from core.wizard.payloads import MergingPayloadBuilder from exporter.applications.views.goods.common.payloads import get_cleaned_data from .constants import ApplicationFormSteps # /PS-IGNORE @@ -11,3 +13,13 @@ class F680CreatePayloadBuilder(MergingPayloadBuilder): # /PS-IGNORE def build(self, form_dict): payload = super().build(form_dict) return {"application": payload} + + +class F680PatchPayloadBuilder: + def build(self, section, application_data, form_dict): + payload = {} + for step_name, form in form_dict.items(): + if form: + always_merger.merge(payload, get_cleaned_data(form)) + application_data[section] = payload + return {"application": application_data} diff --git a/exporter/f680/services.py b/exporter/f680/services.py index a4c5405a4a..9d84a09d01 100644 --- a/exporter/f680/services.py +++ b/exporter/f680/services.py @@ -9,3 +9,8 @@ def post_f680_application(request, json): def get_f680_application(request, application_id): data = client.get(request, f"/exporter/f680/application/{application_id}/") return data.json() + + +def patch_f680_application(request, application_id, json): + data = client.patch(request, f"/exporter/f680/application/{application_id}/", json) + return data.json(), data.status_code diff --git a/exporter/f680/urls.py b/exporter/f680/urls.py index 3717f1bd5f..efea85419b 100644 --- a/exporter/f680/urls.py +++ b/exporter/f680/urls.py @@ -1,4 +1,4 @@ -from django.urls import path +from django.urls import include, path from . import views @@ -6,7 +6,11 @@ app_name = "f680" urlpatterns = [ - path("apply/", views.F680ApplicationCreateView.as_view(), name="apply"), # PS-IGNORE - path("/apply/", views.F680ApplicationSummaryView.as_view(), name="summary"), # PS-IGNORE - path("/apply/", views.F680ApplicationSummaryView.as_view(), name="submit"), # PS-IGNORE + path("apply/", views.F680ApplicationCreateView.as_view(), name="apply"), + path("/apply/", views.F680ApplicationSummaryView.as_view(), name="summary"), + path("/apply/", views.F680ApplicationSummaryView.as_view(), name="submit"), + path( + "/general-application-details/", + include("exporter.f680.application_sections.general_application_details.urls"), + ), ] diff --git a/exporter/f680/views.py b/exporter/f680/views.py index ee84912255..5ebe6a4f02 100644 --- a/exporter/f680/views.py +++ b/exporter/f680/views.py @@ -26,7 +26,7 @@ ) -class F680FeatureRequiredMixin(AccessMixin): # PS-IGNORE +class F680FeatureRequiredMixin(AccessMixin): def dispatch(self, request, *args, **kwargs): if not settings.FEATURE_FLAG_ALLOW_F680: self.raise_exception = True diff --git a/exporter/templates/f680/summary.html b/exporter/templates/f680/summary.html index 374e2aa2ea..edb384bef1 100644 --- a/exporter/templates/f680/summary.html +++ b/exporter/templates/f680/summary.html @@ -17,13 +17,27 @@

  • - Your reference + Application type
    - Saved + Completed
    - {{ application.application.name }} + F680 +
    +
  • +
  • +
    + General application details + {% if application.application.general_application_details %} +
    + Completed +
    + {% else %} +
    + Not Started +
    + {% endif %}
From 170c2bfc0a218b7421d88c0b71a2c019a3862a99 Mon Sep 17 00:00:00 2001 From: Brendan Smith Date: Wed, 5 Feb 2025 16:25:29 +0000 Subject: [PATCH 03/60] Ensure F680 general application details forms reflect designs --- .../general_application_details/constants.py | 1 + .../general_application_details/forms.py | 37 ++++++++++++++++++- .../general_application_details/views.py | 3 +- .../forms/help_exceptional_circumstances.html | 6 +++ 4 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 exporter/templates/f680/forms/help_exceptional_circumstances.html diff --git a/exporter/f680/application_sections/general_application_details/constants.py b/exporter/f680/application_sections/general_application_details/constants.py index 2f0c2ea7f0..14857d6de9 100644 --- a/exporter/f680/application_sections/general_application_details/constants.py +++ b/exporter/f680/application_sections/general_application_details/constants.py @@ -1,3 +1,4 @@ class FormSteps: APPLICATION_NAME = "APPLICATION_NAME" EXCEPTIONAL_CIRCUMSTANCES = "EXCEPTIONAL_CIRCUMSTANCES" + EXCEPTIONAL_CIRCUMSTANCES_REASONS = "EXCEPTIONAL_CIRCUMSTANCES_REASONS" diff --git a/exporter/f680/application_sections/general_application_details/forms.py b/exporter/f680/application_sections/general_application_details/forms.py index 5b0e5bfe9e..7fe5596a36 100644 --- a/exporter/f680/application_sections/general_application_details/forms.py +++ b/exporter/f680/application_sections/general_application_details/forms.py @@ -1,4 +1,8 @@ from django import forms +from django.template.loader import render_to_string + +from crispy_forms_gds.fields import DateInputField +from crispy_forms_gds.layout.content import HTML from core.common.forms import BaseForm @@ -34,4 +38,35 @@ class Layout: ) def get_layout_fields(self): - return ("is_exceptional_circumstances",) + return ( + "is_exceptional_circumstances", + HTML.details( + "Help with exceptional circumstances", + render_to_string("f680/forms/help_exceptional_circumstances.html"), + ), + ) + + +class ExplainExceptionalCircumstancesForm(BaseForm): + class Layout: + TITLE = "Explain your exceptional circumstances" + SUBMIT_BUTTON_TEXT = "Save and continue" + + exceptional_circumstances_date = DateInputField( + label="When do you need your F680 approval?", + ) + exceptional_circumstances_reason = forms.CharField( + label="Why do you need approval in less than 30 days?", + widget=forms.Textarea(attrs={"rows": "5"}), + ) + + def clean(self): + cleaned_data = super().clean() + cleaned_data["exceptional_circumstances_date"] = cleaned_data["exceptional_circumstances_date"].isoformat() + return cleaned_data + + def get_layout_fields(self): + return ( + "exceptional_circumstances_date", + "exceptional_circumstances_reason", + ) diff --git a/exporter/f680/application_sections/general_application_details/views.py b/exporter/f680/application_sections/general_application_details/views.py index 041886a5cc..460ff7b505 100644 --- a/exporter/f680/application_sections/general_application_details/views.py +++ b/exporter/f680/application_sections/general_application_details/views.py @@ -11,13 +11,14 @@ from exporter.f680.views import F680FeatureRequiredMixin from .constants import FormSteps -from .forms import ApplicationNameForm, ExceptionalCircumstancesForm +from .forms import ApplicationNameForm, ExceptionalCircumstancesForm, ExplainExceptionalCircumstancesForm class GeneralApplicationDetailsView(LoginRequiredMixin, F680FeatureRequiredMixin, BaseSessionWizardView): form_list = [ (FormSteps.APPLICATION_NAME, ApplicationNameForm), (FormSteps.EXCEPTIONAL_CIRCUMSTANCES, ExceptionalCircumstancesForm), + (FormSteps.EXCEPTIONAL_CIRCUMSTANCES_REASONS, ExplainExceptionalCircumstancesForm), ] def setup(self, request, *args, **kwargs): diff --git a/exporter/templates/f680/forms/help_exceptional_circumstances.html b/exporter/templates/f680/forms/help_exceptional_circumstances.html new file mode 100644 index 0000000000..f0769cd018 --- /dev/null +++ b/exporter/templates/f680/forms/help_exceptional_circumstances.html @@ -0,0 +1,6 @@ +

We only accept certain reasons, such as:

+
    +
  • humanitarian efforts such as search and rescue/recovery
  • +
  • life threatening scenarios
  • +
  • the UK's defence interests, for example short notice government export campaigns
  • +
From e38118f82f70d3920e5b525ef5098a4995c0cf16 Mon Sep 17 00:00:00 2001 From: Brendan Smith Date: Thu, 6 Feb 2025 09:58:43 +0000 Subject: [PATCH 04/60] Relocate common get_cleaned_data function --- core/wizard/payloads.py | 4 ++++ exporter/applications/views/goods/common/payloads.py | 6 +----- exporter/applications/views/parties/payloads.py | 3 +-- .../applications/views/security_approvals/edit_views.py | 2 +- exporter/applications/views/security_approvals/payloads.py | 2 +- exporter/core/organisation/payloads.py | 3 +-- exporter/f680/payloads.py | 3 +-- exporter/organisation/members/users/payloads.py | 3 +-- 8 files changed, 11 insertions(+), 15 deletions(-) diff --git a/core/wizard/payloads.py b/core/wizard/payloads.py index 01b54b13b5..9db53a90b0 100644 --- a/core/wizard/payloads.py +++ b/core/wizard/payloads.py @@ -9,3 +9,7 @@ def build(self, form_dict): if form: always_merger.merge(payload, payload_func(form)) return payload + + +def get_cleaned_data(form): + return form.cleaned_data diff --git a/exporter/applications/views/goods/common/payloads.py b/exporter/applications/views/goods/common/payloads.py index 555f389335..dcf1606355 100644 --- a/exporter/applications/views/goods/common/payloads.py +++ b/exporter/applications/views/goods/common/payloads.py @@ -1,9 +1,5 @@ from exporter.applications.views.goods.common import constants -from core.wizard.payloads import MergingPayloadBuilder - - -def get_cleaned_data(form): - return form.cleaned_data +from core.wizard.payloads import MergingPayloadBuilder, get_cleaned_data def get_pv_grading_payload(form): diff --git a/exporter/applications/views/parties/payloads.py b/exporter/applications/views/parties/payloads.py index 6618396512..a048eb83fe 100644 --- a/exporter/applications/views/parties/payloads.py +++ b/exporter/applications/views/parties/payloads.py @@ -1,5 +1,4 @@ -from core.wizard.payloads import MergingPayloadBuilder -from exporter.applications.views.goods.common.payloads import get_cleaned_data +from core.wizard.payloads import MergingPayloadBuilder, get_cleaned_data from exporter.core.constants import ( SetPartyFormSteps, ) diff --git a/exporter/applications/views/security_approvals/edit_views.py b/exporter/applications/views/security_approvals/edit_views.py index c8fb67c502..550fb1c23a 100644 --- a/exporter/applications/views/security_approvals/edit_views.py +++ b/exporter/applications/views/security_approvals/edit_views.py @@ -9,7 +9,7 @@ from core.decorators import expect_status from exporter.applications.views.goods.common.mixins import ApplicationMixin -from exporter.applications.views.goods.common.payloads import get_cleaned_data +from core.wizard.payloads import get_cleaned_data from exporter.applications.services import put_application from core.wizard.views import BaseSessionWizardView diff --git a/exporter/applications/views/security_approvals/payloads.py b/exporter/applications/views/security_approvals/payloads.py index d422140c1c..0c8ad9c7a6 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 core.wizard.payloads import get_cleaned_data def get_f1686_data(form): diff --git a/exporter/core/organisation/payloads.py b/exporter/core/organisation/payloads.py index 6ca2df8443..abddb4c194 100644 --- a/exporter/core/organisation/payloads.py +++ b/exporter/core/organisation/payloads.py @@ -1,5 +1,4 @@ -from exporter.applications.views.goods.common.payloads import get_cleaned_data -from core.wizard.payloads import MergingPayloadBuilder +from core.wizard.payloads import MergingPayloadBuilder, get_cleaned_data from .constants import RegistrationSteps from .forms import RegisterAddressDetailsUKIndividualForm, RegisterAddressDetailsUKCommercialForm diff --git a/exporter/f680/payloads.py b/exporter/f680/payloads.py index 6abbb81d50..ceb4d0533c 100644 --- a/exporter/f680/payloads.py +++ b/exporter/f680/payloads.py @@ -1,7 +1,6 @@ from deepmerge import always_merger -from core.wizard.payloads import MergingPayloadBuilder -from exporter.applications.views.goods.common.payloads import get_cleaned_data +from core.wizard.payloads import MergingPayloadBuilder, get_cleaned_data from .constants import ApplicationFormSteps # /PS-IGNORE diff --git a/exporter/organisation/members/users/payloads.py b/exporter/organisation/members/users/payloads.py index affbb1d253..a41d649d1b 100644 --- a/exporter/organisation/members/users/payloads.py +++ b/exporter/organisation/members/users/payloads.py @@ -1,5 +1,4 @@ -from exporter.applications.views.goods.common.payloads import get_cleaned_data -from core.wizard.payloads import MergingPayloadBuilder +from core.wizard.payloads import MergingPayloadBuilder, get_cleaned_data from .constants import AddUserSteps From 9fb427094a9fcdf6296ba06e88587a74d92240a3 Mon Sep 17 00:00:00 2001 From: Brendan Smith Date: Thu, 6 Feb 2025 10:24:09 +0000 Subject: [PATCH 05/60] Set questions and answers in API application JSON payload --- core/wizard/payloads.py | 9 +++++++++ exporter/assets/scss/components/_all.scss | 1 + .../general_application_details/forms.py | 4 ++-- exporter/f680/payloads.py | 16 +++++++++------- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/core/wizard/payloads.py b/core/wizard/payloads.py index 9db53a90b0..4b1548c874 100644 --- a/core/wizard/payloads.py +++ b/core/wizard/payloads.py @@ -13,3 +13,12 @@ def build(self, form_dict): 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 diff --git a/exporter/assets/scss/components/_all.scss b/exporter/assets/scss/components/_all.scss index 7acdd930f7..04477be486 100644 --- a/exporter/assets/scss/components/_all.scss +++ b/exporter/assets/scss/components/_all.scss @@ -13,3 +13,4 @@ @import "user-menu"; @import "appeal-details"; @import "star-rating"; +@import "form"; diff --git a/exporter/f680/application_sections/general_application_details/forms.py b/exporter/f680/application_sections/general_application_details/forms.py index 7fe5596a36..d9b5c4265c 100644 --- a/exporter/f680/application_sections/general_application_details/forms.py +++ b/exporter/f680/application_sections/general_application_details/forms.py @@ -14,7 +14,7 @@ class Layout: SUBMIT_BUTTON_TEXT = "Continue" name = forms.CharField( - label="", + label=Layout.TITLE, help_text="Give the application a reference name so you can refer back to it when needed", ) @@ -33,7 +33,7 @@ class Layout: (True, "Yes"), (False, "No"), ), - label="", + label="Do you have exceptional circumstances that mean you need F680 approval in less than 30 days?", widget=forms.RadioSelect, ) diff --git a/exporter/f680/payloads.py b/exporter/f680/payloads.py index ceb4d0533c..815a70bca9 100644 --- a/exporter/f680/payloads.py +++ b/exporter/f680/payloads.py @@ -1,12 +1,12 @@ from deepmerge import always_merger -from core.wizard.payloads import MergingPayloadBuilder, get_cleaned_data -from .constants import ApplicationFormSteps # /PS-IGNORE +from core.wizard.payloads import MergingPayloadBuilder, get_cleaned_data, get_questions_data +from .constants import ApplicationFormSteps -class F680CreatePayloadBuilder(MergingPayloadBuilder): # /PS-IGNORE +class F680CreatePayloadBuilder(MergingPayloadBuilder): payload_dict = { - ApplicationFormSteps.APPLICATION_NAME: get_cleaned_data, # /PS-IGNORE + ApplicationFormSteps.APPLICATION_NAME: get_cleaned_data, } def build(self, form_dict): @@ -16,9 +16,11 @@ def build(self, form_dict): class F680PatchPayloadBuilder: def build(self, section, application_data, form_dict): - payload = {} + answer_payload = {} + question_payload = {} for step_name, form in form_dict.items(): if form: - always_merger.merge(payload, get_cleaned_data(form)) - application_data[section] = payload + always_merger.merge(answer_payload, get_cleaned_data(form)) + always_merger.merge(question_payload, get_questions_data(form)) + application_data[section] = {"answers": answer_payload, "questions": question_payload} return {"application": application_data} From 569f8b8fa597e93e6e5d9e21d03c294d44ded622 Mon Sep 17 00:00:00 2001 From: Brendan Smith Date: Thu, 6 Feb 2025 10:32:09 +0000 Subject: [PATCH 06/60] Make final general application details wizard step conditional --- .../general_application_details/views.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/exporter/f680/application_sections/general_application_details/views.py b/exporter/f680/application_sections/general_application_details/views.py index 460ff7b505..ac527246c9 100644 --- a/exporter/f680/application_sections/general_application_details/views.py +++ b/exporter/f680/application_sections/general_application_details/views.py @@ -14,12 +14,20 @@ from .forms import ApplicationNameForm, ExceptionalCircumstancesForm, ExplainExceptionalCircumstancesForm +def is_exceptional_circumstances(wizard): + cleaned_data = wizard.get_cleaned_data_for_step(FormSteps.EXCEPTIONAL_CIRCUMSTANCES) or {} + return cleaned_data.get("is_exceptional_circumstances", False) + + class GeneralApplicationDetailsView(LoginRequiredMixin, F680FeatureRequiredMixin, BaseSessionWizardView): form_list = [ (FormSteps.APPLICATION_NAME, ApplicationNameForm), (FormSteps.EXCEPTIONAL_CIRCUMSTANCES, ExceptionalCircumstancesForm), (FormSteps.EXCEPTIONAL_CIRCUMSTANCES_REASONS, ExplainExceptionalCircumstancesForm), ] + condition_dict = { + FormSteps.EXCEPTIONAL_CIRCUMSTANCES_REASONS: is_exceptional_circumstances, + } def setup(self, request, *args, **kwargs): super().setup(request, *args, **kwargs) From c9b68110bc00d428fa011b270eed1a70cbcfbe12 Mon Sep 17 00:00:00 2001 From: Brendan Smith Date: Thu, 6 Feb 2025 11:11:05 +0000 Subject: [PATCH 07/60] Make answers editable via initial data --- .../general_application_details/forms.py | 16 ++++++++++++++++ .../general_application_details/views.py | 9 ++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/exporter/f680/application_sections/general_application_details/forms.py b/exporter/f680/application_sections/general_application_details/forms.py index d9b5c4265c..e13256142a 100644 --- a/exporter/f680/application_sections/general_application_details/forms.py +++ b/exporter/f680/application_sections/general_application_details/forms.py @@ -1,3 +1,4 @@ +from datetime import datetime from django import forms from django.template.loader import render_to_string @@ -5,6 +6,7 @@ from crispy_forms_gds.layout.content import HTML from core.common.forms import BaseForm +from core.forms.utils import coerce_str_to_bool class ApplicationNameForm(BaseForm): @@ -35,6 +37,7 @@ class Layout: ), label="Do you have exceptional circumstances that mean you need F680 approval in less than 30 days?", widget=forms.RadioSelect, + coerce=coerce_str_to_bool, ) def get_layout_fields(self): @@ -60,7 +63,20 @@ class Layout: widget=forms.Textarea(attrs={"rows": "5"}), ) + def __init__(self, *args, **kwargs): + # We have to do some coercion from string to datetime object here due to JSON serialization + if ( + "initial" in kwargs + and "exceptional_circumstances_date" in kwargs["initial"] + and isinstance(kwargs["initial"]["exceptional_circumstances_date"], str) + ): + kwargs["initial"]["exceptional_circumstances_date"] = datetime.fromisoformat( + kwargs["initial"]["exceptional_circumstances_date"] + ) + return super().__init__(*args, **kwargs) + def clean(self): + # We have to do some coercion from datetime object to string here due to JSON serialization cleaned_data = super().clean() cleaned_data["exceptional_circumstances_date"] = cleaned_data["exceptional_circumstances_date"].isoformat() return cleaned_data diff --git a/exporter/f680/application_sections/general_application_details/views.py b/exporter/f680/application_sections/general_application_details/views.py index ac527246c9..54c32b3660 100644 --- a/exporter/f680/application_sections/general_application_details/views.py +++ b/exporter/f680/application_sections/general_application_details/views.py @@ -28,6 +28,7 @@ class GeneralApplicationDetailsView(LoginRequiredMixin, F680FeatureRequiredMixin condition_dict = { FormSteps.EXCEPTIONAL_CIRCUMSTANCES_REASONS: is_exceptional_circumstances, } + section = "general_application_details" def setup(self, request, *args, **kwargs): super().setup(request, *args, **kwargs) @@ -41,6 +42,9 @@ def setup(self, request, *args, **kwargs): def patch_f680_application(self, data): return patch_f680_application(self.request, self.application["id"], data) + def get_form_initial(self, step): + return self.application.get("application", {}).get("general_application_details", {}).get("answers", {}) + def get_success_url(self, application_id): return reverse( "f680:summary", @@ -50,9 +54,8 @@ def get_success_url(self, application_id): ) def get_payload(self, form_dict): - section = "general_application_details" - current_application = self.application["application"] - return F680PatchPayloadBuilder().build(section, current_application, form_dict) + current_application = self.application.get("application", {}) + return F680PatchPayloadBuilder().build(self.section, current_application, form_dict) def done(self, form_list, form_dict, **kwargs): data = self.get_payload(form_dict) From 87874ce1accb7b46497a7ecef9ee9581c21dabff Mon Sep 17 00:00:00 2001 From: Brendan Smith Date: Thu, 6 Feb 2025 14:44:54 +0000 Subject: [PATCH 08/60] Ensure GeneralApplicationDetailsView 404s when application not found --- .../general_application_details/views.py | 13 +++++++++++-- exporter/f680/services.py | 2 +- exporter/f680/views.py | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/exporter/f680/application_sections/general_application_details/views.py b/exporter/f680/application_sections/general_application_details/views.py index 54c32b3660..7f8c9b9ed7 100644 --- a/exporter/f680/application_sections/general_application_details/views.py +++ b/exporter/f680/application_sections/general_application_details/views.py @@ -32,7 +32,16 @@ class GeneralApplicationDetailsView(LoginRequiredMixin, F680FeatureRequiredMixin def setup(self, request, *args, **kwargs): super().setup(request, *args, **kwargs) - self.application = get_f680_application(request, kwargs["pk"]) + self.application, _ = self.get_f680_application(kwargs["pk"]) + + @expect_status( + HTTPStatus.OK, + "Error retrieving F680 application", + "Unexpected error retrieving F680 application", + reraise_404=True, + ) + def get_f680_application(self, pk): + return get_f680_application(self.request, pk) @expect_status( HTTPStatus.OK, @@ -43,7 +52,7 @@ def patch_f680_application(self, data): return patch_f680_application(self.request, self.application["id"], data) def get_form_initial(self, step): - return self.application.get("application", {}).get("general_application_details", {}).get("answers", {}) + return self.application.get("application", {}).get(self.section, {}).get("answers", {}) def get_success_url(self, application_id): return reverse( diff --git a/exporter/f680/services.py b/exporter/f680/services.py index 9d84a09d01..195751217a 100644 --- a/exporter/f680/services.py +++ b/exporter/f680/services.py @@ -8,7 +8,7 @@ def post_f680_application(request, json): def get_f680_application(request, application_id): data = client.get(request, f"/exporter/f680/application/{application_id}/") - return data.json() + return data.json(), data.status_code def patch_f680_application(request, application_id, json): diff --git a/exporter/f680/views.py b/exporter/f680/views.py index 5ebe6a4f02..99f466fbeb 100644 --- a/exporter/f680/views.py +++ b/exporter/f680/views.py @@ -73,7 +73,7 @@ class F680ApplicationSummaryView(LoginRequiredMixin, F680FeatureRequiredMixin, F def setup(self, request, *args, **kwargs): super().setup(request, *args, **kwargs) - self.application = get_f680_application(request, kwargs["pk"]) # PS-IGNORE + self.application, _ = get_f680_application(request, kwargs["pk"]) # PS-IGNORE def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) From 893e7ccfafb8facf48f5679980de74a73a03db0f Mon Sep 17 00:00:00 2001 From: Brendan Smith Date: Thu, 6 Feb 2025 18:16:42 +0000 Subject: [PATCH 09/60] Add unit tests for GeneralApplicationDetailsView --- exporter/conftest.py | 3 + .../general_application_details/forms.py | 3 +- .../tests/__init__.py | 0 .../tests/test_views.py | 249 ++++++++++++++++++ 4 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 exporter/conftest.py create mode 100644 exporter/f680/application_sections/general_application_details/tests/__init__.py create mode 100644 exporter/f680/application_sections/general_application_details/tests/test_views.py diff --git a/exporter/conftest.py b/exporter/conftest.py new file mode 100644 index 0000000000..be5f00a035 --- /dev/null +++ b/exporter/conftest.py @@ -0,0 +1,3 @@ +# pylint: disable=unused-wildcard-import,wildcard-import +from unit_tests.conftest import * +from unit_tests.exporter.conftest import * diff --git a/exporter/f680/application_sections/general_application_details/forms.py b/exporter/f680/application_sections/general_application_details/forms.py index e13256142a..6b7852c40e 100644 --- a/exporter/f680/application_sections/general_application_details/forms.py +++ b/exporter/f680/application_sections/general_application_details/forms.py @@ -78,7 +78,8 @@ def __init__(self, *args, **kwargs): def clean(self): # We have to do some coercion from datetime object to string here due to JSON serialization cleaned_data = super().clean() - cleaned_data["exceptional_circumstances_date"] = cleaned_data["exceptional_circumstances_date"].isoformat() + if "exceptional_circumstances_date" in cleaned_data: + cleaned_data["exceptional_circumstances_date"] = cleaned_data["exceptional_circumstances_date"].isoformat() return cleaned_data def get_layout_fields(self): diff --git a/exporter/f680/application_sections/general_application_details/tests/__init__.py b/exporter/f680/application_sections/general_application_details/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exporter/f680/application_sections/general_application_details/tests/test_views.py b/exporter/f680/application_sections/general_application_details/tests/test_views.py new file mode 100644 index 0000000000..85f3e3c20e --- /dev/null +++ b/exporter/f680/application_sections/general_application_details/tests/test_views.py @@ -0,0 +1,249 @@ +import pytest + +from django.urls import reverse + +from core import client + +from exporter.f680.application_sections.general_application_details.forms import ( + ApplicationNameForm, + ExceptionalCircumstancesForm, + ExplainExceptionalCircumstancesForm, +) +from exporter.f680.application_sections.general_application_details.constants import FormSteps + + +@pytest.fixture() +def unset_f680_feature_flag(settings): + settings.FEATURE_FLAG_ALLOW_F680 = False + + +@pytest.fixture(autouse=True) +def setup(mock_exporter_user_me, settings): + settings.FEATURE_FLAG_ALLOW_F680 = True + + +@pytest.fixture +def missing_application_id(): + return "6bb0828c-1520-4624-b729-7f3e6e5b9f5d" + + +@pytest.fixture +def missing_f680_application_wizard_url(missing_application_id): + return reverse( + "f680:general_application_details:wizard", + kwargs={"pk": missing_application_id}, + ) + + +@pytest.fixture +def f680_application_wizard_url(data_f680_case): + return reverse( + "f680:general_application_details:wizard", + kwargs={"pk": data_f680_case["id"]}, + ) + + +@pytest.fixture +def mock_f680_application_get_404(requests_mock, missing_application_id): + url = client._build_absolute_uri(f"/exporter/f680/application/{missing_application_id}/") + return requests_mock.get(url=url, json={}, status_code=404) + + +@pytest.fixture +def mock_f680_application_get(requests_mock, data_f680_case): + application_id = data_f680_case["id"] + url = client._build_absolute_uri(f"/exporter/f680/application/{application_id}/") + return requests_mock.get(url=url, json=data_f680_case) + + +@pytest.fixture +def force_exceptional_circumstances(goto_step, post_to_step): + goto_step(FormSteps.EXCEPTIONAL_CIRCUMSTANCES) + post_to_step( + FormSteps.EXCEPTIONAL_CIRCUMSTANCES, + {"is_exceptional_circumstances": True}, + ) + + +@pytest.fixture +def mock_f680_application_get_existing_data(requests_mock, data_f680_case): + data_f680_case["application"] = { + "general_application_details": { + "answers": { + "name": "my first F680", + "is_exceptional_circumstances": False, + }, + "questions": { + "name": "What is the name of the application?", + "is_exceptional_circumstances": "Are there exceptional circumstances?", + }, + } + } + application_id = data_f680_case["id"] + url = client._build_absolute_uri(f"/exporter/f680/application/{application_id}/") + return requests_mock.get(url=url, json=data_f680_case) + + +@pytest.fixture +def mock_patch_f680_application(requests_mock, data_f680_case): + application_id = data_f680_case["id"] + url = client._build_absolute_uri(f"/exporter/f680/application/{application_id}/") + return requests_mock.patch(url=url, json=data_f680_case) + + +@pytest.fixture +def post_to_step(post_to_step_factory, f680_application_wizard_url): + return post_to_step_factory(f680_application_wizard_url) + + +@pytest.fixture +def goto_step(goto_step_factory, f680_application_wizard_url): + return goto_step_factory(f680_application_wizard_url) + + +class TestGeneralApplicationDetailsView: + + def test_GET_no_application_404( + self, + authorized_client, + missing_f680_application_wizard_url, + mock_f680_application_get_404, + ): + response = authorized_client.get(missing_f680_application_wizard_url) + assert response.status_code == 404 + + def test_GET_success( + self, + authorized_client, + mock_f680_application_get, + f680_application_wizard_url, + ): + response = authorized_client.get(f680_application_wizard_url) + assert response.status_code == 200 + assert isinstance(response.context["form"], ApplicationNameForm) + + def test_GET_no_feature_flag_forbidden( + self, + authorized_client, + mock_f680_application_get, + f680_application_wizard_url, + unset_f680_feature_flag, + ): + response = authorized_client.get(f680_application_wizard_url) + assert response.status_code == 200 + assert response.context["title"] == "Forbidden" + + @pytest.mark.parametrize( + "step, data, expected_next_form", + ( + (FormSteps.APPLICATION_NAME, {"name": "some application name"}, ExceptionalCircumstancesForm), + ( + FormSteps.EXCEPTIONAL_CIRCUMSTANCES, + {"is_exceptional_circumstances": True}, + ExplainExceptionalCircumstancesForm, + ), + ), + ) + def test_POST_to_step_success( + self, + step, + data, + expected_next_form, + post_to_step, + goto_step, + mock_f680_application_get, + ): + goto_step(step) + response = post_to_step( + step, + data, + ) + assert response.status_code == 200 + assert isinstance(response.context["form"], expected_next_form) + + @pytest.mark.parametrize( + "step, data, expected_errors", + ( + (FormSteps.APPLICATION_NAME, {"name": ""}, {"name": ["This field is required."]}), + (FormSteps.EXCEPTIONAL_CIRCUMSTANCES, {}, {"is_exceptional_circumstances": ["This field is required."]}), + ( + FormSteps.EXCEPTIONAL_CIRCUMSTANCES_REASONS, + {}, + { + "exceptional_circumstances_date": ["Enter the day, month and year"], + "exceptional_circumstances_reason": ["This field is required."], + }, + ), + ), + ) + def test_POST_to_step_validation_error( + self, + step, + data, + expected_errors, + post_to_step, + goto_step, + mock_f680_application_get, + force_exceptional_circumstances, + ): + goto_step(step) + response = post_to_step( + step, + data, + ) + assert response.status_code == 200 + for field_name, error in expected_errors.items(): + assert response.context["form"][field_name].errors == error + + def test_POST_submit_wizard_success( + self, post_to_step, goto_step, mock_f680_application_get, mock_patch_f680_application + ): + response = post_to_step( + FormSteps.APPLICATION_NAME, + {"name": "some test app"}, + ) + response = post_to_step( + FormSteps.EXCEPTIONAL_CIRCUMSTANCES, + {"is_exceptional_circumstances": True}, + ) + response = post_to_step( + FormSteps.EXCEPTIONAL_CIRCUMSTANCES_REASONS, + { + "exceptional_circumstances_reason": "because", + "exceptional_circumstances_date_0": "1", + "exceptional_circumstances_date_1": "12", + "exceptional_circumstances_date_2": "2026", + }, + ) + assert response.status_code == 302 + assert mock_patch_f680_application.called_once + assert mock_patch_f680_application.last_request.json() == { + "application": { + "name": "F680 Test 1", + "general_application_details": { + "answers": { + "name": "some test app", + "is_exceptional_circumstances": True, + "exceptional_circumstances_date": "2026-12-01", + "exceptional_circumstances_reason": "because", + }, + "questions": { + "name": "Name the application", + "is_exceptional_circumstances": "Do you have exceptional circumstances that mean you need F680 approval in less than 30 days?", + "exceptional_circumstances_date": "When do you need your F680 approval?", + "exceptional_circumstances_reason": "Why do you need approval in less than 30 days?", + }, + }, + } + } + + def test_GET_with_existing_data_success( + self, + authorized_client, + mock_f680_application_get_existing_data, + f680_application_wizard_url, + ): + response = authorized_client.get(f680_application_wizard_url) + assert response.status_code == 200 + assert isinstance(response.context["form"], ApplicationNameForm) + assert response.context["form"]["name"].initial == "my first F680" From 79defa5b64d0f68c9334473eed089daaeeb110b9 Mon Sep 17 00:00:00 2001 From: Brendan Smith Date: Fri, 7 Feb 2025 10:23:01 +0000 Subject: [PATCH 10/60] Ensure exporter-located unit tests run in CI --- .circleci/config.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 80f94383e2..2747e36367 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,7 +1,7 @@ version: 2.1 orbs: - browser-tools: circleci/browser-tools@1.4.7 + browser-tools: circleci/browser-tools@1.4.7 # /PS-IGNORE parameters: run_ui_tests: @@ -131,7 +131,7 @@ commands: - run: name: Clone API command: | - git clone git@github.com:uktrade/lite-api.git + git clone git@github.com:uktrade/lite-api.git # /PS-IGNORE cd lite-api git checkout $(python ../which_branch.py <>) - run: @@ -252,7 +252,7 @@ jobs: environment: <<: *common_env_vars PIPENV_DOTENV_LOCATION: tests.exporter.env - PYTEST_ADDOPTS: unit_tests/exporter --capture=no --nomigrations + PYTEST_ADDOPTS: exporter unit_tests/exporter --capture=no --nomigrations FILE_UPLOAD_HANDLERS: django.core.files.uploadhandler.MemoryFileUploadHandler,django.core.files.uploadhandler.TemporaryFileUploadHandler steps: - backend_unit_tests: From 72bc1be050428755a5565bd29bbec5fdb8bf3b65 Mon Sep 17 00:00:00 2001 From: Brendan Smith Date: Fri, 7 Feb 2025 10:56:38 +0000 Subject: [PATCH 11/60] Fix SCSS build --- exporter/assets/scss/components/_all.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/exporter/assets/scss/components/_all.scss b/exporter/assets/scss/components/_all.scss index 04477be486..7acdd930f7 100644 --- a/exporter/assets/scss/components/_all.scss +++ b/exporter/assets/scss/components/_all.scss @@ -13,4 +13,3 @@ @import "user-menu"; @import "appeal-details"; @import "star-rating"; -@import "form"; From 6656a76416ee61f536955ea9e6a891ccbfa22fbc Mon Sep 17 00:00:00 2001 From: Brendan Smith Date: Fri, 7 Feb 2025 12:27:36 +0000 Subject: [PATCH 12/60] Fix lint complaints --- .../application_sections/general_application_details/forms.py | 2 +- exporter/f680/payloads.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/exporter/f680/application_sections/general_application_details/forms.py b/exporter/f680/application_sections/general_application_details/forms.py index 6b7852c40e..37c9605064 100644 --- a/exporter/f680/application_sections/general_application_details/forms.py +++ b/exporter/f680/application_sections/general_application_details/forms.py @@ -73,7 +73,7 @@ def __init__(self, *args, **kwargs): kwargs["initial"]["exceptional_circumstances_date"] = datetime.fromisoformat( kwargs["initial"]["exceptional_circumstances_date"] ) - return super().__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def clean(self): # We have to do some coercion from datetime object to string here due to JSON serialization diff --git a/exporter/f680/payloads.py b/exporter/f680/payloads.py index 815a70bca9..b7e84b492f 100644 --- a/exporter/f680/payloads.py +++ b/exporter/f680/payloads.py @@ -18,7 +18,7 @@ class F680PatchPayloadBuilder: def build(self, section, application_data, form_dict): answer_payload = {} question_payload = {} - for step_name, form in form_dict.items(): + for form in form_dict.values(): if form: always_merger.merge(answer_payload, get_cleaned_data(form)) always_merger.merge(question_payload, get_questions_data(form)) From 50731bb20b0c1195c77c2dc2fd5d1ba7d7bc63fa Mon Sep 17 00:00:00 2001 From: Brendan Smith Date: Fri, 7 Feb 2025 13:56:02 +0000 Subject: [PATCH 13/60] Add tests for payload helpers --- .circleci/config.yml | 2 +- core/wizard/tests/__init__.py | 0 core/wizard/tests/test_payloads.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 core/wizard/tests/__init__.py create mode 100644 core/wizard/tests/test_payloads.py diff --git a/.circleci/config.yml b/.circleci/config.yml index 2747e36367..b3e80c6c6b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -266,7 +266,7 @@ jobs: environment: <<: *common_env_vars PIPENV_DOTENV_LOCATION: tests.exporter.env - PYTEST_ADDOPTS: unit_tests/core --capture=no --nomigrations + PYTEST_ADDOPTS: core unit_tests/core --capture=no --nomigrations FILE_UPLOAD_HANDLERS: django.core.files.uploadhandler.MemoryFileUploadHandler,django.core.files.uploadhandler.TemporaryFileUploadHandler steps: - backend_unit_tests: diff --git a/core/wizard/tests/__init__.py b/core/wizard/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/core/wizard/tests/test_payloads.py b/core/wizard/tests/test_payloads.py new file mode 100644 index 0000000000..0c1afefea3 --- /dev/null +++ b/core/wizard/tests/test_payloads.py @@ -0,0 +1,29 @@ +from unittest import mock + +from core.wizard.payloads import get_questions_data, get_cleaned_data + + +class TestGetQuestionsData: + + def test_get_questions_data_no_cleaned_data(self): + form = mock.Mock() + form.cleaned_data = None + assert get_questions_data(form) == {} + + def test_get_questions_data_form_with_declared_fields(self): + form = mock.Mock() + form.cleaned_data.return_value = {"cleaned": "data"} + mock_field = mock.Mock() + mock_field.label = "Name" + form.declared_fields = { + "name": mock_field, + } + assert get_questions_data(form) == {"name": "Name"} + + +class TestGetCleanedData: + + def test_get_cleaned_data(self): + form = mock.Mock() + form.cleaned_data = {"some": "value"} + assert get_cleaned_data(form) == {"some": "value"} From 36eaeb8a3d5be393460101b5ab7aba231c2e2d25 Mon Sep 17 00:00:00 2001 From: Brendan Smith Date: Fri, 7 Feb 2025 14:03:11 +0000 Subject: [PATCH 14/60] Fix coverage complaint --- .../general_application_details/tests/test_views.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/exporter/f680/application_sections/general_application_details/tests/test_views.py b/exporter/f680/application_sections/general_application_details/tests/test_views.py index 85f3e3c20e..d311327bf9 100644 --- a/exporter/f680/application_sections/general_application_details/tests/test_views.py +++ b/exporter/f680/application_sections/general_application_details/tests/test_views.py @@ -71,7 +71,9 @@ def mock_f680_application_get_existing_data(requests_mock, data_f680_case): "general_application_details": { "answers": { "name": "my first F680", - "is_exceptional_circumstances": False, + "is_exceptional_circumstances": True, + "exceptional_circumstances_date": "2090-01-01", + "exceptional_circumstances_reason": "some reason", }, "questions": { "name": "What is the name of the application?", From bc0ec7d0c941e4364c37ba7ecc86f46429a7d806 Mon Sep 17 00:00:00 2001 From: Brendan Smith Date: Fri, 7 Feb 2025 14:13:49 +0000 Subject: [PATCH 15/60] Factor out common F680 wizard logic to parent class --- .../general_application_details/views.py | 55 +---------------- exporter/f680/application_sections/views.py | 59 +++++++++++++++++++ 2 files changed, 61 insertions(+), 53 deletions(-) create mode 100644 exporter/f680/application_sections/views.py diff --git a/exporter/f680/application_sections/general_application_details/views.py b/exporter/f680/application_sections/general_application_details/views.py index 7f8c9b9ed7..30ce244042 100644 --- a/exporter/f680/application_sections/general_application_details/views.py +++ b/exporter/f680/application_sections/general_application_details/views.py @@ -1,14 +1,4 @@ -from http import HTTPStatus -from django.shortcuts import redirect -from django.urls import reverse - -from core.auth.views import LoginRequiredMixin -from core.decorators import expect_status -from core.wizard.views import BaseSessionWizardView - -from exporter.f680.services import patch_f680_application, get_f680_application -from exporter.f680.payloads import F680PatchPayloadBuilder -from exporter.f680.views import F680FeatureRequiredMixin +from exporter.f680.application_sections.views import F680ApplicationSectionWizard from .constants import FormSteps from .forms import ApplicationNameForm, ExceptionalCircumstancesForm, ExplainExceptionalCircumstancesForm @@ -19,7 +9,7 @@ def is_exceptional_circumstances(wizard): return cleaned_data.get("is_exceptional_circumstances", False) -class GeneralApplicationDetailsView(LoginRequiredMixin, F680FeatureRequiredMixin, BaseSessionWizardView): +class GeneralApplicationDetailsView(F680ApplicationSectionWizard): form_list = [ (FormSteps.APPLICATION_NAME, ApplicationNameForm), (FormSteps.EXCEPTIONAL_CIRCUMSTANCES, ExceptionalCircumstancesForm), @@ -29,44 +19,3 @@ class GeneralApplicationDetailsView(LoginRequiredMixin, F680FeatureRequiredMixin FormSteps.EXCEPTIONAL_CIRCUMSTANCES_REASONS: is_exceptional_circumstances, } section = "general_application_details" - - def setup(self, request, *args, **kwargs): - super().setup(request, *args, **kwargs) - self.application, _ = self.get_f680_application(kwargs["pk"]) - - @expect_status( - HTTPStatus.OK, - "Error retrieving F680 application", - "Unexpected error retrieving F680 application", - reraise_404=True, - ) - def get_f680_application(self, pk): - return get_f680_application(self.request, pk) - - @expect_status( - HTTPStatus.OK, - "Error updating F680 application", - "Unexpected error updating F680 application", - ) - def patch_f680_application(self, data): - return patch_f680_application(self.request, self.application["id"], data) - - def get_form_initial(self, step): - return self.application.get("application", {}).get(self.section, {}).get("answers", {}) - - def get_success_url(self, application_id): - return reverse( - "f680:summary", - kwargs={ - "pk": application_id, - }, - ) - - def get_payload(self, form_dict): - current_application = self.application.get("application", {}) - return F680PatchPayloadBuilder().build(self.section, current_application, form_dict) - - def done(self, form_list, form_dict, **kwargs): - data = self.get_payload(form_dict) - response_data, _ = self.patch_f680_application(data) - return redirect(self.get_success_url(response_data["id"])) diff --git a/exporter/f680/application_sections/views.py b/exporter/f680/application_sections/views.py new file mode 100644 index 0000000000..6cc27eb84f --- /dev/null +++ b/exporter/f680/application_sections/views.py @@ -0,0 +1,59 @@ +from http import HTTPStatus + +from django.shortcuts import redirect +from django.urls import reverse + +from core.auth.views import LoginRequiredMixin +from core.decorators import expect_status +from core.wizard.views import BaseSessionWizardView + +from exporter.f680.services import patch_f680_application, get_f680_application +from exporter.f680.payloads import F680PatchPayloadBuilder +from exporter.f680.views import F680FeatureRequiredMixin + + +class F680ApplicationSectionWizard(LoginRequiredMixin, F680FeatureRequiredMixin, BaseSessionWizardView): + form_list = [] + condition_dict = {} + section = None + + def setup(self, request, *args, **kwargs): + super().setup(request, *args, **kwargs) + self.application, _ = self.get_f680_application(kwargs["pk"]) + + @expect_status( + HTTPStatus.OK, + "Error retrieving F680 application", + "Unexpected error retrieving F680 application", + reraise_404=True, + ) + def get_f680_application(self, pk): + return get_f680_application(self.request, pk) + + @expect_status( + HTTPStatus.OK, + "Error updating F680 application", + "Unexpected error updating F680 application", + ) + def patch_f680_application(self, data): + return patch_f680_application(self.request, self.application["id"], data) + + def get_form_initial(self, step): + return self.application.get("application", {}).get(self.section, {}).get("answers", {}) + + def get_success_url(self, application_id): + return reverse( + "f680:summary", + kwargs={ + "pk": application_id, + }, + ) + + def get_payload(self, form_dict): + current_application = self.application.get("application", {}) + return F680PatchPayloadBuilder().build(self.section, current_application, form_dict) + + def done(self, form_list, form_dict, **kwargs): + data = self.get_payload(form_dict) + response_data, _ = self.patch_f680_application(data) + return redirect(self.get_success_url(response_data["id"])) From a49ae14ba8b2b3bb52df34d1475bf96fef2874f4 Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Wed, 5 Feb 2025 11:48:57 +0000 Subject: [PATCH 16/60] add http status code to get and add extra checking in tests --- .../exporter/applications/views/test_f680s.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/unit_tests/exporter/applications/views/test_f680s.py b/unit_tests/exporter/applications/views/test_f680s.py index 124d98b3f5..16c1eb9269 100644 --- a/unit_tests/exporter/applications/views/test_f680s.py +++ b/unit_tests/exporter/applications/views/test_f680s.py @@ -1,4 +1,5 @@ import pytest +from uuid import uuid4 from bs4 import BeautifulSoup from django.urls import reverse @@ -38,6 +39,13 @@ def mock_f680_application_get(requests_mock, data_f680_case): # PS-IGNORE return requests_mock.get(url=url, json=data_f680_case) # PS-IGNORE +@pytest.fixture +def mock_f680_application_get_404(requests_mock, data_f680_case): # PS-IGNORE + application_id = str(uuid4) + url = client._build_absolute_uri(f"/exporter/f680/application/{application_id}/") # PS-IGNORE + return requests_mock.get(url=url, json={}, status_code=404) # PS-IGNORE + + @pytest.fixture def mock_application_post(requests_mock, data_f680_case): # PS-IGNORE application = data_f680_case # PS-IGNORE @@ -134,6 +142,13 @@ def test_get_f680_summary_view_success( heading_element = content.find("h1", class_="govuk-heading-l govuk-!-margin-bottom-2") assert heading_element.string.strip() == "F680 Application" # PS-IGNORE + def test_get_f680_summary_view_case_not_found( + self, authorized_client, set_f680_feature_flag, mock_f680_application_get_404 + ): + response = authorized_client.get(str(uuid4)) # PS-IGNORE + breakpoint() + assert response.status_code == 404 + def test_get_f680_summary_view_fail_with_feature_flag_off( self, authorized_client, From 7139af2f05a31780dc3cb6a3238c58362deb8f51 Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Wed, 5 Feb 2025 12:11:48 +0000 Subject: [PATCH 17/60] remove breakpoint --- exporter/f680/views.py | 1 + unit_tests/exporter/applications/views/test_f680s.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/exporter/f680/views.py b/exporter/f680/views.py index 99f466fbeb..9bf45ba647 100644 --- a/exporter/f680/views.py +++ b/exporter/f680/views.py @@ -59,6 +59,7 @@ def get_success_url(self, application_id): ) def get_payload(self, form_dict): + return F680CreatePayloadBuilder().build(form_dict) # /PS-IGNORE def done(self, form_list, form_dict, **kwargs): diff --git a/unit_tests/exporter/applications/views/test_f680s.py b/unit_tests/exporter/applications/views/test_f680s.py index 16c1eb9269..cdc1d47f4d 100644 --- a/unit_tests/exporter/applications/views/test_f680s.py +++ b/unit_tests/exporter/applications/views/test_f680s.py @@ -146,7 +146,6 @@ def test_get_f680_summary_view_case_not_found( self, authorized_client, set_f680_feature_flag, mock_f680_application_get_404 ): response = authorized_client.get(str(uuid4)) # PS-IGNORE - breakpoint() assert response.status_code == 404 def test_get_f680_summary_view_fail_with_feature_flag_off( From 94cdb206a20fa7c3d2bda90d53eae2e3e1e76add Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Thu, 6 Feb 2025 16:54:25 +0000 Subject: [PATCH 18/60] fix get application for summary view not found test --- .../exporter/applications/views/test_f680s.py | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/unit_tests/exporter/applications/views/test_f680s.py b/unit_tests/exporter/applications/views/test_f680s.py index cdc1d47f4d..4ddba7fefe 100644 --- a/unit_tests/exporter/applications/views/test_f680s.py +++ b/unit_tests/exporter/applications/views/test_f680s.py @@ -39,13 +39,6 @@ def mock_f680_application_get(requests_mock, data_f680_case): # PS-IGNORE return requests_mock.get(url=url, json=data_f680_case) # PS-IGNORE -@pytest.fixture -def mock_f680_application_get_404(requests_mock, data_f680_case): # PS-IGNORE - application_id = str(uuid4) - url = client._build_absolute_uri(f"/exporter/f680/application/{application_id}/") # PS-IGNORE - return requests_mock.get(url=url, json={}, status_code=404) # PS-IGNORE - - @pytest.fixture def mock_application_post(requests_mock, data_f680_case): # PS-IGNORE application = data_f680_case # PS-IGNORE @@ -143,9 +136,18 @@ def test_get_f680_summary_view_success( assert heading_element.string.strip() == "F680 Application" # PS-IGNORE def test_get_f680_summary_view_case_not_found( - self, authorized_client, set_f680_feature_flag, mock_f680_application_get_404 + self, + authorized_client, + requests_mock, + set_f680_feature_flag, ): - response = authorized_client.get(str(uuid4)) # PS-IGNORE + + app_pk = str(uuid4()) + client_uri = client._build_absolute_uri(f"/exporter/f680/application/{app_pk}/") + + requests_mock.get(client_uri, json={}, status_code=404) + + response = authorized_client.get(reverse("f680:summary", kwargs={"pk": app_pk})) assert response.status_code == 404 def test_get_f680_summary_view_fail_with_feature_flag_off( From 84b14431fc28b73fd64c77548f69daf64dd6e571 Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Fri, 7 Feb 2025 14:35:55 +0000 Subject: [PATCH 19/60] add help text and approval type form first pass --- .../forms/help_with_approval_type.html | 21 ++++++ exporter/f680/forms.py | 73 ++++++++++++++++++- 2 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 exporter/core/templates/applications/forms/help_with_approval_type.html diff --git a/exporter/core/templates/applications/forms/help_with_approval_type.html b/exporter/core/templates/applications/forms/help_with_approval_type.html new file mode 100644 index 0000000000..cca54b2402 --- /dev/null +++ b/exporter/core/templates/applications/forms/help_with_approval_type.html @@ -0,0 +1,21 @@ +

+ Initial discussions or promoting the products
+ Talking about or promoting your products to potential customers, including offering enhanced through life support to an existing customer. This can be in the UK or overseas. It includes meetings hosted by government organisations. + +

+

+ Product demonstrations in the UK or overseas
+ Carrying out a 'live' activity to showcase your products. Trials and evaluations count as demonstrations. You can demonstrate the actual products or substitutes. You must include any substitutes as products in your application. The demonstration can take place anywhere, but you must include all entities and locations in your application. +

+

+ Training
+ You do not need training approval to provide basic instructions for how to a use products. You do need training approval to teach operational employment of products, including tactics, techniques and procedures. Training can happen in the UK or overseas. +

+

+ Through life support
+ This covers all aspect of an export programme. It includes exporting and delivering products to a customer. It also includes all support offered throughout the life of products, such as maintenance, enhancements, obsolescence management and disposal. You must have a valid F680 throughout the life of the export programme. +

+

+ Supply
+ Content TBC needs to come from MOD as this is a new option. +

diff --git a/exporter/f680/forms.py b/exporter/f680/forms.py index d9e1118874..dc794f7663 100644 --- a/exporter/f680/forms.py +++ b/exporter/f680/forms.py @@ -1,6 +1,10 @@ from django import forms +from crispy_forms_gds.layout import HTML -from core.common.forms import BaseForm +from core.common.forms import BaseForm, FieldsetForm, TextChoice +from core.forms.layouts import ConditionalCheckboxes, ConditionalCheckboxesQuestion +from django.db.models import TextChoices +from django.template.loader import render_to_string class ApplicationNameForm(BaseForm): @@ -25,3 +29,70 @@ class Layout: def get_layout_fields(self): return [] + + +class ApprovalTypeForm(FieldsetForm): + class Layout: + TITLE = "Select the types of approvals you need" + TITLE_AS_LABEL_FOR = "approval_choices" + SUBMIT_BUTTON_TEXT = "Save and continue" + + class ApprovalTypeChoices(TextChoices): + INITIAL_DISCUSSIONS_OR_PROMOTING = ( + "INITIAL_DISCUSSIONS_OR_PROMOTING", + "Initial discussions or promoting products", + ) + DEMO_IN_UK_TO_OVERSEAS = "DEMO_IN_UK_TO_OVERSEAS", "Demonstration in the United Kingdom to overseas customers" + DEMO_OVERSEAS = "DEMO_OVERSEAS", "Demonstration overseas" + TRAINING = "TRAINING", "Training" + THROUGH_LIFE_SUPPORT = "THROUGH_LIFE_SUPPORT", "Through life support" + SUPPLY = "SUPPLY", "Supply" + + ApprovalTypeChoices = ( + TextChoice(ApprovalTypeChoices.INITIAL_DISCUSSIONS_OR_PROMOTING), + TextChoice(ApprovalTypeChoices.DEMO_IN_UK_TO_OVERSEAS), + TextChoice(ApprovalTypeChoices.DEMO_OVERSEAS), + TextChoice(ApprovalTypeChoices.TRAINING), + TextChoice(ApprovalTypeChoices.THROUGH_LIFE_SUPPORT), + TextChoice(ApprovalTypeChoices.SUPPLY), + ) + + approval_choices = forms.MultipleChoiceField( + choices=ApprovalTypeChoices, + error_messages={ + "required": 'Select an approval choice"', + }, + widget=forms.CheckboxSelectMultiple(), + ) + + demonstration_text = forms.MultipleChoiceField( + label="Explain what you are demonstrating and why", + choices=(), # set in __init__ + required=False, + # setting id for javascript to use + widget=forms.SelectMultiple( + attrs={ + "id": "demonstration_text", + "data-module": "multi-select", + } + ), + ) + + def get_layout_fields(self): + return ( + ConditionalCheckboxes( + "approval_choices", + ConditionalCheckboxesQuestion( + "Demonstration in the United Kingdom to overseas customers", + "demonstration_text", + ), + ConditionalCheckboxesQuestion( + "Demonstration overseas", + "demonstration_text", + ), + ), + HTML.details( + "Help with exceptional circumstances", + render_to_string("applications/forms/help_with_approval_type.html"), + ), + ) From 97f372063f7375b86ef57ae355b484024838ccae Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Fri, 7 Feb 2025 16:47:56 +0000 Subject: [PATCH 20/60] form update --- exporter/f680/forms.py | 85 ++++++++++++++---------------------------- 1 file changed, 28 insertions(+), 57 deletions(-) diff --git a/exporter/f680/forms.py b/exporter/f680/forms.py index dc794f7663..16242811b8 100644 --- a/exporter/f680/forms.py +++ b/exporter/f680/forms.py @@ -1,10 +1,8 @@ from django import forms -from crispy_forms_gds.layout import HTML +from crispy_forms_gds.choices import Choice -from core.common.forms import BaseForm, FieldsetForm, TextChoice -from core.forms.layouts import ConditionalCheckboxes, ConditionalCheckboxesQuestion -from django.db.models import TextChoices -from django.template.loader import render_to_string +from core.common.forms import BaseForm, FieldsetForm +from core.forms.layouts import ConditionalCheckboxes class ApplicationNameForm(BaseForm): @@ -37,62 +35,35 @@ class Layout: TITLE_AS_LABEL_FOR = "approval_choices" SUBMIT_BUTTON_TEXT = "Save and continue" - class ApprovalTypeChoices(TextChoices): - INITIAL_DISCUSSIONS_OR_PROMOTING = ( - "INITIAL_DISCUSSIONS_OR_PROMOTING", - "Initial discussions or promoting products", - ) - DEMO_IN_UK_TO_OVERSEAS = "DEMO_IN_UK_TO_OVERSEAS", "Demonstration in the United Kingdom to overseas customers" - DEMO_OVERSEAS = "DEMO_OVERSEAS", "Demonstration overseas" - TRAINING = "TRAINING", "Training" - THROUGH_LIFE_SUPPORT = "THROUGH_LIFE_SUPPORT", "Through life support" - SUPPLY = "SUPPLY", "Supply" + choice_list = [ + "initial discussions or promoting products", + "demonstration in the United Kingdom to overseas customers", + "demonstration overseas", + "training", + "through life support", + "supply", + ] - ApprovalTypeChoices = ( - TextChoice(ApprovalTypeChoices.INITIAL_DISCUSSIONS_OR_PROMOTING), - TextChoice(ApprovalTypeChoices.DEMO_IN_UK_TO_OVERSEAS), - TextChoice(ApprovalTypeChoices.DEMO_OVERSEAS), - TextChoice(ApprovalTypeChoices.TRAINING), - TextChoice(ApprovalTypeChoices.THROUGH_LIFE_SUPPORT), - TextChoice(ApprovalTypeChoices.SUPPLY), - ) + def _get_choices(self, choice_list): + approval_choices = [] + approval_text = {} - approval_choices = forms.MultipleChoiceField( - choices=ApprovalTypeChoices, - error_messages={ - "required": 'Select an approval choice"', - }, - widget=forms.CheckboxSelectMultiple(), - ) + approval_list = choice_list + for result in approval_list: + key = "_".join(result.lower().split()) + choice = Choice(key, result) + if result == approval_list[-1]: + choice = Choice(key, result, divider="or") + approval_choices.append(choice) + approval_text[key] = result.capitalize() + return approval_choices, approval_text - demonstration_text = forms.MultipleChoiceField( - label="Explain what you are demonstrating and why", - choices=(), # set in __init__ + approval_choices = forms.MultipleChoiceField( + label="", required=False, - # setting id for javascript to use - widget=forms.SelectMultiple( - attrs={ - "id": "demonstration_text", - "data-module": "multi-select", - } - ), + widget=forms.CheckboxSelectMultiple, + choices=(), ) def get_layout_fields(self): - return ( - ConditionalCheckboxes( - "approval_choices", - ConditionalCheckboxesQuestion( - "Demonstration in the United Kingdom to overseas customers", - "demonstration_text", - ), - ConditionalCheckboxesQuestion( - "Demonstration overseas", - "demonstration_text", - ), - ), - HTML.details( - "Help with exceptional circumstances", - render_to_string("applications/forms/help_with_approval_type.html"), - ), - ) + return (ConditionalCheckboxes("approval_choices", *self.conditional_checkbox_choices),) From 669fd63eab97e1b99c84721a42f8cbdcd75fc6dd Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Fri, 7 Feb 2025 16:56:39 +0000 Subject: [PATCH 21/60] form init method added --- exporter/f680/forms.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/exporter/f680/forms.py b/exporter/f680/forms.py index 16242811b8..0d97aa5c3d 100644 --- a/exporter/f680/forms.py +++ b/exporter/f680/forms.py @@ -2,7 +2,7 @@ from crispy_forms_gds.choices import Choice from core.common.forms import BaseForm, FieldsetForm -from core.forms.layouts import ConditionalCheckboxes +from core.forms.layouts import ConditionalCheckboxes, ConditionalCheckboxesQuestion class ApplicationNameForm(BaseForm): @@ -49,6 +49,7 @@ def _get_choices(self, choice_list): approval_text = {} approval_list = choice_list + breakpoint() for result in approval_list: key = "_".join(result.lower().split()) choice = Choice(key, result) @@ -56,6 +57,7 @@ def _get_choices(self, choice_list): choice = Choice(key, result, divider="or") approval_choices.append(choice) approval_text[key] = result.capitalize() + breakpoint() return approval_choices, approval_text approval_choices = forms.MultipleChoiceField( @@ -65,5 +67,25 @@ def _get_choices(self, choice_list): choices=(), ) + def __init__(self, *args, **kwargs): + + approval_choices, approval_text = self._get_choices(self.choice_list) + + self.conditional_checkbox_choices = ( + ConditionalCheckboxesQuestion(choices.label, choices.value) for choices in approval_choices + ) + + super().__init__(*args, **kwargs) + + self.fields["approval_choices"].choices = approval_choices + for choices in approval_choices: + self.fields[choices.value] = forms.CharField( + widget=forms.Textarea(attrs={"rows": 3}), + label="Description", + required=False, + initial=approval_text[choices.value], + ) + def get_layout_fields(self): + return (ConditionalCheckboxes("approval_choices", *self.conditional_checkbox_choices),) From fb3254bfe3c6fbb8562699cfe94e39360aede48a Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Fri, 7 Feb 2025 16:57:12 +0000 Subject: [PATCH 22/60] remove breakpoints --- exporter/f680/forms.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/exporter/f680/forms.py b/exporter/f680/forms.py index 0d97aa5c3d..73de190c08 100644 --- a/exporter/f680/forms.py +++ b/exporter/f680/forms.py @@ -49,7 +49,6 @@ def _get_choices(self, choice_list): approval_text = {} approval_list = choice_list - breakpoint() for result in approval_list: key = "_".join(result.lower().split()) choice = Choice(key, result) @@ -57,7 +56,6 @@ def _get_choices(self, choice_list): choice = Choice(key, result, divider="or") approval_choices.append(choice) approval_text[key] = result.capitalize() - breakpoint() return approval_choices, approval_text approval_choices = forms.MultipleChoiceField( From 2ae7fb7c763c616f035c72a55925433e3c845d1a Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Fri, 7 Feb 2025 17:43:35 +0000 Subject: [PATCH 23/60] working version with textareas --- exporter/f680/forms.py | 128 ++++++++++++++++++++++++++--------------- 1 file changed, 81 insertions(+), 47 deletions(-) diff --git a/exporter/f680/forms.py b/exporter/f680/forms.py index 73de190c08..595c4b2611 100644 --- a/exporter/f680/forms.py +++ b/exporter/f680/forms.py @@ -1,8 +1,10 @@ from django import forms -from crispy_forms_gds.choices import Choice +from crispy_forms_gds.layout import HTML -from core.common.forms import BaseForm, FieldsetForm +from core.common.forms import BaseForm, FieldsetForm, TextChoice from core.forms.layouts import ConditionalCheckboxes, ConditionalCheckboxesQuestion +from django.db.models import TextChoices +from django.template.loader import render_to_string class ApplicationNameForm(BaseForm): @@ -35,55 +37,87 @@ class Layout: TITLE_AS_LABEL_FOR = "approval_choices" SUBMIT_BUTTON_TEXT = "Save and continue" - choice_list = [ - "initial discussions or promoting products", - "demonstration in the United Kingdom to overseas customers", - "demonstration overseas", - "training", - "through life support", - "supply", - ] - - def _get_choices(self, choice_list): - approval_choices = [] - approval_text = {} - - approval_list = choice_list - for result in approval_list: - key = "_".join(result.lower().split()) - choice = Choice(key, result) - if result == approval_list[-1]: - choice = Choice(key, result, divider="or") - approval_choices.append(choice) - approval_text[key] = result.capitalize() - return approval_choices, approval_text + class ApprovalTypeChoices(TextChoices): + INITIAL_DISCUSSIONS_OR_PROMOTING = ( + "INITIAL_DISCUSSIONS_OR_PROMOTING", + "Initial discussions or promoting products", + ) + DEMO_IN_UK_TO_OVERSEAS = "DEMO_IN_UK_TO_OVERSEAS", "Demonstration in the United Kingdom to overseas customers" + DEMO_OVERSEAS = "DEMO_OVERSEAS", "Demonstration overseas" + TRAINING = "TRAINING", "Training" + THROUGH_LIFE_SUPPORT = "THROUGH_LIFE_SUPPORT", "Through life support" + SUPPLY = "SUPPLY", "Supply" + + ApprovalTypeChoices = ( + TextChoice(ApprovalTypeChoices.INITIAL_DISCUSSIONS_OR_PROMOTING), + TextChoice(ApprovalTypeChoices.DEMO_IN_UK_TO_OVERSEAS), + TextChoice(ApprovalTypeChoices.DEMO_OVERSEAS), + TextChoice(ApprovalTypeChoices.TRAINING), + TextChoice(ApprovalTypeChoices.THROUGH_LIFE_SUPPORT), + TextChoice(ApprovalTypeChoices.SUPPLY), + ) approval_choices = forms.MultipleChoiceField( - label="", - required=False, - widget=forms.CheckboxSelectMultiple, - choices=(), + choices=ApprovalTypeChoices, + error_messages={ + "required": 'Select an approval choice"', + }, + widget=forms.CheckboxSelectMultiple(), ) - def __init__(self, *args, **kwargs): - - approval_choices, approval_text = self._get_choices(self.choice_list) - - self.conditional_checkbox_choices = ( - ConditionalCheckboxesQuestion(choices.label, choices.value) for choices in approval_choices - ) - - super().__init__(*args, **kwargs) + demonstration_in_uk_text = forms.MultipleChoiceField( + label="Explain what you are demonstrating and why", + choices=(), # set in __init__ + required=False, + # setting id for javascript to use + widget=forms.SelectMultiple( + attrs={ + "id": "demonstration_in_uk_text", + "data-module": "multi-select", + } + ), + ) - self.fields["approval_choices"].choices = approval_choices - for choices in approval_choices: - self.fields[choices.value] = forms.CharField( - widget=forms.Textarea(attrs={"rows": 3}), - label="Description", - required=False, - initial=approval_text[choices.value], - ) + demonstration_overseas_text = forms.MultipleChoiceField( + label="Explain what you are demonstrating and why", + choices=(), # set in __init__ + required=False, + # setting id for javascript to use + widget=forms.SelectMultiple( + attrs={ + "id": "demonstration_text", + "data-module": "multi-select", + } + ), + ) def get_layout_fields(self): - - return (ConditionalCheckboxes("approval_choices", *self.conditional_checkbox_choices),) + return ( + ConditionalCheckboxes( + "approval_choices", + ConditionalCheckboxesQuestion( + "Initial discussions or promoting products", + ), + ConditionalCheckboxesQuestion( + "Demonstration in the United Kingdom to overseas customers", + "demonstration_in_uk_text", + ), + ConditionalCheckboxesQuestion( + "Demonstration overseas", + "demonstration_overseas_text", + ), + ConditionalCheckboxesQuestion( + "Training", + ), + ConditionalCheckboxesQuestion( + "Through life support", + ), + ConditionalCheckboxesQuestion( + "Supply", + ), + ), + HTML.details( + "Help with exceptional circumstances", + render_to_string("applications/forms/help_with_approval_type.html"), + ), + ) From bd24725d91451df2d79c78269703233306f32649 Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Mon, 10 Feb 2025 12:14:35 +0000 Subject: [PATCH 24/60] simplify form --- exporter/f680/forms.py | 26 +++++--------------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/exporter/f680/forms.py b/exporter/f680/forms.py index 595c4b2611..8ff0ac0e54 100644 --- a/exporter/f680/forms.py +++ b/exporter/f680/forms.py @@ -31,7 +31,7 @@ def get_layout_fields(self): return [] -class ApprovalTypeForm(FieldsetForm): +class ApprovalTypeForm(BaseForm): class Layout: TITLE = "Select the types of approvals you need" TITLE_AS_LABEL_FOR = "approval_choices" @@ -65,30 +65,14 @@ class ApprovalTypeChoices(TextChoices): widget=forms.CheckboxSelectMultiple(), ) - demonstration_in_uk_text = forms.MultipleChoiceField( + demonstration_in_uk_text = forms.CharField( label="Explain what you are demonstrating and why", - choices=(), # set in __init__ - required=False, - # setting id for javascript to use - widget=forms.SelectMultiple( - attrs={ - "id": "demonstration_in_uk_text", - "data-module": "multi-select", - } - ), + widget=forms.Textarea(attrs={"rows": 5}), ) - demonstration_overseas_text = forms.MultipleChoiceField( + demonstration_overseas_text = forms.CharField( label="Explain what you are demonstrating and why", - choices=(), # set in __init__ - required=False, - # setting id for javascript to use - widget=forms.SelectMultiple( - attrs={ - "id": "demonstration_text", - "data-module": "multi-select", - } - ), + widget=forms.Textarea(attrs={"rows": 5}), ) def get_layout_fields(self): From dad986fb49a5e3238133773a5219400116fe755c Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Mon, 10 Feb 2025 12:22:33 +0000 Subject: [PATCH 25/60] simplify further --- exporter/f680/forms.py | 33 ++++++++++----------------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/exporter/f680/forms.py b/exporter/f680/forms.py index 8ff0ac0e54..883a0d3308 100644 --- a/exporter/f680/forms.py +++ b/exporter/f680/forms.py @@ -1,9 +1,8 @@ from django import forms from crispy_forms_gds.layout import HTML -from core.common.forms import BaseForm, FieldsetForm, TextChoice +from core.common.forms import BaseForm from core.forms.layouts import ConditionalCheckboxes, ConditionalCheckboxesQuestion -from django.db.models import TextChoices from django.template.loader import render_to_string @@ -37,28 +36,16 @@ class Layout: TITLE_AS_LABEL_FOR = "approval_choices" SUBMIT_BUTTON_TEXT = "Save and continue" - class ApprovalTypeChoices(TextChoices): - INITIAL_DISCUSSIONS_OR_PROMOTING = ( - "INITIAL_DISCUSSIONS_OR_PROMOTING", - "Initial discussions or promoting products", - ) - DEMO_IN_UK_TO_OVERSEAS = "DEMO_IN_UK_TO_OVERSEAS", "Demonstration in the United Kingdom to overseas customers" - DEMO_OVERSEAS = "DEMO_OVERSEAS", "Demonstration overseas" - TRAINING = "TRAINING", "Training" - THROUGH_LIFE_SUPPORT = "THROUGH_LIFE_SUPPORT", "Through life support" - SUPPLY = "SUPPLY", "Supply" - - ApprovalTypeChoices = ( - TextChoice(ApprovalTypeChoices.INITIAL_DISCUSSIONS_OR_PROMOTING), - TextChoice(ApprovalTypeChoices.DEMO_IN_UK_TO_OVERSEAS), - TextChoice(ApprovalTypeChoices.DEMO_OVERSEAS), - TextChoice(ApprovalTypeChoices.TRAINING), - TextChoice(ApprovalTypeChoices.THROUGH_LIFE_SUPPORT), - TextChoice(ApprovalTypeChoices.SUPPLY), - ) - + APPROVAL_CHOICES = [ + ("INITIAL_DISCUSSIONS_OR_PROMOTING", "Initial discussions or promoting products"), + ("DEMO_IN_UK_TO_OVERSEAS", "Demonstration in the United Kingdom to overseas customers"), + ("DEMO_OVERSEAS", "Demonstration overseas"), + ("TRAINING", "Training"), + ("THROUGH_LIFE_SUPPORT", "Through life support"), + ("SUPPLY", "Supply"), + ] approval_choices = forms.MultipleChoiceField( - choices=ApprovalTypeChoices, + choices=APPROVAL_CHOICES, error_messages={ "required": 'Select an approval choice"', }, From 1496e51a25b1a9e01e467d85e4f7e5422d187b9c Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Mon, 10 Feb 2025 12:52:38 +0000 Subject: [PATCH 26/60] Revert "simplify further" This reverts commit cc257cb3f135d8ba46acaf9d5e102664e7241743. --- exporter/f680/forms.py | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/exporter/f680/forms.py b/exporter/f680/forms.py index 883a0d3308..8ff0ac0e54 100644 --- a/exporter/f680/forms.py +++ b/exporter/f680/forms.py @@ -1,8 +1,9 @@ from django import forms from crispy_forms_gds.layout import HTML -from core.common.forms import BaseForm +from core.common.forms import BaseForm, FieldsetForm, TextChoice from core.forms.layouts import ConditionalCheckboxes, ConditionalCheckboxesQuestion +from django.db.models import TextChoices from django.template.loader import render_to_string @@ -36,16 +37,28 @@ class Layout: TITLE_AS_LABEL_FOR = "approval_choices" SUBMIT_BUTTON_TEXT = "Save and continue" - APPROVAL_CHOICES = [ - ("INITIAL_DISCUSSIONS_OR_PROMOTING", "Initial discussions or promoting products"), - ("DEMO_IN_UK_TO_OVERSEAS", "Demonstration in the United Kingdom to overseas customers"), - ("DEMO_OVERSEAS", "Demonstration overseas"), - ("TRAINING", "Training"), - ("THROUGH_LIFE_SUPPORT", "Through life support"), - ("SUPPLY", "Supply"), - ] + class ApprovalTypeChoices(TextChoices): + INITIAL_DISCUSSIONS_OR_PROMOTING = ( + "INITIAL_DISCUSSIONS_OR_PROMOTING", + "Initial discussions or promoting products", + ) + DEMO_IN_UK_TO_OVERSEAS = "DEMO_IN_UK_TO_OVERSEAS", "Demonstration in the United Kingdom to overseas customers" + DEMO_OVERSEAS = "DEMO_OVERSEAS", "Demonstration overseas" + TRAINING = "TRAINING", "Training" + THROUGH_LIFE_SUPPORT = "THROUGH_LIFE_SUPPORT", "Through life support" + SUPPLY = "SUPPLY", "Supply" + + ApprovalTypeChoices = ( + TextChoice(ApprovalTypeChoices.INITIAL_DISCUSSIONS_OR_PROMOTING), + TextChoice(ApprovalTypeChoices.DEMO_IN_UK_TO_OVERSEAS), + TextChoice(ApprovalTypeChoices.DEMO_OVERSEAS), + TextChoice(ApprovalTypeChoices.TRAINING), + TextChoice(ApprovalTypeChoices.THROUGH_LIFE_SUPPORT), + TextChoice(ApprovalTypeChoices.SUPPLY), + ) + approval_choices = forms.MultipleChoiceField( - choices=APPROVAL_CHOICES, + choices=ApprovalTypeChoices, error_messages={ "required": 'Select an approval choice"', }, From 3b825d5b168f7cea68e0d46b538373d9de4d75ab Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Mon, 10 Feb 2025 14:40:24 +0000 Subject: [PATCH 27/60] textboxes working dynamically --- exporter/f680/forms.py | 51 +++++++++++++++++------------------------- 1 file changed, 20 insertions(+), 31 deletions(-) diff --git a/exporter/f680/forms.py b/exporter/f680/forms.py index 8ff0ac0e54..5fc1b8a0b3 100644 --- a/exporter/f680/forms.py +++ b/exporter/f680/forms.py @@ -1,7 +1,8 @@ from django import forms from crispy_forms_gds.layout import HTML +from crispy_forms_gds.choices import Choice -from core.common.forms import BaseForm, FieldsetForm, TextChoice +from core.common.forms import BaseForm, TextChoice from core.forms.layouts import ConditionalCheckboxes, ConditionalCheckboxesQuestion from django.db.models import TextChoices from django.template.loader import render_to_string @@ -42,64 +43,52 @@ class ApprovalTypeChoices(TextChoices): "INITIAL_DISCUSSIONS_OR_PROMOTING", "Initial discussions or promoting products", ) - DEMO_IN_UK_TO_OVERSEAS = "DEMO_IN_UK_TO_OVERSEAS", "Demonstration in the United Kingdom to overseas customers" - DEMO_OVERSEAS = "DEMO_OVERSEAS", "Demonstration overseas" + demonstration_in_uk = ( + "demonstration_in_uk", + "Demonstration in the United Kingdom to overseas customers", + ) + demonstration_overseas = "demonstration_overseas", "Demonstration overseas" TRAINING = "TRAINING", "Training" THROUGH_LIFE_SUPPORT = "THROUGH_LIFE_SUPPORT", "Through life support" SUPPLY = "SUPPLY", "Supply" ApprovalTypeChoices = ( TextChoice(ApprovalTypeChoices.INITIAL_DISCUSSIONS_OR_PROMOTING), - TextChoice(ApprovalTypeChoices.DEMO_IN_UK_TO_OVERSEAS), - TextChoice(ApprovalTypeChoices.DEMO_OVERSEAS), + TextChoice(ApprovalTypeChoices.demonstration_in_uk), + TextChoice(ApprovalTypeChoices.demonstration_overseas), TextChoice(ApprovalTypeChoices.TRAINING), TextChoice(ApprovalTypeChoices.THROUGH_LIFE_SUPPORT), TextChoice(ApprovalTypeChoices.SUPPLY), ) approval_choices = forms.MultipleChoiceField( - choices=ApprovalTypeChoices, + choices=(), error_messages={ "required": 'Select an approval choice"', }, widget=forms.CheckboxSelectMultiple(), ) - demonstration_in_uk_text = forms.CharField( + demonstration_in_uk = forms.CharField( label="Explain what you are demonstrating and why", widget=forms.Textarea(attrs={"rows": 5}), ) - demonstration_overseas_text = forms.CharField( + demonstration_overseas = forms.CharField( label="Explain what you are demonstrating and why", widget=forms.Textarea(attrs={"rows": 5}), ) + def __init__(self, *args, **kwargs): + self.conditional_checkbox_choices = ( + ConditionalCheckboxesQuestion(choices.label, choices.value) for choices in self.ApprovalTypeChoices + ) + super().__init__(*args, **kwargs) + self.fields["approval_choices"].choices = self.ApprovalTypeChoices + def get_layout_fields(self): return ( - ConditionalCheckboxes( - "approval_choices", - ConditionalCheckboxesQuestion( - "Initial discussions or promoting products", - ), - ConditionalCheckboxesQuestion( - "Demonstration in the United Kingdom to overseas customers", - "demonstration_in_uk_text", - ), - ConditionalCheckboxesQuestion( - "Demonstration overseas", - "demonstration_overseas_text", - ), - ConditionalCheckboxesQuestion( - "Training", - ), - ConditionalCheckboxesQuestion( - "Through life support", - ), - ConditionalCheckboxesQuestion( - "Supply", - ), - ), + ConditionalCheckboxes("approval_choices", *self.conditional_checkbox_choices), HTML.details( "Help with exceptional circumstances", render_to_string("applications/forms/help_with_approval_type.html"), From 084bcd761fd2007ba2ee3628e64a64d6fc28add7 Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Mon, 10 Feb 2025 15:06:40 +0000 Subject: [PATCH 28/60] add framework --- exporter/f680/constants.py | 4 ++++ exporter/f680/views.py | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/exporter/f680/constants.py b/exporter/f680/constants.py index 17ba9c48cd..452db76305 100644 --- a/exporter/f680/constants.py +++ b/exporter/f680/constants.py @@ -1,2 +1,6 @@ class ApplicationFormSteps: APPLICATION_NAME = "APPLICATION_NAME" + + +class ApprovalTypeSteps: + APPROVAL_TYPE = "APPROVAL_TYPE" diff --git a/exporter/f680/views.py b/exporter/f680/views.py index 9bf45ba647..b5b8e6c646 100644 --- a/exporter/f680/views.py +++ b/exporter/f680/views.py @@ -12,10 +12,12 @@ from .constants import ( ApplicationFormSteps, + ApprovalTypeSteps, ) from .forms import ( ApplicationNameForm, ApplicationSubmissionForm, + ApprovalTypeForm, ) from .payloads import ( F680CreatePayloadBuilder, # PS-IGNORE @@ -68,6 +70,12 @@ def done(self, form_list, form_dict, **kwargs): return redirect(self.get_success_url(response_data["id"])) +class F680ApprovalTypeView(LoginRequiredMixin, F680FeatureRequiredMixin, BaseSessionWizardView): # PS-IGNORE + form_list = [ + (ApprovalTypeSteps.APPROVAL_TYPE, ApprovalTypeForm), + ] + + class F680ApplicationSummaryView(LoginRequiredMixin, F680FeatureRequiredMixin, FormView): # PS-IGNORE form_class = ApplicationSubmissionForm template_name = "f680/summary.html" # PS-IGNORE From ee492ac57388dad2bc168defea0543f0b2f14c07 Mon Sep 17 00:00:00 2001 From: Brendan Smith Date: Mon, 10 Feb 2025 15:37:32 +0000 Subject: [PATCH 29/60] Demonstrate adjusting BaseConditionalQuestion to accommodate optional embedded fields --- core/forms/layouts.py | 2 ++ exporter/f680/forms.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/core/forms/layouts.py b/core/forms/layouts.py index 6d77aa8ac5..59ae6378bd 100644 --- a/core/forms/layouts.py +++ b/core/forms/layouts.py @@ -37,6 +37,8 @@ def render(self, bound_field, form, form_style, context, template_pack=TEMPLATE_ conditional_content = "" for field in self.fields: + if field not in form.declared_fields: + continue conditional_content += render_field(field, form, form_style, context, template_pack=template_pack, **kwargs) context.update( diff --git a/exporter/f680/forms.py b/exporter/f680/forms.py index 5fc1b8a0b3..06beb287f3 100644 --- a/exporter/f680/forms.py +++ b/exporter/f680/forms.py @@ -1,6 +1,5 @@ from django import forms from crispy_forms_gds.layout import HTML -from crispy_forms_gds.choices import Choice from core.common.forms import BaseForm, TextChoice from core.forms.layouts import ConditionalCheckboxes, ConditionalCheckboxesQuestion @@ -72,11 +71,13 @@ class ApprovalTypeChoices(TextChoices): demonstration_in_uk = forms.CharField( label="Explain what you are demonstrating and why", widget=forms.Textarea(attrs={"rows": 5}), + required=False, ) demonstration_overseas = forms.CharField( label="Explain what you are demonstrating and why", widget=forms.Textarea(attrs={"rows": 5}), + required=False, ) def __init__(self, *args, **kwargs): From 9e5eec4b1cb00ac2901978e02d69a1824eef0a8d Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Mon, 10 Feb 2025 15:57:32 +0000 Subject: [PATCH 30/60] tidy --- exporter/f680/forms.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/exporter/f680/forms.py b/exporter/f680/forms.py index 06beb287f3..8f4396d6ac 100644 --- a/exporter/f680/forms.py +++ b/exporter/f680/forms.py @@ -1,10 +1,10 @@ from django import forms +from django.db.models import TextChoices +from django.template.loader import render_to_string from crispy_forms_gds.layout import HTML from core.common.forms import BaseForm, TextChoice from core.forms.layouts import ConditionalCheckboxes, ConditionalCheckboxesQuestion -from django.db.models import TextChoices -from django.template.loader import render_to_string class ApplicationNameForm(BaseForm): @@ -39,22 +39,22 @@ class Layout: class ApprovalTypeChoices(TextChoices): INITIAL_DISCUSSIONS_OR_PROMOTING = ( - "INITIAL_DISCUSSIONS_OR_PROMOTING", + "initial_discussion_or_promoting", "Initial discussions or promoting products", ) - demonstration_in_uk = ( + DEMONSTRATION_IN_THE_UK = ( "demonstration_in_uk", "Demonstration in the United Kingdom to overseas customers", ) - demonstration_overseas = "demonstration_overseas", "Demonstration overseas" - TRAINING = "TRAINING", "Training" - THROUGH_LIFE_SUPPORT = "THROUGH_LIFE_SUPPORT", "Through life support" - SUPPLY = "SUPPLY", "Supply" + DEMONSTRATION_OVERSEAS = "demonstration_overseas", "Demonstration overseas" + TRAINING = "training", "Training" + THROUGH_LIFE_SUPPORT = "through_life_support", "Through life support" + SUPPLY = "supply", "Supply" ApprovalTypeChoices = ( TextChoice(ApprovalTypeChoices.INITIAL_DISCUSSIONS_OR_PROMOTING), - TextChoice(ApprovalTypeChoices.demonstration_in_uk), - TextChoice(ApprovalTypeChoices.demonstration_overseas), + TextChoice(ApprovalTypeChoices.DEMONSTRATION_IN_THE_UK), + TextChoice(ApprovalTypeChoices.DEMONSTRATION_OVERSEAS), TextChoice(ApprovalTypeChoices.TRAINING), TextChoice(ApprovalTypeChoices.THROUGH_LIFE_SUPPORT), TextChoice(ApprovalTypeChoices.SUPPLY), From 65b0008efcf8ff9e35bc3086d8ed21349c7a2d31 Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Tue, 11 Feb 2025 11:21:50 +0000 Subject: [PATCH 31/60] add in approval details section in structure --- .../approval_details/__init__.py | 0 .../approval_details/constants.py | 2 + .../approval_details/forms.py | 74 +++++++++++++++++++ .../approval_details/urls.py | 10 +++ .../approval_details/views.py | 10 +++ exporter/f680/forms.py | 4 +- exporter/f680/urls.py | 4 + .../f680}/forms/help_with_approval_type.html | 0 exporter/templates/f680/summary.html | 20 +++++ 9 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 exporter/f680/application_sections/approval_details/__init__.py create mode 100644 exporter/f680/application_sections/approval_details/constants.py create mode 100644 exporter/f680/application_sections/approval_details/forms.py create mode 100644 exporter/f680/application_sections/approval_details/urls.py create mode 100644 exporter/f680/application_sections/approval_details/views.py rename exporter/{core/templates/applications => templates/f680}/forms/help_with_approval_type.html (100%) diff --git a/exporter/f680/application_sections/approval_details/__init__.py b/exporter/f680/application_sections/approval_details/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exporter/f680/application_sections/approval_details/constants.py b/exporter/f680/application_sections/approval_details/constants.py new file mode 100644 index 0000000000..8b460f0236 --- /dev/null +++ b/exporter/f680/application_sections/approval_details/constants.py @@ -0,0 +1,2 @@ +class FormSteps: + APPROVAL_TYPE = "APPROVAL_TYPE" diff --git a/exporter/f680/application_sections/approval_details/forms.py b/exporter/f680/application_sections/approval_details/forms.py new file mode 100644 index 0000000000..002c74e17b --- /dev/null +++ b/exporter/f680/application_sections/approval_details/forms.py @@ -0,0 +1,74 @@ +from django import forms +from django.db.models import TextChoices +from django.template.loader import render_to_string + +from crispy_forms_gds.layout.content import HTML + +from core.common.forms import BaseForm, TextChoice +from core.forms.layouts import ConditionalCheckboxes, ConditionalCheckboxesQuestion + + +class ApprovalTypeForm(BaseForm): + class Layout: + TITLE = "Select the types of approvals you need" + TITLE_AS_LABEL_FOR = "approval_choices" + SUBMIT_BUTTON_TEXT = "Save and continue" + + class ApprovalTypeChoices(TextChoices): + INITIAL_DISCUSSIONS_OR_PROMOTING = ( + "initial_discussion_or_promoting", + "Initial discussions or promoting products", + ) + DEMONSTRATION_IN_THE_UK = ( + "demonstration_in_uk", + "Demonstration in the United Kingdom to overseas customers", + ) + DEMONSTRATION_OVERSEAS = "demonstration_overseas", "Demonstration overseas" + TRAINING = "training", "Training" + THROUGH_LIFE_SUPPORT = "through_life_support", "Through life support" + SUPPLY = "supply", "Supply" + + ApprovalTypeChoices = ( + TextChoice(ApprovalTypeChoices.INITIAL_DISCUSSIONS_OR_PROMOTING), + TextChoice(ApprovalTypeChoices.DEMONSTRATION_IN_THE_UK), + TextChoice(ApprovalTypeChoices.DEMONSTRATION_OVERSEAS), + TextChoice(ApprovalTypeChoices.TRAINING), + TextChoice(ApprovalTypeChoices.THROUGH_LIFE_SUPPORT), + TextChoice(ApprovalTypeChoices.SUPPLY), + ) + + approval_choices = forms.MultipleChoiceField( + choices=(), + error_messages={ + "required": 'Select an approval choice"', + }, + widget=forms.CheckboxSelectMultiple(), + ) + + demonstration_in_uk = forms.CharField( + label="Explain what you are demonstrating and why", + widget=forms.Textarea(attrs={"rows": 5}), + required=False, + ) + + demonstration_overseas = forms.CharField( + label="Explain what you are demonstrating and why", + widget=forms.Textarea(attrs={"rows": 5}), + required=False, + ) + + def __init__(self, *args, **kwargs): + self.conditional_checkbox_choices = ( + ConditionalCheckboxesQuestion(choices.label, choices.value) for choices in self.ApprovalTypeChoices + ) + super().__init__(*args, **kwargs) + self.fields["approval_choices"].choices = self.ApprovalTypeChoices + + def get_layout_fields(self): + return ( + ConditionalCheckboxes("approval_choices", *self.conditional_checkbox_choices), + HTML.details( + "Help with exceptional circumstances", + render_to_string("f680/forms/help_with_approval_type.html"), + ), + ) diff --git a/exporter/f680/application_sections/approval_details/urls.py b/exporter/f680/application_sections/approval_details/urls.py new file mode 100644 index 0000000000..32351c694b --- /dev/null +++ b/exporter/f680/application_sections/approval_details/urls.py @@ -0,0 +1,10 @@ +from django.urls import path + +from . import views + + +app_name = "approval_details" + +urlpatterns = [ + path("type/", views.ApprovalTypeView.as_view(), name="type_wizard"), +] diff --git a/exporter/f680/application_sections/approval_details/views.py b/exporter/f680/application_sections/approval_details/views.py new file mode 100644 index 0000000000..bfb4dfabc6 --- /dev/null +++ b/exporter/f680/application_sections/approval_details/views.py @@ -0,0 +1,10 @@ +from exporter.f680.application_sections.views import F680ApplicationSectionWizard +from .constants import FormSteps +from .forms import ApprovalTypeForm + + +class ApprovalTypeView(F680ApplicationSectionWizard): + form_list = [ + (FormSteps.APPROVAL_TYPE, ApprovalTypeForm), + ] + section = "approval_details" diff --git a/exporter/f680/forms.py b/exporter/f680/forms.py index 8f4396d6ac..2ed20d5a55 100644 --- a/exporter/f680/forms.py +++ b/exporter/f680/forms.py @@ -63,7 +63,7 @@ class ApprovalTypeChoices(TextChoices): approval_choices = forms.MultipleChoiceField( choices=(), error_messages={ - "required": 'Select an approval choice"', + "required": "Select an approval choice", }, widget=forms.CheckboxSelectMultiple(), ) @@ -92,6 +92,6 @@ def get_layout_fields(self): ConditionalCheckboxes("approval_choices", *self.conditional_checkbox_choices), HTML.details( "Help with exceptional circumstances", - render_to_string("applications/forms/help_with_approval_type.html"), + render_to_string("f680/forms/help_with_approval_type.html"), ), ) diff --git a/exporter/f680/urls.py b/exporter/f680/urls.py index efea85419b..f4d740802c 100644 --- a/exporter/f680/urls.py +++ b/exporter/f680/urls.py @@ -13,4 +13,8 @@ "/general-application-details/", include("exporter.f680.application_sections.general_application_details.urls"), ), + path( + "/approval-details/", + include("exporter.f680.application_sections.approval_details.urls"), + ), ] diff --git a/exporter/core/templates/applications/forms/help_with_approval_type.html b/exporter/templates/f680/forms/help_with_approval_type.html similarity index 100% rename from exporter/core/templates/applications/forms/help_with_approval_type.html rename to exporter/templates/f680/forms/help_with_approval_type.html diff --git a/exporter/templates/f680/summary.html b/exporter/templates/f680/summary.html index edb384bef1..4974012ea6 100644 --- a/exporter/templates/f680/summary.html +++ b/exporter/templates/f680/summary.html @@ -41,6 +41,26 @@

+

+ 2. + Complete approval details +

+
    +
  • +
    + Approval type + {% if application.application.approval_details %} +
    + Completed +
    + {% else %} +
    + Not Started +
    + {% endif %} +
    +
  • +
From ef5776c9cdafe3ca8816d6a7639065f3d6b280dd Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Tue, 11 Feb 2025 15:35:33 +0000 Subject: [PATCH 32/60] test fix and remove unneeded --- exporter/f680/urls.py | 1 - exporter/f680/views.py | 37 +++++---- .../exporter/applications/views/test_f680s.py | 80 +++++++++---------- 3 files changed, 63 insertions(+), 55 deletions(-) diff --git a/exporter/f680/urls.py b/exporter/f680/urls.py index f4d740802c..6292db5920 100644 --- a/exporter/f680/urls.py +++ b/exporter/f680/urls.py @@ -8,7 +8,6 @@ urlpatterns = [ path("apply/", views.F680ApplicationCreateView.as_view(), name="apply"), path("/apply/", views.F680ApplicationSummaryView.as_view(), name="summary"), - path("/apply/", views.F680ApplicationSummaryView.as_view(), name="submit"), path( "/general-application-details/", include("exporter.f680.application_sections.general_application_details.urls"), diff --git a/exporter/f680/views.py b/exporter/f680/views.py index b5b8e6c646..15b03364f0 100644 --- a/exporter/f680/views.py +++ b/exporter/f680/views.py @@ -20,10 +20,10 @@ ApprovalTypeForm, ) from .payloads import ( - F680CreatePayloadBuilder, # PS-IGNORE + F680CreatePayloadBuilder, ) from .services import ( - post_f680_application, # PS-IGNORE + post_f680_application, get_f680_application, ) @@ -39,22 +39,22 @@ def dispatch(self, request, *args, **kwargs): return super().dispatch(request, *args, **kwargs) -class F680ApplicationCreateView(LoginRequiredMixin, F680FeatureRequiredMixin, BaseSessionWizardView): # PS-IGNORE +class F680ApplicationCreateView(LoginRequiredMixin, F680FeatureRequiredMixin, BaseSessionWizardView): form_list = [ (ApplicationFormSteps.APPLICATION_NAME, ApplicationNameForm), ] @expect_status( HTTPStatus.CREATED, - "Error creating F680 application", # PS-IGNORE + "Error creating F680 application", "Unexpected error creating F680 application", ) - def post_f680_application(self, data): # PS-IGNORE - return post_f680_application(self.request, data) # PS-IGNORE + def post_f680_application(self, data): + return post_f680_application(self.request, data) def get_success_url(self, application_id): return reverse( - "f680:summary", # PS-IGNORE + "f680:summary", kwargs={ "pk": application_id, }, @@ -62,27 +62,36 @@ def get_success_url(self, application_id): def get_payload(self, form_dict): - return F680CreatePayloadBuilder().build(form_dict) # /PS-IGNORE + return F680CreatePayloadBuilder().build(form_dict) def done(self, form_list, form_dict, **kwargs): data = self.get_payload(form_dict) - response_data, _ = self.post_f680_application(data) # PS-IGNORE + response_data, _ = self.post_f680_application(data) return redirect(self.get_success_url(response_data["id"])) -class F680ApprovalTypeView(LoginRequiredMixin, F680FeatureRequiredMixin, BaseSessionWizardView): # PS-IGNORE +class F680ApprovalTypeView(LoginRequiredMixin, F680FeatureRequiredMixin, BaseSessionWizardView): form_list = [ (ApprovalTypeSteps.APPROVAL_TYPE, ApprovalTypeForm), ] -class F680ApplicationSummaryView(LoginRequiredMixin, F680FeatureRequiredMixin, FormView): # PS-IGNORE +class F680ApplicationSummaryView(LoginRequiredMixin, F680FeatureRequiredMixin, FormView): form_class = ApplicationSubmissionForm - template_name = "f680/summary.html" # PS-IGNORE + template_name = "f680/summary.html" def setup(self, request, *args, **kwargs): super().setup(request, *args, **kwargs) - self.application, _ = get_f680_application(request, kwargs["pk"]) # PS-IGNORE + self.application, _ = self.get_f680_application(kwargs["pk"]) + + @expect_status( + HTTPStatus.OK, + "Error retrieving F680 application", + "Unexpected error retrieving F680 application", + reraise_404=True, + ) + def get_f680_application(self, pk): + return get_f680_application(self.request, pk) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -91,4 +100,4 @@ def get_context_data(self, **kwargs): return context def get_success_url(self): - return reverse("f680:summary", kwargs={"pk": self.application["id"]}) # PS-IGNORE + return reverse("f680:summary", kwargs={"pk": self.application["id"]}) diff --git a/unit_tests/exporter/applications/views/test_f680s.py b/unit_tests/exporter/applications/views/test_f680s.py index 4ddba7fefe..1114f376df 100644 --- a/unit_tests/exporter/applications/views/test_f680s.py +++ b/unit_tests/exporter/applications/views/test_f680s.py @@ -18,42 +18,42 @@ def authorized_client(authorized_client_factory, mock_exporter_user): @pytest.fixture -def f680_apply_url(): # PS-IGNORE - return reverse("f680:apply") # PS-IGNORE +def f680_apply_url(): + return reverse("f680:apply") @pytest.fixture -def f680_summary_url_with_application(data_f680_case): # PS-IGNORE - return reverse("f680:summary", kwargs={"pk": data_f680_case["id"]}) # PS-IGNORE +def f680_summary_url_with_application(data_f680_case): + return reverse("f680:summary", kwargs={"pk": data_f680_case["id"]}) @pytest.fixture def post_to_step(post_to_step_factory, f680_apply_url, mock_application_post): - return post_to_step_factory(f680_apply_url) # PS-IGNORE + return post_to_step_factory(f680_apply_url) @pytest.fixture -def mock_f680_application_get(requests_mock, data_f680_case): # PS-IGNORE - application_id = data_f680_case["id"] # PS-IGNORE - url = client._build_absolute_uri(f"/exporter/f680/application/{application_id}/") # PS-IGNORE - return requests_mock.get(url=url, json=data_f680_case) # PS-IGNORE +def mock_f680_application_get(requests_mock, data_f680_case): + application_id = data_f680_case["id"] + url = client._build_absolute_uri(f"/exporter/f680/application/{application_id}/") + return requests_mock.get(url=url, json=data_f680_case) @pytest.fixture -def mock_application_post(requests_mock, data_f680_case): # PS-IGNORE - application = data_f680_case # PS-IGNORE - url = client._build_absolute_uri(f"/exporter/f680/application/") # PS-IGNORE +def mock_application_post(requests_mock, data_f680_case): + application = data_f680_case + url = client._build_absolute_uri(f"/exporter/f680/application/") return requests_mock.post(url=url, json=application, status_code=201) @pytest.fixture() -def set_f680_feature_flag(settings): # PS-IGNORE - settings.FEATURE_FLAG_ALLOW_F680 = True # PS-IGNORE +def set_f680_feature_flag(settings): + settings.FEATURE_FLAG_ALLOW_F680 = True class TestApplyForLicenceQuestionsClass: - def test_triage_f680_apply_redirect_success(self, authorized_client, f680_apply_url): # PS-IGNORE - response = authorized_client.post(reverse("apply_for_a_licence:f680_questions")) # PS-IGNORE + def test_triage_f680_apply_redirect_success(self, authorized_client, f680_apply_url): + response = authorized_client.post(reverse("apply_for_a_licence:f680_questions")) assert response.status_code == 302 assert response.url == f680_apply_url @@ -62,11 +62,11 @@ class TestF680ApplicationCreateView: def test_get_create_f680_view_success( self, authorized_client, - f680_apply_url, # PS-IGNORE - mock_f680_application_get, # PS-IGNORE + f680_apply_url, + mock_f680_application_get, set_f680_feature_flag, ): - response = authorized_client.get(f680_apply_url) # PS-IGNORE + response = authorized_client.get(f680_apply_url) assert isinstance(response.context["form"], ApplicationNameForm) soup = BeautifulSoup(response.content, "html.parser") @@ -75,10 +75,10 @@ def test_get_create_f680_view_success( def test_get_create_f680_view_fail_with_feature_flag_off( self, authorized_client, - f680_apply_url, # PS-IGNORE - mock_f680_application_get, # PS-IGNORE + f680_apply_url, + mock_f680_application_get, ): - response = authorized_client.get(f680_apply_url) # PS-IGNORE + response = authorized_client.get(f680_apply_url) assert response.context[0].get("title") == "Forbidden" assert ( "You are not authorised to use the F680 Security Clearance application feature" @@ -88,14 +88,14 @@ def test_get_create_f680_view_fail_with_feature_flag_off( def test_post_to_create_f680_name_step_success( self, authorized_client, - f680_apply_url, # PS-IGNORE + f680_apply_url, post_to_step, f680_summary_url_with_application, set_f680_feature_flag, ): response = post_to_step( ApplicationFormSteps.APPLICATION_NAME, - {"name": "F680 Test"}, # PS-IGNORE + {"name": "F680 Test"}, ) assert response.status_code == 302 @@ -104,13 +104,13 @@ def test_post_to_create_f680_name_step_success( def test_post_to_create_f680_name_step_invalid_data( self, authorized_client, - f680_apply_url, # PS-IGNORE + f680_apply_url, post_to_step, set_f680_feature_flag, ): response = post_to_step( ApplicationFormSteps.APPLICATION_NAME, - {"name": ""}, # PS-IGNORE + {"name": ""}, ) assert isinstance(response.context["form"], ApplicationNameForm) @@ -122,18 +122,18 @@ class TestF680ApplicationSummaryView: def test_get_f680_summary_view_success( self, authorized_client, - f680_summary_url_with_application, # PS-IGNORE - mock_f680_application_get, # PS-IGNORE + f680_summary_url_with_application, + mock_f680_application_get, set_f680_feature_flag, ): - response = authorized_client.get(f680_summary_url_with_application) # PS-IGNORE + response = authorized_client.get(f680_summary_url_with_application) assert isinstance(response.context["form"], ApplicationSubmissionForm) - assertTemplateUsed(response, "f680/summary.html") # PS-IGNORE + assertTemplateUsed(response, "f680/summary.html") content = BeautifulSoup(response.content, "html.parser") heading_element = content.find("h1", class_="govuk-heading-l govuk-!-margin-bottom-2") - assert heading_element.string.strip() == "F680 Application" # PS-IGNORE + assert heading_element.string.strip() == "F680 Application" def test_get_f680_summary_view_case_not_found( self, @@ -153,10 +153,10 @@ def test_get_f680_summary_view_case_not_found( def test_get_f680_summary_view_fail_with_feature_flag_off( self, authorized_client, - f680_summary_url_with_application, # PS-IGNORE - mock_f680_application_get, # PS-IGNORE + f680_summary_url_with_application, + mock_f680_application_get, ): - response = authorized_client.get(f680_summary_url_with_application) # PS-IGNORE + response = authorized_client.get(f680_summary_url_with_application) assert response.status_code == 200 assert response.context[0].get("title") == "Forbidden" assert ( @@ -167,25 +167,25 @@ def test_get_f680_summary_view_fail_with_feature_flag_off( def test_post_f680_submission_form_success( self, authorized_client, - f680_summary_url_with_application, # PS-IGNORE - mock_f680_application_get, # PS-IGNORE + f680_summary_url_with_application, + mock_f680_application_get, set_f680_feature_flag, ): response = authorized_client.post( - f680_summary_url_with_application, # PS-IGNORE + f680_summary_url_with_application, ) assert response.status_code == 302 - assert response.url == f680_summary_url_with_application # PS-IGNORE + assert response.url == f680_summary_url_with_application def test_post_f680_submission_form_fail_with_feature_flag_off( self, authorized_client, - f680_summary_url_with_application, # PS-IGNORE + f680_summary_url_with_application, mock_f680_application_get, ): response = authorized_client.post( - f680_summary_url_with_application, # PS-IGNORE + f680_summary_url_with_application, ) assert response.context[0].get("title") == "Forbidden" From 04fb04d9129ebff5b77e5c3ff8f2ce4285e04a4c Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Tue, 11 Feb 2025 18:51:53 +0000 Subject: [PATCH 33/60] overwrite existing conditional checkbox class --- core/forms/layouts.py | 29 +++++++++++++++++-- .../approval_details/forms.py | 6 ++-- exporter/f680/forms.py | 6 ++-- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/core/forms/layouts.py b/core/forms/layouts.py index 59ae6378bd..6b41adf46b 100644 --- a/core/forms/layouts.py +++ b/core/forms/layouts.py @@ -37,8 +37,6 @@ def render(self, bound_field, form, form_style, context, template_pack=TEMPLATE_ conditional_content = "" for field in self.fields: - if field not in form.declared_fields: - continue conditional_content += render_field(field, form, form_style, context, template_pack=template_pack, **kwargs) context.update( @@ -92,11 +90,38 @@ class ConditionalCheckboxesQuestion(BaseConditionalQuestion): template = "%s/layout/conditional_checkboxes_question.html" +class F680ConditionalCheckboxesQuestion(ConditionalCheckboxesQuestion): + def render(self, bound_field, form, form_style, context, template_pack=TEMPLATE_PACK, **kwargs): + template = self.get_template_name(template_pack) + + mapped_choices = {choice[1]: choice for choice in bound_field.field.choices} + value = self.value + choice = mapped_choices[value] + position = list(mapped_choices.keys()).index(self.value) + + conditional_content = "" + for field in self.fields: + if field not in form.declared_fields: + continue + conditional_content += render_field(field, form, form_style, context, template_pack=template_pack, **kwargs) + + context.update( + {"choice": choice, "field": bound_field, "position": position, "conditional_content": conditional_content} + ) + + return render_to_string(template, context.flatten()) + + class ConditionalCheckboxes(BaseConditional): question_class = ConditionalCheckboxesQuestion template = "%s/layout/conditional_checkboxes.html" +class F680ConditionalCheckboxes(BaseConditional): + question_class = F680ConditionalCheckboxesQuestion + template = "%s/layout/conditional_checkboxes.html" + + class ConditionalCheckbox(TemplateNameMixin): template = "%s/layout/conditional_checkbox.html" diff --git a/exporter/f680/application_sections/approval_details/forms.py b/exporter/f680/application_sections/approval_details/forms.py index 002c74e17b..77f8171117 100644 --- a/exporter/f680/application_sections/approval_details/forms.py +++ b/exporter/f680/application_sections/approval_details/forms.py @@ -5,7 +5,7 @@ from crispy_forms_gds.layout.content import HTML from core.common.forms import BaseForm, TextChoice -from core.forms.layouts import ConditionalCheckboxes, ConditionalCheckboxesQuestion +from core.forms.layouts import F680ConditionalCheckboxes, F680ConditionalCheckboxesQuestion class ApprovalTypeForm(BaseForm): @@ -59,14 +59,14 @@ class ApprovalTypeChoices(TextChoices): def __init__(self, *args, **kwargs): self.conditional_checkbox_choices = ( - ConditionalCheckboxesQuestion(choices.label, choices.value) for choices in self.ApprovalTypeChoices + F680ConditionalCheckboxesQuestion(choices.label, choices.value) for choices in self.ApprovalTypeChoices ) super().__init__(*args, **kwargs) self.fields["approval_choices"].choices = self.ApprovalTypeChoices def get_layout_fields(self): return ( - ConditionalCheckboxes("approval_choices", *self.conditional_checkbox_choices), + F680ConditionalCheckboxes("approval_choices", *self.conditional_checkbox_choices), HTML.details( "Help with exceptional circumstances", render_to_string("f680/forms/help_with_approval_type.html"), diff --git a/exporter/f680/forms.py b/exporter/f680/forms.py index 2ed20d5a55..0432d93344 100644 --- a/exporter/f680/forms.py +++ b/exporter/f680/forms.py @@ -4,7 +4,7 @@ from crispy_forms_gds.layout import HTML from core.common.forms import BaseForm, TextChoice -from core.forms.layouts import ConditionalCheckboxes, ConditionalCheckboxesQuestion +from core.forms.layouts import F680ConditionalCheckboxes, F680ConditionalCheckboxesQuestion class ApplicationNameForm(BaseForm): @@ -82,14 +82,14 @@ class ApprovalTypeChoices(TextChoices): def __init__(self, *args, **kwargs): self.conditional_checkbox_choices = ( - ConditionalCheckboxesQuestion(choices.label, choices.value) for choices in self.ApprovalTypeChoices + F680ConditionalCheckboxesQuestion(choices.label, choices.value) for choices in self.ApprovalTypeChoices ) super().__init__(*args, **kwargs) self.fields["approval_choices"].choices = self.ApprovalTypeChoices def get_layout_fields(self): return ( - ConditionalCheckboxes("approval_choices", *self.conditional_checkbox_choices), + F680ConditionalCheckboxes("approval_choices", *self.conditional_checkbox_choices), HTML.details( "Help with exceptional circumstances", render_to_string("f680/forms/help_with_approval_type.html"), From 335b98118d5bf19ab4854e5099f82366f71deef8 Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Tue, 11 Feb 2025 19:45:18 +0000 Subject: [PATCH 34/60] remove unneeded --- core/forms/layouts.py | 7 ++-- exporter/f680/forms.py | 72 +----------------------------------------- exporter/f680/views.py | 8 ----- 3 files changed, 5 insertions(+), 82 deletions(-) diff --git a/core/forms/layouts.py b/core/forms/layouts.py index 6b41adf46b..a1e79ee273 100644 --- a/core/forms/layouts.py +++ b/core/forms/layouts.py @@ -101,9 +101,10 @@ def render(self, bound_field, form, form_style, context, template_pack=TEMPLATE_ conditional_content = "" for field in self.fields: - if field not in form.declared_fields: - continue - conditional_content += render_field(field, form, form_style, context, template_pack=template_pack, **kwargs) + if field in form.declared_fields: + conditional_content += render_field( + field, form, form_style, context, template_pack=template_pack, **kwargs + ) context.update( {"choice": choice, "field": bound_field, "position": position, "conditional_content": conditional_content} diff --git a/exporter/f680/forms.py b/exporter/f680/forms.py index 0432d93344..d9e1118874 100644 --- a/exporter/f680/forms.py +++ b/exporter/f680/forms.py @@ -1,10 +1,6 @@ from django import forms -from django.db.models import TextChoices -from django.template.loader import render_to_string -from crispy_forms_gds.layout import HTML -from core.common.forms import BaseForm, TextChoice -from core.forms.layouts import F680ConditionalCheckboxes, F680ConditionalCheckboxesQuestion +from core.common.forms import BaseForm class ApplicationNameForm(BaseForm): @@ -29,69 +25,3 @@ class Layout: def get_layout_fields(self): return [] - - -class ApprovalTypeForm(BaseForm): - class Layout: - TITLE = "Select the types of approvals you need" - TITLE_AS_LABEL_FOR = "approval_choices" - SUBMIT_BUTTON_TEXT = "Save and continue" - - class ApprovalTypeChoices(TextChoices): - INITIAL_DISCUSSIONS_OR_PROMOTING = ( - "initial_discussion_or_promoting", - "Initial discussions or promoting products", - ) - DEMONSTRATION_IN_THE_UK = ( - "demonstration_in_uk", - "Demonstration in the United Kingdom to overseas customers", - ) - DEMONSTRATION_OVERSEAS = "demonstration_overseas", "Demonstration overseas" - TRAINING = "training", "Training" - THROUGH_LIFE_SUPPORT = "through_life_support", "Through life support" - SUPPLY = "supply", "Supply" - - ApprovalTypeChoices = ( - TextChoice(ApprovalTypeChoices.INITIAL_DISCUSSIONS_OR_PROMOTING), - TextChoice(ApprovalTypeChoices.DEMONSTRATION_IN_THE_UK), - TextChoice(ApprovalTypeChoices.DEMONSTRATION_OVERSEAS), - TextChoice(ApprovalTypeChoices.TRAINING), - TextChoice(ApprovalTypeChoices.THROUGH_LIFE_SUPPORT), - TextChoice(ApprovalTypeChoices.SUPPLY), - ) - - approval_choices = forms.MultipleChoiceField( - choices=(), - error_messages={ - "required": "Select an approval choice", - }, - widget=forms.CheckboxSelectMultiple(), - ) - - demonstration_in_uk = forms.CharField( - label="Explain what you are demonstrating and why", - widget=forms.Textarea(attrs={"rows": 5}), - required=False, - ) - - demonstration_overseas = forms.CharField( - label="Explain what you are demonstrating and why", - widget=forms.Textarea(attrs={"rows": 5}), - required=False, - ) - - def __init__(self, *args, **kwargs): - self.conditional_checkbox_choices = ( - F680ConditionalCheckboxesQuestion(choices.label, choices.value) for choices in self.ApprovalTypeChoices - ) - super().__init__(*args, **kwargs) - self.fields["approval_choices"].choices = self.ApprovalTypeChoices - - def get_layout_fields(self): - return ( - F680ConditionalCheckboxes("approval_choices", *self.conditional_checkbox_choices), - HTML.details( - "Help with exceptional circumstances", - render_to_string("f680/forms/help_with_approval_type.html"), - ), - ) diff --git a/exporter/f680/views.py b/exporter/f680/views.py index 15b03364f0..0df71ebbe6 100644 --- a/exporter/f680/views.py +++ b/exporter/f680/views.py @@ -12,12 +12,10 @@ from .constants import ( ApplicationFormSteps, - ApprovalTypeSteps, ) from .forms import ( ApplicationNameForm, ApplicationSubmissionForm, - ApprovalTypeForm, ) from .payloads import ( F680CreatePayloadBuilder, @@ -70,12 +68,6 @@ def done(self, form_list, form_dict, **kwargs): return redirect(self.get_success_url(response_data["id"])) -class F680ApprovalTypeView(LoginRequiredMixin, F680FeatureRequiredMixin, BaseSessionWizardView): - form_list = [ - (ApprovalTypeSteps.APPROVAL_TYPE, ApprovalTypeForm), - ] - - class F680ApplicationSummaryView(LoginRequiredMixin, F680FeatureRequiredMixin, FormView): form_class = ApplicationSubmissionForm template_name = "f680/summary.html" From 32c5593a48a69f1fbbe8ec59b0899ae4db4da5e4 Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Wed, 12 Feb 2025 10:07:45 +0000 Subject: [PATCH 35/60] add tests --- .../approval_details/forms.py | 2 +- .../approval_details/tests/__init__.py | 0 .../approval_details/tests/test_views.py | 265 ++++++++++++++++++ 3 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 exporter/f680/application_sections/approval_details/tests/__init__.py create mode 100644 exporter/f680/application_sections/approval_details/tests/test_views.py diff --git a/exporter/f680/application_sections/approval_details/forms.py b/exporter/f680/application_sections/approval_details/forms.py index 77f8171117..068889d793 100644 --- a/exporter/f680/application_sections/approval_details/forms.py +++ b/exporter/f680/application_sections/approval_details/forms.py @@ -40,7 +40,7 @@ class ApprovalTypeChoices(TextChoices): approval_choices = forms.MultipleChoiceField( choices=(), error_messages={ - "required": 'Select an approval choice"', + "required": "Select an approval choice", }, widget=forms.CheckboxSelectMultiple(), ) diff --git a/exporter/f680/application_sections/approval_details/tests/__init__.py b/exporter/f680/application_sections/approval_details/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exporter/f680/application_sections/approval_details/tests/test_views.py b/exporter/f680/application_sections/approval_details/tests/test_views.py new file mode 100644 index 0000000000..c052079941 --- /dev/null +++ b/exporter/f680/application_sections/approval_details/tests/test_views.py @@ -0,0 +1,265 @@ +import pytest + +from django.urls import reverse + +from core import client + +from ..forms import ApprovalTypeForm +from ..constants import FormSteps + + +@pytest.fixture() +def unset_f680_feature_flag(settings): + settings.FEATURE_FLAG_ALLOW_F680 = False + + +@pytest.fixture(autouse=True) +def setup(mock_exporter_user_me, settings): + settings.FEATURE_FLAG_ALLOW_F680 = True + + +@pytest.fixture +def missing_application_id(): + return "6bb0828c-1520-4624-b729-7f3e6e5b9f5d" + + +@pytest.fixture +def missing_f680_application_wizard_url(missing_application_id): + return reverse( + "f680:approval_details:type_wizard", + kwargs={"pk": missing_application_id}, + ) + + +@pytest.fixture +def f680_application_wizard_url(data_f680_case): + return reverse( + "f680:approval_details:type_wizard", + kwargs={"pk": data_f680_case["id"]}, + ) + + +@pytest.fixture +def mock_f680_application_get_404(requests_mock, missing_application_id): + url = client._build_absolute_uri(f"/exporter/f680/application/{missing_application_id}/") + return requests_mock.get(url=url, json={}, status_code=404) + + +@pytest.fixture +def mock_f680_application_get(requests_mock, data_f680_case): + application_id = data_f680_case["id"] + url = client._build_absolute_uri(f"/exporter/f680/application/{application_id}/") + return requests_mock.get(url=url, json=data_f680_case) + + +@pytest.fixture +def mock_f680_application_get_existing_data(requests_mock, data_f680_case): + data_f680_case["application"] = { + "general_application_details": { + "answers": { + "name": "my first F680", + "is_exceptional_circumstances": True, + "exceptional_circumstances_date": "2090-01-01", + "exceptional_circumstances_reason": "some reason", + }, + "questions": { + "name": "What is the name of the application?", + "is_exceptional_circumstances": "Are there exceptional circumstances?", + }, + }, + "approval_details": { + "answers": { + "approval_choices": [ + "initial_discussion_or_promoting", + "demonstration_in_uk", + "demonstration_overseas", + "training", + "through_life_support", + "supply", + ], + "demonstration_in_uk": "Test text 1", + "demonstration_overseas": "Test text 2", + }, + "questions": { + "approval_choices": None, + "demonstration_in_uk": "Explain what you are demonstrating and why", + "demonstration_overseas": "Explain what you are demonstrating and why", + }, + }, + } + application_id = data_f680_case["id"] + url = client._build_absolute_uri(f"/exporter/f680/application/{application_id}/") + return requests_mock.get(url=url, json=data_f680_case) + + +@pytest.fixture +def mock_patch_f680_application(requests_mock, data_f680_case): + application_id = data_f680_case["id"] + url = client._build_absolute_uri(f"/exporter/f680/application/{application_id}/") + return requests_mock.patch(url=url, json=data_f680_case) + + +@pytest.fixture +def post_to_step(post_to_step_factory, f680_application_wizard_url): + return post_to_step_factory(f680_application_wizard_url) + + +@pytest.fixture +def goto_step(goto_step_factory, f680_application_wizard_url): + return goto_step_factory(f680_application_wizard_url) + + +class TestApprovalDetailsView: + + def test_GET_no_application_404( + self, + authorized_client, + missing_f680_application_wizard_url, + mock_f680_application_get_404, + ): + response = authorized_client.get(missing_f680_application_wizard_url) + assert response.status_code == 404 + + def test_GET_success( + self, + authorized_client, + mock_f680_application_get, + f680_application_wizard_url, + ): + response = authorized_client.get(f680_application_wizard_url) + assert response.status_code == 200 + assert isinstance(response.context["form"], ApprovalTypeForm) + + def test_GET_no_feature_flag_forbidden( + self, + authorized_client, + mock_f680_application_get, + f680_application_wizard_url, + unset_f680_feature_flag, + ): + response = authorized_client.get(f680_application_wizard_url) + assert response.status_code == 200 + assert response.context["title"] == "Forbidden" + + def test_POST_to_step_success( + self, + post_to_step, + goto_step, + mock_f680_application_get, + ): + goto_step(FormSteps.APPROVAL_TYPE) + response = post_to_step( + FormSteps.APPROVAL_TYPE, + { + "approval_details": { + "answers": { + "approval_choices": [ + "initial_discussion_or_promoting", + "demonstration_in_uk", + "demonstration_overseas", + "training", + "through_life_support", + "supply", + ], + "demonstration_in_uk": "Test Text", + "demonstration_overseas": "Test Text", + }, + "questions": { + "approval_choices": None, + "demonstration_in_uk": "Explain what you are demonstrating and why", + "demonstration_overseas": "Explain what you are demonstrating and why", + }, + }, + }, + ) + assert response.status_code == 200 + assert isinstance(response.context["form"], ApprovalTypeForm) + + def test_POST_to_step_validation_error( + self, + post_to_step, + goto_step, + mock_f680_application_get, + ): + goto_step(FormSteps.APPROVAL_TYPE) + response = post_to_step( + FormSteps.APPROVAL_TYPE, + {}, + ) + assert response.status_code == 200 + assert "Select an approval choice" in response.context["form"]["approval_choices"].errors + + def test_GET_with_existing_data_success( + self, + authorized_client, + mock_f680_application_get_existing_data, + f680_application_wizard_url, + ): + response = authorized_client.get(f680_application_wizard_url) + assert response.status_code == 200 + assert isinstance(response.context["form"], ApprovalTypeForm) + assert response.context["form"]["approval_choices"].initial == [ + "initial_discussion_or_promoting", + "demonstration_in_uk", + "demonstration_overseas", + "training", + "through_life_support", + "supply", + ] + assert response.context["form"]["demonstration_in_uk"].initial == "Test text 1" + assert response.context["form"]["demonstration_overseas"].initial == "Test text 2" + + # def test_POST_submit_wizard_success( + # self, post_to_step, goto_step, mock_f680_application_get, mock_patch_f680_application + # ): + # response = post_to_step( + # FormSteps.APPROVAL_TYPE, + # { + # "approval_details": { + # "answers": { + # "approval_choices": [ + # "initial_discussion_or_promoting", + # "demonstration_in_uk", + # "demonstration_overseas", + # "training", + # "through_life_support", + # "supply", + # ], + # "demonstration_in_uk": "Test Text", + # "demonstration_overseas": "Test Text", + # }, + # "questions": { + # "approval_choices": None, + # "demonstration_in_uk": "Explain what you are demonstrating and why", + # "demonstration_overseas": "Explain what you are demonstrating and why", + # }, + # }, + # }, + # ) + # breakpoint() + # assert response.status_code == 302 + # assert mock_patch_f680_application.called_once + # assert mock_patch_f680_application.last_request.json() == { + # "application": { + # "name": "F680 Test 1", + # "approval_details": { + # "answers": { + # "approval_choices": [ + # "initial_discussion_or_promoting", + # "demonstration_in_uk", + # "demonstration_overseas", + # "training", + # "through_life_support", + # "supply", + # ], + # "demonstration_in_uk": "fghfdhfg", + # "demonstration_overseas": "fghdfgh", + # }, + # "questions": { + # "approval_choices": None, + # "demonstration_in_uk": "Explain what you are demonstrating and why", + # "demonstration_overseas": "Explain what you are demonstrating and why", + # }, + # }, + # } + # } From 82a4723538f81b35feb35295b57d296396f74589 Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Wed, 12 Feb 2025 10:22:38 +0000 Subject: [PATCH 36/60] fortmat --- unit_tests/exporter/applications/views/test_f680s.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/unit_tests/exporter/applications/views/test_f680s.py b/unit_tests/exporter/applications/views/test_f680s.py index a95b32b87d..db6877a2fc 100644 --- a/unit_tests/exporter/applications/views/test_f680s.py +++ b/unit_tests/exporter/applications/views/test_f680s.py @@ -55,7 +55,8 @@ def mock_application_post(requests_mock, data_f680_case): def set_f680_feature_flag(settings): settings.FEATURE_FLAG_ALLOW_F680 = True - @pytest.fixture() + +@pytest.fixture() def unset_f680_feature_flag(settings): settings.FEATURE_FLAG_ALLOW_F680 = False From 90404319e43befede4cd5d87c95b59cca3c8eec7 Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Wed, 12 Feb 2025 10:26:45 +0000 Subject: [PATCH 37/60] tidy --- .../general_application_details/tests/test_views.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/exporter/f680/application_sections/general_application_details/tests/test_views.py b/exporter/f680/application_sections/general_application_details/tests/test_views.py index ed90d9b41d..66c7dfa3bb 100644 --- a/exporter/f680/application_sections/general_application_details/tests/test_views.py +++ b/exporter/f680/application_sections/general_application_details/tests/test_views.py @@ -1,8 +1,4 @@ import pytest - - -from django.urls import reverse - from datetime import datetime, timedelta from django.urls import reverse @@ -231,7 +227,6 @@ def test_POST_to_step_validation_error( for field_name, error in expected_errors.items(): assert response.context["form"][field_name].errors == error - @freeze_time("2026-11-30") def test_POST_submit_wizard_success( self, post_to_step, goto_step, mock_f680_application_get, mock_patch_f680_application @@ -275,7 +270,6 @@ def test_POST_submit_wizard_success( } } - @pytest.mark.parametrize( "step, expected_form, expected_initial", ( @@ -307,4 +301,3 @@ def test_GET_with_existing_data_success( assert isinstance(response.context["form"], expected_form) for key, expected_value in expected_initial.items(): assert response.context["form"][key].initial == expected_value - From fc3d948e65f613e4d9144f982adfa5f3e43ec083 Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Wed, 12 Feb 2025 10:27:25 +0000 Subject: [PATCH 38/60] tidy --- .../application_sections/general_application_details/forms.py | 1 + 1 file changed, 1 insertion(+) diff --git a/exporter/f680/application_sections/general_application_details/forms.py b/exporter/f680/application_sections/general_application_details/forms.py index 905051ab28..ce46e3cfb2 100644 --- a/exporter/f680/application_sections/general_application_details/forms.py +++ b/exporter/f680/application_sections/general_application_details/forms.py @@ -13,6 +13,7 @@ RelativeDeltaDateValidator, ) + class ApplicationNameForm(BaseForm): class Layout: TITLE = "Name the application" From 9a02e10e75380a4ed61df8b9e54b1610abadfbd4 Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Wed, 12 Feb 2025 11:08:10 +0000 Subject: [PATCH 39/60] test changes --- .../approval_details/tests/test_views.py | 146 +++++++----------- 1 file changed, 59 insertions(+), 87 deletions(-) diff --git a/exporter/f680/application_sections/approval_details/tests/test_views.py b/exporter/f680/application_sections/approval_details/tests/test_views.py index c052079941..cdf80a7c23 100644 --- a/exporter/f680/application_sections/approval_details/tests/test_views.py +++ b/exporter/f680/application_sections/approval_details/tests/test_views.py @@ -141,39 +141,39 @@ def test_GET_no_feature_flag_forbidden( assert response.status_code == 200 assert response.context["title"] == "Forbidden" - def test_POST_to_step_success( - self, - post_to_step, - goto_step, - mock_f680_application_get, - ): - goto_step(FormSteps.APPROVAL_TYPE) - response = post_to_step( - FormSteps.APPROVAL_TYPE, - { - "approval_details": { - "answers": { - "approval_choices": [ - "initial_discussion_or_promoting", - "demonstration_in_uk", - "demonstration_overseas", - "training", - "through_life_support", - "supply", - ], - "demonstration_in_uk": "Test Text", - "demonstration_overseas": "Test Text", - }, - "questions": { - "approval_choices": None, - "demonstration_in_uk": "Explain what you are demonstrating and why", - "demonstration_overseas": "Explain what you are demonstrating and why", - }, - }, - }, - ) - assert response.status_code == 200 - assert isinstance(response.context["form"], ApprovalTypeForm) + # def test_POST_to_step_success( + # self, + # post_to_step, + # goto_step, + # mock_f680_application_get, + # ): + # goto_step(FormSteps.APPROVAL_TYPE) + # response = post_to_step( + # FormSteps.APPROVAL_TYPE, + # { + # "approval_details": { + # "answers": { + # "approval_choices": [ + # "initial_discussion_or_promoting", + # "demonstration_in_uk", + # "demonstration_overseas", + # "training", + # "through_life_support", + # "supply", + # ], + # "demonstration_in_uk": "Test Text", + # "demonstration_overseas": "Test Text", + # }, + # "questions": { + # "approval_choices": None, + # "demonstration_in_uk": "Explain what you are demonstrating and why", + # "demonstration_overseas": "Explain what you are demonstrating and why", + # }, + # }, + # }, + # ) + # assert response.status_code == 200 + # assert isinstance(response.context["form"], ApprovalTypeForm) def test_POST_to_step_validation_error( self, @@ -209,57 +209,29 @@ def test_GET_with_existing_data_success( assert response.context["form"]["demonstration_in_uk"].initial == "Test text 1" assert response.context["form"]["demonstration_overseas"].initial == "Test text 2" - # def test_POST_submit_wizard_success( - # self, post_to_step, goto_step, mock_f680_application_get, mock_patch_f680_application - # ): - # response = post_to_step( - # FormSteps.APPROVAL_TYPE, - # { - # "approval_details": { - # "answers": { - # "approval_choices": [ - # "initial_discussion_or_promoting", - # "demonstration_in_uk", - # "demonstration_overseas", - # "training", - # "through_life_support", - # "supply", - # ], - # "demonstration_in_uk": "Test Text", - # "demonstration_overseas": "Test Text", - # }, - # "questions": { - # "approval_choices": None, - # "demonstration_in_uk": "Explain what you are demonstrating and why", - # "demonstration_overseas": "Explain what you are demonstrating and why", - # }, - # }, - # }, - # ) - # breakpoint() - # assert response.status_code == 302 - # assert mock_patch_f680_application.called_once - # assert mock_patch_f680_application.last_request.json() == { - # "application": { - # "name": "F680 Test 1", - # "approval_details": { - # "answers": { - # "approval_choices": [ - # "initial_discussion_or_promoting", - # "demonstration_in_uk", - # "demonstration_overseas", - # "training", - # "through_life_support", - # "supply", - # ], - # "demonstration_in_uk": "fghfdhfg", - # "demonstration_overseas": "fghdfgh", - # }, - # "questions": { - # "approval_choices": None, - # "demonstration_in_uk": "Explain what you are demonstrating and why", - # "demonstration_overseas": "Explain what you are demonstrating and why", - # }, - # }, - # } - # } + def test_POST_approval_type_and_submit_wizard_success( + self, post_to_step, goto_step, mock_f680_application_get, mock_patch_f680_application + ): + response = post_to_step( + FormSteps.APPROVAL_TYPE, + {"approval_choices": ["training", "supply"]}, + ) + assert response.status_code == 302 + assert mock_patch_f680_application.called_once + assert mock_patch_f680_application.last_request.json() == { + "application": { + "name": "F680 Test 1", + "approval_details": { + "answers": { + "approval_choices": ["training", "supply"], + "demonstration_in_uk": "", + "demonstration_overseas": "", + }, + "questions": { + "approval_choices": None, + "demonstration_in_uk": "Explain what you are demonstrating and why", + "demonstration_overseas": "Explain what you are demonstrating and why", + }, + }, + } + } From a751fbdf4b2e0638298d333d1c0063cae444c23d Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Wed, 12 Feb 2025 11:12:06 +0000 Subject: [PATCH 40/60] remove unused test --- .../approval_details/tests/test_views.py | 34 ------------------- 1 file changed, 34 deletions(-) diff --git a/exporter/f680/application_sections/approval_details/tests/test_views.py b/exporter/f680/application_sections/approval_details/tests/test_views.py index cdf80a7c23..4aae12b47d 100644 --- a/exporter/f680/application_sections/approval_details/tests/test_views.py +++ b/exporter/f680/application_sections/approval_details/tests/test_views.py @@ -141,40 +141,6 @@ def test_GET_no_feature_flag_forbidden( assert response.status_code == 200 assert response.context["title"] == "Forbidden" - # def test_POST_to_step_success( - # self, - # post_to_step, - # goto_step, - # mock_f680_application_get, - # ): - # goto_step(FormSteps.APPROVAL_TYPE) - # response = post_to_step( - # FormSteps.APPROVAL_TYPE, - # { - # "approval_details": { - # "answers": { - # "approval_choices": [ - # "initial_discussion_or_promoting", - # "demonstration_in_uk", - # "demonstration_overseas", - # "training", - # "through_life_support", - # "supply", - # ], - # "demonstration_in_uk": "Test Text", - # "demonstration_overseas": "Test Text", - # }, - # "questions": { - # "approval_choices": None, - # "demonstration_in_uk": "Explain what you are demonstrating and why", - # "demonstration_overseas": "Explain what you are demonstrating and why", - # }, - # }, - # }, - # ) - # assert response.status_code == 200 - # assert isinstance(response.context["form"], ApprovalTypeForm) - def test_POST_to_step_validation_error( self, post_to_step, From 4810175ef4475d5f0ecb97a0fdb5376d9bdf846b Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Wed, 12 Feb 2025 11:16:49 +0000 Subject: [PATCH 41/60] remove unused and change order --- .../approval_details/tests/test_views.py | 66 ++++++++----------- 1 file changed, 27 insertions(+), 39 deletions(-) diff --git a/exporter/f680/application_sections/approval_details/tests/test_views.py b/exporter/f680/application_sections/approval_details/tests/test_views.py index 4aae12b47d..b44a16329d 100644 --- a/exporter/f680/application_sections/approval_details/tests/test_views.py +++ b/exporter/f680/application_sections/approval_details/tests/test_views.py @@ -55,18 +55,6 @@ def mock_f680_application_get(requests_mock, data_f680_case): @pytest.fixture def mock_f680_application_get_existing_data(requests_mock, data_f680_case): data_f680_case["application"] = { - "general_application_details": { - "answers": { - "name": "my first F680", - "is_exceptional_circumstances": True, - "exceptional_circumstances_date": "2090-01-01", - "exceptional_circumstances_reason": "some reason", - }, - "questions": { - "name": "What is the name of the application?", - "is_exceptional_circumstances": "Are there exceptional circumstances?", - }, - }, "approval_details": { "answers": { "approval_choices": [ @@ -141,6 +129,33 @@ def test_GET_no_feature_flag_forbidden( assert response.status_code == 200 assert response.context["title"] == "Forbidden" + def test_POST_approval_type_and_submit_wizard_success( + self, post_to_step, goto_step, mock_f680_application_get, mock_patch_f680_application + ): + response = post_to_step( + FormSteps.APPROVAL_TYPE, + {"approval_choices": ["training", "supply"]}, + ) + assert response.status_code == 302 + assert mock_patch_f680_application.called_once + assert mock_patch_f680_application.last_request.json() == { + "application": { + "name": "F680 Test 1", + "approval_details": { + "answers": { + "approval_choices": ["training", "supply"], + "demonstration_in_uk": "", + "demonstration_overseas": "", + }, + "questions": { + "approval_choices": None, + "demonstration_in_uk": "Explain what you are demonstrating and why", + "demonstration_overseas": "Explain what you are demonstrating and why", + }, + }, + } + } + def test_POST_to_step_validation_error( self, post_to_step, @@ -174,30 +189,3 @@ def test_GET_with_existing_data_success( ] assert response.context["form"]["demonstration_in_uk"].initial == "Test text 1" assert response.context["form"]["demonstration_overseas"].initial == "Test text 2" - - def test_POST_approval_type_and_submit_wizard_success( - self, post_to_step, goto_step, mock_f680_application_get, mock_patch_f680_application - ): - response = post_to_step( - FormSteps.APPROVAL_TYPE, - {"approval_choices": ["training", "supply"]}, - ) - assert response.status_code == 302 - assert mock_patch_f680_application.called_once - assert mock_patch_f680_application.last_request.json() == { - "application": { - "name": "F680 Test 1", - "approval_details": { - "answers": { - "approval_choices": ["training", "supply"], - "demonstration_in_uk": "", - "demonstration_overseas": "", - }, - "questions": { - "approval_choices": None, - "demonstration_in_uk": "Explain what you are demonstrating and why", - "demonstration_overseas": "Explain what you are demonstrating and why", - }, - }, - } - } From 09191e0f29ffa6cfc1e82bc748a792f8d941753f Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Wed, 12 Feb 2025 11:29:27 +0000 Subject: [PATCH 42/60] add extra textbox field --- .../f680/application_sections/approval_details/forms.py | 7 +++++++ .../approval_details/tests/test_views.py | 2 ++ 2 files changed, 9 insertions(+) diff --git a/exporter/f680/application_sections/approval_details/forms.py b/exporter/f680/application_sections/approval_details/forms.py index 068889d793..2e9f5c5af7 100644 --- a/exporter/f680/application_sections/approval_details/forms.py +++ b/exporter/f680/application_sections/approval_details/forms.py @@ -57,6 +57,12 @@ class ApprovalTypeChoices(TextChoices): required=False, ) + approval_details_text = forms.CharField( + label="Provide details about what you're seeking approval to do", + widget=forms.Textarea(attrs={"rows": 5}), + required=False, + ) + def __init__(self, *args, **kwargs): self.conditional_checkbox_choices = ( F680ConditionalCheckboxesQuestion(choices.label, choices.value) for choices in self.ApprovalTypeChoices @@ -67,6 +73,7 @@ def __init__(self, *args, **kwargs): def get_layout_fields(self): return ( F680ConditionalCheckboxes("approval_choices", *self.conditional_checkbox_choices), + "approval_details_text", HTML.details( "Help with exceptional circumstances", render_to_string("f680/forms/help_with_approval_type.html"), diff --git a/exporter/f680/application_sections/approval_details/tests/test_views.py b/exporter/f680/application_sections/approval_details/tests/test_views.py index b44a16329d..a767cc851c 100644 --- a/exporter/f680/application_sections/approval_details/tests/test_views.py +++ b/exporter/f680/application_sections/approval_details/tests/test_views.py @@ -146,11 +146,13 @@ def test_POST_approval_type_and_submit_wizard_success( "approval_choices": ["training", "supply"], "demonstration_in_uk": "", "demonstration_overseas": "", + "approval_details_text": "", }, "questions": { "approval_choices": None, "demonstration_in_uk": "Explain what you are demonstrating and why", "demonstration_overseas": "Explain what you are demonstrating and why", + "approval_details_text": "Provide details about what you're seeking approval to do", }, }, } From 43bd92667110f73bf6bf68eb57daf816cb593323 Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Wed, 12 Feb 2025 11:35:26 +0000 Subject: [PATCH 43/60] add help text --- exporter/f680/application_sections/approval_details/forms.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/exporter/f680/application_sections/approval_details/forms.py b/exporter/f680/application_sections/approval_details/forms.py index 2e9f5c5af7..6d1e22ca2c 100644 --- a/exporter/f680/application_sections/approval_details/forms.py +++ b/exporter/f680/application_sections/approval_details/forms.py @@ -47,12 +47,14 @@ class ApprovalTypeChoices(TextChoices): demonstration_in_uk = forms.CharField( label="Explain what you are demonstrating and why", + help_text="Explain what materials will be involved and if you'll use a substitute product", widget=forms.Textarea(attrs={"rows": 5}), required=False, ) demonstration_overseas = forms.CharField( label="Explain what you are demonstrating and why", + help_text="Explain what materials will be involved and if you'll use a substitute product", widget=forms.Textarea(attrs={"rows": 5}), required=False, ) From 23c12729f5f96ec30aa8627ac702522e779744b5 Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Wed, 12 Feb 2025 11:41:17 +0000 Subject: [PATCH 44/60] remove unnecessary --- unit_tests/exporter/applications/views/test_f680s.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/unit_tests/exporter/applications/views/test_f680s.py b/unit_tests/exporter/applications/views/test_f680s.py index db6877a2fc..8bd6d6c305 100644 --- a/unit_tests/exporter/applications/views/test_f680s.py +++ b/unit_tests/exporter/applications/views/test_f680s.py @@ -51,11 +51,6 @@ def mock_application_post(requests_mock, data_f680_case): return requests_mock.post(url=url, json=application, status_code=201) -@pytest.fixture() -def set_f680_feature_flag(settings): - settings.FEATURE_FLAG_ALLOW_F680 = True - - @pytest.fixture() def unset_f680_feature_flag(settings): settings.FEATURE_FLAG_ALLOW_F680 = False @@ -74,7 +69,6 @@ def test_get_create_f680_view_success( authorized_client, f680_apply_url, mock_f680_application_get, - set_f680_feature_flag, ): response = authorized_client.get(f680_apply_url) @@ -133,7 +127,6 @@ def test_get_f680_summary_view_success( authorized_client, f680_summary_url_with_application, mock_f680_application_get, - set_f680_feature_flag, ): response = authorized_client.get(f680_summary_url_with_application) @@ -148,7 +141,6 @@ def test_get_f680_summary_view_case_not_found( self, authorized_client, requests_mock, - set_f680_feature_flag, ): app_pk = str(uuid4()) @@ -179,7 +171,6 @@ def test_post_f680_submission_form_success( authorized_client, f680_summary_url_with_application, mock_f680_application_get, - set_f680_feature_flag, ): response = authorized_client.post( f680_summary_url_with_application, From a4c9bc89520b3a5eac81cb670de53a750a43fa68 Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Wed, 12 Feb 2025 12:54:21 +0000 Subject: [PATCH 45/60] add in form structure --- .../additional_information/__init__.py | 0 .../additional_information/constants.py | 2 ++ .../additional_information/forms.py | 10 ++++++++++ .../additional_information/tests/__init__.py | 0 .../additional_information/urls.py | 10 ++++++++++ .../additional_information/views.py | 11 +++++++++++ exporter/f680/urls.py | 4 ++++ 7 files changed, 37 insertions(+) create mode 100644 exporter/f680/application_sections/additional_information/__init__.py create mode 100644 exporter/f680/application_sections/additional_information/constants.py create mode 100644 exporter/f680/application_sections/additional_information/forms.py create mode 100644 exporter/f680/application_sections/additional_information/tests/__init__.py create mode 100644 exporter/f680/application_sections/additional_information/urls.py create mode 100644 exporter/f680/application_sections/additional_information/views.py diff --git a/exporter/f680/application_sections/additional_information/__init__.py b/exporter/f680/application_sections/additional_information/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exporter/f680/application_sections/additional_information/constants.py b/exporter/f680/application_sections/additional_information/constants.py new file mode 100644 index 0000000000..a1af1b72ab --- /dev/null +++ b/exporter/f680/application_sections/additional_information/constants.py @@ -0,0 +1,2 @@ +class FormSteps: + NOTES_FOR_CASEWORKER = "NOTES_FOR_CASEWORKER" diff --git a/exporter/f680/application_sections/additional_information/forms.py b/exporter/f680/application_sections/additional_information/forms.py new file mode 100644 index 0000000000..53191f716d --- /dev/null +++ b/exporter/f680/application_sections/additional_information/forms.py @@ -0,0 +1,10 @@ +from core.common.forms import BaseForm + + +class NotesForCaseOfficerForm(BaseForm): + class Layout: + TITLE = "" + SUBMIT_BUTTON_TEXT = "Submit" + + def get_layout_fields(self): + return [] diff --git a/exporter/f680/application_sections/additional_information/tests/__init__.py b/exporter/f680/application_sections/additional_information/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exporter/f680/application_sections/additional_information/urls.py b/exporter/f680/application_sections/additional_information/urls.py new file mode 100644 index 0000000000..4b8cb9daf7 --- /dev/null +++ b/exporter/f680/application_sections/additional_information/urls.py @@ -0,0 +1,10 @@ +from django.urls import path + +from . import views + + +app_name = "additional_information" + +urlpatterns = [ + path("notes/", views.NotesForCaseOfficersView.as_view(), name="notes_wizard"), +] diff --git a/exporter/f680/application_sections/additional_information/views.py b/exporter/f680/application_sections/additional_information/views.py new file mode 100644 index 0000000000..e5e4aa4d7b --- /dev/null +++ b/exporter/f680/application_sections/additional_information/views.py @@ -0,0 +1,11 @@ +from exporter.f680.application_sections.views import F680ApplicationSectionWizard + +from .constants import FormSteps +from .forms import NotesForCaseOfficerForm + + +class NotesForCaseOfficersView(F680ApplicationSectionWizard): + form_list = [ + (FormSteps.NOTES_FOR_CASEWORKER, NotesForCaseOfficerForm), + ] + section = "additional_information" diff --git a/exporter/f680/urls.py b/exporter/f680/urls.py index 8b35360176..61775571b7 100644 --- a/exporter/f680/urls.py +++ b/exporter/f680/urls.py @@ -12,4 +12,8 @@ "/general-application-details/", include("exporter.f680.application_sections.general_application_details.urls"), ), + path( + "/additional-information/", + include("exporter.f680.application_sections.additional_information.urls"), + ), ] From d6e7e2ddc0537c11666df2a0b758f6120149a4c8 Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Wed, 12 Feb 2025 12:58:58 +0000 Subject: [PATCH 46/60] hooked up to summary page --- exporter/templates/f680/summary.html | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/exporter/templates/f680/summary.html b/exporter/templates/f680/summary.html index edb384bef1..794a0e9990 100644 --- a/exporter/templates/f680/summary.html +++ b/exporter/templates/f680/summary.html @@ -41,6 +41,26 @@

+

+ 4. + Additional Information +

+
    +
  • +
    + Notes for case officers + {% if application.application.general_application_details %} +
    + Completed +
    + {% else %} +
    + Not Started +
    + {% endif %} +
    +
  • +
From 7abf7a811998957440dadbf11305a3fa9ed66957 Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Wed, 12 Feb 2025 14:53:23 +0000 Subject: [PATCH 47/60] display form field --- .../additional_information/forms.py | 16 +++++++++++++--- .../additional_information/views.py | 13 +++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/exporter/f680/application_sections/additional_information/forms.py b/exporter/f680/application_sections/additional_information/forms.py index 53191f716d..7629edcf68 100644 --- a/exporter/f680/application_sections/additional_information/forms.py +++ b/exporter/f680/application_sections/additional_information/forms.py @@ -1,10 +1,20 @@ +from django import forms + from core.common.forms import BaseForm class NotesForCaseOfficerForm(BaseForm): class Layout: - TITLE = "" - SUBMIT_BUTTON_TEXT = "Submit" + TITLE = "Notes" + SUBMIT_BUTTON_TEXT = "Save and continue" + + note = forms.CharField( + label="Add note", + widget=forms.Textarea(attrs={"cols": "80"}), + ) + + def get_context(self): + return super().get_context() def get_layout_fields(self): - return [] + return ("note",) diff --git a/exporter/f680/application_sections/additional_information/views.py b/exporter/f680/application_sections/additional_information/views.py index e5e4aa4d7b..ca02c63fb1 100644 --- a/exporter/f680/application_sections/additional_information/views.py +++ b/exporter/f680/application_sections/additional_information/views.py @@ -9,3 +9,16 @@ class NotesForCaseOfficersView(F680ApplicationSectionWizard): (FormSteps.NOTES_FOR_CASEWORKER, NotesForCaseOfficerForm), ] section = "additional_information" + # template_name = "applications/case-notes.html" + + # def get_context_data(self, form, **kwargs): + # context = super().get_context_data(form, **kwargs) + # notes = get_case_notes(self.request, self.application["id"])["case_notes"] + # return { + # **context, + # "application": self.application, + # "notes": notes, + # "post_url": reverse_lazy("applications:notes", kwargs={"pk": self.application["id"]}), + # "error": kwargs.get("error"), + # "text": kwargs.get("text", ""), + # } From e7d3047bc8803f5e4b13ac67b841b27185292bfc Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Wed, 12 Feb 2025 14:58:40 +0000 Subject: [PATCH 48/60] add custom widget --- .../application_sections/additional_information/forms.py | 6 +++++- exporter/templates/applications/f680case-notes.html | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 exporter/templates/applications/f680case-notes.html diff --git a/exporter/f680/application_sections/additional_information/forms.py b/exporter/f680/application_sections/additional_information/forms.py index 7629edcf68..dbecd2da12 100644 --- a/exporter/f680/application_sections/additional_information/forms.py +++ b/exporter/f680/application_sections/additional_information/forms.py @@ -3,6 +3,10 @@ from core.common.forms import BaseForm +class NoteWidget(forms.Textarea): + template_name = "applications/f680case-notes.html" # /PS-IGNORE + + class NotesForCaseOfficerForm(BaseForm): class Layout: TITLE = "Notes" @@ -10,7 +14,7 @@ class Layout: note = forms.CharField( label="Add note", - widget=forms.Textarea(attrs={"cols": "80"}), + widget=NoteWidget(attrs={"cols": "80"}), ) def get_context(self): diff --git a/exporter/templates/applications/f680case-notes.html b/exporter/templates/applications/f680case-notes.html new file mode 100644 index 0000000000..9c851c7e64 --- /dev/null +++ b/exporter/templates/applications/f680case-notes.html @@ -0,0 +1 @@ + From 5136b0db4a00409da5f0b3c9ed70189ebf8b597e Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Wed, 12 Feb 2025 15:56:33 +0000 Subject: [PATCH 49/60] simplify form to just use json field --- .../additional_information/forms.py | 9 +-------- .../additional_information/views.py | 13 ------------- exporter/templates/f680/summary.html | 2 +- 3 files changed, 2 insertions(+), 22 deletions(-) diff --git a/exporter/f680/application_sections/additional_information/forms.py b/exporter/f680/application_sections/additional_information/forms.py index dbecd2da12..bcfffe0c5b 100644 --- a/exporter/f680/application_sections/additional_information/forms.py +++ b/exporter/f680/application_sections/additional_information/forms.py @@ -3,10 +3,6 @@ from core.common.forms import BaseForm -class NoteWidget(forms.Textarea): - template_name = "applications/f680case-notes.html" # /PS-IGNORE - - class NotesForCaseOfficerForm(BaseForm): class Layout: TITLE = "Notes" @@ -14,11 +10,8 @@ class Layout: note = forms.CharField( label="Add note", - widget=NoteWidget(attrs={"cols": "80"}), + widget=forms.Textarea(attrs={"cols": "80"}), ) - def get_context(self): - return super().get_context() - def get_layout_fields(self): return ("note",) diff --git a/exporter/f680/application_sections/additional_information/views.py b/exporter/f680/application_sections/additional_information/views.py index ca02c63fb1..e5e4aa4d7b 100644 --- a/exporter/f680/application_sections/additional_information/views.py +++ b/exporter/f680/application_sections/additional_information/views.py @@ -9,16 +9,3 @@ class NotesForCaseOfficersView(F680ApplicationSectionWizard): (FormSteps.NOTES_FOR_CASEWORKER, NotesForCaseOfficerForm), ] section = "additional_information" - # template_name = "applications/case-notes.html" - - # def get_context_data(self, form, **kwargs): - # context = super().get_context_data(form, **kwargs) - # notes = get_case_notes(self.request, self.application["id"])["case_notes"] - # return { - # **context, - # "application": self.application, - # "notes": notes, - # "post_url": reverse_lazy("applications:notes", kwargs={"pk": self.application["id"]}), - # "error": kwargs.get("error"), - # "text": kwargs.get("text", ""), - # } diff --git a/exporter/templates/f680/summary.html b/exporter/templates/f680/summary.html index 794a0e9990..6250d07222 100644 --- a/exporter/templates/f680/summary.html +++ b/exporter/templates/f680/summary.html @@ -49,7 +49,7 @@

  • Notes for case officers - {% if application.application.general_application_details %} + {% if application.application.additional_information.answers.note %}
    Completed
    From 19f72d590aa239ad817cca6b28de57ea204af19e Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Wed, 12 Feb 2025 15:58:00 +0000 Subject: [PATCH 50/60] remove unused template --- exporter/templates/applications/f680case-notes.html | 1 - 1 file changed, 1 deletion(-) delete mode 100644 exporter/templates/applications/f680case-notes.html diff --git a/exporter/templates/applications/f680case-notes.html b/exporter/templates/applications/f680case-notes.html deleted file mode 100644 index 9c851c7e64..0000000000 --- a/exporter/templates/applications/f680case-notes.html +++ /dev/null @@ -1 +0,0 @@ - From de96450059c8fad3565b64db857e8468def6e482 Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Wed, 12 Feb 2025 16:16:30 +0000 Subject: [PATCH 51/60] add tests --- .../tests/test_views.py | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 exporter/f680/application_sections/additional_information/tests/test_views.py diff --git a/exporter/f680/application_sections/additional_information/tests/test_views.py b/exporter/f680/application_sections/additional_information/tests/test_views.py new file mode 100644 index 0000000000..684350ab3d --- /dev/null +++ b/exporter/f680/application_sections/additional_information/tests/test_views.py @@ -0,0 +1,153 @@ +import pytest + +from django.urls import reverse + +from core import client + +from ..forms import NotesForCaseOfficerForm +from ..constants import FormSteps + + +@pytest.fixture() +def unset_f680_feature_flag(settings): + settings.FEATURE_FLAG_ALLOW_F680 = False + + +@pytest.fixture(autouse=True) +def setup(mock_exporter_user_me, settings): + settings.FEATURE_FLAG_ALLOW_F680 = True + + +@pytest.fixture +def missing_application_id(): + return "6bb0828c-1520-4624-b729-7f3e6e5b9f5d" + + +@pytest.fixture +def missing_f680_application_wizard_url(missing_application_id): + return reverse( + "f680:additional_information:notes_wizard", + kwargs={"pk": missing_application_id}, + ) + + +@pytest.fixture +def f680_application_wizard_url(data_f680_case): + return reverse( + "f680:additional_information:notes_wizard", + kwargs={"pk": data_f680_case["id"]}, + ) + + +@pytest.fixture +def mock_f680_application_get_404(requests_mock, missing_application_id): + url = client._build_absolute_uri(f"/exporter/f680/application/{missing_application_id}/") + return requests_mock.get(url=url, json={}, status_code=404) + + +@pytest.fixture +def mock_f680_application_get(requests_mock, data_f680_case): + application_id = data_f680_case["id"] + url = client._build_absolute_uri(f"/exporter/f680/application/{application_id}/") + return requests_mock.get(url=url, json=data_f680_case) + + +@pytest.fixture +def mock_f680_application_get_existing_data(requests_mock, data_f680_case): + data_f680_case["application"] = { + "additional_information": {"answers": {"note": "Some note text"}, "questions": {"note": "Add note"}} + } + application_id = data_f680_case["id"] + url = client._build_absolute_uri(f"/exporter/f680/application/{application_id}/") + return requests_mock.get(url=url, json=data_f680_case) + + +@pytest.fixture +def mock_patch_f680_application(requests_mock, data_f680_case): + application_id = data_f680_case["id"] + url = client._build_absolute_uri(f"/exporter/f680/application/{application_id}/") + return requests_mock.patch(url=url, json=data_f680_case) + + +@pytest.fixture +def post_to_step(post_to_step_factory, f680_application_wizard_url): + return post_to_step_factory(f680_application_wizard_url) + + +@pytest.fixture +def goto_step(goto_step_factory, f680_application_wizard_url): + return goto_step_factory(f680_application_wizard_url) + + +class TestApprovalDetailsView: + + def test_GET_no_application_404( + self, + authorized_client, + missing_f680_application_wizard_url, + mock_f680_application_get_404, + ): + response = authorized_client.get(missing_f680_application_wizard_url) + assert response.status_code == 404 + + def test_GET_success( + self, + authorized_client, + mock_f680_application_get, + f680_application_wizard_url, + ): + response = authorized_client.get(f680_application_wizard_url) + assert response.status_code == 200 + assert isinstance(response.context["form"], NotesForCaseOfficerForm) + + def test_GET_no_feature_flag_forbidden( + self, + authorized_client, + mock_f680_application_get, + f680_application_wizard_url, + unset_f680_feature_flag, + ): + response = authorized_client.get(f680_application_wizard_url) + assert response.status_code == 200 + assert response.context["title"] == "Forbidden" + + def test_POST_approval_type_and_submit_wizard_success( + self, post_to_step, goto_step, mock_f680_application_get, mock_patch_f680_application + ): + response = post_to_step( + FormSteps.NOTES_FOR_CASEWORKER, + {"note": "Some information"}, + ) + assert response.status_code == 302 + assert mock_patch_f680_application.called_once + assert mock_patch_f680_application.last_request.json() == { + "application": { + "name": "F680 Test 1", + "additional_information": {"answers": {"note": "Some information"}, "questions": {"note": "Add note"}}, + } + } + + def test_POST_to_step_validation_error( + self, + post_to_step, + goto_step, + mock_f680_application_get, + ): + goto_step(FormSteps.NOTES_FOR_CASEWORKER) + response = post_to_step( + FormSteps.NOTES_FOR_CASEWORKER, + {}, + ) + assert response.status_code == 200 + assert "This field is required." in response.context["form"]["note"].errors + + def test_GET_with_existing_data_success( + self, + authorized_client, + mock_f680_application_get_existing_data, + f680_application_wizard_url, + ): + response = authorized_client.get(f680_application_wizard_url) + assert response.status_code == 200 + assert isinstance(response.context["form"], NotesForCaseOfficerForm) + assert response.context["form"]["note"].initial == "Some note text" From 822557b119c24fae7f0ad2d03a141480f22fa8ee Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Wed, 12 Feb 2025 16:42:08 +0000 Subject: [PATCH 52/60] rename testclass --- .../additional_information/tests/test_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporter/f680/application_sections/additional_information/tests/test_views.py b/exporter/f680/application_sections/additional_information/tests/test_views.py index 684350ab3d..8f131b745b 100644 --- a/exporter/f680/application_sections/additional_information/tests/test_views.py +++ b/exporter/f680/application_sections/additional_information/tests/test_views.py @@ -79,7 +79,7 @@ def goto_step(goto_step_factory, f680_application_wizard_url): return goto_step_factory(f680_application_wizard_url) -class TestApprovalDetailsView: +class TestAdditionalInformationView: def test_GET_no_application_404( self, From c0c8cc9b6a4268776ad3c753abaf6ccdb4230215 Mon Sep 17 00:00:00 2001 From: Tomos Williams Date: Thu, 13 Feb 2025 15:51:06 +0000 Subject: [PATCH 53/60] content change, county or state -> county --- .../core/registration/includes/address-details-uk.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporter/templates/core/registration/includes/address-details-uk.html b/exporter/templates/core/registration/includes/address-details-uk.html index 31f7903946..95dcd36551 100644 --- a/exporter/templates/core/registration/includes/address-details-uk.html +++ b/exporter/templates/core/registration/includes/address-details-uk.html @@ -36,7 +36,7 @@
    - County or state + County
    {{ registration_data.site.address.region}} From 335fc47a2c74bb1b1fc6891abe17499258546c35 Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Fri, 14 Feb 2025 09:09:25 +0000 Subject: [PATCH 54/60] check all of form errors data in test --- .../additional_information/tests/test_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporter/f680/application_sections/additional_information/tests/test_views.py b/exporter/f680/application_sections/additional_information/tests/test_views.py index 8f131b745b..60fb1eba0f 100644 --- a/exporter/f680/application_sections/additional_information/tests/test_views.py +++ b/exporter/f680/application_sections/additional_information/tests/test_views.py @@ -139,7 +139,7 @@ def test_POST_to_step_validation_error( {}, ) assert response.status_code == 200 - assert "This field is required." in response.context["form"]["note"].errors + assert response.context["form"]["note"].errors == ["This field is required."] def test_GET_with_existing_data_success( self, From 8325fb74c0defcc555c064c0dcb874890d5b44b8 Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Fri, 14 Feb 2025 16:19:06 +0000 Subject: [PATCH 55/60] add label explicitly --- exporter/f680/application_sections/approval_details/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporter/f680/application_sections/approval_details/forms.py b/exporter/f680/application_sections/approval_details/forms.py index 6d1e22ca2c..6a0b7ecd61 100644 --- a/exporter/f680/application_sections/approval_details/forms.py +++ b/exporter/f680/application_sections/approval_details/forms.py @@ -11,7 +11,6 @@ class ApprovalTypeForm(BaseForm): class Layout: TITLE = "Select the types of approvals you need" - TITLE_AS_LABEL_FOR = "approval_choices" SUBMIT_BUTTON_TEXT = "Save and continue" class ApprovalTypeChoices(TextChoices): @@ -38,6 +37,7 @@ class ApprovalTypeChoices(TextChoices): ) approval_choices = forms.MultipleChoiceField( + label=Layout.TITLE, choices=(), error_messages={ "required": "Select an approval choice", From 4d8042eb3c4c17eb5a7cf64f32fd262d77fa210a Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Fri, 14 Feb 2025 16:26:09 +0000 Subject: [PATCH 56/60] TITLE_AS_LABEL_FOR added back in --- exporter/f680/application_sections/approval_details/forms.py | 1 + 1 file changed, 1 insertion(+) diff --git a/exporter/f680/application_sections/approval_details/forms.py b/exporter/f680/application_sections/approval_details/forms.py index 6a0b7ecd61..f1f1b0736a 100644 --- a/exporter/f680/application_sections/approval_details/forms.py +++ b/exporter/f680/application_sections/approval_details/forms.py @@ -11,6 +11,7 @@ class ApprovalTypeForm(BaseForm): class Layout: TITLE = "Select the types of approvals you need" + TITLE_AS_LABEL_FOR = "approval_choices" SUBMIT_BUTTON_TEXT = "Save and continue" class ApprovalTypeChoices(TextChoices): From 4c1f7792546cfc74bae0f3681913a023234086c6 Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Fri, 14 Feb 2025 16:30:37 +0000 Subject: [PATCH 57/60] test amendment --- exporter/f680/application_sections/approval_details/forms.py | 1 - .../application_sections/approval_details/tests/test_views.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/exporter/f680/application_sections/approval_details/forms.py b/exporter/f680/application_sections/approval_details/forms.py index f1f1b0736a..6a0b7ecd61 100644 --- a/exporter/f680/application_sections/approval_details/forms.py +++ b/exporter/f680/application_sections/approval_details/forms.py @@ -11,7 +11,6 @@ class ApprovalTypeForm(BaseForm): class Layout: TITLE = "Select the types of approvals you need" - TITLE_AS_LABEL_FOR = "approval_choices" SUBMIT_BUTTON_TEXT = "Save and continue" class ApprovalTypeChoices(TextChoices): diff --git a/exporter/f680/application_sections/approval_details/tests/test_views.py b/exporter/f680/application_sections/approval_details/tests/test_views.py index a767cc851c..4197f7f48b 100644 --- a/exporter/f680/application_sections/approval_details/tests/test_views.py +++ b/exporter/f680/application_sections/approval_details/tests/test_views.py @@ -149,7 +149,7 @@ def test_POST_approval_type_and_submit_wizard_success( "approval_details_text": "", }, "questions": { - "approval_choices": None, + "approval_choices": "Select the types of approvals you need", "demonstration_in_uk": "Explain what you are demonstrating and why", "demonstration_overseas": "Explain what you are demonstrating and why", "approval_details_text": "Provide details about what you're seeking approval to do", From 5170e696798ec38db47101f04a23220950ba2db7 Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Fri, 14 Feb 2025 16:44:47 +0000 Subject: [PATCH 58/60] add back in --- exporter/f680/application_sections/approval_details/forms.py | 1 + 1 file changed, 1 insertion(+) diff --git a/exporter/f680/application_sections/approval_details/forms.py b/exporter/f680/application_sections/approval_details/forms.py index 6a0b7ecd61..f1f1b0736a 100644 --- a/exporter/f680/application_sections/approval_details/forms.py +++ b/exporter/f680/application_sections/approval_details/forms.py @@ -11,6 +11,7 @@ class ApprovalTypeForm(BaseForm): class Layout: TITLE = "Select the types of approvals you need" + TITLE_AS_LABEL_FOR = "approval_choices" SUBMIT_BUTTON_TEXT = "Save and continue" class ApprovalTypeChoices(TextChoices): From e11cc8352a5c5f7d7283f8263844b0f21ac9492f Mon Sep 17 00:00:00 2001 From: "mark.j0hnst0n" Date: Mon, 17 Feb 2025 11:50:44 +0000 Subject: [PATCH 59/60] update urls --- exporter/f680/urls.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/exporter/f680/urls.py b/exporter/f680/urls.py index b43647e2af..070fd5cef4 100644 --- a/exporter/f680/urls.py +++ b/exporter/f680/urls.py @@ -15,6 +15,8 @@ path( "/approval-details/", include("exporter.f680.application_sections.approval_details.urls"), + ), + path( "/additional-information/", include("exporter.f680.application_sections.additional_information.urls"), ), From fd45a06b989fd1fedecfdf7d3945f78e9bc16ee8 Mon Sep 17 00:00:00 2001 From: Gurdeep Atwal Date: Mon, 17 Feb 2025 15:58:45 +0000 Subject: [PATCH 60/60] change sort order default --- caseworker/queues/views/cases.py | 6 +++-- .../caseworker/queues/views/test_cases.py | 24 +++++++++---------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/caseworker/queues/views/cases.py b/caseworker/queues/views/cases.py index 48e5162a01..1a684a90cb 100644 --- a/caseworker/queues/views/cases.py +++ b/caseworker/queues/views/cases.py @@ -143,9 +143,11 @@ def get_params(self): if session_sort_by: params["sort_by"] = session_sort_by elif self.queue_pk == ALL_CASES_QUEUE_ID: - params["sort_by"] = "submitted_at" - else: + # newest to oldest params["sort_by"] = "-submitted_at" + else: + # oldest to newest + params["sort_by"] = "submitted_at" self.request.session["case_search_sort_by"] = params["sort_by"] diff --git a/unit_tests/caseworker/queues/views/test_cases.py b/unit_tests/caseworker/queues/views/test_cases.py index 518d657e6f..a4d81ab7a9 100644 --- a/unit_tests/caseworker/queues/views/test_cases.py +++ b/unit_tests/caseworker/queues/views/test_cases.py @@ -19,7 +19,7 @@ "queue_id": ["00000000-0000-0000-0000-000000000001"], "selected_tab": ["all_cases"], "hidden": ["true"], - "sort_by": ["submitted_at"], + "sort_by": ["-submitted_at"], } @@ -313,7 +313,7 @@ def test_cases_queue_page_assigned_queues(authorized_client, mock_cases_search_t "queue_id": [queue_pk], "selected_tab": ["all_cases"], "hidden": ["false"], - "sort_by": ["-submitted_at"], + "sort_by": ["submitted_at"], } @@ -620,7 +620,7 @@ def test_tabs_with_all_cases_default(authorized_client, mock_cases_search, mock_ "page": ["1"], "queue_id": ["00000000-0000-0000-0000-000000000001"], "selected_tab": [tab], - "sort_by": ["submitted_at"], + "sort_by": ["-submitted_at"], } in head_request_history @@ -655,7 +655,7 @@ def test_tabs_on_all_cases_queue(authorized_client, mock_cases_search, tab_name, "page": ["1"], "queue_id": ["00000000-0000-0000-0000-000000000001"], "selected_tab": [tab_name], - "sort_by": ["submitted_at"], + "sort_by": ["-submitted_at"], } @@ -684,7 +684,7 @@ def test_tabs_on_team_queue( "page": ["1"], "queue_id": [queue_pk], "selected_tab": [tab_name], - "sort_by": ["-submitted_at"], + "sort_by": ["submitted_at"], } head_request_history = [x.qs for x in mock_cases_search_head.request_history] assert { @@ -692,7 +692,7 @@ def test_tabs_on_team_queue( "page": ["1"], "queue_id": [queue_pk], "selected_tab": ["all_cases"], - "sort_by": ["-submitted_at"], + "sort_by": ["submitted_at"], } in head_request_history tabs_with_hidden_param = ("my_cases", "open_queries") @@ -702,7 +702,7 @@ def test_tabs_on_team_queue( "page": ["1"], "queue_id": [queue_pk], "selected_tab": [tab], - "sort_by": ["-submitted_at"], + "sort_by": ["submitted_at"], } in head_request_history @@ -717,7 +717,7 @@ def test_tabs_on_team_queue_with_hidden_param( "page": ["1"], "queue_id": [queue_pk], "selected_tab": ["all_cases"], - "sort_by": ["-submitted_at"], + "sort_by": ["submitted_at"], } head_request_history = [x.qs for x in mock_cases_search_head.request_history] tabs_with_hidden_param = ("all_cases", "my_cases", "open_queries") @@ -727,7 +727,7 @@ def test_tabs_on_team_queue_with_hidden_param( "page": ["1"], "queue_id": [queue_pk], "selected_tab": [tab], - "sort_by": ["-submitted_at"], + "sort_by": ["submitted_at"], } in head_request_history @@ -1156,8 +1156,8 @@ def test_product_search_is_visible_to_specific_users_only( def test_queue_view_sort_params_persist(authorized_client): response = authorized_client.get(reverse("core:index")) assert response.status_code == 200 - assert authorized_client.session["case_search_sort_by"] == "submitted_at" + assert authorized_client.session["case_search_sort_by"] == "-submitted_at" - authorized_client.get(reverse("core:index") + "?sort_by=-submitted_at") + authorized_client.get(reverse("core:index") + "?sort_by=submitted_at") assert response.status_code == 200 - assert authorized_client.session["case_search_sort_by"] == "-submitted_at" + assert authorized_client.session["case_search_sort_by"] == "submitted_at"