mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-09-11 10:28:41 -04:00
pull main
This commit is contained in:
+5
-5
@@ -133,7 +133,7 @@
|
|||||||
"filename": ".github/workflows/checks.yml",
|
"filename": ".github/workflows/checks.yml",
|
||||||
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
|
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
|
||||||
"is_verified": false,
|
"is_verified": false,
|
||||||
"line_number": 67,
|
"line_number": 65,
|
||||||
"is_secret": false
|
"is_secret": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -141,7 +141,7 @@
|
|||||||
"filename": ".github/workflows/checks.yml",
|
"filename": ".github/workflows/checks.yml",
|
||||||
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
|
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
|
||||||
"is_verified": false,
|
"is_verified": false,
|
||||||
"line_number": 101,
|
"line_number": 99,
|
||||||
"is_secret": false
|
"is_secret": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -413,7 +413,7 @@
|
|||||||
"filename": "app/templates/new/components/head.html",
|
"filename": "app/templates/new/components/head.html",
|
||||||
"hashed_secret": "ee5048791fc7ff45a1545e24f85bec3317371327",
|
"hashed_secret": "ee5048791fc7ff45a1545e24f85bec3317371327",
|
||||||
"is_verified": false,
|
"is_verified": false,
|
||||||
"line_number": 34,
|
"line_number": 33,
|
||||||
"is_secret": false
|
"is_secret": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -535,7 +535,7 @@
|
|||||||
"filename": "tests/app/main/views/test_accept_invite.py",
|
"filename": "tests/app/main/views/test_accept_invite.py",
|
||||||
"hashed_secret": "07f0a6c13923fc3b5f0c57ffa2d29b715eb80d71",
|
"hashed_secret": "07f0a6c13923fc3b5f0c57ffa2d29b715eb80d71",
|
||||||
"is_verified": false,
|
"is_verified": false,
|
||||||
"line_number": 626,
|
"line_number": 643,
|
||||||
"is_secret": false
|
"is_secret": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -692,5 +692,5 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"generated_at": "2024-07-24T14:13:02Z"
|
"generated_at": "2024-08-15T16:29:15Z"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,14 @@
|
|||||||
# Please see the documentation for all configuration options:
|
# Please see the documentation for all configuration options:
|
||||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
|
||||||
version: 3
|
version: 2
|
||||||
updates:
|
updates:
|
||||||
- package-ecosystem: "pip" # See documentation for possible values
|
- package-ecosystem: "pip" # See documentation for possible values
|
||||||
directory: "/" # Location of package manifests
|
directory: "/" # Location of package manifests
|
||||||
schedule:
|
schedule:
|
||||||
interval: "daily"
|
interval: "daily"
|
||||||
- package-ecosystem: 'npm'
|
- package-ecosystem: "npm"
|
||||||
directory: '/'
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: 'daily'
|
interval: "daily"
|
||||||
versioning-strategy: 'increase'
|
versioning-strategy: increase
|
||||||
|
|||||||
@@ -44,8 +44,6 @@ jobs:
|
|||||||
run: poetry run isort --check-only ./app ./tests
|
run: poetry run isort --check-only ./app ./tests
|
||||||
- name: Check dead code
|
- name: Check dead code
|
||||||
run: make dead-code
|
run: make dead-code
|
||||||
- name: Run js lint
|
|
||||||
run: npm run lint
|
|
||||||
- name: Run js tests
|
- name: Run js tests
|
||||||
run: npm test
|
run: npm test
|
||||||
- name: Run py tests with coverage
|
- name: Run py tests with coverage
|
||||||
@@ -54,6 +52,7 @@ jobs:
|
|||||||
run: poetry run coverage report --fail-under=90
|
run: poetry run coverage report --fail-under=90
|
||||||
|
|
||||||
end-to-end-tests:
|
end-to-end-tests:
|
||||||
|
if: ${{ github.actor != 'dependabot[bot]' }}
|
||||||
permissions:
|
permissions:
|
||||||
checks: write
|
checks: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
@@ -84,6 +83,7 @@ jobs:
|
|||||||
ports:
|
ports:
|
||||||
# Maps tcp port 6379 on service container to the host
|
# Maps tcp port 6379 on service container to the host
|
||||||
- 6379:6379
|
- 6379:6379
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: ./.github/actions/setup-project
|
- uses: ./.github/actions/setup-project
|
||||||
@@ -136,6 +136,12 @@ jobs:
|
|||||||
# Debugging for now to troubleshoot a connectivity issue to the local servers
|
# Debugging for now to troubleshoot a connectivity issue to the local servers
|
||||||
# run: curl --request GET --url "http://localhost:6012"
|
# run: curl --request GET --url "http://localhost:6012"
|
||||||
env:
|
env:
|
||||||
|
API_HOST_NAME: http://localhost:6011
|
||||||
|
DANGEROUS_SALT: ${{ secrets.DANGEROUS_SALT }}
|
||||||
|
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||||
|
ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }}
|
||||||
|
ADMIN_CLIENT_USERNAME: notify-admin
|
||||||
|
|
||||||
NOTIFY_ENVIRONMENT: e2etest
|
NOTIFY_ENVIRONMENT: e2etest
|
||||||
NOTIFY_E2E_AUTH_STATE_PATH: ${{ secrets.NOTIFY_E2E_AUTH_STATE_PATH }}
|
NOTIFY_E2E_AUTH_STATE_PATH: ${{ secrets.NOTIFY_E2E_AUTH_STATE_PATH }}
|
||||||
NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }}
|
NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ from flask import (
|
|||||||
)
|
)
|
||||||
from flask.globals import request_ctx
|
from flask.globals import request_ctx
|
||||||
from flask_login import LoginManager, current_user
|
from flask_login import LoginManager, current_user
|
||||||
from flask_socketio import SocketIO
|
|
||||||
from flask_talisman import Talisman
|
from flask_talisman import Talisman
|
||||||
from flask_wtf import CSRFProtect
|
from flask_wtf import CSRFProtect
|
||||||
from flask_wtf.csrf import CSRFError
|
from flask_wtf.csrf import CSRFError
|
||||||
@@ -119,8 +118,6 @@ from notifications_utils.recipients import format_phone_number_human_readable
|
|||||||
login_manager = LoginManager()
|
login_manager = LoginManager()
|
||||||
csrf = CSRFProtect()
|
csrf = CSRFProtect()
|
||||||
talisman = Talisman()
|
talisman = Talisman()
|
||||||
socketio = SocketIO()
|
|
||||||
|
|
||||||
|
|
||||||
# The current service attached to the request stack.
|
# The current service attached to the request stack.
|
||||||
current_service = LocalProxy(partial(getattr, request_ctx, "service"))
|
current_service = LocalProxy(partial(getattr, request_ctx, "service"))
|
||||||
@@ -177,7 +174,6 @@ def create_app(application):
|
|||||||
|
|
||||||
init_govuk_frontend(application)
|
init_govuk_frontend(application)
|
||||||
init_jinja(application)
|
init_jinja(application)
|
||||||
socketio.init_app(application)
|
|
||||||
|
|
||||||
for client in (
|
for client in (
|
||||||
csrf,
|
csrf,
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 13.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
width="719px" height="720px" viewBox="0 0 129.996 130.238" enable-background="new 0 0 129.996 130.238"
|
||||||
|
xml:space="preserve">
|
||||||
|
<rect fill="#00538E" width="129.996" height="130.238"/>
|
||||||
|
<path fill="#FFFFFF" d="M78.611,96.076c0,9.138-7.175,12.953-15.451,12.688c-2.938-0.095-5.799-0.838-8.916-1.855l0.707-6.578
|
||||||
|
c2.638,1.086,6.057,2.263,8.92,2.219c3.74-0.058,7.477-1.794,7.477-6.124c0-3.335-2.67-4.976-5.252-6.283
|
||||||
|
c-5.709-2.896-12.275-4.907-12.275-12.998c0-7.65,6.084-11.64,13.625-11.64c2.741,0,6.014,0.477,9.299,1.641l-0.985,6.478
|
||||||
|
c-3.577-1.403-5.14-1.901-8.078-1.901c-3.253,0-6.597,1.143-6.597,4.896c0,2.933,3.01,4.456,5.248,5.573
|
||||||
|
C72.398,85.218,78.611,87.703,78.611,96.076"/>
|
||||||
|
<path fill="#FFFFFF" d="M44.561,106.8c-2.632,0.901-6.662,1.964-13.092,1.964c-13.195,0-22.027-8.529-22.027-21.805
|
||||||
|
c0-12.882,9.375-21.455,22.086-21.455c6.036,0,8.727,0.896,12.541,2.212l-0.574,7.216c-3.51-2.294-6.498-3.211-11.674-3.211
|
||||||
|
c-9.09,0-15.158,6.578-15.115,15.412c0.043,9.506,6.648,15.416,14.822,15.416c2.437,0,4.508-0.178,5.996-0.569V91.15h-7.861v-6.103
|
||||||
|
h14.898V106.8"/>
|
||||||
|
<polyline fill="#FFFFFF" points="102.041,64.587 112.063,86.918 105.08,86.918 102.041,79.497 98.996,86.918 92.014,86.918
|
||||||
|
102.041,64.587 "/>
|
||||||
|
<polyline fill="#FFFFFF" points="112.178,87.179 121.507,108.029 114.176,108.029 107.203,91.839 112.178,87.179 "/>
|
||||||
|
<polyline fill="#FFFFFF" points="91.9,87.183 96.873,91.841 93.199,100.24 102.027,95.611 107.109,99.525 89.727,108.031
|
||||||
|
82.57,108.031 91.9,87.183 "/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -26,6 +26,13 @@
|
|||||||
.append('g')
|
.append('g')
|
||||||
.attr('transform', `translate(${margin.left},${margin.top})`);
|
.attr('transform', `translate(${margin.left},${margin.top})`);
|
||||||
|
|
||||||
|
let tooltip = d3.select('#tooltip');
|
||||||
|
|
||||||
|
if (tooltip.empty()) {
|
||||||
|
tooltip = d3.select('body').append('div')
|
||||||
|
.attr('id', 'tooltip')
|
||||||
|
.style('display', 'none');
|
||||||
|
}
|
||||||
// Create legend
|
// Create legend
|
||||||
const legendContainer = d3.select('.chart-legend');
|
const legendContainer = d3.select('.chart-legend');
|
||||||
legendContainer.selectAll('*').remove(); // Clear any existing legend
|
legendContainer.selectAll('*').remove(); // Clear any existing legend
|
||||||
@@ -95,10 +102,6 @@
|
|||||||
const color = d3.scaleOrdinal()
|
const color = d3.scaleOrdinal()
|
||||||
.domain(['delivered', 'failed'])
|
.domain(['delivered', 'failed'])
|
||||||
.range([COLORS.delivered, COLORS.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
|
// Create bars with animation
|
||||||
const barGroups = svg.selectAll('.bar-group')
|
const barGroups = svg.selectAll('.bar-group')
|
||||||
@@ -185,37 +188,35 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var socket = io();
|
var url = type === 'service' ? `/daily_stats.json` : `/daily_stats_by_user.json`;
|
||||||
var eventType = type === 'service' ? 'fetch_daily_stats' : 'fetch_daily_stats_by_user';
|
return fetch(url)
|
||||||
var socketConnect = type === 'service' ? 'daily_stats_update' : 'daily_stats_by_user_update';
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Network response was not ok');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
labels = [];
|
||||||
|
deliveredData = [];
|
||||||
|
failedData = [];
|
||||||
|
|
||||||
socket.on('connect', function () {
|
for (var dateString in data) {
|
||||||
socket.emit(eventType);
|
if (data.hasOwnProperty(dateString)) {
|
||||||
});
|
const dateParts = dateString.split('-');
|
||||||
|
const formattedDate = `${dateParts[1]}/${dateParts[2]}/${dateParts[0].slice(2)}`;
|
||||||
|
|
||||||
socket.on(socketConnect, function(data) {
|
labels.push(formattedDate);
|
||||||
|
deliveredData.push(data[dateString].sms.delivered);
|
||||||
|
failedData.push(data[dateString].sms.failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var labels = [];
|
createChart('#weeklyChart', labels, deliveredData, failedData);
|
||||||
var deliveredData = [];
|
createTable('weeklyTable', 'activityChart', labels, deliveredData, failedData);
|
||||||
var failedData = [];
|
return data;
|
||||||
|
})
|
||||||
for (var dateString in data) {
|
.catch(error => console.error('Error fetching daily stats:', error));
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
createChart('#weeklyChart', labels, deliveredData, failedData);
|
|
||||||
createTable('weeklyTable', 'activityChart', labels, deliveredData, failedData);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('error', function(data) {
|
|
||||||
console.log('Error:', data);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDropdownChange = function(event) {
|
const handleDropdownChange = function(event) {
|
||||||
@@ -224,32 +225,40 @@
|
|||||||
const selectElement = document.getElementById('options');
|
const selectElement = document.getElementById('options');
|
||||||
const selectedText = selectElement.options[selectElement.selectedIndex].text;
|
const selectedText = selectElement.options[selectElement.selectedIndex].text;
|
||||||
|
|
||||||
if (selectedValue === "individual") {
|
subTitle.textContent = `${selectedText} - last 7 days`;
|
||||||
subTitle.textContent = selectedText + " - Last 7 Days";
|
fetchData(selectedValue);
|
||||||
fetchData('individual');
|
|
||||||
} else if (selectedValue === "service") {
|
|
||||||
subTitle.textContent = selectedText + " - Last 7 Days";
|
|
||||||
fetchData('service');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update ARIA live region
|
// Update ARIA live region
|
||||||
const liveRegion = document.getElementById('aria-live-account');
|
const liveRegion = document.getElementById('aria-live-account');
|
||||||
liveRegion.textContent = `Data updated for ${selectedText} - Last 7 Days`;
|
liveRegion.textContent = `Data updated for ${selectedText} - last 7 days`;
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
// Switch tables based on dropdown selection
|
||||||
// Initialize activityChart chart and table with service data by default
|
const selectedTable = selectedValue === "individual" ? "table1" : "table2";
|
||||||
fetchData('service');
|
const tables = document.querySelectorAll('.table-overflow-x-auto');
|
||||||
|
tables.forEach(function(table) {
|
||||||
// Add event listener to the dropdown
|
table.classList.add('hidden'); // Hide all tables by adding the hidden class
|
||||||
const dropdown = document.getElementById('options');
|
table.classList.remove('visible'); // Ensure they are not visible
|
||||||
dropdown.addEventListener('change', handleDropdownChange);
|
|
||||||
});
|
});
|
||||||
|
const tableToShow = document.getElementById(selectedTable);
|
||||||
|
tableToShow.classList.remove('hidden'); // Remove hidden class
|
||||||
|
tableToShow.classList.add('visible'); // Add visible class
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
// Resize chart on window resize
|
||||||
window.addEventListener('resize', function() {
|
window.addEventListener('resize', function() {
|
||||||
const selectedValue = document.getElementById('options').value;
|
if (labels.length > 0 && deliveredData.length > 0 && failedData.length > 0) {
|
||||||
handleDropdownChange({ target: { value: selectedValue } });
|
createChart('#weeklyChart', labels, deliveredData, failedData);
|
||||||
|
createTable('weeklyTable', 'activityChart', labels, deliveredData, failedData);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Export functions for testing
|
// Export functions for testing
|
||||||
|
|||||||
@@ -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`;
|
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
|
// 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 minSentValue = totalMessages * minSentPercentage;
|
||||||
var displaySent = Math.max(sms_sent, minSentValue);
|
var displaySent = Math.max(sms_sent, minSentValue);
|
||||||
var displayRemaining = totalMessages - displaySent;
|
var displayRemaining = totalMessages - displaySent;
|
||||||
|
|
||||||
var svg = d3.select("#totalMessageChart");
|
var svg = d3.select("#totalMessageChart");
|
||||||
var width = chartContainer.clientWidth;
|
var width = chartContainer.clientWidth;
|
||||||
var height = 64;
|
var height = 48;
|
||||||
|
|
||||||
// Ensure the width is set correctly
|
// Ensure the width is set correctly
|
||||||
if (width === 0) {
|
if (width === 0) {
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
.attr("x", 0) // Initially set to 0, will be updated during animation
|
.attr("x", 0) // Initially set to 0, will be updated during animation
|
||||||
.attr("y", 0)
|
.attr("y", 0)
|
||||||
.attr("height", height)
|
.attr("height", height)
|
||||||
.attr("fill", '#fa9441')
|
.attr("fill", '#C7CACE')
|
||||||
.attr("width", 0) // Start with width 0 for animation
|
.attr("width", 0) // Start with width 0 for animation
|
||||||
.on('mouseover', function(event) {
|
.on('mouseover', function(event) {
|
||||||
tooltip.style('display', 'block')
|
tooltip.style('display', 'block')
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
$delivered: color('blue-50v');
|
$delivered: color('blue-50v');
|
||||||
$pending: color('green-cool-40v');
|
$pending: color('green-cool-40v');
|
||||||
$failed: color('orange-30v');
|
$failed: color('gray-cool-20');
|
||||||
|
|
||||||
.chart-container {
|
.chart-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -11,6 +11,10 @@ $failed: color('orange-30v');
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#totalMessageChartContainer {
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
.bar {
|
.bar {
|
||||||
border-radius: units(0.5);
|
border-radius: units(0.5);
|
||||||
&.delivered, &.usage {
|
&.delivered, &.usage {
|
||||||
|
|||||||
@@ -332,6 +332,12 @@ td.table-empty-message {
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table-overflow-x-auto {
|
||||||
|
&.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: units('desktop-lg')) {
|
@media (max-width: units('desktop-lg')) {
|
||||||
.table-overflow-x-auto {
|
.table-overflow-x-auto {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
@@ -464,7 +470,7 @@ td.table-empty-message {
|
|||||||
width: 25%;
|
width: 25%;
|
||||||
}
|
}
|
||||||
td.time-sent {
|
td.time-sent {
|
||||||
width: 30%;
|
width: 15%;
|
||||||
}
|
}
|
||||||
td.sender {
|
td.sender {
|
||||||
width: 20%;
|
width: 20%;
|
||||||
@@ -474,12 +480,20 @@ td.table-empty-message {
|
|||||||
width: 5%;
|
width: 5%;
|
||||||
}
|
}
|
||||||
td.report {
|
td.report {
|
||||||
width: 5%;
|
width: 2%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
td.delivered {
|
||||||
|
width: 2%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
td.failed {
|
||||||
|
width: 2%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
td.report img {
|
td.report img {
|
||||||
padding-top: 5px;
|
padding-top: 5px;
|
||||||
}
|
}
|
||||||
th {
|
th {
|
||||||
padding: 0.5rem 1rem
|
padding: 0.5rem 1rem
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,5 +13,6 @@ in the form $setting: value,
|
|||||||
$theme-banner-max-width: "desktop-lg",
|
$theme-banner-max-width: "desktop-lg",
|
||||||
$theme-grid-container-max-width: "desktop-lg",
|
$theme-grid-container-max-width: "desktop-lg",
|
||||||
$theme-footer-max-width: "desktop-lg",
|
$theme-footer-max-width: "desktop-lg",
|
||||||
$theme-header-max-width: "desktop-lg"
|
$theme-header-max-width: "desktop-lg",
|
||||||
|
$theme-identifier-max-width: "desktop-lg"
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ from app.utils.user import user_has_permissions
|
|||||||
|
|
||||||
|
|
||||||
@main.route("/activity/services/<uuid:service_id>")
|
@main.route("/activity/services/<uuid:service_id>")
|
||||||
@user_has_permissions()
|
@user_has_permissions("view_activity")
|
||||||
def all_jobs_activity(service_id):
|
def all_jobs_activity(service_id):
|
||||||
service_data_retention_days = 7
|
service_data_retention_days = 7
|
||||||
page = get_page_from_request()
|
page = get_page_from_request()
|
||||||
jobs = job_api_client.get_page_of_jobs(service_id, page=page)
|
jobs = job_api_client.get_page_of_jobs(service_id, page=page)
|
||||||
all_jobs_dict = generate_job_dict(jobs)
|
all_jobs_dict = generate_job_dict(jobs)
|
||||||
prev_page, next_page, pagination = handle_pagination(jobs, service_id, page)
|
prev_page, next_page, pagination = handle_pagination(jobs, service_id, page)
|
||||||
|
message_type = ("sms",)
|
||||||
return render_template(
|
return render_template(
|
||||||
"views/activity/all-activity.html",
|
"views/activity/all-activity.html",
|
||||||
all_jobs_dict=all_jobs_dict,
|
all_jobs_dict=all_jobs_dict,
|
||||||
@@ -28,6 +28,34 @@ def all_jobs_activity(service_id):
|
|||||||
next_page=next_page,
|
next_page=next_page,
|
||||||
prev_page=prev_page,
|
prev_page=prev_page,
|
||||||
pagination=pagination,
|
pagination=pagination,
|
||||||
|
download_link_one_day=url_for(
|
||||||
|
".download_notifications_csv",
|
||||||
|
service_id=current_service.id,
|
||||||
|
message_type=message_type,
|
||||||
|
status=request.args.get("status"),
|
||||||
|
number_of_days="one_day",
|
||||||
|
),
|
||||||
|
download_link_three_day=url_for(
|
||||||
|
".download_notifications_csv",
|
||||||
|
service_id=current_service.id,
|
||||||
|
message_type=message_type,
|
||||||
|
status=request.args.get("status"),
|
||||||
|
number_of_days="three_day",
|
||||||
|
),
|
||||||
|
download_link_five_day=url_for(
|
||||||
|
".download_notifications_csv",
|
||||||
|
service_id=current_service.id,
|
||||||
|
message_type=message_type,
|
||||||
|
status=request.args.get("status"),
|
||||||
|
number_of_days="five_day",
|
||||||
|
),
|
||||||
|
download_link_seven_day=url_for(
|
||||||
|
".download_notifications_csv",
|
||||||
|
service_id=current_service.id,
|
||||||
|
message_type=message_type,
|
||||||
|
status=request.args.get("status"),
|
||||||
|
number_of_days="seven_day",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -75,6 +103,22 @@ def generate_job_dict(jobs):
|
|||||||
"processing_started": job["processing_started"],
|
"processing_started": job["processing_started"],
|
||||||
"created_by": job["created_by"],
|
"created_by": job["created_by"],
|
||||||
"template_name": job["template_name"],
|
"template_name": job["template_name"],
|
||||||
|
"delivered_count": next(
|
||||||
|
(
|
||||||
|
stat["count"]
|
||||||
|
for stat in job.get("statistics", [])
|
||||||
|
if stat["status"] == "delivered"
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
"failed_count": next(
|
||||||
|
(
|
||||||
|
stat["count"]
|
||||||
|
for stat in job.get("statistics", [])
|
||||||
|
if stat["status"] == "failed"
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
for job in jobs["data"]
|
for job in jobs["data"]
|
||||||
]
|
]
|
||||||
|
|||||||
+26
-34
@@ -5,7 +5,6 @@ from itertools import groupby
|
|||||||
|
|
||||||
from flask import Response, abort, jsonify, render_template, request, session, url_for
|
from flask import Response, abort, jsonify, render_template, request, session, url_for
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_socketio import emit
|
|
||||||
from werkzeug.utils import redirect
|
from werkzeug.utils import redirect
|
||||||
|
|
||||||
from app import (
|
from app import (
|
||||||
@@ -13,7 +12,6 @@ from app import (
|
|||||||
current_service,
|
current_service,
|
||||||
job_api_client,
|
job_api_client,
|
||||||
service_api_client,
|
service_api_client,
|
||||||
socketio,
|
|
||||||
template_statistics_client,
|
template_statistics_client,
|
||||||
)
|
)
|
||||||
from app.formatters import format_date_numeric, format_datetime_numeric, get_time_left
|
from app.formatters import format_date_numeric, format_datetime_numeric, get_time_left
|
||||||
@@ -32,38 +30,6 @@ from app.utils.user import user_has_permissions
|
|||||||
from notifications_utils.recipients import format_phone_number_human_readable
|
from notifications_utils.recipients import format_phone_number_human_readable
|
||||||
|
|
||||||
|
|
||||||
@socketio.on("fetch_daily_stats")
|
|
||||||
def handle_fetch_daily_stats():
|
|
||||||
service_id = 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")
|
|
||||||
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")
|
@main.route("/services/<uuid:service_id>/dashboard")
|
||||||
@user_has_permissions("view_activity", "send_messages")
|
@user_has_permissions("view_activity", "send_messages")
|
||||||
def old_service_dashboard(service_id):
|
def old_service_dashboard(service_id):
|
||||||
@@ -113,6 +79,7 @@ def service_dashboard(service_id):
|
|||||||
"original_file_name": job["original_file_name"],
|
"original_file_name": job["original_file_name"],
|
||||||
}
|
}
|
||||||
for job in job_response
|
for job in job_response
|
||||||
|
if job["job_status"] != "cancelled"
|
||||||
]
|
]
|
||||||
return render_template(
|
return render_template(
|
||||||
"views/dashboard/dashboard.html",
|
"views/dashboard/dashboard.html",
|
||||||
@@ -125,6 +92,31 @@ def service_dashboard(service_id):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@main.route("/daily_stats.json")
|
||||||
|
def get_daily_stats():
|
||||||
|
service_id = session.get("service_id")
|
||||||
|
date_range = get_stats_date_range()
|
||||||
|
|
||||||
|
stats = service_api_client.get_service_notification_statistics_by_day(
|
||||||
|
service_id, start_date=date_range["start_date"], days=date_range["days"]
|
||||||
|
)
|
||||||
|
return jsonify(stats)
|
||||||
|
|
||||||
|
|
||||||
|
@main.route("/daily_stats_by_user.json")
|
||||||
|
def get_daily_stats_by_user():
|
||||||
|
service_id = session.get("service_id")
|
||||||
|
date_range = get_stats_date_range()
|
||||||
|
user_id = current_user.id
|
||||||
|
stats = service_api_client.get_user_service_notification_statistics_by_day(
|
||||||
|
service_id,
|
||||||
|
user_id,
|
||||||
|
start_date=date_range["start_date"],
|
||||||
|
days=date_range["days"],
|
||||||
|
)
|
||||||
|
return jsonify(stats)
|
||||||
|
|
||||||
|
|
||||||
@main.route("/services/<uuid:service_id>/dashboard.json")
|
@main.route("/services/<uuid:service_id>/dashboard.json")
|
||||||
@user_has_permissions("view_activity")
|
@user_has_permissions("view_activity")
|
||||||
def service_dashboard_updates(service_id):
|
def service_dashboard_updates(service_id):
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import csv
|
||||||
import itertools
|
import itertools
|
||||||
import json
|
import json
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
from flask import abort, flash, render_template, request, url_for
|
from flask import Response, abort, flash, render_template, request, url_for
|
||||||
from notifications_python_client.errors import HTTPError
|
from notifications_python_client.errors import HTTPError
|
||||||
|
|
||||||
from app import (
|
from app import (
|
||||||
@@ -70,6 +72,40 @@ def platform_admin():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@main.route("/platform-admin/download-all-users")
|
||||||
|
@user_is_platform_admin
|
||||||
|
def download_all_users():
|
||||||
|
|
||||||
|
# Create a CSV string from the user data
|
||||||
|
users = user_api_client.get_all_users_detailed()
|
||||||
|
|
||||||
|
if len(users) == 0:
|
||||||
|
return "No data to download."
|
||||||
|
|
||||||
|
output = StringIO()
|
||||||
|
header = ["Name", "Email Address", "Phone Number", "Service"]
|
||||||
|
fieldnames = ["name", "email_address", "mobile_number", "service"]
|
||||||
|
writer = csv.DictWriter(
|
||||||
|
output,
|
||||||
|
fieldnames=fieldnames,
|
||||||
|
delimiter=",",
|
||||||
|
)
|
||||||
|
# Write custom header
|
||||||
|
writer.writerow(dict(zip(fieldnames, header)))
|
||||||
|
for user in users:
|
||||||
|
user_no_commas = {key: value.replace(",", "") for key, value in user.items()}
|
||||||
|
if user_no_commas["name"].startswith("e2e"):
|
||||||
|
continue
|
||||||
|
writer.writerow(user_no_commas)
|
||||||
|
csv_data = output.getvalue()
|
||||||
|
|
||||||
|
# Create a direct download response with the CSV data and appropriate headers
|
||||||
|
response = Response(csv_data, content_type="text/csv; charset=utf-8")
|
||||||
|
response.headers["Content-Disposition"] = "attachment; filename=users.csv"
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
def is_over_threshold(number, total, threshold):
|
def is_over_threshold(number, total, threshold):
|
||||||
percentage = number / total * 100 if total else 0
|
percentage = number / total * 100 if total else 0
|
||||||
return percentage > threshold
|
return percentage > threshold
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ class HeaderNavigation(Navigation):
|
|||||||
"get_billing_report",
|
"get_billing_report",
|
||||||
"get_users_report",
|
"get_users_report",
|
||||||
"get_daily_volumes",
|
"get_daily_volumes",
|
||||||
|
"download_all_users",
|
||||||
"get_daily_sms_provider_volumes",
|
"get_daily_sms_provider_volumes",
|
||||||
"get_volumes_by_service",
|
"get_volumes_by_service",
|
||||||
"organizations",
|
"organizations",
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ class NotifyAdminAPIClient(BaseAPIClient):
|
|||||||
):
|
):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
def is_calling_signin_url(self, arg):
|
||||||
|
return arg.startswith("('/user")
|
||||||
|
|
||||||
def check_inactive_user(self, *args):
|
def check_inactive_user(self, *args):
|
||||||
still_signing_in = False
|
still_signing_in = False
|
||||||
|
|
||||||
@@ -64,14 +67,7 @@ class NotifyAdminAPIClient(BaseAPIClient):
|
|||||||
# and we only want to check the first arg
|
# and we only want to check the first arg
|
||||||
for arg in args:
|
for arg in args:
|
||||||
arg = str(arg)
|
arg = str(arg)
|
||||||
if (
|
if self.is_calling_signin_url(arg):
|
||||||
"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
|
|
||||||
):
|
|
||||||
still_signing_in = True
|
still_signing_in = True
|
||||||
|
|
||||||
# This seems to be a weird edge case that happens intermittently with invites
|
# This seems to be a weird edge case that happens intermittently with invites
|
||||||
|
|||||||
@@ -27,9 +27,7 @@ class JobApiClient(NotifyAdminAPIClient):
|
|||||||
|
|
||||||
def get_job(self, service_id, job_id):
|
def get_job(self, service_id, job_id):
|
||||||
params = {}
|
params = {}
|
||||||
job = self.get(
|
job = self.get(url=f"/service/{service_id}/job/{job_id}", params=params)
|
||||||
url="/service/{}/job/{}".format(service_id, job_id), params=params
|
|
||||||
)
|
|
||||||
|
|
||||||
return job
|
return job
|
||||||
|
|
||||||
@@ -40,13 +38,14 @@ class JobApiClient(NotifyAdminAPIClient):
|
|||||||
if statuses is not None:
|
if statuses is not None:
|
||||||
params["statuses"] = ",".join(statuses)
|
params["statuses"] = ",".join(statuses)
|
||||||
|
|
||||||
return self.get(url="/service/{}/job".format(service_id), params=params)
|
job = self.get(url=f"/service/{service_id}/job", params=params)
|
||||||
|
return job
|
||||||
|
|
||||||
def get_uploads(self, service_id, limit_days=None, page=1):
|
def get_uploads(self, service_id, limit_days=None, page=1):
|
||||||
params = {"page": page}
|
params = {"page": page}
|
||||||
if limit_days is not None:
|
if limit_days is not None:
|
||||||
params["limit_days"] = limit_days
|
params["limit_days"] = limit_days
|
||||||
return self.get(url="/service/{}/upload".format(service_id), params=params)
|
return self.get(url=f"/service/{service_id}/upload", params=params)
|
||||||
|
|
||||||
def has_sent_previously(
|
def has_sent_previously(
|
||||||
self, service_id, template_id, template_version, original_file_name
|
self, service_id, template_id, template_version, original_file_name
|
||||||
|
|||||||
@@ -157,6 +157,10 @@ class UserApiClient(NotifyAdminAPIClient):
|
|||||||
endpoint = "/user"
|
endpoint = "/user"
|
||||||
return self.get(endpoint)["data"]
|
return self.get(endpoint)["data"]
|
||||||
|
|
||||||
|
def get_all_users_detailed(self):
|
||||||
|
endpoint = "/user/report-all-users"
|
||||||
|
return self.get(endpoint)["data"]
|
||||||
|
|
||||||
@cache.delete("service-{service_id}")
|
@cache.delete("service-{service_id}")
|
||||||
@cache.delete("service-{service_id}-template-folders")
|
@cache.delete("service-{service_id}-template-folders")
|
||||||
@cache.delete("user-{user_id}")
|
@cache.delete("user-{user_id}")
|
||||||
|
|||||||
@@ -11,17 +11,16 @@
|
|||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
|
||||||
|
|
||||||
<link rel="shortcut icon" href="{{ assetPath | default('/assets') }}/images/favicon.ico" />
|
<link rel="shortcut icon" href="{{ assetPath | default('/static') }}/images/favicon.ico" />
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ assetPath | default('/assets') }}/images/favicon-32x32.png" />
|
<link rel="icon" type="image/png" sizes="32x32" href="{{ assetPath | default('/static') }}/images/favicon-32x32.png" />
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="{{ assetPath | default('/assets') }}/images/favicon-16x16.png" />
|
<link rel="icon" type="image/png" sizes="16x16" href="{{ assetPath | default('/static') }}/images/favicon-16x16.png" />
|
||||||
<link rel="mask-icon" href="{{ assetPath | default('/assets') }}/images/safari-pinned-tab.svg" color="#5bbad5">
|
<link rel="mask-icon" href="{{ assetPath | default('/static') }}/images/safari-pinned-tab.svg" color="#5bbad5">
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ assetPath | default('/assets') }}/images/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="180x180" href="{{ assetPath | default('/static') }}/images/apple-touch-icon.png">
|
||||||
<link rel="manifest" href="/site.webmanifest">
|
<link rel="manifest" href="/site.webmanifest">
|
||||||
<meta name="msapplication-TileColor" content="#da532c">
|
<meta name="msapplication-TileColor" content="#da532c">
|
||||||
<link href="{{ assetPath | default('/assets') }}/images/notify-dark-favicon.png" rel="icon" media="(prefers-color-scheme: dark)">
|
<link href="{{ assetPath | default('/static') }}/images/notify-dark-favicon.png" rel="icon" media="(prefers-color-scheme: dark)">
|
||||||
<meta name="theme-color" content="#ffffff">
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
|
||||||
|
|
||||||
<link rel="stylesheet" media="screen" href="{{ asset_url('css/styles.css') }}" />
|
<link rel="stylesheet" media="screen" href="{{ asset_url('css/styles.css') }}" />
|
||||||
{% block extra_stylesheets %}{% endblock %}
|
{% block extra_stylesheets %}{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -19,14 +19,14 @@
|
|||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
|
||||||
{% block headIcons %}
|
{% block headIcons %}
|
||||||
<link rel="shortcut icon" href="{{ assetPath | default('/assets') }}/images/favicon.ico" />
|
<link rel="shortcut icon" href="{{ assetPath | default('/static') }}/images/favicon.ico" />
|
||||||
<link rel="icon" type="image/png" sizes="32x32"href="{{ assetPath | default('/assets') }}/images/favicon-32x32.png"/>
|
<link rel="icon" type="image/png" sizes="32x32"href="{{ assetPath | default('/static') }}/images/favicon-32x32.png"/>
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="{{ assetPath | default('/assets') }}/images/favicon-16x16.png" />
|
<link rel="icon" type="image/png" sizes="16x16" href="{{ assetPath | default('/static') }}/images/favicon-16x16.png" />
|
||||||
<link rel="mask-icon" href="{{ assetPath | default('/assets') }}/images/safari-pinned-tab.svg" color="#5bbad5">
|
<link rel="mask-icon" href="{{ assetPath | default('/static') }}/images/safari-pinned-tab.svg" color="#5bbad5">
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ assetPath | default('/assets') }}/images/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="180x180" href="{{ assetPath | default('/static') }}/images/apple-touch-icon.png">
|
||||||
<link rel="manifest" href="/site.webmanifest">
|
<link rel="manifest" href="/site.webmanifest">
|
||||||
<meta name="msapplication-TileColor" content="#da532c">
|
<meta name="msapplication-TileColor" content="#da532c">
|
||||||
<link href="{{ assetPath | default('/assets') }}/images/notify-dark-favicon.png" rel="icon" media="(prefers-color-scheme: dark)">
|
<link href="{{ assetPath | default('/static') }}/images/notify-dark-favicon.png" rel="icon" media="(prefers-color-scheme: dark)">
|
||||||
<meta name="theme-color" content="#ffffff">
|
<meta name="theme-color" content="#ffffff">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
class="usa-pagination__link usa-pagination__previous-page"
|
class="usa-pagination__link usa-pagination__previous-page"
|
||||||
aria-label="Previous page"
|
aria-label="Previous page"
|
||||||
>
|
>
|
||||||
<img src="{{ url_for('static', filename='/img/usa-icons/navigate_before.svg') }}" alt="arrow">
|
<img src="{{ asset_url('img/usa-icons/navigate_before.svg') }}" alt="arrow">
|
||||||
<span class="usa-pagination__link-text">Previous</span></a
|
<span class="usa-pagination__link-text">Previous</span></a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
aria-label="Next page"
|
aria-label="Next page"
|
||||||
>
|
>
|
||||||
<span class="usa-pagination__link-text">Next </span>
|
<span class="usa-pagination__link-text">Next </span>
|
||||||
<img src="{{ url_for('static', filename='/img/usa-icons/navigate_next.svg') }}" alt="arrow">
|
<img src="{{ asset_url('img/usa-icons/navigate_next.svg') }}" alt="arrow">
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -84,6 +84,12 @@
|
|||||||
<th data-sortable scope="col" role="columnheader" class="table-field-heading">
|
<th data-sortable scope="col" role="columnheader" class="table-field-heading">
|
||||||
<span>Report</span>
|
<span>Report</span>
|
||||||
</th>
|
</th>
|
||||||
|
<th data-sortable scope="col" role="columnheader" class="table-field-heading">
|
||||||
|
<span>Delivered</span>
|
||||||
|
</th>
|
||||||
|
<th data-sortable scope="col" role="columnheader" class="table-field-heading">
|
||||||
|
<span>Failed</span>
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -108,6 +114,8 @@
|
|||||||
<span>N/A</span>
|
<span>N/A</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
<td class="table-field delivered">{{ job.delivered_count if job.delivered_count is not none else '0' }}</td>
|
||||||
|
<td class="table-field failed">{{ job.failed_count if job.failed_count is not none else '0' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -121,6 +129,21 @@
|
|||||||
<p><b>Note: </b>Report data is only available for 7 days after your message has been sent</p>
|
<p><b>Note: </b>Report data is only available for 7 days after your message has been sent</p>
|
||||||
</div>
|
</div>
|
||||||
{{show_pagination}}
|
{{show_pagination}}
|
||||||
|
{% if current_user.has_permissions('view_activity') %}
|
||||||
|
<h2 class="line-height-sans-2 margin-bottom-0 margin-top-4">Download recent reports</h2>
|
||||||
|
<p class="font-body-sm">
|
||||||
|
<a href="{{ download_link_one_day }}" download="download" class="usa-link">Download all data last 24 hours (<abbr title="Comma separated values">CSV</abbr>)</a>
|
||||||
|
</p>
|
||||||
|
<p class="font-body-sm">
|
||||||
|
<a href="{{ download_link_three_day }}" download="download" class="usa-link">Download all data last 3 days (<abbr title="Comma separated values">CSV</abbr>)</a>
|
||||||
|
 
|
||||||
|
</p>
|
||||||
|
<p class="font-body-sm">
|
||||||
|
<a href="{{ download_link_five_day }}" download="download" class="usa-link">Download all data last 5 days (<abbr title="Comma separated values">CSV</abbr>)</a>
|
||||||
|
</p>
|
||||||
|
<p class="font-body-sm">
|
||||||
|
<a href="{{ download_link_seven_day }}" download="download" class="usa-link">Download all data last 7 days (<abbr title="Comma separated values">CSV</abbr>)</a>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -23,15 +23,11 @@
|
|||||||
{{ ajax_block(partials, updates_url, 'inbox') }}
|
{{ ajax_block(partials, updates_url, 'inbox') }}
|
||||||
|
|
||||||
<div id="totalMessageChartContainer" data-sms-sent="{{ sms_sent }}" data-sms-allowance-remaining="{{ sms_allowance_remaining }}">
|
<div id="totalMessageChartContainer" data-sms-sent="{{ sms_sent }}" data-sms-allowance-remaining="{{ sms_allowance_remaining }}">
|
||||||
<h2 id="chartTitle">Total Messages</h2>
|
<h2 id="chartTitle">Total messages</h2>
|
||||||
<svg id="totalMessageChart"></svg>
|
<svg id="totalMessageChart"></svg>
|
||||||
<div id="message"></div>
|
<div id="message"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="totalMessageTable" class="margin-0"></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">
|
<h2 class="line-height-sans-2 margin-bottom-0 margin-top-4">
|
||||||
Activity snapshot
|
Activity snapshot
|
||||||
@@ -47,7 +43,7 @@
|
|||||||
</form>
|
</form>
|
||||||
<div id="activityChart">
|
<div id="activityChart">
|
||||||
<div class="chart-header">
|
<div class="chart-header">
|
||||||
<div class="chart-subtitle">{{ current_service.name }} - Last 7 Days</div>
|
<div class="chart-subtitle">{{ current_service.name }} - last 7 days</div>
|
||||||
<div class="chart-legend" aria-label="Legend"></div>
|
<div class="chart-legend" aria-label="Legend"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-container" id="weeklyChart"></div>
|
<div class="chart-container" id="weeklyChart"></div>
|
||||||
@@ -58,75 +54,87 @@
|
|||||||
|
|
||||||
{% if current_user.has_permissions('manage_service') %}{% endif %}
|
{% if current_user.has_permissions('manage_service') %}{% endif %}
|
||||||
|
|
||||||
{{ ajax_block(partials, updates_url, 'template-statistics') }}
|
<div class="table-container">
|
||||||
<h2 class="margin-top-4 margin-bottom-1">Recent Batches</h2>
|
<div id="table1" class="table-overflow-x-auto hidden">
|
||||||
<div class="table-overflow-x-auto">
|
<h2 class="margin-top-4 margin-bottom-1">My activity</h2>
|
||||||
<table class="usa-table usa-table--borderless job-table">
|
<table class="usa-table job-table">
|
||||||
<thead class="table-field-headings">
|
<thead class="table-field-headings">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" class="table-field-heading-first">
|
<th scope="col" class="table-field-heading-first" id="jobId"><span>Job ID#</span></th>
|
||||||
<span>File name</span>
|
<th data-sortable scope="col" class="table-field-heading"><span>Template</span></th>
|
||||||
</th>
|
<th data-sortable scope="col" class="table-field-heading"><span>Job status</span></th>
|
||||||
<th scope="col" class="table-field-heading">
|
<th data-sortable scope="col" class="table-field-heading"><span># of Recipients</span></th>
|
||||||
<span>Template</span>
|
</tr>
|
||||||
</th>
|
</thead>
|
||||||
<th scope="col" class="table-field-heading">
|
<tbody>
|
||||||
<span>Job status</span>
|
{% if jobs %}
|
||||||
</th>
|
{% for job in jobs[:5] %}
|
||||||
<th scope="col" class="table-field-heading">
|
{% if job.created_by.name == current_user.name %}
|
||||||
<span>Sender</span>
|
{% set notification = job.notifications[0] %}
|
||||||
</th>
|
<tr id="{{ job.job_id }}">
|
||||||
<th scope="col" class="table-field-heading">
|
<td class="table-field jobid" scope="row" role="rowheader">
|
||||||
<span># of Recipients</span>
|
<a class="usa-link" href="{{ job.view_job_link }}">
|
||||||
</th>
|
{{ job.job_id[:8] if job.job_id else 'Manually entered number' }}
|
||||||
<th scope="col" class="table-field-heading">
|
</a>
|
||||||
<span>Report</span>
|
</td>
|
||||||
</th>
|
<td class="table-field template">{{ job.template_name }}</td>
|
||||||
</tr>
|
<td class="table-field time-sent">Sent on
|
||||||
</thead>
|
{{ (job.processing_finished if job.processing_finished else job.processing_started
|
||||||
<tbody>
|
if job.processing_started else job.created_at)|format_datetime_table }}
|
||||||
{% if jobs %}
|
</td>
|
||||||
{% for job in jobs[:5] %}
|
<td class="table-field count-of-recipients">{{ job.notification_count }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<tr class="table-row">
|
||||||
|
<td class="table-empty-message" colspan="10">No batched job messages found  (messages are kept for {{ service_data_retention_days }} days).</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="table2" class="table-overflow-x-auto visible">
|
||||||
|
<h2 class="margin-top-4 margin-bottom-1">Service activity</h2>
|
||||||
|
<table class="usa-table job-table">
|
||||||
|
<thead class="table-field-headings">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" role="columnheader" class="table-field-heading-first" id="jobId"><span>Job ID#</span></th>
|
||||||
|
<th data-sortable scope="col" role="columnheader" class="table-field-heading"><span>Template</span></th>
|
||||||
|
<th data-sortable scope="col" role="columnheader" class="table-field-heading"><span>Job status</span></th>
|
||||||
|
<th data-sortable scope="col" role="columnheader" class="table-field-heading"><span>Sender</span></th>
|
||||||
|
<th data-sortable scope="col" role="columnheader" class="table-field-heading"><span># of Recipients</span></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% if jobs %}
|
||||||
|
{% for job in jobs[:5] %}
|
||||||
{% set notification = job.notifications[0] %}
|
{% set notification = job.notifications[0] %}
|
||||||
<tr id="{{ job.job_id }}">
|
<tr id="{{ job.job_id }}">
|
||||||
<td class="table-field file-name">
|
<td class="table-field jobid" scope="row" role="rowheader">
|
||||||
{{ job.original_file_name[:12] if job.original_file_name else 'Manually entered number'}}
|
<a class="usa-link" href="{{ job.view_job_link }}">
|
||||||
<br>
|
{{ job.job_id[:8] if job.job_id else 'Manually entered number' }}
|
||||||
<a class="usa-link file-list-filename" href="{{ job.view_job_link }}">View Batch</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="table-field template">
|
<td class="table-field template">{{ job.template_name }}</td>
|
||||||
{{ job.template_name }}
|
<td class="table-field time-sent">Sent on
|
||||||
</td>
|
|
||||||
<td class="table-field time-sent">
|
|
||||||
{{ (job.processing_finished if job.processing_finished else job.processing_started
|
{{ (job.processing_finished if job.processing_finished else job.processing_started
|
||||||
if job.processing_started else job.created_at)|format_datetime_table }}
|
if job.processing_started else job.created_at)|format_datetime_table }}
|
||||||
</td>
|
</td>
|
||||||
<td class="table-field sender">
|
<td class="table-field sender">{{ job.created_by.name }}</td>
|
||||||
{{ job.created_by.name }}
|
<td class="table-field count-of-recipients">{{ job.notification_count }}</td>
|
||||||
</td>
|
|
||||||
<td class="table-field count-of-recipients">
|
|
||||||
{{ job.notification_count}}
|
|
||||||
</td>
|
|
||||||
<td class="table-field report">
|
|
||||||
{% if job.time_left != "Data no longer available" %}
|
|
||||||
<a class="usa-link file-list-filename" href="{{ job.download_link }}">Download</a>
|
|
||||||
<span class="usa-hint">{{ job.time_left }}</span>
|
|
||||||
{% elif job %}
|
|
||||||
<span>{{ job.time_left }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr class="table-row">
|
<tr class="table-row">
|
||||||
<td class="table-empty-message" colspan="10">No batched job messages found  (messages are kept for {{ service_data_retention_days }} days).</td>
|
<td class="table-empty-message" colspan="10">No batched job messages found  (messages are kept for {{ service_data_retention_days }} days).</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>Recent templates</h2>
|
|
||||||
{{ ajax_block(partials, updates_url, 'template-statistics') }}
|
{{ ajax_block(partials, updates_url, 'template-statistics') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
{% from "components/table.html" import list_table, field, right_aligned_field_heading, row_heading, spark_bar_field %}
|
{% from "components/table.html" import list_table, field, right_aligned_field_heading, row_heading, spark_bar_field %}
|
||||||
|
|
||||||
<div class="ajax-block-container">
|
<div class="ajax-block-container">
|
||||||
{% if template_statistics|length > 1 %}
|
{% if template_statistics|length > 0 %}
|
||||||
|
<h2 class="margin-top-4 margin-bottom-1">Recent templates</h2>
|
||||||
<div class='template-statistics-table table-overflow-x-auto'>
|
<div class='template-statistics-table table-overflow-x-auto'>
|
||||||
{% call(item, row_number) list_table(
|
{% call(item, row_number) list_table(
|
||||||
template_statistics,
|
template_statistics,
|
||||||
|
|||||||
@@ -31,4 +31,8 @@
|
|||||||
<p>
|
<p>
|
||||||
<a class="usa-link" href="{{ url_for('main.get_users_report') }}">Users Report</a>
|
<a class="usa-link" href="{{ url_for('main.get_users_report') }}">Users Report</a>
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
<a class="usa-link" href="{{ url_for('main.download_all_users') }}">Download All Users</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
### Setting Up Environment Variables for Local Development to the Staging API
|
||||||
|
|
||||||
|
When you’re working locally, you can point your local admin repo to the staging API and use that to help debug
|
||||||
|
issues with the staging data set. To do this, you’ll need to modify your .env file for the admin project and
|
||||||
|
include the following new environment variables:
|
||||||
|
|
||||||
|
- `ADMIN_CLIENT_SECRET`
|
||||||
|
- `ADMIN_CLIENT_USERNAME`
|
||||||
|
- `DANGEROUS_SALT`
|
||||||
|
- `SECRET_KEY`
|
||||||
|
|
||||||
|
Additionally, update `API_HOST_NAME` and `NOTIFY_ENVIRONMENT`:
|
||||||
|
|
||||||
|
1. Change `API_HOST_NAME` to `API_HOST_NAME=https://notify-api-staging.app.cloud.gov`
|
||||||
|
2. Change `NOTIFY_ENVIRONMENT` to `NOTIFY_ENVIRONMENT=staging`
|
||||||
|
|
||||||
|
### Retrieving Environment Variables for Staging
|
||||||
|
|
||||||
|
You can retrieve the values needed for these by using the `cf` CLI (Cloud Foundry CLI tool) and making sure
|
||||||
|
you’re targeting the `notify-staging` space.
|
||||||
|
|
||||||
|
1. `cf login -a [api.fr.cloud.gov](http://api.fr.cloud.gov/) --sso`
|
||||||
|
2. select `notify-staging`
|
||||||
|
3. `cf env notify-admin-staging`
|
||||||
|
|
||||||
|
By pointing your local environment to staging, it should mirror what's in staging.
|
||||||
+55
-181
@@ -1,15 +1,10 @@
|
|||||||
// GULPFILE
|
const { src, dest, series } = require('gulp');
|
||||||
// - - - - - - - - - - - - - - -
|
const rollup = require('@rollup/stream');
|
||||||
// This file processes all of the assets in the "src" folder
|
const rollupPluginCommonjs = require('@rollup/plugin-commonjs');
|
||||||
// and outputs the finished files in the "dist" folder.
|
const rollupPluginNodeResolve = require('@rollup/plugin-node-resolve');
|
||||||
|
const source = require('vinyl-source-stream');
|
||||||
// 1. LIBRARIES
|
const buffer = require('vinyl-buffer');
|
||||||
// - - - - - - - - - - - - - - -
|
const gulpMerge = require('gulp-merge');
|
||||||
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 uswds = require("@uswds/compile");
|
const uswds = require("@uswds/compile");
|
||||||
|
|
||||||
const plugins = {};
|
const plugins = {};
|
||||||
@@ -19,11 +14,8 @@ plugins.cleanCSS = require('gulp-clean-css');
|
|||||||
plugins.concat = require('gulp-concat');
|
plugins.concat = require('gulp-concat');
|
||||||
plugins.jshint = require('gulp-jshint');
|
plugins.jshint = require('gulp-jshint');
|
||||||
plugins.prettyerror = require('gulp-prettyerror');
|
plugins.prettyerror = require('gulp-prettyerror');
|
||||||
plugins.rollup = require('gulp-better-rollup')
|
|
||||||
plugins.uglify = require('gulp-uglify');
|
plugins.uglify = require('gulp-uglify');
|
||||||
|
|
||||||
// 2. CONFIGURATION
|
|
||||||
// - - - - - - - - - - - - - - -
|
|
||||||
const paths = {
|
const paths = {
|
||||||
src: 'app/assets/',
|
src: 'app/assets/',
|
||||||
dist: 'app/static/',
|
dist: 'app/static/',
|
||||||
@@ -31,63 +23,25 @@ const paths = {
|
|||||||
toolkit: 'node_modules/govuk_frontend_toolkit/',
|
toolkit: 'node_modules/govuk_frontend_toolkit/',
|
||||||
govuk_frontend: 'node_modules/govuk-frontend/'
|
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 = () => {
|
const javascripts = () => {
|
||||||
// JS from third-party sources
|
const vendored = rollup({
|
||||||
// We assume none of it will need to pass through Babel
|
input: paths.src + 'javascripts/modules/all.mjs',
|
||||||
const vendored = src(paths.src + 'javascripts/modules/all.mjs')
|
plugins: [
|
||||||
// Use Rollup to combine all JS in JS module format into a Immediately Invoked Function
|
rollupPluginNodeResolve({
|
||||||
// Expression (IIFE) to:
|
mainFields: ['module', 'main']
|
||||||
// - deliver it in one bundle
|
}),
|
||||||
// - allow it to run in browsers without support for JS Modules
|
rollupPluginCommonjs({
|
||||||
.pipe(plugins.rollup(
|
include: 'node_modules/**'
|
||||||
{
|
})
|
||||||
plugins: [
|
],
|
||||||
// determine module entry points from either 'module' or 'main' fields in package.json
|
output: {
|
||||||
rollupPluginNodeResolve({
|
format: 'iife',
|
||||||
mainFields: ['module', 'main']
|
name: 'GOVUK'
|
||||||
}),
|
}
|
||||||
// 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
|
.pipe(source('all.mjs'))
|
||||||
rollupPluginCommonjs({
|
.pipe(buffer())
|
||||||
include: 'node_modules/**'
|
|
||||||
})
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
format: 'iife',
|
|
||||||
name: 'GOVUK'
|
|
||||||
}
|
|
||||||
))
|
|
||||||
// return a stream which pipes these files before the JS modules bundle
|
|
||||||
.pipe(plugins.addSrc.prepend([
|
.pipe(plugins.addSrc.prepend([
|
||||||
paths.npm + 'hogan.js/dist/hogan-3.0.2.js',
|
paths.npm + 'hogan.js/dist/hogan-3.0.2.js',
|
||||||
paths.npm + 'jquery/dist/jquery.min.js',
|
paths.npm + 'jquery/dist/jquery.min.js',
|
||||||
@@ -95,11 +49,9 @@ const javascripts = () => {
|
|||||||
paths.npm + 'timeago/jquery.timeago.js',
|
paths.npm + 'timeago/jquery.timeago.js',
|
||||||
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 + 'd3/dist/d3.min.js'
|
paths.npm + 'd3/dist/d3.min.js'
|
||||||
]));
|
]));
|
||||||
|
|
||||||
// JS local to this application
|
|
||||||
const local = src([
|
const local = src([
|
||||||
paths.toolkit + 'javascripts/govuk/modules.js',
|
paths.toolkit + 'javascripts/govuk/modules.js',
|
||||||
paths.toolkit + 'javascripts/govuk/show-hide-content.js',
|
paths.toolkit + 'javascripts/govuk/show-hide-content.js',
|
||||||
@@ -124,128 +76,50 @@ const javascripts = () => {
|
|||||||
paths.src + 'javascripts/timeoutPopup.js',
|
paths.src + 'javascripts/timeoutPopup.js',
|
||||||
paths.src + 'javascripts/date.js',
|
paths.src + 'javascripts/date.js',
|
||||||
paths.src + 'javascripts/loginAlert.js',
|
paths.src + 'javascripts/loginAlert.js',
|
||||||
|
paths.src + 'javascripts/main.js',
|
||||||
paths.src + 'javascripts/totalMessagesChart.js',
|
paths.src + 'javascripts/totalMessagesChart.js',
|
||||||
paths.src + 'javascripts/activityChart.js',
|
paths.src + 'javascripts/activityChart.js',
|
||||||
paths.src + 'javascripts/main.js',
|
|
||||||
])
|
])
|
||||||
.pipe(plugins.prettyerror())
|
.pipe(plugins.prettyerror())
|
||||||
.pipe(plugins.babel({
|
.pipe(plugins.babel({
|
||||||
presets: ['@babel/preset-env']
|
presets: ['@babel/preset-env']
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// return single stream of all vinyl objects piped from the end of the vendored stream, then
|
return gulpMerge(vendored, local)
|
||||||
// those from the end of the local stream
|
|
||||||
return streamqueue({ objectMode: true }, vendored, local)
|
|
||||||
.pipe(plugins.uglify())
|
.pipe(plugins.uglify())
|
||||||
.pipe(plugins.concat('all.js'))
|
.pipe(plugins.concat('all.js'))
|
||||||
.pipe(dest(paths.dist + 'javascripts/'))
|
.pipe(dest(paths.dist + 'javascripts/'));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Task to copy `gtm_head.js`
|
||||||
|
const copyGtmHead = () => {
|
||||||
|
return src(paths.src + 'js/gtm_head.js')
|
||||||
|
.pipe(dest(paths.dist + 'js/'));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Task to copy images
|
||||||
|
const copyImages = () => {
|
||||||
|
return src(paths.src + 'images/**/*', { encoding: false })
|
||||||
|
.pipe(dest(paths.dist + 'images/'));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// Copy images
|
// Configure USWDS paths
|
||||||
|
|
||||||
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/'))
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
*/
|
|
||||||
uswds.settings.version = 3;
|
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';
|
||||||
|
|
||||||
/**
|
// Task to compile USWDS styles
|
||||||
* Path settings
|
const styles = async () => {
|
||||||
* Set as many as you need
|
await uswds.compile();
|
||||||
*/
|
};
|
||||||
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 copy USWDS assets
|
||||||
* Exports
|
const copyAssets = async () => {
|
||||||
* Add as many as you need
|
await uswds.copyAssets();
|
||||||
*/
|
};
|
||||||
exports.init = uswds.init;
|
|
||||||
exports.compile = uswds.compile;
|
exports.default = series(styles, javascripts, copyGtmHead, copyImages, copyAssets);
|
||||||
exports.copyAll = uswds.copyAll;
|
|
||||||
exports.watch = uswds.watch;
|
|
||||||
exports.copyAssets = uswds.copyAssets;
|
|
||||||
|
|||||||
Generated
+1210
-523
File diff suppressed because it is too large
Load Diff
+16
-18
@@ -25,49 +25,47 @@
|
|||||||
"graceful-fs": "^4.2.11"
|
"graceful-fs": "^4.2.11"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@uswds/uswds": "^3.8.1",
|
"@rollup/plugin-commonjs": "^26.0.1",
|
||||||
|
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||||
|
"@rollup/stream": "^3.0.1",
|
||||||
|
"@uswds/uswds": "^3.8.2",
|
||||||
"cbor-js": "0.1.0",
|
"cbor-js": "0.1.0",
|
||||||
"d3": "^7.9.0",
|
"d3": "^7.9.0",
|
||||||
"govuk_frontend_toolkit": "^9.0.1",
|
"govuk_frontend_toolkit": "^9.0.1",
|
||||||
"govuk-frontend": "2.13.0",
|
"govuk-frontend": "2.13.0",
|
||||||
|
"gulp-merge": "^0.1.1",
|
||||||
"hogan": "1.0.2",
|
"hogan": "1.0.2",
|
||||||
"jquery": "3.7.1",
|
"jquery": "3.7.1",
|
||||||
"morphdom": "^2.7.3",
|
"morphdom": "^2.7.4",
|
||||||
"python": "^0.0.4",
|
"python": "^0.0.4",
|
||||||
"query-command-supported": "1.0.0",
|
"query-command-supported": "1.0.0",
|
||||||
"sass-embedded": "^1.77.5",
|
"sass-embedded": "^1.77.8",
|
||||||
"socket.io-client": "^4.2.0",
|
|
||||||
"textarea-caret": "3.1.0",
|
"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": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.24.7",
|
"@babel/core": "^7.25.2",
|
||||||
"@babel/preset-env": "^7.24.7",
|
"@babel/preset-env": "^7.25.3",
|
||||||
"@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": "^5.0.0",
|
"gulp": "^5.0.0",
|
||||||
"gulp-add-src": "^1.0.0",
|
"gulp-add-src": "^1.0.0",
|
||||||
"gulp-babel": "8.0.0",
|
"gulp-babel": "8.0.0",
|
||||||
"gulp-better-rollup": "4.0.1",
|
|
||||||
"gulp-clean-css": "4.3.0",
|
"gulp-clean-css": "4.3.0",
|
||||||
"gulp-concat": "2.6.1",
|
"gulp-concat": "^2.6.1",
|
||||||
"gulp-include": "2.4.1",
|
"gulp-include": "2.4.1",
|
||||||
"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",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"jest": "29.7.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",
|
||||||
"jshint-stylish": "2.2.1",
|
"jshint-stylish": "2.2.1",
|
||||||
"rollup": "1.32.1",
|
"rollup": "^4.20.0",
|
||||||
"rollup-plugin-commonjs": "10.1.0",
|
"rollup-plugin-commonjs": "10.1.0",
|
||||||
"rollup-plugin-node-resolve": "5.2.0",
|
"rollup-plugin-node-resolve": "5.2.0"
|
||||||
"streamqueue": "1.1.2"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"sass-embedded-linux-x64": "^1.77.8"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+9
-119
@@ -85,17 +85,6 @@ charset-normalizer = ["charset-normalizer"]
|
|||||||
html5lib = ["html5lib"]
|
html5lib = ["html5lib"]
|
||||||
lxml = ["lxml"]
|
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]]
|
[[package]]
|
||||||
name = "black"
|
name = "black"
|
||||||
version = "24.4.2"
|
version = "24.4.2"
|
||||||
@@ -201,13 +190,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "botocore"
|
name = "botocore"
|
||||||
version = "1.34.150"
|
version = "1.34.156"
|
||||||
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.150-py3-none-any.whl", hash = "sha256:b988d47f4d502df85befce11a48002421e4e6ea4289997b5e0261bac5fa76ce6"},
|
{file = "botocore-1.34.156-py3-none-any.whl", hash = "sha256:c48f8c8996216dfdeeb0aa6d3c0f2c7ae25234766434a2ea3e57bdc08494bdda"},
|
||||||
{file = "botocore-1.34.150.tar.gz", hash = "sha256:4d23387e0f076d87b637a2a35c0ff2b8daca16eace36b63ce27f65630c6b375a"},
|
{file = "botocore-1.34.156.tar.gz", hash = "sha256:5d1478c41ab9681e660b3322432fe09c4055759c317984b7b8d3af9557ff769a"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -216,7 +205,7 @@ python-dateutil = ">=2.1,<3.0.0"
|
|||||||
urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""}
|
urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""}
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
crt = ["awscrt (==0.20.11)"]
|
crt = ["awscrt (==0.21.2)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cachecontrol"
|
name = "cachecontrol"
|
||||||
@@ -893,24 +882,6 @@ redis = ">=2.7.6"
|
|||||||
dev = ["coverage", "pre-commit", "pytest", "pytest-mock"]
|
dev = ["coverage", "pre-commit", "pytest", "pytest-mock"]
|
||||||
tests = ["coverage", "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]]
|
[[package]]
|
||||||
name = "flask-talisman"
|
name = "flask-talisman"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -1059,17 +1030,6 @@ setproctitle = ["setproctitle"]
|
|||||||
testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"]
|
testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"]
|
||||||
tornado = ["tornado (>=0.2)"]
|
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]]
|
[[package]]
|
||||||
name = "html5lib"
|
name = "html5lib"
|
||||||
version = "1.1"
|
version = "1.1"
|
||||||
@@ -2409,25 +2369,6 @@ files = [
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
cli = ["click (>=5.0)"]
|
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]]
|
[[package]]
|
||||||
name = "python-json-logger"
|
name = "python-json-logger"
|
||||||
version = "2.0.7"
|
version = "2.0.7"
|
||||||
@@ -2456,26 +2397,6 @@ text-unidecode = ">=1.3"
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
unidecode = ["Unidecode (>=1.1.1)"]
|
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]]
|
[[package]]
|
||||||
name = "pytz"
|
name = "pytz"
|
||||||
version = "2024.1"
|
version = "2024.1"
|
||||||
@@ -2566,17 +2487,17 @@ toml = ["tomli (>=2.0.1)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redis"
|
name = "redis"
|
||||||
version = "5.0.7"
|
version = "5.0.8"
|
||||||
description = "Python client for Redis database and key-value store"
|
description = "Python client for Redis database and key-value store"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "redis-5.0.7-py3-none-any.whl", hash = "sha256:0e479e24da960c690be5d9b96d21f7b918a98c0cf49af3b6fafaa0753f93a0db"},
|
{file = "redis-5.0.8-py3-none-any.whl", hash = "sha256:56134ee08ea909106090934adc36f65c9bcbbaecea5b21ba704ba6fb561f8eb4"},
|
||||||
{file = "redis-5.0.7.tar.gz", hash = "sha256:8f611490b93c8109b50adc317b31bfd84fff31def3475b92e7e80bf39f48175b"},
|
{file = "redis-5.0.8.tar.gz", hash = "sha256:0c5b10d387568dfe0698c6fad6615750c24170e548ca2deac10c649d463e9870"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.extras]
|
[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)"]
|
ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2825,23 +2746,6 @@ numpy = ">=1.14,<3"
|
|||||||
docs = ["matplotlib", "numpydoc (==1.1.*)", "sphinx", "sphinx-book-theme", "sphinx-remove-toctrees"]
|
docs = ["matplotlib", "numpydoc (==1.1.*)", "sphinx", "sphinx-book-theme", "sphinx-remove-toctrees"]
|
||||||
test = ["pytest", "pytest-cov"]
|
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]]
|
[[package]]
|
||||||
name = "six"
|
name = "six"
|
||||||
version = "1.16.0"
|
version = "1.16.0"
|
||||||
@@ -3019,20 +2923,6 @@ MarkupSafe = ">=2.1.1"
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
watchdog = ["watchdog (>=2.3)"]
|
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]]
|
[[package]]
|
||||||
name = "wtforms"
|
name = "wtforms"
|
||||||
version = "3.1.2"
|
version = "3.1.2"
|
||||||
@@ -3091,4 +2981,4 @@ files = [
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.12.2"
|
python-versions = "^3.12.2"
|
||||||
content-hash = "b271104f669ce0a8e78fb09299b61cf0502cc81a18213dda00f77c759b6e0209"
|
content-hash = "2c1efc5e8d38c709aec2bc3aa39f96c82a58df13bcbf1912b27c4c26b7b4fc9d"
|
||||||
|
|||||||
+2
-3
@@ -39,7 +39,7 @@ 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.150"
|
boto3 = "^1.34.150"
|
||||||
botocore = "^1.34.150"
|
botocore = "^1.34.156"
|
||||||
cachetools = "^5.4.0"
|
cachetools = "^5.4.0"
|
||||||
cffi = "^1.16.0"
|
cffi = "^1.16.0"
|
||||||
cryptography = "^43.0.0"
|
cryptography = "^43.0.0"
|
||||||
@@ -52,7 +52,7 @@ ordered-set = "^4.1.0"
|
|||||||
phonenumbers = "^8.13.40"
|
phonenumbers = "^8.13.40"
|
||||||
pycparser = "^2.22"
|
pycparser = "^2.22"
|
||||||
python-json-logger = "^2.0.7"
|
python-json-logger = "^2.0.7"
|
||||||
redis = "^5.0.7"
|
redis = "^5.0.8"
|
||||||
regex = "^2024.7.24"
|
regex = "^2024.7.24"
|
||||||
s3transfer = "^0.10.2"
|
s3transfer = "^0.10.2"
|
||||||
shapely = "^2.0.5"
|
shapely = "^2.0.5"
|
||||||
@@ -68,7 +68,6 @@ requests = "^2.32.3"
|
|||||||
six = "^1.16.0"
|
six = "^1.16.0"
|
||||||
urllib3 = "^2.2.2"
|
urllib3 = "^2.2.2"
|
||||||
webencodings = "^0.5.1"
|
webencodings = "^0.5.1"
|
||||||
flask-socketio = "^5.3.6"
|
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
|||||||
@@ -59,10 +59,10 @@ module "api_network_route" {
|
|||||||
module "domain" {
|
module "domain" {
|
||||||
source = "github.com/GSA-TTS/terraform-cloudgov//domain?ref=v1.0.0"
|
source = "github.com/GSA-TTS/terraform-cloudgov//domain?ref=v1.0.0"
|
||||||
|
|
||||||
cf_org_name = local.cf_org_name
|
cf_org_name = local.cf_org_name
|
||||||
cf_space_name = local.cf_space_name
|
cf_space_name = local.cf_space_name
|
||||||
app_name_or_id = "${local.app_name}-${local.env}"
|
app_name_or_id = "${local.app_name}-${local.env}"
|
||||||
name = "${local.app_name}-domain-${local.env}"
|
name = "${local.app_name}-domain-${local.env}"
|
||||||
cdn_plan_name = "domain"
|
cdn_plan_name = "domain"
|
||||||
domain_name = "beta.notify.gov"
|
domain_name = "beta.notify.gov"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -300,9 +300,26 @@ def test_accepting_invite_removes_invite_from_session(
|
|||||||
|
|
||||||
client_request.login(user)
|
client_request.login(user)
|
||||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||||
|
date_range = {"start_date": "2024-01-01", "days": 7}
|
||||||
|
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
"app.notification_api_client.get_notifications_for_service",
|
"app.main.views.dashboard.get_daily_stats",
|
||||||
return_value=FAKE_ONE_OFF_NOTIFICATION,
|
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(
|
page = client_request.get(
|
||||||
"main.accept_invite",
|
"main.accept_invite",
|
||||||
|
|||||||
@@ -3,11 +3,9 @@ import json
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from flask import Flask, url_for
|
from flask import url_for
|
||||||
from flask_socketio import SocketIO, SocketIOTestClient
|
|
||||||
from freezegun import freeze_time
|
from freezegun import freeze_time
|
||||||
|
|
||||||
from app import create_app
|
|
||||||
from app.main.views.dashboard import (
|
from app.main.views.dashboard import (
|
||||||
aggregate_notifications_stats,
|
aggregate_notifications_stats,
|
||||||
aggregate_status_types,
|
aggregate_status_types,
|
||||||
@@ -15,8 +13,6 @@ from app.main.views.dashboard import (
|
|||||||
format_monthly_stats_to_list,
|
format_monthly_stats_to_list,
|
||||||
get_dashboard_totals,
|
get_dashboard_totals,
|
||||||
get_tuples_of_financial_years,
|
get_tuples_of_financial_years,
|
||||||
handle_fetch_daily_stats,
|
|
||||||
handle_fetch_daily_stats_by_user,
|
|
||||||
)
|
)
|
||||||
from tests import (
|
from tests import (
|
||||||
organization_json,
|
organization_json,
|
||||||
@@ -27,8 +23,6 @@ from tests import (
|
|||||||
from tests.conftest import (
|
from tests.conftest import (
|
||||||
ORGANISATION_ID,
|
ORGANISATION_ID,
|
||||||
SERVICE_ONE_ID,
|
SERVICE_ONE_ID,
|
||||||
SERVICE_TWO_ID,
|
|
||||||
USER_ONE_ID,
|
|
||||||
create_active_caseworking_user,
|
create_active_caseworking_user,
|
||||||
create_active_user_view_permissions,
|
create_active_user_view_permissions,
|
||||||
normalize_spaces,
|
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(
|
@pytest.mark.parametrize(
|
||||||
"user",
|
"user",
|
||||||
@@ -222,8 +232,12 @@ def test_get_started(
|
|||||||
)
|
)
|
||||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
"app.notification_api_client.get_notifications_for_service",
|
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||||
return_value=FAKE_ONE_OFF_NOTIFICATION,
|
)
|
||||||
|
|
||||||
|
mocker.patch(
|
||||||
|
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||||
|
return_value=mock_daily_stats_by_user,
|
||||||
)
|
)
|
||||||
page = client_request.get(
|
page = client_request.get(
|
||||||
"main.service_dashboard",
|
"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.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
"app.notification_api_client.get_notifications_for_service",
|
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||||
return_value=FAKE_ONE_OFF_NOTIFICATION,
|
)
|
||||||
|
|
||||||
|
mocker.patch(
|
||||||
|
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||||
|
return_value=mock_daily_stats_by_user,
|
||||||
)
|
)
|
||||||
page = client_request.get(
|
page = client_request.get(
|
||||||
"main.service_dashboard",
|
"main.service_dashboard",
|
||||||
@@ -279,8 +297,12 @@ def test_inbound_messages_not_visible_to_service_without_permissions(
|
|||||||
service_one["permissions"] = []
|
service_one["permissions"] = []
|
||||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
"app.notification_api_client.get_notifications_for_service",
|
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||||
return_value=FAKE_ONE_OFF_NOTIFICATION,
|
)
|
||||||
|
|
||||||
|
mocker.patch(
|
||||||
|
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||||
|
return_value=mock_daily_stats_by_user,
|
||||||
)
|
)
|
||||||
page = client_request.get(
|
page = client_request.get(
|
||||||
"main.service_dashboard",
|
"main.service_dashboard",
|
||||||
@@ -305,6 +327,14 @@ def test_inbound_messages_shows_count_of_messages_when_there_are_messages(
|
|||||||
mock_get_inbound_sms_summary,
|
mock_get_inbound_sms_summary,
|
||||||
):
|
):
|
||||||
service_one["permissions"] = ["inbound_sms"]
|
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(
|
page = client_request.get(
|
||||||
"main.service_dashboard",
|
"main.service_dashboard",
|
||||||
service_id=SERVICE_ONE_ID,
|
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,
|
mock_get_inbound_sms_summary_with_no_messages,
|
||||||
):
|
):
|
||||||
service_one["permissions"] = ["inbound_sms"]
|
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(
|
page = client_request.get(
|
||||||
"main.service_dashboard",
|
"main.service_dashboard",
|
||||||
service_id=SERVICE_ONE_ID,
|
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.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
"app.notification_api_client.get_notifications_for_service",
|
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||||
return_value=FAKE_ONE_OFF_NOTIFICATION,
|
)
|
||||||
|
|
||||||
|
mocker.patch(
|
||||||
|
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||||
|
return_value=mock_daily_stats_by_user,
|
||||||
)
|
)
|
||||||
page = client_request.get(
|
page = client_request.get(
|
||||||
"main.service_dashboard",
|
"main.service_dashboard",
|
||||||
@@ -593,19 +635,11 @@ def test_should_show_recent_templates_on_dashboard(
|
|||||||
headers = [
|
headers = [
|
||||||
header.text.strip() for header in page.find_all("h2") + page.find_all("h1")
|
header.text.strip() for header in page.find_all("h2") + page.find_all("h1")
|
||||||
]
|
]
|
||||||
assert "Total Messages" in headers
|
assert "Total messages" in headers
|
||||||
|
|
||||||
table_rows = page.find_all("tbody")[0].find_all("tr")
|
table_rows = page.find_all("tbody")[0].find_all("tr")
|
||||||
|
|
||||||
assert len(table_rows) == 2
|
assert len(table_rows) == 0
|
||||||
|
|
||||||
assert "two" in table_rows[0].find_all("td")[0].text
|
|
||||||
assert "Email template" in table_rows[0].find_all("td")[0].text
|
|
||||||
assert "200" in table_rows[0].find_all("td")[1].text
|
|
||||||
|
|
||||||
assert "one" in table_rows[1].find_all("td")[0].text
|
|
||||||
assert "Text message template" in table_rows[1].find_all("td")[0].text
|
|
||||||
assert "100" in table_rows[1].find_all("td")[1].text
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@@ -637,8 +671,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.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
"app.notification_api_client.get_notifications_for_service",
|
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||||
return_value=FAKE_ONE_OFF_NOTIFICATION,
|
)
|
||||||
|
|
||||||
|
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)
|
page = client_request.get("main.service_dashboard", service_id=SERVICE_ONE_ID)
|
||||||
main = page.select_one("main").text
|
main = page.select_one("main").text
|
||||||
@@ -792,8 +830,12 @@ def test_should_show_upcoming_jobs_on_dashboard(
|
|||||||
mock_get_inbound_sms_summary,
|
mock_get_inbound_sms_summary,
|
||||||
):
|
):
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
"app.notification_api_client.get_notifications_for_service",
|
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||||
return_value=FAKE_ONE_OFF_NOTIFICATION,
|
)
|
||||||
|
|
||||||
|
mocker.patch(
|
||||||
|
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||||
|
return_value=mock_daily_stats_by_user,
|
||||||
)
|
)
|
||||||
page = client_request.get(
|
page = client_request.get(
|
||||||
"main.service_dashboard",
|
"main.service_dashboard",
|
||||||
@@ -834,6 +876,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.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(
|
page = client_request.get(
|
||||||
"main.service_dashboard",
|
"main.service_dashboard",
|
||||||
service_id=SERVICE_ONE_ID,
|
service_id=SERVICE_ONE_ID,
|
||||||
@@ -857,8 +907,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.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
"app.notification_api_client.get_notifications_for_service",
|
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||||
return_value=FAKE_ONE_OFF_NOTIFICATION,
|
)
|
||||||
|
|
||||||
|
mocker.patch(
|
||||||
|
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||||
|
return_value=mock_daily_stats_by_user,
|
||||||
)
|
)
|
||||||
page = client_request.get(
|
page = client_request.get(
|
||||||
"main.service_dashboard",
|
"main.service_dashboard",
|
||||||
@@ -942,6 +996,14 @@ def test_should_not_show_jobs_on_dashboard_for_users_with_uploads_page(
|
|||||||
mock_get_free_sms_fragment_limit,
|
mock_get_free_sms_fragment_limit,
|
||||||
mock_get_inbound_sms_summary,
|
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(
|
page = client_request.get(
|
||||||
"main.service_dashboard",
|
"main.service_dashboard",
|
||||||
service_id=SERVICE_ONE_ID,
|
service_id=SERVICE_ONE_ID,
|
||||||
@@ -1203,8 +1265,12 @@ def test_menu_send_messages(
|
|||||||
|
|
||||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
"app.notification_api_client.get_notifications_for_service",
|
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||||
return_value=FAKE_ONE_OFF_NOTIFICATION,
|
)
|
||||||
|
|
||||||
|
mocker.patch(
|
||||||
|
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||||
|
return_value=mock_daily_stats_by_user,
|
||||||
)
|
)
|
||||||
page = _test_dashboard_menu(
|
page = _test_dashboard_menu(
|
||||||
client_request,
|
client_request,
|
||||||
@@ -1240,8 +1306,12 @@ def test_menu_manage_service(
|
|||||||
):
|
):
|
||||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
"app.notification_api_client.get_notifications_for_service",
|
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||||
return_value=FAKE_ONE_OFF_NOTIFICATION,
|
)
|
||||||
|
|
||||||
|
mocker.patch(
|
||||||
|
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||||
|
return_value=mock_daily_stats_by_user,
|
||||||
)
|
)
|
||||||
page = _test_dashboard_menu(
|
page = _test_dashboard_menu(
|
||||||
client_request,
|
client_request,
|
||||||
@@ -1277,8 +1347,12 @@ def test_menu_main_settings(
|
|||||||
):
|
):
|
||||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
"app.notification_api_client.get_notifications_for_service",
|
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||||
return_value=FAKE_ONE_OFF_NOTIFICATION,
|
)
|
||||||
|
|
||||||
|
mocker.patch(
|
||||||
|
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||||
|
return_value=mock_daily_stats_by_user,
|
||||||
)
|
)
|
||||||
page = _test_settings_menu(
|
page = _test_settings_menu(
|
||||||
client_request,
|
client_request,
|
||||||
@@ -1313,8 +1387,12 @@ def test_menu_manage_api_keys(
|
|||||||
):
|
):
|
||||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
"app.notification_api_client.get_notifications_for_service",
|
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||||
return_value=FAKE_ONE_OFF_NOTIFICATION,
|
)
|
||||||
|
|
||||||
|
mocker.patch(
|
||||||
|
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||||
|
return_value=mock_daily_stats_by_user,
|
||||||
)
|
)
|
||||||
page = _test_dashboard_menu(
|
page = _test_dashboard_menu(
|
||||||
client_request,
|
client_request,
|
||||||
@@ -1353,8 +1431,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.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
"app.notification_api_client.get_notifications_for_service",
|
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||||
return_value=FAKE_ONE_OFF_NOTIFICATION,
|
)
|
||||||
|
|
||||||
|
mocker.patch(
|
||||||
|
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||||
|
return_value=mock_daily_stats_by_user,
|
||||||
)
|
)
|
||||||
page = _test_dashboard_menu(
|
page = _test_dashboard_menu(
|
||||||
client_request, mocker, platform_admin_user, service_one, []
|
client_request, mocker, platform_admin_user, service_one, []
|
||||||
@@ -1362,7 +1444,7 @@ def test_menu_all_services_for_platform_admin_user(
|
|||||||
page = str(page)
|
page = str(page)
|
||||||
assert url_for("main.choose_template", service_id=service_one["id"]) in page
|
assert url_for("main.choose_template", service_id=service_one["id"]) in page
|
||||||
assert url_for("main.service_settings", service_id=service_one["id"]) in page
|
assert url_for("main.service_settings", service_id=service_one["id"]) in page
|
||||||
assert url_for('main.api_keys', service_id=service_one['id']) not in page
|
assert url_for("main.api_keys", service_id=service_one["id"]) not in page
|
||||||
|
|
||||||
|
|
||||||
def test_route_for_service_permissions(
|
def test_route_for_service_permissions(
|
||||||
@@ -1392,8 +1474,12 @@ def test_route_for_service_permissions(
|
|||||||
)
|
)
|
||||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
"app.notification_api_client.get_notifications_for_service",
|
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||||
return_value=FAKE_ONE_OFF_NOTIFICATION,
|
)
|
||||||
|
|
||||||
|
mocker.patch(
|
||||||
|
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||||
|
return_value=mock_daily_stats_by_user,
|
||||||
)
|
)
|
||||||
validate_route_permission(
|
validate_route_permission(
|
||||||
mocker,
|
mocker,
|
||||||
@@ -1537,8 +1623,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.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
"app.notification_api_client.get_notifications_for_service",
|
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||||
return_value=FAKE_ONE_OFF_NOTIFICATION,
|
)
|
||||||
|
|
||||||
|
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)
|
page = client_request.get("main.service_dashboard", service_id=SERVICE_ONE_ID)
|
||||||
|
|
||||||
@@ -1604,8 +1694,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.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
"app.notification_api_client.get_notifications_for_service",
|
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||||
return_value=FAKE_ONE_OFF_NOTIFICATION,
|
)
|
||||||
|
|
||||||
|
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)
|
page = client_request.get("main.service_dashboard", service_id=SERVICE_ONE_ID)
|
||||||
@@ -1639,10 +1733,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.models.service.Organization")
|
||||||
|
|
||||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
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(
|
mocker.patch(
|
||||||
"app.notification_api_client.get_notifications_for_service",
|
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||||
return_value=FAKE_ONE_OFF_NOTIFICATION,
|
return_value=mock_daily_stats_by_user,
|
||||||
)
|
)
|
||||||
|
|
||||||
page = client_request.get("main.service_dashboard", service_id=SERVICE_ONE_ID)
|
page = client_request.get("main.service_dashboard", service_id=SERVICE_ONE_ID)
|
||||||
@@ -1679,8 +1776,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.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||||
|
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
"app.notification_api_client.get_notifications_for_service",
|
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||||
return_value=FAKE_ONE_OFF_NOTIFICATION,
|
)
|
||||||
|
|
||||||
|
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)
|
client_request.login(platform_admin_user, service_one_json)
|
||||||
@@ -1715,9 +1816,14 @@ def test_breadcrumb_shows_if_service_is_suspended(
|
|||||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||||
|
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
"app.notification_api_client.get_notifications_for_service",
|
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||||
return_value=FAKE_ONE_OFF_NOTIFICATION,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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)
|
page = client_request.get("main.service_dashboard", service_id=SERVICE_ONE_ID)
|
||||||
|
|
||||||
assert "Suspended" in page.select_one(".navigation-service-name").text
|
assert "Suspended" in page.select_one(".navigation-service-name").text
|
||||||
@@ -1748,8 +1854,12 @@ def test_service_dashboard_shows_usage(
|
|||||||
)
|
)
|
||||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
"app.notification_api_client.get_notifications_for_service",
|
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||||
return_value=FAKE_ONE_OFF_NOTIFICATION,
|
)
|
||||||
|
|
||||||
|
mocker.patch(
|
||||||
|
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||||
|
return_value=mock_daily_stats_by_user,
|
||||||
)
|
)
|
||||||
|
|
||||||
service_one["permissions"] = permissions
|
service_one["permissions"] = permissions
|
||||||
@@ -1783,10 +1893,13 @@ def test_service_dashboard_shows_free_allowance(
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
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(
|
mocker.patch(
|
||||||
"app.notification_api_client.get_notifications_for_service",
|
"app.main.views.dashboard.get_daily_stats_by_user",
|
||||||
return_value=FAKE_ONE_OFF_NOTIFICATION,
|
return_value=mock_daily_stats_by_user,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1802,171 +1915,19 @@ def test_service_dashboard_shows_batched_jobs(
|
|||||||
):
|
):
|
||||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
"app.notification_api_client.get_notifications_for_service",
|
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
|
||||||
return_value=FAKE_ONE_OFF_NOTIFICATION,
|
)
|
||||||
|
|
||||||
|
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)
|
page = client_request.get("main.service_dashboard", service_id=SERVICE_ONE_ID)
|
||||||
|
|
||||||
job_table_body = page.find("table", class_="job-table")
|
job_table_body = page.find("table", class_="job-table")
|
||||||
|
|
||||||
rows = job_table_body.find_all("tbody")[0].find_all("tr")
|
rows = job_table_body.find_all("tbody")[0].find_all("tr")
|
||||||
|
|
||||||
|
assert len(rows) == 0
|
||||||
|
|
||||||
assert job_table_body is not None
|
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"
|
|
||||||
|
|||||||
@@ -494,5 +494,5 @@ def test_should_show_message_note(
|
|||||||
|
|
||||||
assert normalize_spaces(page.select_one("main p.notification-status").text) == (
|
assert normalize_spaces(page.select_one("main p.notification-status").text) == (
|
||||||
'Messages are sent immediately to the cell phone carrier, but will remain in "pending" status until we hear '
|
'Messages are sent immediately to the cell phone carrier, but will remain in "pending" status until we hear '
|
||||||
'back from the carrier they have received it and attempted deliver. More information on delivery status.'
|
"back from the carrier they have received it and attempted deliver. More information on delivery status."
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ MOCK_JOBS = {
|
|||||||
"scheduled_for": None,
|
"scheduled_for": None,
|
||||||
"service": "21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3",
|
"service": "21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3",
|
||||||
"service_name": {"name": "Mock Texting Service"},
|
"service_name": {"name": "Mock Texting Service"},
|
||||||
"statistics": [{"count": 1, "status": "sending"}],
|
"statistics": [
|
||||||
|
{"count": 1, "status": "delivered"},
|
||||||
|
{"count": 5, "status": "failed"},
|
||||||
|
],
|
||||||
"template": "6a456418-498c-4c86-b0cd-9403c14a216c",
|
"template": "6a456418-498c-4c86-b0cd-9403c14a216c",
|
||||||
"template_name": "Mock Template Name",
|
"template_name": "Mock Template Name",
|
||||||
"template_type": "sms",
|
"template_type": "sms",
|
||||||
@@ -29,13 +32,13 @@ MOCK_JOBS = {
|
|||||||
"updated_at": "2024-01-25T23:02:25+00:00",
|
"updated_at": "2024-01-25T23:02:25+00:00",
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
'links': {
|
"links": {
|
||||||
'last': '/service/21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3/job?page=3',
|
"last": "/service/21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3/job?page=3",
|
||||||
'next': '/service/21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3/job?page=3',
|
"next": "/service/21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3/job?page=3",
|
||||||
'prev': '/service/21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3/job?page=1'
|
"prev": "/service/21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3/job?page=1",
|
||||||
},
|
},
|
||||||
'page_size': 50,
|
"page_size": 50,
|
||||||
'total': 115
|
"total": 115,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -58,59 +61,77 @@ def test_all_activity(
|
|||||||
|
|
||||||
assert "All activity" in response.text
|
assert "All activity" in response.text
|
||||||
mock_get_page_of_jobs.assert_called_with(SERVICE_ONE_ID, page=current_page)
|
mock_get_page_of_jobs.assert_called_with(SERVICE_ONE_ID, page=current_page)
|
||||||
page = BeautifulSoup(response.data, 'html.parser')
|
page = BeautifulSoup(response.data, "html.parser")
|
||||||
table = page.find('table')
|
table = page.find("table")
|
||||||
assert table is not None, "Table not found in the response"
|
assert table is not None, "Table not found in the response"
|
||||||
|
|
||||||
headers = [th.get_text(strip=True) for th in table.find_all('th')]
|
headers = [th.get_text(strip=True) for th in table.find_all("th")]
|
||||||
expected_headers = ["Job ID#", "Template", "Time sent", "Sender", "Report"]
|
expected_headers = [
|
||||||
|
"Job ID#",
|
||||||
|
"Template",
|
||||||
|
"Time sent",
|
||||||
|
"Sender",
|
||||||
|
"Report",
|
||||||
|
"Delivered",
|
||||||
|
"Failed",
|
||||||
|
]
|
||||||
|
|
||||||
assert headers == expected_headers, f"Expected headers {expected_headers}, but got {headers}"
|
assert (
|
||||||
|
headers == expected_headers
|
||||||
|
), f"Expected headers {expected_headers}, but got {headers}"
|
||||||
|
|
||||||
rows = table.find('tbody').find_all('tr', class_='table-row')
|
rows = table.find("tbody").find_all("tr", class_="table-row")
|
||||||
assert len(rows) == 1, "Expected one job row in the table"
|
assert len(rows) == 1, "Expected one job row in the table"
|
||||||
|
|
||||||
job_row = rows[0]
|
job_row = rows[0]
|
||||||
cells = job_row.find_all('td')
|
cells = job_row.find_all("td")
|
||||||
assert len(cells) == 5, "Expected five columns in the job row"
|
assert len(cells) == 7, "Expected five columns in the job row"
|
||||||
|
|
||||||
job_id_cell = cells[0].find('a').get_text(strip=True)
|
job_id_cell = cells[0].find("a").get_text(strip=True)
|
||||||
|
|
||||||
assert job_id_cell == "55b242b5", f"Expected job ID '55b242b5', but got '{job_id_cell}'"
|
assert (
|
||||||
|
job_id_cell == "55b242b5"
|
||||||
|
), f"Expected job ID '55b242b5', but got '{job_id_cell}'"
|
||||||
template_cell = cells[1].get_text(strip=True)
|
template_cell = cells[1].get_text(strip=True)
|
||||||
assert template_cell == "Mock Template Name", (
|
assert (
|
||||||
f"Expected template 'Mock Template Name', but got '{template_cell}'"
|
template_cell == "Mock Template Name"
|
||||||
)
|
), f"Expected template 'Mock Template Name', but got '{template_cell}'"
|
||||||
time_sent_cell = cells[2].get_text(strip=True)
|
time_sent_cell = cells[2].get_text(strip=True)
|
||||||
assert time_sent_cell == "01-25-2024 at 06:02 PM", (
|
assert (
|
||||||
f"Expected time sent '01-25-2024 at 06:02 PM', but got '{time_sent_cell}'"
|
time_sent_cell == "01-25-2024 at 06:02 PM"
|
||||||
)
|
), f"Expected time sent '01-25-2024 at 06:02 PM', but got '{time_sent_cell}'"
|
||||||
sender_cell = cells[3].get_text(strip=True)
|
sender_cell = cells[3].get_text(strip=True)
|
||||||
assert sender_cell == "mocked_user", f"Expected sender 'mocked_user', but got '{sender_cell}'"
|
assert (
|
||||||
|
sender_cell == "mocked_user"
|
||||||
|
), f"Expected sender 'mocked_user', but got '{sender_cell}'"
|
||||||
|
|
||||||
report_cell = cells[4].find('span').get_text(strip=True)
|
report_cell = cells[4].find("span").get_text(strip=True)
|
||||||
assert report_cell == "N/A", f"Expected report 'N/A', but got '{report_cell}'"
|
assert report_cell == "N/A", f"Expected report 'N/A', but got '{report_cell}'"
|
||||||
|
|
||||||
|
delivered_cell = cells[5].get_text(strip=True)
|
||||||
|
assert (
|
||||||
|
delivered_cell == "1"
|
||||||
|
), f"Expected delivered count '1', but got '{delivered_cell}'"
|
||||||
|
|
||||||
|
failed_cell = cells[6].get_text(strip=True)
|
||||||
|
assert failed_cell == "5", f"Expected failed count '5', but got '{failed_cell}'"
|
||||||
mock_get_page_of_jobs.assert_called_with(SERVICE_ONE_ID, page=current_page)
|
mock_get_page_of_jobs.assert_called_with(SERVICE_ONE_ID, page=current_page)
|
||||||
|
|
||||||
|
|
||||||
def test_all_activity_no_jobs(
|
def test_all_activity_no_jobs(client_request, mocker):
|
||||||
client_request,
|
|
||||||
mocker
|
|
||||||
):
|
|
||||||
current_page = get_page_from_request()
|
current_page = get_page_from_request()
|
||||||
mock_get_page_of_jobs = mocker.patch(
|
mock_get_page_of_jobs = mocker.patch(
|
||||||
"app.job_api_client.get_page_of_jobs",
|
"app.job_api_client.get_page_of_jobs",
|
||||||
return_value={
|
return_value={
|
||||||
"data": [],
|
"data": [],
|
||||||
'links': {
|
"links": {
|
||||||
'last': '/service/21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3/job?page=1',
|
"last": "/service/21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3/job?page=1",
|
||||||
'next': None,
|
"next": None,
|
||||||
'prev': None
|
"prev": None,
|
||||||
},
|
},
|
||||||
'page_size': 50,
|
"page_size": 50,
|
||||||
'total': 0
|
"total": 0,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
response = client_request.get_response(
|
response = client_request.get_response(
|
||||||
"main.all_jobs_activity",
|
"main.all_jobs_activity",
|
||||||
@@ -120,17 +141,17 @@ def test_all_activity_no_jobs(
|
|||||||
|
|
||||||
assert response.status_code == 200, "Request failed"
|
assert response.status_code == 200, "Request failed"
|
||||||
|
|
||||||
page = BeautifulSoup(response.data, 'html.parser')
|
page = BeautifulSoup(response.data, "html.parser")
|
||||||
|
|
||||||
no_jobs_message_td = page.find('td', class_='table-empty-message')
|
no_jobs_message_td = page.find("td", class_="table-empty-message")
|
||||||
assert no_jobs_message_td is not None, "No jobs message not found in the response"
|
assert no_jobs_message_td is not None, "No jobs message not found in the response"
|
||||||
|
|
||||||
expected_message = "No batched job messages found (messages are kept for 7 days)."
|
expected_message = "No batched job messages found (messages are kept for 7 days)."
|
||||||
actual_message = no_jobs_message_td.get_text(strip=True)
|
actual_message = no_jobs_message_td.get_text(strip=True)
|
||||||
|
|
||||||
assert expected_message == actual_message, (
|
assert (
|
||||||
f"Expected message '{expected_message}', but got '{actual_message}'"
|
expected_message == actual_message
|
||||||
)
|
), f"Expected message '{expected_message}', but got '{actual_message}'"
|
||||||
mock_get_page_of_jobs.assert_called_with(SERVICE_ONE_ID, page=current_page)
|
mock_get_page_of_jobs.assert_called_with(SERVICE_ONE_ID, page=current_page)
|
||||||
|
|
||||||
|
|
||||||
@@ -148,17 +169,18 @@ def test_all_activity_pagination(client_request, mocker):
|
|||||||
"processing_started": "2024-01-25T23:02:24+00:00",
|
"processing_started": "2024-01-25T23:02:24+00:00",
|
||||||
"template_name": "Mock Template Name",
|
"template_name": "Mock Template Name",
|
||||||
"original_file_name": "mocked_file.csv",
|
"original_file_name": "mocked_file.csv",
|
||||||
"notification_count": 1
|
"notification_count": 1,
|
||||||
} for i in range(1, 101)
|
}
|
||||||
|
for i in range(1, 101)
|
||||||
],
|
],
|
||||||
'links': {
|
"links": {
|
||||||
'last': '/service/21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3/job?page=2',
|
"last": "/service/21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3/job?page=2",
|
||||||
'next': '/service/21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3/job?page=2',
|
"next": "/service/21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3/job?page=2",
|
||||||
'prev': None
|
"prev": None,
|
||||||
},
|
},
|
||||||
'page_size': 50,
|
"page_size": 50,
|
||||||
'total': 100
|
"total": 100,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
response = client_request.get_response(
|
response = client_request.get_response(
|
||||||
@@ -168,12 +190,12 @@ def test_all_activity_pagination(client_request, mocker):
|
|||||||
)
|
)
|
||||||
mock_get_page_of_jobs.assert_called_with(SERVICE_ONE_ID, page=current_page)
|
mock_get_page_of_jobs.assert_called_with(SERVICE_ONE_ID, page=current_page)
|
||||||
|
|
||||||
page = BeautifulSoup(response.data, 'html.parser')
|
page = BeautifulSoup(response.data, "html.parser")
|
||||||
pagination_controls = page.find_all('li', class_='usa-pagination__item')
|
pagination_controls = page.find_all("li", class_="usa-pagination__item")
|
||||||
assert pagination_controls, "Pagination controls not found in the response"
|
assert pagination_controls, "Pagination controls not found in the response"
|
||||||
|
|
||||||
pagination_texts = [item.get_text(strip=True) for item in pagination_controls]
|
pagination_texts = [item.get_text(strip=True) for item in pagination_controls]
|
||||||
expected_pagination_texts = ['1', '2', 'Next']
|
expected_pagination_texts = ["1", "2", "Next"]
|
||||||
assert pagination_texts == expected_pagination_texts, (
|
assert (
|
||||||
f"Expected pagination controls {expected_pagination_texts}, but got {pagination_texts}"
|
pagination_texts == expected_pagination_texts
|
||||||
)
|
), f"Expected pagination controls {expected_pagination_texts}, but got {pagination_texts}"
|
||||||
|
|||||||
@@ -1254,3 +1254,36 @@ def test_get_daily_sms_provider_volumes_report_calls_api_and_download_data(
|
|||||||
+ "80"
|
+ "80"
|
||||||
+ "\r\n"
|
+ "\r\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_download_all_users(client_request, platform_admin_user, mocker):
|
||||||
|
mocker.patch(
|
||||||
|
"app.main.views.platform_admin.user_api_client.get_all_users_detailed",
|
||||||
|
return_value=[
|
||||||
|
{
|
||||||
|
"name": "Johnny Sokko",
|
||||||
|
"email_address": "j_sokko@unicorn.gov",
|
||||||
|
"mobile_number": "15555555555",
|
||||||
|
"service": "Emperor, Guillotine, Service",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
client_request.login(platform_admin_user)
|
||||||
|
response = client_request.get_response(
|
||||||
|
"main.download_all_users",
|
||||||
|
_data={},
|
||||||
|
_expected_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.content_type == "text/csv; charset=utf-8"
|
||||||
|
assert "attachment" in response.headers["Content-Disposition"]
|
||||||
|
assert "filename" in response.headers["Content-Disposition"]
|
||||||
|
assert "users" in response.headers["Content-Disposition"]
|
||||||
|
|
||||||
|
my_response = response.get_data(as_text=True)
|
||||||
|
|
||||||
|
assert "Johnny Sokko" in my_response
|
||||||
|
assert "Emperor Guillotine Service" in my_response
|
||||||
|
assert "j_sokko@unicorn.gov" in my_response
|
||||||
|
assert "15555555555" in my_response
|
||||||
|
|||||||
@@ -128,9 +128,26 @@ def test_sign_out_user(
|
|||||||
# Check we are logged in
|
# Check we are logged in
|
||||||
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
|
||||||
|
|
||||||
|
date_range = {"start_date": "2024-01-01", "days": 7}
|
||||||
|
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
"app.notification_api_client.get_notifications_for_service",
|
"app.main.views.dashboard.get_daily_stats",
|
||||||
return_value=FAKE_ONE_OFF_NOTIFICATION,
|
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(
|
client_request.get(
|
||||||
|
|||||||
@@ -41,6 +41,26 @@ def test_active_service_can_be_modified(notify_admin, method, user, service):
|
|||||||
assert ret == request.return_value
|
assert ret == request.return_value
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("arg", "expected_result"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"('/user/c5f8a5c9-56d5-4fa9-8c30-3449ae10c072/verify/code',)",
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
("('/user/get-login-gov-user',)", True),
|
||||||
|
(
|
||||||
|
"('/service/blahblahblah',)",
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_is_calling_signin_url(arg, expected_result):
|
||||||
|
api_client = NotifyAdminAPIClient()
|
||||||
|
result = api_client.is_calling_signin_url(arg)
|
||||||
|
assert result == expected_result
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("method", ["put", "post", "delete"])
|
@pytest.mark.parametrize("method", ["put", "post", "delete"])
|
||||||
def test_inactive_service_cannot_be_modified_by_normal_user(
|
def test_inactive_service_cannot_be_modified_by_normal_user(
|
||||||
notify_admin, api_user_active, method
|
notify_admin, api_user_active, method
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ EXCLUDED_ENDPOINTS = tuple(
|
|||||||
"delivery_status_callback",
|
"delivery_status_callback",
|
||||||
"design_content",
|
"design_content",
|
||||||
"documentation",
|
"documentation",
|
||||||
|
"download_all_users",
|
||||||
"download_notifications_csv",
|
"download_notifications_csv",
|
||||||
"download_organization_usage_report",
|
"download_organization_usage_report",
|
||||||
"edit_and_format_messages",
|
"edit_and_format_messages",
|
||||||
@@ -95,6 +96,8 @@ EXCLUDED_ENDPOINTS = tuple(
|
|||||||
"get_users_report",
|
"get_users_report",
|
||||||
"get_daily_volumes",
|
"get_daily_volumes",
|
||||||
"get_daily_sms_provider_volumes",
|
"get_daily_sms_provider_volumes",
|
||||||
|
"get_daily_stats",
|
||||||
|
"get_daily_stats_by_user",
|
||||||
"get_volumes_by_service",
|
"get_volumes_by_service",
|
||||||
"get_example_csv",
|
"get_example_csv",
|
||||||
"get_notifications_as_json",
|
"get_notifications_as_json",
|
||||||
|
|||||||
@@ -36,8 +36,18 @@ def test_generate_previous_next_dict_adds_other_url_args(client_request):
|
|||||||
(500, 50, 1, {"current": 1, "pages": [1, 2, 3, 4, 5, 6, 7, 8, 9], "last": 10}),
|
(500, 50, 1, {"current": 1, "pages": [1, 2, 3, 4, 5, 6, 7, 8, 9], "last": 10}),
|
||||||
(500, 50, 5, {"current": 5, "pages": [1, 2, 3, 4, 5, 6, 7, 8, 9], "last": 10}),
|
(500, 50, 5, {"current": 5, "pages": [1, 2, 3, 4, 5, 6, 7, 8, 9], "last": 10}),
|
||||||
(500, 50, 6, {"current": 6, "pages": [2, 3, 4, 5, 6, 7, 8, 9, 10], "last": 10}),
|
(500, 50, 6, {"current": 6, "pages": [2, 3, 4, 5, 6, 7, 8, 9, 10], "last": 10}),
|
||||||
(500, 50, 10, {"current": 10, "pages": [2, 3, 4, 5, 6, 7, 8, 9, 10], "last": 10}),
|
(
|
||||||
(950, 50, 15, {"current": 15, "pages": [11, 12, 13, 14, 15, 16, 17, 18, 19], "last": 19}),
|
500,
|
||||||
|
50,
|
||||||
|
10,
|
||||||
|
{"current": 10, "pages": [2, 3, 4, 5, 6, 7, 8, 9, 10], "last": 10},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
950,
|
||||||
|
50,
|
||||||
|
15,
|
||||||
|
{"current": 15, "pages": [11, 12, 13, 14, 15, 16, 17, 18, 19], "last": 19},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_generate_pagination_pages(total_items, page_size, current_page, expected):
|
def test_generate_pagination_pages(total_items, page_size, current_page, expected):
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ Object.defineProperty(HTMLElement.prototype, 'clientWidth', {
|
|||||||
beforeAll(done => {
|
beforeAll(done => {
|
||||||
// Set up the DOM with the D3 script included
|
// Set up the DOM with the D3 script included
|
||||||
document.body.innerHTML = `
|
document.body.innerHTML = `
|
||||||
<div id="activityChartContainer">
|
<div id="activityChartContainer"">
|
||||||
<form class="usa-form">
|
<form class="usa-form">
|
||||||
<label class="usa-label" for="options">Account</label>
|
<label class="usa-label" for="options">Account</label>
|
||||||
<select class="usa-select margin-bottom-2" name="options" id="options">
|
<select class="usa-select margin-bottom-2" name="options" id="options">
|
||||||
@@ -30,9 +30,9 @@ beforeAll(done => {
|
|||||||
<option value="individual">User Name</option>
|
<option value="individual">User Name</option>
|
||||||
</select>
|
</select>
|
||||||
</form>
|
</form>
|
||||||
<div id="activityChart">
|
<div id="activityChart" >
|
||||||
<div class="chart-header">
|
<div class="chart-header">
|
||||||
<div class="chart-subtitle">Service Name - Last 7 Days</div>
|
<div class="chart-subtitle">Service Name - last 7 days</div>
|
||||||
<div class="chart-legend" aria-label="Legend"></div>
|
<div class="chart-legend" aria-label="Legend"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-container" id="weeklyChart" data-service-id="12345" style="width: 600px;"></div>
|
<div class="chart-container" id="weeklyChart" data-service-id="12345" style="width: 600px;"></div>
|
||||||
@@ -124,3 +124,26 @@ test('Check HTML content after chart creation', () => {
|
|||||||
expect(container.querySelector('svg')).not.toBeNull();
|
expect(container.querySelector('svg')).not.toBeNull();
|
||||||
expect(container.querySelectorAll('rect').length).toBeGreaterThan(0);
|
expect(container.querySelectorAll('rect').length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Fetches data and creates chart and table correctly', async () => {
|
||||||
|
const mockResponse = {
|
||||||
|
'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 } },
|
||||||
|
};
|
||||||
|
global.fetch = jest.fn(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve(mockResponse),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await fetchData('service');
|
||||||
|
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith('/daily_stats.json');
|
||||||
|
expect(data).toEqual(mockResponse);
|
||||||
|
});
|
||||||
|
|||||||
@@ -15,10 +15,11 @@ function loadScript(scriptContent) {
|
|||||||
Object.defineProperty(HTMLElement.prototype, 'clientWidth', {
|
Object.defineProperty(HTMLElement.prototype, 'clientWidth', {
|
||||||
value: 600,
|
value: 600,
|
||||||
writable: true,
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// beforeAll hook to set up the DOM and load D3.js script
|
// beforeAll hook to set up the DOM and load D3.js script
|
||||||
beforeAll(done => {
|
beforeEach(() => {
|
||||||
// Set up the DOM with the D3 script included
|
// Set up the DOM with the D3 script included
|
||||||
document.body.innerHTML = `
|
document.body.innerHTML = `
|
||||||
<div id="totalMessageChartContainer" data-sms-sent="100" data-sms-allowance-remaining="249900" style="width: 600px;">
|
<div id="totalMessageChartContainer" data-sms-sent="100" data-sms-allowance-remaining="249900" style="width: 600px;">
|
||||||
@@ -33,15 +34,13 @@ beforeAll(done => {
|
|||||||
loadScript(d3ScriptContent);
|
loadScript(d3ScriptContent);
|
||||||
|
|
||||||
// Wait a bit to ensure the script is executed
|
// Wait a bit to ensure the script is executed
|
||||||
setTimeout(() => {
|
return new Promise(resolve => {
|
||||||
// Require the actual JavaScript file you are testing
|
setTimeout(() => {
|
||||||
require('../../app/assets/javascripts/totalMessagesChart.js');
|
// Require the actual JavaScript file you are testing
|
||||||
|
require('../../app/assets/javascripts/totalMessagesChart.js');
|
||||||
// Call the function to create the chart
|
resolve();
|
||||||
window.createTotalMessagesChart();
|
}, 100);
|
||||||
|
});
|
||||||
done();
|
|
||||||
}, 100);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Single test to check if D3 is loaded correctly
|
// Single test to check if D3 is loaded correctly
|
||||||
@@ -52,15 +51,20 @@ test('D3 is loaded correctly', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Test to check if the SVG element is correctly set up
|
// Test to check if the SVG element is correctly set up
|
||||||
test('SVG element is correctly set up', () => {
|
test('SVG element is correctly set up', done => {
|
||||||
const svg = document.getElementById('totalMessageChart');
|
window.createTotalMessagesChart();
|
||||||
expect(svg).not.toBeNull();
|
|
||||||
expect(svg.getAttribute('width')).toBe('600');
|
setTimeout(() => {
|
||||||
expect(svg.getAttribute('height')).toBe('64');
|
const svg = document.getElementById('totalMessageChart');
|
||||||
|
expect(svg.getAttribute('width')).toBe('600');
|
||||||
|
expect(svg.getAttribute('height')).toBe('48');
|
||||||
|
done();
|
||||||
|
}, 1000); // Ensure enough time for the DOM updates
|
||||||
});
|
});
|
||||||
|
|
||||||
// Test to check if the table is created and populated correctly
|
// Test to check if the table is created and populated correctly
|
||||||
test('Populates the accessible table correctly', () => {
|
test('Populates the accessible table correctly', () => {
|
||||||
|
window.createTotalMessagesChart();
|
||||||
const table = document.getElementById('totalMessageTable').getElementsByTagName('table')[0];
|
const table = document.getElementById('totalMessageTable').getElementsByTagName('table')[0];
|
||||||
expect(table).toBeDefined();
|
expect(table).toBeDefined();
|
||||||
|
|
||||||
@@ -84,6 +88,8 @@ test('Chart title is correctly set', () => {
|
|||||||
|
|
||||||
// Test to check if the chart resizes correctly on window resize
|
// Test to check if the chart resizes correctly on window resize
|
||||||
test('Chart resizes correctly on window resize', done => {
|
test('Chart resizes correctly on window resize', done => {
|
||||||
|
window.createTotalMessagesChart();
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const svg = document.getElementById('totalMessageChart');
|
const svg = document.getElementById('totalMessageChart');
|
||||||
const chartContainer = document.getElementById('totalMessageChartContainer');
|
const chartContainer = document.getElementById('totalMessageChartContainer');
|
||||||
@@ -92,7 +98,7 @@ test('Chart resizes correctly on window resize', done => {
|
|||||||
expect(svg.getAttribute('width')).toBe('600');
|
expect(svg.getAttribute('width')).toBe('600');
|
||||||
|
|
||||||
// Set new container width
|
// Set new container width
|
||||||
Object.defineProperty(chartContainer, 'clientWidth', { value: 800 });
|
Object.defineProperty(chartContainer, 'clientWidth', { value: 800, configurable: true });
|
||||||
|
|
||||||
// Trigger resize event
|
// Trigger resize event
|
||||||
window.dispatchEvent(new Event('resize'));
|
window.dispatchEvent(new Event('resize'));
|
||||||
@@ -101,9 +107,9 @@ test('Chart resizes correctly on window resize', done => {
|
|||||||
// Check if SVG width is updated
|
// Check if SVG width is updated
|
||||||
expect(svg.getAttribute('width')).toBe('800');
|
expect(svg.getAttribute('width')).toBe('800');
|
||||||
done();
|
done();
|
||||||
}, 500); // Adjust the timeout if necessary
|
}, 1000); // Adjust the timeout if necessary
|
||||||
}, 1000); // Initial wait for the chart to render
|
}, 1000); // Initial wait for the chart to render
|
||||||
}, 10000); // Adjust the overall test timeout if necessary
|
}, 15000); // Adjust the overall test timeout if necessary
|
||||||
|
|
||||||
// Testing the tooltip
|
// Testing the tooltip
|
||||||
test('Tooltip displays on hover', () => {
|
test('Tooltip displays on hover', () => {
|
||||||
@@ -148,11 +154,12 @@ test('Tooltip displays on hover', () => {
|
|||||||
|
|
||||||
// Test to ensure SVG bars are created and animated correctly
|
// Test to ensure SVG bars are created and animated correctly
|
||||||
test('SVG bars are created and animated correctly', done => {
|
test('SVG bars are created and animated correctly', done => {
|
||||||
|
window.createTotalMessagesChart();
|
||||||
const svg = document.getElementById('totalMessageChart');
|
const svg = document.getElementById('totalMessageChart');
|
||||||
|
|
||||||
// Initial check
|
// Initial check
|
||||||
const sentBar = svg.querySelector('rect[fill="#0076d6"]');
|
const sentBar = svg.querySelector('rect[fill="#0076d6"]');
|
||||||
const remainingBar = svg.querySelector('rect[fill="#fa9441"]');
|
const remainingBar = svg.querySelector('rect[fill="#C7CACE"]');
|
||||||
|
|
||||||
expect(sentBar).not.toBeNull();
|
expect(sentBar).not.toBeNull();
|
||||||
expect(remainingBar).not.toBeNull();
|
expect(remainingBar).not.toBeNull();
|
||||||
|
|||||||
Reference in New Issue
Block a user