diff --git a/app/assets/stylesheets/app.scss b/app/assets/stylesheets/app.scss
index 7bc54d38f..0e64b1e23 100644
--- a/app/assets/stylesheets/app.scss
+++ b/app/assets/stylesheets/app.scss
@@ -251,3 +251,9 @@ details .arrow {
cursor: pointer;
}
}
+
+.bordered-text-box {
+ padding: 5px;
+ outline: 2px solid $black;
+ max-width: 100%;
+}
diff --git a/app/assets/stylesheets/components/big-number.scss b/app/assets/stylesheets/components/big-number.scss
index 57590d461..7e4fa81a8 100644
--- a/app/assets/stylesheets/components/big-number.scss
+++ b/app/assets/stylesheets/components/big-number.scss
@@ -16,6 +16,18 @@
}
+.big-number-dark {
+ @extend %big-number;
+ padding: $gutter-half;
+ position: relative;
+ background: $black;
+ color: $white;
+
+ .big-number-number {
+ @include bold-36($tabular-numbers: true);
+ }
+}
+
.big-number-smaller {
@extend %big-number;
diff --git a/app/main/views/platform_admin.py b/app/main/views/platform_admin.py
index 6321d862d..a8d80f037 100644
--- a/app/main/views/platform_admin.py
+++ b/app/main/views/platform_admin.py
@@ -1,7 +1,7 @@
import itertools
from datetime import datetime
-from flask import render_template, request
+from flask import render_template, request, url_for
from flask_login import login_required
from app import complaint_api_client, service_api_client
@@ -10,6 +10,10 @@ from app.main.forms import DateFilterForm
from app.statistics_utils import get_formatted_percentage
from app.utils import user_is_platform_admin
+COMPLAINT_THRESHOLD = 0.02
+FAILURE_THRESHOLD = 3
+ZERO_FAILURE_THRESHOLD = 0
+
@main.route("/platform-admin")
@login_required
@@ -38,6 +42,112 @@ def platform_admin():
)
+@main.route("/platform-admin-new")
+@login_required
+@user_is_platform_admin
+def platform_admin_new():
+ form = DateFilterForm(request.args)
+ api_args = {}
+
+ if form.start_date.data:
+ api_args['start_date'] = form.start_date.data
+ api_args['end_date'] = form.end_date.data or datetime.utcnow().date()
+
+ platform_stats = service_api_client.get_new_aggregate_platform_stats(api_args)
+ number_of_complaints = complaint_api_client.get_complaint_count(api_args)
+
+ return render_template(
+ 'views/platform-admin/index_new.html',
+ form=form,
+ global_stats=make_columns(platform_stats, number_of_complaints)
+ )
+
+
+def is_over_threshold(number, total, threshold):
+ percentage = number / total * 100 if total else 0
+ return percentage > threshold
+
+
+def get_status_box_data(stats, key, label, threshold=FAILURE_THRESHOLD):
+ return {
+ 'number': stats['failures'][key],
+ 'label': label,
+ 'failing': is_over_threshold(
+ stats['failures'][key],
+ stats['total'],
+ threshold
+ ),
+ 'percentage': get_formatted_percentage(stats['failures'][key], stats['total'])
+ }
+
+
+def get_tech_failure_status_box_data(stats):
+ stats = get_status_box_data(stats, 'technical-failure', 'technical failures', ZERO_FAILURE_THRESHOLD)
+ stats.pop('percentage')
+ return stats
+
+
+def make_columns(global_stats, complaints_number):
+ return [
+ # email
+ {
+ 'black_box': {
+ 'number': global_stats['email']['total'],
+ 'notification_type': 'email'
+ },
+ 'other_data': [
+ get_tech_failure_status_box_data(global_stats['email']),
+ get_status_box_data(global_stats['email'], 'permanent-failure', 'permanent failures'),
+ get_status_box_data(global_stats['email'], 'temporary-failure', 'temporary failures'),
+ {
+ 'number': complaints_number,
+ 'label': 'complaints',
+ 'failing': is_over_threshold(complaints_number,
+ global_stats['email']['total'], COMPLAINT_THRESHOLD),
+ 'percentage': get_formatted_percentage(complaints_number, global_stats['email']['total']),
+ 'url': url_for('main.platform_admin_list_complaints')
+ }
+ ],
+ 'test_data': {
+ 'number': global_stats['email']['test-key'],
+ 'label': 'test emails'
+ }
+ },
+ # sms
+ {
+ 'black_box': {
+ 'number': global_stats['sms']['total'],
+ 'notification_type': 'sms'
+ },
+ 'other_data': [
+ get_tech_failure_status_box_data(global_stats['sms']),
+ get_status_box_data(global_stats['sms'], 'permanent-failure', 'permanent failures'),
+ get_status_box_data(global_stats['sms'], 'temporary-failure', 'temporary failures')
+ ],
+ 'test_data': {
+ 'number': global_stats['sms']['test-key'],
+ 'label': 'test text messages'
+ }
+ },
+ # letter
+ {
+ 'black_box': {
+ 'number': global_stats['letter']['total'],
+ 'notification_type': 'letter'
+ },
+ 'other_data': [
+ get_tech_failure_status_box_data(global_stats['letter']),
+ get_status_box_data(global_stats['letter'],
+ 'virus-scan-failed', 'virus scan failures', ZERO_FAILURE_THRESHOLD)
+ ],
+ 'test_data': {
+ 'number': global_stats['letter']['test-key'],
+ 'label': 'test letters'
+ }
+ },
+ ]
+
+
@main.route("/platform-admin/live-services", endpoint='live_services')
@main.route("/platform-admin/trial-services", endpoint='trial_services')
@login_required
diff --git a/app/navigation.py b/app/navigation.py
index 2725f50ca..0b5f8bfc3 100644
--- a/app/navigation.py
+++ b/app/navigation.py
@@ -79,6 +79,7 @@ class HeaderNavigation(Navigation):
'live_services',
'organisations',
'platform_admin',
+ 'platform_admin_new',
'suspend_service',
'trial_services',
'update_email_branding',
@@ -421,6 +422,7 @@ class MainNavigation(Navigation):
'organisation_settings',
'organisations',
'platform_admin',
+ 'platform_admin_new',
'platform_admin_list_complaints',
'pricing',
'privacy',
@@ -590,6 +592,7 @@ class OrgNavigation(Navigation):
'old_using_notify',
'organisations',
'platform_admin',
+ 'platform_admin_new',
'platform_admin_list_complaints',
'pricing',
'privacy',
diff --git a/app/templates/components/big-number.html b/app/templates/components/big-number.html
index 454a0cc05..b5db30d5b 100644
--- a/app/templates/components/big-number.html
+++ b/app/templates/components/big-number.html
@@ -55,3 +55,19 @@
{% endif %}
{% endmacro %}
+
+
+{% macro big_number_simple(number, label) %}
+
+
+ {% if number is number %}
+ {{ "{:,}".format(number) }}
+ {% else %}
+ {{ number }}
+ {% endif %}
+
+ {% if label %}
+
{{ label }}
+ {% endif %}
+
+{% endmacro %}
diff --git a/app/templates/components/status-box.html b/app/templates/components/status-box.html
new file mode 100644
index 000000000..257430e2b
--- /dev/null
+++ b/app/templates/components/status-box.html
@@ -0,0 +1,15 @@
+{% macro status_box(number, label, failing=false, percentage=None, url=None) %}
+
+
+ {% if url %}
+
{{ number }} {{ label }}
+ {% else %}
+ {{ number }} {{ label }}
+ {% endif %}
+
+ {% if percentage %}
+ - {{ percentage }}%
+ {% endif %}
+
+
+{% endmacro %}
diff --git a/app/templates/views/platform-admin/index_new.html b/app/templates/views/platform-admin/index_new.html
new file mode 100644
index 000000000..fd82bb17c
--- /dev/null
+++ b/app/templates/views/platform-admin/index_new.html
@@ -0,0 +1,59 @@
+{% extends "views/platform-admin/_base_template.html" %}
+{% from "components/textbox.html" import textbox %}
+{% from "components/big-number.html" import big_number_simple %}
+{% from "components/message-count-label.html" import message_count_label %}
+{% from "components/status-box.html" import status_box %}
+
+{% block per_page_title %}
+ Platform admin
+{% endblock %}
+
+{% block platform_admin_content %}
+
+
+ Summary
+
+
+
+ Apply filters
+
+
+
+
+ {% for noti_type in global_stats %}
+
+ {{ big_number_simple(
+ noti_type.black_box.number,
+ message_count_label(noti_type.black_box.number, noti_type.black_box.notification_type)
+ ) }}
+
+ {% for item in noti_type.other_data %}
+ {{ status_box(
+ number=item.number,
+ label=item.label,
+ failing=item.failing,
+ percentage=item.percentage,
+ url=item.url)
+ }}
+ {% endfor %}
+
+ {% endfor %}
+
+
+
+ {% for noti_type in global_stats %}
+
+
+ {{ "{:,}".format(noti_type.test_data.number) }}
+ {{ noti_type.test_data.label }}
+
+
+ {% endfor %}
+
+
+{% endblock %}
diff --git a/tests/app/main/views/test_platform_admin.py b/tests/app/main/views/test_platform_admin.py
index dd0d47f13..0e0878c08 100644
--- a/tests/app/main/views/test_platform_admin.py
+++ b/tests/app/main/views/test_platform_admin.py
@@ -1,14 +1,18 @@
import datetime
+import re
import uuid
from unittest.mock import ANY
import pytest
from bs4 import BeautifulSoup
from flask import url_for
+from freezegun import freeze_time
from app.main.views.platform_admin import (
create_global_stats,
format_stats_by_service,
+ get_tech_failure_status_box_data,
+ is_over_threshold,
sum_service_usage,
)
from tests import service_json
@@ -667,3 +671,114 @@ def test_platform_admin_list_complaints(
resp_data = response.get_data(as_text=True)
assert 'Email complaints' in resp_data
assert mock.called
+
+
+@pytest.mark.parametrize('number, total, threshold, result', [
+ (0, 0, 0, False),
+ (1, 1, 0, True),
+ (2, 3, 66, True),
+ (2, 3, 67, False),
+])
+def test_is_over_threshold(number, total, threshold, result):
+ assert is_over_threshold(number, total, threshold) is result
+
+
+def test_get_tech_failure_status_box_data_removes_percentage_data():
+ stats = {
+ 'failures':
+ {'permanent-failure': 0, 'technical-failure': 0, 'temporary-failure': 1, 'virus-scan-failed': 0},
+ 'test-key': 0,
+ 'total': 5589
+ }
+ tech_failure_data = get_tech_failure_status_box_data(stats)
+
+ assert 'percentage' not in tech_failure_data
+
+
+def test_platform_admin_new_with_start_and_end_dates_provided(mocker, logged_in_platform_admin_client):
+ start_date = '2018-01-01'
+ end_date = '2018-06-01'
+ api_args = {'start_date': datetime.date(2018, 1, 1), 'end_date': datetime.date(2018, 6, 1)}
+
+ mocker.patch('app.main.views.platform_admin.make_columns')
+ aggregate_stats_mock = mocker.patch(
+ 'app.main.views.platform_admin.service_api_client.get_new_aggregate_platform_stats')
+ complaint_count_mock = mocker.patch('app.main.views.platform_admin.complaint_api_client.get_complaint_count')
+
+ logged_in_platform_admin_client.get(
+ url_for('main.platform_admin_new', start_date=start_date, end_date=end_date)
+ )
+
+ aggregate_stats_mock.assert_called_with(api_args)
+ complaint_count_mock.assert_called_with(api_args)
+
+
+@freeze_time('2018-6-11')
+def test_platform_admin_new_with_only_a_start_date_provided(mocker, logged_in_platform_admin_client):
+ start_date = '2018-01-01'
+ api_args = {'start_date': datetime.date(2018, 1, 1), 'end_date': datetime.datetime.utcnow().date()}
+
+ mocker.patch('app.main.views.platform_admin.make_columns')
+ aggregate_stats_mock = mocker.patch(
+ 'app.main.views.platform_admin.service_api_client.get_new_aggregate_platform_stats')
+ complaint_count_mock = mocker.patch('app.main.views.platform_admin.complaint_api_client.get_complaint_count')
+
+ logged_in_platform_admin_client.get(url_for('main.platform_admin_new', start_date=start_date))
+
+ aggregate_stats_mock.assert_called_with(api_args)
+ complaint_count_mock.assert_called_with(api_args)
+
+
+def test_platform_admin_new_without_dates_provided(mocker, logged_in_platform_admin_client):
+ api_args = {}
+
+ mocker.patch('app.main.views.platform_admin.make_columns')
+ aggregate_stats_mock = mocker.patch(
+ 'app.main.views.platform_admin.service_api_client.get_new_aggregate_platform_stats')
+ complaint_count_mock = mocker.patch('app.main.views.platform_admin.complaint_api_client.get_complaint_count')
+
+ logged_in_platform_admin_client.get(url_for('main.platform_admin_new'))
+
+ aggregate_stats_mock.assert_called_with(api_args)
+ complaint_count_mock.assert_called_with(api_args)
+
+
+def test_platform_admin_new_displays_stats_in_right_boxes_and_with_correct_styling(
+ mocker,
+ logged_in_platform_admin_client,
+):
+ platform_stats = {
+ 'email': {'failures':
+ {'permanent-failure': 3, 'technical-failure': 0, 'temporary-failure': 0, 'virus-scan-failed': 0},
+ 'test-key': 0,
+ 'total': 145},
+ 'sms': {'failures':
+ {'permanent-failure': 0, 'technical-failure': 1, 'temporary-failure': 0, 'virus-scan-failed': 0},
+ 'test-key': 5,
+ 'total': 168},
+ 'letter': {'failures':
+ {'permanent-failure': 0, 'technical-failure': 0, 'temporary-failure': 1, 'virus-scan-failed': 1},
+ 'test-key': 0,
+ 'total': 500}
+ }
+ mocker.patch('app.main.views.platform_admin.service_api_client.get_new_aggregate_platform_stats',
+ return_value=platform_stats)
+ mocker.patch('app.main.views.platform_admin.complaint_api_client.get_complaint_count', return_value=15)
+
+ response = logged_in_platform_admin_client.get(url_for('main.platform_admin_new'))
+ page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
+
+ # Email permanent failure status box - number is correct
+ assert '3 permanent failures' in page.find_all('div', class_='column-third')[0].find(string=re.compile('permanent'))
+ # Email complaints status box - link exists and number is correct
+ assert page.find('a', string='15 complaints')
+ # SMS total box - number is correct
+ assert page.find_all('div', class_='big-number-number')[1].text.strip() == '168'
+ # Test SMS box - number is correct
+ assert '5' in page.find_all('div', class_='column-third')[4].text
+ # SMS technical failure status box - number is correct and failure class is used
+ assert '1 technical failures' in page.find_all('div', class_='column-third')[1].find(
+ 'div', class_='big-number-status-failing').text
+ # Letter virus scan failure status box - number is correct and failure class is used
+ assert '1 virus scan failures' in page.find_all('div', class_='column-third')[2].find(
+ 'div', class_='big-number-status-failing').text