mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-02-06 11:23:48 -05:00
Show letter preview once file is uploaded
This shows the sanitised letter preview if the file had no validation errors or the preview with the overlay if it failed validation.
This commit is contained in:
@@ -13,16 +13,17 @@ from notifications_utils.pdf import pdf_page_count
|
||||
from PyPDF2.utils import PdfReadError
|
||||
from requests import RequestException
|
||||
|
||||
from app import current_service
|
||||
from app import current_service, service_api_client
|
||||
from app.extensions import antivirus_client
|
||||
from app.main import main
|
||||
from app.main.forms import PDFUploadForm
|
||||
from app.s3_client.s3_letter_upload_client import (
|
||||
get_letter_pdf_and_metadata,
|
||||
get_transient_letter_file_location,
|
||||
upload_letter_to_s3,
|
||||
)
|
||||
from app.template_previews import sanitise_letter
|
||||
from app.utils import user_has_permissions
|
||||
from app.template_previews import TemplatePreview, sanitise_letter
|
||||
from app.utils import get_template, user_has_permissions
|
||||
|
||||
MAX_FILE_UPLOAD_SIZE = 2 * 1024 * 1024 # 2MB
|
||||
|
||||
@@ -49,7 +50,7 @@ def upload_letter(service_id):
|
||||
return invalid_upload_error('Your file must be smaller than 2MB')
|
||||
|
||||
try:
|
||||
pdf_page_count(BytesIO(pdf_file_bytes))
|
||||
page_count = pdf_page_count(BytesIO(pdf_file_bytes))
|
||||
except PdfReadError:
|
||||
current_app.logger.info('Invalid PDF uploaded for service_id: {}'.format(service_id))
|
||||
return invalid_upload_error('Your file must be a valid PDF')
|
||||
@@ -76,6 +77,7 @@ def upload_letter(service_id):
|
||||
service_id=current_service.id,
|
||||
file_id=upload_id,
|
||||
original_filename=form.file.data.filename,
|
||||
page_count=page_count,
|
||||
status=status,
|
||||
)
|
||||
)
|
||||
@@ -92,6 +94,39 @@ def invalid_upload_error(message):
|
||||
@user_has_permissions('send_messages')
|
||||
def uploaded_letter_preview(service_id, file_id):
|
||||
original_filename = request.args.get('original_filename')
|
||||
page_count = request.args.get('page_count')
|
||||
status = request.args.get('status')
|
||||
|
||||
return render_template('views/uploads/preview.html', original_filename=original_filename, status=status)
|
||||
template_dict = service_api_client.get_precompiled_template(service_id)
|
||||
|
||||
template = get_template(
|
||||
template_dict,
|
||||
service_id,
|
||||
letter_preview_url=url_for(
|
||||
'.view_letter_upload_as_preview',
|
||||
service_id=service_id,
|
||||
file_id=file_id
|
||||
),
|
||||
page_count=page_count
|
||||
)
|
||||
|
||||
return render_template(
|
||||
'views/uploads/preview.html',
|
||||
original_filename=original_filename,
|
||||
template=template,
|
||||
status=status,
|
||||
)
|
||||
|
||||
|
||||
@main.route("/services/<service_id>/preview-letter-image/<file_id>")
|
||||
@user_has_permissions('send_messages')
|
||||
def view_letter_upload_as_preview(service_id, file_id):
|
||||
file_location = get_transient_letter_file_location(service_id, file_id)
|
||||
pdf_file, metadata = get_letter_pdf_and_metadata(file_location)
|
||||
|
||||
page = request.args.get('page')
|
||||
|
||||
if metadata['status'] == 'invalid':
|
||||
return TemplatePreview.from_invalid_pdf_file(pdf_file, page)
|
||||
else:
|
||||
return TemplatePreview.from_valid_pdf_file(pdf_file, page)
|
||||
|
||||
@@ -314,6 +314,7 @@ class HeaderNavigation(Navigation):
|
||||
'view_jobs',
|
||||
'view_letter_notification_as_preview',
|
||||
'view_letter_template_preview',
|
||||
'view_letter_upload_as_preview',
|
||||
'view_notification',
|
||||
'view_notification_updates',
|
||||
'view_notifications',
|
||||
@@ -607,6 +608,7 @@ class MainNavigation(Navigation):
|
||||
'view_job_updates',
|
||||
'view_letter_notification_as_preview',
|
||||
'view_letter_template_preview',
|
||||
'view_letter_upload_as_preview',
|
||||
'view_notification_updates',
|
||||
'view_notifications_csv',
|
||||
'view_provider',
|
||||
@@ -887,6 +889,7 @@ class CaseworkNavigation(Navigation):
|
||||
'view_job_updates',
|
||||
'view_letter_notification_as_preview',
|
||||
'view_letter_template_preview',
|
||||
'view_letter_upload_as_preview',
|
||||
'view_notification_updates',
|
||||
'view_notifications_csv',
|
||||
'view_provider',
|
||||
@@ -1170,6 +1173,7 @@ class OrgNavigation(Navigation):
|
||||
'view_jobs',
|
||||
'view_letter_notification_as_preview',
|
||||
'view_letter_template_preview',
|
||||
'view_letter_upload_as_preview',
|
||||
'view_notification',
|
||||
'view_notification_updates',
|
||||
'view_notifications',
|
||||
|
||||
@@ -261,6 +261,12 @@ class ServiceAPIClient(NotifyAdminAPIClient):
|
||||
)
|
||||
return self.get(endpoint)
|
||||
|
||||
def get_precompiled_template(self, service_id):
|
||||
"""
|
||||
Returns the precompiled template for a service, creating it if it doesn't already exist
|
||||
"""
|
||||
return self.get('/service/{}/template/precompiled'.format(service_id))
|
||||
|
||||
@cache.set('service-{service_id}-templates')
|
||||
def get_service_templates(self, service_id):
|
||||
"""
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from boto3 import resource
|
||||
from flask import current_app
|
||||
from notifications_utils.s3 import s3upload as utils_s3upload
|
||||
|
||||
@@ -14,3 +15,13 @@ def upload_letter_to_s3(data, file_location, status):
|
||||
file_location=file_location,
|
||||
metadata={'status': status}
|
||||
)
|
||||
|
||||
|
||||
def get_letter_pdf_and_metadata(file_location):
|
||||
s3 = resource('s3')
|
||||
s3_object = s3.Object(current_app.config['TRANSIENT_UPLOADED_LETTERS'], file_location).get()
|
||||
|
||||
pdf = s3_object['Body'].read()
|
||||
metadata = s3_object['Metadata']
|
||||
|
||||
return pdf, metadata
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import base64
|
||||
from io import BytesIO
|
||||
|
||||
import requests
|
||||
from flask import current_app, json
|
||||
from notifications_utils.pdf import extract_page_from_pdf
|
||||
|
||||
from app import current_service
|
||||
|
||||
@@ -24,6 +28,36 @@ class TemplatePreview:
|
||||
)
|
||||
return (resp.content, resp.status_code, resp.headers.items())
|
||||
|
||||
@classmethod
|
||||
def from_valid_pdf_file(cls, pdf_file, page):
|
||||
pdf_page = extract_page_from_pdf(BytesIO(pdf_file), int(page) - 1)
|
||||
|
||||
response = requests.post(
|
||||
'{}/precompiled-preview.png{}'.format(
|
||||
current_app.config['TEMPLATE_PREVIEW_API_HOST'],
|
||||
'?hide_notify=true' if page == '1' else ''
|
||||
),
|
||||
data=base64.b64encode(pdf_page).decode('utf-8'),
|
||||
headers={'Authorization': 'Token {}'.format(current_app.config['TEMPLATE_PREVIEW_API_KEY'])}
|
||||
)
|
||||
|
||||
return (response.content, response.status_code, response.headers.items())
|
||||
|
||||
@classmethod
|
||||
def from_invalid_pdf_file(cls, pdf_file, page):
|
||||
pdf_page = extract_page_from_pdf(BytesIO(pdf_file), int(page) - 1)
|
||||
|
||||
response = requests.post(
|
||||
'{}/precompiled/overlay.png{}'.format(
|
||||
current_app.config['TEMPLATE_PREVIEW_API_HOST'],
|
||||
'?page_number={}'.format(page)
|
||||
),
|
||||
data=pdf_page,
|
||||
headers={'Authorization': 'Token {}'.format(current_app.config['TEMPLATE_PREVIEW_API_KEY'])}
|
||||
)
|
||||
|
||||
return (response.content, response.status_code, response.headers.items())
|
||||
|
||||
@classmethod
|
||||
def from_example_template(cls, template, filename):
|
||||
data = {
|
||||
|
||||
@@ -16,4 +16,8 @@
|
||||
Validation failed
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="letter-sent">
|
||||
{{ template|string }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -30,6 +30,7 @@ def test_post_upload_letter_redirects_for_valid_file(mocker, client_request):
|
||||
antivirus_mock = mocker.patch('app.main.views.uploads.antivirus_client.scan', return_value=True)
|
||||
mocker.patch('app.main.views.uploads.sanitise_letter', return_value=Mock(content='The sanitised content'))
|
||||
mock_s3 = mocker.patch('app.main.views.uploads.upload_letter_to_s3')
|
||||
mocker.patch('app.main.views.uploads.service_api_client.get_precompiled_template')
|
||||
|
||||
with open('tests/test_pdf_files/one_page_pdf.pdf', 'rb') as file:
|
||||
page = client_request.post(
|
||||
@@ -50,6 +51,43 @@ def test_post_upload_letter_redirects_for_valid_file(mocker, client_request):
|
||||
assert not page.find(id='validation-error-message')
|
||||
|
||||
|
||||
def test_post_upload_letter_shows_letter_preview_for_valid_file(mocker, client_request):
|
||||
letter_template = {'template_type': 'letter',
|
||||
'reply_to_text': '',
|
||||
'postage': 'second',
|
||||
'subject': 'hi',
|
||||
'content': 'my letter'}
|
||||
|
||||
mocker.patch('uuid.uuid4', return_value='fake-uuid')
|
||||
mocker.patch('app.main.views.uploads.antivirus_client.scan', return_value=True)
|
||||
mocker.patch('app.main.views.uploads.sanitise_letter', return_value=Mock(content='The sanitised content'))
|
||||
mocker.patch('app.main.views.uploads.upload_letter_to_s3')
|
||||
mocker.patch('app.main.views.uploads.pdf_page_count', return_value=3)
|
||||
mocker.patch('app.main.views.uploads.service_api_client.get_precompiled_template', return_value=letter_template)
|
||||
|
||||
with open('tests/test_pdf_files/one_page_pdf.pdf', 'rb') as file:
|
||||
page = client_request.post(
|
||||
'main.upload_letter',
|
||||
service_id=SERVICE_ONE_ID,
|
||||
_data={'file': file},
|
||||
_follow_redirects=True,
|
||||
)
|
||||
|
||||
assert len(page.select('.letter-postage')) == 1
|
||||
assert normalize_spaces(page.select_one('.letter-postage').text) == ('Postage: second class')
|
||||
assert page.select_one('.letter-postage')['class'] == ['letter-postage', 'letter-postage-second']
|
||||
|
||||
letter_images = page.select('main img')
|
||||
assert len(letter_images) == 3
|
||||
|
||||
for page_no, img in enumerate(letter_images, start=1):
|
||||
assert img['src'] == url_for(
|
||||
'.view_letter_upload_as_preview',
|
||||
service_id=SERVICE_ONE_ID,
|
||||
file_id='fake-uuid',
|
||||
page=page_no)
|
||||
|
||||
|
||||
def test_post_upload_letter_shows_error_when_file_is_not_a_pdf(client_request):
|
||||
with open('tests/non_spreadsheet_files/actually_a_png.csv', 'rb') as file:
|
||||
page = client_request.post(
|
||||
@@ -121,6 +159,7 @@ def test_post_upload_letter_with_invalid_file(mocker, client_request):
|
||||
mock_sanitise_response = Mock()
|
||||
mock_sanitise_response.raise_for_status.side_effect = RequestException(response=Mock(status_code=400))
|
||||
mocker.patch('app.main.views.uploads.sanitise_letter', return_value=mock_sanitise_response)
|
||||
mocker.patch('app.main.views.uploads.service_api_client.get_precompiled_template')
|
||||
|
||||
with open('tests/test_pdf_files/one_page_pdf.pdf', 'rb') as file:
|
||||
file_contents = file.read()
|
||||
@@ -145,6 +184,43 @@ def test_post_upload_letter_with_invalid_file(mocker, client_request):
|
||||
) == 'Validation failed'
|
||||
|
||||
|
||||
def test_post_upload_letter_shows_letter_preview_for_invalid_file(mocker, client_request):
|
||||
letter_template = {'template_type': 'letter',
|
||||
'reply_to_text': '',
|
||||
'postage': 'first',
|
||||
'subject': 'hi',
|
||||
'content': 'my letter'}
|
||||
|
||||
mocker.patch('uuid.uuid4', return_value='fake-uuid')
|
||||
mocker.patch('app.main.views.uploads.antivirus_client.scan', return_value=True)
|
||||
mocker.patch('app.main.views.uploads.upload_letter_to_s3')
|
||||
mock_sanitise_response = Mock()
|
||||
mock_sanitise_response.raise_for_status.side_effect = RequestException(response=Mock(status_code=400))
|
||||
mocker.patch('app.main.views.uploads.sanitise_letter', return_value=mock_sanitise_response)
|
||||
mocker.patch('app.main.views.uploads.service_api_client.get_precompiled_template', return_value=letter_template)
|
||||
|
||||
with open('tests/test_pdf_files/one_page_pdf.pdf', 'rb') as file:
|
||||
page = client_request.post(
|
||||
'main.upload_letter',
|
||||
service_id=SERVICE_ONE_ID,
|
||||
_data={'file': file},
|
||||
_follow_redirects=True,
|
||||
)
|
||||
|
||||
assert len(page.select('.letter-postage')) == 1
|
||||
assert normalize_spaces(page.select_one('.letter-postage').text) == ('Postage: first class')
|
||||
assert page.select_one('.letter-postage')['class'] == ['letter-postage', 'letter-postage-first']
|
||||
|
||||
letter_images = page.select('main img')
|
||||
assert len(letter_images) == 1
|
||||
assert letter_images[0]['src'] == url_for(
|
||||
'.view_letter_upload_as_preview',
|
||||
service_id=SERVICE_ONE_ID,
|
||||
file_id='fake-uuid',
|
||||
page=1
|
||||
)
|
||||
|
||||
|
||||
def test_post_upload_letter_does_not_upload_to_s3_if_template_preview_raises_unknown_error(mocker, client_request):
|
||||
mocker.patch('uuid.uuid4', return_value='fake-uuid')
|
||||
mocker.patch('app.main.views.uploads.antivirus_client.scan', return_value=True)
|
||||
@@ -164,12 +240,17 @@ def test_post_upload_letter_does_not_upload_to_s3_if_template_preview_raises_unk
|
||||
assert not mock_s3.called
|
||||
|
||||
|
||||
def test_uploaded_letter_preview(client_request):
|
||||
def test_uploaded_letter_preview(mocker, client_request):
|
||||
mocker.patch('app.main.views.uploads.service_api_client')
|
||||
|
||||
page = client_request.get(
|
||||
'main.uploaded_letter_preview',
|
||||
service_id=SERVICE_ONE_ID,
|
||||
file_id='fake-uuid',
|
||||
original_filename='my_letter.pdf',
|
||||
page_count=1,
|
||||
status='valid',
|
||||
)
|
||||
|
||||
assert page.find('h1').text == 'my_letter.pdf'
|
||||
assert page.find('div', class_='letter-sent')
|
||||
|
||||
@@ -93,6 +93,14 @@ def test_client_creates_service_with_correct_data(
|
||||
)
|
||||
|
||||
|
||||
def test_get_precompiled_template(mocker):
|
||||
client = ServiceAPIClient()
|
||||
mock_get = mocker.patch.object(client, 'get')
|
||||
|
||||
client.get_precompiled_template(SERVICE_ONE_ID)
|
||||
mock_get.assert_called_once_with('/service/{}/template/precompiled'.format(SERVICE_ONE_ID))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('template_data, extra_args, expected_count', (
|
||||
(
|
||||
[],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import base64
|
||||
from functools import partial
|
||||
from unittest.mock import Mock
|
||||
|
||||
@@ -79,6 +80,44 @@ def test_from_database_object_makes_request(
|
||||
request_mock.assert_called_once_with(expected_url, json=data, headers=headers)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('page_number, expected_url', [
|
||||
('1', 'http://localhost:9999/precompiled-preview.png?hide_notify=true'),
|
||||
('2', 'http://localhost:9999/precompiled-preview.png'),
|
||||
])
|
||||
def test_from_valid_pdf_file_makes_request(mocker, page_number, expected_url):
|
||||
mocker.patch('app.template_previews.extract_page_from_pdf', return_value=b'pdf page')
|
||||
request_mock = mocker.patch(
|
||||
'app.template_previews.requests.post',
|
||||
return_value=Mock(content='a', status_code='b', headers={'c': 'd'})
|
||||
)
|
||||
|
||||
response = TemplatePreview.from_valid_pdf_file(b'pdf file', page_number)
|
||||
|
||||
assert response == ('a', 'b', {'c': 'd'}.items())
|
||||
request_mock.assert_called_once_with(
|
||||
expected_url,
|
||||
data=base64.b64encode(b'pdf page').decode('utf-8'),
|
||||
headers={'Authorization': 'Token my-secret-key'},
|
||||
)
|
||||
|
||||
|
||||
def test_from_invalid_pdf_file_makes_request(mocker):
|
||||
mocker.patch('app.template_previews.extract_page_from_pdf', return_value=b'pdf page')
|
||||
request_mock = mocker.patch(
|
||||
'app.template_previews.requests.post',
|
||||
return_value=Mock(content='a', status_code='b', headers={'c': 'd'})
|
||||
)
|
||||
|
||||
response = TemplatePreview.from_invalid_pdf_file(b'pdf file', '1')
|
||||
|
||||
assert response == ('a', 'b', {'c': 'd'}.items())
|
||||
request_mock.assert_called_once_with(
|
||||
'http://localhost:9999/precompiled/overlay.png?page_number=1',
|
||||
data=b'pdf page',
|
||||
headers={'Authorization': 'Token my-secret-key'},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('template_type', [
|
||||
'email', 'sms'
|
||||
])
|
||||
|
||||
Reference in New Issue
Block a user