Compare commits

..

1 Commits

Author SHA1 Message Date
Sheev Davé
f384ba00bb Update issue_template.yml
added accessibility secton
2025-01-27 11:49:05 -08:00
39 changed files with 2435 additions and 1273 deletions

View File

@@ -68,7 +68,7 @@ body:
attributes:
value: '**Accessibility:**'
- type: textarea
id: accessibility
id: notes
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."

View File

@@ -23,7 +23,5 @@ Please enter a detailed description here.
## A11y Checks (if applicable)
* 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/)
* Conduct automated 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)
* Make sure there are no linting errors in VSCode or other IDE of choice

View File

@@ -165,9 +165,8 @@ jobs:
run: make run-flask &
env:
NOTIFY_ENVIRONMENT: scanning
FEATURE_ABOUT_PAGE_ENABLED: true
- name: Run OWASP Baseline Scan
uses: zaproxy/action-baseline@v0.14.0
uses: zaproxy/action-baseline@v0.9.0
with:
docker_name: "ghcr.io/zaproxy/zaproxy:weekly"
target: "http://localhost:6012"

View File

@@ -50,7 +50,7 @@ jobs:
env:
NOTIFY_ENVIRONMENT: scanning
- name: Run OWASP Full Scan
uses: zaproxy/action-full-scan@v0.12.0
uses: zaproxy/action-full-scan@v0.7.0
with:
docker_name: 'ghcr.io/zaproxy/zaproxy:weekly'
target: 'http://localhost:6012'

View File

@@ -19,7 +19,7 @@
<link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicon-16x16.png">
<link rel="manifest" href="{{ asset_url('images/site.webmanifest') }}">
<link rel="stylesheet" media="screen" href="/static/stylesheets/legacy/uk.css?d077f86473501ab244de0b30600536ee" />
<link rel="stylesheet" media="screen" href="/static/stylesheets/main.css?d077f86473501ab244de0b30600536ee" />
<link rel="stylesheet" media="print" href="/static/stylesheets/print.css?28010888ca5719dc83d7c2c80f6ed2b2" />
<meta property="og:image" content="/static/images/notify-og-image.png">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 474 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 362 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 373 KiB

View File

@@ -1,14 +1,11 @@
(function (window) {
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 = {
delivered: '#0076d6',
failed: '#fa9441',
pending: '#C7CACE',
text: '#666'
};
@@ -16,7 +13,7 @@
const FONT_WEIGHT = 'bold';
const MAX_Y = 120;
const createChart = function(containerId, labels, deliveredData, failedData, pendingData) {
const createChart = function(containerId, labels, deliveredData, failedData) {
const container = d3.select(containerId);
container.selectAll('*').remove(); // Clear any existing content
@@ -39,7 +36,7 @@
}
// Calculate total messages
const totalMessages = d3.sum(deliveredData) + d3.sum(failedData) + d3.sum(pendingData);
const totalMessages = d3.sum(deliveredData) + d3.sum(failedData);
// Create legend only if there are messages
const legendContainer = d3.select('.chart-legend');
@@ -49,8 +46,7 @@
// Show legend if there are messages
const legendData = [
{ label: 'Delivered', color: COLORS.delivered },
{ label: 'Failed', color: COLORS.failed },
{ label: 'Pending', color: COLORS.pending }
{ label: 'Failed', color: COLORS.failed }
];
const legendItem = legendContainer.selectAll('.legend-item')
@@ -81,9 +77,8 @@
.range([0, width])
.padding(0.1);
// Adjust the y-axis domain to add some space above the tallest bar
const maxY = d3.max(deliveredData.map((d, i) => d + (failedData[i] || 0) + (pendingData[i] || 0)));
const y = d3.scaleSymlog()
const maxY = d3.max(deliveredData.map((d, i) => d + (failedData[i] || 0)));
const y = d3.scaleSqrt()
.domain([0, maxY + 2]) // Add 2 units of space at the top
.nice()
.range([height, 0]);
@@ -95,7 +90,7 @@
// Generate the y-axis with whole numbers
const yAxis = d3.axisLeft(y)
.ticks(Math.min(maxY + 2, 3))
.ticks(Math.min(maxY + 2, 10)) // Generate up to 10 ticks based on the data
.tickFormat(d3.format('d')); // Ensure whole numbers on the y-axis
svg.append('g')
@@ -106,13 +101,12 @@
const stackData = labels.map((label, i) => ({
label: label,
delivered: deliveredData[i],
failed: failedData[i] || 0,
pending: pendingData[i] || 0
failed: failedData[i] || 0 // Ensure there's a value for failed, even if it's 0
}));
// Stack the data
const stack = d3.stack()
.keys(['delivered', 'failed', 'pending'])
.keys(['delivered', 'failed'])
.order(d3.stackOrderNone)
.offset(d3.stackOffsetNone);
@@ -120,8 +114,8 @@
// Color scale
const color = d3.scaleOrdinal()
.domain(['delivered', 'failed', 'pending'])
.range([COLORS.delivered, COLORS.failed, COLORS.pending]);
.domain(['delivered', 'failed'])
.range([COLORS.delivered, COLORS.failed]);
// Create bars with animation
const barGroups = svg.selectAll('.bar-group')
@@ -159,7 +153,7 @@
};
// Function to create an accessible table
const createTable = function(tableId, chartType, labels, deliveredData, failedData, pendingData) {
const createTable = function(tableId, chartType, labels, deliveredData, failedData) {
const table = document.getElementById(tableId);
table.innerHTML = ""; // Clear previous data
@@ -171,7 +165,7 @@
// Create table header
const headerRow = document.createElement('tr');
const headers = ['Day', 'Delivered', 'Failed', 'Pending'];
const headers = ['Day', 'Delivered', 'Failed'];
headers.forEach(headerText => {
const th = document.createElement('th');
th.textContent = headerText;
@@ -194,10 +188,6 @@
cellFailed.textContent = failedData[index];
row.appendChild(cellFailed);
const cellPending = document.createElement('td');
cellPending.textContent = pendingData[index];
row.appendChild(cellPending);
tbody.appendChild(row);
});
@@ -207,13 +197,12 @@
};
const fetchData = function(type) {
var ctx = document.getElementById('weeklyChart');
if (!ctx) {
return;
}
var url = type === 'service' ? `/services/${currentServiceId}/daily-stats.json` : `/services/${currentServiceId}/daily-stats-by-user.json`;
var url = type === 'service' ? `/daily_stats.json` : `/daily_stats_by_user.json`;
return fetch(url)
.then(response => {
if (!response.ok) {
@@ -225,7 +214,7 @@
labels = [];
deliveredData = [];
failedData = [];
pendingData = [];
let totalMessages = 0;
for (var dateString in data) {
@@ -236,8 +225,6 @@
labels.push(formattedDate);
deliveredData.push(data[dateString].sms.delivered);
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
totalMessages += data[dateString].sms.delivered + data[dateString].sms.failure;
@@ -266,18 +253,17 @@
}
} else {
// If there are messages, create the chart and table
createChart('#weeklyChart', labels, deliveredData, failedData, pendingData);
createTable('weeklyTable', 'activityChart', labels, deliveredData, failedData, pendingData);
}
createChart('#weeklyChart', labels, deliveredData, failedData);
createTable('weeklyTable', 'activityChart', labels, deliveredData, failedData);
}
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 selectedValue = event.target.value;
currentType = selectedValue;
const subTitle = document.querySelector(`#activityChartContainer .chart-subtitle`);
const selectElement = document.getElementById('options');
const selectedText = selectElement.options[selectElement.selectedIndex].text;
@@ -330,7 +316,7 @@
document.addEventListener('DOMContentLoaded', function() {
// Initialize activityChart chart and table with service data by default
fetchData(currentType);
fetchData('service');
const allRows = Array.from(document.querySelectorAll('#activity-table tbody tr'));
allRows.forEach((row, index) => {
@@ -343,9 +329,9 @@
// Resize chart on window resize
window.addEventListener('resize', function() {
if (labels.length > 0 && deliveredData.length > 0 && failedData.length > 0 && pendingData.length > 0) {
createChart('#weeklyChart', labels, deliveredData, failedData, pendingData);
createTable('weeklyTable', 'activityChart', labels, deliveredData, failedData, pendingData);
if (labels.length > 0 && deliveredData.length > 0 && failedData.length > 0) {
createChart('#weeklyChart', labels, deliveredData, failedData);
createTable('weeklyTable', 'activityChart', labels, deliveredData, failedData);
}
});

View File

@@ -1,186 +0,0 @@
@use "uswds-core" as *;
.dashboard {
.big-number-with-status {
.big-number-smaller {
display: flex;
flex-direction: column;
.big-number-number {
font-size: units(5);
line-height: units(5);
}
.big-number-label {
font-size: units(2.5);
}
}
.big-number-status {
background: color('green-cool-40v');
display: flex;
padding: units(1) units(2);
&--failing {
padding: 0;
a.usa-link {
color: white;
background: color('red-warm-50v');
padding: units(1) units(2);
margin: 0;
width: 100%;
&:hover {
background: color('red-warm-60v');
}
}
}
}
}
.usa-table {
width: 100%;
caption {
margin-bottom: 0;
}
.table-field-center-aligned {
text-align: center;
}
.template-statistics-table-template-name {
padding-left: units(4);
display: inline-block;
background-repeat: no-repeat;
background-image: url(../img/material-icons/description.svg);
}
}
.get-started {
border: 1px solid color('gray-90');
padding: units(2);
margin-bottom: units(4);
}
}
.dashboard-table {
table {
width: 100%;
}
.file-list-filename {
font-weight: bold;
}
.file-list-hint {
margin: 0;
word-break: break-word;
}
.table-field,
.table-field-right-aligned {
width: 50%;
}
&.usage-table {
.table-field,
.table-field-left-aligned,
.table-field-right-aligned {
width: auto;
}
}
}
.job-status-table {
table-layout: fixed;
thead tr th {
border-bottom: 0;
}
thead,
tbody,
tr {
width: 100%;
}
th:first-child,
td:first-child {
width: 75%;
}
th:nth-child(2),
R td:nth-child(2) {
width: 25%;
}
}
.usage-table {
ul {
list-style: none;
padding: 0;
margin: 0;
}
.big-number-smallest {
display: flex;
flex-direction: column;
}
}
.job-table {
width: 100%;
border-collapse: collapse;
td.file-name {
width: 25%;
overflow-wrap: anywhere;
}
td.jobid {
width: 5%;
}
td.template {
width: 25%;
}
td.time-sent {
width: 15%;
}
td.sender {
width: 20%;
overflow-wrap: break-word;
}
td.count-of-recipients {
width: 5%;
}
td.report {
width: 2%;
text-align: center;
}
td.delivered {
width: 2%;
text-align: center;
}
td.failed {
width: 2%;
text-align: center;
}
td.report img {
padding-top: 5px;
}
th {
padding: 0.5rem 1rem;
}
}
@media (max-width: 768px) {
.usa-table-container--scrollable-mobile {
margin: 0;
overflow-y: hidden;
}
}
.usa-table th[data-sortable][aria-sort='ascending'],
.usa-table th[data-sortable][aria-sort='descending'] {
background-color: #a1d3ff;
}
#template-list {
max-height: 500px;
overflow-y: auto;
padding: units(1) 0 units(1) units(1);
margin: units(2) 0 units(5);
ul {
padding: 0;
margin: 0;
list-style: none;
}
}
.usa-prose > p.max-width-full {
max-width: 100%;
}

View File

@@ -1,81 +0,0 @@
@use 'uswds-core' as *;
$do-dont-color-do: 'green-cool-50v';
$do-dont-color-dont: 'red-cool-50v';
$do-dont-color-border: 'gray-cool-20';
$do-dont-padding: 3;
$do-dont-top-bar-width: 1;
.do-dont {
display: flex;
height: 100%;
padding-top: units(2);
}
.do-dont__do,
.do-dont__dont {
background-color: color('white');
border: 1px solid color($do-dont-color-border);
flex: 1 0 0;
height: 100%;
position: relative;
}
.do-dont__do {
padding: 0rem;
}
.do-dont__do:before,
.do-dont__dont:before {
content: '';
position: absolute;
height: units($do-dont-top-bar-width);
width: calc(100% + 2px);
top: -1px;
left: -1px;
}
.do-dont__do:before {
background-color: color($do-dont-color-do);
}
.do-dont__dont:before {
background-color: color($do-dont-color-dont);
}
.do-dont__heading {
// @include typeset("lang", "lg", 2);
align-items: center;
border-bottom: 1px solid color($do-dont-color-border);
display: flex;
font-weight: fw('bold');
margin: 0;
padding: units(4) units($do-dont-padding) units($do-dont-padding);
.usa-icon {
@include u-square(3);
margin-right: units(0.5);
}
}
.do-dont__do .do-dont__heading {
color: color($do-dont-color-do);
}
.do-dont__dont .do-dont__heading {
color: color($do-dont-color-dont);
}
.do-dont__content {
padding: 0 units($do-dont-padding) units($do-dont-padding)
units($do-dont-padding);
ul {
margin-bottom: 0;
overflow-wrap: anywhere;
}
li + li {
margin-top: units(1);
}
}

View File

@@ -1,79 +0,0 @@
@use "uswds-core" as *;
@use "tokens" as vars;
.usa-section--dark {
background-color: color('gray-90');
h1 {
font-family: family('sans');
font-size: units(7);
color: vars.$notify-light-blue;
@media (max-width: units('desktop')) {
font-size: units(5);
}
}
}
.usa-hero > .grid-container {
@media (max-width: units('tablet')) {
padding-top: 1rem;
padding-bottom: 1rem;
}
}
.usa-section {
&.key-features {
background-color: color('gray-cool-2');
}
&.security-and-privacy {
img {
filter: brightness(0) saturate(100%) invert(22%) sepia(98%) saturate(1829%) hue-rotate(187deg) brightness(88%) contrast(101%);
}
}
&__home {
h2 {
font-size: units(6);
line-height: 1.2;
@media (max-width: units('desktop')) {
font-size: units(4);
}
}
p {
font-size: 20px;
}
img {
@media (max-width: units('tablet')) {
max-width: 60%;
}
}
}
}
.home-cards {
justify-content: space-between;
.usa-card__container {
align-items: center;
text-align: center;
overflow: hidden;
border: 0;
background-color: transparent;
.img-container {
width: units('card');
height: units('card');
margin: units(3) 0;
border-radius: 50%;
background: color('gray-cool-10');
display: flex;
align-items: center;
justify-content: center;
img {
width: units(10);
height: units(10);
filter: brightness(0) saturate(100%) invert(22%) sepia(98%) saturate(1829%) hue-rotate(187deg) brightness(88%) contrast(101%);
}
}
h3, p {
font-size: units(3);
line-height: 1.3;
}
}
}

View File

@@ -1,35 +0,0 @@
@use "uswds-core" as *;
// Tabs
.tabs {
.pill {
display: flex;
list-style: none;
padding: 0;
.pill-item__container {
border: 1px solid color('gray-cool-10');
flex: 1;
display: flex;
flex-direction: column;
text-align: center;
font-size: units(2);
a {
padding: units(4);
.big-number-smaller {
font-size: units(5);
line-height: units(6);
}
.big-number-smallest {
font-size: units(3);
}
&:not(.pill-item--selected):hover {
background: color('blue-warm-70v');
}
&.pill-item--selected:hover {
color: color('blue-60v');
}
}
}
}
}

View File

@@ -1,8 +0,0 @@
@use "uswds-core" as *;
// Colors
$notify-primary: color('blue-60v');
$notify-secondary: color('red-warm-50v');
$notify-light-blue: color('blue-40v');
$notify-tertiary: color('gold-20v');
$notify-text: color('ink');

View File

@@ -1,3 +1,25 @@
/*
* * * * * ==============================
* * * * * ==============================
* * * * * ==============================
* * * * * ==============================
========================================
========================================
========================================
----------------------------------------
USWDS THEME CUSTOM STYLES
----------------------------------------
!! Copy this file to your project's
sass root. Don't edit the version
in node_modules.
----------------------------------------
Custom project SASS goes here.
i.e.
@include u-padding-right('05');
----------------------------------------
*/
@use 'uswds-core' as *;
iframe:focus,
@@ -34,10 +56,7 @@ button:not([disabled]):focus {
}
}
@include at-media-max('desktop') {
.usa-nav__secondary-links
{
padding-left: units(2);
}
padding: 0 units(2);
ul li {
padding-bottom: units(1);
}
@@ -141,17 +160,9 @@ td.table-empty-message {
}
}
.login-button {
margin-right: 0;
img {
height: 1rem;
}
@media (max-width: units('desktop')) {
img {
height: 14px;
}
font-size: 14px;
}
.usa-button img {
margin-left: 0.5rem;
height: 1rem;
}
.usa-button.login-button.login-button--primary,
@@ -354,6 +365,227 @@ td.table-empty-message {
}
}
// Dashboard
.dashboard {
.big-number-with-status {
.big-number-smaller {
display: flex;
flex-direction: column;
.big-number-number {
font-size: units(5);
line-height: units(5);
}
.big-number-label {
font-size: units(2.5);
}
}
.big-number-status {
background: color('green-cool-40v');
display: flex;
padding: units(1) units(2);
&--failing {
padding: 0;
a.usa-link {
color: white;
background: color('red-warm-50v');
padding: units(1) units(2);
margin: 0;
width: 100%;
&:hover {
background: color('red-warm-60v');
}
}
}
}
}
.usa-table {
width: 100%;
caption {
margin-bottom: 0;
}
.table-field-center-aligned {
text-align: center;
}
.template-statistics-table-template-name {
padding-left: units(4);
display: inline-block;
background-repeat: no-repeat;
background-image: url(../img/material-icons/description.svg);
}
}
.get-started {
border: 1px solid color('gray-90');
padding: units(2);
margin-bottom: units(4);
}
}
.dashboard-table {
table {
width: 100%;
}
.file-list-filename {
font-weight: bold;
}
.file-list-hint {
margin: 0;
word-break: break-word;
}
.table-field,
.table-field-right-aligned {
width: 50%;
}
&.usage-table {
.table-field,
.table-field-left-aligned,
.table-field-right-aligned {
width: auto;
}
}
}
.job-status-table {
table-layout: fixed;
thead tr th {
border-bottom: 0;
}
thead,
tbody,
tr {
width: 100%;
}
th:first-child,
td:first-child {
width: 75%;
}
th:nth-child(2),
R td:nth-child(2) {
width: 25%;
}
}
.usage-table {
ul {
list-style: none;
padding: 0;
margin: 0;
}
.big-number-smallest {
display: flex;
flex-direction: column;
}
}
.job-table {
width: 100%;
border-collapse: collapse;
td.file-name {
width: 25%;
overflow-wrap: anywhere;
}
td.jobid {
width: 5%;
}
td.template {
width: 25%;
}
td.time-sent {
width: 15%;
}
td.sender {
width: 20%;
overflow-wrap: break-word;
}
td.count-of-recipients {
width: 5%;
}
td.report {
width: 2%;
text-align: center;
}
td.delivered {
width: 2%;
text-align: center;
}
td.failed {
width: 2%;
text-align: center;
}
td.report img {
padding-top: 5px;
}
th {
padding: 0.5rem 1rem;
}
}
@media (max-width: 768px) {
.usa-table-container--scrollable-mobile {
margin: 0;
overflow-y: hidden;
}
}
.usa-table th[data-sortable][aria-sort='ascending'],
.usa-table th[data-sortable][aria-sort='descending'] {
background-color: #a1d3ff;
}
#template-list {
max-height: 500px;
overflow-y: auto;
padding: units(1) 0 units(1) units(1);
margin: units(2) 0 units(5);
ul {
padding: 0;
margin: 0;
list-style: none;
}
}
.usa-prose > p.max-width-full {
max-width: 100%;
}
// Tabs
.tabs {
.pill {
display: flex;
list-style: none;
padding: 0;
.pill-item__container {
border: 1px solid color('gray-cool-10');
flex: 1;
display: flex;
flex-direction: column;
text-align: center;
font-size: units(2);
a {
padding: units(4);
.big-number-smaller {
font-size: units(5);
line-height: units(6);
}
.big-number-smallest {
font-size: units(3);
}
&:not(.pill-item--selected):hover {
background: color('blue-warm-70v');
}
&.pill-item--selected:hover {
color: color('blue-60v');
}
}
}
}
}
// Etc
.email-brand,
@@ -515,6 +747,86 @@ div.guides {
height: 400px;
}
$do-dont-color-do: 'green-cool-50v';
$do-dont-color-dont: 'red-cool-50v';
$do-dont-color-border: 'gray-cool-20';
$do-dont-padding: 3;
$do-dont-top-bar-width: 1;
.do-dont {
display: flex;
height: 100%;
padding-top: units(2);
}
.do-dont__do,
.do-dont__dont {
background-color: color('white');
border: 1px solid color($do-dont-color-border);
flex: 1 0 0;
height: 100%;
position: relative;
}
.do-dont__do {
padding: 0rem;
}
.do-dont__do:before,
.do-dont__dont:before {
content: '';
position: absolute;
height: units($do-dont-top-bar-width);
width: calc(100% + 2px);
top: -1px;
left: -1px;
}
.do-dont__do:before {
background-color: color($do-dont-color-do);
}
.do-dont__dont:before {
background-color: color($do-dont-color-dont);
}
.do-dont__heading {
// @include typeset("lang", "lg", 2);
align-items: center;
border-bottom: 1px solid color($do-dont-color-border);
display: flex;
font-weight: fw('bold');
margin: 0;
padding: units(4) units($do-dont-padding) units($do-dont-padding);
.usa-icon {
@include u-square(3);
margin-right: units(0.5);
}
}
.do-dont__do .do-dont__heading {
color: color($do-dont-color-do);
}
.do-dont__dont .do-dont__heading {
color: color($do-dont-color-dont);
}
.do-dont__content {
padding: 0 units($do-dont-padding) units($do-dont-padding)
units($do-dont-padding);
ul {
margin-bottom: 0;
overflow-wrap: anywhere;
}
li + li {
margin-top: units(1);
}
}
.site-note {
background-color: #dfe1e2;
max-width: 72ex;
@@ -659,6 +971,32 @@ nav.nav {
}
}
.home-cards {
justify-content: space-between;
.usa-card__container {
align-items: center;
text-align: center;
border-radius: 4px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
img {
margin: units(4) auto 0;
width: units(15);
height: units(15);
}
.usa-card__body {
margin-bottom: units(2);
}
.blue-bar {
background-color: #005eb8;
height: 1.3em;
width: 100%;
margin: 0;
border-radius: 0;
}
}
}
.contact-us-card {
border: 2px solid color("ink");
@@ -686,7 +1024,3 @@ nav.nav {
font-size: units(3);
font-weight: bold;
}
.form-control-error {
border: 4px solid #b10e1e
}

View File

@@ -1,10 +1,5 @@
@forward "uswds-theme";
@forward "uswds";
@forward "uswds-theme-custom-styles";
@use "tokens" as vars;
@forward "legacy-styles";
@forward "home";
@forward "dashboard";
@forward "tabs";
@forward "do-dont";
@forward "data-visualization";

View File

@@ -0,0 +1,90 @@
// Path to assets for use with file-url()
$path: '/static/images/';
// Dependencies from GOV.UK Frontend Toolkit
// https://github.com/alphagov/govuk_frontend_toolkit/
@import 'conditionals';
@import 'shims';
@import 'measurements';
@import 'css3';
@import 'colours';
@import 'typography';
@import 'grid_layout';
@import 'helpers';
// Dependencies from GOVU.UK Frontend Toolkit, rewritten for this application
@import 'url-helpers';
// Specific to this application, needs to go at the top of the cascade
@import 'globals';
// Dependencies from GOV.UK Elements
// https://github.com/alphagov/govuk_elements
@import 'elements/helpers';
@import 'elements/reset';
@import 'elements/details';
@import 'elements/elements-typography';
@import 'elements/forms';
@import 'elements/forms/form-multiple-choice';
@import 'elements/forms/form-validation';
@import 'elements/lists';
@import 'elements/panels';
@import 'elements/tables';
// Dependencies from GOV.UK Frontend, packaged to be specific to this application
@import './govuk-frontend/all';
// Custom overrides
.govuk-link {
font-weight: bold;
}
// Specific to this application
@import 'local/typography';
@import 'grids';
@import 'components/site-footer';
@import 'components/placeholder';
@import 'components/sms-message';
@import 'components/page-footer';
@import 'components/table';
@import 'components/navigation';
@import 'components/big-number';
@import 'components/banner';
@import 'components/textbox';
@import 'components/file-upload';
@import 'components/browse-list';
@import 'components/email-message';
@import 'components/copy-to-clipboard';
@import 'components/vendor/previous-next-navigation';
@import 'components/radios';
@import 'components/checkboxes';
@import 'components/pill';
@import 'components/show-more';
@import 'components/message';
@import 'components/research-mode';
@import 'components/tick-cross';
@import 'components/list-entry';
@import 'components/live-search';
@import 'components/stick-at-top-when-scrolling';
@import 'components/fullscreen-table';
@import 'components/conditional-radios';
@import 'components/vendor/breadcrumbs';
@import 'components/vendor/responsive-embed';
@import 'components/preview-pane';
@import 'components/task-list';
@import 'components/loading-indicator';
@import 'components/area-list';
@import 'components/content-metadata';
@import 'views/dashboard';
@import 'views/users';
@import 'views/api';
@import 'views/product-page';
@import 'views/template';
@import 'views/notification';
@import 'views/send';
@import 'views/get_started';
@import 'views/history';
// TODO: break this up
@import 'app';

View File

@@ -76,24 +76,25 @@ def service_dashboard(service_id):
)
@main.route("/services/<uuid:service_id>/daily-stats.json")
@user_has_permissions()
def get_daily_stats(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("/services/<uuid:service_id>/daily-stats-by-user.json")
@user_has_permissions()
def get_daily_stats_by_user(service_id):
@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=current_user.id,
user_id,
start_date=date_range["start_date"],
days=date_range["days"],
)

View File

@@ -401,9 +401,7 @@ def get_job_partials(job):
)
if request.referrer is not None:
session["arrived_from_preview_page"] = ("check" in request.referrer) or (
"help=0" in request.referrer
)
session["arrived_from_preview_page"] = ("check" in request.referrer) or ("help=0" in request.referrer)
else:
session["arrived_from_preview_page"] = False

View File

@@ -6,36 +6,17 @@ from app.notify_client import NotifyAdminAPIClient
class BillingAPIClient(NotifyAdminAPIClient):
def get_monthly_usage_for_service(self, service_id, year):
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(
return self.get(
"/service/{0}/billing/monthly-usage".format(service_id),
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):
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(
return self.get(
"/service/{0}/billing/yearly-usage-summary".format(service_id),
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):
frag_limit = redis_client.get(f"free-sms-fragment-limit-{service_id}-{year}")
if frag_limit is not None:
@@ -67,28 +48,13 @@ class BillingAPIClient(NotifyAdminAPIClient):
)
def get_data_for_billing_report(self, start_date, end_date):
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(
return self.get(
url="/platform-stats/data-for-billing-report",
params={
"start_date": str(start_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):
return self.get(

View File

@@ -1,6 +1,3 @@
import json
from app.extensions import redis_client
from app.notify_client import NotifyAdminAPIClient, _attach_current_user
@@ -44,7 +41,7 @@ class NotificationApiClient(NotifyAdminAPIClient):
if job_id:
return method(
url="/service/{}/job/{}/notifications".format(service_id, job_id),
**kwargs,
**kwargs
)
else:
if limit_days is not None:
@@ -99,20 +96,9 @@ class NotificationApiClient(NotifyAdminAPIClient):
)
def get_notification_count_for_job_id(self, *, service_id, job_id):
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(
return self.get(
url="/service/{}/job/{}/notification_count".format(service_id, job_id)
)
redis_client.set(
f"notification-count-for-job-id-{service_id}-{job_id}",
json.dumps(result["count"]),
ex=30,
)
return result["count"]
)["count"]
notification_api_client = NotificationApiClient()

View File

@@ -34,7 +34,7 @@
attributes: params.errorMessage.attributes,
html: params.errorMessage.html,
text: params.errorMessage.text,
visuallyHiddenText: params.errorMessage.visuallyHiddenText,
visuallyHiddenText: params.errorMessage.visuallyHiddenText
}) | indent(2) | trim }}
{% 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') }}"
@@ -42,7 +42,5 @@
{%- if describedBy %} aria-describedby="{{ describedBy }}"{% endif %}
{%- if params.autocomplete %} autocomplete="{{ params.autocomplete}}"{% endif %}
{%- if params.pattern %} pattern="{{ params.pattern }}"{% endif %}
{%- for attribute, value in params.attributes %} {{ attribute }}="{{ value }}"{% endfor -%}
{%- if params.required %} required{% endif %}
/>
{%- for attribute, value in params.attributes %} {{ attribute }}="{{ value }}"{% endfor -%}>
</div>

View File

@@ -39,6 +39,12 @@
] %}
{% endif %}
<header class="usa-header usa-header--extended">
<div class="usa-nav-container">
<div class="usa-navbar">
@@ -47,7 +53,7 @@
<div class="logo-img display-flex">
<a href="/">
<span class="usa-sr-only">Notify.gov logo</span>
<img src="{{ (asset_path | default('/static')) + 'images/notify-logo.svg' }}" alt=""
<img src="{{ (asset_path | default('/static')) + 'images/notify-logo.svg' }}" alt="Notify.gov logo"
class="usa-flag-logo margin-right-1">
</a>
</div>
@@ -55,6 +61,14 @@
<button type="button" class="usa-menu-btn">Menu</button>
{% endif %}
</div>
{% if not current_user.is_authenticated and is_information_section%}
<div class="usa-nav__login">
<a class="usa-button usa-button login-button login-button--primary margin-right-2"
href="{{ initial_signin_url }}">Sign
in with <img src="{{ asset_url('images/logo-login.svg') }}" alt="Login.gov logo">
</a>
</div>
{% endif %}
</div>
</div>
@@ -75,11 +89,7 @@
{% endif %}
{% endfor %}
</ul>
{% if not current_user.is_authenticated %}
<div class="usa-nav__secondary margin-bottom-2">
{% else %}
<div class="usa-nav__secondary margin-bottom-6">
{% endif %}
<ul class="usa-nav__secondary-links">
{% if secondaryNavigation %}
{% for item in secondaryNavigation %}
@@ -94,19 +104,8 @@
{% endfor %}
{% endif %}
</ul>
{% if not current_user.is_authenticated %}
<div class="usa-nav__login display-flex flex-align-center">
<span class="margin-right-2 display-none desktop:display-block">
If you are an existing partner
</span>
<a id="header_login_button" class="usa-button login-button login-button--primary desktop:padding-2 padding-1"
href="{{ initial_signin_url }}">
Sign in with
<img src="{{ asset_url('images/logo-login.svg') }}" alt="Login.gov logo">
</a>
</div>
{% endif %}
</div>
</div>
</nav>
</div>

View File

@@ -16,9 +16,19 @@
placeholder=''
) %}
<div
class="usa-form-group{% if field.errors %} usa-form-group--error{% endif %} {{ extra_form_group_classes }}"
class="form-group{% if field.errors %} form-group-error{% endif %} {{ extra_form_group_classes }}"
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 }}">
{% if label %}
{{ label }}
@@ -31,12 +41,6 @@
{{ hint }}
</div>
{% 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
%}
@@ -55,8 +59,6 @@
data_highlight_placeholders='true' if highlight_placeholders else 'false',
rows=rows|string,
placeholder=placeholder,
aria_describedby=field.name+"-error",
required='required' if required else None,
**kwargs
) }}
{% if suffix %}

View File

@@ -18,7 +18,7 @@
</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 class="table-container" id="activityContainer" data-currentUserName="{{ current_user.name }}">
<div id="tableActivity" class="table-overflow-x-auto">
<h2 id="table-heading" class="margin-top-4 margin-bottom-1">Service activity</h2>

View File

@@ -32,8 +32,6 @@
<div class="tablet:grid-col-9 mobile-lg:grid-col-12">
{{ form.name(param_extensions={
"extra_form_group_classes": "margin-bottom-2",
"id": "name",
"required": True,
"hint": {"text": "Your recipients will not see this"}
}) }}
{{ textbox(
@@ -43,8 +41,7 @@
hint=content_hint,
rows=5,
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!',
required=True
placeholder='Edit me! Check out the Personalization section below for details on cool ((stuff)) you can do with your messages!'
) }}
{% if current_user.platform_admin %}
{{ form.process_type }}

View File

@@ -37,8 +37,8 @@
data_kwargs={'force-focus': True}
) %}
<div class="grid-row">
<div class="grid-col-12 {% if form.placeholder_value.label.text == 'phone number' %}extra-tracking{% endif %}">
{{ form.placeholder_value(param_extensions={"classes": "", "id": "phone-number"}) }}
<div class="grid-col-12 {% if form.placeholder_value.label.text == 'phone number' %}extra-tracking{% endif %}" aria-live="polite" role="alert">
{{ form.placeholder_value(param_extensions={"classes": ""}) }}
</div>
{% if skip_link or link_to_upload %}
<div class="grid-col-12 margin-top-1">

View File

@@ -9,65 +9,70 @@ import usaButton %} {% block meta %}
<main id="main-content" role="main">
{% block content %}
<section class="usa-section--dark usa-hero padding-y-2" aria-label="Introduction">
<section class="usa-section--dark usa-hero" aria-label="Introduction">
<div class="grid-container padding-y-4">
<div class="grid-row grid-gap display-flex flex-align-center">
<div class="desktop:grid-col-8 tablet:grid-col-12">
<h1 class="usa-hero__heading">
<h1 class="font-sans-2xl usa-hero__heading">
Reach people where they are with government-powered text messages
</h1>
<p class="desktop:font-sans-lg font-sans-md">
<p class="font-sans-lg">
Notify.gov is a text messaging service that helps federal, state,
local, tribal and territorial governments more effectively
communicate with the people they serve.
</p>
<div class="usa-button-group margin-bottom-5 flex-align-center">
<a
class="usa-button usa-button login-button login-button--primary margin-right-2"
href="{{ initial_signin_url }}"
>Sign in with
<img
src="{{ asset_url('images/logo-login.svg') }}"
alt="Login.gov logo"
/>
</a>
if you are an existing partner
</div>
</div>
<div
class="desktop:grid-col-4 desktop:display-block display-none"
class="desktop:grid-col-3 grid-offset-1 desktop:display-block display-none margin-x-5"
>
<img
src="{{ asset_url('images/product/hero-icons.svg') }}"
alt=""
src="{{ asset_url('images/product/phone-text.png') }}"
alt="Illustration of a mobile phone displaying a text message and number on its screen, with a speech bubble coming out from outside the phone, symbolizing communication or notification."
/>
</div>
</div>
</div>
</section>
<section class="grid-container usa-section usa-section__home border-top border-base-lighter usa-prose padding-bottom-4">
<div class="grid-row flex-align-center flex-justify-center">
<div class="tablet:grid-col-4 mobile-lg:grid-col-12 display-flex flex-justify-center margin-bottom-6">
<img
src="{{ asset_url('images/product/couple-looking-at-phone.png') }}"
alt="A couple smiling and looking at a cell phone together"/>
</div>
<div class="tablet:grid-col-6 tablet:margin-left-8 mobile-lg:grid-col-12">
<h2 class="font-body-xl margin-top-0 margin-bottom-3">
Government texting made easy
</h2>
<p class="usa-body margin-bottom-5">
Notify.gov is a text messaging platform built for government agencies.
With minimal set-up and secure, personalized messaging, you can make
one-way texting a part of your outreach program.
</p>
<a class="usa-button usa-button--big" href="/about">Learn more about Notify.gov</a>
</div>
</div>
<section
class="grid-container usa-section usa-section__home border-top border-base-lighter usa-prose padding-bottom-1"
>
<h2 class="font-body-xl margin-top-0 margin-bottom-3">
Government texting made easy
</h2>
<p class="usa-body">
Notify.gov is a text messaging platform built for government agencies.
With minimal set-up and secure, personalized messaging, you can make
one-way texting a part of your outreach program.
</p>
</section>
<section class="usa-section usa-section__home usa-prose key-features padding-bottom-4">
<h2 class="text-center margin-top-0">Key features</h2>
<div class="grid-container home-cards margin-top-5">
<section
class="grid-container usa-section usa-section__home usa-prose padding-bottom-1"
>
<h2 class="font-sans-xl margin-top-0">Key features</h2>
<div class="home-cards margin-top-5">
<ul class="usa-card-group display-flex margin-bottom-4">
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
<div class="usa-card__container">
<div class="img-container">
<img
src="{{ asset_url('images/internet.svg') }}"
alt=""
/>
</div>
<div class="usa-card__header padding-bottom-0">
<h3>Web-based</h3>
<div class="blue-bar"></div>
<img
src="{{ asset_url('images/internet.svg') }}"
alt="Globe on top of a web browser"
/>
<div class="usa-card__header">
<h3 class="font-heading-md">Web-based</h3>
</div>
<div class="usa-card__body">
<p>Nothing to download or install</p>
@@ -76,14 +81,13 @@ import usaButton %} {% block meta %}
</li>
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
<div class="usa-card__container">
<div class="img-container">
<img
src="{{ asset_url('images/fast.svg') }}"
alt=""
/>
</div>
<div class="usa-card__header padding-bottom-0">
<h3>Fast and easy</h3>
<div class="blue-bar"></div>
<img
src="{{ asset_url('images/fast.svg') }}"
alt="Stopwatch with a notification speech bubble with a star inside"
/>
<div class="usa-card__header">
<h3 class="font-heading-md">Fast and easy</h3>
</div>
<div class="usa-card__body">
<p>No technical expertise required</p>
@@ -92,14 +96,13 @@ import usaButton %} {% block meta %}
</li>
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
<div class="usa-card__container">
<div class="img-container">
<img
src="{{ asset_url('images/status.svg') }}"
alt=""
/>
</div>
<div class="usa-card__header padding-bottom-0">
<h3>Track message delivery</h3>
<div class="blue-bar"></div>
<img
src="{{ asset_url('images/status.svg') }}"
alt="3 status messages, 2 successes and one failure"
/>
<div class="usa-card__header">
<h3 class="font-heading-md">Track message delivery</h3>
</div>
<div class="usa-card__body">
<p>See which messages were received</p>
@@ -108,16 +111,17 @@ import usaButton %} {% block meta %}
</li>
</ul>
<ul class="usa-card-group">
<li class="usa-card tablet:grid-offset-2 tablet:grid-col-4 mobile-lg:grid-col-12">
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
<div class="usa-card__container">
<div class="img-container">
<img
src="{{ asset_url('images/translation.svg') }}"
alt=""
/>
</div>
<div class="usa-card__header padding-bottom-0">
<h3>Send in recipients' preferred language</h3>
<div class="blue-bar"></div>
<img
src="{{ asset_url('images/translation.svg') }}"
alt="Speech bubbles with the letter A and the Chinese character for language"
/>
<div class="usa-card__header">
<h3 class="font-heading-md">
Send in recipients' preferred language
</h3>
</div>
<div class="usa-card__body">
<p>Notify.gov has support for more than 30 character sets</p>
@@ -126,14 +130,31 @@ import usaButton %} {% block meta %}
</li>
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
<div class="usa-card__container">
<div class="img-container">
<img
src="{{ asset_url('images/send.svg') }}"
alt=""
/>
</div>
<div class="blue-bar"></div>
<img
src="{{ asset_url('images/security.svg') }}"
alt="Lock with code icon inside on top of a web browser"
/>
<div class="usa-card__header">
<h3 class="padding-bottom-0">
<h3 class="font-heading-md">Security and privacy</h3>
</div>
<div class="usa-card__body">
<p>
Limited data retention, encryption, and multi-factor
authentication protect user data and manage risk with <br><a href="/about/security">our security efforts</a>
</p>
</div>
</div>
</li>
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
<div class="usa-card__container">
<div class="blue-bar"></div>
<img
src="{{ asset_url('images/send.svg') }}"
alt="Paper airplane and a notification icon with the number 1 inside"
/>
<div class="usa-card__header">
<h3 class="font-heading-md">
Send bulk, customized, one-way messages
</h3>
</div>
@@ -149,52 +170,51 @@ import usaButton %} {% block meta %}
</div>
</section>
<section class="usa-section usa-section__home usa-prose security-and-privacy padding-y-6 bg-base-lighter">
<div class="grid-container">
<div class="grid-row flex-align-center flex-justify-center">
<div class="tablet:grid-col-7 mobile-lg:grid-col-12 margin-bottom-4">
<h2 class="font-heading-xl margin-top-0 margin-bottom-3">
Security and privacy
</h2>
<p class="usa-body margin-bottom-5">
Limited data retention, encryption, and multi-factor authentication protect user data and manage risk.
</p>
<a class="usa-button usa-button--big" href="/join-notify">Our security efforts</a>
</div>
<div class="tablet:grid-col-3 mobile-lg:grid-col-12 display-flex flex-justify-center">
<img
class="width-card-lg"
src="{{ asset_url('images/security.svg') }}"
alt=""
/>
</div>
<section
class="grid-container usa-section usa-section__home usa-prose grid-container padding-bottom-10"
>
<h2 class="font-heading-xl margin-top-0 margin-bottom-3">
Who can use Notify.gov?
</h2>
<p class="usa-body">
All <span class="text-bold">federal</span> agencies and programs are
eligible to use Notify.gov.
</p>
<p class="usa-body">
US
<span class="text-bold">state, local, territorial, or tribal</span>
governments that administer or deliver federally-funded programs or
services may qualify to use Notify.gov to communicate with applicants and
participants in these programs.
</p>
<div class="grid-container margin-top-4 padding-left-0 padding-right-0">
<div class="grid-row grid-gap-3">
<a
class="text-no-underline tablet:grid-col-4 mobile-lg:grid-col-12"
href="mailto:tts-notify@gsa.gov"
>
<div class="contact-us-card">
<div class="grid-row flex-align-center grid-gap-2">
<div class="grid-col-auto">
<img
src="{{ asset_url('images/contact.svg') }}"
alt=""
class="height-7 width-7"
/>
</div>
<div class="grid-col">
<p class="margin-0">
<strong>Contact us</strong><br />
To learn more about becoming a partner!
</p>
</div>
</div>
</div>
</a>
</div>
</div>
</section>
<section class="grid-container usa-section usa-section__home usa-prose margin-bottom-15">
<div class="grid-row flex-align-center flex-justify-center">
<div class="tablet:grid-col-4 mobile-lg:grid-col-12 display-flex flex-justify-center margin-right-8 margin-bottom-5">
<img src="{{ asset_url('images/product/who-can-use-notify.png') }}" alt="Woman smiling behind two computer screens"/>
</div>
<div class="tablet:grid-col-6 mobile-lg:grid-col-12">
<h2 class="font-heading-xl margin-top-0 margin-bottom-3">
Who can use Notify.gov?
</h2>
<p class="usa-body">
All <span class="text-bold">federal</span> agencies and programs are
eligible to use Notify.gov.
</p>
<p class="usa-body margin-bottom-5">
US
<span class="text-bold">state, local, territorial, or tribal</span>
governments that administer or deliver federally-funded programs or
services may qualify to use Notify.gov to communicate with applicants and
participants in these programs.
</p>
<a class="usa-button usa-button--big" href="/join-notify">Become a Notify.gov partner</a>
</div>
</section>
{% endblock %}
</main>
{% endblock %}

View File

@@ -90,4 +90,8 @@
{% endif %}
</div>
<!--<div class="">
{{ copy_to_clipboard(template.id, name="Template ID", thing='template ID') }}
</div>-->
{% endblock %}

View File

@@ -16,7 +16,7 @@
<link rel="apple-touch-icon" sizes="180x180"
href="/static_503/images/apple-touch-icon.png?a0f7e1b728a42016b247dc54ee40d055">
<link rel="stylesheet" media="screen" href="/static_503/stylesheets/legacy/uk.css?d077f86473501ab244de0b30600536ee" />
<link rel="stylesheet" media="screen" href="/static_503/stylesheets/main.css?d077f86473501ab244de0b30600536ee" />
<link rel="stylesheet" media="print" href="/static_503/stylesheets/print.css?28010888ca5719dc83d7c2c80f6ed2b2" />
<style>
.govuk-header__container {
@@ -127,4 +127,4 @@
</body>
</html>
</html>

2176
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -37,7 +37,7 @@
"hogan": "1.0.2",
"jquery": "3.7.1",
"morphdom": "^2.7.4",
"playwright": "^1.50.1",
"playwright": "^1.50.0",
"python": "^0.0.4",
"query-command-supported": "1.0.0",
"sass-embedded": "^1.83.4",
@@ -48,7 +48,7 @@
},
"devDependencies": {
"@babel/core": "^7.26.7",
"@babel/preset-env": "^7.26.7",
"@babel/preset-env": "^7.26.0",
"@uswds/compile": "^1.2.1",
"backstopjs": "^6.3.25",
"better-npm-audit": "^3.11.0",
@@ -61,12 +61,12 @@
"gulp-jshint": "2.1.0",
"gulp-prettyerror": "2.0.0",
"gulp-uglify": "3.0.2",
"jest": "^29.7.0",
"jest": "29.7.0",
"jest-each": "^29.2.1",
"jest-environment-jsdom": "^29.2.2",
"jshint": "2.13.6",
"jshint-stylish": "2.2.1",
"rollup": "^4.34.4",
"rollup": "^4.32.0",
"rollup-plugin-commonjs": "10.1.0",
"rollup-plugin-node-resolve": "5.2.0"
}

View File

@@ -22,7 +22,7 @@ def test_non_logged_in_user_can_see_homepage(
# Assert the entire HTML of the button to include the image
button = page.select_one(
"#header_login_button"
"a.usa-button.login-button.login-button--primary.margin-right-2"
)
assert "Sign in with" in button.text.strip() # Assert button text
assert button.find("img")["alt"] == "Login.gov logo" # Assert image presence

View File

@@ -1521,7 +1521,7 @@ def test_link_to_upload_not_offered_when_entering_personalisation(
# Were entering personalization
assert page.select_one("input[type=text]")["name"] == "placeholder_value"
assert page.select_one("label[for=phone-number]").text.strip() == "name"
assert page.select_one("label[for=placeholder_value]").text.strip() == "name"
# No Upload link shown
assert len(page.select("main a")) == 0
assert "Upload" not in page.select_one("main").text

View File

@@ -174,7 +174,9 @@ def test_should_show_empty_text_box(
# data-module=autofocus is set on a containing element so it
# shouldnt also be set on the textbox itself
assert "data-module" not in textbox
assert normalize_spaces(page.select_one("label[for=phone-number]").text) == "one"
assert (
normalize_spaces(page.select_one("label[for=placeholder_value]").text) == "one"
)
def test_should_prefill_answers_for_get_tour_step(

View File

@@ -141,15 +141,8 @@ def test_get_notification(mocker):
def test_get_notification_count_for_job_id(mocker):
mock_get = mocker.patch(
"app.notify_client.notification_api_client.NotificationApiClient.get",
return_value={"count": 0},
"app.notify_client.notification_api_client.NotificationApiClient.get"
)
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(
service_id="foo", job_id="bar"
)

View File

@@ -40,7 +40,7 @@ beforeAll(done => {
</div>
</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="activityContainer" data-currentUserName="Test User"></div>
<div id="aria-live-account" class="usa-sr-only" aria-live="polite"></div>
`;
@@ -64,13 +64,13 @@ test('D3 is loaded correctly', () => {
test('Populates the accessible table for activity chart correctly', () => {
const sampleData = {
'2024-07-01': { sms: { delivered: 50, failed: 5, pending: 10 } },
'2024-07-02': { sms: { delivered: 60, failed: 2, pending: 5 } },
'2024-07-03': { sms: { delivered: 70, failed: 1, pending: 3 } },
'2024-07-04': { sms: { delivered: 80, failed: 0, pending: 0 } },
'2024-07-05': { sms: { delivered: 90, failed: 3, pending: 8 } },
'2024-07-06': { sms: { delivered: 100, failed: 4, pending: 7 } },
'2024-07-07': { sms: { delivered: 110, failed: 2, pending: 6 } },
'2024-07-01': { sms: { delivered: 50, failed: 5 } },
'2024-07-02': { sms: { delivered: 60, failed: 2 } },
'2024-07-03': { sms: { delivered: 70, failed: 1 } },
'2024-07-04': { sms: { delivered: 80, failed: 0 } },
'2024-07-05': { sms: { delivered: 90, failed: 3 } },
'2024-07-06': { sms: { delivered: 100, failed: 4 } },
'2024-07-07': { sms: { delivered: 110, failed: 2 } },
};
const labels = Object.keys(sampleData).map(dateString => {
@@ -79,9 +79,8 @@ test('Populates the accessible table for activity chart correctly', () => {
});
const deliveredData = Object.values(sampleData).map(d => d.sms.delivered);
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, pendingData);
window.createTable('weeklyTable', 'activityChart', labels, deliveredData, failedData);
const table = document.getElementById('weeklyTable');
expect(table).toBeDefined();
@@ -93,7 +92,6 @@ test('Populates the accessible table for activity chart correctly', () => {
expect(headers[0].textContent).toBe('Day');
expect(headers[1].textContent).toBe('Delivered');
expect(headers[2].textContent).toBe('Failed');
expect(headers[3].textContent).toBe('Pending');
const firstRowCells = rows[1].getElementsByTagName('td');
expect(firstRowCells[0].textContent).toBe('07/01/24');
@@ -102,73 +100,58 @@ test('Populates the accessible table for activity chart correctly', () => {
});
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],
[10, 5, 3, 0, 8, 7, 6]
);
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]);
const svg = document.getElementById('weeklyChart').querySelector('svg');
expect(svg).not.toBeNull();
expect(svg.querySelectorAll('.bar-group').length).toBe(3);
expect(svg.getAttribute('width')).toBe('0');
expect(svg.getAttribute('height')).toBe('400');
});
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 deliveredData = [50, 60, 70, 80, 90, 100, 110];
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');
container.style.width = '600px';
container.style.width = '600px'; // Force a specific width
const containerWidth = container.clientWidth;
expect(containerWidth).toBeGreaterThan(0);
window.createChart('#weeklyChart', labels, deliveredData, failedData, pendingData);
// Call the function to create the chart
window.createChart('#weeklyChart', labels, deliveredData, failedData);
const svg = container.querySelector('svg');
expect(svg).not.toBeNull();
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);
// Optionally, you can add assertions to check for specific elements
expect(container.querySelector('svg')).not.toBeNull();
expect(container.querySelectorAll('rect').length).toBeGreaterThan(0);
});
test('Legend includes pending when data exists', () => {
test('Legend is visible when there are delivered or failed messages', () => {
// Example data with delivered and failed messages
const labels = ['Day 1', 'Day 2'];
const deliveredData = [10, 20];
const failedData = [5, 0];
const pendingData = [3, 2];
window.createChart('#weeklyChart', labels, deliveredData, failedData, pendingData);
const legendContainer = document.querySelector('.chart-legend');
const legendItems = legendContainer.querySelectorAll('.legend-item');
expect(legendItems.length).toBe(3);
const pendingLegend = Array.from(legendItems).find(item =>
item.textContent.includes('Pending')
);
expect(pendingLegend).not.toBeNull();
});
test('Legend is hidden when there are no delivered, failed, or pending messages', () => {
const labels = ['Day 1', 'Day 2'];
const deliveredData = [0, 0];
const failedData = [0, 0];
const pendingData = [0, 0];
const deliveredData = [10, 20]; // Mock delivered data
const failedData = [5, 0]; // Mock failed data
// 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 legendDisplayStyle = window.getComputedStyle(legendContainer).display;
expect(legendDisplayStyle).toBe('flex');
expect(legendContainer.querySelectorAll('.legend-item').length).toBe(2); // Ensure two legend items
});
test('Legend is hidden when there are no delivered or failed messages', () => {
// Example data with no delivered and no failed messages
const labels = ['Day 1', 'Day 2'];
const deliveredData = [0, 0]; // No delivered messages
const failedData = [0, 0]; // No failed messages
// Call the createChart function
window.createChart('#weeklyChart', labels, deliveredData, failedData);
// Check if the legend is hidden using computed style
const legendContainer = document.querySelector('.chart-legend');
@@ -178,48 +161,25 @@ test('Legend is hidden when there are no delivered, failed, or pending messages'
test('Fetches data and creates chart and table correctly', async () => {
const mockResponse = {
'2024-07-01': { sms: { delivered: 50, failed: 5, pending: 10 } },
'2024-07-02': { sms: { delivered: 60, failed: 2, pending: 8 } },
'2024-07-03': { sms: { delivered: 70, failed: 1, pending: 6 } },
'2024-07-04': { sms: { delivered: 80, failed: 0, pending: 4 } },
'2024-07-05': { sms: { delivered: 90, failed: 3, pending: 7 } },
'2024-07-06': { sms: { delivered: 100, failed: 4, pending: 5 } },
'2024-07-07': { sms: { delivered: 110, failed: 2, pending: 3 } },
'2024-07-01': { sms: { delivered: 50, failed: 5 } },
'2024-07-02': { sms: { delivered: 60, failed: 2 } },
'2024-07-03': { sms: { delivered: 70, failed: 1 } },
'2024-07-04': { sms: { delivered: 80, failed: 0 } },
'2024-07-05': { sms: { delivered: 90, failed: 3 } },
'2024-07-06': { sms: { delivered: 100, failed: 4 } },
'2024-07-07': { sms: { delivered: 110, failed: 2 } },
};
const tableContainer = document.getElementById('activityContainer');
const currentServiceId = tableContainer.getAttribute('data-currentServiceId');
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve(mockResponse),
ok: true,
json: () => Promise.resolve(mockResponse),
})
);
);
const data = await fetchData('service');
const data = await fetchData('service');
expect(global.fetch).toHaveBeenCalledWith(`/services/${currentServiceId}/daily-stats.json`);
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);
expect(global.fetch).toHaveBeenCalledWith('/daily_stats.json');
expect(data).toEqual(mockResponse);
});
test('handleDropdownChange updates DOM for individual selection', () => {
@@ -228,7 +188,7 @@ test('handleDropdownChange updates DOM for individual selection', () => {
<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="activityContainer" data-currentUserName="Test User"></div>
<div id="tableActivity">
<h2 id="table-heading"></h2>
<table id="activity-table">