diff --git a/app/__init__.py b/app/__init__.py index 0436f5e98..62d070d19 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -85,6 +85,7 @@ from app.notify_client.template_statistics_api_client import ( from app.notify_client.user_api_client import user_api_client from app.url_converters import ( LetterFileExtensionConverter, + SimpleDateTypeConverter, TemplateTypeConverter, ) from app.utils import format_thousands, get_logo_cdn_domain, id_safe @@ -221,6 +222,7 @@ def init_app(application): application.url_map.converters['uuid'].to_python = lambda self, value: value application.url_map.converters['template_type'] = TemplateTypeConverter application.url_map.converters['letter_file_extension'] = LetterFileExtensionConverter + application.url_map.converters['simple_date'] = SimpleDateTypeConverter def convert_to_boolean(value): diff --git a/app/main/__init__.py b/app/main/__init__.py index 4bb70bf45..97aa9f228 100644 --- a/app/main/__init__.py +++ b/app/main/__init__.py @@ -29,6 +29,7 @@ from app.main.views import ( # noqa isort:skip platform_admin, providers, register, + returned_letters, send, service_settings, sign_in, diff --git a/app/main/views/returned_letters.py b/app/main/views/returned_letters.py new file mode 100644 index 000000000..6d6b24d12 --- /dev/null +++ b/app/main/views/returned_letters.py @@ -0,0 +1,46 @@ +from collections import OrderedDict + +from flask import render_template + +from app import service_api_client +from app.main import main +from app.utils import Spreadsheet, user_has_permissions + + +@main.route("/services//returned-letter-summary", methods=["GET"]) +@user_has_permissions('view_activity') +def returned_letter_summary(service_id): + summary = service_api_client.get_returned_letter_summary(service_id) + return render_template( + 'views/returned-letter-summary.html', + data=summary, + ) + + +@main.route("/services//returned-letters-csv/", methods=["GET"]) +@user_has_permissions('view_activity') +def returned_letters_report(service_id, reported_at): + returned_letters = service_api_client.get_returned_letters(service_id, reported_at) + column_names = OrderedDict([ + ('notification_id', 'Notification ID'), + ('client_reference', 'Reference'), + ('created_at', 'Date sent'), + ('email_address', 'Sent by'), + ('template_name', 'Template name'), + ('template_id', 'Template ID'), + ('template_version', 'Template version'), + ('original_file_name', 'Spreadsheet file name'), + ('job_row_number', 'Spreadsheet row number'), + ('uploaded_letter_file_name', 'Uploaded letter file name') + ]) + + # initialise with header row + data = [[x for x in column_names.values()]] + + for row in returned_letters: + data.append([row[key] for key in column_names.keys()]) + + return Spreadsheet.from_rows(data).as_csv_data, 200, { + 'Content-Type': 'text/csv; charset=utf-8', + 'Content-Disposition': 'inline; filename="{} returned letters.csv"'.format(reported_at) + } diff --git a/app/navigation.py b/app/navigation.py index 4e9cdef8c..fe2f2092b 100644 --- a/app/navigation.py +++ b/app/navigation.py @@ -242,6 +242,8 @@ class HeaderNavigation(Navigation): 'resend_email_link', 'resend_email_verification', 'resume_service', + 'returned_letter_summary', + 'returned_letters_report', 'revoke_api_key', 'robots', 'send_messages', @@ -564,6 +566,8 @@ class MainNavigation(Navigation): 'resend_email_link', 'resend_email_verification', 'resume_service', + 'returned_letter_summary', + 'returned_letters_report', 'roadmap', 'robots', 'security', @@ -801,6 +805,8 @@ class CaseworkNavigation(Navigation): 'resend_email_link', 'resend_email_verification', 'resume_service', + 'returned_letter_summary', + 'returned_letters_report', 'revoke_api_key', 'roadmap', 'robots', @@ -1077,6 +1083,8 @@ class OrgNavigation(Navigation): 'resend_email_link', 'resend_email_verification', 'resume_service', + 'returned_letter_summary', + 'returned_letters_report', 'revoke_api_key', 'roadmap', 'robots', diff --git a/app/notify_client/service_api_client.py b/app/notify_client/service_api_client.py index 8c45fb1b3..eaaf5b59e 100644 --- a/app/notify_client/service_api_client.py +++ b/app/notify_client/service_api_client.py @@ -569,5 +569,11 @@ class ServiceAPIClient(NotifyAdminAPIClient): def get_service_data_retention(self, service_id): return self.get("/service/{}/data-retention".format(service_id)) + def get_returned_letter_summary(self, service_id): + return self.get("service/{}/returned-letter-summary".format(service_id)) + + def get_returned_letters(self, service_id, reported_at): + return self.get("service/{}/returned-letters?reported_at={}".format(service_id, reported_at)) + service_api_client = ServiceAPIClient() diff --git a/app/templates/views/returned-letter-summary.html b/app/templates/views/returned-letter-summary.html new file mode 100644 index 000000000..9c50d1c40 --- /dev/null +++ b/app/templates/views/returned-letter-summary.html @@ -0,0 +1,27 @@ +{% from "components/table.html" import list_table, field %} +{% from "components/message-count-label.html" import message_count_label %} +{% extends "withnav_template.html" %} + +{% block service_page_title %} + Returned letters +{% endblock %} + +{% block maincolumn_content %} +

+ Returned letters +

+
+ {% call(item, row_number) list_table( + data, + caption="Returned letters report", + caption_visible=False, + empty_message='If you have returned letter reports they will be listed here' + ) %} + {% call field() %} + Returned letters reported on {{ item.reported_at | format_date}} - {{ item.returned_letter_count}} {{ message_count_label(item.returned_letter_count, 'letter', suffix='')}} + {% endcall %} + {% endcall %} +
+ +{% endblock %} \ No newline at end of file diff --git a/app/url_converters.py b/app/url_converters.py index e90012f59..9dc649b5a 100644 --- a/app/url_converters.py +++ b/app/url_converters.py @@ -9,3 +9,7 @@ class TemplateTypeConverter(BaseConverter): class LetterFileExtensionConverter(BaseConverter): regex = '(?:pdf|png)' + + +class SimpleDateTypeConverter(BaseConverter): + regex = r'([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))' diff --git a/tests/app/main/views/test_returned_letters.py b/tests/app/main/views/test_returned_letters.py new file mode 100644 index 000000000..eb373b07d --- /dev/null +++ b/tests/app/main/views/test_returned_letters.py @@ -0,0 +1,84 @@ +from flask import url_for + +from tests.conftest import SERVICE_ONE_ID, normalize_spaces + + +def test_returned_letter_summary( + client_request, + mocker +): + summary_data = [{'returned_letter_count': 30, 'reported_at': '2019-12-24'}] + mock = mocker.patch("app.service_api_client.get_returned_letter_summary", + return_value=summary_data) + + page = client_request.get("main.returned_letter_summary", service_id=SERVICE_ONE_ID) + + mock.assert_called_once_with(SERVICE_ONE_ID) + + expected_text = "Returned letters reported on Tuesday 24 December 2019 - 30 letters" + assert page.h1.string.strip() == 'Returned letters' + assert normalize_spaces(page.select('.table-field-left-aligned')[0].text) == expected_text + assert page.select_one('.table-field-left-aligned a')['href'] == url_for('.returned_letters_report', + service_id=SERVICE_ONE_ID, + reported_at='2019-12-24') + + +def test_returned_letter_summary_with_one_letter( + client_request, + mocker +): + summary_data = [{'returned_letter_count': 1, 'reported_at': '2019-12-24'}] + mock = mocker.patch("app.service_api_client.get_returned_letter_summary", + return_value=summary_data) + + page = client_request.get("main.returned_letter_summary", service_id=SERVICE_ONE_ID) + + mock.assert_called_once_with(SERVICE_ONE_ID) + + expected_text = "Returned letters reported on Tuesday 24 December 2019 - 1 letter" + assert page.h1.string.strip() == 'Returned letters' + assert normalize_spaces(page.select('.table-field-left-aligned')[0].text) == expected_text + + +def test_returned_letters_reports( + client_request, + mocker +): + data = [{ + 'notification_id': '12345678', + 'client_reference': '2344567', + 'created_at': '2019-12-24 13:30', + 'email_address': 'test@gov.uk', + 'template_name': 'First letter template', + 'template_id': '3445667', + 'template_version': 2, + 'original_file_name': None, + 'job_row_number': None, + 'uploaded_letter_file_name': 'test_letter.pdf', + }] + mock = mocker.patch("app.service_api_client.get_returned_letters", return_value=data) + + response = client_request.get_response("main.returned_letters_report", + service_id=SERVICE_ONE_ID, + reported_at='2019-12-24') + + report = response.get_data(as_text=True) + mock.assert_called_once_with(SERVICE_ONE_ID, '2019-12-24') + assert report.strip() == ( + 'Notification ID,Reference,Date sent,Sent by,Template name,Template ID,Template version,' + + 'Spreadsheet file name,Spreadsheet row number,Uploaded letter file name\r\n' + + '12345678,2344567,2019-12-24 13:30,test@gov.uk,' + + 'First letter template,3445667,2,,,test_letter.pdf' + ) + + +def test_returned_letters_reports_returns_404_for_bad_date( + client_request, + mocker +): + mock = mocker.patch("app.service_api_client.get_returned_letters") + client_request.get_response("main.returned_letters_report", + service_id=SERVICE_ONE_ID, + reported_at='19-12-2019', + _expected_status=404) + mock.assert_not_called() diff --git a/tests/conftest.py b/tests/conftest.py index c56337126..c5e4b371f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2921,6 +2921,19 @@ def client_request( assert_url_expected(resp.location, _expected_redirect) return BeautifulSoup(resp.data.decode('utf-8'), 'html.parser') + @staticmethod + def get_response( + endpoint, + _expected_status=200, + _optional_args="", + **endpoint_kwargs + ): + resp = logged_in_client.get( + url_for(endpoint, **(endpoint_kwargs or {})) + _optional_args, + ) + assert resp.status_code == _expected_status + return resp + return ClientRequest