diff --git a/app/assets/stylesheets/components/table.scss b/app/assets/stylesheets/components/table.scss index 8f908ddde..085b16e6f 100644 --- a/app/assets/stylesheets/components/table.scss +++ b/app/assets/stylesheets/components/table.scss @@ -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; +} diff --git a/app/assets/stylesheets/views/dashboard.scss b/app/assets/stylesheets/views/dashboard.scss index 0eee9993d..a99623384 100644 --- a/app/assets/stylesheets/views/dashboard.scss +++ b/app/assets/stylesheets/views/dashboard.scss @@ -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; + } + +} diff --git a/app/main/views/dashboard.py b/app/main/views/dashboard.py index bca543a31..c3000324e 100644 --- a/app/main/views/dashboard.py +++ b/app/main/views/dashboard.py @@ -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) + ) diff --git a/app/notify_client/template_statistics_api_client.py b/app/notify_client/template_statistics_api_client.py index 91dc7446c..fc44fa7b6 100644 --- a/app/notify_client/template_statistics_api_client.py +++ b/app/notify_client/template_statistics_api_client.py @@ -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( diff --git a/app/templates/components/table.html b/app/templates/components/table.html index 51974483e..62f74e851 100644 --- a/app/templates/components/table.html +++ b/app/templates/components/table.html @@ -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) -%}
@@ -129,3 +131,20 @@ {% endcall %} {% endmacro %} + + +{% macro spark_bar_field( + count, + max_count +) %} + {% call field(align='right') %} + + + {{ big_number( + count, + smallest=True + ) }} + + + {% endcall %} +{% endmacro %} diff --git a/app/templates/views/dashboard/all-template-statistics.html b/app/templates/views/dashboard/all-template-statistics.html index 39857108f..a0c803fce 100644 --- a/app/templates/views/dashboard/all-template-statistics.html +++ b/app/templates/views/dashboard/all-template-statistics.html @@ -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 %} -

All templates used this year

+

Templates used

- {% include 'views/dashboard/template-statistics.html' %} +
+ {{ pill( + items=years, + current_value=selected_year, + big_number_args={'smallest': True}, + ) }} +
+ +
+ {% for month in months %} +

{{ month.name }}

+ {% if not month.templates_used %} +

+ No messages sent +

+ {% else %} +
+ {% 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() %} + + {{ item.name }} + + {{ message_count_label(1, item.type, suffix='template')|capitalize }} + + + {% endcall %} + {{ spark_bar_field(item.requested_count, most_used_template_count) }} + {% endcall %} +
+ {% endif %} + {% endfor %} +
{% endblock %} diff --git a/app/templates/views/dashboard/template-statistics.html b/app/templates/views/dashboard/template-statistics.html index c94ec4e6b..9f3e0fab7 100644 --- a/app/templates/views/dashboard/template-statistics.html +++ b/app/templates/views/dashboard/template-statistics.html @@ -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 %}
{% call(item, row_number) list_table( @@ -25,16 +25,7 @@ {% endcall %} {% if template_statistics|length > 1 %} - {% call field(align='right') %} - - - {{ big_number( - item.count, - smallest=True - ) }} - - - {% endcall %} + {{ spark_bar_field(item.count, most_used_template_count) }} {% else %} {% call field() %} diff --git a/tests/app/main/views/test_dashboard.py b/tests/app/main/views/test_dashboard.py index 781ab73f7..b7143a7d3 100644 --- a/tests/app/main/views/test_dashboard.py +++ b/tests/app/main/views/test_dashboard.py @@ -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") diff --git a/tests/conftest.py b/tests/conftest.py index 0da1bcf65..c9c4d1c63 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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):