mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-09-02 17:48:50 -04: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.globals import request_ctx
|
||||||
from flask_login import LoginManager, current_user
|
from flask_login import LoginManager, current_user
|
||||||
|
from flask_socketio import SocketIO
|
||||||
from flask_talisman import Talisman
|
from flask_talisman import Talisman
|
||||||
from flask_wtf import CSRFProtect
|
from flask_wtf import CSRFProtect
|
||||||
from flask_wtf.csrf import CSRFError
|
from flask_wtf.csrf import CSRFError
|
||||||
@@ -118,6 +119,7 @@ from notifications_utils.recipients import format_phone_number_human_readable
|
|||||||
login_manager = LoginManager()
|
login_manager = LoginManager()
|
||||||
csrf = CSRFProtect()
|
csrf = CSRFProtect()
|
||||||
talisman = Talisman()
|
talisman = Talisman()
|
||||||
|
socketio = SocketIO()
|
||||||
|
|
||||||
|
|
||||||
# The current service attached to the request stack.
|
# The current service attached to the request stack.
|
||||||
@@ -175,6 +177,7 @@ def create_app(application):
|
|||||||
|
|
||||||
init_govuk_frontend(application)
|
init_govuk_frontend(application)
|
||||||
init_jinja(application)
|
init_jinja(application)
|
||||||
|
socketio.init_app(application)
|
||||||
|
|
||||||
for client in (
|
for client in (
|
||||||
csrf,
|
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 import Response, abort, jsonify, render_template, request, session, url_for
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
from flask_socketio import emit
|
||||||
from werkzeug.utils import redirect
|
from werkzeug.utils import redirect
|
||||||
|
|
||||||
from app import (
|
from app import (
|
||||||
@@ -14,6 +15,7 @@ from app import (
|
|||||||
job_api_client,
|
job_api_client,
|
||||||
notification_api_client,
|
notification_api_client,
|
||||||
service_api_client,
|
service_api_client,
|
||||||
|
socketio,
|
||||||
template_statistics_client,
|
template_statistics_client,
|
||||||
)
|
)
|
||||||
from app.formatters import format_date_numeric, format_datetime_numeric, get_time_left
|
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
|
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")
|
@main.route("/services/<uuid:service_id>/dashboard")
|
||||||
@user_has_permissions("view_activity", "send_messages")
|
@user_has_permissions("view_activity", "send_messages")
|
||||||
def old_service_dashboard(service_id):
|
def old_service_dashboard(service_id):
|
||||||
@@ -97,6 +111,7 @@ def service_dashboard(service_id):
|
|||||||
service_data_retention_days=service_data_retention_days,
|
service_data_retention_days=service_data_retention_days,
|
||||||
sms_sent=sms_sent[0],
|
sms_sent=sms_sent[0],
|
||||||
sms_allowance_remaining=sms_allowance_remaining[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))]
|
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):
|
def get_months_for_year(start, end, year):
|
||||||
return [datetime(year, month, 1) for month in range(start, end)]
|
return [datetime(year, month, 1) for month in range(start, end)]
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,16 @@ class ServiceAPIClient(NotifyAdminAPIClient):
|
|||||||
params={"limit_days": limit_days},
|
params={"limit_days": limit_days},
|
||||||
)["data"]
|
)["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):
|
def get_services(self, params_dict=None):
|
||||||
"""
|
"""
|
||||||
Retrieve a list of services.
|
Retrieve a list of services.
|
||||||
|
|||||||
@@ -22,6 +22,8 @@
|
|||||||
Messages sent
|
Messages sent
|
||||||
</h2>
|
</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, 'inbox') }}
|
||||||
|
|
||||||
{{ ajax_block(partials, updates_url, 'totals') }}
|
{{ ajax_block(partials, updates_url, 'totals') }}
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ const javascripts = () => {
|
|||||||
paths.src + 'javascripts/date.js',
|
paths.src + 'javascripts/date.js',
|
||||||
paths.src + 'javascripts/loginAlert.js',
|
paths.src + 'javascripts/loginAlert.js',
|
||||||
paths.src + 'javascripts/main.js',
|
paths.src + 'javascripts/main.js',
|
||||||
paths.src + 'javascripts/chartDashboard.js',
|
paths.src + 'javascripts/sampleChartDashboard.js',
|
||||||
])
|
])
|
||||||
.pipe(plugins.prettyerror())
|
.pipe(plugins.prettyerror())
|
||||||
.pipe(plugins.babel({
|
.pipe(plugins.babel({
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ requests = "^2.32.3"
|
|||||||
six = "^1.16.0"
|
six = "^1.16.0"
|
||||||
urllib3 = "^2.2.1"
|
urllib3 = "^2.2.1"
|
||||||
webencodings = "^0.5.1"
|
webencodings = "^0.5.1"
|
||||||
|
flask-socketio = "^5.3.6"
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import json
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from flask import url_for
|
from flask import Flask, url_for
|
||||||
|
from flask_socketio import SocketIOTestClient
|
||||||
from freezegun import freeze_time
|
from freezegun import freeze_time
|
||||||
|
|
||||||
|
from app import create_app, socketio
|
||||||
from app.main.views.dashboard import (
|
from app.main.views.dashboard import (
|
||||||
aggregate_notifications_stats,
|
aggregate_notifications_stats,
|
||||||
aggregate_status_types,
|
aggregate_status_types,
|
||||||
@@ -23,6 +25,7 @@ from tests import (
|
|||||||
from tests.conftest import (
|
from tests.conftest import (
|
||||||
ORGANISATION_ID,
|
ORGANISATION_ID,
|
||||||
SERVICE_ONE_ID,
|
SERVICE_ONE_ID,
|
||||||
|
SERVICE_TWO_ID,
|
||||||
create_active_caseworking_user,
|
create_active_caseworking_user,
|
||||||
create_active_user_view_permissions,
|
create_active_user_view_permissions,
|
||||||
normalize_spaces,
|
normalize_spaces,
|
||||||
@@ -1869,3 +1872,76 @@ def test_service_dashboard_shows_batched_jobs(
|
|||||||
assert job_table_body is not None
|
assert job_table_body is not None
|
||||||
|
|
||||||
assert len(rows) == 1
|
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