mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-09-10 18:22:37 -04:00
Fixed merge conflicts
This commit is contained in:
@@ -64,6 +64,17 @@ body:
|
|||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: '**Accessibility:**'
|
||||||
|
- type: textarea
|
||||||
|
id: accessibility
|
||||||
|
attributes:
|
||||||
|
label: "List any specific accessibility guidance or tests that need to be considered for this user story."
|
||||||
|
description: "List what type of accessibility tests need to pass."
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: '**Notes:**'
|
value: '**Notes:**'
|
||||||
|
|||||||
@@ -23,5 +23,7 @@ Please enter a detailed description here.
|
|||||||
|
|
||||||
## A11y Checks (if applicable)
|
## A11y Checks (if applicable)
|
||||||
|
|
||||||
* Conduct automated tests through [AxeDevTools](https://www.deque.com/axe/devtools/) and [WAVE](https://wave.webaim.org/)
|
* Double check work is getting picked up by the automated E2E tests
|
||||||
|
* Conduct browser-based tests through [AxeDevTools](https://www.deque.com/axe/devtools/) and [WAVE](https://wave.webaim.org/)
|
||||||
* Review the [Manual Checklist](https://docs.google.com/document/d/192bBXStebdXWtYhZQ73qaWMJhGcuSB1W6c9YBXhWZvc/edit?usp=sharing)
|
* Review the [Manual Checklist](https://docs.google.com/document/d/192bBXStebdXWtYhZQ73qaWMJhGcuSB1W6c9YBXhWZvc/edit?usp=sharing)
|
||||||
|
* Make sure there are no linting errors in VSCode or other IDE of choice
|
||||||
|
|||||||
@@ -165,8 +165,9 @@ jobs:
|
|||||||
run: make run-flask &
|
run: make run-flask &
|
||||||
env:
|
env:
|
||||||
NOTIFY_ENVIRONMENT: scanning
|
NOTIFY_ENVIRONMENT: scanning
|
||||||
|
FEATURE_ABOUT_PAGE_ENABLED: true
|
||||||
- name: Run OWASP Baseline Scan
|
- name: Run OWASP Baseline Scan
|
||||||
uses: zaproxy/action-baseline@v0.9.0
|
uses: zaproxy/action-baseline@v0.14.0
|
||||||
with:
|
with:
|
||||||
docker_name: "ghcr.io/zaproxy/zaproxy:weekly"
|
docker_name: "ghcr.io/zaproxy/zaproxy:weekly"
|
||||||
target: "http://localhost:6012"
|
target: "http://localhost:6012"
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
NOTIFY_ENVIRONMENT: scanning
|
NOTIFY_ENVIRONMENT: scanning
|
||||||
- name: Run OWASP Full Scan
|
- name: Run OWASP Full Scan
|
||||||
uses: zaproxy/action-full-scan@v0.7.0
|
uses: zaproxy/action-full-scan@v0.12.0
|
||||||
with:
|
with:
|
||||||
docker_name: 'ghcr.io/zaproxy/zaproxy:weekly'
|
docker_name: 'ghcr.io/zaproxy/zaproxy:weekly'
|
||||||
target: 'http://localhost:6012'
|
target: 'http://localhost:6012'
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
(function (window) {
|
(function (window) {
|
||||||
|
|
||||||
if (document.getElementById('activityChartContainer')) {
|
if (document.getElementById('activityChartContainer')) {
|
||||||
|
let currentType = 'service';
|
||||||
|
const tableContainer = document.getElementById('activityContainer');
|
||||||
|
const currentUserName = tableContainer.getAttribute('data-currentUserName');
|
||||||
|
const currentServiceId = tableContainer.getAttribute('data-currentServiceId');
|
||||||
const COLORS = {
|
const COLORS = {
|
||||||
delivered: '#0076d6',
|
delivered: '#0076d6',
|
||||||
failed: '#fa9441',
|
failed: '#fa9441',
|
||||||
|
pending: '#C7CACE',
|
||||||
text: '#666'
|
text: '#666'
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -12,7 +16,7 @@
|
|||||||
const FONT_WEIGHT = 'bold';
|
const FONT_WEIGHT = 'bold';
|
||||||
const MAX_Y = 120;
|
const MAX_Y = 120;
|
||||||
|
|
||||||
const createChart = function(containerId, labels, deliveredData, failedData) {
|
const createChart = function(containerId, labels, deliveredData, failedData, pendingData) {
|
||||||
const container = d3.select(containerId);
|
const container = d3.select(containerId);
|
||||||
container.selectAll('*').remove(); // Clear any existing content
|
container.selectAll('*').remove(); // Clear any existing content
|
||||||
|
|
||||||
@@ -35,7 +39,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Calculate total messages
|
// Calculate total messages
|
||||||
const totalMessages = d3.sum(deliveredData) + d3.sum(failedData);
|
const totalMessages = d3.sum(deliveredData) + d3.sum(failedData) + d3.sum(pendingData);
|
||||||
|
|
||||||
// Create legend only if there are messages
|
// Create legend only if there are messages
|
||||||
const legendContainer = d3.select('.chart-legend');
|
const legendContainer = d3.select('.chart-legend');
|
||||||
@@ -45,7 +49,8 @@
|
|||||||
// Show legend if there are messages
|
// Show legend if there are messages
|
||||||
const legendData = [
|
const legendData = [
|
||||||
{ label: 'Delivered', color: COLORS.delivered },
|
{ label: 'Delivered', color: COLORS.delivered },
|
||||||
{ label: 'Failed', color: COLORS.failed }
|
{ label: 'Failed', color: COLORS.failed },
|
||||||
|
{ label: 'Pending', color: COLORS.pending }
|
||||||
];
|
];
|
||||||
|
|
||||||
const legendItem = legendContainer.selectAll('.legend-item')
|
const legendItem = legendContainer.selectAll('.legend-item')
|
||||||
@@ -76,8 +81,9 @@
|
|||||||
.range([0, width])
|
.range([0, width])
|
||||||
.padding(0.1);
|
.padding(0.1);
|
||||||
// Adjust the y-axis domain to add some space above the tallest bar
|
// Adjust the y-axis domain to add some space above the tallest bar
|
||||||
const maxY = d3.max(deliveredData.map((d, i) => d + (failedData[i] || 0)));
|
const maxY = d3.max(deliveredData.map((d, i) => d + (failedData[i] || 0) + (pendingData[i] || 0)));
|
||||||
const y = d3.scaleSqrt()
|
|
||||||
|
const y = d3.scaleSymlog()
|
||||||
.domain([0, maxY + 2]) // Add 2 units of space at the top
|
.domain([0, maxY + 2]) // Add 2 units of space at the top
|
||||||
.nice()
|
.nice()
|
||||||
.range([height, 0]);
|
.range([height, 0]);
|
||||||
@@ -89,7 +95,7 @@
|
|||||||
|
|
||||||
// Generate the y-axis with whole numbers
|
// Generate the y-axis with whole numbers
|
||||||
const yAxis = d3.axisLeft(y)
|
const yAxis = d3.axisLeft(y)
|
||||||
.ticks(Math.min(maxY + 2, 10)) // Generate up to 10 ticks based on the data
|
.ticks(Math.min(maxY + 2, 3))
|
||||||
.tickFormat(d3.format('d')); // Ensure whole numbers on the y-axis
|
.tickFormat(d3.format('d')); // Ensure whole numbers on the y-axis
|
||||||
|
|
||||||
svg.append('g')
|
svg.append('g')
|
||||||
@@ -100,12 +106,13 @@
|
|||||||
const stackData = labels.map((label, i) => ({
|
const stackData = labels.map((label, i) => ({
|
||||||
label: label,
|
label: label,
|
||||||
delivered: deliveredData[i],
|
delivered: deliveredData[i],
|
||||||
failed: failedData[i] || 0 // Ensure there's a value for failed, even if it's 0
|
failed: failedData[i] || 0,
|
||||||
|
pending: pendingData[i] || 0
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Stack the data
|
// Stack the data
|
||||||
const stack = d3.stack()
|
const stack = d3.stack()
|
||||||
.keys(['delivered', 'failed'])
|
.keys(['delivered', 'failed', 'pending'])
|
||||||
.order(d3.stackOrderNone)
|
.order(d3.stackOrderNone)
|
||||||
.offset(d3.stackOffsetNone);
|
.offset(d3.stackOffsetNone);
|
||||||
|
|
||||||
@@ -113,8 +120,8 @@
|
|||||||
|
|
||||||
// Color scale
|
// Color scale
|
||||||
const color = d3.scaleOrdinal()
|
const color = d3.scaleOrdinal()
|
||||||
.domain(['delivered', 'failed'])
|
.domain(['delivered', 'failed', 'pending'])
|
||||||
.range([COLORS.delivered, COLORS.failed]);
|
.range([COLORS.delivered, COLORS.failed, COLORS.pending]);
|
||||||
|
|
||||||
// Create bars with animation
|
// Create bars with animation
|
||||||
const barGroups = svg.selectAll('.bar-group')
|
const barGroups = svg.selectAll('.bar-group')
|
||||||
@@ -152,7 +159,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Function to create an accessible table
|
// Function to create an accessible table
|
||||||
const createTable = function(tableId, chartType, labels, deliveredData, failedData) {
|
const createTable = function(tableId, chartType, labels, deliveredData, failedData, pendingData) {
|
||||||
const table = document.getElementById(tableId);
|
const table = document.getElementById(tableId);
|
||||||
table.innerHTML = ""; // Clear previous data
|
table.innerHTML = ""; // Clear previous data
|
||||||
|
|
||||||
@@ -164,7 +171,7 @@
|
|||||||
|
|
||||||
// Create table header
|
// Create table header
|
||||||
const headerRow = document.createElement('tr');
|
const headerRow = document.createElement('tr');
|
||||||
const headers = ['Day', 'Delivered', 'Failed'];
|
const headers = ['Day', 'Delivered', 'Failed', 'Pending'];
|
||||||
headers.forEach(headerText => {
|
headers.forEach(headerText => {
|
||||||
const th = document.createElement('th');
|
const th = document.createElement('th');
|
||||||
th.textContent = headerText;
|
th.textContent = headerText;
|
||||||
@@ -187,6 +194,10 @@
|
|||||||
cellFailed.textContent = failedData[index];
|
cellFailed.textContent = failedData[index];
|
||||||
row.appendChild(cellFailed);
|
row.appendChild(cellFailed);
|
||||||
|
|
||||||
|
const cellPending = document.createElement('td');
|
||||||
|
cellPending.textContent = pendingData[index];
|
||||||
|
row.appendChild(cellPending);
|
||||||
|
|
||||||
tbody.appendChild(row);
|
tbody.appendChild(row);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -196,6 +207,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const fetchData = function(type) {
|
const fetchData = function(type) {
|
||||||
|
|
||||||
var ctx = document.getElementById('weeklyChart');
|
var ctx = document.getElementById('weeklyChart');
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
return;
|
return;
|
||||||
@@ -203,9 +215,11 @@
|
|||||||
|
|
||||||
// get local timezone
|
// get local timezone
|
||||||
var userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
var userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
var url = type === 'service'
|
|
||||||
? `/daily_stats.json?timezone=${encodeURIComponent(userTimezone)}`
|
// build the URL depending on "type"
|
||||||
: `/daily_stats_by_user.json`;
|
var url = (type === 'service')
|
||||||
|
? `/services/${currentServiceId}/daily-stats.json?timezone=${encodeURIComponent(userTimezone)}`
|
||||||
|
: `/services/${currentServiceId}/daily-stats-by-user.json?timezone=${encodeURIComponent(userTimezone)}`;
|
||||||
|
|
||||||
return fetch(url)
|
return fetch(url)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
@@ -220,7 +234,7 @@
|
|||||||
labels = [];
|
labels = [];
|
||||||
deliveredData = [];
|
deliveredData = [];
|
||||||
failedData = [];
|
failedData = [];
|
||||||
|
pendingData = [];
|
||||||
let totalMessages = 0;
|
let totalMessages = 0;
|
||||||
|
|
||||||
for (var dateString in data) {
|
for (var dateString in data) {
|
||||||
@@ -231,6 +245,8 @@
|
|||||||
labels.push(formattedDate);
|
labels.push(formattedDate);
|
||||||
deliveredData.push(data[dateString].sms.delivered);
|
deliveredData.push(data[dateString].sms.delivered);
|
||||||
failedData.push(data[dateString].sms.failure);
|
failedData.push(data[dateString].sms.failure);
|
||||||
|
pendingData.push(data[dateString].sms.pending || 0);
|
||||||
|
totalMessages += data[dateString].sms.delivered + data[dateString].sms.failure + data[dateString].sms.pending;
|
||||||
|
|
||||||
// Calculate the total number of messages
|
// Calculate the total number of messages
|
||||||
totalMessages += data[dateString].sms.delivered + data[dateString].sms.failure;
|
totalMessages += data[dateString].sms.delivered + data[dateString].sms.failure;
|
||||||
@@ -259,17 +275,18 @@
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If there are messages, create the chart and table
|
// If there are messages, create the chart and table
|
||||||
createChart('#weeklyChart', labels, deliveredData, failedData);
|
createChart('#weeklyChart', labels, deliveredData, failedData, pendingData);
|
||||||
createTable('weeklyTable', 'activityChart', labels, deliveredData, failedData);
|
createTable('weeklyTable', 'activityChart', labels, deliveredData, failedData, pendingData);
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
|
||||||
})
|
|
||||||
.catch(error => console.error('Error fetching daily stats:', error));
|
|
||||||
};
|
|
||||||
|
|
||||||
|
return data;
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Error fetching daily stats:', error));
|
||||||
|
};
|
||||||
|
setInterval(() => fetchData(currentType), 25000);
|
||||||
const handleDropdownChange = function(event) {
|
const handleDropdownChange = function(event) {
|
||||||
const selectedValue = event.target.value;
|
const selectedValue = event.target.value;
|
||||||
|
currentType = selectedValue;
|
||||||
const subTitle = document.querySelector(`#activityChartContainer .chart-subtitle`);
|
const subTitle = document.querySelector(`#activityChartContainer .chart-subtitle`);
|
||||||
const selectElement = document.getElementById('options');
|
const selectElement = document.getElementById('options');
|
||||||
const selectedText = selectElement.options[selectElement.selectedIndex].text;
|
const selectedText = selectElement.options[selectElement.selectedIndex].text;
|
||||||
@@ -277,36 +294,67 @@
|
|||||||
subTitle.textContent = `${selectedText} - last 7 days`;
|
subTitle.textContent = `${selectedText} - last 7 days`;
|
||||||
fetchData(selectedValue);
|
fetchData(selectedValue);
|
||||||
|
|
||||||
// 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`;
|
||||||
|
|
||||||
// Switch tables based on dropdown selection
|
const tableHeading = document.querySelector('#tableActivity h2');
|
||||||
const selectedTable = selectedValue === "individual" ? "table1" : "table2";
|
const senderColumns = document.querySelectorAll('.sender-column');
|
||||||
const tables = document.querySelectorAll('.table-overflow-x-auto');
|
const allRows = document.querySelectorAll('#activity-table tbody tr');
|
||||||
tables.forEach(function(table) {
|
const caption = document.querySelector('#activity-table caption');
|
||||||
table.classList.add('hidden'); // Hide all tables by adding the hidden class
|
|
||||||
table.classList.remove('visible'); // Ensure they are not visible
|
if (selectedValue === 'individual') {
|
||||||
});
|
|
||||||
const tableToShow = document.getElementById(selectedTable);
|
tableHeading.textContent = 'My activity';
|
||||||
tableToShow.classList.remove('hidden'); // Remove hidden class
|
caption.textContent = `Table showing the sent jobs for ${currentUserName}`;
|
||||||
tableToShow.classList.add('visible'); // Add visible class
|
|
||||||
|
senderColumns.forEach(col => {
|
||||||
|
col.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
allRows.forEach(row => row.style.display = 'none');
|
||||||
|
|
||||||
|
const userRows = Array.from(allRows).filter(row => {
|
||||||
|
const senderCell = row.querySelector('.sender-column');
|
||||||
|
const rowSender = senderCell ? senderCell.textContent.trim() : '';
|
||||||
|
return rowSender === currentUserName;
|
||||||
|
});
|
||||||
|
|
||||||
|
userRows.slice(0, 5).forEach(row => {
|
||||||
|
row.style.display = '';
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
|
||||||
|
tableHeading.textContent = 'Service activity';
|
||||||
|
caption.textContent = `Table showing the sent jobs for service`;
|
||||||
|
|
||||||
|
senderColumns.forEach(col => {
|
||||||
|
col.style.display = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
allRows.forEach((row, index) => {
|
||||||
|
row.style.display = (index < 5) ? '' : 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Initialize activityChart chart and table with service data by default
|
// Initialize activityChart chart and table with service data by default
|
||||||
fetchData('service');
|
fetchData(currentType);
|
||||||
|
|
||||||
|
const allRows = Array.from(document.querySelectorAll('#activity-table tbody tr'));
|
||||||
|
allRows.forEach((row, index) => {
|
||||||
|
row.style.display = (index < 5) ? '' : 'none';
|
||||||
|
});
|
||||||
|
|
||||||
// Add event listener to the dropdown
|
|
||||||
const dropdown = document.getElementById('options');
|
const dropdown = document.getElementById('options');
|
||||||
dropdown.addEventListener('change', handleDropdownChange);
|
dropdown.addEventListener('change', handleDropdownChange);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Resize chart on window resize
|
// Resize chart on window resize
|
||||||
window.addEventListener('resize', function() {
|
window.addEventListener('resize', function() {
|
||||||
if (labels.length > 0 && deliveredData.length > 0 && failedData.length > 0) {
|
if (labels.length > 0 && deliveredData.length > 0 && failedData.length > 0 && pendingData.length > 0) {
|
||||||
createChart('#weeklyChart', labels, deliveredData, failedData);
|
createChart('#weeklyChart', labels, deliveredData, failedData, pendingData);
|
||||||
createTable('weeklyTable', 'activityChart', labels, deliveredData, failedData);
|
createTable('weeklyTable', 'activityChart', labels, deliveredData, failedData, pendingData);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1024,3 +1024,7 @@ nav.nav {
|
|||||||
font-size: units(3);
|
font-size: units(3);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-control-error {
|
||||||
|
border: 4px solid #b10e1e
|
||||||
|
}
|
||||||
|
|||||||
+12
-30
@@ -14,7 +14,7 @@ from app import (
|
|||||||
service_api_client,
|
service_api_client,
|
||||||
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
|
||||||
from app.main import main
|
from app.main import main
|
||||||
from app.main.views.user_profile import set_timezone
|
from app.main.views.user_profile import set_timezone
|
||||||
from app.statistics_utils import get_formatted_percentage
|
from app.statistics_utils import get_formatted_percentage
|
||||||
@@ -62,41 +62,23 @@ def service_dashboard(service_id):
|
|||||||
job_response = job_api_client.get_jobs(service_id)["data"]
|
job_response = job_api_client.get_jobs(service_id)["data"]
|
||||||
service_data_retention_days = 7
|
service_data_retention_days = 7
|
||||||
|
|
||||||
jobs = [
|
filtered_jobs = [job for job in job_response if job["job_status"] != "cancelled"]
|
||||||
{
|
sorted_jobs = sorted(filtered_jobs, key=lambda job: job["created_at"], reverse=True)
|
||||||
"job_id": job["id"],
|
|
||||||
"time_left": get_time_left(job["created_at"]),
|
|
||||||
"download_link": url_for(
|
|
||||||
".view_job_csv", service_id=current_service.id, job_id=job["id"]
|
|
||||||
),
|
|
||||||
"view_job_link": url_for(
|
|
||||||
".view_job", service_id=current_service.id, job_id=job["id"]
|
|
||||||
),
|
|
||||||
"created_at": job["created_at"],
|
|
||||||
"processing_finished": job.get("processing_finished"),
|
|
||||||
"processing_started": job.get("processing_started"),
|
|
||||||
"notification_count": job["notification_count"],
|
|
||||||
"created_by": job["created_by"],
|
|
||||||
"template_name": job["template_name"],
|
|
||||||
"original_file_name": job["original_file_name"],
|
|
||||||
}
|
|
||||||
for job in job_response
|
|
||||||
if job["job_status"] != "cancelled"
|
|
||||||
]
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"views/dashboard/dashboard.html",
|
"views/dashboard/dashboard.html",
|
||||||
updates_url=url_for(".service_dashboard_updates", service_id=service_id),
|
updates_url=url_for(".service_dashboard_updates", service_id=service_id),
|
||||||
partials=get_dashboard_partials(service_id),
|
partials=get_dashboard_partials(service_id),
|
||||||
jobs=jobs,
|
jobs=sorted_jobs,
|
||||||
service_data_retention_days=service_data_retention_days,
|
service_data_retention_days=service_data_retention_days,
|
||||||
sms_sent=sms_sent,
|
sms_sent=sms_sent,
|
||||||
sms_allowance_remaining=sms_allowance_remaining,
|
sms_allowance_remaining=sms_allowance_remaining,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@main.route("/daily_stats.json")
|
@main.route("/services/<uuid:service_id>/daily-stats.json")
|
||||||
def get_daily_stats():
|
@user_has_permissions()
|
||||||
service_id = session.get("service_id")
|
def get_daily_stats(service_id):
|
||||||
date_range = get_stats_date_range()
|
date_range = get_stats_date_range()
|
||||||
|
|
||||||
# Get timezone from request (default to UTC if not provided)
|
# Get timezone from request (default to UTC if not provided)
|
||||||
@@ -109,14 +91,14 @@ def get_daily_stats():
|
|||||||
return jsonify(stats)
|
return jsonify(stats)
|
||||||
|
|
||||||
|
|
||||||
@main.route("/daily_stats_by_user.json")
|
@main.route("/services/<uuid:service_id>/daily-stats-by-user.json")
|
||||||
def get_daily_stats_by_user():
|
@user_has_permissions()
|
||||||
|
def get_daily_stats_by_user(service_id):
|
||||||
service_id = session.get("service_id")
|
service_id = session.get("service_id")
|
||||||
date_range = get_stats_date_range()
|
date_range = get_stats_date_range()
|
||||||
user_id = current_user.id
|
|
||||||
stats = service_api_client.get_user_service_notification_statistics_by_day(
|
stats = service_api_client.get_user_service_notification_statistics_by_day(
|
||||||
service_id,
|
service_id,
|
||||||
user_id,
|
user_id=current_user.id,
|
||||||
start_date=date_range["start_date"],
|
start_date=date_range["start_date"],
|
||||||
days=date_range["days"],
|
days=date_range["days"],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ def view_job(service_id, job_id):
|
|||||||
|
|
||||||
filter_args = parse_filter_args(request.args)
|
filter_args = parse_filter_args(request.args)
|
||||||
filter_args["status"] = set_status_filters(filter_args)
|
filter_args["status"] = set_status_filters(filter_args)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"views/jobs/job.html",
|
"views/jobs/job.html",
|
||||||
job=job,
|
job=job,
|
||||||
@@ -402,7 +401,9 @@ def get_job_partials(job):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if request.referrer is not None:
|
if request.referrer is not None:
|
||||||
session["arrived_from_preview_page"] = "check" in request.referrer
|
session["arrived_from_preview_page"] = ("check" in request.referrer) or (
|
||||||
|
"help=0" in request.referrer
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
session["arrived_from_preview_page"] = False
|
session["arrived_from_preview_page"] = False
|
||||||
|
|
||||||
|
|||||||
@@ -6,17 +6,36 @@ from app.notify_client import NotifyAdminAPIClient
|
|||||||
|
|
||||||
class BillingAPIClient(NotifyAdminAPIClient):
|
class BillingAPIClient(NotifyAdminAPIClient):
|
||||||
def get_monthly_usage_for_service(self, service_id, year):
|
def get_monthly_usage_for_service(self, service_id, year):
|
||||||
return self.get(
|
monthly_usage = redis_client.get(f"monthly-usage-summary-{service_id}-{year}")
|
||||||
|
if monthly_usage is not None:
|
||||||
|
return json.loads(monthly_usage.decode("utf-8"))
|
||||||
|
result = self.get(
|
||||||
"/service/{0}/billing/monthly-usage".format(service_id),
|
"/service/{0}/billing/monthly-usage".format(service_id),
|
||||||
params=dict(year=year),
|
params=dict(year=year),
|
||||||
)
|
)
|
||||||
|
redis_client.set(
|
||||||
|
f"monthly-usage-summary-{service_id}-{year}",
|
||||||
|
json.dumps(result),
|
||||||
|
ex=30,
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
def get_annual_usage_for_service(self, service_id, year=None):
|
def get_annual_usage_for_service(self, service_id, year=None):
|
||||||
return self.get(
|
annual_usage = redis_client.get(f"yearly-usage-summary-{service_id}-{year}")
|
||||||
|
if annual_usage is not None:
|
||||||
|
return json.loads(annual_usage.decode("utf-8"))
|
||||||
|
result = self.get(
|
||||||
"/service/{0}/billing/yearly-usage-summary".format(service_id),
|
"/service/{0}/billing/yearly-usage-summary".format(service_id),
|
||||||
params=dict(year=year),
|
params=dict(year=year),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
redis_client.set(
|
||||||
|
f"yearly-usage-summary-{service_id}-{year}",
|
||||||
|
json.dumps(result),
|
||||||
|
ex=30,
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
def get_free_sms_fragment_limit_for_year(self, service_id, year=None):
|
def get_free_sms_fragment_limit_for_year(self, service_id, year=None):
|
||||||
frag_limit = redis_client.get(f"free-sms-fragment-limit-{service_id}-{year}")
|
frag_limit = redis_client.get(f"free-sms-fragment-limit-{service_id}-{year}")
|
||||||
if frag_limit is not None:
|
if frag_limit is not None:
|
||||||
@@ -48,13 +67,28 @@ class BillingAPIClient(NotifyAdminAPIClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_data_for_billing_report(self, start_date, end_date):
|
def get_data_for_billing_report(self, start_date, end_date):
|
||||||
return self.get(
|
x_start_date = str(start_date)
|
||||||
|
x_start_date = x_start_date.replace(" ", "_")
|
||||||
|
x_end_date = str(end_date)
|
||||||
|
x_end_date = x_end_date.replace(" ", "_")
|
||||||
|
billing_data = redis_client.get(
|
||||||
|
f"get-data-for-billing-report-{x_start_date}-{x_end_date}"
|
||||||
|
)
|
||||||
|
if billing_data is not None:
|
||||||
|
return json.loads(billing_data.decode("utf-8"))
|
||||||
|
result = self.get(
|
||||||
url="/platform-stats/data-for-billing-report",
|
url="/platform-stats/data-for-billing-report",
|
||||||
params={
|
params={
|
||||||
"start_date": str(start_date),
|
"start_date": str(start_date),
|
||||||
"end_date": str(end_date),
|
"end_date": str(end_date),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
redis_client.set(
|
||||||
|
f"get-data-for-billing-report-{x_start_date}-{x_end_date}",
|
||||||
|
json.dumps(result),
|
||||||
|
ex=30,
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
def get_data_for_volumes_by_service_report(self, start_date, end_date):
|
def get_data_for_volumes_by_service_report(self, start_date, end_date):
|
||||||
return self.get(
|
return self.get(
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from app.extensions import redis_client
|
||||||
from app.notify_client import NotifyAdminAPIClient, _attach_current_user
|
from app.notify_client import NotifyAdminAPIClient, _attach_current_user
|
||||||
|
|
||||||
|
|
||||||
@@ -41,7 +44,7 @@ class NotificationApiClient(NotifyAdminAPIClient):
|
|||||||
if job_id:
|
if job_id:
|
||||||
return method(
|
return method(
|
||||||
url="/service/{}/job/{}/notifications".format(service_id, job_id),
|
url="/service/{}/job/{}/notifications".format(service_id, job_id),
|
||||||
**kwargs
|
**kwargs,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if limit_days is not None:
|
if limit_days is not None:
|
||||||
@@ -96,9 +99,20 @@ class NotificationApiClient(NotifyAdminAPIClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_notification_count_for_job_id(self, *, service_id, job_id):
|
def get_notification_count_for_job_id(self, *, service_id, job_id):
|
||||||
return self.get(
|
counts = redis_client.get(
|
||||||
|
f"notification-count-for-job-id-{service_id}-{job_id}"
|
||||||
|
)
|
||||||
|
if counts is not None:
|
||||||
|
return json.loads(counts.decode("utf-8"))
|
||||||
|
result = self.get(
|
||||||
url="/service/{}/job/{}/notification_count".format(service_id, job_id)
|
url="/service/{}/job/{}/notification_count".format(service_id, job_id)
|
||||||
)["count"]
|
)
|
||||||
|
redis_client.set(
|
||||||
|
f"notification-count-for-job-id-{service_id}-{job_id}",
|
||||||
|
json.dumps(result["count"]),
|
||||||
|
ex=30,
|
||||||
|
)
|
||||||
|
return result["count"]
|
||||||
|
|
||||||
|
|
||||||
notification_api_client = NotificationApiClient()
|
notification_api_client = NotificationApiClient()
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
attributes: params.errorMessage.attributes,
|
attributes: params.errorMessage.attributes,
|
||||||
html: params.errorMessage.html,
|
html: params.errorMessage.html,
|
||||||
text: params.errorMessage.text,
|
text: params.errorMessage.text,
|
||||||
visuallyHiddenText: params.errorMessage.visuallyHiddenText
|
visuallyHiddenText: params.errorMessage.visuallyHiddenText,
|
||||||
}) | indent(2) | trim }}
|
}) | indent(2) | trim }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<input class="usa-input {%- if params.classes %} {{ params.classes }}{% endif %} {%- if params.errorMessage %} usa-input--error{% endif %}" id="{{ params.id }}" name="{{ params.name }}" type="{{ params.type | default('text') }}"
|
<input class="usa-input {%- if params.classes %} {{ params.classes }}{% endif %} {%- if params.errorMessage %} usa-input--error{% endif %}" id="{{ params.id }}" name="{{ params.name }}" type="{{ params.type | default('text') }}"
|
||||||
@@ -42,5 +42,7 @@
|
|||||||
{%- if describedBy %} aria-describedby="{{ describedBy }}"{% endif %}
|
{%- if describedBy %} aria-describedby="{{ describedBy }}"{% endif %}
|
||||||
{%- if params.autocomplete %} autocomplete="{{ params.autocomplete}}"{% endif %}
|
{%- if params.autocomplete %} autocomplete="{{ params.autocomplete}}"{% endif %}
|
||||||
{%- if params.pattern %} pattern="{{ params.pattern }}"{% endif %}
|
{%- if params.pattern %} pattern="{{ params.pattern }}"{% endif %}
|
||||||
{%- for attribute, value in params.attributes %} {{ attribute }}="{{ value }}"{% endfor -%}>
|
{%- for attribute, value in params.attributes %} {{ attribute }}="{{ value }}"{% endfor -%}
|
||||||
|
{%- if params.required %} required{% endif %}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,19 +16,9 @@
|
|||||||
placeholder=''
|
placeholder=''
|
||||||
) %}
|
) %}
|
||||||
<div
|
<div
|
||||||
class="form-group{% if field.errors %} form-group-error{% endif %} {{ extra_form_group_classes }}"
|
class="usa-form-group{% if field.errors %} usa-form-group--error{% endif %} {{ extra_form_group_classes }}"
|
||||||
data-module="{% if autofocus %}autofocus{% elif colour_preview %}colour-preview{% endif %}"
|
data-module="{% if autofocus %}autofocus{% elif colour_preview %}colour-preview{% endif %}"
|
||||||
>
|
>
|
||||||
{% if field.errors %}
|
|
||||||
<div class="usa-alert usa-alert--error edit-textbox-error-mt" role="alert">
|
|
||||||
<div class="usa-alert__body">
|
|
||||||
<h4 class="usa-alert__heading">Error message</h4>
|
|
||||||
<p class="usa-alert__text" data-module="track-error" data-error-type="{{ field.errors[0] }}" data-error-label="{{ field.name }}">
|
|
||||||
{% if not safe_error_message %}{{ field.errors[0] }}{% else %}{{ field.errors[0]|safe }}{% endif %}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<label class="usa-label" for="{{ field.name }}">
|
<label class="usa-label" for="{{ field.name }}">
|
||||||
{% if label %}
|
{% if label %}
|
||||||
{{ label }}
|
{{ label }}
|
||||||
@@ -41,6 +31,12 @@
|
|||||||
{{ hint }}
|
{{ hint }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if field.errors %}
|
||||||
|
<span id="{{ field.name}}-error" class="usa-error-message" data-module="track-error" data-error-type="{{ field.errors[0] }}" data-error-label="{{ field.name }}" tabindex="-1" aria-live="assertive" role="alert">
|
||||||
|
<span class="usa-sr-only">Error:</span>
|
||||||
|
{% if not safe_error_message %}{{ field.errors[0] }}{% else %}{{ field.errors[0]|safe }}{% endif %}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
{%
|
{%
|
||||||
if highlight_placeholders or autosize
|
if highlight_placeholders or autosize
|
||||||
%}
|
%}
|
||||||
@@ -59,6 +55,8 @@
|
|||||||
data_highlight_placeholders='true' if highlight_placeholders else 'false',
|
data_highlight_placeholders='true' if highlight_placeholders else 'false',
|
||||||
rows=rows|string,
|
rows=rows|string,
|
||||||
placeholder=placeholder,
|
placeholder=placeholder,
|
||||||
|
aria_describedby=field.name+"-error",
|
||||||
|
required='required' if required else None,
|
||||||
**kwargs
|
**kwargs
|
||||||
) }}
|
) }}
|
||||||
{% if suffix %}
|
{% if suffix %}
|
||||||
|
|||||||
@@ -21,11 +21,11 @@
|
|||||||
|
|
||||||
<div class="ajax-block-container">
|
<div class="ajax-block-container">
|
||||||
<p class='bottom-gutter'>
|
<p class='bottom-gutter'>
|
||||||
{% if job.still_processing or arrived_from_preview_page_url %}
|
{% if not job.finished_processing %}
|
||||||
{% if job.scheduled_for %}
|
{% if job.scheduled_for %}
|
||||||
<div class="usa-alert usa-alert--info">
|
<div class="usa-alert usa-alert--info">
|
||||||
<div class="usa-alert__body">
|
<div class="usa-alert__body">
|
||||||
<h2 class="usa-alert__heading">Your text has been scheduled</h2>
|
<h2 class="usa-alert__heading">Your message has been scheduled</h2>
|
||||||
<p class="usa-alert__text">
|
<p class="usa-alert__text">
|
||||||
{{ job.template_name }} - {{ current_service.name }} was scheduled on {{ job.scheduled_for|format_datetime_normal }} by {{ job.created_by.name }}
|
{{ job.template_name }} - {{ current_service.name }} was scheduled on {{ job.scheduled_for|format_datetime_normal }} by {{ job.created_by.name }}
|
||||||
</p>
|
</p>
|
||||||
@@ -33,18 +33,46 @@
|
|||||||
</div>
|
</div>
|
||||||
{{display_message_status}}
|
{{display_message_status}}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="usa-alert usa-alert--success">
|
{% if job.processing_started %}
|
||||||
<div class="usa-alert__body">
|
<div class="usa-alert usa-alert--success">
|
||||||
<h2 class="usa-alert__heading">Your text has been sent</h2>
|
<div class="usa-alert__body">
|
||||||
<p class="usa-alert__text">
|
<h2 class="usa-alert__heading">
|
||||||
{{ job.template_name }} - {{ current_service.name }} was sent on {% if job.processing_started %}
|
Your message is sending
|
||||||
{{ job.processing_started|format_datetime_table }} {% else %}
|
</h2>
|
||||||
{{ job.created_at|format_datetime_table }} {% endif %} by {{ job.created_by.name }}
|
<p class="usa-alert__text">
|
||||||
</p>
|
{{ job.template_name }} - {{ current_service.name }}
|
||||||
|
has been sending since {{job.processing_started| format_datetime_normal}} by {{ job.created_by.name }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{% else %}
|
||||||
|
<div class="usa-alert usa-alert--info">
|
||||||
|
<div class="usa-alert__body">
|
||||||
|
<h2 class="usa-alert__heading">
|
||||||
|
Your message is pending
|
||||||
|
</h2>
|
||||||
|
<p class="usa-alert__text">
|
||||||
|
{{ job.template_name }} - {{ current_service.name }}
|
||||||
|
has been pending since {{job.created_at|format_datetime_normal}} by {{ job.created_by.name }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{{display_message_status}}
|
{{display_message_status}}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% elif arrived_from_preview_page_url %}
|
||||||
|
<div class="usa-alert usa-alert--success">
|
||||||
|
<div class="usa-alert__body">
|
||||||
|
<h2 class="usa-alert__heading">
|
||||||
|
Your message has been sent
|
||||||
|
</h2>
|
||||||
|
<p class="usa-alert__text">
|
||||||
|
{{ job.template_name }} - {{ current_service.name }}
|
||||||
|
was sent on {{job.processing_started|format_datetime_normal}} by {{ job.created_by.name }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{display_message_status}}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
{% if job.status == 'sending limits exceeded'%}
|
{% if job.status == 'sending limits exceeded'%}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<h2 class="line-height-sans-2 margin-bottom-0 margin-top-4">Recent activity</h2>
|
||||||
|
<div id="activityChartContainer">
|
||||||
|
<form class="usa-form">
|
||||||
|
<label class="usa-label" for="options">Account</label>
|
||||||
|
<select class="usa-select margin-bottom-2" name="options" id="options">
|
||||||
|
<option value disabled>- Select -</option>
|
||||||
|
<option value="service" selected>{{ current_service.name }}</option>
|
||||||
|
<option value="individual">{{ current_user.name }}</option>
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
<div id="activityChart">
|
||||||
|
<div class="chart-header">
|
||||||
|
<div class="chart-subtitle">{{ current_service.name }} - last 7 days</div>
|
||||||
|
<div class="chart-legend" role="region" aria-label="Legend"></div>
|
||||||
|
</div>
|
||||||
|
<div class="chart-container" id="weeklyChart"></div>
|
||||||
|
<table id="weeklyTable" class="usa-sr-only usa-table"></table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="aria-live-account" class="usa-sr-only" aria-live="polite"></div>
|
||||||
|
<div class="table-container" id="activityContainer" data-currentUserName="{{ current_user.name }}" data-currentServiceId="{{current_service.id}}">
|
||||||
|
<div id="tableActivity" class="table-overflow-x-auto">
|
||||||
|
<h2 id="table-heading" class="margin-top-4 margin-bottom-1">Service activity</h2>
|
||||||
|
|
||||||
|
<table class="usa-table job-table" id="activity-table">
|
||||||
|
<caption class="usa-sr-only">Table showing the sent jobs for {{current_service.name}}</caption>
|
||||||
|
|
||||||
|
<thead class="table-field-headings">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="table-field-heading-first" id="jobId">Job ID#</th>
|
||||||
|
<th data-sortable scope="col" class="table-field-heading" scope="col">Template</th>
|
||||||
|
<th data-sortable scope="col" class="table-field-heading">Job status</th>
|
||||||
|
<th data-sortable scope="col" role="columnheader" class="table-field-heading sender-column">Sender
|
||||||
|
</th>
|
||||||
|
<th data-sortable scope="col" class="table-field-heading"># of Recipients</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% if jobs %}
|
||||||
|
{% for job in jobs %}
|
||||||
|
<tr id="{{ job.id }}">
|
||||||
|
<td class="table-field jobid" role="rowheader">
|
||||||
|
<a class="usa-link" href="{{ url_for('.view_job', service_id=current_service.id, job_id=job.id )}}">
|
||||||
|
{{ job.id[:8] if job.id else 'Manually entered number' }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="table-field template">{{ job.template_name }}</td>
|
||||||
|
<td class="table-field time-sent">
|
||||||
|
{% if job.scheduled_for and not job.processing_finished %}
|
||||||
|
Scheduled for {{ job.scheduled_for|format_datetime_table }}
|
||||||
|
{% elif job.processing_finished and not job.statistics|selectattr('status', 'equalto', 'sending')|list %}
|
||||||
|
Sent on {{ job.processing_finished|format_datetime_table }}
|
||||||
|
{% elif job.processing_started %}
|
||||||
|
Sending since {{ job.processing_started|format_datetime_table }}
|
||||||
|
{% else %}
|
||||||
|
Pending since {{ job.created_at|format_datetime_table }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="table-field sender sender-column">{{ job.created_by.name }}</td>
|
||||||
|
<td class="table-field count-of-recipients">{{ job.notification_count }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<tr class="table-row">
|
||||||
|
<td class="table-empty-message" colspan="10">No batched job messages found  (messages are
|
||||||
|
kept for {{ service_data_retention_days }} days).</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -6,140 +6,38 @@
|
|||||||
{% from "components/ajax-block.html" import ajax_block %}
|
{% from "components/ajax-block.html" import ajax_block %}
|
||||||
|
|
||||||
{% block service_page_title %}
|
{% block service_page_title %}
|
||||||
Dashboard
|
Dashboard
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block maincolumn_content %}
|
{% block maincolumn_content %}
|
||||||
<script type="text/javascript" src="{{ asset_url('js/setTimezone.js') }}"></script>
|
<script type="text/javascript" src="{{ asset_url('js/setTimezone.js') }}"></script>
|
||||||
|
|
||||||
|
|
||||||
<div class="dashboard margin-top-0 margin-bottom-2">
|
<div class="dashboard margin-top-0 margin-bottom-2">
|
||||||
|
|
||||||
<h1 class="usa-sr-only">Dashboard</h1>
|
<h1 class="usa-sr-only">Dashboard</h1>
|
||||||
{% if current_user.has_permissions('manage_templates') and not current_service.all_templates %}
|
{% if current_user.has_permissions('manage_templates') and not current_service.all_templates %}
|
||||||
{% include 'views/dashboard/write-first-messages.html' %}
|
{% include 'views/dashboard/write-first-messages.html' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{{ ajax_block(partials, updates_url, 'upcoming') }}
|
{{ ajax_block(partials, updates_url, 'upcoming') }}
|
||||||
|
|
||||||
<h2 class="font-body-2xl line-height-sans-2 margin-top-0">{{ current_service.name }} Dashboard</h2>
|
<h2 class="font-body-2xl line-height-sans-2 margin-top-0">{{ current_service.name }} Dashboard</h2>
|
||||||
|
|
||||||
{{ 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>
|
||||||
|
|
||||||
<h2 class="line-height-sans-2 margin-bottom-0 margin-top-4">Recent activity</h2>
|
{% include 'views/dashboard/activity-table.html' %}
|
||||||
<div id="activityChartContainer">
|
|
||||||
<form class="usa-form">
|
|
||||||
<label class="usa-label" for="options">Account</label>
|
|
||||||
<select class="usa-select margin-bottom-2" name="options" id="options">
|
|
||||||
<option value disabled>- Select -</option>
|
|
||||||
<option value="service" selected>{{ current_service.name }}</option>
|
|
||||||
<option value="individual">{{ current_user.name }}</option>
|
|
||||||
</select>
|
|
||||||
</form>
|
|
||||||
<div id="activityChart">
|
|
||||||
<div class="chart-header">
|
|
||||||
<div class="chart-subtitle">{{ current_service.name }} - last 7 days</div>
|
|
||||||
<div class="chart-legend" role="region" aria-label="Legend"></div>
|
|
||||||
</div>
|
|
||||||
<div class="chart-container" id="weeklyChart"></div>
|
|
||||||
<table id="weeklyTable" class="usa-sr-only usa-table"></table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="aria-live-account" class="usa-sr-only" aria-live="polite"></div>
|
|
||||||
|
|
||||||
{% if current_user.has_permissions('manage_service') %}{% endif %}
|
{% if current_user.has_permissions('manage_service') %}{% endif %}
|
||||||
|
|
||||||
<div class="table-container">
|
{{ ajax_block(partials, updates_url, 'template-statistics') }}
|
||||||
<div id="table1" class="table-overflow-x-auto hidden">
|
</div>
|
||||||
<h2 class="margin-top-4 margin-bottom-1">My activity</h2>
|
|
||||||
<table class="usa-table job-table">
|
|
||||||
<caption class="usa-sr-only">Table showing the sent jobs for {{current_user.name}}</caption>
|
|
||||||
<thead class="table-field-headings">
|
|
||||||
<tr>
|
|
||||||
<th scope="col" class="table-field-heading-first" id="jobId"><span>Job ID#</span></th>
|
|
||||||
<th data-sortable scope="col" class="table-field-heading"><span>Template</span></th>
|
|
||||||
<th data-sortable scope="col" class="table-field-heading"><span>Job status</span></th>
|
|
||||||
<th data-sortable scope="col" class="table-field-heading"><span># of Recipients</span></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% if jobs %}
|
|
||||||
{% for job in jobs[:5] %}
|
|
||||||
{% if job.created_by.name == current_user.name %}
|
|
||||||
{% set notification = job.notifications[0] %}
|
|
||||||
<tr id="{{ job.job_id }}">
|
|
||||||
<td class="table-field jobid" role="rowheader">
|
|
||||||
<a class="usa-link" href="{{ job.view_job_link }}">
|
|
||||||
{{ job.job_id[:8] if job.job_id else 'Manually entered number' }}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td class="table-field template">{{ job.template_name }}</td>
|
|
||||||
<td class="table-field time-sent">Sent on
|
|
||||||
{{ (job.processing_finished if job.processing_finished else job.processing_started
|
|
||||||
if job.processing_started else job.created_at)|format_datetime_table }}
|
|
||||||
</td>
|
|
||||||
<td class="table-field count-of-recipients">{{ job.notification_count }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
|
||||||
<tr class="table-row">
|
|
||||||
<td class="table-empty-message" colspan="10">No batched job messages found  (messages are kept for {{ service_data_retention_days }} days).</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="table2" class="table-overflow-x-auto visible">
|
|
||||||
<h2 class="margin-top-4 margin-bottom-1">Service activity</h2>
|
|
||||||
<table class="usa-table job-table">
|
|
||||||
<caption class="usa-sr-only">Table showing the sent jobs for this service</caption>
|
|
||||||
<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] %}
|
|
||||||
<tr id="{{ job.job_id }}">
|
|
||||||
<td class="table-field jobid" role="rowheader">
|
|
||||||
<a class="usa-link" href="{{ job.view_job_link }}">
|
|
||||||
{{ job.job_id[:8] if job.job_id else 'Manually entered number' }}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td class="table-field template">{{ job.template_name }}</td>
|
|
||||||
<td class="table-field time-sent">Sent on
|
|
||||||
{{ (job.processing_finished if job.processing_finished else job.processing_started
|
|
||||||
if job.processing_started else job.created_at)|format_datetime_table }}
|
|
||||||
</td>
|
|
||||||
<td class="table-field sender">{{ job.created_by.name }}</td>
|
|
||||||
<td class="table-field count-of-recipients">{{ job.notification_count }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
|
||||||
<tr class="table-row">
|
|
||||||
<td class="table-empty-message" colspan="10">No batched job messages found  (messages are kept for {{ service_data_retention_days }} days).</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{ ajax_block(partials, updates_url, 'template-statistics') }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -32,6 +32,8 @@
|
|||||||
<div class="tablet:grid-col-9 mobile-lg:grid-col-12">
|
<div class="tablet:grid-col-9 mobile-lg:grid-col-12">
|
||||||
{{ form.name(param_extensions={
|
{{ form.name(param_extensions={
|
||||||
"extra_form_group_classes": "margin-bottom-2",
|
"extra_form_group_classes": "margin-bottom-2",
|
||||||
|
"id": "name",
|
||||||
|
"required": True,
|
||||||
"hint": {"text": "Your recipients will not see this"}
|
"hint": {"text": "Your recipients will not see this"}
|
||||||
}) }}
|
}) }}
|
||||||
{{ textbox(
|
{{ textbox(
|
||||||
@@ -41,7 +43,8 @@
|
|||||||
hint=content_hint,
|
hint=content_hint,
|
||||||
rows=5,
|
rows=5,
|
||||||
extra_form_group_classes='margin-bottom-1',
|
extra_form_group_classes='margin-bottom-1',
|
||||||
placeholder='Edit me! Check out the Personalization section below for details on cool ((stuff)) you can do with your messages!'
|
placeholder='Edit me! Check out the Personalization section below for details on cool ((stuff)) you can do with your messages!',
|
||||||
|
required=True
|
||||||
) }}
|
) }}
|
||||||
{% if current_user.platform_admin %}
|
{% if current_user.platform_admin %}
|
||||||
{{ form.process_type }}
|
{{ form.process_type }}
|
||||||
|
|||||||
@@ -11,7 +11,18 @@
|
|||||||
{% block maincolumn_content %}
|
{% block maincolumn_content %}
|
||||||
|
|
||||||
{{ page_header("Message status") }}
|
{{ page_header("Message status") }}
|
||||||
{{ partials['status']|safe }}
|
{% if not job.processing_finished %}
|
||||||
|
<div
|
||||||
|
data-module="update-content"
|
||||||
|
data-resource="{{ updates_url }}"
|
||||||
|
data-key="status"
|
||||||
|
data-form=""
|
||||||
|
>
|
||||||
|
{% endif %}
|
||||||
|
{{ partials['status']|safe }}
|
||||||
|
{% if not job.processing_finished %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% if not finished %}
|
{% if not finished %}
|
||||||
<div
|
<div
|
||||||
data-module="update-content"
|
data-module="update-content"
|
||||||
|
|||||||
@@ -37,8 +37,8 @@
|
|||||||
data_kwargs={'force-focus': True}
|
data_kwargs={'force-focus': True}
|
||||||
) %}
|
) %}
|
||||||
<div class="grid-row">
|
<div class="grid-row">
|
||||||
<div class="grid-col-12 {% if form.placeholder_value.label.text == 'phone number' %}extra-tracking{% endif %}" aria-live="polite" role="alert">
|
<div class="grid-col-12 {% if form.placeholder_value.label.text == 'phone number' %}extra-tracking{% endif %}">
|
||||||
{{ form.placeholder_value(param_extensions={"classes": ""}) }}
|
{{ form.placeholder_value(param_extensions={"classes": "", "id": "phone-number"}) }}
|
||||||
</div>
|
</div>
|
||||||
{% if skip_link or link_to_upload %}
|
{% if skip_link or link_to_upload %}
|
||||||
<div class="grid-col-12 margin-top-1">
|
<div class="grid-col-12 margin-top-1">
|
||||||
|
|||||||
@@ -90,8 +90,4 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--<div class="">
|
|
||||||
{{ copy_to_clipboard(template.id, name="Template ID", thing='template ID') }}
|
|
||||||
</div>-->
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Generated
+518
-1755
File diff suppressed because it is too large
Load Diff
+5
-5
@@ -37,7 +37,7 @@
|
|||||||
"hogan": "1.0.2",
|
"hogan": "1.0.2",
|
||||||
"jquery": "3.7.1",
|
"jquery": "3.7.1",
|
||||||
"morphdom": "^2.7.4",
|
"morphdom": "^2.7.4",
|
||||||
"playwright": "^1.49.1",
|
"playwright": "^1.50.0",
|
||||||
"python": "^0.0.4",
|
"python": "^0.0.4",
|
||||||
"query-command-supported": "1.0.0",
|
"query-command-supported": "1.0.0",
|
||||||
"sass-embedded": "^1.83.4",
|
"sass-embedded": "^1.83.4",
|
||||||
@@ -47,8 +47,8 @@
|
|||||||
"vinyl-source-stream": "^2.0.0"
|
"vinyl-source-stream": "^2.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.26.0",
|
"@babel/core": "^7.26.7",
|
||||||
"@babel/preset-env": "^7.26.0",
|
"@babel/preset-env": "^7.26.7",
|
||||||
"@uswds/compile": "^1.2.1",
|
"@uswds/compile": "^1.2.1",
|
||||||
"backstopjs": "^6.3.25",
|
"backstopjs": "^6.3.25",
|
||||||
"better-npm-audit": "^3.11.0",
|
"better-npm-audit": "^3.11.0",
|
||||||
@@ -61,12 +61,12 @@
|
|||||||
"gulp-jshint": "2.1.0",
|
"gulp-jshint": "2.1.0",
|
||||||
"gulp-prettyerror": "2.0.0",
|
"gulp-prettyerror": "2.0.0",
|
||||||
"gulp-uglify": "3.0.2",
|
"gulp-uglify": "3.0.2",
|
||||||
"jest": "29.7.0",
|
"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": "^4.31.0",
|
"rollup": "^4.32.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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -657,9 +657,8 @@ def test_should_show_recent_templates_on_dashboard(
|
|||||||
]
|
]
|
||||||
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")[1].find_all("tr")
|
||||||
|
assert len(table_rows) == 2
|
||||||
assert len(table_rows) == 0
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@@ -1943,7 +1942,6 @@ def test_service_dashboard_shows_batched_jobs(
|
|||||||
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) == 1
|
||||||
assert len(rows) == 0
|
|
||||||
|
|
||||||
assert job_table_body is not None
|
assert job_table_body is not None
|
||||||
|
|||||||
@@ -1521,7 +1521,7 @@ def test_link_to_upload_not_offered_when_entering_personalisation(
|
|||||||
|
|
||||||
# We’re entering personalization
|
# We’re entering personalization
|
||||||
assert page.select_one("input[type=text]")["name"] == "placeholder_value"
|
assert page.select_one("input[type=text]")["name"] == "placeholder_value"
|
||||||
assert page.select_one("label[for=placeholder_value]").text.strip() == "name"
|
assert page.select_one("label[for=phone-number]").text.strip() == "name"
|
||||||
# No ‘Upload’ link shown
|
# No ‘Upload’ link shown
|
||||||
assert len(page.select("main a")) == 0
|
assert len(page.select("main a")) == 0
|
||||||
assert "Upload" not in page.select_one("main").text
|
assert "Upload" not in page.select_one("main").text
|
||||||
|
|||||||
@@ -174,9 +174,7 @@ def test_should_show_empty_text_box(
|
|||||||
# data-module=autofocus is set on a containing element so it
|
# data-module=autofocus is set on a containing element so it
|
||||||
# shouldn’t also be set on the textbox itself
|
# shouldn’t also be set on the textbox itself
|
||||||
assert "data-module" not in textbox
|
assert "data-module" not in textbox
|
||||||
assert (
|
assert normalize_spaces(page.select_one("label[for=phone-number]").text) == "one"
|
||||||
normalize_spaces(page.select_one("label[for=placeholder_value]").text) == "one"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_should_prefill_answers_for_get_tour_step(
|
def test_should_prefill_answers_for_get_tour_step(
|
||||||
|
|||||||
@@ -141,8 +141,15 @@ def test_get_notification(mocker):
|
|||||||
|
|
||||||
def test_get_notification_count_for_job_id(mocker):
|
def test_get_notification_count_for_job_id(mocker):
|
||||||
mock_get = mocker.patch(
|
mock_get = mocker.patch(
|
||||||
"app.notify_client.notification_api_client.NotificationApiClient.get"
|
"app.notify_client.notification_api_client.NotificationApiClient.get",
|
||||||
|
return_value={"count": 0},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
mocker.patch(
|
||||||
|
"app.notify_client.notification_api_client.redis_client.get", return_value=None
|
||||||
|
)
|
||||||
|
|
||||||
|
mocker.patch("app.notify_client.billing_api_client.redis_client.set")
|
||||||
NotificationApiClient().get_notification_count_for_job_id(
|
NotificationApiClient().get_notification_count_for_job_id(
|
||||||
service_id="foo", job_id="bar"
|
service_id="foo", job_id="bar"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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">
|
||||||
@@ -40,6 +40,9 @@ beforeAll(done => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="aria-live-account" class="usa-sr-only" aria-live="polite"></div>
|
<div id="aria-live-account" class="usa-sr-only" aria-live="polite"></div>
|
||||||
|
<div id="activityContainer" data-currentUserName="Test User" data-currentServiceId="12345"></div>
|
||||||
|
|
||||||
|
<div id="aria-live-account" class="usa-sr-only" aria-live="polite"></div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Load the D3 script dynamically
|
// Load the D3 script dynamically
|
||||||
@@ -61,13 +64,13 @@ test('D3 is loaded correctly', () => {
|
|||||||
|
|
||||||
test('Populates the accessible table for activity chart correctly', () => {
|
test('Populates the accessible table for activity chart correctly', () => {
|
||||||
const sampleData = {
|
const sampleData = {
|
||||||
'2024-07-01': { sms: { delivered: 50, failed: 5 } },
|
'2024-07-01': { sms: { delivered: 50, failed: 5, pending: 10 } },
|
||||||
'2024-07-02': { sms: { delivered: 60, failed: 2 } },
|
'2024-07-02': { sms: { delivered: 60, failed: 2, pending: 5 } },
|
||||||
'2024-07-03': { sms: { delivered: 70, failed: 1 } },
|
'2024-07-03': { sms: { delivered: 70, failed: 1, pending: 3 } },
|
||||||
'2024-07-04': { sms: { delivered: 80, failed: 0 } },
|
'2024-07-04': { sms: { delivered: 80, failed: 0, pending: 0 } },
|
||||||
'2024-07-05': { sms: { delivered: 90, failed: 3 } },
|
'2024-07-05': { sms: { delivered: 90, failed: 3, pending: 8 } },
|
||||||
'2024-07-06': { sms: { delivered: 100, failed: 4 } },
|
'2024-07-06': { sms: { delivered: 100, failed: 4, pending: 7 } },
|
||||||
'2024-07-07': { sms: { delivered: 110, failed: 2 } },
|
'2024-07-07': { sms: { delivered: 110, failed: 2, pending: 6 } },
|
||||||
};
|
};
|
||||||
|
|
||||||
const labels = Object.keys(sampleData).map(dateString => {
|
const labels = Object.keys(sampleData).map(dateString => {
|
||||||
@@ -76,8 +79,9 @@ test('Populates the accessible table for activity chart correctly', () => {
|
|||||||
});
|
});
|
||||||
const deliveredData = Object.values(sampleData).map(d => d.sms.delivered);
|
const deliveredData = Object.values(sampleData).map(d => d.sms.delivered);
|
||||||
const failedData = Object.values(sampleData).map(d => d.sms.failed);
|
const failedData = Object.values(sampleData).map(d => d.sms.failed);
|
||||||
|
const pendingData = Object.values(sampleData).map(d => d.sms.pending);
|
||||||
|
|
||||||
window.createTable('weeklyTable', 'activityChart', labels, deliveredData, failedData);
|
window.createTable('weeklyTable', 'activityChart', labels, deliveredData, failedData, pendingData);
|
||||||
|
|
||||||
const table = document.getElementById('weeklyTable');
|
const table = document.getElementById('weeklyTable');
|
||||||
expect(table).toBeDefined();
|
expect(table).toBeDefined();
|
||||||
@@ -89,6 +93,7 @@ test('Populates the accessible table for activity chart correctly', () => {
|
|||||||
expect(headers[0].textContent).toBe('Day');
|
expect(headers[0].textContent).toBe('Day');
|
||||||
expect(headers[1].textContent).toBe('Delivered');
|
expect(headers[1].textContent).toBe('Delivered');
|
||||||
expect(headers[2].textContent).toBe('Failed');
|
expect(headers[2].textContent).toBe('Failed');
|
||||||
|
expect(headers[3].textContent).toBe('Pending');
|
||||||
|
|
||||||
const firstRowCells = rows[1].getElementsByTagName('td');
|
const firstRowCells = rows[1].getElementsByTagName('td');
|
||||||
expect(firstRowCells[0].textContent).toBe('07/01/24');
|
expect(firstRowCells[0].textContent).toBe('07/01/24');
|
||||||
@@ -97,58 +102,73 @@ test('Populates the accessible table for activity chart correctly', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('SVG element is correctly set up', () => {
|
test('SVG element is correctly set up', () => {
|
||||||
window.createChart('#weeklyChart', ['07/01/24', '07/02/24', '07/03/24', '07/04/24', '07/05/24', '07/06/24', '07/07/24'], [50, 60, 70, 80, 90, 100, 110], [5, 2, 1, 0, 3, 4, 2]);
|
window.createChart(
|
||||||
|
'#weeklyChart',
|
||||||
|
['07/01/24', '07/02/24', '07/03/24', '07/04/24', '07/05/24', '07/06/24', '07/07/24'],
|
||||||
|
[50, 60, 70, 80, 90, 100, 110],
|
||||||
|
[5, 2, 1, 0, 3, 4, 2],
|
||||||
|
[10, 5, 3, 0, 8, 7, 6]
|
||||||
|
);
|
||||||
|
|
||||||
const svg = document.getElementById('weeklyChart').querySelector('svg');
|
const svg = document.getElementById('weeklyChart').querySelector('svg');
|
||||||
expect(svg).not.toBeNull();
|
expect(svg).not.toBeNull();
|
||||||
expect(svg.getAttribute('width')).toBe('0');
|
expect(svg.querySelectorAll('.bar-group').length).toBe(3);
|
||||||
expect(svg.getAttribute('height')).toBe('400');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Check HTML content after chart creation', () => {
|
test('Check HTML content after chart creation', () => {
|
||||||
// Create sample data for the chart
|
|
||||||
const labels = ['07/01/24', '07/02/24', '07/03/24', '07/04/24', '07/05/24', '07/06/24', '07/07/24'];
|
const labels = ['07/01/24', '07/02/24', '07/03/24', '07/04/24', '07/05/24', '07/06/24', '07/07/24'];
|
||||||
const deliveredData = [50, 60, 70, 80, 90, 100, 110];
|
const deliveredData = [50, 60, 70, 80, 90, 100, 110];
|
||||||
const failedData = [5, 2, 1, 0, 3, 4, 2];
|
const failedData = [5, 2, 1, 0, 3, 4, 2];
|
||||||
|
const pendingData = [10, 5, 8, 3, 6, 7, 4];
|
||||||
|
|
||||||
// Ensure the container has the correct width
|
|
||||||
const container = document.getElementById('weeklyChart');
|
const container = document.getElementById('weeklyChart');
|
||||||
container.style.width = '600px'; // Force a specific width
|
container.style.width = '600px';
|
||||||
const containerWidth = container.clientWidth;
|
const containerWidth = container.clientWidth;
|
||||||
expect(containerWidth).toBeGreaterThan(0);
|
expect(containerWidth).toBeGreaterThan(0);
|
||||||
|
|
||||||
// Call the function to create the chart
|
window.createChart('#weeklyChart', labels, deliveredData, failedData, pendingData);
|
||||||
window.createChart('#weeklyChart', labels, deliveredData, failedData);
|
|
||||||
|
|
||||||
// Optionally, you can add assertions to check for specific elements
|
const svg = container.querySelector('svg');
|
||||||
expect(container.querySelector('svg')).not.toBeNull();
|
expect(svg).not.toBeNull();
|
||||||
expect(container.querySelectorAll('rect').length).toBeGreaterThan(0);
|
|
||||||
|
const bars = container.querySelectorAll('rect');
|
||||||
|
expect(bars.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
const barGroups = svg.querySelectorAll('.bar-group');
|
||||||
|
expect(barGroups.length).toBe(3);
|
||||||
|
|
||||||
|
const pendingBars = Array.from(bars).filter(bar =>
|
||||||
|
bar.parentNode.getAttribute('fill') === '#C7CACE'
|
||||||
|
);
|
||||||
|
expect(pendingBars.length).toBe(labels.length);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Legend is visible when there are delivered or failed messages', () => {
|
test('Legend includes pending when data exists', () => {
|
||||||
// Example data with delivered and failed messages
|
|
||||||
const labels = ['Day 1', 'Day 2'];
|
const labels = ['Day 1', 'Day 2'];
|
||||||
const deliveredData = [10, 20]; // Mock delivered data
|
const deliveredData = [10, 20];
|
||||||
const failedData = [5, 0]; // Mock failed data
|
const failedData = [5, 0];
|
||||||
|
const pendingData = [3, 2];
|
||||||
|
|
||||||
// Call the createChart function
|
window.createChart('#weeklyChart', labels, deliveredData, failedData, pendingData);
|
||||||
window.createChart('#weeklyChart', labels, deliveredData, failedData);
|
|
||||||
|
|
||||||
// Check if the legend is displayed using computed style
|
|
||||||
const legendContainer = document.querySelector('.chart-legend');
|
const legendContainer = document.querySelector('.chart-legend');
|
||||||
const legendDisplayStyle = window.getComputedStyle(legendContainer).display;
|
const legendItems = legendContainer.querySelectorAll('.legend-item');
|
||||||
expect(legendDisplayStyle).toBe('flex');
|
expect(legendItems.length).toBe(3);
|
||||||
expect(legendContainer.querySelectorAll('.legend-item').length).toBe(2); // Ensure two legend items
|
|
||||||
|
const pendingLegend = Array.from(legendItems).find(item =>
|
||||||
|
item.textContent.includes('Pending')
|
||||||
|
);
|
||||||
|
expect(pendingLegend).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Legend is hidden when there are no delivered or failed messages', () => {
|
test('Legend is hidden when there are no delivered, failed, or pending messages', () => {
|
||||||
// Example data with no delivered and no failed messages
|
|
||||||
const labels = ['Day 1', 'Day 2'];
|
const labels = ['Day 1', 'Day 2'];
|
||||||
const deliveredData = [0, 0]; // No delivered messages
|
const deliveredData = [0, 0];
|
||||||
const failedData = [0, 0]; // No failed messages
|
const failedData = [0, 0];
|
||||||
|
const pendingData = [0, 0];
|
||||||
|
|
||||||
// Call the createChart function
|
// Call the createChart function
|
||||||
window.createChart('#weeklyChart', labels, deliveredData, failedData);
|
window.createChart('#weeklyChart', labels, deliveredData, failedData, pendingData);
|
||||||
|
|
||||||
// Check if the legend is hidden using computed style
|
// Check if the legend is hidden using computed style
|
||||||
const legendContainer = document.querySelector('.chart-legend');
|
const legendContainer = document.querySelector('.chart-legend');
|
||||||
@@ -158,23 +178,102 @@ test('Legend is hidden when there are no delivered or failed messages', () => {
|
|||||||
|
|
||||||
test('Fetches data and creates chart and table correctly', async () => {
|
test('Fetches data and creates chart and table correctly', async () => {
|
||||||
const mockResponse = {
|
const mockResponse = {
|
||||||
'2024-07-01': { sms: { delivered: 50, failed: 5 } },
|
'2024-07-01': { sms: { delivered: 50, failed: 5, pending: 10 } },
|
||||||
'2024-07-02': { sms: { delivered: 60, failed: 2 } },
|
'2024-07-02': { sms: { delivered: 60, failed: 2, pending: 8 } },
|
||||||
'2024-07-03': { sms: { delivered: 70, failed: 1 } },
|
'2024-07-03': { sms: { delivered: 70, failed: 1, pending: 6 } },
|
||||||
'2024-07-04': { sms: { delivered: 80, failed: 0 } },
|
'2024-07-04': { sms: { delivered: 80, failed: 0, pending: 4 } },
|
||||||
'2024-07-05': { sms: { delivered: 90, failed: 3 } },
|
'2024-07-05': { sms: { delivered: 90, failed: 3, pending: 7 } },
|
||||||
'2024-07-06': { sms: { delivered: 100, failed: 4 } },
|
'2024-07-06': { sms: { delivered: 100, failed: 4, pending: 5 } },
|
||||||
'2024-07-07': { sms: { delivered: 110, failed: 2 } },
|
'2024-07-07': { sms: { delivered: 110, failed: 2, pending: 3 } },
|
||||||
};
|
};
|
||||||
|
const tableContainer = document.getElementById('activityContainer');
|
||||||
|
const currentServiceId = tableContainer.getAttribute('data-currentServiceId');
|
||||||
|
|
||||||
|
|
||||||
global.fetch = jest.fn(() =>
|
global.fetch = jest.fn(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
ok: true,
|
ok: true,
|
||||||
json: () => Promise.resolve(mockResponse),
|
json: () => Promise.resolve(mockResponse),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const data = await fetchData('service');
|
const data = await fetchData('service');
|
||||||
|
|
||||||
expect(global.fetch).toHaveBeenCalledWith('/daily_stats.json');
|
expect(global.fetch).toHaveBeenCalledWith(`/services/${currentServiceId}/daily-stats.json`);
|
||||||
expect(data).toEqual(mockResponse);
|
expect(data).toEqual(mockResponse);
|
||||||
|
|
||||||
|
const labels = Object.keys(mockResponse).map(dateString => {
|
||||||
|
const dateParts = dateString.split('-');
|
||||||
|
return `${dateParts[1]}/${dateParts[2]}/${dateParts[0].slice(2)}`;
|
||||||
|
});
|
||||||
|
const deliveredData = Object.values(mockResponse).map(d => d.sms.delivered);
|
||||||
|
const failedData = Object.values(mockResponse).map(d => d.sms.failed);
|
||||||
|
const pendingData = Object.values(mockResponse).map(d => d.sms.pending);
|
||||||
|
|
||||||
|
window.createChart('#weeklyChart', labels, deliveredData, failedData, pendingData);
|
||||||
|
window.createTable('weeklyTable', 'activityChart', labels, deliveredData, failedData, pendingData);
|
||||||
|
|
||||||
|
const chart = document.getElementById('weeklyChart').querySelector('svg');
|
||||||
|
expect(chart).not.toBeNull();
|
||||||
|
|
||||||
|
const table = document.getElementById('weeklyTable');
|
||||||
|
expect(table).toBeDefined();
|
||||||
|
const rows = table.getElementsByTagName('tr');
|
||||||
|
expect(rows.length).toBe(8);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handleDropdownChange updates DOM for individual selection', () => {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<div id="activityChartContainer">
|
||||||
|
<div class="chart-subtitle"></div>
|
||||||
|
</div>
|
||||||
|
<div id="aria-live-account"></div>
|
||||||
|
<div id="activityContainer" data-currentUserName="Test User" data-currentServiceId="12345"></div>
|
||||||
|
<div id="tableActivity">
|
||||||
|
<h2 id="table-heading"></h2>
|
||||||
|
<table id="activity-table">
|
||||||
|
<caption id="caption"></caption>
|
||||||
|
<tbody>
|
||||||
|
<tr><td class="sender-column">Test User</td></tr>
|
||||||
|
<tr><td class="sender-column">Other User</td></tr>
|
||||||
|
<tr><td class="sender-column">Test User</td></tr>
|
||||||
|
<tr><td class="sender-column">Test User</td></tr>
|
||||||
|
<tr><td class="sender-column">Other User</td></tr>
|
||||||
|
<tr><td class="sender-column">Test User</td></tr>
|
||||||
|
<tr><td class="sender-column">Test User</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<select id="options">
|
||||||
|
<option value="service">Service</option>
|
||||||
|
<option value="individual">Individual</option>
|
||||||
|
</select>
|
||||||
|
`;
|
||||||
|
|
||||||
|
window.currentUserName = "Test User";
|
||||||
|
|
||||||
|
jest.spyOn(window, 'fetchData').mockImplementation(() => {});
|
||||||
|
|
||||||
|
const selectElement = document.getElementById('options');
|
||||||
|
selectElement.value = 'individual';
|
||||||
|
const event = { target: selectElement };
|
||||||
|
|
||||||
|
window.handleDropdownChange(event);
|
||||||
|
|
||||||
|
expect(document.getElementById('table-heading').textContent).toBe('My activity');
|
||||||
|
expect(document.getElementById('caption').textContent).toContain('Test User');
|
||||||
|
|
||||||
|
document.querySelectorAll('.sender-column').forEach(col => {
|
||||||
|
expect(col.style.display).toBe('none');
|
||||||
|
});
|
||||||
|
|
||||||
|
const rows = Array.from(document.querySelectorAll('#activity-table tbody tr'));
|
||||||
|
const visibleRows = rows.filter(row => row.style.display !== 'none');
|
||||||
|
expect(visibleRows.length).toBeLessThanOrEqual(5);
|
||||||
|
visibleRows.forEach(row => {
|
||||||
|
const sender = row.querySelector('.sender-column').textContent.trim();
|
||||||
|
expect(sender).toBe('Test User');
|
||||||
|
});
|
||||||
|
|
||||||
|
window.fetchData.mockRestore();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user