Skip to content

Commit

Permalink
Merge branch 'dev' into LTD-5817-add-application-history
Browse files Browse the repository at this point in the history
  • Loading branch information
depsiatwal authored Jan 31, 2025
2 parents 4d00a35 + 2ebdb15 commit 83507f3
Show file tree
Hide file tree
Showing 15 changed files with 247 additions and 50 deletions.
7 changes: 7 additions & 0 deletions caseworker/advice/templates/advice/form_wizard.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{% extends 'core/form-wizard.html' %}

{% block right_side_panel %}
<div class="govuk-grid-column-one-third">
{% include "advice/case_detail.html" %}
</div>
{% endblock %}
1 change: 1 addition & 0 deletions caseworker/advice/views/approval.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def get_context_data(self, **kwargs):


class BaseApprovalAdviceView(LoginRequiredMixin, CaseContextMixin, BaseSessionWizardView):
template_name = "advice/form_wizard.html"

condition_dict = {
AdviceSteps.RECOMMEND_APPROVAL: ~C(is_fcdo_team),
Expand Down
1 change: 1 addition & 0 deletions caseworker/templates/core/form-wizard.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
{% crispy wizard.form %}
{% endif %}
</div>
{% block right_side_panel %}{% endblock %}
</div>
</div>
</form>
Expand Down
47 changes: 43 additions & 4 deletions core/file_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import magic

from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.files.uploadhandler import UploadFileException
from django.core.files.uploadedfile import UploadedFile
from django.http import StreamingHttpResponse

from django_chunk_upload_handlers.s3 import S3FileUploadHandler
Expand Down Expand Up @@ -45,6 +47,39 @@ def s3_client():
return S3Wrapper.get_client()


def validate_mime_type(file):
if isinstance(file, UnacceptableMimeTypeFile):
raise ValidationError(
"The file type is not supported. Upload a supported file type",
code="invalid_mime_type",
)


class UnacceptableMimeTypeFile(UploadedFile):
def __init__(self, field_name):
super().__init__(file="unacceptable-mime-type", name="unacceptable-mime-type", size="unacceptable-mime-type")
self.field_name = field_name

def open(self, mode=None):
raise UnacceptableMimeTypeError()

def chunks(self, chunk_size=None):
raise UnacceptableMimeTypeError()

def multiple_chunks(self, chunk_size=None):
raise UnacceptableMimeTypeError()

def __iter__(self):
raise UnacceptableMimeTypeError()

def __enter__(self):
raise UnacceptableMimeTypeError()

@property
def obj(self):
raise UnacceptableMimeTypeError()


class SafeS3FileUploadHandler(S3FileUploadHandler):
"""
S3FileUploadHandler with mime-type validation.
Expand All @@ -62,8 +97,9 @@ def receive_data_chunk(self, raw_data, start):
mime = magic.from_buffer(raw_data, mime=True)
if mime not in self.ACCEPTED_FILE_UPLOAD_MIME_TYPES:
self.abort()
raise UnacceptableMimeTypeError(f"Unsupported file type: {mime}")
super().receive_data_chunk(raw_data, start)
self.failed_mime_type = True
return None
return super().receive_data_chunk(raw_data, start)

def file_complete(self, *args, **kwargs):
"""Override `file_complete` to ensure that all necessary attributes
Expand All @@ -72,18 +108,21 @@ def file_complete(self, *args, **kwargs):
Some frameworks, e.g. formtools' SessionWizardView will fall over
if these attributes aren't present.
"""
if getattr(self, "failed_mime_type", False):
return UnacceptableMimeTypeFile(self.field_name)

file = super().file_complete(*args, **kwargs)
file.charset = self.charset

return file


class UploadFailed(UploadFileException):
pass
message = "File upload failed."


class UnacceptableMimeTypeError(UploadFailed):
pass
message = "Invalid file type uploaded."


def generate_file(result):
Expand Down
6 changes: 4 additions & 2 deletions core/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
from django.http import HttpResponseForbidden

from core.file_handler import UploadFailed
from lite_content.lite_internal_frontend.strings import cases
from lite_forms.generators import error_page
from json import JSONDecodeError

Expand All @@ -37,7 +36,10 @@ def process_exception(self, request, exception):
if not isinstance(exception, UploadFailed):
return None

return error_page(request, cases.Manage.Documents.AttachDocuments.FILE_TOO_LARGE)
return error_page(
request,
exception.message,
)


class SessionTimeoutMiddleware:
Expand Down
18 changes: 17 additions & 1 deletion exporter/applications/forms/parties.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from django.urls import reverse_lazy

from core.common.forms import BaseForm, FieldsetForm
from core.file_handler import validate_mime_type
from core.forms.layouts import ConditionalRadios, ConditionalRadiosQuestion
from core.forms.widgets import Autocomplete
from exporter.core.constants import CaseTypes, FileUploadFileTypes
Expand Down Expand Up @@ -433,6 +434,9 @@ class PartyDocumentUploadForm(forms.Form):
error_messages={
"required": "Select an end-user document",
},
validators=[
validate_mime_type,
],
)
product_differences_note = forms.CharField(
widget=forms.Textarea(attrs={"rows": "5"}),
Expand Down Expand Up @@ -492,6 +496,9 @@ class PartyEnglishTranslationDocumentUploadForm(forms.Form):
error_messages={
"required": "Select an English translation",
},
validators=[
validate_mime_type,
],
)

def __init__(self, edit, *args, **kwargs):
Expand Down Expand Up @@ -520,6 +527,9 @@ class PartyCompanyLetterheadDocumentUploadForm(forms.Form):
error_messages={
"required": "Select a document on company letterhead",
},
validators=[
validate_mime_type,
],
)

def __init__(self, edit, *args, **kwargs):
Expand All @@ -543,7 +553,13 @@ def get_title(self):

class PartyEC3DocumentUploadForm(forms.Form):
title = "Upload an EC3 form (optional)"
party_ec3_document = forms.FileField(label="", required=False)
party_ec3_document = forms.FileField(
label="",
required=False,
validators=[
validate_mime_type,
],
)
ec3_missing_reason = forms.CharField(
widget=forms.Textarea(attrs={"rows": "5"}),
label="",
Expand Down
4 changes: 4 additions & 0 deletions exporter/goods/forms/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
ConditionalCheckbox,
Prefixed,
)
from core.file_handler import validate_mime_type
from core.forms.utils import coerce_str_to_bool

from core.common.forms import BaseForm, FieldsetForm
Expand Down Expand Up @@ -363,6 +364,9 @@ class Layout:
error_messages={
"required": "Select a document that shows what your product is designed to do",
},
validators=[
validate_mime_type,
],
)
description = forms.CharField(
widget=forms.Textarea(attrs={"rows": "5"}),
Expand Down
7 changes: 7 additions & 0 deletions exporter/goods/forms/firearms.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
FirearmsActSections,
SerialChoices,
)
from core.file_handler import validate_mime_type
from core.forms.layouts import (
ConditionalCheckbox,
ConditionalRadiosQuestion,
Expand Down Expand Up @@ -222,6 +223,9 @@ class Layout:
error_messages={
"required": "Select a registered firearms dealer certificate",
},
validators=[
validate_mime_type,
],
widget=PotentiallyUnsafeClearableFileInput,
)

Expand Down Expand Up @@ -330,6 +334,9 @@ class Layout:
file = forms.FileField(
label="",
required=False,
validators=[
validate_mime_type,
],
widget=PotentiallyUnsafeClearableFileInput(
force_required=True,
),
Expand Down
4 changes: 4 additions & 0 deletions exporter/goods/forms/goods.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from core.builtins.custom_tags import default_na, linkify
from core.constants import ComponentAccessoryChoices, ProductCategories
from core.file_handler import validate_mime_type
from core.forms.layouts import ConditionalRadiosQuestion, ConditionalRadios, summary_list
from core.forms.utils import coerce_str_to_bool

Expand Down Expand Up @@ -1106,6 +1107,9 @@ class AttachFirearmsDealerCertificateForm(forms.Form):
error_messages={
"required": "Select certificate file to upload",
},
validators=[
validate_mime_type,
],
)

reference_code = forms.CharField(
Expand Down
8 changes: 8 additions & 0 deletions exporter/organisation/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from crispy_forms_gds.helper import FormHelper
from crispy_forms_gds.layout import Submit, Layout, HTML

from core.file_handler import validate_mime_type

from exporter.core.constants import FileUploadFileTypes


Expand All @@ -28,6 +30,9 @@ class Layout:
label="",
help_text="The file must be smaller than 50MB",
error_messages={"required": "Select certificate file to upload"},
validators=[
validate_mime_type,
],
)
reference_code = forms.CharField(
label="Certificate number",
Expand Down Expand Up @@ -63,6 +68,9 @@ class Layout:
label=FileUploadFileTypes.UPLOAD_GUIDANCE_TEXT,
help_text="The file must be smaller than 50MB",
error_messages={"required": "Select certificate file to upload"},
validators=[
validate_mime_type,
],
)
reference_code = forms.CharField(
label="Certificate number",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,42 @@ def post_to_step(post_to_step_factory, url_approve):
return post_to_step_factory(url_approve)


def test_give_approval_advice_get(authorized_client, url):
def test_give_approval_advice_legacy_get(authorized_client, url, beautiful_soup):
response = authorized_client.get(url)
assert response.status_code == 200
soup = beautiful_soup(response.content)
header = soup.find("h1", {"class": "govuk-heading-xl"})
assert header.text == "Recommend an approval"

summary_header = soup.find("h2", {"class": "govuk-heading-m"})
assert summary_header.text == "Case details"

details = soup.find_all("span", {"class": "govuk-details__summary-text"})
assert {detail.text.strip() for detail in details} == {
"Add a licence condition, instruction to exporter or footnote",
"Products",
"Destinations",
"View serial numbers",
}


def test_give_approval_advice_get(
authorized_client,
beautiful_soup,
url_approve,
):
response = authorized_client.get(url_approve)
assert response.status_code == 200

soup = beautiful_soup(response.content)
header = soup.find("h1", {"class": "govuk-heading-xl"})
assert header.text == "Recommend an approval"

summary_header = soup.find("h2", {"class": "govuk-heading-m"})
assert summary_header.text == "Case details"

details = soup.find_all("span", {"class": "govuk-details__summary-text"})
assert {detail.text.strip() for detail in details} == {"Products", "Destinations", "View serial numbers"}


def test_approval_advice_post_valid(
Expand Down Expand Up @@ -75,6 +108,12 @@ def test_approval_advice_post_valid_add_conditional(
header = soup.find("h1")
assert header.text == "Add licence conditions (optional)"

summary_header = soup.find("h2", {"class": "govuk-heading-m"})
assert summary_header.text == "Case details"

details = soup.find_all("span", {"class": "govuk-details__summary-text"})
assert {detail.text.strip() for detail in details} == {"Products", "Destinations", "View serial numbers"}

add_LC_response = post_to_step(
AdviceSteps.LICENCE_CONDITIONS,
{"proviso": "proviso"},
Expand Down
23 changes: 23 additions & 0 deletions unit_tests/core/middleware/test_upload_failed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from core.middleware import UploadFailedMiddleware
from core.file_handler import UploadFailed


def test_upload_failed_middleware_general_exception(rf, mocker):
get_response = mocker.MagicMock()
upload_failed_middleware = UploadFailedMiddleware(get_response)
request = rf.get("/")
not_file_upload_exception = Exception()

assert upload_failed_middleware.process_exception(request, not_file_upload_exception) is None


def test_upload_failed_middleware_upload_failed_exception(rf, mocker, beautiful_soup):
get_response = mocker.MagicMock()
upload_failed_middleware = UploadFailedMiddleware(get_response)
request = rf.get("/")
not_file_upload_exception = UploadFailed()

error_page_reponse = upload_failed_middleware.process_exception(request, not_file_upload_exception)
soup = beautiful_soup(error_page_reponse.content)

assert "An error occurred" in soup.title.text.strip()
Loading

0 comments on commit 83507f3

Please sign in to comment.