/user-information/",
+ include("exporter.f680.application_sections.user_information.urls"),
+ ),
]
diff --git a/exporter/f680/views.py b/exporter/f680/views.py
index 175ef5a9df..723f82c572 100644
--- a/exporter/f680/views.py
+++ b/exporter/f680/views.py
@@ -1,34 +1,26 @@
from http import HTTPStatus
+import rules
-from django.conf import settings
from django.contrib.auth.mixins import AccessMixin
from django.shortcuts import redirect
from django.urls import reverse
-from django.views.generic import FormView
+from django.views.generic import FormView, RedirectView
from core.auth.views import LoginRequiredMixin
from core.decorators import expect_status
-from core.wizard.views import BaseSessionWizardView
-from .constants import (
- ApplicationFormSteps,
-)
-from .forms import (
- ApplicationNameForm,
- ApplicationSubmissionForm,
-)
-from .payloads import (
- F680CreatePayloadBuilder,
-)
+from .forms import ApplicationSubmissionForm
+
from .services import (
post_f680_application,
get_f680_application,
+ submit_f680_application,
)
class F680FeatureRequiredMixin(AccessMixin):
def dispatch(self, request, *args, **kwargs):
- if not settings.FEATURE_FLAG_ALLOW_F680:
+ if not rules.test_rule("can_exporter_use_f680s", request):
self.raise_exception = True
self.permission_denied_message = (
"You are not authorised to use the F680 Security Clearance application feature"
@@ -37,10 +29,7 @@ def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
-class F680ApplicationCreateView(LoginRequiredMixin, F680FeatureRequiredMixin, BaseSessionWizardView):
- form_list = [
- (ApplicationFormSteps.APPLICATION_NAME, ApplicationNameForm),
- ]
+class F680ApplicationCreateView(LoginRequiredMixin, F680FeatureRequiredMixin, RedirectView):
@expect_status(
HTTPStatus.CREATED,
@@ -58,11 +47,10 @@ def get_success_url(self, application_id):
},
)
- def get_payload(self, form_dict):
- return F680CreatePayloadBuilder().build(form_dict)
-
- def done(self, form_list, form_dict, **kwargs):
- data = self.get_payload(form_dict)
+ def get(self, request, *args, **kwargs):
+ super().get(request, *args, **kwargs)
+ # Data required to create a base application
+ data = {"application": {}}
response_data, _ = self.post_f680_application(data)
return redirect(self.get_success_url(response_data["id"]))
@@ -71,6 +59,10 @@ class F680ApplicationSummaryView(LoginRequiredMixin, F680FeatureRequiredMixin, F
form_class = ApplicationSubmissionForm
template_name = "f680/summary.html"
+ def setup(self, request, *args, **kwargs):
+ super().setup(request, *args, **kwargs)
+ self.application, _ = self.get_f680_application(kwargs["pk"])
+
@expect_status(
HTTPStatus.OK,
"Error getting F680 application",
@@ -80,15 +72,43 @@ class F680ApplicationSummaryView(LoginRequiredMixin, F680FeatureRequiredMixin, F
def get_f680_application(self, application_id):
return get_f680_application(self.request, application_id)
- def setup(self, request, *args, **kwargs):
- super().setup(request, *args, **kwargs)
- self.application, _ = self.get_f680_application(kwargs["pk"])
-
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["application"] = self.application
-
return context
+ @expect_status(
+ HTTPStatus.OK,
+ "Error submitting F680 application",
+ "Unexpected error submitting F680 application",
+ reraise_404=True,
+ )
+ def submit_f680_application(self, application_id):
+ return submit_f680_application(self.request, application_id)
+
+ def all_sections_complete(self):
+ # TODO: Think more about pre-submit validation as this is very barebones right now
+ complete_sections = set(self.application["application"].get("sections", {}).keys())
+ required_sections = set(
+ [
+ "general_application_details",
+ "approval_type",
+ "user_information",
+ "product_information",
+ ]
+ )
+ missing_sections = required_sections - complete_sections
+ return len(missing_sections) == 0, missing_sections
+
+ def form_valid(self, form):
+ is_sections_completed, _ = self.all_sections_complete()
+ if not is_sections_completed:
+ context_data = self.get_context_data(form=form)
+ context_data["errors"] = {"missing_sections": ["Please complete all required sections"]}
+ return self.render_to_response(context_data)
+
+ self.submit_f680_application(self.application["id"])
+ return super().form_valid(form)
+
def get_success_url(self):
- return reverse("f680:summary", kwargs={"pk": self.application["id"]})
+ return reverse("applications:success_page", kwargs={"pk": self.application["id"]})
diff --git a/exporter/templates/f680/forms/help_ITAR.html b/exporter/templates/f680/forms/help_ITAR.html
new file mode 100644
index 0000000000..b036063f77
--- /dev/null
+++ b/exporter/templates/f680/forms/help_ITAR.html
@@ -0,0 +1,5 @@
+
+ ITAR is the United States regulation that controls the export of sensitive goods and technology.
+ You can apply for approval to share unclassified ITAR items separately using our US unclassified ITAR application. You can get a decision faster using this application.
+ If you have classified UK items, please continue with this F680 for those items.
+
diff --git a/exporter/templates/f680/forms/help_electronic_warfare_data.html b/exporter/templates/f680/forms/help_electronic_warfare_data.html
new file mode 100644
index 0000000000..58a5c38db6
--- /dev/null
+++ b/exporter/templates/f680/forms/help_electronic_warfare_data.html
@@ -0,0 +1,16 @@
+
+ MOD owned electronic warfare data includes any data or information in the following forms:
+
+ - pre and post analysed Emitter Parametric data and Emitter related information
+ - authoritative data on all emitters, platforms and weapons
+ - electronic fits of all systems including Electromagnetic Order of Battle (EOB)
+ - emitter geographic location data and information
+ - electronic warfare evaluation and research and development
+ - technical and administrative documentation required for continuous exchange of data
+ - operational electronic warfare information including Lawful Intercepts
+ - Mission Dependent Data for all ESM and RWR fitted platforms
+ - electromagnetic countermeasures (CM) advice and the provision of CM (for example, DIRCM codes)
+ - algorithms and waveforms required to make software and EW systems work
+ - representative emitter data used for training, trials and capability development
+
+
diff --git a/exporter/templates/f680/forms/help_manpads.html b/exporter/templates/f680/forms/help_manpads.html
new file mode 100644
index 0000000000..eb756d0301
--- /dev/null
+++ b/exporter/templates/f680/forms/help_manpads.html
@@ -0,0 +1,3 @@
+
+ MANPADS are are lightweight anti-aircraft weapons. They are designed to protect soldiers on the battlefield from attacking aircraft
+
diff --git a/exporter/templates/f680/forms/help_mctr_categories.html b/exporter/templates/f680/forms/help_mctr_categories.html
new file mode 100644
index 0000000000..a9fa933eb2
--- /dev/null
+++ b/exporter/templates/f680/forms/help_mctr_categories.html
@@ -0,0 +1,5 @@
+
+ MTCR Category 1 items include complete rockets and unmanned aerial vehicle systems that can deliver a payload of at least 500kg to a range of at least 300km.
+ MTCR Category 2 items include missile systems and unmanned aerial vehicles that can deliver a payload of less than 500kg to a range of 300km.
+ For both Categories, unmanned aerial vehicles include ballistic missiles, space launch vehicles, sounding rockets, cruise missiles, target drones and reconnaissance drones.
+
diff --git a/exporter/templates/f680/forms/help_product_description.html b/exporter/templates/f680/forms/help_product_description.html
new file mode 100644
index 0000000000..d8871ad431
--- /dev/null
+++ b/exporter/templates/f680/forms/help_product_description.html
@@ -0,0 +1 @@
+Incorporating an item means integrating it with a higher level system, platform or software.
diff --git a/exporter/templates/f680/forms/help_security_features.html b/exporter/templates/f680/forms/help_security_features.html
new file mode 100644
index 0000000000..015f172551
--- /dev/null
+++ b/exporter/templates/f680/forms/help_security_features.html
@@ -0,0 +1,3 @@
+
+ Information security features include cryptography, and cryptanalytic functions. They are often found in communication, wireless or internet-based products, digital forensics and surveillance tools.
+
diff --git a/exporter/templates/f680/forms/help_security_grading.html b/exporter/templates/f680/forms/help_security_grading.html
new file mode 100644
index 0000000000..4d07ec54db
--- /dev/null
+++ b/exporter/templates/f680/forms/help_security_grading.html
@@ -0,0 +1,8 @@
+
+ The government classifies information assets to ensure they are appropriately protected.
+ Guidance on government security gradings (opens in new tab)
+ The grading can sometimes include a prefix and suffix. There are many in use and so it is important that you know the full classification of the product.
+ If the item was developed with Ministry of Defence (MOD) funding you will find the grading in the Security Aspects Letter provided by the MOD project team.
+ If the item was developed with overseas government support, that government is responsible for providing the grading.
+ If the item was developed without UK or overseas government support, you should apply for a private venture grading using SPIRE - the online export licensing system. The grading will be provided by MOD.
+
diff --git a/exporter/templates/f680/forms/subtitle_product_description.html b/exporter/templates/f680/forms/subtitle_product_description.html
new file mode 100644
index 0000000000..b421608b4f
--- /dev/null
+++ b/exporter/templates/f680/forms/subtitle_product_description.html
@@ -0,0 +1,13 @@
+
+
+
Include as much information as you can. Explain:
+
+ - What the item is including the make and model
+ - how you plan to share it
+ - if anyone else will use it
+ - if it will be incorporated into other items
+
+ If you're using a substitute product you must include it in your application. You must also include the original product that is being substituted.
+ You can upload supporting documents later in your application explaining more about the item.
+
+
diff --git a/exporter/templates/f680/forms/subtitle_product_name.html b/exporter/templates/f680/forms/subtitle_product_name.html
new file mode 100644
index 0000000000..53f4318d14
--- /dev/null
+++ b/exporter/templates/f680/forms/subtitle_product_name.html
@@ -0,0 +1,10 @@
+We need to understand what you will be sharing with non-uk entities.
+ This includes:
+
- physical products
+ - substitute products
+ - agreements
+ - brochures
+ - manuals
+ - training guides
+
+
diff --git a/exporter/templates/f680/summary.html b/exporter/templates/f680/summary.html
index cc1994cde2..3d31c64d4f 100644
--- a/exporter/templates/f680/summary.html
+++ b/exporter/templates/f680/summary.html
@@ -5,6 +5,27 @@
{% block title %}Apply for an F680 Application{% endblock%}
{% block body %}
+{% if errors %}
+
+
+ {% lcs 'applications.EditApplicationPage.ERRORS' %}
+
+
+
+ {% for key, values in errors.items %}
+ {% for value in values %}
+ -
+
+ {{ value }}
+
+
+ {% endfor %}
+ {% endfor %}
+
+
+
+{% endif %}
+
F680 Application
@@ -29,7 +50,7 @@
+
+
2.
Complete approval details
@@ -49,7 +72,21 @@
+
+
+
+
+
+
+
+ 3.
+ Tell us who is involved
+
+
+
+
4.
Additional Information
@@ -69,13 +130,13 @@
diff --git a/exporter/templates/f680/user_information/summary.html b/exporter/templates/f680/user_information/summary.html
new file mode 100644
index 0000000000..75f511cd0b
--- /dev/null
+++ b/exporter/templates/f680/user_information/summary.html
@@ -0,0 +1,45 @@
+{% extends "layouts/base.html" %}
+
+{% block title %}F680 User information{% endblock%}
+
+{% block back_link_url %}{% url "f680:summary" application.id %}{% endblock %}
+
+{% block body %}
+
+
+
Tell us about the entity
+
+
+
+
+
+
+
+
+
+
+ {% for id, user_entity in user_entities.items %}
+
+
+ {{user_entity.end_user_name}}
+ |
+
+ {{user_entity.entity_type}}
+ |
+
+
+ Edit
+
+ |
+
+ {% endfor %}
+
+
+
Add a new entity
+
+ Save and continue
+
+
+
+
+{% endblock %}
diff --git a/unit_tests/conftest.py b/unit_tests/conftest.py
index 41512096df..0f8dca8f0a 100644
--- a/unit_tests/conftest.py
+++ b/unit_tests/conftest.py
@@ -555,23 +555,6 @@ def data_open_case():
}
-@pytest.fixture
-def data_f680_case(data_organisation):
- return {
- "id": "6cf7b401-62dc-4577-ad1d-4282f2aabc96",
- "application": {"name": "F680 Test 1"},
- "reference_code": None,
- "organisation": {
- "id": "3913ff20-5a2b-468a-bf5d-427228459b06",
- "name": "Archway Communications",
- "type": "commercial",
- "status": "active",
- },
- "submitted_at": None,
- "submitted_by": None,
- }
-
-
@pytest.fixture
def data_standard_case(
data_organisation,
diff --git a/unit_tests/exporter/applications/views/test_f680s.py b/unit_tests/exporter/applications/views/test_f680s.py
deleted file mode 100644
index 8bd6d6c305..0000000000
--- a/unit_tests/exporter/applications/views/test_f680s.py
+++ /dev/null
@@ -1,197 +0,0 @@
-import pytest
-from uuid import uuid4
-
-from bs4 import BeautifulSoup
-from django.urls import reverse
-from pytest_django.asserts import assertTemplateUsed
-
-from core import client
-from exporter.f680.constants import (
- ApplicationFormSteps,
-)
-from exporter.f680.forms import ApplicationNameForm, ApplicationSubmissionForm
-
-
-@pytest.fixture(autouse=True)
-def setup(settings):
- settings.FEATURE_FLAG_ALLOW_F680 = True
-
-
-@pytest.fixture
-def authorized_client(authorized_client_factory, mock_exporter_user):
- return authorized_client_factory(mock_exporter_user["user"])
-
-
-@pytest.fixture
-def f680_apply_url():
- return reverse("f680:apply")
-
-
-@pytest.fixture
-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)
-
-
-@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_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 unset_f680_feature_flag(settings):
- settings.FEATURE_FLAG_ALLOW_F680 = False
-
-
-class TestApplyForLicenceQuestionsClass:
- 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
-
-
-class TestF680ApplicationCreateView:
- def test_get_create_f680_view_success(
- self,
- authorized_client,
- f680_apply_url,
- mock_f680_application_get,
- ):
- response = authorized_client.get(f680_apply_url)
-
- assert isinstance(response.context["form"], ApplicationNameForm)
- soup = BeautifulSoup(response.content, "html.parser")
- assert "Name of the application" in soup.find("h1").text
-
- def test_get_create_f680_view_fail_with_feature_flag_off(
- self,
- authorized_client,
- f680_apply_url,
- mock_f680_application_get,
- unset_f680_feature_flag,
- ):
- 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"
- in response.context[0].get("description").args
- )
-
- def test_post_to_create_f680_name_step_success(
- self,
- authorized_client,
- f680_apply_url,
- post_to_step,
- f680_summary_url_with_application,
- ):
- response = post_to_step(
- ApplicationFormSteps.APPLICATION_NAME,
- {"name": "F680 Test"},
- )
-
- assert response.status_code == 302
- assert response.url == f680_summary_url_with_application
-
- def test_post_to_create_f680_name_step_invalid_data(
- self,
- authorized_client,
- f680_apply_url,
- post_to_step,
- ):
- response = post_to_step(
- ApplicationFormSteps.APPLICATION_NAME,
- {"name": ""},
- )
-
- assert isinstance(response.context["form"], ApplicationNameForm)
- assert response.context["form"].errors
- assert response.context["form"].errors.get("name")[0] == "This field is required."
-
-
-class TestF680ApplicationSummaryView:
- def test_get_f680_summary_view_success(
- self,
- authorized_client,
- f680_summary_url_with_application,
- mock_f680_application_get,
- ):
- response = authorized_client.get(f680_summary_url_with_application)
-
- assert isinstance(response.context["form"], ApplicationSubmissionForm)
- 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"
-
- def test_get_f680_summary_view_case_not_found(
- self,
- authorized_client,
- requests_mock,
- ):
-
- 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(
- self,
- authorized_client,
- f680_summary_url_with_application,
- mock_f680_application_get,
- unset_f680_feature_flag,
- ):
- response = authorized_client.get(f680_summary_url_with_application)
- assert response.status_code == 200
- assert response.context[0].get("title") == "Forbidden"
- assert (
- "You are not authorised to use the F680 Security Clearance application feature"
- in response.context[0].get("description").args
- )
-
- def test_post_f680_submission_form_success(
- self,
- authorized_client,
- f680_summary_url_with_application,
- mock_f680_application_get,
- ):
- response = authorized_client.post(
- f680_summary_url_with_application,
- )
-
- assert response.status_code == 302
- 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,
- mock_f680_application_get,
- unset_f680_feature_flag,
- ):
- response = authorized_client.post(
- f680_summary_url_with_application,
- )
-
- assert response.context[0].get("title") == "Forbidden"
- assert (
- "You are not authorised to use the F680 Security Clearance application feature"
- in response.context[0].get("description").args
- )
diff --git a/unit_tests/exporter/applications/views/test_success_page.py b/unit_tests/exporter/applications/views/test_success_page.py
index a9c59261ab..8e36165f85 100644
--- a/unit_tests/exporter/applications/views/test_success_page.py
+++ b/unit_tests/exporter/applications/views/test_success_page.py
@@ -76,6 +76,12 @@ def test_success_view(
assert soup.find("input", {"id": "submit-id-submit"})["value"] == "Submit and continue"
+def test_apply_for_licence_start_view(authorized_client, application_start_url):
+ response = authorized_client.get(application_start_url)
+
+ assert response.status_code == 200
+
+
def test_post_survey_feedback(authorized_client, success_url, application_pk, survey_id, mock_post_survey):
response = authorized_client.post(success_url, data={"satisfaction_rating": "NEITHER"})
assert response.status_code == 302
diff --git a/unit_tests/exporter/apply_for_a_licence/forms/test_triage_questions.py b/unit_tests/exporter/apply_for_a_licence/forms/test_triage_questions.py
index 4e8ca98db3..2aa4d8f9b3 100644
--- a/unit_tests/exporter/apply_for_a_licence/forms/test_triage_questions.py
+++ b/unit_tests/exporter/apply_for_a_licence/forms/test_triage_questions.py
@@ -14,20 +14,58 @@
(False, False, ["export_licence", "transhipment", "trade_control_licence"], ["f680"]),
),
)
-def test_opening_question_feature_flags(settings, siel_only_allowed, f680_allowed, expect_enabled, expect_disabled):
+def test_opening_question_feature_flags(
+ rf, client, settings, siel_only_allowed, f680_allowed, expect_enabled, expect_disabled
+):
+ request = rf.get("/")
+ request.session = client.session
+ session = request.session
+ session["organisation"] = "123"
+ session.save()
+
# given the flag is set or unset
settings.FEATURE_FLAG_ONLY_ALLOW_SIEL = siel_only_allowed
settings.FEATURE_FLAG_ALLOW_F680 = f680_allowed
reload_urlconf(["exporter.apply_for_a_licence.urls", settings.ROOT_URLCONF])
# when the form is created
- form = triage_questions.opening_question()
+ form = triage_questions.opening_question(request)
# then the disabled options reflect the feature flag
assert [item.key for item in form.questions[0].options if not item.disabled] == expect_enabled
assert [item.key for item in form.questions[0].options if item.disabled] == expect_disabled
+@pytest.mark.parametrize(
+ "f680_allowed, f680_orgs_allowed, user_org, expect_enabled",
+ (
+ (True, [], "1234", ["export_licence", "f680"]),
+ (False, [], "1234", ["export_licence"]),
+ (False, ["1234"], "1234", ["export_licence", "f680"]),
+ (False, ["1234"], "12345", ["export_licence"]),
+ ),
+)
+def test_opening_question_f680_feature_flags(
+ rf, client, settings, f680_allowed, f680_orgs_allowed, user_org, expect_enabled
+):
+ request = rf.get("/")
+ request.session = client.session
+ session = request.session
+ session["organisation"] = user_org
+ session.save()
+
+ # given the flag is set
+ settings.FEATURE_FLAG_ALLOW_F680 = f680_allowed
+ settings.FEATURE_FLAG_F680_ALLOWED_ORGANISATIONS = f680_orgs_allowed
+ reload_urlconf(["exporter.apply_for_a_licence.urls", settings.ROOT_URLCONF])
+
+ # when the form is created
+ form = triage_questions.opening_question(request)
+
+ # then the disabled options reflect the feature flag
+ assert [item.key for item in form.questions[0].options if not item.disabled] == expect_enabled
+
+
@pytest.mark.parametrize(
"value, expect_enabled, expect_disabled",
(