Merge pull request #1094 from alphagov/usage-by-year

Filter and navigate usage by financial year
This commit is contained in:
Chris Hill-Scott
2017-02-06 12:33:25 +00:00
committed by GitHub
8 changed files with 112 additions and 75 deletions

View File

@@ -19,7 +19,7 @@ from app import (
template_statistics_client template_statistics_client
) )
from app.statistics_utils import get_formatted_percentage, add_rate_to_job from app.statistics_utils import get_formatted_percentage, add_rate_to_job
from app.utils import user_has_permissions from app.utils import user_has_permissions, get_current_financial_year
# This is a placeholder view method to be replaced # This is a placeholder view method to be replaced
@@ -79,17 +79,26 @@ def template_history(service_id):
@login_required @login_required
@user_has_permissions('manage_settings', admin_override=True) @user_has_permissions('manage_settings', admin_override=True)
def usage(service_id): def usage(service_id):
current_financial_year = get_current_financial_year()
try: try:
year = int(request.args.get('year', 2016)) year = int(request.args.get('year', current_financial_year))
except ValueError: except ValueError:
abort(404) abort(404)
return render_template( return render_template(
'views/usage.html', 'views/usage.html',
months=get_free_paid_breakdown_for_billable_units( months=list(get_free_paid_breakdown_for_billable_units(
year, service_api_client.get_billable_units(service_id, year) year, service_api_client.get_billable_units(service_id, year)
), )),
year=year, selected_year=year,
**calculate_usage(service_api_client.get_service_usage(service_id)['data']) 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)
],
**calculate_usage(service_api_client.get_service_usage(service_id, year)['data'])
) )
@@ -156,7 +165,10 @@ def get_dashboard_partials(service_id):
'has_jobs': bool(immediate_jobs), 'has_jobs': bool(immediate_jobs),
'usage': render_template( 'usage': render_template(
'views/dashboard/_usage.html', 'views/dashboard/_usage.html',
**calculate_usage(service_api_client.get_service_usage(service_id)['data']) **calculate_usage(service_api_client.get_service_usage(
service_id,
get_current_financial_year(),
)['data'])
), ),
} }

View File

@@ -213,8 +213,11 @@ class ServiceAPIClient(NotifyAdminAPIClient):
def get_service_history(self, service_id): def get_service_history(self, service_id):
return self.get('/service/{0}/history'.format(service_id)) return self.get('/service/{0}/history'.format(service_id))
def get_service_usage(self, service_id): def get_service_usage(self, service_id, year=None):
return self.get('/service/{0}/fragment/aggregate_statistics'.format(service_id)) return self.get(
'/service/{0}/fragment/aggregate_statistics'.format(service_id),
params=dict(year=year)
)
def get_weekly_notification_stats(self, service_id): def get_weekly_notification_stats(self, service_id):
return self.get(url='/service/{}/notifications/weekly'.format(service_id)) return self.get(url='/service/{}/notifications/weekly'.format(service_id))

View File

@@ -3,7 +3,8 @@
{% macro pill( {% macro pill(
title, title,
items=[], items=[],
current_value=None current_value=None,
big_number_args={'smaller': True}
) %} ) %}
<nav role='navigation' class='pill' aria-labelledby="pill_{{title}}"> <nav role='navigation' class='pill' aria-labelledby="pill_{{title}}">
<h2 id="pill_{{title}}" class="visuallyhidden">{{title}}</h2> <h2 id="pill_{{title}}" class="visuallyhidden">{{title}}</h2>
@@ -13,8 +14,8 @@
{% else %} {% else %}
<a href="{{ link }}"> <a href="{{ link }}">
{% endif %} {% endif %}
{{ big_number(count, smaller=True) }} {{ big_number(count, **big_number_args) }}
<div class="pill-label">{{ label }}</div> <div class="pill-label">{{ label }}</div>
{% if current_value == option %} {% if current_value == option %}
</span> </span>
{% else %} {% else %}

View File

@@ -1,5 +1,6 @@
{% from "components/big-number.html" import big_number %} {% from "components/big-number.html" import big_number %}
{% from "components/table.html" import list_table, field, hidden_field_heading, row_heading, text_field %} {% from "components/table.html" import list_table, field, hidden_field_heading, row_heading, text_field %}
{% from "components/pill.html" import pill %}
{% extends "withnav_template.html" %} {% extends "withnav_template.html" %}
@@ -9,15 +10,10 @@
{% block maincolumn_content %} {% block maincolumn_content %}
<div class='grid-row'> <h2 class='heading-large'>Usage</h2>
<div class='column-half'>
<h2 class='heading-large'>Usage</h2> <div class="bottom-gutter">
</div> {{ pill('Year', years, selected_year, big_number_args={'smallest': True}) }}
<div class='column-half'>
<div class='align-with-heading-copy'>
Financial year {{ year }} to {{ year + 1 }}
</div>
</div>
</div> </div>
<div class='grid-row'> <div class='grid-row'>
@@ -63,45 +59,51 @@
</div> </div>
</div> </div>
<div class="dashboard-table body-copy-table"> {% if months %}
{% call(month, row_index) list_table( <div class="dashboard-table body-copy-table">
months, {% call(month, row_index) list_table(
caption="Total spend", months,
caption_visible=False, caption="Total spend",
empty_message='', caption_visible=False,
field_headings=[ empty_message='',
'By month', field_headings=[
hidden_field_heading('Cost'), 'By month',
], hidden_field_heading('Cost'),
field_headings_visible=True ],
) %} field_headings_visible=True
{% call row_heading() %} ) %}
{{ month.name }} {% call row_heading() %}
{{ month.name }}
{% endcall %}
{% call field(align='left') %}
{{ big_number(
sms_rate * month.paid,
currency="£",
smallest=True
) }}
<ul>
{% if month.free %}
<li class="tabular-numbers">{{ "{:,}".format(month.free) }} free text messages</li>
{% endif %}
{% if month.paid %}
<li class="tabular-numbers">{{ "{:,}".format(month.paid) }} text messages at
{{- ' {:.2f}p'.format(sms_rate * 100) }}</li>
{% endif %}
{% if not (month.free or month.paid) %}
<li aria-hidden="true"></li>
{% endif %}
</ul>
{% endcall %}
{% endcall %} {% endcall %}
{% call field(align='left') %} </div>
{{ big_number( {% endif %}
sms_rate * month.paid,
currency="£",
smallest=True
) }}
<ul>
{% if month.free %}
<li class="tabular-numbers">{{ "{:,}".format(month.free) }} free text messages</li>
{% endif %}
{% if month.paid %}
<li class="tabular-numbers">{{ "{:,}".format(month.paid) }} text messages at
{{- ' {:.2f}p'.format(sms_rate * 100) }}</li>
{% endif %}
{% if not (month.free or month.paid) %}
<li aria-hidden="true"></li>
{% endif %}
</ul>
{% endcall %}
{% endcall %}
</div>
<div class="grid-row"> <div class="grid-row">
<div class="column-half">&nbsp;</div> <div class="column-half">
<p class="align-with-heading-copy">
Financial year ends 31 March.
</p>
</div>
<div class="column-half"> <div class="column-half">
<p class="align-with-heading-copy"> <p class="align-with-heading-copy">
What counts as 1 text message?<br /> What counts as 1 text message?<br />

View File

@@ -4,6 +4,7 @@ from io import StringIO, BytesIO
from os import path from os import path
from functools import wraps from functools import wraps
import unicodedata import unicodedata
from datetime import datetime
from flask import ( from flask import (
abort, abort,
@@ -322,3 +323,10 @@ def png_from_pdf(pdf_endpoint):
filename_or_fp=output, filename_or_fp=output,
mimetype='image/png', mimetype='image/png',
) )
def get_current_financial_year():
now = datetime.utcnow()
current_month = int(now.strftime('%-m'))
current_year = int(now.strftime('%Y'))
return current_year if current_month > 3 else current_year - 1

View File

@@ -14,6 +14,7 @@ from app.main.views.dashboard import (
from tests import validate_route_permission from tests import validate_route_permission
from tests.conftest import SERVICE_ONE_ID from tests.conftest import SERVICE_ONE_ID
from tests.app.test_utils import normalize_spaces
stub_template_stats = [ stub_template_stats = [
{ {
@@ -233,33 +234,34 @@ def test_should_show_recent_jobs_on_dashboard(
assert table_rows[index].find_all('td')[column_index].text.strip() == str(count) assert table_rows[index].find_all('td')[column_index].text.strip() == str(count)
@freeze_time("2016-12-31 11:09:00.061258") @freeze_time("2012-03-31 12:12:12")
def test_usage_page( def test_usage_page(
logged_in_client, logged_in_client,
api_user_active,
mock_get_service,
mock_get_user,
mock_has_permissions,
mock_get_usage, mock_get_usage,
mock_get_billable_units, mock_get_billable_units,
): ):
response = logged_in_client.get(url_for('main.usage', service_id=SERVICE_ONE_ID, year=2000)) response = logged_in_client.get(url_for('main.usage', service_id=SERVICE_ONE_ID))
assert response.status_code == 200 assert response.status_code == 200
mock_get_billable_units.assert_called_once_with(SERVICE_ONE_ID, 2000) mock_get_billable_units.assert_called_once_with(SERVICE_ONE_ID, 2011)
mock_get_usage.assert_called_once_with(SERVICE_ONE_ID, 2011)
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser') page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
cols = page.find_all('div', {'class': 'column-half'}) cols = page.find_all('div', {'class': 'column-half'})
nav = page.find('nav', {'class': 'pill'})
nav_links = nav.find_all('a')
assert cols[1].text.strip() == 'Financial year 2000 to 2001' assert normalize_spaces(nav_links[0].text) == '2010 to 2011 financial year'
assert normalize_spaces(nav.find('span').text) == '2011 to 2012 financial year'
assert normalize_spaces(nav_links[1].text) == '2012 to 2013 financial year'
assert '123' in cols[2].text assert '123' in cols[0].text
assert 'Emails' in cols[2].text assert 'Emails' in cols[0].text
assert '456,123' in cols[3].text assert '456,123' in cols[1].text
assert 'Text messages' in cols[3].text assert 'Text messages' in cols[1].text
table = page.find('table').text.strip() table = page.find('table').text.strip()
@@ -271,13 +273,18 @@ def test_usage_page(
assert '206,246 text messages at 1.65p' in table assert '206,246 text messages at 1.65p' in table
@freeze_time("2016-12-31 11:09:00.061258") def test_usage_page_with_year_argument(
logged_in_client,
mock_get_usage,
mock_get_billable_units
):
assert logged_in_client.get(url_for('main.usage', service_id=SERVICE_ONE_ID, year=2000)).status_code == 200
mock_get_billable_units.assert_called_once_with(SERVICE_ONE_ID, 2000)
mock_get_usage.assert_called_once_with(SERVICE_ONE_ID, 2000)
def test_usage_page_for_invalid_year( def test_usage_page_for_invalid_year(
logged_in_client, logged_in_client,
api_user_active,
mock_get_service,
mock_get_user,
mock_has_permissions,
): ):
assert logged_in_client.get(url_for('main.usage', service_id=SERVICE_ONE_ID, year='abcd')).status_code == 404 assert logged_in_client.get(url_for('main.usage', service_id=SERVICE_ONE_ID, year='abcd')).status_code == 404

View File

@@ -148,3 +148,7 @@ def test_generate_notifications_csv_formats_row_number_correctly(mocker, row_num
assert len(csv_rows) == 1 assert len(csv_rows) == 1
assert csv_rows[0].get('Row number') == expected_result assert csv_rows[0].get('Row number') == expected_result
def normalize_spaces(string):
return ' '.join(string.split())

View File

@@ -1256,7 +1256,7 @@ def mock_get_template_statistics_for_template(mocker, service_one):
@pytest.fixture(scope='function') @pytest.fixture(scope='function')
def mock_get_usage(mocker, service_one, fake_uuid): def mock_get_usage(mocker, service_one, fake_uuid):
def _get_usage(service_id): def _get_usage(service_id, year=None):
return {'data': { return {'data': {
"sms_count": 456123, "sms_count": 456123,
"email_count": 123 "email_count": 123