pull main

This commit is contained in:
Andrew Shumway
2024-08-16 10:59:40 -06:00
42 changed files with 2107 additions and 1431 deletions
+5 -5
View File
@@ -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"
} }
+5 -5
View File
@@ -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
+8 -2
View File
@@ -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 }}
-4
View File
@@ -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,
+22
View File
@@ -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

+59 -50
View File
@@ -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);
+3 -3
View File
@@ -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
} }
+2 -1
View File
@@ -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"
); );
+46 -2
View File
@@ -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
View File
@@ -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):
+37 -1
View File
@@ -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
+1
View File
@@ -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",
+4 -8
View File
@@ -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
+4 -5
View File
@@ -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
+4
View File
@@ -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}")
+6 -7
View File
@@ -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 %}
+6 -6
View File
@@ -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 %}
+26 -3
View File
@@ -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>
&emsp;
</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 %}
+75 -67
View File
@@ -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 &thinsp;(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 &thinsp;(messages are kept for {{ service_data_retention_days }} days).</td> <td class="table-empty-message" colspan="10">No batched job messages found &thinsp;(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 %}
+26
View File
@@ -0,0 +1,26 @@
### Setting Up Environment Variables for Local Development to the Staging API
When youre 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, youll 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
youre 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
View File
@@ -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;
+1210 -523
View File
File diff suppressed because it is too large Load Diff
+16 -18
View File
@@ -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
View File
@@ -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
View File
@@ -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]
+6 -6
View File
@@ -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"
} }
+19 -2
View File
@@ -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",
+179 -218
View File
@@ -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"
+1 -1
View File
@@ -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."
) )
+78 -56
View File
@@ -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
+19 -2
View File
@@ -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
+3
View File
@@ -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",
+12 -2
View File
@@ -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):
+26 -3
View File
@@ -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);
});
+26 -19
View File
@@ -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();