mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-09-08 08:28:15 -04:00
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:
@@ -251,3 +251,9 @@ details .arrow {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bordered-text-box {
|
||||||
|
padding: 5px;
|
||||||
|
outline: 2px solid $black;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
15
app/templates/components/status-box.html
Normal file
15
app/templates/components/status-box.html
Normal 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 %}
|
||||||
59
app/templates/views/platform-admin/index_new.html
Normal file
59
app/templates/views/platform-admin/index_new.html
Normal 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 %}
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user