mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-08-24 00:07:02 -04:00
Merge pull request #961 from alphagov/add-quarterly-breakdown
Add monthly breakdown of usage
This commit is contained in:
@@ -179,3 +179,16 @@ details summary {
|
|||||||
border: 0;
|
border: 0;
|
||||||
margin-bottom: -$gutter + 2px;
|
margin-bottom: -$gutter + 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.body-copy-table {
|
||||||
|
|
||||||
|
table th,
|
||||||
|
table td {
|
||||||
|
font-size: 19px;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabular-numbers {
|
||||||
|
@include core-19($tabular-numbers: true);
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.table-field-heading-first {
|
.table-field-heading-first {
|
||||||
width: 52.5%
|
width: 52.5%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-row {
|
.table-row {
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ from flask import (
|
|||||||
url_for,
|
url_for,
|
||||||
session,
|
session,
|
||||||
jsonify,
|
jsonify,
|
||||||
current_app
|
current_app,
|
||||||
|
request,
|
||||||
|
abort
|
||||||
)
|
)
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
|
|
||||||
@@ -80,8 +82,16 @@ 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):
|
||||||
|
try:
|
||||||
|
year = int(request.args.get('year', 2016))
|
||||||
|
except ValueError:
|
||||||
|
abort(404)
|
||||||
return render_template(
|
return render_template(
|
||||||
'views/usage.html',
|
'views/usage.html',
|
||||||
|
months=get_free_paid_breakdown_for_billable_units(
|
||||||
|
year, service_api_client.get_billable_units(service_id, year)
|
||||||
|
),
|
||||||
|
year=year,
|
||||||
**calculate_usage(service_api_client.get_service_usage(service_id)['data'])
|
**calculate_usage(service_api_client.get_service_usage(service_id)['data'])
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -195,3 +205,58 @@ def format_weekly_stats_to_list(historical_stats):
|
|||||||
out.append(weekly_stats)
|
out.append(weekly_stats)
|
||||||
|
|
||||||
return sorted(out, key=lambda x: x['week_start'], reverse=True)
|
return sorted(out, key=lambda x: x['week_start'], reverse=True)
|
||||||
|
|
||||||
|
|
||||||
|
def get_months_for_financial_year(year):
|
||||||
|
return [
|
||||||
|
month.strftime('%B')
|
||||||
|
for month in (
|
||||||
|
get_months_for_year(4, 13, year) +
|
||||||
|
get_months_for_year(1, 4, year + 1)
|
||||||
|
)
|
||||||
|
if month < datetime.now()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_months_for_year(start, end, year):
|
||||||
|
return [datetime(year, month, 1) for month in range(start, end)]
|
||||||
|
|
||||||
|
|
||||||
|
def get_free_paid_breakdown_for_billable_units(year, billable_units):
|
||||||
|
cumulative = 0
|
||||||
|
for month in get_months_for_financial_year(year):
|
||||||
|
previous_cumulative = cumulative
|
||||||
|
monthly_usage = billable_units.get(month, 0)
|
||||||
|
cumulative += monthly_usage
|
||||||
|
breakdown = get_free_paid_breakdown_for_month(
|
||||||
|
cumulative, previous_cumulative, monthly_usage
|
||||||
|
)
|
||||||
|
yield {
|
||||||
|
'name': month,
|
||||||
|
'paid': breakdown['paid'],
|
||||||
|
'free': breakdown['free']
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_free_paid_breakdown_for_month(
|
||||||
|
cumulative,
|
||||||
|
previous_cumulative,
|
||||||
|
monthly_usage
|
||||||
|
):
|
||||||
|
allowance = 250000
|
||||||
|
|
||||||
|
if cumulative < allowance:
|
||||||
|
return {
|
||||||
|
'paid': 0,
|
||||||
|
'free': monthly_usage,
|
||||||
|
}
|
||||||
|
elif previous_cumulative < allowance:
|
||||||
|
return {
|
||||||
|
'paid': monthly_usage - (allowance - previous_cumulative),
|
||||||
|
'free': allowance - previous_cumulative
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
'paid': monthly_usage,
|
||||||
|
'free': 0
|
||||||
|
}
|
||||||
|
|||||||
@@ -211,6 +211,9 @@ class ServiceAPIClient(NotificationsAPIClient):
|
|||||||
def update_whitelist(self, service_id, data):
|
def update_whitelist(self, service_id, data):
|
||||||
return self.put(url='/service/{}/whitelist'.format(service_id), data=data)
|
return self.put(url='/service/{}/whitelist'.format(service_id), data=data)
|
||||||
|
|
||||||
|
def get_billable_units(self, service_id, year):
|
||||||
|
return self.get(url='/service/{}/billable-units?year={}'.format(service_id, year))
|
||||||
|
|
||||||
|
|
||||||
class ServicesBrowsableItem(BrowsableItem):
|
class ServicesBrowsableItem(BrowsableItem):
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% 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 %}
|
||||||
|
|
||||||
{% extends "withnav_template.html" %}
|
{% extends "withnav_template.html" %}
|
||||||
|
|
||||||
@@ -13,7 +14,9 @@
|
|||||||
<h2 class='heading-large'>Usage</h2>
|
<h2 class='heading-large'>Usage</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class='column-half'>
|
<div class='column-half'>
|
||||||
<span class="align-with-heading-copy">1 April 2016 to date</span>
|
<div class='align-with-heading-copy'>
|
||||||
|
Financial year {{ year }} to {{ year + 1 }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -49,7 +52,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class='column-half'>
|
<div class='column-half'>
|
||||||
<div class="keyline-block bottom-gutter">
|
<div class="keyline-block">
|
||||||
{{ big_number(
|
{{ big_number(
|
||||||
(sms_chargeable * sms_rate),
|
(sms_chargeable * sms_rate),
|
||||||
'spent',
|
'spent',
|
||||||
@@ -57,7 +60,50 @@
|
|||||||
smaller=True
|
smaller=True
|
||||||
) }}
|
) }}
|
||||||
</div>
|
</div>
|
||||||
<p>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dashboard-table body-copy-table">
|
||||||
|
{% call(month, row_index) list_table(
|
||||||
|
months,
|
||||||
|
caption="Total spend",
|
||||||
|
caption_visible=False,
|
||||||
|
empty_message='',
|
||||||
|
field_headings=[
|
||||||
|
'By month',
|
||||||
|
hidden_field_heading('Cost'),
|
||||||
|
],
|
||||||
|
field_headings_visible=True
|
||||||
|
) %}
|
||||||
|
{% 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 %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid-row">
|
||||||
|
<div class="column-half"> </div>
|
||||||
|
<div class="column-half">
|
||||||
|
<p class="align-with-heading-copy">
|
||||||
What counts as 1 text message?<br />
|
What counts as 1 text message?<br />
|
||||||
See <a href="{{ url_for('.pricing') }}">pricing</a>.
|
See <a href="{{ url_for('.pricing') }}">pricing</a>.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ import pytest
|
|||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from freezegun import freeze_time
|
from freezegun import freeze_time
|
||||||
|
|
||||||
from app.main.views.dashboard import get_dashboard_totals, format_weekly_stats_to_list
|
from app.main.views.dashboard import (
|
||||||
|
get_dashboard_totals,
|
||||||
|
format_weekly_stats_to_list,
|
||||||
|
get_free_paid_breakdown_for_billable_units
|
||||||
|
)
|
||||||
|
|
||||||
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
|
||||||
@@ -235,6 +239,57 @@ 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")
|
||||||
|
def test_usage_page(
|
||||||
|
client,
|
||||||
|
api_user_active,
|
||||||
|
mock_get_service,
|
||||||
|
mock_get_user,
|
||||||
|
mock_has_permissions,
|
||||||
|
mock_get_usage,
|
||||||
|
mock_get_billable_units
|
||||||
|
):
|
||||||
|
client.login(api_user_active)
|
||||||
|
response = client.get(url_for('main.usage', service_id=SERVICE_ONE_ID, year=2000))
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
mock_get_billable_units.assert_called_once_with(SERVICE_ONE_ID, 2000)
|
||||||
|
|
||||||
|
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
|
||||||
|
|
||||||
|
cols = page.find_all('div', {'class': 'column-half'})
|
||||||
|
|
||||||
|
assert cols[1].text.strip() == 'Financial year 2000 to 2001'
|
||||||
|
|
||||||
|
assert '123' in cols[2].text
|
||||||
|
assert 'Emails' in cols[2].text
|
||||||
|
|
||||||
|
assert '456,123' in cols[3].text
|
||||||
|
assert 'Text messages' in cols[3].text
|
||||||
|
|
||||||
|
table = page.find('table').text.strip()
|
||||||
|
|
||||||
|
assert 'April' in table
|
||||||
|
assert 'March' in table
|
||||||
|
assert '123 free text messages' in table
|
||||||
|
assert '£3,403.06' in table
|
||||||
|
assert '249,877 free text messages' 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_for_invalid_year(
|
||||||
|
client,
|
||||||
|
api_user_active,
|
||||||
|
mock_get_service,
|
||||||
|
mock_get_user,
|
||||||
|
mock_has_permissions
|
||||||
|
):
|
||||||
|
client.login(api_user_active)
|
||||||
|
assert client.get(url_for('main.usage', service_id=SERVICE_ONE_ID, year='abcd')).status_code == 404
|
||||||
|
|
||||||
|
|
||||||
def _test_dashboard_menu(mocker, app_, usr, service, permissions):
|
def _test_dashboard_menu(mocker, app_, usr, service, permissions):
|
||||||
with app_.test_request_context():
|
with app_.test_request_context():
|
||||||
with app_.test_client() as client:
|
with app_.test_client() as client:
|
||||||
@@ -520,3 +575,34 @@ def test_format_weekly_stats_to_list_has_stats_with_failure_rate():
|
|||||||
|
|
||||||
def _stats(requested, delivered, failed):
|
def _stats(requested, delivered, failed):
|
||||||
return {'requested': requested, 'delivered': delivered, 'failed': failed}
|
return {'requested': requested, 'delivered': delivered, 'failed': failed}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'now, expected_number_of_months', [
|
||||||
|
(freeze_time("2017-12-31 11:09:00.061258"), 12),
|
||||||
|
(freeze_time("2017-01-01 11:09:00.061258"), 10)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_get_free_paid_breakdown_for_billable_units(now, expected_number_of_months):
|
||||||
|
with now:
|
||||||
|
assert list(get_free_paid_breakdown_for_billable_units(
|
||||||
|
2016, {
|
||||||
|
'April': 100000,
|
||||||
|
'May': 100000,
|
||||||
|
'June': 100000,
|
||||||
|
'February': 1234
|
||||||
|
}
|
||||||
|
)) == [
|
||||||
|
{'name': 'April', 'free': 100000, 'paid': 0},
|
||||||
|
{'name': 'May', 'free': 100000, 'paid': 0},
|
||||||
|
{'name': 'June', 'free': 50000, 'paid': 50000},
|
||||||
|
{'name': 'July', 'free': 0, 'paid': 0},
|
||||||
|
{'name': 'August', 'free': 0, 'paid': 0},
|
||||||
|
{'name': 'September', 'free': 0, 'paid': 0},
|
||||||
|
{'name': 'October', 'free': 0, 'paid': 0},
|
||||||
|
{'name': 'November', 'free': 0, 'paid': 0},
|
||||||
|
{'name': 'December', 'free': 0, 'paid': 0},
|
||||||
|
{'name': 'January', 'free': 0, 'paid': 0},
|
||||||
|
{'name': 'February', 'free': 0, 'paid': 1234},
|
||||||
|
{'name': 'March', 'free': 0, 'paid': 0}
|
||||||
|
][:expected_number_of_months]
|
||||||
|
|||||||
@@ -335,6 +335,17 @@ def test_can_show_notifications(
|
|||||||
page=expected_page_argument
|
page=expected_page_argument
|
||||||
) == page.find("div", {'data-key': 'notifications'})['data-resource']
|
) == page.find("div", {'data-key': 'notifications'})['data-resource']
|
||||||
|
|
||||||
|
path_to_json = page.find("div", {'data-key': 'notifications'})['data-resource']
|
||||||
|
|
||||||
|
assert (
|
||||||
|
'/services/{}/notifications/{}.json?status={}&page={}'.format(
|
||||||
|
service_one['id'], message_type, status_argument, expected_page_argument
|
||||||
|
) in path_to_json or
|
||||||
|
'/services/{}/notifications/{}.json?page={}&status={}'.format(
|
||||||
|
service_one['id'], message_type, expected_page_argument, status_argument
|
||||||
|
) in path_to_json
|
||||||
|
)
|
||||||
|
|
||||||
mock_get_notifications.assert_called_with(
|
mock_get_notifications.assert_called_with(
|
||||||
limit_days=7,
|
limit_days=7,
|
||||||
page=expected_page_argument,
|
page=expected_page_argument,
|
||||||
|
|||||||
@@ -1172,14 +1172,26 @@ def mock_get_template_statistics_for_template(mocker, service_one):
|
|||||||
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):
|
||||||
return {'data': {
|
return {'data': {
|
||||||
"sms_count": 123,
|
"sms_count": 456123,
|
||||||
"email_count": 456
|
"email_count": 123
|
||||||
}}
|
}}
|
||||||
|
|
||||||
return mocker.patch(
|
return mocker.patch(
|
||||||
'app.service_api_client.get_service_usage', side_effect=_get_usage)
|
'app.service_api_client.get_service_usage', side_effect=_get_usage)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='function')
|
||||||
|
def mock_get_billable_units(mocker):
|
||||||
|
def _get_usage(service_id, year):
|
||||||
|
return {
|
||||||
|
"April": 123,
|
||||||
|
"March": 456123
|
||||||
|
}
|
||||||
|
|
||||||
|
return mocker.patch(
|
||||||
|
'app.service_api_client.get_billable_units', side_effect=_get_usage)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='function')
|
@pytest.fixture(scope='function')
|
||||||
def mock_events(mocker):
|
def mock_events(mocker):
|
||||||
def _create_event(event_type, event_data):
|
def _create_event(event_type, event_data):
|
||||||
|
|||||||
Reference in New Issue
Block a user