mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-08-24 08:16:51 -04:00
Merge pull request #291 from alphagov/new-dash
Put some statistics on the dashboard
This commit is contained in:
@@ -2,7 +2,7 @@ import os
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
import dateutil
|
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._compat import string_types
|
||||||
from flask_login import LoginManager
|
from flask_login import LoginManager
|
||||||
from flask_wtf import CsrfProtect
|
from flask_wtf import CsrfProtect
|
||||||
@@ -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.notification_api_client import NotificationApiClient
|
||||||
from app.notify_client.status_api_client import StatusApiClient
|
from app.notify_client.status_api_client import StatusApiClient
|
||||||
from app.notify_client.invite_api_client import InviteApiClient
|
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.its_dangerous_session import ItsdangerousSessionInterface
|
||||||
from app.asset_fingerprinter import AssetFingerprinter
|
from app.asset_fingerprinter import AssetFingerprinter
|
||||||
from utils.recipients import validate_phone_number, InvalidPhoneError
|
from utils.recipients import validate_phone_number, InvalidPhoneError
|
||||||
@@ -36,6 +37,7 @@ job_api_client = JobApiClient()
|
|||||||
notification_api_client = NotificationApiClient()
|
notification_api_client = NotificationApiClient()
|
||||||
status_api_client = StatusApiClient()
|
status_api_client = StatusApiClient()
|
||||||
invite_api_client = InviteApiClient()
|
invite_api_client = InviteApiClient()
|
||||||
|
statistics_api_client = StatisticsApiClient()
|
||||||
asset_fingerprinter = AssetFingerprinter()
|
asset_fingerprinter = AssetFingerprinter()
|
||||||
|
|
||||||
|
|
||||||
@@ -55,6 +57,7 @@ def create_app(config_name, config_overrides=None):
|
|||||||
notification_api_client.init_app(application)
|
notification_api_client.init_app(application)
|
||||||
status_api_client.init_app(application)
|
status_api_client.init_app(application)
|
||||||
invite_api_client.init_app(application)
|
invite_api_client.init_app(application)
|
||||||
|
statistics_api_client.init_app(application)
|
||||||
|
|
||||||
login_manager.init_app(application)
|
login_manager.init_app(application)
|
||||||
login_manager.login_view = 'main.sign_in'
|
login_manager.login_view = 'main.sign_in'
|
||||||
@@ -203,4 +206,6 @@ def register_errorhandlers(application):
|
|||||||
|
|
||||||
@application.errorhandler(Exception)
|
@application.errorhandler(Exception)
|
||||||
def handle_bad_request(error):
|
def handle_bad_request(error):
|
||||||
|
if current_app.config.get('DEBUG', None):
|
||||||
|
raise error
|
||||||
return _error_response(500)
|
return _error_response(500)
|
||||||
|
|||||||
BIN
app/assets/images/tick-black.png
Normal file
BIN
app/assets/images/tick-black.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.0 KiB |
@@ -2,6 +2,10 @@
|
|||||||
@include grid-column(3/4);
|
@include grid-column(3/4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.column-one-sixth {
|
||||||
|
@include grid-column(1/6);
|
||||||
|
}
|
||||||
|
|
||||||
.column-one-eighth {
|
.column-one-eighth {
|
||||||
@include grid-column(1/8);
|
@include grid-column(1/8);
|
||||||
}
|
}
|
||||||
@@ -16,7 +20,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bottom-gutter-2-3 {
|
.bottom-gutter-2-3 {
|
||||||
margin-bottom: $gutter * 2/3;
|
margin-bottom: $gutter-two-thirds;
|
||||||
}
|
}
|
||||||
|
|
||||||
.align-with-heading {
|
.align-with-heading {
|
||||||
|
|||||||
@@ -26,9 +26,8 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.banner-with-tick,
|
%banner-with-tick,
|
||||||
.banner-default-with-tick {
|
.banner-with-tick {
|
||||||
@extend %banner;
|
|
||||||
padding: $gutter-half ($gutter + $gutter-half);
|
padding: $gutter-half ($gutter + $gutter-half);
|
||||||
background-image: file-url('tick-white.png');
|
background-image: file-url('tick-white.png');
|
||||||
background-size: 19px;
|
background-size: 19px;
|
||||||
@@ -37,6 +36,11 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.banner-default-with-tick {
|
||||||
|
@extend %banner;
|
||||||
|
@extend %banner-with-tick;
|
||||||
|
}
|
||||||
|
|
||||||
.banner-dangerous {
|
.banner-dangerous {
|
||||||
|
|
||||||
@extend %banner;
|
@extend %banner;
|
||||||
@@ -54,13 +58,15 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
%banner-tip,
|
||||||
.banner-tip {
|
.banner-tip {
|
||||||
|
|
||||||
@extend %banner;
|
@extend %banner;
|
||||||
background: $yellow;
|
@include bold-19;
|
||||||
|
background-color: $yellow;
|
||||||
color: $text-colour;
|
color: $text-colour;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-weight: bold;
|
margin-top: 0;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
&:link,
|
&: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-info,
|
||||||
.banner-important {
|
.banner-important {
|
||||||
@extend %banner;
|
@extend %banner;
|
||||||
@@ -91,3 +103,30 @@
|
|||||||
.banner-info {
|
.banner-info {
|
||||||
background-image: file-url('icon-information-2x.png');
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
%big-number,
|
||||||
.big-number {
|
.big-number {
|
||||||
|
|
||||||
@include bold-48($tabular-numbers: true);
|
@include bold-48($tabular-numbers: true);
|
||||||
@@ -5,6 +6,62 @@
|
|||||||
&-label {
|
&-label {
|
||||||
@include core-19;
|
@include core-19;
|
||||||
display: block;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from flask_login import login_required
|
|||||||
from app.main import main
|
from app.main import main
|
||||||
from app.main.dao.services_dao import get_service_by_id
|
from app.main.dao.services_dao import get_service_by_id
|
||||||
from app.main.dao import templates_dao
|
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/<service_id>/dashboard")
|
@main.route("/services/<service_id>/dashboard")
|
||||||
@@ -27,11 +27,37 @@ def service_dashboard(service_id):
|
|||||||
message = 'You have successfully accepted your invitation and been added to {}'.format(service_name)
|
message = 'You have successfully accepted your invitation and been added to {}'.format(service_name)
|
||||||
flash(message, 'default_with_tick')
|
flash(message, 'default_with_tick')
|
||||||
|
|
||||||
|
statistics = statistics_api_client.get_statistics_for_service(service_id)['data']
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'views/service_dashboard.html',
|
'views/dashboard/dashboard.html',
|
||||||
jobs=jobs[:5],
|
jobs=jobs[:5],
|
||||||
more_jobs_to_show=(len(jobs) > 5),
|
more_jobs_to_show=(len(jobs) > 5),
|
||||||
free_text_messages_remaining='250,000',
|
free_text_messages_remaining='250,000',
|
||||||
spent_this_month='0.00',
|
spent_this_month='0.00',
|
||||||
template_count=len(templates),
|
service=service['data'],
|
||||||
|
statistics=expand_statistics(statistics),
|
||||||
|
templates=templates,
|
||||||
service_id=str(service_id))
|
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) 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({
|
||||||
|
'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
|
||||||
|
|||||||
18
app/notify_client/statistics_api_client.py
Normal file
18
app/notify_client/statistics_api_client.py
Normal file
@@ -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),
|
||||||
|
)
|
||||||
@@ -1,19 +1,9 @@
|
|||||||
{% macro banner(body, type=None, with_tick=False, delete_button=None, subhead=None) %}
|
{% macro banner(body, type=None, with_tick=False, delete_button=None, subhead=None) %}
|
||||||
<div class='banner{% if type %}-{{ type }}{% endif %}{% if with_tick %}-with-tick{% endif %}'>
|
<div class='banner{% if type %}-{{ type }}{% endif %}{% if with_tick %}-with-tick{% endif %}'>
|
||||||
{% if subhead %}
|
{% if subhead -%}
|
||||||
<div class="grid-row">
|
{{ subhead }} 
|
||||||
<div class="column-one-third">
|
{%- endif -%}
|
||||||
{{ subhead }}
|
|
||||||
</div>
|
|
||||||
<div class="column-two-thirds">
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{{ body }}
|
{{ body }}
|
||||||
|
|
||||||
{% if subhead %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if delete_button %}
|
{% if delete_button %}
|
||||||
<form method='post'>
|
<form method='post'>
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
|
|||||||
@@ -4,3 +4,14 @@
|
|||||||
<span class="big-number-label">{{ label }}</span>
|
<span class="big-number-label">{{ label }}</span>
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
|
{% macro big_number_with_status(number, label, status_number, status_label, percentage_bad=0) %}
|
||||||
|
<div class="big-number-with-status">
|
||||||
|
{{ big_number(number, label) }}
|
||||||
|
<div class="big-number-status">
|
||||||
|
<div class="big-number-status-error-percentage" style="opacity: {{ percentage_bad / 100 }}"></div>
|
||||||
|
{{ big_number(status_number, status_label) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|||||||
@@ -37,9 +37,9 @@
|
|||||||
{% if current_user.has_permissions(['send_texts', 'send_emails', 'send_letters'], or_=True) %}
|
{% if current_user.has_permissions(['send_texts', 'send_emails', 'send_letters'], or_=True) %}
|
||||||
{{ banner(
|
{{ banner(
|
||||||
"""
|
"""
|
||||||
Send yourself a test message
|
Send yourself a test
|
||||||
""",
|
""",
|
||||||
subhead='Next step',
|
subhead='Next step:',
|
||||||
type="tip"
|
type="tip"
|
||||||
)}}
|
)}}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
20
app/templates/views/dashboard/dashboard.html
Normal file
20
app/templates/views/dashboard/dashboard.html
Normal file
@@ -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 %}
|
||||||
19
app/templates/views/dashboard/get-started.html
Normal file
19
app/templates/views/dashboard/get-started.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{% from "components/banner.html" import banner_wrapper %}
|
||||||
|
|
||||||
|
<h2 class="heading-medium">Get started</h2>
|
||||||
|
<ol class="grid-row">
|
||||||
|
{% if current_user.has_permissions(['manage_templates']) %}
|
||||||
|
<li class="column-half">
|
||||||
|
{% call banner_wrapper(type="tip", subhead='1.' if not templates else None, with_tick=templates|length) %}
|
||||||
|
<a href='{{ url_for(".add_service_template", service_id=service_id, template_type="sms") }}'>Add a template</a>
|
||||||
|
{% endcall %}
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if current_user.has_permissions(['send_texts', 'send_emails', 'send_letters']) %}
|
||||||
|
<li class="column-half">
|
||||||
|
{% call banner_wrapper(type="tip", subhead='2.') %}
|
||||||
|
<a href='{{ url_for(".choose_template", service_id=service_id, template_type="sms") }}'>Send yourself a message</a>
|
||||||
|
{% endcall %}
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ol>
|
||||||
28
app/templates/views/dashboard/jobs.html
Normal file
28
app/templates/views/dashboard/jobs.html
Normal file
@@ -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() %}
|
||||||
|
<a href="{{ url_for('.view_job', service_id=service_id, job_id=item.id) }}">{{ item.original_file_name }}</a>
|
||||||
|
{% 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']) %}
|
||||||
|
<p class="table-show-more-link">
|
||||||
|
<a href="{{ url_for('.view_jobs', service_id=service_id) }}">See all sent text messages</a>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
25
app/templates/views/dashboard/today.html
Normal file
25
app/templates/views/dashboard/today.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{% from "components/big-number.html" import big_number_with_status %}
|
||||||
|
|
||||||
|
<h2 class="heading-medium">
|
||||||
|
Sent today
|
||||||
|
</h2>
|
||||||
|
<div class="grid-row">
|
||||||
|
<div class="column-half">
|
||||||
|
{{ 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)
|
||||||
|
) }}
|
||||||
|
</div>
|
||||||
|
<div class="column-half">
|
||||||
|
{{ 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)
|
||||||
|
) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
24
app/templates/views/dashboard/trial-mode-banner.html
Normal file
24
app/templates/views/dashboard/trial-mode-banner.html
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{% from "components/banner.html" import banner_wrapper %}
|
||||||
|
{% from "components/big-number.html" import big_number %}
|
||||||
|
|
||||||
|
{% call banner_wrapper(type="mode") %}
|
||||||
|
|
||||||
|
<div class="grid-row">
|
||||||
|
<div class="column-one-half">
|
||||||
|
<h2 class="heading-medium">Trial mode</h2>
|
||||||
|
<p>
|
||||||
|
We’ll only deliver messages to you and members of your team
|
||||||
|
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="column-one-sixth">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="column-one-third">
|
||||||
|
{{ big_number(
|
||||||
|
service.limit - statistics.get('emails_requested', 0) - statistics.get('sms_requested', 0),
|
||||||
|
'messages left today'
|
||||||
|
) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endcall %}
|
||||||
@@ -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 %}
|
|
||||||
|
|
||||||
<ul class="grid-row job-totals">
|
|
||||||
<li class="column-half">
|
|
||||||
{{ big_number(
|
|
||||||
free_text_messages_remaining,
|
|
||||||
'free text messages remaining'
|
|
||||||
)}}
|
|
||||||
</li>
|
|
||||||
<li class="column-half">
|
|
||||||
{{ big_number(
|
|
||||||
'£' + spent_this_month,
|
|
||||||
'spent this month'
|
|
||||||
)}}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
{% if not template_count and not jobs %}
|
|
||||||
{% call banner_wrapper(subhead='Get started', type="tip") %}
|
|
||||||
<ol>
|
|
||||||
{% if current_user.has_permissions(['manage_templates']) %}
|
|
||||||
<li>
|
|
||||||
<a href='{{ url_for(".add_service_template", service_id=service_id, template_type="sms") }}'>Add a template</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% if current_user.has_permissions(['send_texts', 'send_emails', 'send_letters']) %}
|
|
||||||
<li>
|
|
||||||
<a href='{{ url_for(".choose_template", service_id=service_id, template_type="sms") }}'>Send yourself a text message</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
</ol>
|
|
||||||
{% endcall %}
|
|
||||||
{% elif not jobs %}
|
|
||||||
{% call banner_wrapper(subhead='Next step', type="tip") %}
|
|
||||||
{% if current_user.has_permissions(['send_texts', 'send_emails', 'send_letters']) %}
|
|
||||||
<a href='{{ url_for(".choose_template", service_id=service_id, template_type="sms") }}'>Send yourself a text message</a>
|
|
||||||
{% 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() %}
|
|
||||||
<a href="{{ url_for('.view_job', service_id=service_id, job_id=item.id) }}">{{ item.original_file_name }}</a>
|
|
||||||
{% 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']) %}
|
|
||||||
<p class="table-show-more-link">
|
|
||||||
<a href="{{ url_for('.view_jobs', service_id=service_id) }}">See all sent text messages</a>
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
13
tests/app/main/notify_client/test_statistics_client.py
Normal file
13
tests/app/main/notify_client/test_statistics_client.py
Normal file
@@ -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)
|
||||||
@@ -275,6 +275,7 @@ def test_new_invited_user_verifies_and_added_to_service(app_,
|
|||||||
mock_accept_invite,
|
mock_accept_invite,
|
||||||
mock_get_service,
|
mock_get_service,
|
||||||
mock_get_service_templates,
|
mock_get_service_templates,
|
||||||
|
mock_get_service_statistics,
|
||||||
mock_get_jobs):
|
mock_get_jobs):
|
||||||
|
|
||||||
with app_.test_request_context():
|
with app_.test_request_context():
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ def test_should_show_recent_jobs_on_dashboard(app_,
|
|||||||
api_user_active,
|
api_user_active,
|
||||||
mock_get_service,
|
mock_get_service,
|
||||||
mock_get_service_templates,
|
mock_get_service_templates,
|
||||||
|
mock_get_service_statistics,
|
||||||
mock_get_user,
|
mock_get_user,
|
||||||
mock_get_user_by_email,
|
mock_get_user_by_email,
|
||||||
mock_login,
|
mock_login,
|
||||||
@@ -34,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', return_value=usr)
|
||||||
mocker.patch('app.user_api_client.get_user_by_email', 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.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)
|
client.login(usr)
|
||||||
return client.get(url_for('main.service_dashboard', service_id=service['id']))
|
return client.get(url_for('main.service_dashboard', service_id=service['id']))
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ def test_sign_out_user(app_,
|
|||||||
mock_get_user,
|
mock_get_user,
|
||||||
mock_get_user_by_email,
|
mock_get_user_by_email,
|
||||||
mock_get_service_templates,
|
mock_get_service_templates,
|
||||||
|
mock_get_service_statistics,
|
||||||
mock_login,
|
mock_login,
|
||||||
mock_get_jobs):
|
mock_get_jobs):
|
||||||
with app_.test_request_context():
|
with app_.test_request_context():
|
||||||
|
|||||||
@@ -154,6 +154,15 @@ def mock_delete_service(mocker, mock_get_service):
|
|||||||
'app.notifications_api_client.delete_service', side_effect=_delete)
|
'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')
|
@pytest.fixture(scope='function')
|
||||||
def mock_get_service_template(mocker):
|
def mock_get_service_template(mocker):
|
||||||
def _create(service_id, template_id):
|
def _create(service_id, template_id):
|
||||||
|
|||||||
Reference in New Issue
Block a user