Merge branch 'main' into notify-admin-1218

This commit is contained in:
Kenneth Kehl
2024-08-13 11:27:14 -07:00
committed by GitHub
16 changed files with 1549 additions and 1216 deletions
+4 -4
View File
@@ -133,7 +133,7 @@
"filename": ".github/workflows/checks.yml",
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
"is_verified": false,
"line_number": 68,
"line_number": 65,
"is_secret": false
},
{
@@ -141,7 +141,7 @@
"filename": ".github/workflows/checks.yml",
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
"is_verified": false,
"line_number": 103,
"line_number": 99,
"is_secret": false
}
],
@@ -535,7 +535,7 @@
"filename": "tests/app/main/views/test_accept_invite.py",
"hashed_secret": "07f0a6c13923fc3b5f0c57ffa2d29b715eb80d71",
"is_verified": false,
"line_number": 626,
"line_number": 643,
"is_secret": false
}
],
@@ -692,5 +692,5 @@
}
]
},
"generated_at": "2024-08-07T14:36:40Z"
"generated_at": "2024-08-08T19:45:34Z"
}
-2
View File
@@ -44,8 +44,6 @@ jobs:
run: poetry run isort --check-only ./app ./tests
- name: Check dead code
run: make dead-code
- name: Run js lint
run: npm run lint
- name: Run js tests
run: npm test
- name: Run py tests with coverage
+1 -4
View File
@@ -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,7 @@ 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,
+24 -24
View File
@@ -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);
+22 -34
View File
@@ -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"],
)
+1 -1
View File
@@ -32,7 +32,7 @@
<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">
+51 -178
View File
@@ -1,15 +1,10 @@
// GULPFILE
// - - - - - - - - - - - - - - -
// This file processes all of the assets in the "src" folder
// and outputs the finished files in the "dist" folder.
// 1. LIBRARIES
// - - - - - - - - - - - - - - -
const { src, pipe, dest, series, parallel, watch } = require('gulp');
const rollupPluginCommonjs = require('rollup-plugin-commonjs');
const rollupPluginNodeResolve = require('rollup-plugin-node-resolve');
const streamqueue = require('streamqueue');
const stylish = require('jshint-stylish');
const { src, dest, series } = require('gulp');
const rollup = require('@rollup/stream');
const rollupPluginCommonjs = require('@rollup/plugin-commonjs');
const rollupPluginNodeResolve = require('@rollup/plugin-node-resolve');
const source = require('vinyl-source-stream');
const buffer = require('vinyl-buffer');
const gulpMerge = require('gulp-merge');
const uswds = require("@uswds/compile");
const plugins = {};
@@ -19,11 +14,8 @@ plugins.cleanCSS = require('gulp-clean-css');
plugins.concat = require('gulp-concat');
plugins.jshint = require('gulp-jshint');
plugins.prettyerror = require('gulp-prettyerror');
plugins.rollup = require('gulp-better-rollup')
plugins.uglify = require('gulp-uglify');
// 2. CONFIGURATION
// - - - - - - - - - - - - - - -
const paths = {
src: 'app/assets/',
dist: 'app/static/',
@@ -31,63 +23,25 @@ const paths = {
toolkit: 'node_modules/govuk_frontend_toolkit/',
govuk_frontend: 'node_modules/govuk-frontend/'
};
// Rewrite /static prefix for URLs in CSS files
let staticPathMatcher = new RegExp('^\/static\/');
if (process.env.NOTIFY_ENVIRONMENT == 'development') { // pass through if on development
staticPathMatcher = url => url;
}
// 3. TASKS
// - - - - - - - - - - - - - - -
// Move GOV.UK template resources
const copy = {
error_pages: () => {
return src(paths.src + 'error_pages/**/*')
.pipe(dest(paths.dist + 'error_pages/'))
},
fonts: () => {
return src(paths.src + 'fonts/**/*')
.pipe(dest(paths.dist + 'fonts/'));
},
gtm: () => {
return src(paths.src + 'js/gtm_head.js')
.pipe(dest(paths.dist + 'js/'));
}
};
const javascripts = () => {
// JS from third-party sources
// We assume none of it will need to pass through Babel
const vendored = src(paths.src + 'javascripts/modules/all.mjs')
// Use Rollup to combine all JS in JS module format into a Immediately Invoked Function
// Expression (IIFE) to:
// - deliver it in one bundle
// - allow it to run in browsers without support for JS Modules
.pipe(plugins.rollup(
{
plugins: [
// determine module entry points from either 'module' or 'main' fields in package.json
rollupPluginNodeResolve({
mainFields: ['module', 'main']
}),
// gulp rollup runs on nodeJS so reads modules in commonJS format
// this adds node_modules to the require path so it can find the GOVUK Frontend modules
rollupPluginCommonjs({
include: 'node_modules/**'
})
]
},
{
format: 'iife',
name: 'GOVUK'
}
))
// return a stream which pipes these files before the JS modules bundle
const vendored = rollup({
input: paths.src + 'javascripts/modules/all.mjs',
plugins: [
rollupPluginNodeResolve({
mainFields: ['module', 'main']
}),
rollupPluginCommonjs({
include: 'node_modules/**'
})
],
output: {
format: 'iife',
name: 'GOVUK'
}
})
.pipe(source('all.mjs'))
.pipe(buffer())
.pipe(plugins.addSrc.prepend([
paths.npm + 'hogan.js/dist/hogan-3.0.2.js',
paths.npm + 'jquery/dist/jquery.min.js',
@@ -95,11 +49,9 @@ const javascripts = () => {
paths.npm + 'timeago/jquery.timeago.js',
paths.npm + 'textarea-caret/index.js',
paths.npm + 'cbor-js/cbor.js',
paths.npm + 'socket.io-client/dist/socket.io.min.js',
paths.npm + 'd3/dist/d3.min.js'
]));
// JS local to this application
const local = src([
paths.toolkit + 'javascripts/govuk/modules.js',
paths.toolkit + 'javascripts/govuk/show-hide-content.js',
@@ -124,128 +76,49 @@ const javascripts = () => {
paths.src + 'javascripts/timeoutPopup.js',
paths.src + 'javascripts/date.js',
paths.src + 'javascripts/loginAlert.js',
paths.src + 'javascripts/main.js',
paths.src + 'javascripts/totalMessagesChart.js',
paths.src + 'javascripts/activityChart.js',
paths.src + 'javascripts/main.js',
])
.pipe(plugins.prettyerror())
.pipe(plugins.babel({
presets: ['@babel/preset-env']
}));
// return single stream of all vinyl objects piped from the end of the vendored stream, then
// those from the end of the local stream
return streamqueue({ objectMode: true }, vendored, local)
return gulpMerge(vendored, local)
.pipe(plugins.uglify())
.pipe(plugins.concat('all.js'))
.pipe(dest(paths.dist + 'javascripts/'))
.pipe(dest(paths.dist + 'javascripts/'));
};
// Copy images
const images = () => {
return src([
paths.toolkit + 'images/**/*',
paths.govuk_frontend + 'assets/images/**/*',
paths.src + 'images/**/*',
paths.src + 'img/**/*',
], {encoding: false})
.pipe(dest(paths.dist + 'images/'))
// Task to copy `gtm_head.js`
const copyGtmHead = () => {
return src(paths.src + 'js/gtm_head.js')
.pipe(dest(paths.dist + 'js/'));
};
const watchFiles = {
javascripts: (cb) => {
watch([paths.src + 'javascripts/**/*'], javascripts);
cb();
},
images: (cb) => {
watch([paths.src + 'images/**/*'], images);
cb();
},
uswds: (cb) => {
watch([paths.src + 'sass/**/*'], uswds.watch);
cb();
},
self: (cb) => {
watch(['gulpfile.js'], defaultTask);
cb();
}
// Task to copy images
const copyImages = () => {
return src(paths.src + 'images/**/*')
.pipe(dest(paths.dist + 'images/'));
};
const lint = {
'js': (cb) => {
return src(
paths.src + 'javascripts/**/*.js'
)
.pipe(plugins.jshint())
.pipe(plugins.jshint.reporter(stylish))
.pipe(plugins.jshint.reporter('fail'))
}
};
// Default: compile everything
const defaultTask = parallel(
parallel(
copy.fonts,
images
),
series(
copy.error_pages,
series(
javascripts
),
uswds.compile,
uswds.copyAssets,
copy.gtm
)
);
// Watch for changes and re-run tasks
const watchForChanges = parallel(
watchFiles.javascripts,
watchFiles.images,
watchFiles.self
);
exports.default = defaultTask;
exports.lint = series(lint.js);
// Optional: recompile on changes
exports.watch = series(defaultTask, watchForChanges);
// 3. Compile USWDS
/**
* USWDS version
* Set the major version of USWDS you're using
* (Current options are the numbers 2 or 3)
*/
// Configure USWDS paths
uswds.settings.version = 3;
uswds.paths.dist.css = paths.dist + 'css';
uswds.paths.dist.js = paths.dist + 'js';
uswds.paths.dist.img = paths.dist + 'img';
uswds.paths.dist.fonts = paths.dist + 'fonts';
uswds.paths.dist.theme = paths.src + 'sass/uswds';
/**
* Path settings
* Set as many as you need
*/
uswds.paths.dist.css = './app/static/css';
uswds.paths.dist.js = './app/static/js';
uswds.paths.dist.img = './app/static/img';
uswds.paths.dist.fonts = './app/static/fonts';
uswds.paths.dist.theme = './app/assets/sass/uswds';
// Task to compile USWDS styles
const styles = async () => {
await uswds.compile();
};
/**
* Exports
* Add as many as you need
*/
exports.init = uswds.init;
exports.compile = uswds.compile;
exports.copyAll = uswds.copyAll;
exports.watch = uswds.watch;
exports.copyAssets = uswds.copyAssets;
// Task to copy USWDS assets
const copyAssets = async () => {
await uswds.copyAssets();
};
exports.default = series(styles, javascripts, copyGtmHead, copyImages, copyAssets);
+1206 -513
View File
File diff suppressed because it is too large Load Diff
+15 -17
View File
@@ -25,49 +25,47 @@
"graceful-fs": "^4.2.11"
},
"dependencies": {
"@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/stream": "^3.0.1",
"@uswds/uswds": "^3.8.1",
"cbor-js": "0.1.0",
"d3": "^7.9.0",
"govuk_frontend_toolkit": "^9.0.1",
"govuk-frontend": "2.13.0",
"gulp-merge": "^0.1.1",
"hogan": "1.0.2",
"jquery": "3.7.1",
"morphdom": "^2.7.3",
"morphdom": "^2.7.4",
"python": "^0.0.4",
"query-command-supported": "1.0.0",
"sass-embedded": "^1.77.5",
"socket.io-client": "^4.2.0",
"sass-embedded": "^1.77.8",
"textarea-caret": "3.1.0",
"timeago": "1.6.7"
"timeago": "1.6.7",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0"
},
"devDependencies": {
"@babel/core": "^7.24.7",
"@babel/preset-env": "^7.24.7",
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.3",
"@uswds/compile": "^1.1.0",
"babel-jest": "^29.7.0",
"better-npm-audit": "^3.7.3",
"gulp": "^5.0.0",
"gulp-add-src": "^1.0.0",
"gulp-babel": "8.0.0",
"gulp-better-rollup": "4.0.1",
"gulp-clean-css": "4.3.0",
"gulp-concat": "2.6.1",
"gulp-concat": "^2.6.1",
"gulp-include": "2.4.1",
"gulp-jshint": "2.1.0",
"gulp-prettyerror": "2.0.0",
"gulp-uglify": "3.0.2",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0",
"jest": "29.7.0",
"jest-each": "^29.2.1",
"jest-environment-jsdom": "^29.2.2",
"jshint": "2.13.6",
"jshint-stylish": "2.2.1",
"rollup": "1.32.1",
"rollup": "^4.20.0",
"rollup-plugin-commonjs": "10.1.0",
"rollup-plugin-node-resolve": "5.2.0",
"streamqueue": "1.1.2"
},
"optionalDependencies": {
"sass-embedded-linux-x64": "^1.77.8"
"rollup-plugin-node-resolve": "5.2.0"
}
}
Generated
+10 -116
View File
@@ -85,17 +85,6 @@ charset-normalizer = ["charset-normalizer"]
html5lib = ["html5lib"]
lxml = ["lxml"]
[[package]]
name = "bidict"
version = "0.23.1"
description = "The bidirectional mapping library for Python."
optional = false
python-versions = ">=3.8"
files = [
{file = "bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5"},
{file = "bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71"},
]
[[package]]
name = "black"
version = "24.4.2"
@@ -893,24 +882,6 @@ redis = ">=2.7.6"
dev = ["coverage", "pre-commit", "pytest", "pytest-mock"]
tests = ["coverage", "pytest", "pytest-mock"]
[[package]]
name = "flask-socketio"
version = "5.3.6"
description = "Socket.IO integration for Flask applications"
optional = false
python-versions = ">=3.6"
files = [
{file = "Flask-SocketIO-5.3.6.tar.gz", hash = "sha256:bb8f9f9123ef47632f5ce57a33514b0c0023ec3696b2384457f0fcaa5b70501c"},
{file = "Flask_SocketIO-5.3.6-py3-none-any.whl", hash = "sha256:9e62d2131842878ae6bfdd7067dfc3be397c1f2b117ab1dc74e6fe74aad7a579"},
]
[package.dependencies]
Flask = ">=0.9"
python-socketio = ">=5.0.2"
[package.extras]
docs = ["sphinx"]
[[package]]
name = "flask-talisman"
version = "1.1.0"
@@ -1059,17 +1030,6 @@ setproctitle = ["setproctitle"]
testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"]
tornado = ["tornado (>=0.2)"]
[[package]]
name = "h11"
version = "0.14.0"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
optional = false
python-versions = ">=3.7"
files = [
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
]
[[package]]
name = "html5lib"
version = "1.1"
@@ -1325,9 +1285,13 @@ files = [
{file = "lxml-5.2.2-cp36-cp36m-win_amd64.whl", hash = "sha256:edcfa83e03370032a489430215c1e7783128808fd3e2e0a3225deee278585196"},
{file = "lxml-5.2.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:28bf95177400066596cdbcfc933312493799382879da504633d16cf60bba735b"},
{file = "lxml-5.2.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a745cc98d504d5bd2c19b10c79c61c7c3df9222629f1b6210c0368177589fb8"},
{file = "lxml-5.2.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b590b39ef90c6b22ec0be925b211298e810b4856909c8ca60d27ffbca6c12e6"},
{file = "lxml-5.2.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b336b0416828022bfd5a2e3083e7f5ba54b96242159f83c7e3eebaec752f1716"},
{file = "lxml-5.2.2-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:c2faf60c583af0d135e853c86ac2735ce178f0e338a3c7f9ae8f622fd2eb788c"},
{file = "lxml-5.2.2-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:4bc6cb140a7a0ad1f7bc37e018d0ed690b7b6520ade518285dc3171f7a117905"},
{file = "lxml-5.2.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7ff762670cada8e05b32bf1e4dc50b140790909caa8303cfddc4d702b71ea184"},
{file = "lxml-5.2.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:57f0a0bbc9868e10ebe874e9f129d2917750adf008fe7b9c1598c0fbbfdde6a6"},
{file = "lxml-5.2.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:a6d2092797b388342c1bc932077ad232f914351932353e2e8706851c870bca1f"},
{file = "lxml-5.2.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:60499fe961b21264e17a471ec296dcbf4365fbea611bf9e303ab69db7159ce61"},
{file = "lxml-5.2.2-cp37-cp37m-win32.whl", hash = "sha256:d9b342c76003c6b9336a80efcc766748a333573abf9350f4094ee46b006ec18f"},
{file = "lxml-5.2.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b16db2770517b8799c79aa80f4053cd6f8b716f21f8aca962725a9565ce3ee40"},
@@ -1662,7 +1626,6 @@ files = [
{file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"},
{file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"},
{file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"},
{file = "msgpack-1.0.8-py3-none-any.whl", hash = "sha256:24f727df1e20b9876fa6e95f840a2a2651e34c0ad147676356f4bf5fbb0206ca"},
{file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"},
]
@@ -2406,25 +2369,6 @@ files = [
[package.extras]
cli = ["click (>=5.0)"]
[[package]]
name = "python-engineio"
version = "4.9.1"
description = "Engine.IO server and client for Python"
optional = false
python-versions = ">=3.6"
files = [
{file = "python_engineio-4.9.1-py3-none-any.whl", hash = "sha256:f995e702b21f6b9ebde4e2000cd2ad0112ba0e5116ec8d22fe3515e76ba9dddd"},
{file = "python_engineio-4.9.1.tar.gz", hash = "sha256:7631cf5563086076611e494c643b3fa93dd3a854634b5488be0bba0ef9b99709"},
]
[package.dependencies]
simple-websocket = ">=0.10.0"
[package.extras]
asyncio-client = ["aiohttp (>=3.4)"]
client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"]
docs = ["sphinx"]
[[package]]
name = "python-json-logger"
version = "2.0.7"
@@ -2453,26 +2397,6 @@ text-unidecode = ">=1.3"
[package.extras]
unidecode = ["Unidecode (>=1.1.1)"]
[[package]]
name = "python-socketio"
version = "5.11.3"
description = "Socket.IO server and client for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "python_socketio-5.11.3-py3-none-any.whl", hash = "sha256:2a923a831ff70664b7c502df093c423eb6aa93c1ce68b8319e840227a26d8b69"},
{file = "python_socketio-5.11.3.tar.gz", hash = "sha256:194af8cdbb7b0768c2e807ba76c7abc288eb5bb85559b7cddee51a6bc7a65737"},
]
[package.dependencies]
bidict = ">=0.21.0"
python-engineio = ">=4.8.0"
[package.extras]
asyncio-client = ["aiohttp (>=3.4)"]
client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"]
docs = ["sphinx"]
[[package]]
name = "pytz"
version = "2024.1"
@@ -2509,6 +2433,7 @@ files = [
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
@@ -2563,17 +2488,17 @@ toml = ["tomli (>=2.0.1)"]
[[package]]
name = "redis"
version = "5.0.7"
version = "5.0.8"
description = "Python client for Redis database and key-value store"
optional = false
python-versions = ">=3.7"
files = [
{file = "redis-5.0.7-py3-none-any.whl", hash = "sha256:0e479e24da960c690be5d9b96d21f7b918a98c0cf49af3b6fafaa0753f93a0db"},
{file = "redis-5.0.7.tar.gz", hash = "sha256:8f611490b93c8109b50adc317b31bfd84fff31def3475b92e7e80bf39f48175b"},
{file = "redis-5.0.8-py3-none-any.whl", hash = "sha256:56134ee08ea909106090934adc36f65c9bcbbaecea5b21ba704ba6fb561f8eb4"},
{file = "redis-5.0.8.tar.gz", hash = "sha256:0c5b10d387568dfe0698c6fad6615750c24170e548ca2deac10c649d463e9870"},
]
[package.extras]
hiredis = ["hiredis (>=1.0.0)"]
hiredis = ["hiredis (>1.0.0)"]
ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"]
[[package]]
@@ -2822,23 +2747,6 @@ numpy = ">=1.14,<3"
docs = ["matplotlib", "numpydoc (==1.1.*)", "sphinx", "sphinx-book-theme", "sphinx-remove-toctrees"]
test = ["pytest", "pytest-cov"]
[[package]]
name = "simple-websocket"
version = "1.0.0"
description = "Simple WebSocket server and client for Python"
optional = false
python-versions = ">=3.6"
files = [
{file = "simple-websocket-1.0.0.tar.gz", hash = "sha256:17d2c72f4a2bd85174a97e3e4c88b01c40c3f81b7b648b0cc3ce1305968928c8"},
{file = "simple_websocket-1.0.0-py3-none-any.whl", hash = "sha256:1d5bf585e415eaa2083e2bcf02a3ecf91f9712e7b3e6b9fa0b461ad04e0837bc"},
]
[package.dependencies]
wsproto = "*"
[package.extras]
docs = ["sphinx"]
[[package]]
name = "six"
version = "1.16.0"
@@ -3016,20 +2924,6 @@ MarkupSafe = ">=2.1.1"
[package.extras]
watchdog = ["watchdog (>=2.3)"]
[[package]]
name = "wsproto"
version = "1.2.0"
description = "WebSockets state-machine based protocol implementation"
optional = false
python-versions = ">=3.7.0"
files = [
{file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"},
{file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"},
]
[package.dependencies]
h11 = ">=0.9.0,<1"
[[package]]
name = "wtforms"
version = "3.1.2"
@@ -3088,4 +2982,4 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = "^3.12.2"
content-hash = "9d6309a76755b2639d787f99944c1ead0bd93ab9f2f13208f88c51cecb5e0081"
content-hash = "2c1efc5e8d38c709aec2bc3aa39f96c82a58df13bcbf1912b27c4c26b7b4fc9d"
+1 -2
View File
@@ -52,7 +52,7 @@ ordered-set = "^4.1.0"
phonenumbers = "^8.13.40"
pycparser = "^2.22"
python-json-logger = "^2.0.7"
redis = "^5.0.7"
redis = "^5.0.8"
regex = "^2024.7.24"
s3transfer = "^0.10.2"
shapely = "^2.0.5"
@@ -68,7 +68,6 @@ requests = "^2.32.3"
six = "^1.16.0"
urllib3 = "^2.2.2"
webencodings = "^0.5.1"
flask-socketio = "^5.3.6"
[tool.poetry.group.dev.dependencies]
+19 -2
View File
@@ -300,9 +300,26 @@ def test_accepting_invite_removes_invite_from_session(
client_request.login(user)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
date_range = {"start_date": "2024-01-01", "days": 7}
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
"app.main.views.dashboard.get_daily_stats",
return_value={
date_range["start_date"]: {
"email": {"delivered": 0, "failure": 0, "requested": 0},
"sms": {"delivered": 0, "failure": 1, "requested": 1},
},
},
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value={
date_range["start_date"]: {
"email": {"delivered": 1, "failure": 0, "requested": 1},
"sms": {"delivered": 1, "failure": 0, "requested": 1},
},
},
)
page = client_request.get(
"main.accept_invite",
+174 -205
View File
@@ -3,11 +3,9 @@ import json
from datetime import datetime
import pytest
from flask import Flask, url_for
from flask_socketio import SocketIO, SocketIOTestClient
from flask import url_for
from freezegun import freeze_time
from app import create_app
from app.main.views.dashboard import (
aggregate_notifications_stats,
aggregate_status_types,
@@ -15,8 +13,6 @@ from app.main.views.dashboard import (
format_monthly_stats_to_list,
get_dashboard_totals,
get_tuples_of_financial_years,
handle_fetch_daily_stats,
handle_fetch_daily_stats_by_user,
)
from tests import (
organization_json,
@@ -27,8 +23,6 @@ from tests import (
from tests.conftest import (
ORGANISATION_ID,
SERVICE_ONE_ID,
SERVICE_TWO_ID,
USER_ONE_ID,
create_active_caseworking_user,
create_active_user_view_permissions,
normalize_spaces,
@@ -163,6 +157,22 @@ stub_template_stats = [
},
]
date_range = {"start_date": "2024-01-01", "days": 7}
mock_daily_stats = {
date_range["start_date"]: {
"email": {"delivered": 0, "failure": 0, "requested": 0},
"sms": {"delivered": 0, "failure": 1, "requested": 1},
},
}
mock_daily_stats_by_user = {
date_range["start_date"]: {
"email": {"delivered": 1, "failure": 0, "requested": 1},
"sms": {"delivered": 1, "failure": 0, "requested": 1},
},
}
@pytest.mark.parametrize(
"user",
@@ -222,8 +232,12 @@ def test_get_started(
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = client_request.get(
"main.service_dashboard",
@@ -252,8 +266,12 @@ def test_get_started_is_hidden_once_templates_exist(
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = client_request.get(
"main.service_dashboard",
@@ -279,8 +297,12 @@ def test_inbound_messages_not_visible_to_service_without_permissions(
service_one["permissions"] = []
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = client_request.get(
"main.service_dashboard",
@@ -305,6 +327,14 @@ def test_inbound_messages_shows_count_of_messages_when_there_are_messages(
mock_get_inbound_sms_summary,
):
service_one["permissions"] = ["inbound_sms"]
mocker.patch(
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = client_request.get(
"main.service_dashboard",
service_id=SERVICE_ONE_ID,
@@ -333,6 +363,14 @@ def test_inbound_messages_shows_count_of_messages_when_there_are_no_messages(
mock_get_inbound_sms_summary_with_no_messages,
):
service_one["permissions"] = ["inbound_sms"]
mocker.patch(
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = client_request.get(
"main.service_dashboard",
service_id=SERVICE_ONE_ID,
@@ -580,8 +618,12 @@ def test_should_show_recent_templates_on_dashboard(
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = client_request.get(
"main.service_dashboard",
@@ -637,8 +679,12 @@ def test_should_not_show_recent_templates_on_dashboard_if_only_one_template_used
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = client_request.get("main.service_dashboard", service_id=SERVICE_ONE_ID)
main = page.select_one("main").text
@@ -792,8 +838,12 @@ def test_should_show_upcoming_jobs_on_dashboard(
mock_get_inbound_sms_summary,
):
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = client_request.get(
"main.service_dashboard",
@@ -834,6 +884,14 @@ def test_should_not_show_upcoming_jobs_on_dashboard_if_count_is_0(
},
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = client_request.get(
"main.service_dashboard",
service_id=SERVICE_ONE_ID,
@@ -857,8 +915,12 @@ def test_should_not_show_upcoming_jobs_on_dashboard_if_service_has_no_jobs(
):
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = client_request.get(
"main.service_dashboard",
@@ -942,6 +1004,14 @@ def test_should_not_show_jobs_on_dashboard_for_users_with_uploads_page(
mock_get_free_sms_fragment_limit,
mock_get_inbound_sms_summary,
):
mocker.patch(
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = client_request.get(
"main.service_dashboard",
service_id=SERVICE_ONE_ID,
@@ -1203,8 +1273,12 @@ def test_menu_send_messages(
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = _test_dashboard_menu(
client_request,
@@ -1240,8 +1314,12 @@ def test_menu_manage_service(
):
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = _test_dashboard_menu(
client_request,
@@ -1277,8 +1355,12 @@ def test_menu_main_settings(
):
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = _test_settings_menu(
client_request,
@@ -1313,8 +1395,12 @@ def test_menu_manage_api_keys(
):
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = _test_dashboard_menu(
client_request,
@@ -1353,8 +1439,12 @@ def test_menu_all_services_for_platform_admin_user(
):
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = _test_dashboard_menu(
client_request, mocker, platform_admin_user, service_one, []
@@ -1392,8 +1482,12 @@ def test_route_for_service_permissions(
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
validate_route_permission(
mocker,
@@ -1537,8 +1631,12 @@ def test_org_breadcrumbs_do_not_show_if_service_has_no_org(
):
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = client_request.get("main.service_dashboard", service_id=SERVICE_ONE_ID)
@@ -1604,8 +1702,12 @@ def test_org_breadcrumbs_show_if_user_is_a_member_of_the_services_org(
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = client_request.get("main.service_dashboard", service_id=SERVICE_ONE_ID)
@@ -1639,10 +1741,13 @@ def test_org_breadcrumbs_do_not_show_if_user_is_a_member_of_the_services_org_but
mocker.patch("app.models.service.Organization")
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = client_request.get("main.service_dashboard", service_id=SERVICE_ONE_ID)
@@ -1679,8 +1784,12 @@ def test_org_breadcrumbs_show_if_user_is_platform_admin(
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
client_request.login(platform_admin_user, service_one_json)
@@ -1715,9 +1824,14 @@ def test_breadcrumb_shows_if_service_is_suspended(
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = client_request.get("main.service_dashboard", service_id=SERVICE_ONE_ID)
assert "Suspended" in page.select_one(".navigation-service-name").text
@@ -1748,8 +1862,12 @@ def test_service_dashboard_shows_usage(
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
service_one["permissions"] = permissions
@@ -1783,10 +1901,13 @@ def test_service_dashboard_shows_free_allowance(
],
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
@@ -1802,12 +1923,15 @@ def test_service_dashboard_shows_batched_jobs(
):
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = client_request.get("main.service_dashboard", service_id=SERVICE_ONE_ID)
job_table_body = page.find("table", class_="job-table")
rows = job_table_body.find_all("tbody")[0].find_all("tr")
@@ -1815,158 +1939,3 @@ 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)
socketio = SocketIO(app)
socketio.on_event("fetch_daily_stats", handle_fetch_daily_stats)
socketio.on_event("fetch_daily_stats_by_user", handle_fetch_daily_stats_by_user)
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},
},
},
)
with app.test_client() as client:
with client.session_transaction() as sess:
sess["service_id"] = service_id
socketio_client = SocketIOTestClient(app, socketio, flask_test_client=client)
connected = socketio_client.is_connected()
assert connected, "Client should be connected"
socketio_client.emit("fetch_daily_stats")
received = socketio_client.get_received()
mock_service_api.assert_called_once_with(
expected_call_args["service_id"],
start_date=expected_call_args["start_date"],
days=expected_call_args["days"],
)
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},
},
}
socketio_client.disconnect()
disconnected = not socketio_client.is_connected()
assert disconnected, "Client should be disconnected"
@pytest.mark.parametrize(
("service_id", "user_id", "date_range", "expected_call_args", "user"),
[
(
SERVICE_ONE_ID,
USER_ONE_ID,
{"start_date": "2024-01-01", "days": 7},
{
"service_id": SERVICE_ONE_ID,
"user_id": USER_ONE_ID,
"start_date": "2024-01-01",
"days": 7,
},
{"id": USER_ONE_ID, "name": "Test User"},
),
],
)
def test_fetch_daily_stats_by_user(
app_with_socketio,
mocker,
service_id,
user_id,
date_range,
expected_call_args,
user,
):
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_user_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},
},
},
)
mocker.patch("app.user_api_client.get_user", return_value=user)
with app.test_client() as client:
with client.session_transaction() as sess:
sess["service_id"] = service_id
sess["user_id"] = user_id
socketio_client = SocketIOTestClient(app, socketio, flask_test_client=client)
connected = socketio_client.is_connected()
assert connected, "Client should be connected"
socketio_client.emit("fetch_daily_stats_by_user")
received = socketio_client.get_received()
mock_service_api.assert_called_once_with(
expected_call_args["service_id"],
expected_call_args["user_id"],
start_date=expected_call_args["start_date"],
days=expected_call_args["days"],
)
assert received, "Should receive a response message"
assert received[0]["name"] == "daily_stats_by_user_update"
assert received[0]["args"][0] == {
date_range["start_date"]: {
"email": {"delivered": 0, "failure": 0, "requested": 0},
"sms": {"delivered": 0, "failure": 1, "requested": 1},
},
}
socketio_client.disconnect()
disconnected = not socketio_client.is_connected()
assert disconnected, "Client should be disconnected"
+19 -2
View File
@@ -128,9 +128,26 @@ def test_sign_out_user(
# Check we are logged in
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
date_range = {"start_date": "2024-01-01", "days": 7}
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
"app.main.views.dashboard.get_daily_stats",
return_value={
date_range["start_date"]: {
"email": {"delivered": 0, "failure": 0, "requested": 0},
"sms": {"delivered": 0, "failure": 1, "requested": 1},
},
},
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value={
date_range["start_date"]: {
"email": {"delivered": 1, "failure": 0, "requested": 1},
"sms": {"delivered": 1, "failure": 0, "requested": 1},
},
},
)
client_request.get(
+2 -42
View File
@@ -21,7 +21,7 @@ Object.defineProperty(HTMLElement.prototype, 'clientWidth', {
beforeAll(done => {
// Set up the DOM with the D3 script included
document.body.innerHTML = `
<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">
@@ -30,7 +30,7 @@ beforeAll(done => {
<option value="individual">User Name</option>
</select>
</form>
<div id="activityChart">
<div id="activityChart" >
<div class="chart-header">
<div class="chart-subtitle">Service Name - Last 7 Days</div>
<div class="chart-legend" aria-label="Legend"></div>
@@ -124,43 +124,3 @@ test('Check HTML content after chart creation', () => {
expect(container.querySelector('svg')).not.toBeNull();
expect(container.querySelectorAll('rect').length).toBeGreaterThan(0);
});
test('Initial fetch data populates chart and table', done => {
const mockData = {
'2024-07-01': { sms: { delivered: 50, failed: 5 } },
'2024-07-02': { sms: { delivered: 60, failed: 2 } },
'2024-07-03': { sms: { delivered: 70, failed: 1 } },
'2024-07-04': { sms: { delivered: 80, failed: 0 } },
'2024-07-05': { sms: { delivered: 90, failed: 3 } },
'2024-07-06': { sms: { delivered: 100, failed: 4 } },
'2024-07-07': { sms: { delivered: 110, failed: 2 } },
};
const socket = {
on: jest.fn((event, callback) => {
if (event === 'daily_stats_update') {
callback(mockData);
done();
}
}),
emit: jest.fn(),
};
window.io = jest.fn(() => socket);
document.dispatchEvent(new Event('DOMContentLoaded'));
setTimeout(() => {
const table = document.getElementById('weeklyTable');
expect(table).toBeDefined();
const rows = table.getElementsByTagName('tr');
expect(rows.length).toBe(8);
const firstRowCells = rows[1].getElementsByTagName('td');
console.log('First row cells:', firstRowCells);
expect(firstRowCells[0].textContent).toBe('07/01/24');
expect(firstRowCells[1].textContent).toBe('50');
expect(firstRowCells[2].textContent).toBe('5');
}, 100);
});