Add new platform admin page

Added a new platform admin page, at '/plaform-admin-new' which shows
different data. This no longer offers the option to filter by test-key,
only by date, and also gives a more detailed break-down of the
notifications and failures sent with a normal / research key.

The existing platform admin stats page ('/platform-admin') has not been
deleted yet so that both pages can be compared.
This commit is contained in:
Katie Smith
2018-06-25 13:34:54 +01:00
parent 7a40ad6ac0
commit d9aeac4dca
8 changed files with 337 additions and 1 deletions

View File

@@ -251,3 +251,9 @@ details .arrow {
cursor: pointer; cursor: pointer;
} }
} }
.bordered-text-box {
padding: 5px;
outline: 2px solid $black;
max-width: 100%;
}

View File

@@ -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 { .big-number-smaller {
@extend %big-number; @extend %big-number;

View File

@@ -1,7 +1,7 @@
import itertools import itertools
from datetime import datetime 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 flask_login import login_required
from app import complaint_api_client, service_api_client 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.statistics_utils import get_formatted_percentage
from app.utils import user_is_platform_admin from app.utils import user_is_platform_admin
COMPLAINT_THRESHOLD = 0.02
FAILURE_THRESHOLD = 3
ZERO_FAILURE_THRESHOLD = 0
@main.route("/platform-admin") @main.route("/platform-admin")
@login_required @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/live-services", endpoint='live_services')
@main.route("/platform-admin/trial-services", endpoint='trial_services') @main.route("/platform-admin/trial-services", endpoint='trial_services')
@login_required @login_required

View File

@@ -79,6 +79,7 @@ class HeaderNavigation(Navigation):
'live_services', 'live_services',
'organisations', 'organisations',
'platform_admin', 'platform_admin',
'platform_admin_new',
'suspend_service', 'suspend_service',
'trial_services', 'trial_services',
'update_email_branding', 'update_email_branding',
@@ -421,6 +422,7 @@ class MainNavigation(Navigation):
'organisation_settings', 'organisation_settings',
'organisations', 'organisations',
'platform_admin', 'platform_admin',
'platform_admin_new',
'platform_admin_list_complaints', 'platform_admin_list_complaints',
'pricing', 'pricing',
'privacy', 'privacy',
@@ -590,6 +592,7 @@ class OrgNavigation(Navigation):
'old_using_notify', 'old_using_notify',
'organisations', 'organisations',
'platform_admin', 'platform_admin',
'platform_admin_new',
'platform_admin_list_complaints', 'platform_admin_list_complaints',
'pricing', 'pricing',
'privacy', 'privacy',

View File

@@ -55,3 +55,19 @@
{% endif %} {% endif %}
</div> </div>
{% endmacro %} {% endmacro %}
{% macro big_number_simple(number, label) %}
<div class="big-number-dark bottom-gutter-2-3">
<div class="big-number-number">
{% if number is number %}
{{ "{:,}".format(number) }}
{% else %}
{{ number }}
{% endif %}
</div>
{% if label %}
<span class="big-number-label">{{ label }}</span>
{% endif %}
</div>
{% endmacro %}

View File

@@ -0,0 +1,15 @@
{% macro status_box(number, label, failing=false, percentage=None, url=None) %}
<div class="big-number-with-status">
<div class="big-number-status{% if failing %}-failing{% endif %}">
{% if url %}
<a href="{{ url }}">{{ number }} {{ label }}</a>
{% else %}
{{ number }} {{ label }}
{% endif %}
{% if percentage %}
- {{ percentage }}%
{% endif %}
</div>
</div>
{% endmacro %}

View File

@@ -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 %}
<h1 class="heading-large">
Summary
</h1>
<details>
<summary>Apply filters</summary>
<form autocomplete="off" method="get">
{{ 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") }}
</br>
<button type="submit" class="button">Filter</button>
</form>
</details>
<div class="grid-row bottom-gutter">
{% for noti_type in global_stats %}
<div class="column-third">
{{ 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 %}
</div>
{% endfor %}
</div>
<div class="grid-row bottom-gutter">
{% for noti_type in global_stats %}
<div class="column-third">
<div class="bordered-text-box">
<span class="big-number-number">{{ "{:,}".format(noti_type.test_data.number) }}</span>
{{ noti_type.test_data.label }}
</div>
</div>
{% endfor %}
</div>
{% endblock %}

View File

@@ -1,14 +1,18 @@
import datetime import datetime
import re
import uuid import uuid
from unittest.mock import ANY from unittest.mock import ANY
import pytest import pytest
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from flask import url_for from flask import url_for
from freezegun import freeze_time
from app.main.views.platform_admin import ( from app.main.views.platform_admin import (
create_global_stats, create_global_stats,
format_stats_by_service, format_stats_by_service,
get_tech_failure_status_box_data,
is_over_threshold,
sum_service_usage, sum_service_usage,
) )
from tests import service_json from tests import service_json
@@ -667,3 +671,114 @@ def test_platform_admin_list_complaints(
resp_data = response.get_data(as_text=True) resp_data = response.get_data(as_text=True)
assert 'Email complaints' in resp_data assert 'Email complaints' in resp_data
assert mock.called 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