Merge pull request #802 from alphagov/dashboard-stats-untangling

Remove notification-statistics endpoints from totals boxes
This commit is contained in:
Leo Hemsted
2016-07-25 10:29:36 +01:00
committed by GitHub
12 changed files with 171 additions and 52 deletions
+5 -1
View File
@@ -324,7 +324,11 @@ def register_errorhandlers(application):
@application.errorhandler(HTTPError) @application.errorhandler(HTTPError)
def render_http_error(error): def render_http_error(error):
application.logger.error("API called failed with status {} message {}".format(error.status_code, error.message)) application.logger.error("API {} failed with status {} message {}".format(
error.response.url if error.response else 'unknown',
error.status_code,
error.message
))
error_code = error.status_code error_code = error.status_code
if error_code not in [401, 404, 403, 500]: if error_code not in [401, 404, 403, 500]:
error_code = 500 error_code = 500
+11 -6
View File
@@ -1,11 +1,9 @@
from datetime import datetime, date, timedelta from datetime import datetime, date, timedelta
from collections import namedtuple from collections import namedtuple
from itertools import groupby from itertools import groupby
import json
from flask import ( from flask import (
render_template, render_template,
redirect,
url_for, url_for,
session, session,
jsonify, jsonify,
@@ -21,7 +19,7 @@ from app import (
service_api_client, service_api_client,
template_statistics_client template_statistics_client
) )
from app.statistics_utils import sum_of_statistics, add_rates_to, add_rate_to_jobs from app.statistics_utils import add_rates_to, get_formatted_percentage, add_rate_to_jobs
from app.utils import user_has_permissions from app.utils import user_has_permissions
@@ -150,13 +148,13 @@ def get_dashboard_partials(service_id):
lambda job: job['original_file_name'] != current_app.config['TEST_MESSAGE_FILENAME'], lambda job: job['original_file_name'] != current_app.config['TEST_MESSAGE_FILENAME'],
job_api_client.get_job(service_id, limit_days=7)['data'] job_api_client.get_job(service_id, limit_days=7)['data']
)) ))
service = service_api_client.get_detailed_service(service_id)
return { return {
'totals': render_template( 'totals': render_template(
'views/dashboard/_totals.html', 'views/dashboard/_totals.html',
statistics=add_rates_to(sum_of_statistics( service_id=service_id,
statistics_api_client.get_statistics_for_service(service_id, limit_days=7)['data'] statistics=get_dashboard_totals(service['data']['statistics'])
))
), ),
'template-statistics': render_template( 'template-statistics': render_template(
'views/dashboard/template-statistics.html', 'views/dashboard/template-statistics.html',
@@ -178,6 +176,13 @@ def get_dashboard_partials(service_id):
} }
def get_dashboard_totals(statistics):
for msg_type in statistics.values():
msg_type['failed_percentage'] = get_formatted_percentage(msg_type['failed'], msg_type['requested'])
msg_type['show_warning'] = float(msg_type['failed_percentage']) > 3
return statistics
def calculate_usage(usage): def calculate_usage(usage):
# TODO: Don't hardcode these - get em from the API # TODO: Don't hardcode these - get em from the API
sms_free_allowance = 250000 sms_free_allowance = 250000
+10 -2
View File
@@ -41,12 +41,20 @@ class ServiceAPIClient(NotificationsAPIClient):
data = _attach_current_user({}) data = _attach_current_user({})
return self.delete(endpoint, data) return self.delete(endpoint, data)
def get_service(self, service_id, *params): def get_service(self, service_id):
return self._get_service(service_id, False)
def get_detailed_service(self, service_id):
return self._get_service(service_id, True)
def _get_service(self, service_id, detailed):
""" """
Retrieve a service. Retrieve a service.
""" """
params = {'detailed': True} if detailed else {}
return self.get( return self.get(
'/service/{0}'.format(service_id)) '/service/{0}'.format(service_id),
params=params)
def get_services(self, *params): def get_services(self, *params):
""" """
+13 -12
View File
@@ -31,18 +31,12 @@ def sum_of_statistics(delivery_statistics):
def add_rates_to(delivery_statistics): def add_rates_to(delivery_statistics):
return dict( return dict(
emails_failure_rate=( emails_failure_rate=get_formatted_percentage(
"{0:.1f}".format( delivery_statistics['emails_failed'],
float(delivery_statistics['emails_failed']) / delivery_statistics['emails_requested'] * 100 delivery_statistics['emails_requested']),
) sms_failure_rate=get_formatted_percentage(
if delivery_statistics['emails_requested'] else 0 delivery_statistics['sms_failed'],
), delivery_statistics['sms_requested']),
sms_failure_rate=(
"{0:.1f}".format(
float(delivery_statistics['sms_failed']) / delivery_statistics['sms_requested'] * 100
)
if delivery_statistics['sms_requested'] else 0
),
week_end_datetime=parser.parse( week_end_datetime=parser.parse(
delivery_statistics.get('week_end', str(datetime.utcnow())) delivery_statistics.get('week_end', str(datetime.utcnow()))
), ),
@@ -50,6 +44,13 @@ def add_rates_to(delivery_statistics):
) )
def get_formatted_percentage(x, tot):
"""
Return a percentage to one decimal place (respecting )
"""
return "{0:.1f}".format((float(x) / tot * 100)) if tot else '0'
def statistics_by_state(statistics): def statistics_by_state(statistics):
return { return {
'sms': { 'sms': {
+14 -14
View File
@@ -4,24 +4,24 @@
<div class="grid-row"> <div class="grid-row">
<div class="column-half"> <div class="column-half">
{{ big_number_with_status( {{ big_number_with_status(
statistics.emails_requested, statistics['email']['requested'],
message_count_label(statistics.emails_requested, 'email', suffix=''), message_count_label(statistics['email']['requested'], 'email', suffix=''),
statistics.emails_failed, statistics['email']['failed'],
statistics.get('emails_failure_rate', 0.0), statistics['email']['failed_percentage'],
statistics.get('emails_failure_rate', 0)|float > 3, statistics['email']['show_warning'],
failure_link=url_for(".view_notifications", service_id=current_service.id, message_type='email', status='failed'), failure_link=url_for(".view_notifications", service_id=service_id, message_type='email', status='failed'),
link=url_for(".view_notifications", service_id=current_service.id, message_type='email', status='sending,delivered,failed') link=url_for(".view_notifications", service_id=service_id, message_type='email', status='sending,delivered,failed')
) }} ) }}
</div> </div>
<div class="column-half"> <div class="column-half">
{{ big_number_with_status( {{ big_number_with_status(
statistics.sms_requested, statistics['sms']['requested'],
message_count_label(statistics.sms_requested, 'sms', suffix=''), message_count_label(statistics['sms']['requested'], 'sms', suffix=''),
statistics.sms_failed, statistics['sms']['failed'],
statistics.get('sms_failure_rate', 0.0), statistics['sms']['failed_percentage'],
statistics.get('sms_failure_rate', 0)|float > 3, statistics['sms']['show_warning'],
failure_link=url_for(".view_notifications", service_id=current_service.id, message_type='sms', status='failed'), failure_link=url_for(".view_notifications", service_id=service_id, message_type='sms', status='failed'),
link=url_for(".view_notifications", service_id=current_service.id, message_type='sms', status='sending,delivered,failed') link=url_for(".view_notifications", service_id=service_id, message_type='sms', status='sending,delivered,failed')
) }} ) }}
</div> </div>
</div> </div>
+4 -1
View File
@@ -314,7 +314,10 @@ def test_new_invited_user_verifies_and_added_to_service(app_,
mock_get_service_statistics, mock_get_service_statistics,
mock_get_template_statistics, mock_get_template_statistics,
mock_get_jobs, mock_get_jobs,
mock_has_permissions): mock_has_permissions,
mock_get_users_by_service,
mock_get_detailed_service,
mock_get_usage):
with app_.test_request_context(): with app_.test_request_context():
with app_.test_client() as client: with app_.test_client() as client:
+2 -2
View File
@@ -6,14 +6,14 @@ from tests import validate_route_permission
def test_should_show_empty_api_keys_page(app_, def test_should_show_empty_api_keys_page(app_,
api_user_active, api_user_pending,
mock_login, mock_login,
mock_get_no_api_keys, mock_get_no_api_keys,
mock_get_service, mock_get_service,
mock_has_permissions): mock_has_permissions):
with app_.test_request_context(): with app_.test_request_context():
with app_.test_client() as client: with app_.test_client() as client:
client.login(api_user_active) client.login(api_user_pending)
service_id = str(uuid.uuid4()) service_id = str(uuid.uuid4())
response = client.get(url_for('main.api_keys', service_id=service_id)) response = client.get(url_for('main.api_keys', service_id=service_id))
+77 -12
View File
@@ -1,9 +1,12 @@
import copy import copy
from flask import url_for from flask import url_for
import pytest
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from freezegun import freeze_time from freezegun import freeze_time
from app.main.views.dashboard import get_dashboard_totals
from tests import validate_route_permission from tests import validate_route_permission
from tests.conftest import SERVICE_ONE_ID from tests.conftest import SERVICE_ONE_ID
@@ -69,6 +72,7 @@ def test_get_started(
mock_login, mock_login,
mock_get_jobs, mock_get_jobs,
mock_has_permissions, mock_has_permissions,
mock_get_detailed_service,
mock_get_usage mock_get_usage
): ):
@@ -98,12 +102,11 @@ def test_get_started_is_hidden_once_templates_exist(
mock_login, mock_login,
mock_get_jobs, mock_get_jobs,
mock_has_permissions, mock_has_permissions,
mock_get_detailed_service,
mock_get_usage mock_get_usage
): ):
mock_template_stats = mocker.patch('app.template_statistics_client.get_template_statistics_for_service', mock_template_stats = mocker.patch('app.template_statistics_client.get_template_statistics_for_service',
return_value=copy.deepcopy(stub_template_stats)) return_value=copy.deepcopy(stub_template_stats))
with app_.test_request_context(), app_.test_client() as client: with app_.test_request_context(), app_.test_client() as client:
client.login(api_user_active) client.login(api_user_active)
response = client.get(url_for('main.service_dashboard', service_id=SERVICE_ONE_ID)) response = client.get(url_for('main.service_dashboard', service_id=SERVICE_ONE_ID))
@@ -118,13 +121,13 @@ def test_should_show_recent_templates_on_dashboard(app_,
api_user_active, api_user_active,
mock_get_service, mock_get_service,
mock_get_service_templates, mock_get_service_templates,
mock_get_service_statistics,
mock_get_aggregate_service_statistics, mock_get_aggregate_service_statistics,
mock_get_user, mock_get_user,
mock_get_user_by_email, mock_get_user_by_email,
mock_login, mock_login,
mock_get_jobs, mock_get_jobs,
mock_has_permissions, mock_has_permissions,
mock_get_detailed_service,
mock_get_usage): mock_get_usage):
mock_template_stats = mocker.patch('app.template_statistics_client.get_template_statistics_for_service', mock_template_stats = mocker.patch('app.template_statistics_client.get_template_statistics_for_service',
@@ -137,7 +140,6 @@ def test_should_show_recent_templates_on_dashboard(app_,
assert response.status_code == 200 assert response.status_code == 200
response.get_data(as_text=True) response.get_data(as_text=True)
mock_get_service_statistics.assert_called_once_with(SERVICE_ONE_ID, limit_days=7)
mock_template_stats.assert_called_once_with(SERVICE_ONE_ID, limit_days=7) mock_template_stats.assert_called_once_with(SERVICE_ONE_ID, limit_days=7)
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser') page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
@@ -211,6 +213,7 @@ def test_should_show_recent_jobs_on_dashboard(
mock_get_user_by_email, mock_get_user_by_email,
mock_login, mock_login,
mock_get_template_statistics, mock_get_template_statistics,
mock_get_detailed_service,
mock_get_jobs, mock_get_jobs,
mock_has_permissions, mock_has_permissions,
mock_get_usage mock_get_usage
@@ -262,6 +265,7 @@ def test_menu_send_messages(mocker,
mock_get_service_templates, mock_get_service_templates,
mock_get_jobs, mock_get_jobs,
mock_get_template_statistics, mock_get_template_statistics,
mock_get_detailed_service,
mock_get_usage): mock_get_usage):
with app_.test_request_context(): with app_.test_request_context():
@@ -296,6 +300,7 @@ def test_menu_manage_service(mocker,
mock_get_service_templates, mock_get_service_templates,
mock_get_jobs, mock_get_jobs,
mock_get_template_statistics, mock_get_template_statistics,
mock_get_detailed_service,
mock_get_usage): mock_get_usage):
with app_.test_request_context(): with app_.test_request_context():
resp = _test_dashboard_menu( resp = _test_dashboard_menu(
@@ -328,6 +333,7 @@ def test_menu_manage_api_keys(mocker,
mock_get_service_templates, mock_get_service_templates,
mock_get_jobs, mock_get_jobs,
mock_get_template_statistics, mock_get_template_statistics,
mock_get_detailed_service,
mock_get_usage): mock_get_usage):
with app_.test_request_context(): with app_.test_request_context():
resp = _test_dashboard_menu( resp = _test_dashboard_menu(
@@ -359,6 +365,7 @@ def test_menu_all_services_for_platform_admin_user(mocker,
mock_get_service_templates, mock_get_service_templates,
mock_get_jobs, mock_get_jobs,
mock_get_template_statistics, mock_get_template_statistics,
mock_get_detailed_service,
mock_get_usage): mock_get_usage):
with app_.test_request_context(): with app_.test_request_context():
resp = _test_dashboard_menu( resp = _test_dashboard_menu(
@@ -376,14 +383,6 @@ def test_menu_all_services_for_platform_admin_user(mocker,
assert url_for('main.view_notifications', service_id=service_one['id'], message_type='sms') in page assert url_for('main.view_notifications', service_id=service_one['id'], message_type='sms') in page
assert url_for('main.api_keys', service_id=service_one['id']) not in page assert url_for('main.api_keys', service_id=service_one['id']) not in page
# Should this be here??
# template_json = mock_get_service_templates(service_one['id'])['data'][0]
# assert url_for(
# 'main.edit_service_template',
# service_id=service_one['id'],
# template_id=template_json['id']) in page
def test_route_for_service_permissions(mocker, def test_route_for_service_permissions(mocker,
app_, app_,
@@ -395,6 +394,7 @@ def test_route_for_service_permissions(mocker,
mock_get_jobs, mock_get_jobs,
mock_get_service_statistics, mock_get_service_statistics,
mock_get_template_statistics, mock_get_template_statistics,
mock_get_detailed_service,
mock_get_usage): mock_get_usage):
routes = [ routes = [
'main.service_dashboard'] 'main.service_dashboard']
@@ -424,3 +424,68 @@ def test_aggregate_template_stats():
assert item['usage_count'] == 13 assert item['usage_count'] == 13
elif item['template'].id == 2: elif item['template'].id == 2:
assert item['usage_count'] == 206 assert item['usage_count'] == 206
def test_service_dashboard_updates_gets_dashboard_totals(mocker,
app_,
active_user_with_permissions,
service_one,
mock_get_user,
mock_get_service_templates,
mock_get_template_statistics,
mock_get_detailed_service,
mock_get_jobs,
mock_get_service_statistics,
mock_get_usage):
dashboard_totals = mocker.patch('app.main.views.dashboard.get_dashboard_totals', return_value={
'email': {'requested': 123, 'delivered': 0, 'failed': 0},
'sms': {'requested': 456, 'delivered': 0, 'failed': 0}
})
with app_.test_request_context(), app_.test_client() as client:
client.login(active_user_with_permissions, mocker, service_one)
response = client.get(url_for('main.service_dashboard', service_id=SERVICE_ONE_ID))
assert response.status_code == 200
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
numbers = [number.text.strip() for number in page.find_all('div', class_='big-number-number')]
assert '123' in numbers
assert '456' in numbers
table_rows = page.find_all('tbody')[0].find_all('tr')
def test_get_dashboard_totals_adds_percentages():
stats = {
'sms': {
'requested': 3,
'delivered': 0,
'failed': 2
},
'email': {
'requested': 0,
'delivered': 0,
'failed': 0
}
}
assert get_dashboard_totals(stats)['sms']['failed_percentage'] == '66.7'
assert get_dashboard_totals(stats)['email']['failed_percentage'] == '0'
@pytest.mark.parametrize(
'failures,expected', [
(2, False),
(3, False),
(4, True)
]
)
def test_get_dashboard_totals_adds_warning(failures, expected):
stats = {
'sms': {
'requested': 100,
'delivered': 0,
'failed': failures
}
}
assert get_dashboard_totals(stats)['sms']['show_warning'] == expected
+1
View File
@@ -21,6 +21,7 @@ def test_sign_out_user(app_,
mock_get_jobs, mock_get_jobs,
mock_has_permissions, mock_has_permissions,
mock_get_template_statistics, mock_get_template_statistics,
mock_get_detailed_service,
mock_get_usage): mock_get_usage):
with app_.test_request_context(): with app_.test_request_context():
with app_.test_client() as client: with app_.test_client() as client:
@@ -19,3 +19,19 @@ def test_client_posts_archived_true_when_deleting_template(mocker):
client.delete_service_template(service_id, template_id) client.delete_service_template(service_id, template_id)
mock_post.assert_called_once_with(expected_url, data=expected_data) mock_post.assert_called_once_with(expected_url, data=expected_data)
def test_client_gets_service_with_no_params(mocker):
client = ServiceAPIClient()
mock_post = mocker.patch('app.notify_client.service_api_client.ServiceAPIClient.get')
client.get_service('foo')
mock_post.assert_called_once_with('/service/foo', params={})
def test_client_gets_service_with_detailed_params(mocker):
client = ServiceAPIClient()
mock_post = mocker.patch('app.notify_client.service_api_client.ServiceAPIClient.get')
client.get_detailed_service('foo')
mock_post.assert_called_once_with('/service/foo', params={'detailed': True})
+2 -2
View File
@@ -53,7 +53,7 @@ def test_sum_of_statistics_sums_inputs():
@pytest.mark.parametrize('emails_failed,emails_requested,expected_failure_rate', [ @pytest.mark.parametrize('emails_failed,emails_requested,expected_failure_rate', [
(0, 0, 0), (0, 0, '0'),
(0, 1, '0.0'), (0, 1, '0.0'),
(1, 3, '33.3') (1, 3, '33.3')
]) ])
@@ -69,7 +69,7 @@ def test_add_rates_sets_email_failure_rate(emails_failed, emails_requested, expe
@pytest.mark.parametrize('sms_failed,sms_requested,expected_failure_rate', [ @pytest.mark.parametrize('sms_failed,sms_requested,expected_failure_rate', [
(0, 0, 0), (0, 0, '0'),
(0, 1, '0.0'), (0, 1, '0.0'),
(1, 3, '33.3') (1, 3, '33.3')
]) ])
+16
View File
@@ -64,6 +64,22 @@ def mock_get_service(mocker, api_user_active):
return mocker.patch('app.service_api_client.get_service', side_effect=_get) return mocker.patch('app.service_api_client.get_service', side_effect=_get)
@pytest.fixture(scope='function')
def mock_get_detailed_service(mocker, api_user_active):
def _get(service_id):
return {
'data': {
'id': service_id,
'statistics': {
'email': {'requested': 0, 'delivered': 0, 'failed': 0},
'sms': {'requested': 0, 'delivered': 0, 'failed': 0}
}
}
}
return mocker.patch('app.service_api_client.get_detailed_service', side_effect=_get)
@pytest.fixture(scope='function') @pytest.fixture(scope='function')
def mock_get_live_service(mocker, api_user_active): def mock_get_live_service(mocker, api_user_active):
def _get(service_id): def _get(service_id):