Skip to content

Commit

Permalink
Merge pull request #2354 from uktrade/LTD-5901
Browse files Browse the repository at this point in the history
LTD-5901 Add approval type flow
  • Loading branch information
markj0hnst0n authored Feb 17, 2025
2 parents 331fbd8 + e11cc83 commit e603369
Show file tree
Hide file tree
Showing 12 changed files with 376 additions and 0 deletions.
28 changes: 28 additions & 0 deletions core/forms/layouts.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,39 @@ 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 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}
)

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"

Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class FormSteps:
APPROVAL_TYPE = "APPROVAL_TYPE"
84 changes: 84 additions & 0 deletions exporter/f680/application_sections/approval_details/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
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 F680ConditionalCheckboxes, F680ConditionalCheckboxesQuestion


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(
label=Layout.TITLE,
choices=(),
error_messages={
"required": "Select an approval choice",
},
widget=forms.CheckboxSelectMultiple(),
)

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,
)

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
)
super().__init__(*args, **kwargs)
self.fields["approval_choices"].choices = self.ApprovalTypeChoices

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"),
),
)
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
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"] = {
"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_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": "",
"approval_details_text": "",
},
"questions": {
"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",
},
},
}
}

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"
10 changes: 10 additions & 0 deletions exporter/f680/application_sections/approval_details/urls.py
Original file line number Diff line number Diff line change
@@ -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"),
]
10 changes: 10 additions & 0 deletions exporter/f680/application_sections/approval_details/views.py
Original file line number Diff line number Diff line change
@@ -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"
4 changes: 4 additions & 0 deletions exporter/f680/constants.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
class ApplicationFormSteps:
APPLICATION_NAME = "APPLICATION_NAME"


class ApprovalTypeSteps:
APPROVAL_TYPE = "APPROVAL_TYPE"
4 changes: 4 additions & 0 deletions exporter/f680/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
"<uuid:pk>/general-application-details/",
include("exporter.f680.application_sections.general_application_details.urls"),
),
path(
"<uuid:pk>/approval-details/",
include("exporter.f680.application_sections.approval_details.urls"),
),
path(
"<uuid:pk>/additional-information/",
include("exporter.f680.application_sections.additional_information.urls"),
Expand Down
21 changes: 21 additions & 0 deletions exporter/templates/f680/forms/help_with_approval_type.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<p class="govuk-body">
<b>Initial discussions or promoting the products</b><br>
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.

</p>
<p class="govuk-body">
<b>Product demonstrations in the UK or overseas</b><br>
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.
</p>
<p class="govuk-body">
<b>Training</b><br>
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.
</p>
<p class="govuk-body">
<b>Through life support</b><br>
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.
</p>
<p class="govuk-body">
<b>Supply</b><br>
Content TBC needs to come from MOD as this is a new option.
</p>
Loading

0 comments on commit e603369

Please sign in to comment.