From d04bde5486fe8892b1bca89044a09af38528f459 Mon Sep 17 00:00:00 2001 From: Chris Hill-Scott Date: Thu, 17 Mar 2016 11:44:00 +0000 Subject: [PATCH 1/3] Add API client for notification statistic Adds a client for the endpoints added in https://github.com/alphagov/notifications-api/commit/67c4bd2263364f4f61a11445bf34929442c88da6 --- app/__init__.py | 3 +++ app/notify_client/statistics_api_client.py | 18 ++++++++++++++++++ .../notify_client/test_statistics_client.py | 13 +++++++++++++ tests/app/main/views/test_dashboard.py | 1 + tests/app/main/views/test_sign_out.py | 1 + tests/conftest.py | 9 +++++++++ 6 files changed, 45 insertions(+) create mode 100644 app/notify_client/statistics_api_client.py create mode 100644 tests/app/main/notify_client/test_statistics_client.py diff --git a/app/__init__.py b/app/__init__.py index d0ce1d245..871a5589e 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -19,6 +19,7 @@ from app.notify_client.job_api_client import JobApiClient from app.notify_client.notification_api_client import NotificationApiClient from app.notify_client.status_api_client import StatusApiClient from app.notify_client.invite_api_client import InviteApiClient +from app.notify_client.statistics_api_client import StatisticsApiClient from app.its_dangerous_session import ItsdangerousSessionInterface from app.asset_fingerprinter import AssetFingerprinter from utils.recipients import validate_phone_number, InvalidPhoneError @@ -36,6 +37,7 @@ job_api_client = JobApiClient() notification_api_client = NotificationApiClient() status_api_client = StatusApiClient() invite_api_client = InviteApiClient() +statistics_api_client = StatisticsApiClient() asset_fingerprinter = AssetFingerprinter() @@ -55,6 +57,7 @@ def create_app(config_name, config_overrides=None): notification_api_client.init_app(application) status_api_client.init_app(application) invite_api_client.init_app(application) + statistics_api_client.init_app(application) login_manager.init_app(application) login_manager.login_view = 'main.sign_in' diff --git a/app/notify_client/statistics_api_client.py b/app/notify_client/statistics_api_client.py new file mode 100644 index 000000000..9ef40bbd6 --- /dev/null +++ b/app/notify_client/statistics_api_client.py @@ -0,0 +1,18 @@ +from notifications_python_client.base import BaseAPIClient + + +class StatisticsApiClient(BaseAPIClient): + def __init__(self, base_url=None, client_id=None, secret=None): + super(self.__class__, self).__init__(base_url=base_url or 'base_url', + client_id=client_id or 'client_id', + secret=secret or 'secret') + + def init_app(self, app): + self.base_url = app.config['API_HOST_NAME'] + self.client_id = app.config['ADMIN_CLIENT_USER_NAME'] + self.secret = app.config['ADMIN_CLIENT_SECRET'] + + def get_statistics_for_service(self, service_id): + return self.get( + url='/service/{}/notifications-statistics'.format(service_id), + ) diff --git a/tests/app/main/notify_client/test_statistics_client.py b/tests/app/main/notify_client/test_statistics_client.py new file mode 100644 index 000000000..3dc24c16a --- /dev/null +++ b/tests/app/main/notify_client/test_statistics_client.py @@ -0,0 +1,13 @@ +from app.notify_client.statistics_api_client import StatisticsApiClient + + +def test_client_uses_correct_find_by_email(mocker, api_user_active): + + expected_url = '/service/a1b2c3d4/notifications-statistics' + + client = StatisticsApiClient() + mock_get = mocker.patch('app.notify_client.statistics_api_client.StatisticsApiClient.get') + + client.get_statistics_for_service('a1b2c3d4') + + mock_get.assert_called_once_with(url=expected_url) diff --git a/tests/app/main/views/test_dashboard.py b/tests/app/main/views/test_dashboard.py index 0902f6293..0c88c68d1 100644 --- a/tests/app/main/views/test_dashboard.py +++ b/tests/app/main/views/test_dashboard.py @@ -6,6 +6,7 @@ def test_should_show_recent_jobs_on_dashboard(app_, api_user_active, mock_get_service, mock_get_service_templates, + mock_get_service_statistics, mock_get_user, mock_get_user_by_email, mock_login, diff --git a/tests/app/main/views/test_sign_out.py b/tests/app/main/views/test_sign_out.py index a6b8e5fcb..1d4860a71 100644 --- a/tests/app/main/views/test_sign_out.py +++ b/tests/app/main/views/test_sign_out.py @@ -16,6 +16,7 @@ def test_sign_out_user(app_, mock_get_user, mock_get_user_by_email, mock_get_service_templates, + mock_get_service_statistics, mock_login, mock_get_jobs): with app_.test_request_context(): diff --git a/tests/conftest.py b/tests/conftest.py index 9d5c88f39..b5f01d91f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -154,6 +154,15 @@ def mock_delete_service(mocker, mock_get_service): 'app.notifications_api_client.delete_service', side_effect=_delete) +@pytest.fixture(scope='function') +def mock_get_service_statistics(mocker): + def _create(service_id): + return {'data': []} + + return mocker.patch( + 'app.statistics_api_client.get_statistics_for_service', side_effect=_create) + + @pytest.fixture(scope='function') def mock_get_service_template(mocker): def _create(service_id, template_id): From b38ae08ad6f08e0d6b64fd554d9806d3d4d97fdf Mon Sep 17 00:00:00 2001 From: Chris Hill-Scott Date: Thu, 17 Mar 2016 11:45:48 +0000 Subject: [PATCH 2/3] Put some statistics on the dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds two new sections to the dashboard 1. A banner telling you about trial mode, including a count of how many messages you have left today, which is a restriction of trial mode 2. Panels with counts of how many emails and text messages have been sent in a day, plus the failure rates for each It does **not**: - link through to any further information about what trial mode is (coming later) - link through to pages for the failure rates (coming later) - change the ‘recent jobs’ section to ‘recent notifications’ --- app/assets/images/tick-black.png | Bin 0 -> 4057 bytes app/assets/stylesheets/_grids.scss | 6 +- app/assets/stylesheets/components/banner.scss | 49 ++++++++++-- .../stylesheets/components/big-number.scss | 57 ++++++++++++++ app/main/views/dashboard.py | 30 ++++++- app/templates/components/banner.html | 16 +--- app/templates/components/big-number.html | 11 +++ app/templates/views/choose-template.html | 4 +- app/templates/views/dashboard/dashboard.html | 20 +++++ .../views/dashboard/get-started.html | 19 +++++ app/templates/views/dashboard/jobs.html | 28 +++++++ app/templates/views/dashboard/today.html | 25 ++++++ .../views/dashboard/trial-mode-banner.html | 24 ++++++ app/templates/views/service_dashboard.html | 74 ------------------ tests/app/main/views/test_accept_invite.py | 1 + tests/app/main/views/test_dashboard.py | 1 + tests/conftest.py | 2 +- 17 files changed, 268 insertions(+), 99 deletions(-) create mode 100644 app/assets/images/tick-black.png create mode 100644 app/templates/views/dashboard/dashboard.html create mode 100644 app/templates/views/dashboard/get-started.html create mode 100644 app/templates/views/dashboard/jobs.html create mode 100644 app/templates/views/dashboard/today.html create mode 100644 app/templates/views/dashboard/trial-mode-banner.html delete mode 100644 app/templates/views/service_dashboard.html diff --git a/app/assets/images/tick-black.png b/app/assets/images/tick-black.png new file mode 100644 index 0000000000000000000000000000000000000000..dd050c6c78982b1c170c588e549aa38938654ca8 GIT binary patch literal 4057 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 literal 0 HcmV?d00001 diff --git a/app/assets/stylesheets/_grids.scss b/app/assets/stylesheets/_grids.scss index 490f4d01d..ec083e809 100644 --- a/app/assets/stylesheets/_grids.scss +++ b/app/assets/stylesheets/_grids.scss @@ -2,6 +2,10 @@ @include grid-column(3/4); } +.column-one-sixth { + @include grid-column(1/6); +} + .column-one-eighth { @include grid-column(1/8); } @@ -16,7 +20,7 @@ } .bottom-gutter-2-3 { - margin-bottom: $gutter * 2/3; + margin-bottom: $gutter-two-thirds; } .align-with-heading { diff --git a/app/assets/stylesheets/components/banner.scss b/app/assets/stylesheets/components/banner.scss index 884523bdd..36ebad07d 100644 --- a/app/assets/stylesheets/components/banner.scss +++ b/app/assets/stylesheets/components/banner.scss @@ -26,9 +26,8 @@ } -.banner-with-tick, -.banner-default-with-tick { - @extend %banner; +%banner-with-tick, +.banner-with-tick { padding: $gutter-half ($gutter + $gutter-half); background-image: file-url('tick-white.png'); background-size: 19px; @@ -37,6 +36,11 @@ font-weight: bold; } +.banner-default-with-tick { + @extend %banner; + @extend %banner-with-tick; +} + .banner-dangerous { @extend %banner; @@ -54,13 +58,15 @@ } +%banner-tip, .banner-tip { @extend %banner; - background: $yellow; + @include bold-19; + background-color: $yellow; color: $text-colour; text-align: left; - font-weight: bold; + margin-top: 0; a { &:link, @@ -76,6 +82,12 @@ } +.banner-tip-with-tick { + @extend %banner-with-tick; + @extend %banner-tip; + background-image: file-url('tick-black.png'); +} + .banner-info, .banner-important { @extend %banner; @@ -91,3 +103,30 @@ .banner-info { background-image: file-url('icon-information-2x.png'); } + +.banner-mode { + + @extend %banner; + background: $govuk-blue; + color: $white; + margin-top: $gutter; + + .heading-medium { + margin-top: 0; + } + + a { + + &:link, + &:visited { + color: $white; + } + + &:hover, + &:active { + color: $light-blue-25; + } + + } + +} diff --git a/app/assets/stylesheets/components/big-number.scss b/app/assets/stylesheets/components/big-number.scss index 0bce53b25..a14f30d9b 100644 --- a/app/assets/stylesheets/components/big-number.scss +++ b/app/assets/stylesheets/components/big-number.scss @@ -1,3 +1,4 @@ +%big-number, .big-number { @include bold-48($tabular-numbers: true); @@ -5,6 +6,62 @@ &-label { @include core-19; display: block; + padding-bottom: $gutter-half; + } + +} + +.big-number-with-status { + + @extend %big-number; + background: $text-colour; + color: $white; + + .big-number { + padding: 15px; + } + + .big-number-label { + padding-bottom: 0; + } + + .big-number-status { + + display: block; + background: $green; + position: relative; + + &-error-percentage { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: $error-colour; + z-index: 1; + } + + a { + + &:link, + &:visited, + &:active, + &:hover { + color: $white; + } + + } + + .big-number { + @include bold-19; + position: relative; + z-index: 2; + } + + .big-number-label { + display: inline; + } + } } diff --git a/app/main/views/dashboard.py b/app/main/views/dashboard.py index f681faa45..c1f773842 100644 --- a/app/main/views/dashboard.py +++ b/app/main/views/dashboard.py @@ -8,7 +8,7 @@ from flask_login import login_required from app.main import main from app.main.dao.services_dao import get_service_by_id from app.main.dao import templates_dao -from app import job_api_client +from app import job_api_client, statistics_api_client @main.route("/services//dashboard") @@ -27,11 +27,35 @@ def service_dashboard(service_id): message = 'You have successfully accepted your invitation and been added to {}'.format(service_name) flash(message, 'default_with_tick') + statistics = statistics_api_client.get_statistics_for_service(service_id)['data'] + return render_template( - 'views/service_dashboard.html', + 'views/dashboard/dashboard.html', jobs=jobs[:5], more_jobs_to_show=(len(jobs) > 5), free_text_messages_remaining='250,000', spent_this_month='0.00', - template_count=len(templates), + service=service['data'], + statistics=expand_statistics(statistics), + templates=templates, service_id=str(service_id)) + + +def expand_statistics(statistics, danger_zone=25): + + if not statistics or not statistics[0]: + return {} + + today = statistics[0] + + today.update({ + 'emails_failure_rate': int(today['emails_error'] / today['emails_requested'] * 100), + 'sms_failure_rate': int(today['sms_error'] / today['sms_requested'] * 100) + }) + + today.update({ + 'emails_percentage_of_danger_zone': min(today['emails_failure_rate'] / (danger_zone / 100), 100), + 'sms_percentage_of_danger_zone': min(today['sms_failure_rate'] / (danger_zone / 100), 100) + }) + + return today diff --git a/app/templates/components/banner.html b/app/templates/components/banner.html index 0c0ef7abc..d008b5b57 100644 --- a/app/templates/components/banner.html +++ b/app/templates/components/banner.html @@ -1,19 +1,9 @@ {% macro banner(body, type=None, with_tick=False, delete_button=None, subhead=None) %}
- {% if subhead %} -
-
- {{ subhead }} -
-
- {% endif %} - + {% if subhead -%} + {{ subhead }}  + {%- endif -%} {{ body }} - - {% if subhead %} -
-
- {% endif %} {% if delete_button %}
diff --git a/app/templates/components/big-number.html b/app/templates/components/big-number.html index 089466c25..9ee6affb9 100644 --- a/app/templates/components/big-number.html +++ b/app/templates/components/big-number.html @@ -4,3 +4,14 @@ {{ label }}
{% endmacro %} + + +{% macro big_number_with_status(number, label, status_number, status_label, percentage_bad=0) %} +
+ {{ big_number(number, label) }} +
+
+ {{ big_number(status_number, status_label) }} +
+
+{% endmacro %} diff --git a/app/templates/views/choose-template.html b/app/templates/views/choose-template.html index a5d28fd88..e4c58e947 100644 --- a/app/templates/views/choose-template.html +++ b/app/templates/views/choose-template.html @@ -37,9 +37,9 @@ {% if current_user.has_permissions(['send_texts', 'send_emails', 'send_letters'], or_=True) %} {{ banner( """ - Send yourself a test message + Send yourself a test """, - subhead='Next step', + subhead='Next step:', type="tip" )}} {% endif %} diff --git a/app/templates/views/dashboard/dashboard.html b/app/templates/views/dashboard/dashboard.html new file mode 100644 index 000000000..67fb31a53 --- /dev/null +++ b/app/templates/views/dashboard/dashboard.html @@ -0,0 +1,20 @@ +{% extends "withnav_template.html" %} + +{% block page_title %} + {{ session.get('service_name', 'Dashboard') }} – GOV.UK Notify +{% endblock %} + +{% block maincolumn_content %} + + {% if service.restricted %} + {% include 'views/dashboard/trial-mode-banner.html' %} + {% endif %} + + {% if not jobs %} + {% include 'views/dashboard/get-started.html' %} + {% else %} + {% include 'views/dashboard/today.html' %} + {% include 'views/dashboard/jobs.html' %} + {% endif %} + +{% endblock %} diff --git a/app/templates/views/dashboard/get-started.html b/app/templates/views/dashboard/get-started.html new file mode 100644 index 000000000..d1b8adc01 --- /dev/null +++ b/app/templates/views/dashboard/get-started.html @@ -0,0 +1,19 @@ +{% from "components/banner.html" import banner_wrapper %} + +

Get started

+
    + {% if current_user.has_permissions(['manage_templates']) %} +
  1. + {% call banner_wrapper(type="tip", subhead='1.' if not templates else None, with_tick=templates|length) %} + Add a template + {% endcall %} +
  2. + {% endif %} + {% if current_user.has_permissions(['send_texts', 'send_emails', 'send_letters']) %} +
  3. + {% call banner_wrapper(type="tip", subhead='2.') %} + Send yourself a message + {% endcall %} +
  4. + {% endif %} +
diff --git a/app/templates/views/dashboard/jobs.html b/app/templates/views/dashboard/jobs.html new file mode 100644 index 000000000..941bb51cc --- /dev/null +++ b/app/templates/views/dashboard/jobs.html @@ -0,0 +1,28 @@ +{% from "components/table.html" import list_table, field, right_aligned_field_heading, hidden_field_heading %} + +{% call(item) list_table( + jobs, + caption="Recent batch jobs", + empty_message='You haven’t sent any text messages yet', + field_headings=['File', 'Started', right_aligned_field_heading('Rows'), right_aligned_field_heading('Sent')] +) %} + {% call field() %} + {{ item.original_file_name }} + {% endcall %} + {% call field() %} + {{ item.created_at|format_datetime }} + {% endcall %} + {% call field(align='right') %} + {{ item.notification_count }} + {% endcall %} + {% call field(align='right') %} + {{ item.notifications_sent }} + {% endcall %} +{% endcall %} +{% if more_jobs_to_show %} + {% if current_user.has_permissions(['send_texts', 'send_emails', 'send_letters']) %} + + {% endif %} +{% endif %} diff --git a/app/templates/views/dashboard/today.html b/app/templates/views/dashboard/today.html new file mode 100644 index 000000000..bc1d13abf --- /dev/null +++ b/app/templates/views/dashboard/today.html @@ -0,0 +1,25 @@ +{% from "components/big-number.html" import big_number_with_status %} + +

+ Sent today +

+
+
+ {{ big_number_with_status( + statistics.get('emails_requested', 0), + 'email' if statistics.get('emails_requested') == 1 else 'emails', + '{}%'.format(statistics.get('emails_failure_rate', 0)), + 'failed', + statistics.get('emails_percentage_of_danger_zone', 0) + ) }} +
+
+ {{ big_number_with_status( + statistics.get('sms_requested', 0), + 'text message' if statistics.get('sms_requested') == 1 else 'text messages', + '{}%'.format(statistics.get('sms_failure_rate', 0)), + 'failed', + statistics.get('sms_percentage_of_danger_zone', 0) + ) }} +
+
diff --git a/app/templates/views/dashboard/trial-mode-banner.html b/app/templates/views/dashboard/trial-mode-banner.html new file mode 100644 index 000000000..1ad62066e --- /dev/null +++ b/app/templates/views/dashboard/trial-mode-banner.html @@ -0,0 +1,24 @@ +{% from "components/banner.html" import banner_wrapper %} +{% from "components/big-number.html" import big_number %} + +{% call banner_wrapper(type="mode") %} + +
+
+

Trial mode

+

+ We’ll only deliver messages to you and members of your team + +

+
+
+   +
+
+ {{ big_number( + service.limit - statistics.get('emails_requested', 0) - statistics.get('sms_requested', 0), + 'messages left today' + ) }} +
+
+{% endcall %} diff --git a/app/templates/views/service_dashboard.html b/app/templates/views/service_dashboard.html deleted file mode 100644 index f7a6dc7fa..000000000 --- a/app/templates/views/service_dashboard.html +++ /dev/null @@ -1,74 +0,0 @@ -{% extends "withnav_template.html" %} -{% from "components/banner.html" import banner_wrapper %} -{% from "components/table.html" import list_table, field, right_aligned_field_heading %} -{% from "components/big-number.html" import big_number %} - -{% block page_title %} - {{ session.get('service_name', 'Dashboard') }} – GOV.UK Notify -{% endblock %} - -{% block maincolumn_content %} - -
    -
  • - {{ big_number( - free_text_messages_remaining, - 'free text messages remaining' - )}} -
  • -
  • - {{ big_number( - '£' + spent_this_month, - 'spent this month' - )}} -
  • -
- - {% if not template_count and not jobs %} - {% call banner_wrapper(subhead='Get started', type="tip") %} -
    - {% if current_user.has_permissions(['manage_templates']) %} -
  1. - Add a template -
  2. - {% endif %} - {% if current_user.has_permissions(['send_texts', 'send_emails', 'send_letters']) %} -
  3. - Send yourself a text message -
  4. - {% endif %} -
- {% endcall %} - {% elif not jobs %} - {% call banner_wrapper(subhead='Next step', type="tip") %} - {% if current_user.has_permissions(['send_texts', 'send_emails', 'send_letters']) %} - Send yourself a text message - {% endif %} - {% endcall %} - {% else %} - {% call(item) list_table( - jobs, - caption="Recent text messages", - empty_message='You haven’t sent any text messages yet', - field_headings=['Job', 'Created', right_aligned_field_heading('completion')] - ) %} - {% call field() %} - {{ item.original_file_name }} - {% endcall %} - {% call field() %} - {{ item.created_at|format_datetime }} - {% endcall %} - {% call field(align='right') %} - {{ (item.notifications_sent / item.notification_count * 100)|round|int }}% - {% endcall %} - {% endcall %} - {% if more_jobs_to_show %} - {% if current_user.has_permissions(['send_texts', 'send_emails', 'send_letters']) %} - - {% endif %} - {% endif %} - {% endif %} - -{% endblock %} diff --git a/tests/app/main/views/test_accept_invite.py b/tests/app/main/views/test_accept_invite.py index e38f150ec..2cf6f3198 100644 --- a/tests/app/main/views/test_accept_invite.py +++ b/tests/app/main/views/test_accept_invite.py @@ -275,6 +275,7 @@ def test_new_invited_user_verifies_and_added_to_service(app_, mock_accept_invite, mock_get_service, mock_get_service_templates, + mock_get_service_statistics, mock_get_jobs): with app_.test_request_context(): diff --git a/tests/app/main/views/test_dashboard.py b/tests/app/main/views/test_dashboard.py index 0c88c68d1..9594f0072 100644 --- a/tests/app/main/views/test_dashboard.py +++ b/tests/app/main/views/test_dashboard.py @@ -35,6 +35,7 @@ def _test_dashboard_menu(mocker, app_, usr, service, permissions): mocker.patch('app.user_api_client.get_user', return_value=usr) mocker.patch('app.user_api_client.get_user_by_email', return_value=usr) mocker.patch('app.notifications_api_client.get_service', return_value={'data': service}) + mocker.patch('app.statistics_api_client.get_statistics_for_service', return_value={'data': [{}]}) client.login(usr) return client.get(url_for('main.service_dashboard', service_id=service['id'])) diff --git a/tests/conftest.py b/tests/conftest.py index b5f01d91f..ffa173d77 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -157,7 +157,7 @@ def mock_delete_service(mocker, mock_get_service): @pytest.fixture(scope='function') def mock_get_service_statistics(mocker): def _create(service_id): - return {'data': []} + return {'data': [{}]} return mocker.patch( 'app.statistics_api_client.get_statistics_for_service', side_effect=_create) From 643d0477053aa3ae426dadac81723d7cbba2a2a9 Mon Sep 17 00:00:00 2001 From: Rebecca Law Date: Thu, 17 Mar 2016 14:40:08 +0000 Subject: [PATCH 3/3] Fix division by zero --- app/__init__.py | 4 +++- app/main/views/dashboard.py | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 871a5589e..fe5ea530e 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -2,7 +2,7 @@ import os import re import dateutil -from flask import (Flask, session, Markup, escape, render_template, make_response) +from flask import (Flask, session, Markup, escape, render_template, make_response, current_app) from flask._compat import string_types from flask_login import LoginManager from flask_wtf import CsrfProtect @@ -206,4 +206,6 @@ def register_errorhandlers(application): @application.errorhandler(Exception) def handle_bad_request(error): + if current_app.config.get('DEBUG', None): + raise error return _error_response(500) diff --git a/app/main/views/dashboard.py b/app/main/views/dashboard.py index c1f773842..6fc17c49c 100644 --- a/app/main/views/dashboard.py +++ b/app/main/views/dashboard.py @@ -49,8 +49,10 @@ def expand_statistics(statistics, danger_zone=25): today = statistics[0] today.update({ - 'emails_failure_rate': int(today['emails_error'] / today['emails_requested'] * 100), - 'sms_failure_rate': int(today['sms_error'] / today['sms_requested'] * 100) + 'emails_failure_rate': + int(today['emails_error'] / today['emails_requested'] * 100) if today['emails_requested'] else 0, + 'sms_failure_rate': + int(today['sms_error'] / today['sms_requested'] * 100) if today['sms_requested'] else 0 }) today.update({