mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-02-05 19:03:30 -05:00
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:
@@ -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,
|
||||
|
||||
66
app/assets/javascripts/sampleChartDashboard.js
Normal file
66
app/assets/javascripts/sampleChartDashboard.js
Normal 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);
|
||||
@@ -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)]
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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') }}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user