diff --git a/app/main/forms.py b/app/main/forms.py index 5d840462a..50f510d19 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -1120,6 +1120,11 @@ class DateFilterForm(StripWhitespaceForm): include_from_test_key = BooleanField("Include test keys", default="checked", false_values={"N"}) +class RequiredDateFilterForm(StripWhitespaceForm): + start_date = DateField("Start Date") + end_date = DateField("End Date") + + class SearchByNameForm(StripWhitespaceForm): search = SearchField('Search by name') diff --git a/app/main/views/platform_admin.py b/app/main/views/platform_admin.py index 743f08e70..962c1c3b4 100644 --- a/app/main/views/platform_admin.py +++ b/app/main/views/platform_admin.py @@ -11,6 +11,7 @@ from app import ( complaint_api_client, format_date_numeric, letter_jobs_client, + notification_api_client, platform_stats_api_client, service_api_client, ) @@ -20,6 +21,7 @@ from app.main.forms import ( ClearCacheForm, DateFilterForm, PDFUploadForm, + RequiredDateFilterForm, ReturnedLettersForm, ) from app.statistics_utils import ( @@ -263,6 +265,33 @@ def performance_platform_xlsx(): } +@main.route("/platform-admin/reports/notifications-sent-by-service", methods=['GET', 'POST']) +@user_is_platform_admin +def notifications_sent_by_service(): + form = RequiredDateFilterForm() + + if form.validate_on_submit(): + start_date = form.start_date.data + end_date = form.end_date.data + + headers = [ + 'date_created', 'service_id', 'service_name', 'notification_type', 'count_sending', 'count_delivered', + 'count_technical_failure', 'count_temporary_failure', 'count_permanent_failure', 'count_sent' + ] + result = notification_api_client.get_notification_status_by_service(start_date, end_date) + + for row in result: + row[0] = datetime.strptime(row[0], '%a, %d %b %Y %X %Z').strftime('%Y-%m-%d') + + return Spreadsheet.from_rows([headers] + result).as_csv_data, 200, { + 'Content-Type': 'text/csv; charset=utf-8', + 'Content-Disposition': 'attachment; filename="{} to {} notification status per service report.csv"'.format( + start_date, end_date) + } + + return render_template('views/platform-admin/notifications_by_service.html', form=form) + + @main.route("/platform-admin/complaints") @user_is_platform_admin def platform_admin_list_complaints(): diff --git a/app/navigation.py b/app/navigation.py index 5d236f4ae..e2161f8ff 100644 --- a/app/navigation.py +++ b/app/navigation.py @@ -89,6 +89,7 @@ class HeaderNavigation(Navigation): 'letter_branding', 'live_services', 'live_services_csv', + 'notifications_sent_by_service', 'performance_platform_xlsx', 'platform_admin', 'platform_admin_letter_validation_preview', @@ -499,6 +500,7 @@ class MainNavigation(Navigation): 'message_status', 'manage_org_users', 'new_password', + 'notifications_sent_by_service', 'old_integration_testing', 'old_roadmap', 'old_service_dashboard', @@ -721,6 +723,7 @@ class CaseworkNavigation(Navigation): 'message_status', 'monthly', 'new_password', + 'notifications_sent_by_service', 'old_integration_testing', 'old_roadmap', 'old_service_dashboard', @@ -993,6 +996,7 @@ class OrgNavigation(Navigation): 'message_status', 'monthly', 'new_password', + 'notifications_sent_by_service', 'old_integration_testing', 'old_roadmap', 'old_service_dashboard', diff --git a/app/notify_client/notification_api_client.py b/app/notify_client/notification_api_client.py index 50d483ee9..66b16448a 100644 --- a/app/notify_client/notification_api_client.py +++ b/app/notify_client/notification_api_client.py @@ -110,5 +110,14 @@ class NotificationApiClient(NotifyAdminAPIClient): url='/service/{}/notifications/{}/cancel'.format(service_id, notification_id), data={}) + def get_notification_status_by_service(self, start_date, end_date): + return self.get( + url='service/monthly-data-by-service', + params={ + 'start_date': str(start_date), + 'end_date': str(end_date), + } + ) + notification_api_client = NotificationApiClient() diff --git a/app/templates/views/platform-admin/notifications_by_service.html b/app/templates/views/platform-admin/notifications_by_service.html new file mode 100644 index 000000000..2bed2ba3b --- /dev/null +++ b/app/templates/views/platform-admin/notifications_by_service.html @@ -0,0 +1,20 @@ +{% extends "views/platform-admin/_base_template.html" %} +{% from "components/form.html" import form_wrapper %} + +{% block per_page_title %} + Monthly notification statuses for live services +{% endblock %} + +{% block platform_admin_content %} + +

+ Monthly notification statuses for live services +

+ + {% call form_wrapper() %} + {{ textbox(form.start_date, hint="Enter start date in format YYYY-MM-DD") }} + {{ textbox(form.end_date, hint="Enter end date in format YYYY-MM-DD") }} + {{ page_footer('Download report') }} + {% endcall %} + +{% endblock %} diff --git a/app/templates/views/platform-admin/reports.html b/app/templates/views/platform-admin/reports.html index bed419859..702651b8d 100644 --- a/app/templates/views/platform-admin/reports.html +++ b/app/templates/views/platform-admin/reports.html @@ -17,4 +17,8 @@

Download performance platform report (.xlsx)

+ +

+ Monthly notification statuses for live services +

{% endblock %} diff --git a/tests/app/main/views/test_platform_admin.py b/tests/app/main/views/test_platform_admin.py index 8b88ba547..d3c475456 100644 --- a/tests/app/main/views/test_platform_admin.py +++ b/tests/app/main/views/test_platform_admin.py @@ -19,7 +19,12 @@ from app.main.views.platform_admin import ( sum_service_usage, ) from tests import service_json -from tests.conftest import SERVICE_ONE_ID, mock_get_user, normalize_spaces +from tests.conftest import ( + SERVICE_ONE_ID, + SERVICE_TWO_ID, + mock_get_user, + normalize_spaces, +) @pytest.mark.parametrize('endpoint', [ @@ -968,6 +973,10 @@ def test_reports_page( 'a', text="Download performance platform report (.xlsx)" ).attrs['href'] == '/platform-admin/reports/performance-platform.xlsx' + assert page.find( + 'a', text="Monthly notification statuses for live services" + ).attrs['href'] == url_for('main.notifications_sent_by_service') + def test_get_live_services_report(client, platform_admin_user, mocker): mock_get_user(mocker, user=platform_admin_user) @@ -1034,3 +1043,76 @@ def test_get_performance_platform_report(client, platform_admin_user, mocker): ['abc123', 'Forest', 'jessie the oak tree', '2014-03-29T00:00:00Z', 'govuk-notify', 1], ['def456', 'Forest', 'james the pine tree', '', 'govuk-notify', 1], ] + + +def test_get_notifications_sent_by_service_shows_date_form(client_request, platform_admin_user): + client_request.login(platform_admin_user) + page = client_request.get('main.notifications_sent_by_service') + + assert [ + (input['type'], input['name'], input['value']) + for input in page.select('input') + ] == [ + ('text', 'start_date', ''), + ('text', 'end_date', ''), + ('hidden', 'csrf_token', ANY) + ] + + +def test_get_notifications_sent_by_service_validates_form(mocker, client_request, platform_admin_user): + mock_get_stats_from_api = mocker.patch('app.main.views.platform_admin.notification_api_client') + + client_request.login(platform_admin_user) + + page = client_request.post( + 'main.notifications_sent_by_service', + _expected_status=200, + _data={'start_date': '', 'end_date': '20190101'} + ) + + errors = page.select('.error-message') + assert len(errors) == 2 + + for error in errors: + assert normalize_spaces(error.text) == 'Not a valid date value' + + mock_get_stats_from_api.assert_not_called() + + +def test_get_notifications_sent_by_service_calls_api_and_downloads_data( + mocker, + client, + platform_admin_user, + service_one, + service_two, +): + mock_get_user(mocker, user=platform_admin_user) + api_data = [ + ['Tue, 01 Jan 2019 00:00:00 GMT', SERVICE_ONE_ID, service_one['name'], 'email', 191, 0, 0, 14, 0, 0], + ['Tue, 01 Jan 2019 00:00:00 GMT', SERVICE_ONE_ID, service_one['name'], 'sms', 42, 0, 0, 8, 0, 0], + ['Tue, 01 Jan 2019 00:00:00 GMT', SERVICE_TWO_ID, service_two['name'], 'email', 3, 1, 0, 2, 0, 0], + ] + mocker.patch('app.main.views.platform_admin.notification_api_client.get_notification_status_by_service', + return_value=api_data) + start_date = datetime.date(2019, 1, 1) + end_date = datetime.date(2019, 1, 31) + + client.login(platform_admin_user) + + response = client.post( + url_for('main.notifications_sent_by_service'), + data={'start_date': start_date, 'end_date': end_date} + ) + + assert response.status_code == 200 + assert response.content_type == 'text/csv; charset=utf-8' + assert response.headers['Content-Disposition'] == ( + 'attachment; filename="{} to {} notification status per service report.csv"'.format(start_date, end_date) + ) + assert response.get_data(as_text=True) == ( + 'date_created,service_id,service_name,notification_type,count_sending,count_delivered,count_technical_failure,' + 'count_temporary_failure,count_permanent_failure,count_sent\r\n' + '2019-01-01,596364a0-858e-42c8-9062-a8fe822260eb,service one,email,191,0,0,14,0,0\r\n' + '2019-01-01,596364a0-858e-42c8-9062-a8fe822260eb,service one,sms,42,0,0,8,0,0\r\n' + '2019-01-01,147ad62a-2951-4fa1-9ca0-093cd1a52c52,service two,email,3,1,0,2,0,0\r\n' + ) diff --git a/tests/app/notify_client/test_notify_admin_api_client.py b/tests/app/notify_client/test_notify_admin_api_client.py index bcb292e90..b2f3959df 100644 --- a/tests/app/notify_client/test_notify_admin_api_client.py +++ b/tests/app/notify_client/test_notify_admin_api_client.py @@ -1,4 +1,5 @@ import uuid +from datetime import date from unittest.mock import patch import pytest @@ -6,6 +7,7 @@ import werkzeug from app.models.service import Service from app.notify_client import NotifyAdminAPIClient +from app.notify_client.notification_api_client import notification_api_client from tests import service_json from tests.conftest import api_user_active, platform_admin_user, set_config @@ -102,3 +104,16 @@ def test_generate_headers_sets_request_id_if_in_request_context(app_): } assert headers['X-B3-TraceId'] == request_context.request.request_id assert headers['X-B3-SpanId'] == request_context.request.span_id + + +def test_get_notification_status_by_service(mocker): + mock_get = mocker.patch.object(notification_api_client, 'get') + start_date = date(2019, 4, 1) + end_date = date(2019, 4, 30) + + notification_api_client.get_notification_status_by_service(start_date, end_date) + + mock_get.assert_called_once_with( + url='service/monthly-data-by-service', + params={'start_date': '2019-04-01', 'end_date': '2019-04-30'} + )