mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-07-16 03:10:16 -04:00
Merge branch 'main' of https://github.com/GSA/notifications-admin into 1674-match-recent-batches-to-wireframe
# Conflicts: # tests/app/main/views/test_dashboard.py # tests/javascripts/activityChart.test.js
This commit is contained in:
@@ -18,7 +18,6 @@ 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
|
||||
@@ -119,8 +118,6 @@ 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.
|
||||
current_service = LocalProxy(partial(getattr, request_ctx, "service"))
|
||||
@@ -177,7 +174,6 @@ def create_app(application):
|
||||
|
||||
init_govuk_frontend(application)
|
||||
init_jinja(application)
|
||||
socketio.init_app(application, cors_allowed_origins=['http://localhost:6012'])
|
||||
|
||||
for client in (
|
||||
csrf,
|
||||
|
||||
@@ -185,41 +185,41 @@
|
||||
return;
|
||||
}
|
||||
|
||||
var socket = io("/services");
|
||||
var eventType = type === 'service' ? 'fetch_daily_stats' : 'fetch_daily_stats_by_user';
|
||||
var socketConnect = type === 'service' ? 'daily_stats_update' : 'daily_stats_by_user_update';
|
||||
var daily_stats = activityChartContainer.getAttribute('data-daily-stats');
|
||||
var daily_stats_by_user = activityChartContainer.getAttribute('data-daily_stats_by_user');
|
||||
|
||||
socket.on('connect', function () {
|
||||
socket.emit(eventType);
|
||||
});
|
||||
try {
|
||||
// Choose the correct JSON string based on the type ('service' or 'user'),
|
||||
// replace single quotes with double quotes to ensure valid JSON format,
|
||||
// then parse the JSON string into a JavaScript object.
|
||||
var statsJson = type === 'service' ? daily_stats : daily_stats_by_user;
|
||||
statsJson = statsJson.replace(/'/g, '"');
|
||||
data = JSON.parse(statsJson);
|
||||
} catch (error) {
|
||||
console.error('Error parsing JSON data:', error);
|
||||
return;
|
||||
}
|
||||
var labels = [];
|
||||
var deliveredData = [];
|
||||
var failedData = [];
|
||||
|
||||
socket.on('connect_error', function(error) {
|
||||
console.error('WebSocket connection error:', error);
|
||||
});
|
||||
|
||||
socket.on(socketConnect, function(data) {
|
||||
|
||||
var labels = [];
|
||||
var deliveredData = [];
|
||||
var failedData = [];
|
||||
|
||||
for (var dateString in data) {
|
||||
// Parse the date string (assuming format YYYY-MM-DD)
|
||||
for (var dateString in data) {
|
||||
if (data.hasOwnProperty(dateString)) {
|
||||
const dateParts = dateString.split('-');
|
||||
const formattedDate = `${dateParts[1]}/${dateParts[2]}/${dateParts[0].slice(2)}`; // Format to MM/DD/YY
|
||||
const formattedDate = `${dateParts[1]}/${dateParts[2]}/${dateParts[0].slice(2)}`;
|
||||
|
||||
labels.push(formattedDate);
|
||||
deliveredData.push(data[dateString].sms.delivered);
|
||||
failedData.push(data[dateString].sms.failure);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
createChart('#weeklyChart', labels, deliveredData, failedData);
|
||||
createTable('weeklyTable', 'activityChart', labels, deliveredData, failedData);
|
||||
});
|
||||
|
||||
socket.on('error', function(data) {
|
||||
console.log('Error:', data);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating chart or table:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDropdownChange = function(event) {
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
(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: {
|
||||
animation: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var socket = io();
|
||||
|
||||
socket.on('connect', function() {
|
||||
socket.emit('fetch_daily_stats_by_user');
|
||||
});
|
||||
|
||||
socket.on('daily_stats_by_user_update', function(data) {
|
||||
// console.log('Data received:', data);
|
||||
var labels = [];
|
||||
var deliveredData = [];
|
||||
var failedData = [];
|
||||
|
||||
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_by_user');
|
||||
// console.log('clicked');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initializeChartAndSocket);
|
||||
|
||||
})(window);
|
||||
@@ -14,14 +14,14 @@
|
||||
document.getElementById('message').innerText = `${sms_sent.toLocaleString()} sent / ${sms_remaining_messages.toLocaleString()} remaining`;
|
||||
|
||||
// Calculate minimum width for "Messages Sent" as 1% of the total chart width
|
||||
var minSentPercentage = 0.01; // Minimum width as a percentage of total messages (1% in this case)
|
||||
var minSentPercentage = 0.02; // Minimum width as a percentage of total messages (1% in this case)
|
||||
var minSentValue = totalMessages * minSentPercentage;
|
||||
var displaySent = Math.max(sms_sent, minSentValue);
|
||||
var displayRemaining = totalMessages - displaySent;
|
||||
|
||||
var svg = d3.select("#totalMessageChart");
|
||||
var width = chartContainer.clientWidth;
|
||||
var height = 64;
|
||||
var height = 48;
|
||||
|
||||
// Ensure the width is set correctly
|
||||
if (width === 0) {
|
||||
@@ -62,7 +62,7 @@
|
||||
.attr("x", 0) // Initially set to 0, will be updated during animation
|
||||
.attr("y", 0)
|
||||
.attr("height", height)
|
||||
.attr("fill", '#fa9441')
|
||||
.attr("fill", '#C7CACE')
|
||||
.attr("width", 0) // Start with width 0 for animation
|
||||
.on('mouseover', function(event) {
|
||||
tooltip.style('display', 'block')
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
$delivered: color('blue-50v');
|
||||
$pending: color('green-cool-40v');
|
||||
$failed: color('orange-30v');
|
||||
$failed: color('gray-cool-20');
|
||||
|
||||
.chart-container {
|
||||
display: flex;
|
||||
@@ -11,6 +11,10 @@ $failed: color('orange-30v');
|
||||
}
|
||||
}
|
||||
|
||||
#totalMessageChartContainer {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.bar {
|
||||
border-radius: units(0.5);
|
||||
&.delivered, &.usage {
|
||||
|
||||
@@ -5,7 +5,6 @@ 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 (
|
||||
@@ -13,7 +12,6 @@ from app import (
|
||||
current_service,
|
||||
job_api_client,
|
||||
service_api_client,
|
||||
socketio,
|
||||
template_statistics_client,
|
||||
)
|
||||
from app.formatters import format_date_numeric, format_datetime_numeric, get_time_left
|
||||
@@ -32,38 +30,6 @@ from app.utils.user import user_has_permissions
|
||||
from notifications_utils.recipients import format_phone_number_human_readable
|
||||
|
||||
|
||||
@socketio.on("fetch_daily_stats", namespace="/services")
|
||||
def handle_fetch_daily_stats():
|
||||
service_id = session.get("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"})
|
||||
|
||||
|
||||
@socketio.on("fetch_daily_stats_by_user", namespace="/services")
|
||||
def handle_fetch_daily_stats_by_user():
|
||||
service_id = session.get("service_id")
|
||||
user_id = session.get("user_id")
|
||||
if service_id and user_id:
|
||||
date_range = get_stats_date_range()
|
||||
daily_stats_by_user = (
|
||||
service_api_client.get_user_service_notification_statistics_by_day(
|
||||
service_id,
|
||||
user_id,
|
||||
start_date=date_range["start_date"],
|
||||
days=date_range["days"],
|
||||
)
|
||||
)
|
||||
emit("daily_stats_by_user_update", daily_stats_by_user)
|
||||
else:
|
||||
emit("error", {"error": "No service_id or user_id provided"})
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/dashboard")
|
||||
@user_has_permissions("view_activity", "send_messages")
|
||||
def old_service_dashboard(service_id):
|
||||
@@ -87,12 +53,17 @@ def service_dashboard(service_id):
|
||||
free_sms_allowance = billing_api_client.get_free_sms_fragment_limit_for_year(
|
||||
current_service.id,
|
||||
)
|
||||
date_range = get_stats_date_range()
|
||||
usage_data = get_annual_usage_breakdown(yearly_usage, free_sms_allowance)
|
||||
sms_sent = usage_data["sms_sent"]
|
||||
sms_allowance_remaining = usage_data["sms_allowance_remaining"]
|
||||
|
||||
job_response = job_api_client.get_jobs(service_id)["data"]
|
||||
service_data_retention_days = 7
|
||||
daily_stats = get_daily_stats(service_id, date_range)
|
||||
daily_stats_by_user = get_daily_stats_by_user(
|
||||
service_id, current_user.id, date_range
|
||||
)
|
||||
|
||||
jobs = [
|
||||
{
|
||||
@@ -123,6 +94,23 @@ def service_dashboard(service_id):
|
||||
service_data_retention_days=service_data_retention_days,
|
||||
sms_sent=sms_sent,
|
||||
sms_allowance_remaining=sms_allowance_remaining,
|
||||
daily_stats=daily_stats,
|
||||
daily_stats_by_user=daily_stats_by_user,
|
||||
)
|
||||
|
||||
|
||||
def get_daily_stats(service_id, date_range):
|
||||
return service_api_client.get_service_notification_statistics_by_day(
|
||||
service_id, start_date=date_range["start_date"], days=date_range["days"]
|
||||
)
|
||||
|
||||
|
||||
def get_daily_stats_by_user(service_id, user_id, date_range):
|
||||
return service_api_client.get_user_service_notification_statistics_by_day(
|
||||
service_id,
|
||||
user_id,
|
||||
start_date=date_range["start_date"],
|
||||
days=date_range["days"],
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -56,6 +56,9 @@ class NotifyAdminAPIClient(BaseAPIClient):
|
||||
):
|
||||
abort(403)
|
||||
|
||||
def is_calling_signin_url(self, arg):
|
||||
return arg.startswith("('/user")
|
||||
|
||||
def check_inactive_user(self, *args):
|
||||
still_signing_in = False
|
||||
|
||||
@@ -64,14 +67,7 @@ class NotifyAdminAPIClient(BaseAPIClient):
|
||||
# and we only want to check the first arg
|
||||
for arg in args:
|
||||
arg = str(arg)
|
||||
if (
|
||||
"get-login-gov-user" in arg
|
||||
or "user/email" in arg
|
||||
or "/activate" in arg
|
||||
or "/email-code" in arg
|
||||
or "/verify/code" in arg
|
||||
or "/user" in arg
|
||||
):
|
||||
if self.is_calling_signin_url(arg):
|
||||
still_signing_in = True
|
||||
|
||||
# This seems to be a weird edge case that happens intermittently with invites
|
||||
|
||||
@@ -28,15 +28,11 @@
|
||||
<div id="message"></div>
|
||||
</div>
|
||||
<div id="totalMessageTable" class="margin-0"></div>
|
||||
<p class="align-with-heading-copy margin-bottom-4">
|
||||
What counts as 1 text message part?<br />
|
||||
See <a class="usa-link" href="{{ url_for('.pricing') }}">Tracking usage</a>.
|
||||
</p>
|
||||
|
||||
<h2 class="line-height-sans-2 margin-bottom-0 margin-top-4">
|
||||
Activity snapshot
|
||||
</h2>
|
||||
<div id="activityChartContainer">
|
||||
<div id="activityChartContainer" data-daily-stats="{{ daily_stats }}" data-daily_stats_by_user="{{ daily_stats_by_user }}">
|
||||
<form class="usa-form">
|
||||
<label class="usa-label" for="options">Account</label>
|
||||
<select class="usa-select margin-bottom-2" name="options" id="options">
|
||||
|
||||
Reference in New Issue
Block a user