mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-09-10 10:03:21 -04:00
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:
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
|||||||
@@ -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 haven’t used any templates {}'.format(period),
|
empty_message='You haven’t 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 %}
|
||||||
|
|
||||||
|
|||||||
@@ -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
app/templates/views/weekly.html
Normal file
50
app/templates/views/weekly.html
Normal 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 %}
|
||||||
@@ -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):
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user