Add a page to show delivery rates week-by-week

Implements
https://github.com/alphagov/notifications-api/pull/286

Will always show weeks as Monday to Sunday.
This commit is contained in:
Chris Hill-Scott
2016-05-05 10:56:20 +01:00
parent afc8fb08f3
commit 50427ecd3f
13 changed files with 170 additions and 35 deletions
+6
View File
@@ -101,6 +101,7 @@ def create_app():
application.add_template_filter(valid_phone_number) application.add_template_filter(valid_phone_number)
application.add_template_filter(linkable_name) application.add_template_filter(linkable_name)
application.add_template_filter(format_date) application.add_template_filter(format_date)
application.add_template_filter(format_date_short)
application.after_request(useful_headers_after_request) application.after_request(useful_headers_after_request)
application.after_request(save_service_after_request) application.after_request(save_service_after_request)
@@ -201,6 +202,11 @@ def format_date(date):
return date.strftime('%A %d %B %Y') return date.strftime('%A %d %B %Y')
def format_date_short(date):
date = dateutil.parser.parse(date)
return date.strftime('%d %B').lstrip('0')
def valid_phone_number(phone_number): def valid_phone_number(phone_number):
try: try:
validate_phone_number(phone_number) validate_phone_number(phone_number)
@@ -24,6 +24,21 @@
color: $white; color: $white;
position: relative; position: relative;
&-show-more-link {
@include core-16;
display: block;
padding: 0.75em 0 0.5625em 0;
margin-bottom: $gutter-half;
text-align: center;
border-bottom: 1px solid $border-colour;
&:focus {
outline: none;
color: $text-colour;
border-bottom: 1px solid $brown;
}
}
.big-number { .big-number {
padding: 15px; padding: 15px;
position: relative; position: relative;
+3 -3
View File
@@ -4,7 +4,7 @@
.table-heading { .table-heading {
text-align: left; text-align: left;
margin: 40px 0 5px 0; margin: 40px 0 $gutter-half 0;
} }
.table-field-headings { .table-field-headings {
@@ -111,10 +111,10 @@
.table-show-more-link { .table-show-more-link {
@include core-16; @include core-16;
color: $secondary-text-colour; color: $secondary-text-colour;
margin-top: -20px; margin-top: -30px;
margin-bottom: $gutter * 1.3333; margin-bottom: $gutter * 1.3333;
border-bottom: 1px solid $border-colour; border-bottom: 1px solid $border-colour;
padding-bottom: 10px; padding: 0.75em 0 0.5625em 0;
text-align: center; text-align: center;
} }
+2 -2
View File
@@ -1,6 +1,6 @@
.dashboard { .dashboard {
table th { table th {
@include bold-19; font-size: 0;
} }
} }
@@ -13,7 +13,7 @@
@include core-16; @include core-16;
border-top: 1px solid $border-colour; border-top: 1px solid $border-colour;
border-bottom: 1px solid $border-colour; border-bottom: 1px solid $border-colour;
padding: 10px 0; padding: 0.75em 0 0.5625em 0;
text-align: center; text-align: center;
} }
+47 -14
View File
@@ -1,4 +1,5 @@
from datetime import date from datetime import datetime, date, timedelta
from dateutil import parser
from collections import namedtuple from collections import namedtuple
from itertools import groupby from itertools import groupby
from functools import reduce from functools import reduce
@@ -90,40 +91,72 @@ def usage(service_id):
) )
def add_rates_to(delivery_statistics): @main.route("/services/<service_id>/weekly")
@login_required
@user_has_permissions('manage_settings', admin_override=True)
def weekly(service_id):
keys = [ earliest_date = date(2016, 4, 1) # start of tax year
while earliest_date.weekday() != 0: # 0 for monday
earliest_date -= timedelta(days=1)
return render_template(
'views/weekly.html',
days=(
add_rates_to(day) for day in
statistics_api_client.get_7_day_aggregate_for_service(
service_id,
date_from=earliest_date
)['data']
),
now=datetime.now()
)
def sum_of_statistics(delivery_statistics):
statistics_keys = (
'emails_delivered', 'emails_delivered',
'emails_requested', 'emails_requested',
'emails_failed', 'emails_failed',
'sms_requested', 'sms_requested',
'sms_delivered', 'sms_delivered',
'sms_failed' 'sms_failed'
] )
if not delivery_statistics or not delivery_statistics[0]: if not delivery_statistics or not delivery_statistics[0]:
return { return {
key: 0 for key in keys key: 0 for key in statistics_keys
} }
sum_of_statistics = reduce( return reduce(
lambda x, y: { lambda x, y: {
key: x.get(key, 0) + y.get(key, 0) key: x.get(key, 0) + y.get(key, 0)
for key in keys for key in statistics_keys
}, },
delivery_statistics delivery_statistics
) )
def add_rates_to(delivery_statistics):
return dict( return dict(
emails_failure_rate=( emails_failure_rate=(
"{0:.1f}".format((float(sum_of_statistics['emails_failed']) / sum_of_statistics['emails_requested'] * 100)) "{0:.1f}".format(
if sum_of_statistics['emails_requested'] else 0 float(delivery_statistics['emails_failed']) / delivery_statistics['emails_requested'] * 100
)
if delivery_statistics['emails_requested'] else 0
), ),
sms_failure_rate=( sms_failure_rate=(
"{0:.1f}".format((float(sum_of_statistics['sms_failed']) / sum_of_statistics['sms_requested'] * 100)) "{0:.1f}".format(
if sum_of_statistics['sms_requested'] else 0 float(delivery_statistics['sms_failed']) / delivery_statistics['sms_requested'] * 100
)
if delivery_statistics['sms_requested'] else 0
), ),
**sum_of_statistics week_end_datetime=parser.parse(
delivery_statistics.get('week_end', str(datetime.now()))
),
**delivery_statistics
) )
@@ -167,9 +200,9 @@ def get_dashboard_statistics_for_service(service_id):
emails_sent = usage['data'].get('email_count', 0) emails_sent = usage['data'].get('email_count', 0)
return { return {
'statistics': add_rates_to( 'statistics': add_rates_to(sum_of_statistics(
statistics_api_client.get_statistics_for_service(service_id, limit_days=7)['data'] statistics_api_client.get_statistics_for_service(service_id, limit_days=7)['data']
), )),
'template_statistics': aggregate_usage( 'template_statistics': aggregate_usage(
template_statistics_client.get_template_statistics_for_service(service_id, limit_days=7) template_statistics_client.get_template_statistics_for_service(service_id, limit_days=7)
), ),
@@ -20,3 +20,14 @@ class StatisticsApiClient(BaseAPIClient):
url='/service/{}/notifications-statistics'.format(service_id), url='/service/{}/notifications-statistics'.format(service_id),
params=params params=params
) )
def get_7_day_aggregate_for_service(self, service_id, date_from=None, week_count=None):
params = {}
if date_from is not None:
params['date_from'] = date_from
if week_count is not None:
params['week_count'] = week_count
return self.get(
url='/service/{}/notifications-statistics/seven_day_aggregate'.format(service_id),
params=params
)
+6 -1
View File
@@ -26,7 +26,9 @@
failure_percentage, failure_percentage,
danger_zone=False, danger_zone=False,
failure_link=None, failure_link=None,
label_link=None label_link=None,
show_more_link=None,
show_more_text=''
) %} ) %}
<div class="big-number-with-status"> <div class="big-number-with-status">
{{ big_number(number, label, label_link) }} {{ big_number(number, label, label_link) }}
@@ -44,4 +46,7 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% if show_more_link and show_more_text %}
<a href="{{ show_more_link }}" class="big-number-with-status-show-more-link">{{ show_more_text }}</a>
{% endif %}
{% endmacro %} {% endmacro %}
@@ -20,6 +20,9 @@
{% include 'views/dashboard/no-permissions-banner.html' %} {% include 'views/dashboard/no-permissions-banner.html' %}
{% endif %} {% endif %}
<h1 class="heading-medium">
In the last 7 days
</h1>
{% include 'views/dashboard/today.html' %} {% include 'views/dashboard/today.html' %}
</div> </div>
@@ -1,21 +1,23 @@
{% from "components/table.html" import list_table, field, hidden_field_heading, right_aligned_field_heading %} {% from "components/table.html" import list_table, field, hidden_field_heading, right_aligned_field_heading %}
{% call(item, row_number) list_table( {% call(item, row_number) list_table(
template_statistics, template_statistics,
caption="By template", caption="Templates",
caption_visible=False, caption_visible=True,
empty_message='You havent used any templates {}'.format(period), empty_message='You havent used any templates {}'.format(period),
field_headings=['Template', hidden_field_heading('Type'), right_aligned_field_heading('Messages sent')] field_headings=['Template', hidden_field_heading('Type'), hidden_field_heading('Messages sent')],
field_headings_visible=False
) %} ) %}
{% call field() %} {% call field() %}
<a href="{{ url_for('.view_template', service_id=current_service.id, template_id=item.template.id) }}"> <a href="{{ url_for('.view_template', service_id=current_service.id, template_id=item.template.id) }}">
{{ item.template.name }} {{ item.template.name }}
</a> </a>
{% endcall %} {% endcall %}
{% call field() %}
{{'Text message' if 'sms' == item.template.template_type else 'Email'}}
{% endcall %}
{% call field(align='right') %} {% call field(align='right') %}
{{ item.usage_count }} {{ item.usage_count }}
{% endcall %} {% endcall %}
{% call field() %}
{{'text messages sent' if 'sms' == item.template.template_type else 'emails sent'}}
{% endcall %}
{% endcall %} {% endcall %}
+4 -4
View File
@@ -7,10 +7,7 @@
data-interval-seconds="2" data-interval-seconds="2"
aria-live="polite" aria-live="polite"
> >
<h2 class="heading-medium"> <div class="grid-row">
In the last 7 days
</h2>
<div class="grid-row bottom-gutter">
<div class="column-half"> <div class="column-half">
{{ big_number_with_status( {{ big_number_with_status(
statistics.emails_requested, statistics.emails_requested,
@@ -33,6 +30,9 @@
label_link=url_for(".view_notifications", service_id=current_service.id, template_type='sms', status='delivered,failed') label_link=url_for(".view_notifications", service_id=current_service.id, template_type='sms', status='delivered,failed')
) }} ) }}
</div> </div>
<div class="column-whole">
<a class="big-number-with-status-show-more-link" href="{{ url_for('.weekly', service_id=current_service.id) }}">Compare to previous weeks</a>
</div>
</div> </div>
{% with period = "in the last 7 days" %} {% with period = "in the last 7 days" %}
{% include 'views/dashboard/template-statistics.html' %} {% include 'views/dashboard/template-statistics.html' %}
+50
View File
@@ -0,0 +1,50 @@
{% from "components/table.html" import list_table, field, hidden_field_heading, right_aligned_field_heading %}
{% extends "withnav_template.html" %}
{% block page_title %}
Daily GOV.UK Notify
{% endblock %}
{% block maincolumn_content %}
<h1 class="heading-large">
Previous weeks
</h1>
{% call(item, row_number) list_table(
days,
caption="Daily",
caption_visible=False,
empty_message='No data found',
field_headings=[
hidden_field_heading('Day'),
right_aligned_field_heading('Emails'),
right_aligned_field_heading('Failure rate'),
right_aligned_field_heading('Text messages'),
right_aligned_field_heading('Failure rate')
]
) %}
{% call field() %}
{{ item.week_start|format_date_short }} to
{% if item.week_end_datetime > now %}
today
{% else %}
{{ item.week_end|format_date_short }}
{% endif %}
{% endcall %}
{% call field(align='right') %}
{{ item.emails_requested }}
{% endcall %}
{% call field(align='right') %}
{{ item.emails_failure_rate }}%
{% endcall %}
{% call field(align='right') %}
{{ item.sms_requested }}
{% endcall %}
{% call field(align='right') %}
{{ item.sms_failure_rate }}%
{% endcall %}
{% endcall %}
{% endblock %}
+6 -5
View File
@@ -61,6 +61,7 @@ def test_should_show_recent_templates_on_dashboard(app_,
mock_get_service, mock_get_service,
mock_get_service_templates, mock_get_service_templates,
mock_get_service_statistics, mock_get_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,
@@ -82,7 +83,7 @@ def test_should_show_recent_templates_on_dashboard(app_,
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')
headers = [header.text.strip() for header in page.find_all('h2')] headers = [header.text.strip() for header in page.find_all('h2') + page.find_all('h1')]
assert 'Test Service' in headers assert 'Test Service' in headers
assert 'In the last 7 days' in headers assert 'In the last 7 days' in headers
template_usage_headers = [th.text.strip() for th in page.thead.find_all('th')] template_usage_headers = [th.text.strip() for th in page.thead.find_all('th')]
@@ -94,12 +95,12 @@ def test_should_show_recent_templates_on_dashboard(app_,
first_row = page.tbody.find_all('tr')[0] first_row = page.tbody.find_all('tr')[0]
table_data = first_row.find_all('td') table_data = first_row.find_all('td')
assert len(table_data) == 3 assert len(table_data) == 3
assert table_data[2].text.strip() == '206' assert table_data[1].text.strip() == '206'
second_row = page.tbody.find_all('tr')[1] second_row = page.tbody.find_all('tr')[1]
table_data = second_row.find_all('td') table_data = second_row.find_all('td')
assert len(table_data) == 3 assert len(table_data) == 3
assert table_data[2].text.strip() == '13' assert table_data[1].text.strip() == '13'
def test_should_show_all_templates_on_template_statistics_page( def test_should_show_all_templates_on_template_statistics_page(
@@ -137,12 +138,12 @@ def test_should_show_all_templates_on_template_statistics_page(
first_row = page.tbody.find_all('tr')[0] first_row = page.tbody.find_all('tr')[0]
table_data = first_row.find_all('td') table_data = first_row.find_all('td')
assert len(table_data) == 3 assert len(table_data) == 3
assert table_data[2].text.strip() == '206' assert table_data[1].text.strip() == '206'
second_row = page.tbody.find_all('tr')[1] second_row = page.tbody.find_all('tr')[1]
table_data = second_row.find_all('td') table_data = second_row.find_all('td')
assert len(table_data) == 3 assert len(table_data) == 3
assert table_data[2].text.strip() == '13' assert table_data[1].text.strip() == '13'
def _test_dashboard_menu(mocker, app_, usr, service, permissions): def _test_dashboard_menu(mocker, app_, usr, service, permissions):
+9
View File
@@ -191,6 +191,15 @@ def mock_get_service_statistics(mocker):
'app.statistics_api_client.get_statistics_for_service', side_effect=_create) 'app.statistics_api_client.get_statistics_for_service', side_effect=_create)
@pytest.fixture(scope='function')
def mock_get_aggregate_service_statistics(mocker):
def _create(service_id, limit_days=None):
return {'data': [{}]}
return mocker.patch(
'app.statistics_api_client.get_7_day_aggregate_for_service', side_effect=_create)
@pytest.fixture(scope='function') @pytest.fixture(scope='function')
def mock_get_service_template(mocker): def mock_get_service_template(mocker):
def _get(service_id, template_id): def _get(service_id, template_id):