Merge pull request #3233 from alphagov/returned-letters

Returned letters
This commit is contained in:
Rebecca Law
2019-12-31 09:30:01 +00:00
committed by GitHub
9 changed files with 191 additions and 0 deletions

View File

@@ -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):

View File

@@ -29,6 +29,7 @@ from app.main.views import ( # noqa isort:skip
platform_admin,
providers,
register,
returned_letters,
send,
service_settings,
sign_in,

View File

@@ -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/<uuid:service_id>/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/<uuid:service_id>/returned-letters-csv/<simple_date:reported_at>", 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)
}

View File

@@ -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',

View File

@@ -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()

View File

@@ -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 %}
<h1 class="heading-large">
Returned letters
</h1>
<div class="body-copy-table" id='pill-selected-item'>
{% 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() %}
<a target="_blank" class="govuk_link"
href="{{url_for('.returned_letters_report', service_id=current_service.id, reported_at=item.reported_at)}}">Returned letters reported on {{ item.reported_at | format_date}} - {{ item.returned_letter_count}} {{ message_count_label(item.returned_letter_count, 'letter', suffix='')}}</a>
{% endcall %}
{% endcall %}
</div>
{% endblock %}

View File

@@ -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]))'

View File

@@ -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()

View File

@@ -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