mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-08-24 00:07:02 -04:00
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:
@@ -189,3 +189,13 @@
|
|||||||
a.table-show-more-link {
|
a.table-show-more-link {
|
||||||
color: $link-colour;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -81,3 +81,16 @@
|
|||||||
@include bold-19;
|
@include bold-19;
|
||||||
color: $error-colour;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from functools import partial
|
||||||
from flask import (
|
from flask import (
|
||||||
render_template,
|
render_template,
|
||||||
url_for,
|
url_for,
|
||||||
@@ -65,16 +65,35 @@ def service_dashboard_updates(service_id):
|
|||||||
@login_required
|
@login_required
|
||||||
@user_has_permissions('view_activity', admin_override=True)
|
@user_has_permissions('view_activity', admin_override=True)
|
||||||
def template_history(service_id):
|
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(
|
return render_template(
|
||||||
'views/dashboard/all-template-statistics.html',
|
'views/dashboard/all-template-statistics.html',
|
||||||
template_statistics=template_statistics,
|
months=months,
|
||||||
most_used_template_count=max(
|
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)
|
year, service_api_client.get_billable_units(service_id, year)
|
||||||
)),
|
)),
|
||||||
selected_year=year,
|
selected_year=year,
|
||||||
years=[
|
years=get_tuples_of_financial_years(
|
||||||
(
|
partial(url_for, '.usage', service_id=service_id),
|
||||||
'financial year',
|
start=current_financial_year - 1,
|
||||||
year,
|
end=current_financial_year + 1,
|
||||||
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)
|
|
||||||
],
|
|
||||||
**calculate_usage(service_api_client.get_service_usage(service_id, year)['data'])
|
**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(
|
months=format_monthly_stats_to_list(
|
||||||
service_api_client.get_monthly_notification_stats(service_id, year)['data']
|
service_api_client.get_monthly_notification_stats(service_id, year)['data']
|
||||||
),
|
),
|
||||||
years=[
|
years=get_tuples_of_financial_years(
|
||||||
(
|
partial_url=partial(url_for, '.monthly', service_id=service_id),
|
||||||
'financial year',
|
end=current_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)
|
|
||||||
],
|
|
||||||
selected_year=year,
|
selected_year=year,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def aggregate_usage(template_statistics):
|
def aggregate_usage(template_statistics, sort_key='count'):
|
||||||
return sorted(
|
return sorted(
|
||||||
template_statistics,
|
template_statistics,
|
||||||
key=lambda template_statistic: template_statistic['count'],
|
key=lambda template_statistic: template_statistic[sort_key],
|
||||||
reverse=True
|
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 [
|
return [
|
||||||
month.strftime('%B')
|
month.strftime(time_format)
|
||||||
for month in (
|
for month in (
|
||||||
get_months_for_year(4, 13, year) +
|
get_months_for_year(4, 13, year) +
|
||||||
get_months_for_year(1, 4, year + 1)
|
get_months_for_year(1, 4, year + 1)
|
||||||
@@ -298,3 +310,32 @@ def requested_and_current_financial_year(request):
|
|||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
abort(404)
|
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)
|
||||||
|
)
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ class TemplateStatisticsApiClient(NotifyAdminAPIClient):
|
|||||||
params=params
|
params=params
|
||||||
)['data']
|
)['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):
|
def get_template_statistics_for_template(self, service_id, template_id):
|
||||||
|
|
||||||
return self.get(
|
return self.get(
|
||||||
|
|||||||
@@ -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) -%}
|
{% macro mapping_table(caption='', field_headings=[], field_headings_visible=True, caption_visible=True) -%}
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<caption class="heading-medium table-heading{{ ' visuallyhidden' if not caption_visible}}">
|
<caption class="heading-medium table-heading{{ ' visuallyhidden' if not caption_visible}}">
|
||||||
@@ -129,3 +131,20 @@
|
|||||||
</span>
|
</span>
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
{% endmacro %}
|
{% 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 %}
|
||||||
|
|||||||
@@ -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" %}
|
{% extends "withnav_template.html" %}
|
||||||
|
|
||||||
{% block service_page_title %}
|
{% block service_page_title %}
|
||||||
All templates used this year
|
Templates used
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block maincolumn_content %}
|
{% 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 %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{% from "components/big-number.html" import big_number %}
|
{% from "components/big-number.html" import big_number %}
|
||||||
{% from "components/message-count-label.html" import message_count_label %}
|
{% from "components/message-count-label.html" import message_count_label %}
|
||||||
{% from "components/big-number.html" import big_number %}
|
{% 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'>
|
<div class='dashboard-table'>
|
||||||
{% call(item, row_number) list_table(
|
{% call(item, row_number) list_table(
|
||||||
@@ -25,16 +25,7 @@
|
|||||||
</span>
|
</span>
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
{% if template_statistics|length > 1 %}
|
{% if template_statistics|length > 1 %}
|
||||||
{% call field(align='right') %}
|
{{ spark_bar_field(item.count, most_used_template_count) }}
|
||||||
<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 %}
|
|
||||||
{% else %}
|
{% else %}
|
||||||
{% call field() %}
|
{% call field() %}
|
||||||
<span id='{{item.template_id}}' class="heading-small">
|
<span id='{{item.template_id}}' class="heading-small">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from functools import partial
|
||||||
import copy
|
import copy
|
||||||
from flask import url_for
|
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
|
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,
|
logged_in_client,
|
||||||
mocker,
|
mocker,
|
||||||
api_user_active,
|
api_user_active,
|
||||||
mock_get_service,
|
mock_get_service,
|
||||||
mock_get_service_templates,
|
mock_get_monthly_template_statistics,
|
||||||
mock_get_user,
|
mock_get_user,
|
||||||
mock_get_user_by_email,
|
mock_get_user_by_email,
|
||||||
mock_login,
|
|
||||||
mock_get_jobs,
|
|
||||||
mock_has_permissions,
|
mock_has_permissions,
|
||||||
|
partial_url,
|
||||||
):
|
):
|
||||||
mock_template_stats = mocker.patch('app.template_statistics_client.get_template_statistics_for_service',
|
response = logged_in_client.get(
|
||||||
return_value=copy.deepcopy(stub_template_stats))
|
partial_url('main.template_history', service_id=SERVICE_ONE_ID, _external=True)
|
||||||
|
)
|
||||||
response = logged_in_client.get(url_for('main.template_history', service_id=SERVICE_ONE_ID))
|
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
response.get_data(as_text=True)
|
mock_get_monthly_template_statistics.assert_called_once_with(SERVICE_ONE_ID, 2016)
|
||||||
mock_template_stats.assert_called_once_with(SERVICE_ONE_ID)
|
|
||||||
|
|
||||||
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
|
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 len(table_rows) == len(['April'])
|
||||||
assert 'Email template' in table_rows[0].find_all('th')[0].text
|
assert len(page.select('.table-no-data')) == len(['May', 'June', 'July'])
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
@freeze_time("2016-01-01 11:09:00.061258")
|
@freeze_time("2016-01-01 11:09:00.061258")
|
||||||
|
|||||||
@@ -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)
|
'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')
|
@pytest.fixture(scope='function')
|
||||||
def mock_get_template_statistics_for_template(mocker, service_one):
|
def mock_get_template_statistics_for_template(mocker, service_one):
|
||||||
def _get_stats(service_id, template_id):
|
def _get_stats(service_id, template_id):
|
||||||
|
|||||||
Reference in New Issue
Block a user