Merge branch 'main' of https://github.com/GSA/notifications-admin into 1544-data-viz-total-message-allowance

# Conflicts:
#	.ds.baseline
#	app/assets/javascripts/chartDashboard.js
#	app/main/views/dashboard.py
This commit is contained in:
Jonathan Bobel
2024-06-07 16:51:32 -04:00
8 changed files with 193 additions and 2 deletions

View File

@@ -18,6 +18,7 @@ from flask import (
)
from flask.globals import request_ctx
from flask_login import LoginManager, current_user
from flask_socketio import SocketIO
from flask_talisman import Talisman
from flask_wtf import CSRFProtect
from flask_wtf.csrf import CSRFError
@@ -118,6 +119,7 @@ from notifications_utils.recipients import format_phone_number_human_readable
login_manager = LoginManager()
csrf = CSRFProtect()
talisman = Talisman()
socketio = SocketIO()
# The current service attached to the request stack.
@@ -175,6 +177,7 @@ def create_app(application):
init_govuk_frontend(application)
init_jinja(application)
socketio.init_app(application)
for client in (
csrf,

View File

@@ -0,0 +1,66 @@
(function (window) {
function initializeChartAndSocket() {
var ctx = document.getElementById('myChart');
if (!ctx) {
return;
}
var myBarChart = new Chart(ctx.getContext('2d'), {
type: 'bar',
data: {
labels: [],
datasets: [
{
label: 'Delivered',
data: [],
backgroundColor: '#0076d6',
stack: 'Stack 0'
},
]
},
options: {
scales: {
y: {
beginAtZero: true
}
}
}
});
var socket = io();
var serviceId = ctx.getAttribute('data-service-id');
socket.on('connect', function() {
socket.emit('fetch_daily_stats', serviceId);
});
socket.on('daily_stats_update', function(data) {
var labels = [];
var deliveredData = [];
for (var date in data) {
labels.push(date);
deliveredData.push(data[date].sms.delivered);
}
myBarChart.data.labels = labels;
myBarChart.data.datasets[0].data = deliveredData;
myBarChart.update();
});
socket.on('error', function(data) {
console.log('Error:', data);
});
var sevenDaysButton = document.getElementById('sevenDaysButton');
if (sevenDaysButton) {
sevenDaysButton.addEventListener('click', function() {
socket.emit('fetch_daily_stats', serviceId);
});
}
}
document.addEventListener('DOMContentLoaded', initializeChartAndSocket);
})(window);

View File

@@ -6,6 +6,7 @@ from itertools import groupby
from flask import Response, abort, jsonify, render_template, request, session, url_for
from flask_login import current_user
from flask_socketio import emit
from werkzeug.utils import redirect
from app import (
@@ -14,6 +15,7 @@ from app import (
job_api_client,
notification_api_client,
service_api_client,
socketio,
template_statistics_client,
)
from app.formatters import format_date_numeric, format_datetime_numeric, get_time_left
@@ -32,6 +34,18 @@ from app.utils.user import user_has_permissions
from notifications_utils.recipients import format_phone_number_human_readable
@socketio.on("fetch_daily_stats")
def handle_fetch_daily_stats(service_id):
if service_id:
date_range = get_stats_date_range()
daily_stats = service_api_client.get_service_notification_statistics_by_day(
service_id, start_date=date_range["start_date"], days=date_range["days"]
)
emit("daily_stats_update", daily_stats)
else:
emit("error", {"error": "No service_id provided"})
@main.route("/services/<uuid:service_id>/dashboard")
@user_has_permissions("view_activity", "send_messages")
def old_service_dashboard(service_id):
@@ -97,6 +111,7 @@ def service_dashboard(service_id):
service_data_retention_days=service_data_retention_days,
sms_sent=sms_sent[0],
sms_allowance_remaining=sms_allowance_remaining[0],
service_id=service_id,
)
@@ -447,6 +462,24 @@ def get_months_for_financial_year(year, time_format="%B"):
return [month.strftime(time_format) for month in (get_months_for_year(1, 13, year))]
def get_current_month_for_financial_year(year):
current_month = datetime.now().month
return current_month
def get_stats_date_range():
current_financial_year = get_current_financial_year()
current_month = get_current_month_for_financial_year(current_financial_year)
start_date = datetime.now().strftime("%Y-%m-%d")
days = 7
return {
"current_financial_year": current_financial_year,
"current_month": current_month,
"start_date": start_date,
"days": days,
}
def get_months_for_year(start, end, year):
return [datetime(year, month, 1) for month in range(start, end)]

View File

@@ -43,6 +43,16 @@ class ServiceAPIClient(NotifyAdminAPIClient):
params={"limit_days": limit_days},
)["data"]
def get_service_notification_statistics_by_day(
self, service_id, start_date=None, days=None
):
if start_date is None:
start_date = datetime.now().strftime("%Y-%m-%d")
return self.get(
"/service/{0}/statistics/{1}/{2}".format(service_id, start_date, days),
)["data"]
def get_services(self, params_dict=None):
"""
Retrieve a list of services.

View File

@@ -22,6 +22,8 @@
Messages sent
</h2>
<!-- <button id="sevenDaysButton">7 Days</button>
<canvas id="myChart" data-service-id="{{ service_id }}"></canvas> -->
{{ ajax_block(partials, updates_url, 'inbox') }}
{{ ajax_block(partials, updates_url, 'totals') }}

View File

@@ -131,7 +131,7 @@ const javascripts = () => {
paths.src + 'javascripts/date.js',
paths.src + 'javascripts/loginAlert.js',
paths.src + 'javascripts/main.js',
paths.src + 'javascripts/chartDashboard.js',
paths.src + 'javascripts/sampleChartDashboard.js',
])
.pipe(plugins.prettyerror())
.pipe(plugins.babel({

View File

@@ -69,6 +69,7 @@ requests = "^2.32.3"
six = "^1.16.0"
urllib3 = "^2.2.1"
webencodings = "^0.5.1"
flask-socketio = "^5.3.6"
[tool.poetry.group.dev.dependencies]

View File

@@ -3,9 +3,11 @@ import json
from datetime import datetime
import pytest
from flask import url_for
from flask import Flask, url_for
from flask_socketio import SocketIOTestClient
from freezegun import freeze_time
from app import create_app, socketio
from app.main.views.dashboard import (
aggregate_notifications_stats,
aggregate_status_types,
@@ -23,6 +25,7 @@ from tests import (
from tests.conftest import (
ORGANISATION_ID,
SERVICE_ONE_ID,
SERVICE_TWO_ID,
create_active_caseworking_user,
create_active_user_view_permissions,
normalize_spaces,
@@ -1869,3 +1872,76 @@ def test_service_dashboard_shows_batched_jobs(
assert job_table_body is not None
assert len(rows) == 1
@pytest.fixture()
def app_with_socketio():
app = Flask("app")
create_app(app)
return app, socketio
@pytest.mark.parametrize(
("service_id", "date_range", "expected_call_args"),
[
(
SERVICE_ONE_ID,
{"start_date": "2024-01-01", "days": 7},
{"service_id": SERVICE_ONE_ID, "start_date": "2024-01-01", "days": 7}
),
(
SERVICE_TWO_ID,
{"start_date": "2023-06-01", "days": 7},
{"service_id": SERVICE_TWO_ID, "start_date": "2023-06-01", "days": 7}
),
]
)
def test_fetch_daily_stats(
app_with_socketio, mocker,
service_id,
date_range,
expected_call_args
):
app, socketio = app_with_socketio
mocker.patch(
"app.main.views.dashboard.get_stats_date_range",
return_value=date_range
)
mock_service_api = mocker.patch(
"app.service_api_client.get_service_notification_statistics_by_day",
return_value={
date_range["start_date"]: {
"email": {"delivered": 0, "failure": 0, "requested": 0},
"sms": {"delivered": 0, "failure": 1, "requested": 1}
},
}
)
client = SocketIOTestClient(app, socketio)
try:
connected = client.is_connected()
assert connected, "Client should be connected"
client.emit('fetch_daily_stats', service_id)
received = client.get_received()
assert received, "Should receive a response message"
assert received[0]['name'] == 'daily_stats_update'
assert received[0]['args'][0] == {
date_range["start_date"]: {
"email": {"delivered": 0, "failure": 0, "requested": 0},
"sms": {"delivered": 0, "failure": 1, "requested": 1}
},
}
mock_service_api.assert_called_once_with(
service_id,
start_date=expected_call_args["start_date"],
days=expected_call_args["days"]
)
finally:
client.disconnect()
disconnected = not client.is_connected()
assert disconnected, "Client should be disconnected"