From f2e163dc4324fda8fe5c8d1b1b83b8fefab0fbab Mon Sep 17 00:00:00 2001 From: Alexey Bezhan Date: Wed, 4 Apr 2018 17:34:14 +0100 Subject: [PATCH] Upload files from personalisation data to document download Adds support for a new personalisation value type: file upload. File uploads are represented as a dictionary with a "file" key and a base64-encoded file data as the key's value: ``` personalisation={ 'field1': {'file': ''} } ``` Post notification endpoint checks the request personalisation data looking for the file uploads in personalisation data. If any are found and the service has permissions to upload documents the files are sent to document download API and personalisation values are replaced with the URLs returned in the document download response. A fake document URL is returned for simulated notifications, no documents are stored in Document Download. Multiple files can be uploaded for one notification by providing a file upload in more than one personalisation field. --- app/v2/notifications/post_notifications.py | 33 ++++++- .../notifications/test_post_notifications.py | 96 ++++++++++++++++++- 2 files changed, 124 insertions(+), 5 deletions(-) diff --git a/app/v2/notifications/post_notifications.py b/app/v2/notifications/post_notifications.py index ef1941d01..998636859 100644 --- a/app/v2/notifications/post_notifications.py +++ b/app/v2/notifications/post_notifications.py @@ -7,7 +7,7 @@ from flask import request, jsonify, current_app, abort from notifications_utils.pdf import pdf_page_count, PdfReadError from notifications_utils.recipients import try_validate_and_format_phone_number -from app import api_user, authenticated_service, notify_celery +from app import api_user, authenticated_service, notify_celery, document_download_client from app.config import QueueNames, TaskNames from app.dao.notifications_dao import dao_update_notification, update_notification_status_by_reference from app.dao.templates_dao import dao_create_template @@ -19,6 +19,7 @@ from app.models import ( EMAIL_TYPE, LETTER_TYPE, PRECOMPILED_LETTER, + UPLOAD_DOCUMENT, PRIORITY, KEY_TYPE_TEST, KEY_TYPE_TEAM, @@ -136,7 +137,7 @@ def post_notification(notification_type): reply_to_text=reply_to ) else: - notification = process_sms_or_email_notification( + notification, personalisation = process_sms_or_email_notification( form=form, notification_type=notification_type, api_key=api_user, @@ -145,6 +146,8 @@ def post_notification(notification_type): reply_to_text=reply_to ) + template_with_content.values = personalisation + if notification_type == SMS_TYPE: create_resp_partial = functools.partial( create_post_sms_response_from_notification, @@ -182,12 +185,14 @@ def process_sms_or_email_notification(*, form, notification_type, api_key, templ # Do not persist or send notification to the queue if it is a simulated recipient simulated = simulated_recipient(send_to, notification_type) + personalisation = process_document_uploads(form.get('personalisation'), service, simulated=simulated) + notification = persist_notification( template_id=template.id, template_version=template.version, recipient=form_send_to, service=service, - personalisation=form.get('personalisation', None), + personalisation=personalisation, notification_type=notification_type, api_key_id=api_key.id, key_type=api_key.key_type, @@ -210,7 +215,27 @@ def process_sms_or_email_notification(*, form, notification_type, api_key, templ else: current_app.logger.debug("POST simulated notification for id: {}".format(notification.id)) - return notification + return notification, personalisation + + +def process_document_uploads(personalisation_data, service, simulated=False): + file_keys = [k for k, v in (personalisation_data or {}).items() if isinstance(v, dict) and 'file' in v] + if not file_keys: + return personalisation_data + + personalisation_data = personalisation_data.copy() + + check_service_has_permission(UPLOAD_DOCUMENT, authenticated_service.permissions) + + for key in file_keys: + if simulated: + personalisation_data[key] = document_download_client.get_upload_url(service.id) + '/test-document' + else: + personalisation_data[key] = document_download_client.upload_document( + service.id, personalisation_data[key]['file'] + ) + + return personalisation_data def process_letter_notification(*, letter_data, api_key, template, reply_to_text, precompiled=False): diff --git a/tests/app/v2/notifications/test_post_notifications.py b/tests/app/v2/notifications/test_post_notifications.py index 9c2927e76..2bd3c1b96 100644 --- a/tests/app/v2/notifications/test_post_notifications.py +++ b/tests/app/v2/notifications/test_post_notifications.py @@ -9,7 +9,8 @@ from app.models import ( EMAIL_TYPE, NOTIFICATION_CREATED, SCHEDULE_NOTIFICATIONS, - SMS_TYPE + SMS_TYPE, + UPLOAD_DOCUMENT ) from flask import json, current_app @@ -697,3 +698,96 @@ def test_post_email_notification_with_invalid_reply_to_id_returns_400(client, sa assert 'email_reply_to_id {} does not exist in database for service id {}'. \ format(fake_uuid, sample_email_template.service_id) in resp_json['errors'][0]['message'] assert 'BadRequestError' in resp_json['errors'][0]['error'] + + +def test_post_notification_with_document_upload(client, notify_db, notify_db_session, mocker): + service = sample_service(notify_db, notify_db_session, permissions=[EMAIL_TYPE, UPLOAD_DOCUMENT]) + template = create_sample_template( + notify_db, notify_db_session, service=service, + template_type='email', + content="Document: ((document))" + ) + + mocker.patch('app.celery.provider_tasks.deliver_email.apply_async') + document_download_mock = mocker.patch('app.v2.notifications.post_notifications.document_download_client') + document_download_mock.upload_document.return_value = 'https://document-url/' + + data = { + "email_address": service.users[0].email_address, + "template_id": template.id, + "personalisation": {"document": {"file": "abababab"}} + } + + auth_header = create_authorization_header(service_id=service.id) + response = client.post( + path="v2/notifications/email", + data=json.dumps(data), + headers=[('Content-Type', 'application/json'), auth_header]) + + assert response.status_code == 201, response.get_data(as_text=True) + resp_json = json.loads(response.get_data(as_text=True)) + assert validate(resp_json, post_email_response) == resp_json + + notification = Notification.query.one() + assert notification.status == NOTIFICATION_CREATED + assert notification.personalisation == {'document': 'https://document-url/'} + + assert resp_json['content']['body'] == 'Document: https://document-url/' + + +def test_post_notification_with_document_upload_simulated(client, notify_db, notify_db_session, mocker): + service = sample_service(notify_db, notify_db_session, permissions=[EMAIL_TYPE, UPLOAD_DOCUMENT]) + template = create_sample_template( + notify_db, notify_db_session, service=service, + template_type='email', + content="Document: ((document))" + ) + + mocker.patch('app.celery.provider_tasks.deliver_email.apply_async') + document_download_mock = mocker.patch('app.v2.notifications.post_notifications.document_download_client') + document_download_mock.get_upload_url.return_value = 'https://document-url' + + data = { + "email_address": 'simulate-delivered@notifications.service.gov.uk', + "template_id": template.id, + "personalisation": {"document": {"file": "abababab"}} + } + + auth_header = create_authorization_header(service_id=service.id) + response = client.post( + path="v2/notifications/email", + data=json.dumps(data), + headers=[('Content-Type', 'application/json'), auth_header]) + + assert response.status_code == 201, response.get_data(as_text=True) + resp_json = json.loads(response.get_data(as_text=True)) + assert validate(resp_json, post_email_response) == resp_json + + assert resp_json['content']['body'] == 'Document: https://document-url/test-document' + + +def test_post_notification_without_document_upload_permission(client, notify_db, notify_db_session, mocker): + service = sample_service(notify_db, notify_db_session, permissions=[EMAIL_TYPE]) + template = create_sample_template( + notify_db, notify_db_session, service=service, + template_type='email', + content="Document: ((document))" + ) + + mocker.patch('app.celery.provider_tasks.deliver_email.apply_async') + document_download_mock = mocker.patch('app.v2.notifications.post_notifications.document_download_client') + document_download_mock.upload_document.return_value = 'https://document-url/' + + data = { + "email_address": service.users[0].email_address, + "template_id": template.id, + "personalisation": {"document": {"file": "abababab"}} + } + + auth_header = create_authorization_header(service_id=service.id) + response = client.post( + path="v2/notifications/email", + data=json.dumps(data), + headers=[('Content-Type', 'application/json'), auth_header]) + + assert response.status_code == 400, response.get_data(as_text=True)