Show template usage broken down by month

This follows on from:
- https://github.com/alphagov/notifications-admin/pull/1094
- https://github.com/alphagov/notifications-admin/pull/1109

It depends on:
- [ ] https://github.com/alphagov/notifications-api/pull/829

A year is too long. Month-by-month is a better timeframe for making
decisions or seeing patterns in your usage.
This commit is contained in:
Chris Hill-Scott
2017-02-16 15:08:16 +00:00
parent 7fc5d41531
commit 4a226a7a29
9 changed files with 209 additions and 60 deletions

View File

@@ -189,3 +189,13 @@
a.table-show-more-link {
color: $link-colour;
}
.table-no-data {
@include core-16;
color: $secondary-text-colour;
margin-top: 10px;
margin-bottom: $gutter * 1.3333;
border-top: 1px solid $border-colour;
border-bottom: 1px solid $border-colour;
padding: 0.75em 0 0.5625em 0;
}

View File

@@ -81,3 +81,16 @@
@include bold-19;
color: $error-colour;
}
.template-usage-table {
border-top: 1px solid $border-colour;
border-bottom: 1px solid $border-colour;
margin-top: 10px;
margin-bottom: $gutter * 1.3333;
.table {
margin-bottom: 5px;
}
}

View File

@@ -1,5 +1,5 @@
from datetime import datetime
from functools import partial
from flask import (
render_template,
url_for,
@@ -65,16 +65,35 @@ def service_dashboard_updates(service_id):
@login_required
@user_has_permissions('view_activity', admin_override=True)
def template_history(service_id):
template_statistics = aggregate_usage(
template_statistics_client.get_template_statistics_for_service(service_id)
)
year, current_financial_year = requested_and_current_financial_year(request)
stats = template_statistics_client.get_monthly_template_statistics_for_service(service_id, year)
months = [
{
'name': YYYY_MM_to_datetime(month).strftime('%B'),
'templates_used': aggregate_usage(
format_template_stats_to_list(stats.get(month)), sort_key='requested_count'
),
}
for month in get_months_for_financial_year(year, time_format='%Y-%m')
]
return render_template(
'views/dashboard/all-template-statistics.html',
template_statistics=template_statistics,
months=months,
most_used_template_count=max(
[row['count'] for row in template_statistics] or [0]
)
max((
template['requested_count']
for template in month['templates_used']
), default=0)
for month in months
),
years=get_tuples_of_financial_years(
partial(url_for, '.template_history', service_id=service_id),
end=current_financial_year,
),
selected_year=year,
)
@@ -89,14 +108,11 @@ def usage(service_id):
year, service_api_client.get_billable_units(service_id, year)
)),
selected_year=year,
years=[
(
'financial year',
year,
url_for('.usage', service_id=service_id, year=year),
'{} to {}'.format(year, year + 1),
) for year in range(current_financial_year - 1, current_financial_year + 2)
],
years=get_tuples_of_financial_years(
partial(url_for, '.usage', service_id=service_id),
start=current_financial_year - 1,
end=current_financial_year + 1,
),
**calculate_usage(service_api_client.get_service_usage(service_id, year)['data'])
)
@@ -111,22 +127,18 @@ def monthly(service_id):
months=format_monthly_stats_to_list(
service_api_client.get_monthly_notification_stats(service_id, year)['data']
),
years=[
(
'financial year',
year,
url_for('.monthly', service_id=service_id, year=year),
'{} to {}'.format(year, year + 1),
) for year in range(2015, current_financial_year + 1)
],
years=get_tuples_of_financial_years(
partial_url=partial(url_for, '.monthly', service_id=service_id),
end=current_financial_year,
),
selected_year=year,
)
def aggregate_usage(template_statistics):
def aggregate_usage(template_statistics, sort_key='count'):
return sorted(
template_statistics,
key=lambda template_statistic: template_statistic['count'],
key=lambda template_statistic: template_statistic[sort_key],
reverse=True
)
@@ -235,9 +247,9 @@ def aggregate_status_types(counts_dict):
})
def get_months_for_financial_year(year):
def get_months_for_financial_year(year, time_format='%B'):
return [
month.strftime('%B')
month.strftime(time_format)
for month in (
get_months_for_year(4, 13, year) +
get_months_for_year(1, 4, year + 1)
@@ -298,3 +310,32 @@ def requested_and_current_financial_year(request):
)
except ValueError:
abort(404)
def format_template_stats_to_list(stats_dict):
if not stats_dict:
return []
for template_id, template in stats_dict.items():
yield dict(
requested_count=sum(
template['counts'].get(status, 0)
for status in REQUESTED_STATUSES
),
**template
)
def get_tuples_of_financial_years(
partial_url,
start=2015,
end=None,
):
return (
(
'financial year',
year,
partial_url(year=year),
'{} to {}'.format(year, year + 1),
)
for year in range(start, end + 1)
)

View File

@@ -20,6 +20,12 @@ class TemplateStatisticsApiClient(NotifyAdminAPIClient):
params=params
)['data']
def get_monthly_template_statistics_for_service(self, service_id, year):
return self.get(
url='/service/{}/notifications/templates/monthly?year={}'.format(service_id, year)
)['data']
def get_template_statistics_for_template(self, service_id, template_id):
return self.get(

View File

@@ -1,3 +1,5 @@
{% from "components/big-number.html" import big_number %}
{% macro mapping_table(caption='', field_headings=[], field_headings_visible=True, caption_visible=True) -%}
<table class="table">
<caption class="heading-medium table-heading{{ ' visuallyhidden' if not caption_visible}}">
@@ -129,3 +131,20 @@
</span>
{% endcall %}
{% endmacro %}
{% macro spark_bar_field(
count,
max_count
) %}
{% call field(align='right') %}
<span class="spark-bar">
<span style="width: {{ count / max_count * 100 }}%">
{{ big_number(
count,
smallest=True
) }}
</span>
</span>
{% endcall %}
{% endmacro %}

View File

@@ -1,13 +1,58 @@
{% from "components/message-count-label.html" import message_count_label %}
{% from "components/pill.html" import pill %}
{% from "components/table.html" import list_table, field, right_aligned_field_heading, row_heading, spark_bar_field %}
{% extends "withnav_template.html" %}
{% block service_page_title %}
All templates used this year
Templates used
{% endblock %}
{% block maincolumn_content %}
<h1 class="heading-large">All templates used this year</h1>
<h1 class="heading-large">Templates used</h1>
{% include 'views/dashboard/template-statistics.html' %}
<div class="bottom-gutter-3-2">
{{ pill(
items=years,
current_value=selected_year,
big_number_args={'smallest': True},
) }}
</div>
<div class="dashboard">
{% for month in months %}
<h2>{{ month.name }}</h2>
{% if not month.templates_used %}
<p class="table-no-data">
No messages sent
</p>
{% else %}
<div class='dashboard-table template-usage-table'>
{% call(item, row_number) list_table(
month.templates_used,
caption=month.name,
caption_visible=False,
empty_message='',
field_headings=[
'Template',
'Messages sent'
],
field_headings_visible=False
) %}
{% call row_heading() %}
<span class="spark-bar-label">
<a href="{{ url_for('.view_template', service_id=current_service.id, template_id=item.id) }}">{{ item.name }}</a>
<span class="file-list-hint">
{{ message_count_label(1, item.type, suffix='template')|capitalize }}
</span>
</span>
{% endcall %}
{{ spark_bar_field(item.requested_count, most_used_template_count) }}
{% endcall %}
</div>
{% endif %}
{% endfor %}
</div>
{% endblock %}

View File

@@ -1,7 +1,7 @@
{% from "components/big-number.html" import big_number %}
{% from "components/message-count-label.html" import message_count_label %}
{% from "components/big-number.html" import big_number %}
{% from "components/table.html" import list_table, field, right_aligned_field_heading, row_heading %}
{% from "components/table.html" import list_table, field, right_aligned_field_heading, row_heading, spark_bar_field %}
<div class='dashboard-table'>
{% call(item, row_number) list_table(
@@ -25,16 +25,7 @@
</span>
{% endcall %}
{% if template_statistics|length > 1 %}
{% call field(align='right') %}
<span id='{{item.template_id}}' class="spark-bar">
<span style="width: {{ item.count / most_used_template_count * 100 }}%">
{{ big_number(
item.count,
smallest=True
) }}
</span>
</span>
{% endcall %}
{{ spark_bar_field(item.count, most_used_template_count) }}
{% else %}
{% call field() %}
<span id='{{item.template_id}}' class="heading-small">

View File

@@ -1,4 +1,5 @@
from datetime import datetime
from functools import partial
import copy
from flask import url_for
@@ -120,39 +121,40 @@ def test_should_show_recent_templates_on_dashboard(
assert '100' in table_rows[1].find_all('td')[0].text
def test_should_show_all_templates_on_template_statistics_page(
@freeze_time("2016-07-01 12:00") # 4 months into 2016 financial year
@pytest.mark.parametrize('partial_url', [
partial(url_for),
partial(url_for, year='2016'),
])
def test_should_show_monthly_breakdown_of_template_usage(
logged_in_client,
mocker,
api_user_active,
mock_get_service,
mock_get_service_templates,
mock_get_monthly_template_statistics,
mock_get_user,
mock_get_user_by_email,
mock_login,
mock_get_jobs,
mock_has_permissions,
partial_url,
):
mock_template_stats = mocker.patch('app.template_statistics_client.get_template_statistics_for_service',
return_value=copy.deepcopy(stub_template_stats))
response = logged_in_client.get(url_for('main.template_history', service_id=SERVICE_ONE_ID))
response = logged_in_client.get(
partial_url('main.template_history', service_id=SERVICE_ONE_ID, _external=True)
)
assert response.status_code == 200
response.get_data(as_text=True)
mock_template_stats.assert_called_once_with(SERVICE_ONE_ID)
mock_get_monthly_template_statistics.assert_called_once_with(SERVICE_ONE_ID, 2016)
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
table_rows = page.find_all('tbody')[0].find_all('tr')
table_rows = page.select('tbody tr')
assert len(table_rows) == 2
assert ' '.join(table_rows[0].text.split()) == (
'My first template '
'Text message template '
'2'
)
assert 'two' in table_rows[0].find_all('th')[0].text
assert 'Email template' in table_rows[0].find_all('th')[0].text
assert '200' in table_rows[0].find_all('td')[0].text
assert 'one' in table_rows[1].find_all('th')[0].text
assert 'Text message template' in table_rows[1].find_all('th')[0].text
assert '100' in table_rows[1].find_all('td')[0].text
assert len(table_rows) == len(['April'])
assert len(page.select('.table-no-data')) == len(['May', 'June', 'July'])
@freeze_time("2016-01-01 11:09:00.061258")

View File

@@ -1277,6 +1277,28 @@ def mock_get_template_statistics(mocker, service_one, fake_uuid):
'app.template_statistics_client.get_template_statistics_for_service', side_effect=_get_stats)
@pytest.fixture(scope='function')
def mock_get_monthly_template_statistics(mocker, service_one, fake_uuid):
def _stats(service_id, year):
return {
datetime.utcnow().strftime('%Y-%m'): {
fake_uuid: {
"counts": {
"sending": 1,
"delivered": 1,
},
"name": 'My first template',
"type": 'sms',
"id": fake_uuid,
}
}
}
return mocker.patch(
'app.template_statistics_client.get_monthly_template_statistics_for_service',
side_effect=_stats
)
@pytest.fixture(scope='function')
def mock_get_template_statistics_for_template(mocker, service_one):
def _get_stats(service_id, template_id):