mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-05-12 04:01:13 -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 {
|
||||
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;
|
||||
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 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)
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user