Merge branch '1544-data-viz-total-message-allowance' into 1484-dashboard-visualizations

# Conflicts:
#	app/assets/sass/uswds/_data-visualization.scss
#	app/assets/sass/uswds/styles.scss
#	app/templates/views/dashboard/dashboard.html
#	app/templates/views/dashboard/template-statistics.html
#	gulpfile.js
#	package-lock.json
This commit is contained in:
Jonathan Bobel
2024-07-17 11:23:53 -04:00
15 changed files with 935 additions and 1127 deletions
@@ -1,6 +1,6 @@
(function (window) { (function (window) {
if (document.getElementById('chartsArea')) { if (document.getElementById('totalMessageChartContainer')) {
const COLORS = { const COLORS = {
delivered: '#0076d6', delivered: '#0076d6',
@@ -15,7 +15,7 @@
// Function to create a stacked bar chart with animation using D3.js // Function to create a stacked bar chart with animation using D3.js
function createChart(containerId, labels, deliveredData, failedData) { function createChart(containerId, labels, deliveredData, failedData) {
const container = d3.select(containerId); const container = d3.select(containerId);
container.selectAll('*').remove(); // Clear any existing content container.selectAll('*').remove(); // Clear any existing contentR
const margin = { top: 60, right: 20, bottom: 40, left: 20 }; // Adjusted top margin for legend const margin = { top: 60, right: 20, bottom: 40, left: 20 }; // Adjusted top margin for legend
const width = container.node().getBoundingClientRect().width - margin.left - margin.right; const width = container.node().getBoundingClientRect().width - margin.left - margin.right;
@@ -225,7 +225,7 @@
function handleDropdownChange(event) { function handleDropdownChange(event) {
const selectedValue = event.target.value; const selectedValue = event.target.value;
const subTitle = document.querySelector(`#chartsArea .chart-subtitle`); const subTitle = document.querySelector(`#activityChartContainer .chart-subtitle`);
const selectElement = document.getElementById('options'); const selectElement = document.getElementById('options');
const selectedText = selectElement.options[selectElement.selectedIndex].text; const selectedText = selectElement.options[selectElement.selectedIndex].text;
@@ -0,0 +1,119 @@
(function (window) {
var chartContainer = document.getElementById('chartContainer');
if (chartContainer) {
var chartTitle = document.getElementById('chartTitle').textContent;
// Access data attributes from the HTML
var sms_sent = parseInt(chartContainer.getAttribute('data-sms-sent'));
var sms_remaining_messages = parseInt(chartContainer.getAttribute('data-sms-allowance-remaining'));
var totalMessages = sms_sent + sms_remaining_messages;
// Update the message below the chart
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 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;
svg.attr("width", width).attr("height", height);
var x = d3.scaleLinear()
.domain([0, totalMessages])
.range([0, width]);
// Create tooltip dynamically
var tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("position", "absolute")
.style("background", "#fff")
.style("border", "1px solid #ccc")
.style("padding", "5px")
.style("box-shadow", "0px 0px 10px rgba(0, 0, 0, 0.1)")
.style("pointer-events", "none")
.style("display", "none");
// Create the initial bars
var sentBar = svg.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("height", height)
.attr("fill", '#0076d6')
.attr("width", 0); // Start with width 0 for animation
var remainingBar = svg.append("rect")
.attr("x", 0) // Initially set to 0, will be updated during animation
.attr("y", 0)
.attr("height", height)
.attr("fill", '#fa9441')
.attr("width", 0); // Start with width 0 for animation
// Animate the bars together as a single cohesive line
svg.transition()
.duration(1000) // Total animation duration
.attr("width", width)
.tween("resize", function() {
var interpolator = d3.interpolate(0, width);
return function(t) {
var newWidth = interpolator(t);
var sentWidth = x(displaySent) / width * newWidth;
var remainingWidth = x(displayRemaining) / width * newWidth;
sentBar.attr("width", sentWidth);
remainingBar.attr("x", sentWidth).attr("width", remainingWidth);
};
});
// Create and populate the accessible table
var tableContainer = document.getElementById('totalMessageTable');
var table = document.createElement('table');
table.className = 'usa-sr-only usa-table';
var caption = document.createElement('caption');
caption.textContent = chartTitle;
table.appendChild(caption);
var thead = document.createElement('thead');
var theadRow = document.createElement('tr');
var thLabel = document.createElement('th');
thLabel.textContent = 'Label';
var thValue = document.createElement('th');
thValue.textContent = 'Value';
theadRow.appendChild(thLabel);
theadRow.appendChild(thValue);
table.appendChild(theadRow);
table.appendChild(thead);
var tbody = document.createElement('tbody');
var tableData = [
{ label: 'Messages Sent', value: sms_sent.toLocaleString() },
{ label: 'Remaining', value: sms_remaining_messages.toLocaleString() }
];
tableData.forEach(function (rowData) {
var row = document.createElement('tr');
var cellLabel = document.createElement('td');
var cellValue = document.createElement('td');
cellLabel.textContent = rowData.label;
cellValue.textContent = rowData.value;
row.appendChild(cellLabel);
row.appendChild(cellValue);
tbody.appendChild(row);
});
table.appendChild(tbody);
tableContainer.appendChild(table);
// Ensure the chart resizes correctly on window resize
window.addEventListener('resize', function () {
width = chartContainer.clientWidth;
x.range([0, width]);
svg.attr("width", width);
sentBar.attr("width", x(displaySent));
remainingBar.attr("x", x(displaySent)).attr("width", x(displayRemaining));
});
}
})(window);
+400
View File
@@ -0,0 +1,400 @@
(function (window) {
if (document.getElementById('activityChartContainer')) {
const COLORS = {
delivered: '#0076d6',
failed: '#fa9441',
text: '#666'
};
const FONT_SIZE = 16;
const FONT_WEIGHT = 'bold';
const MAX_Y = 120;
// Function to create a stacked bar chart with animation using D3.js
function createChart(containerId, labels, deliveredData, failedData) {
const container = d3.select(containerId);
container.selectAll('*').remove(); // Clear any existing content
const margin = { top: 60, right: 20, bottom: 40, left: 20 }; // Adjusted top margin for legend
const width = container.node().getBoundingClientRect().width - margin.left - margin.right;
const height = 400 - margin.top - margin.bottom;
const svg = container.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
// Create legend
const legendContainer = d3.select('.chart-legend');
legendContainer.selectAll('*').remove(); // Clear any existing legend
const legendData = [
{ label: 'Delivered', color: COLORS.delivered },
{ label: 'Failed', color: COLORS.failed }
];
const legendItem = legendContainer.selectAll('.legend-item')
.data(legendData)
.enter()
.append('div')
.attr('class', 'legend-item');
legendItem.append('div')
.attr('class', 'legend-rect')
.style('background-color', d => d.color)
.style('display', 'inline-block')
.style('margin-right', '5px');
legendItem.append('span')
.attr('class', 'legend-label')
.text(d => d.label);
const x = d3.scaleBand()
.domain(labels)
.range([0, width])
.padding(0.1);
// Adjust the y-axis domain to add some space above the tallest bar
const maxY = d3.max(deliveredData.map((d, i) => d + (failedData[i] || 0)));
const y = d3.scaleLinear()
.domain([0, maxY + 2]) // Add 2 units of space at the top
.nice()
.range([height, 0]);
svg.append('g')
.attr('class', 'x axis')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(x));
// Generate the y-axis with whole numbers
const yAxis = d3.axisLeft(y)
.ticks(Math.min(maxY + 2, 10)) // Generate up to 10 ticks based on the data
.tickFormat(d3.format('d')); // Ensure whole numbers on the y-axis
svg.append('g')
.attr('class', 'y axis')
.call(yAxis);
// Data for stacking
const stackData = labels.map((label, i) => ({
label: label,
delivered: deliveredData[i],
failed: failedData[i] || 0 // Ensure there's a value for failed, even if it's 0
}));
// Stack the data
const stack = d3.stack()
.keys(['delivered', 'failed'])
.order(d3.stackOrderNone)
.offset(d3.stackOffsetNone);
const series = stack(stackData);
// Color scale
const color = d3.scaleOrdinal()
.domain(['delivered', 'failed'])
.range([COLORS.delivered, COLORS.failed]);
// Create tooltip
const tooltip = d3.select('body').append('div')
.attr('id', 'tooltip')
.style('display', 'none')
// Create bars with animation
const barGroups = svg.selectAll('.bar-group')
.data(series)
.enter()
.append('g')
.attr('class', 'bar-group')
.attr('fill', d => color(d.key));
barGroups.selectAll('rect')
.data(d => d)
.enter()
.append('rect')
.attr('x', d => x(d.data.label))
.attr('y', height)
.attr('height', 0)
.attr('width', x.bandwidth())
.on('mouseover', function(event, d) {
const key = d3.select(this.parentNode).datum().key;
const capitalizedKey = key.charAt(0).toUpperCase() + key.slice(1);
tooltip.style('display', 'block')
.html(`${d.data.label}<br>${capitalizedKey}: ${d.data[key]}`);
})
.on('mousemove', function(event) {
tooltip.style('left', `${event.pageX + 10}px`)
.style('top', `${event.pageY - 20}px`);
})
.on('mouseout', function() {
tooltip.style('display', 'none');
})
.transition()
.duration(1000)
.attr('y', d => y(d[1]))
.attr('height', d => y(d[0]) - y(d[1]));
}
// Function to create an accessible table
function createTable(tableId, chartType, labels, deliveredData, failedData) {
const table = document.getElementById(tableId);
table.innerHTML = ""; // Clear previous data
const captionText = document.querySelector(`#${chartType} .chart-subtitle`).textContent;
const caption = document.createElement('caption');
caption.textContent = captionText;
const thead = document.createElement('thead');
const tbody = document.createElement('tbody');
// Create table header
const headerRow = document.createElement('tr');
const headers = ['Day', 'Delivered', 'Failed'];
headers.forEach(headerText => {
const th = document.createElement('th');
th.textContent = headerText;
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
// Create table body
labels.forEach((label, index) => {
const row = document.createElement('tr');
const cellDay = document.createElement('td');
cellDay.textContent = label;
row.appendChild(cellDay);
const cellDelivered = document.createElement('td');
cellDelivered.textContent = deliveredData[index];
row.appendChild(cellDelivered);
const cellFailed = document.createElement('td');
cellFailed.textContent = failedData[index];
row.appendChild(cellFailed);
tbody.appendChild(row);
});
table.appendChild(caption);
table.appendChild(thead);
table.append(tbody);
}
function fetchData(type) {
var ctx = document.getElementById('weeklyChart');
if (!ctx) {
return;
}
var socket = io();
var eventType = type === 'service' ? 'fetch_daily_stats' : 'fetch_daily_stats_by_user';
var socketConnect = type === 'service' ? 'daily_stats_update' : 'daily_stats_by_user_update';
socket.on('connect', function () {
const userId = ctx.getAttribute('data-service-id'); // Assuming user ID is the same as service ID
socket.emit(eventType);
});
socket.on(socketConnect, function(data) {
console.log('Received data:', data); // Log the received data
var labels = [];
var deliveredData = [];
var failedData = [2, 1, 0, 2, 0, 1, 0];
for (var dateString in data) {
// Parse the date string (assuming format YYYY-MM-DD)
const dateParts = dateString.split('-');
const formattedDate = `${dateParts[1]}/${dateParts[2]}/${dateParts[0].slice(2)}`; // Format to MM/DD/YY
labels.push(formattedDate);
deliveredData.push(data[dateString].sms.delivered);
// failedData.push(data[dateString].sms.failure == [0, 1, 0, 2, 0]);
}
createChart('#weeklyChart', labels, deliveredData, failedData);
createTable('#weeklyTable', 'activityChart', labels, deliveredData, failedData);
});
socket.on('error', function(data) {
console.log('Error:', data);
});
}
function handleDropdownChange(event) {
const selectedValue = event.target.value;
const subTitle = document.querySelector(`#activityChartContainer .chart-subtitle`);
const selectElement = document.getElementById('options');
const selectedText = selectElement.options[selectElement.selectedIndex].text;
if (selectedValue === "individual") {
subTitle.textContent = selectedText + " - Last 7 Days";
fetchData('individual');
} else if (selectedValue === "service") {
subTitle.textContent = selectedText + " - Last 7 Days";
fetchData('service');
}
// Update ARIA live region
const liveRegion = document.getElementById('aria-live-account');
liveRegion.textContent = `Data updated for ${selectedText} - Last 7 Days`;
}
document.addEventListener('DOMContentLoaded', function() {
// Initialize activityChart chart and table with service data by default
fetchData('service');
// Add event listener to the dropdown
const dropdown = document.getElementById('options');
dropdown.addEventListener('change', handleDropdownChange);
});
// Resize chart on window resize
window.addEventListener('resize', function() {
const selectedValue = document.getElementById('options').value;
handleDropdownChange({ target: { value: selectedValue } });
});
}
// Function to create a bar chart for total messages
function createTotalMessagesChart() {
var chartContainer = document.getElementById('totalMessageChartContainer');
if (!chartContainer) return;
var chartTitle = document.getElementById('chartTitle').textContent;
// Access data attributes from the HTML
var sms_sent = parseInt(chartContainer.getAttribute('data-sms-sent'));
var sms_remaining_messages = parseInt(chartContainer.getAttribute('data-sms-allowance-remaining'));
var totalMessages = sms_sent + sms_remaining_messages;
// Update the message below the chart
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 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;
svg.attr("width", width).attr("height", height);
var x = d3.scaleLinear()
.domain([0, totalMessages])
.range([0, width]);
// Create tooltip dynamically
var tooltip = d3.select("body").append("div")
.attr("id", "tooltip");
// Create the initial bars
var sentBar = svg.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("height", height)
.attr("fill", '#0076d6')
.attr("width", 0) // Start with width 0 for animation
.on('mouseover', function(event) {
tooltip.style('display', 'block')
.html(`Messages Sent: ${sms_sent.toLocaleString()}`);
})
.on('mousemove', function(event) {
tooltip.style('left', `${event.pageX + 10}px`)
.style('top', `${event.pageY - 20}px`);
})
.on('mouseout', function() {
tooltip.style('display', 'none');
});
var remainingBar = svg.append("rect")
.attr("x", 0) // Initially set to 0, will be updated during animation
.attr("y", 0)
.attr("height", height)
.attr("fill", '#fa9441')
.attr("width", 0) // Start with width 0 for animation
.on('mouseover', function(event) {
tooltip.style('display', 'block')
.html(`Remaining: ${sms_remaining_messages.toLocaleString()}`);
})
.on('mousemove', function(event) {
tooltip.style('left', `${event.pageX + 10}px`)
.style('top', `${event.pageY - 20}px`);
})
.on('mouseout', function() {
tooltip.style('display', 'none');
});
// Animate the bars together as a single cohesive line
svg.transition()
.duration(1000) // Total animation duration
.attr("width", width)
.tween("resize", function() {
var interpolator = d3.interpolate(0, width);
return function(t) {
var newWidth = interpolator(t);
var sentWidth = x(displaySent) / width * newWidth;
var remainingWidth = x(displayRemaining) / width * newWidth;
sentBar.attr("width", sentWidth);
remainingBar.attr("x", sentWidth).attr("width", remainingWidth);
};
});
// Create and populate the accessible table
var tableContainer = document.getElementById('totalMessageTable');
var table = document.createElement('table');
table.className = 'usa-sr-only usa-table';
var caption = document.createElement('caption');
caption.textContent = chartTitle;
table.appendChild(caption);
var thead = document.createElement('thead'); // Ensure thead is created
var theadRow = document.createElement('tr');
var thMessagesSent = document.createElement('th');
thMessagesSent.textContent = 'Messages Sent'; // First column header
var thRemaining = document.createElement('th');
thRemaining.textContent = 'Remaining'; // Second column header
theadRow.appendChild(thMessagesSent);
theadRow.appendChild(thRemaining);
thead.appendChild(theadRow); // Append theadRow to the thead
table.appendChild(thead);
var tbody = document.createElement('tbody');
var tbodyRow = document.createElement('tr');
var tdMessagesSent = document.createElement('td');
tdMessagesSent.textContent = sms_sent.toLocaleString(); // Value for Messages Sent
var tdRemaining = document.createElement('td');
tdRemaining.textContent = sms_remaining_messages.toLocaleString(); // Value for Remaining
tbodyRow.appendChild(tdMessagesSent);
tbodyRow.appendChild(tdRemaining);
tbody.appendChild(tbodyRow);
table.appendChild(tbody);
tableContainer.appendChild(table);
table.appendChild(tbody);
tableContainer.appendChild(table);
// Ensure the chart resizes correctly on window resize
window.addEventListener('resize', function () {
width = chartContainer.clientWidth;
x.range([0, width]);
svg.attr("width", width);
sentBar.attr("width", x(displaySent));
remainingBar.attr("x", x(displaySent)).attr("width", x(displayRemaining));
});
}
// Initialize total messages chart if the container exists
document.addEventListener('DOMContentLoaded', function() {
createTotalMessagesChart();
});
})(window);
+13
View File
@@ -82,6 +82,17 @@ def service_dashboard(service_id):
if not current_user.has_permissions("view_activity"): if not current_user.has_permissions("view_activity"):
return redirect(url_for("main.choose_template", service_id=service_id)) return redirect(url_for("main.choose_template", service_id=service_id))
yearly_usage = billing_api_client.get_annual_usage_for_service(
service_id,
get_current_financial_year(),
)
free_sms_allowance = billing_api_client.get_free_sms_fragment_limit_for_year(
current_service.id,
)
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"] job_response = job_api_client.get_jobs(service_id)["data"]
notifications_response = notification_api_client.get_notifications_for_service( notifications_response = notification_api_client.get_notifications_for_service(
service_id service_id
@@ -118,6 +129,8 @@ def service_dashboard(service_id):
partials=get_dashboard_partials(service_id), partials=get_dashboard_partials(service_id),
job_and_notifications=job_and_notifications, job_and_notifications=job_and_notifications,
service_data_retention_days=service_data_retention_days, service_data_retention_days=service_data_retention_days,
sms_sent=sms_sent,
sms_allowance_remaining=sms_allowance_remaining,
) )
+1 -3
View File
@@ -101,7 +101,6 @@ const javascripts = () => {
paths.npm + 'textarea-caret/index.js', paths.npm + 'textarea-caret/index.js',
paths.npm + 'cbor-js/cbor.js', paths.npm + 'cbor-js/cbor.js',
paths.npm + 'socket.io-client/dist/socket.io.min.js', paths.npm + 'socket.io-client/dist/socket.io.min.js',
paths.npm + 'chart.js/dist/chart.umd.js',
paths.npm + 'd3/dist/d3.min.js' paths.npm + 'd3/dist/d3.min.js'
])); ]));
@@ -131,9 +130,8 @@ 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/dataVisualization.js', paths.src + 'javascripts/dataVisualization.js',
paths.src + 'javascripts/dashboardVisualization.js', paths.src + 'javascripts/dashboardViz.js',
paths.src + 'javascripts/main.js', paths.src + 'javascripts/main.js',
paths.src + 'javascripts/sampleChartDashboard.js',
]) ])
.pipe(plugins.prettyerror()) .pipe(plugins.prettyerror())
.pipe(plugins.babel({ .pipe(plugins.babel({
+242 -1072
View File
File diff suppressed because it is too large Load Diff
+5 -3
View File
@@ -42,9 +42,10 @@
"timeago": "1.6.7" "timeago": "1.6.7"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.24.7", "@babel/core": "^7.24.7",
"@babel/preset-env": "7.24.7", "@babel/preset-env": "^7.24.7",
"@uswds/compile": "^1.1.0", "@uswds/compile": "^1.1.0",
"babel-jest": "^29.7.0",
"better-npm-audit": "^3.7.3", "better-npm-audit": "^3.7.3",
"gulp": "^4.0.2", "gulp": "^4.0.2",
"gulp-add-src": "^1.0.0", "gulp-add-src": "^1.0.0",
@@ -56,7 +57,8 @@
"gulp-jshint": "2.1.0", "gulp-jshint": "2.1.0",
"gulp-prettyerror": "2.0.0", "gulp-prettyerror": "2.0.0",
"gulp-uglify": "3.0.2", "gulp-uglify": "3.0.2",
"jest": "29.7.0", "identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0",
"jest-each": "^29.2.1", "jest-each": "^29.2.1",
"jest-environment-jsdom": "^29.2.2", "jest-environment-jsdom": "^29.2.2",
"jshint": "2.13.6", "jshint": "2.13.6",
Generated
+28 -28
View File
@@ -182,17 +182,17 @@ files = [
[[package]] [[package]]
name = "boto3" name = "boto3"
version = "1.34.138" version = "1.34.139"
description = "The AWS SDK for Python" description = "The AWS SDK for Python"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "boto3-1.34.138-py3-none-any.whl", hash = "sha256:81518aa95fad71279411fb5c94da4b4a554a5d53fc876faca62b7b5c8737f1cb"}, {file = "boto3-1.34.139-py3-none-any.whl", hash = "sha256:98b2a12bcb30e679fa9f60fc74145a39db5ec2ca7b7c763f42896e3bd9b3a38d"},
{file = "boto3-1.34.138.tar.gz", hash = "sha256:f79c15e33eb7706f197d98d828b193cf0891966682ad3ec5e900f6f9e7362e35"}, {file = "boto3-1.34.139.tar.gz", hash = "sha256:32b99f0d76ec81fdca287ace2c9744a2eb8b92cb62bf4d26d52a4f516b63a6bf"},
] ]
[package.dependencies] [package.dependencies]
botocore = ">=1.34.138,<1.35.0" botocore = ">=1.34.139,<1.35.0"
jmespath = ">=0.7.1,<2.0.0" jmespath = ">=0.7.1,<2.0.0"
s3transfer = ">=0.10.0,<0.11.0" s3transfer = ">=0.10.0,<0.11.0"
@@ -201,13 +201,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]
[[package]] [[package]]
name = "botocore" name = "botocore"
version = "1.34.138" version = "1.34.139"
description = "Low-level, data-driven core of boto 3." description = "Low-level, data-driven core of boto 3."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "botocore-1.34.138-py3-none-any.whl", hash = "sha256:84e96a954c39a6f09cae4ea95b2ae582b5ae01b5040c92507b60509c9be5377a"}, {file = "botocore-1.34.139-py3-none-any.whl", hash = "sha256:dd1e085d4caa2a4c1b7d83e3bc51416111c8238a35d498e9d3b04f3b63b086ba"},
{file = "botocore-1.34.138.tar.gz", hash = "sha256:f558bbea96c4a4abbaeeedc477dabb00902311ba1ca6327974a6819b9f384920"}, {file = "botocore-1.34.139.tar.gz", hash = "sha256:df023d8cf8999d574214dad4645cb90f9d2ccd1494f6ee2b57b1ab7522f6be77"},
] ]
[package.dependencies] [package.dependencies]
@@ -252,13 +252,13 @@ files = [
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2024.6.2" version = "2024.7.4"
description = "Python package for providing Mozilla's CA Bundle." description = "Python package for providing Mozilla's CA Bundle."
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
files = [ files = [
{file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"},
{file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"},
] ]
[[package]] [[package]]
@@ -591,13 +591,13 @@ test-randomorder = ["pytest-randomly"]
[[package]] [[package]]
name = "cyclonedx-python-lib" name = "cyclonedx-python-lib"
version = "7.4.1" version = "7.5.0"
description = "Python library for CycloneDX" description = "Python library for CycloneDX"
optional = false optional = false
python-versions = "<4.0,>=3.8" python-versions = "<4.0,>=3.8"
files = [ files = [
{file = "cyclonedx_python_lib-7.4.1-py3-none-any.whl", hash = "sha256:73bf8d5c09ad10698c75d3ce3f123c84c9aff3959d67b8b5ca9e5a7c5da43abe"}, {file = "cyclonedx_python_lib-7.5.0-py3-none-any.whl", hash = "sha256:0bb301bfee57d21a76a1288c3670d5aca9924bbe212d13d09e264dfde8cc7389"},
{file = "cyclonedx_python_lib-7.4.1.tar.gz", hash = "sha256:23bf8196e008bb8e06c1040ad2ab69492891d8a581cb2aefa36a77f199790a37"}, {file = "cyclonedx_python_lib-7.5.0.tar.gz", hash = "sha256:28ef507c1a803e39f6932f328ca26f0fd21efbd9539175492b16325e400b4e1a"},
] ]
[package.dependencies] [package.dependencies]
@@ -1841,13 +1841,13 @@ dev = ["black", "mypy", "pytest"]
[[package]] [[package]]
name = "packageurl-python" name = "packageurl-python"
version = "0.15.1" version = "0.15.2"
description = "A purl aka. Package URL parser and builder" description = "A purl aka. Package URL parser and builder"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "packageurl_python-0.15.1-py3-none-any.whl", hash = "sha256:f7a44ddb9caaf6197b3b62b890ed0be5cb15e962accab2a51db36846d5174562"}, {file = "packageurl_python-0.15.2-py3-none-any.whl", hash = "sha256:6b81641aeedf0a73377d88a8a640e45a2a0848ffdf5447d24eeef8526c41ac92"},
{file = "packageurl_python-0.15.1.tar.gz", hash = "sha256:9a37b9a7cad9a2872b4612151ba3749fd9dec90485577c14d374b6e66b7edf03"}, {file = "packageurl_python-0.15.2.tar.gz", hash = "sha256:9cd10eeedbc6680728c10a1585c6dd7bbad4ef4b389d80cd0ac223205e9c87df"},
] ]
[package.extras] [package.extras]
@@ -1990,18 +1990,18 @@ type = ["mypy (>=1.8)"]
[[package]] [[package]]
name = "playwright" name = "playwright"
version = "1.44.0" version = "1.45.0"
description = "A high-level API to automate web browsers" description = "A high-level API to automate web browsers"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "playwright-1.44.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:c2317a80896796fdeb03d60f06cc229e775ff2e19b80c64b1bb9b29c8a59d992"}, {file = "playwright-1.45.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:7d49aee5907d8e72060f04bc299cb6851c2dc44cb227540ade89d7aa529e907a"},
{file = "playwright-1.44.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:54d44fb634d870839301c2326e1e12a178a1be0de76d0caaec230ab075c2e077"}, {file = "playwright-1.45.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:210c9f848820f58b5b5ed48047748620b780ca3acc3e2b7560dafb2bfdd6d90a"},
{file = "playwright-1.44.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:64b67194e73b47ae72acf25f1a9cfacfef38ca2b52e4bb8b0abd385c5deeaadf"}, {file = "playwright-1.45.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:13b5398831f5499580e819ddc996633446a93bf88029e89451e51da188e16ae3"},
{file = "playwright-1.44.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:29161b1fae71f7c402df5b15f0bd3deaeecd8b3d1ecd9ff01271700c66210e7b"}, {file = "playwright-1.45.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:0ba5a39f25fb9b9cf1bd48678f44536a29f6d83376329de2dee1567dac220afe"},
{file = "playwright-1.44.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8c8a3bfea17576d3f94a2363eee195cbda8dbba86975588c7eaac7792b25eee"}, {file = "playwright-1.45.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b09fa76614ba2926d45a4c0581f710c13652d5e32290ba6a1490fbafff7f0be8"},
{file = "playwright-1.44.0-py3-none-win32.whl", hash = "sha256:235e37832deaa9af8a629d09955396259ab757533cc1922f9b0308b4ee0d9cdf"}, {file = "playwright-1.45.0-py3-none-win32.whl", hash = "sha256:97a7d53af89af54208b69c051046b462675fcf5b93f7fbfb7c0fa7f813424ee2"},
{file = "playwright-1.44.0-py3-none-win_amd64.whl", hash = "sha256:5b8a4a1d4d50f4ff99b47965576322a8c4e34631854b862a25c1feb824be22a8"}, {file = "playwright-1.45.0-py3-none-win_amd64.whl", hash = "sha256:701db496928429aec103739e48e3110806bd5cf49456cc95b89f28e1abda71da"},
] ]
[package.dependencies] [package.dependencies]
@@ -2365,13 +2365,13 @@ dev = ["pre-commit", "pytest-asyncio", "tox"]
[[package]] [[package]]
name = "pytest-playwright" name = "pytest-playwright"
version = "0.5.0" version = "0.5.1"
description = "A pytest wrapper with fixtures for Playwright to automate web browsers" description = "A pytest wrapper with fixtures for Playwright to automate web browsers"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "pytest-playwright-0.5.0.tar.gz", hash = "sha256:f9f5ae8ade2f773e6e2cd85ec6bfff2ab287f7943108b3956fe5971324151622"}, {file = "pytest-playwright-0.5.1.tar.gz", hash = "sha256:6b0683cbacd060f338b37d0c2cdac25d841e14f1440e986efcceaacd3d61a268"},
{file = "pytest_playwright-0.5.0-py3-none-any.whl", hash = "sha256:b382c870384419c025d66aea14518bab71fb9e79917d4808692cde70d8c5216a"}, {file = "pytest_playwright-0.5.1-py3-none-any.whl", hash = "sha256:54eb12742de16bf50d9630fe06ac398727e52d5c1e55269acb37e1ede91d9e00"},
] ]
[package.dependencies] [package.dependencies]
@@ -3116,4 +3116,4 @@ files = [
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.12.2" python-versions = "^3.12.2"
content-hash = "8ad87349d57d4ff720e720067412e2656326c089ae21d51f6d96215f9546602e" content-hash = "997ddb8a6a1e91e60aaf20793dd9a79947c2f34f1640b6dd2e300de3a392ec12"
+4 -4
View File
@@ -39,8 +39,8 @@ wtforms = "~=3.1"
markdown = "^3.5.2" markdown = "^3.5.2"
async-timeout = "^4.0.3" async-timeout = "^4.0.3"
bleach = "^6.1.0" bleach = "^6.1.0"
boto3 = "^1.34.138" boto3 = "^1.34.139"
botocore = "^1.34.138" botocore = "^1.34.139"
cachetools = "^5.3.3" cachetools = "^5.3.3"
cffi = "^1.16.0" cffi = "^1.16.0"
cryptography = "^42.0.8" cryptography = "^42.0.8"
@@ -58,7 +58,7 @@ regex = "^2024.5.15"
s3transfer = "^0.10.2" s3transfer = "^0.10.2"
shapely = "^2.0.4" shapely = "^2.0.4"
smartypants = "^2.0.1" smartypants = "^2.0.1"
certifi = "^2024.2.2" certifi = "^2024.7.4"
charset-normalizer = "^3.3.2" charset-normalizer = "^3.3.2"
click = "^8.1.7" click = "^8.1.7"
idna = "^3.7" idna = "^3.7"
@@ -89,7 +89,7 @@ pre-commit = "^3.7.1"
pytest = "^8.2.2" pytest = "^8.2.2"
pytest-env = "^1.1.3" pytest-env = "^1.1.3"
pytest-mock = "^3.14.0" pytest-mock = "^3.14.0"
pytest-playwright = "^0.5.0" pytest-playwright = "^0.5.1"
pytest-xdist = "^3.5.0" pytest-xdist = "^3.5.0"
radon = "^6.0.1" radon = "^6.0.1"
requests-mock = "^1.11.0" requests-mock = "^1.11.0"
-9
View File
@@ -657,9 +657,6 @@ def test_should_not_show_recent_templates_on_dashboard_if_only_one_template_used
stats[0]["template_name"] == "one" stats[0]["template_name"] == "one"
), f"Expected template_name to be 'one', but got {stats[0]['template_name']}" ), f"Expected template_name to be 'one', but got {stats[0]['template_name']}"
# Debugging: print the main content to understand where "one" is appearing
print(f"Main content: {main}")
# Check that "one" is not in the main content # Check that "one" is not in the main content
assert ( assert (
stats[0]["template_name"] in main stats[0]["template_name"] in main
@@ -1857,12 +1854,6 @@ def test_service_dashboard_shows_free_allowance(
return_value=FAKE_ONE_OFF_NOTIFICATION, return_value=FAKE_ONE_OFF_NOTIFICATION,
) )
page = client_request.get("main.service_dashboard", service_id=SERVICE_ONE_ID)
usage_text = normalize_spaces(page.select_one("[data-key=usage]").text)
assert "spent on text messages" not in usage_text
assert "Daily Sent Remaining 1,000 249,000" in usage_text
def test_service_dashboard_shows_batched_jobs( def test_service_dashboard_shows_batched_jobs(
mocker, mocker,
+100
View File
@@ -0,0 +1,100 @@
const path = require('path');
const fs = require('fs');
// Load the D3 script content
const d3ScriptContent = fs.readFileSync(path.resolve(__dirname, '../javascripts/support/d3.min.js'), 'utf-8');
// Helper function to dynamically load a script
function loadScript(scriptContent) {
const script = document.createElement('script');
script.textContent = scriptContent;
document.head.appendChild(script);
}
// beforeAll hook to set up the DOM and load D3.js script
beforeAll(done => {
// Set up the DOM with the D3 script included
document.body.innerHTML = `
<div id="chartContainer" data-sms-sent="100" data-sms-allowance-remaining="249900" style="width: 600px;">
<h1 id="chartTitle">Total Messages</h1>
<svg id="totalMessageChart"></svg>
</div>
<div id="totalMessageTable"></div>
<div id="message"></div>
<div class="tooltip hidden"></div>
`;
// Load the D3 script dynamically
loadScript(d3ScriptContent);
// Wait a bit to ensure the script is executed
setTimeout(() => {
// Require the actual JavaScript file you are testing
require('../../app/assets/javascripts/chartDashboard.js');
done();
}, 100);
});
// Single test to check if D3 is loaded correctly
test('D3 is loaded correctly', () => {
// Check if D3 is loaded by verifying the existence of the d3 object
expect(window.d3).toBeDefined();
expect(typeof window.d3.version).toBe('string');
});
// Test to check if the SVG element is correctly set up
test('SVG element is correctly set up', () => {
const svg = document.getElementById('totalMessageChart');
expect(svg).not.toBeNull();
expect(svg.getAttribute('width')).toBe(svg.parentElement.clientWidth.toString());
expect(svg.getAttribute('height')).toBe('64');
});
// Test to check if the table is created and populated correctly
test('Populates the accessible table correctly', () => {
const table = document.getElementById('totalMessageTable').getElementsByTagName('table')[0];
expect(table).toBeDefined();
const rows = table.getElementsByTagName('tr');
expect(rows.length).toBe(3); // Header + 2 data rows
const headers = rows[0].getElementsByTagName('th');
expect(headers[0].textContent).toBe('Label');
expect(headers[1].textContent).toBe('Value');
const firstRowCells = rows[1].getElementsByTagName('td');
expect(firstRowCells[0].textContent).toBe('Messages Sent');
expect(firstRowCells[1].textContent).toBe('100');
const secondRowCells = rows[2].getElementsByTagName('td');
expect(secondRowCells[0].textContent).toBe('Remaining');
expect(secondRowCells[1].textContent).toBe('249,900');
});
// Test to check if the chart title is correctly set
test('Chart title is correctly set', () => {
const chartTitle = document.getElementById('chartTitle').textContent;
expect(chartTitle).toBe('Total Messages');
});
test('Chart resizes correctly on window resize', done => {
setTimeout(() => {
const svg = document.getElementById('totalMessageChart');
const chartContainer = document.getElementById('chartContainer');
// Initial check
expect(svg.getAttribute('width')).toBe(chartContainer.clientWidth.toString());
// Set new container width
chartContainer.style.width = '800px';
// Trigger resize event
window.dispatchEvent(new Event('resize'));
setTimeout(() => {
// Check if SVG width is updated
expect(svg.getAttribute('width')).toBe(chartContainer.clientWidth.toString());
done();
}, 500); // Adjust the timeout if necessary
}, 1000); // Initial wait for the chart to render
}, 10000); // Adjust the overall test timeout if necessary
+1 -1
View File
@@ -9,7 +9,7 @@ module.exports = {
statements: 90, statements: 90,
} }
}, },
setupFiles: ['./support/setup.js'], setupFiles: ['./support/setup.js', './support/jest.setup.js'],
testEnvironment: 'jsdom', testEnvironment: 'jsdom',
testEnvironmentOptions: { testEnvironmentOptions: {
url: 'https://beta.notify.gov', url: 'https://beta.notify.gov',
File diff suppressed because one or more lines are too long
+11
View File
@@ -0,0 +1,11 @@
const fs = require('fs');
const path = require('path');
// Polyfill holes in JSDOM
require('./polyfills.js');
// Set up jQuery
global.$ = global.jQuery = require('jquery');
// Load module code
require('govuk_frontend_toolkit/javascripts/govuk/modules.js');
+6 -4
View File
@@ -1,9 +1,11 @@
const fs = require('fs');
const path = require('path');
// Polyfill holes in JSDOM // Polyfill holes in JSDOM
require('./polyfills.js'); require('./polyfills.js');
// set up jQuery // Set up jQuery
window.jQuery = require('jquery'); global.$ = global.jQuery = require('jquery');
$ = window.jQuery;
// load module code // Load module code
require('govuk_frontend_toolkit/javascripts/govuk/modules.js'); require('govuk_frontend_toolkit/javascripts/govuk/modules.js');