Compare commits

..

8 Commits

Author SHA1 Message Date
Jonathan Bobel
f3bf14d84c Merge branch 'main' of https://github.com/GSA/notifications-admin into 2282-p2-home-page-development 2025-02-07 11:48:27 -05:00
Jonathan Bobel
234569b58a Updated images and styles 2025-02-07 11:48:06 -05:00
Jonathan Bobel
77137390bb Merge branch 'main' of https://github.com/GSA/notifications-admin into 2282-p2-home-page-development 2025-01-31 09:33:03 -05:00
Jonathan Bobel
ac1140c300 Image name was changed and wasn't showing up in static
Updated image
2025-01-30 11:43:19 -05:00
Jonathan Bobel
7f2bf7e590 Update signedout.html
Testing the new image - still need to figure out the failing E2E test
2025-01-30 11:07:01 -05:00
Jonathan Bobel
eac88dc83d Update signedout.html
Testing a different image to see if it passes a test
2025-01-29 14:49:57 -05:00
Jonathan Bobel
f6282f3528 Fixing a test 2025-01-29 14:19:16 -05:00
Jonathan Bobel
4a359b1b17 2282 - Home page changes with new design 2025-01-29 12:52:13 -05:00
59 changed files with 1214 additions and 1717 deletions

View File

@@ -527,7 +527,7 @@
"filename": "tests/app/main/views/test_accept_invite.py",
"hashed_secret": "07f0a6c13923fc3b5f0c57ffa2d29b715eb80d71",
"is_verified": false,
"line_number": 631,
"line_number": 643,
"is_secret": false
}
],
@@ -684,5 +684,5 @@
}
]
},
"generated_at": "2025-02-26T18:19:37Z"
"generated_at": "2025-01-16T16:38:48Z"
}

View File

@@ -10,7 +10,7 @@ env:
FLASK_APP: application.py
WERKZEUG_DEBUG_PIN: off
REDIS_ENABLED: 0
NODE_VERSION: 22.3.0
NODE_VERSION: 16.15.1
AWS_US_TOLL_FREE_NUMBER: "+18556438890"
ADMIN_BASE_URL: http://localhost:6012
@@ -38,10 +38,10 @@ jobs:
output: report-markdown
annotations: failed-tests
prnumber: ${{ steps.findPr.outputs.number }}
- name: Check imports alphabetized
run: poetry run isort --check-only ./app ./tests
- name: Run style checks
run: poetry run flake8 .
- name: Check imports alphabetized
run: poetry run isort --check-only ./app ./tests
- name: Check dead code
run: make dead-code
- name: Run js tests
@@ -139,7 +139,7 @@ jobs:
- uses: ./.github/actions/setup-project
- name: Create requirements.txt
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
- uses: pypa/gh-action-pip-audit@v1.1.0
- uses: pypa/gh-action-pip-audit@v1.0.8
with:
inputs: requirements.txt
ignore-vulns: |

View File

@@ -16,7 +16,7 @@ env:
FLASK_APP: application.py
WERKZEUG_DEBUG_PIN: off
REDIS_ENABLED: 0
NODE_VERSION: 22.3.0
NODE_VERSION: 16.15.1
jobs:
dependency-audits:
@@ -26,7 +26,7 @@ jobs:
- uses: ./.github/actions/setup-project
- name: Create requirements.txt
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
- uses: pypa/gh-action-pip-audit@v1.1.0
- uses: pypa/gh-action-pip-audit@v1.0.6
with:
inputs: requirements.txt
- name: Run npm audit

View File

@@ -16,17 +16,9 @@ jobs:
with:
fetch-depth: 2
# Looks like we need to install Terraform ourselves now!
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "^1.7.5"
terraform_wrapper: false
- name: Check for changes to Terraform
id: changed-terraform-files
uses: tj-actions/changed-files@v45
uses: tj-actions/changed-files@v44
with:
files: |
terraform/demo/**
@@ -101,7 +93,7 @@ jobs:
- name: Check for changes to egress config
id: changed-egress-config
uses: tj-actions/changed-files@v45
uses: tj-actions/changed-files@v44
with:
files: |
deploy-config/egress_proxy/notify-admin-demo.*.acl

View File

@@ -16,17 +16,9 @@ jobs:
with:
fetch-depth: 2
# Looks like we need to install Terraform ourselves now!
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "^1.7.5"
terraform_wrapper: false
- name: Check for changes to Terraform
id: changed-terraform-files
uses: tj-actions/changed-files@v45
uses: tj-actions/changed-files@v44
with:
files: |
terraform/production/**
@@ -101,7 +93,7 @@ jobs:
- name: Check for changes to egress config
id: changed-egress-config
uses: tj-actions/changed-files@v45
uses: tj-actions/changed-files@v44
with:
files: |
deploy-config/egress_proxy/notify-admin-production.*.acl

View File

@@ -21,17 +21,9 @@ jobs:
with:
fetch-depth: 2
# Looks like we need to install Terraform ourselves now!
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "^1.7.5"
terraform_wrapper: false
- name: Check for changes to Terraform
id: changed-terraform-files
uses: tj-actions/changed-files@v45
uses: tj-actions/changed-files@v44
with:
files: |
terraform/staging/**
@@ -108,7 +100,7 @@ jobs:
- name: Check for changes to egress config
id: changed-egress-config
uses: tj-actions/changed-files@v45
uses: tj-actions/changed-files@v44
with:
files: |
deploy-config/egress_proxy/notify-admin-staging.*.acl

View File

@@ -15,14 +15,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
# Looks like we need to install Terraform ourselves now!
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "^1.7.5"
terraform_wrapper: false
- name: Check for drift
uses: dflook/terraform-check@v1
env:
@@ -43,14 +35,6 @@ jobs:
with:
ref: 'production'
# Looks like we need to install Terraform ourselves now!
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "^1.7.5"
terraform_wrapper: false
- name: Check for drift
uses: dflook/terraform-check@v1
env:
@@ -71,14 +55,6 @@ jobs:
with:
ref: 'production'
# Looks like we need to install Terraform ourselves now!
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "^1.7.5"
terraform_wrapper: false
- name: Check for drift
uses: dflook/terraform-check@v1
env:

View File

@@ -18,14 +18,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
# Looks like we need to install Terraform ourselves now!
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "^1.7.5"
terraform_wrapper: false
- name: Terraform format
id: format
run: terraform fmt -check
@@ -59,7 +51,7 @@ jobs:
# inspiration: https://learn.hashicorp.com/tutorials/terraform/github-actions#review-actions-workflow
- name: Update PR
uses: actions/github-script@v7
uses: actions/github-script@v6
# we would like to update the PR even when a prior step failed
if: ${{ always() }}
with:

View File

@@ -18,14 +18,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
# Looks like we need to install Terraform ourselves now!
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "^1.7.5"
terraform_wrapper: false
- name: Terraform format
id: format
run: terraform fmt -check
@@ -59,7 +51,7 @@ jobs:
# inspiration: https://learn.hashicorp.com/tutorials/terraform/github-actions#review-actions-workflow
- name: Update PR
uses: actions/github-script@v7
uses: actions/github-script@v6
# we would like to update the PR even when a prior step failed
if: ${{ always() }}
with:

View File

@@ -18,14 +18,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
# Looks like we need to install Terraform ourselves now!
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "^1.7.5"
terraform_wrapper: false
- name: Terraform format
id: format
run: terraform fmt -check
@@ -59,7 +51,7 @@ jobs:
# inspiration: https://learn.hashicorp.com/tutorials/terraform/github-actions#review-actions-workflow
- name: Update PR
uses: actions/github-script@v7
uses: actions/github-script@v6
# we would like to update the PR even when a prior step failed
if: ${{ always() }}
with:

View File

@@ -62,13 +62,6 @@ py-lint: ## Run python linting scanners and black
poetry run flake8 .
poetry run isort --check-only ./app ./tests
.PHONY: tada
tada: ## Run python linting scanners and black
poetry run isort ./app ./tests
poetry run black .
poetry run flake8 .
.PHONY: avg-complexity
avg-complexity:
echo "*** Shows average complexity in radon of all code ***"
@@ -162,8 +155,3 @@ upload-static:
# @cf map-route notify-admin ${DNS_NAME} --hostname www
# @cf unmap-route notify-admin-failwhale ${DNS_NAME} --hostname www
# @echo "Failwhale is disabled"
.PHONY: test-single
test-single: export NEW_RELIC_ENVIRONMENT=test
test-single: ## Run a single test file
poetry run pytest $(TEST_FILE)

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/main.css?d077f86473501ab244de0b30600536ee" />
<link rel="stylesheet" media="screen" href="/static/stylesheets/legacy/uk.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.

After

Width:  |  Height:  |  Size: 474 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 KiB

View File

@@ -130,12 +130,11 @@
.append('g')
.attr('class', 'bar-group')
.attr('fill', d => color(d.key));
const minBarHeight = 5;
barGroups.selectAll('rect')
.data(d => d)
.enter()
.append('rect')
.filter(d => d[1] - d[0] > 0)
.attr('x', d => x(d.data.label))
.attr('y', height)
.attr('height', 0)
@@ -156,10 +155,8 @@
.transition()
.duration(1000)
.attr('y', d => y(d[1]))
.attr('height', d => {
const calculatedHeight = y(d[0]) - y(d[1]);
return calculatedHeight < minBarHeight ? minBarHeight : calculatedHeight;
}); };
.attr('height', d => y(d[0]) - y(d[1]));
};
// Function to create an accessible table
const createTable = function(tableId, chartType, labels, deliveredData, failedData, pendingData) {
@@ -216,13 +213,7 @@
return;
}
var userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
var url = type === 'service'
? `/services/${currentServiceId}/daily-stats.json?timezone=${encodeURIComponent(userTimezone)}`
: `/services/${currentServiceId}/daily-stats-by-user.json`;
var url = type === 'service' ? `/services/${currentServiceId}/daily-stats.json` : `/services/${currentServiceId}/daily-stats-by-user.json`;
return fetch(url)
.then(response => {
if (!response.ok) {
@@ -247,6 +238,9 @@
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;
}
}

View File

@@ -6,17 +6,17 @@
var chartTitle = document.getElementById('chartTitle').textContent;
// Access data attributes from the HTML
var messagesSent = parseInt(chartContainer.getAttribute('data-messages-sent'));
var messagesRemaining = parseInt(chartContainer.getAttribute('data-messages-remaining'));
var totalMessages = messagesSent + messagesRemaining;
var sms_sent = parseInt(chartContainer.getAttribute('data-sms-sent'));
var sms_remaining_messages = parseInt(chartContainer.getAttribute('data-sms-allowance-remaining'));
var totalMessages = sms_sent + sms_remaining_messages;
// Update the message below the chart
document.getElementById('message').innerText = `${messagesSent.toLocaleString()} sent / ${messagesRemaining.toLocaleString()} remaining`;
document.getElementById('message').innerText = `${sms_sent.toLocaleString()} sent / ${sms_remaining_messages.toLocaleString()} remaining`;
// Calculate minimum width for "Messages Sent" as 1% of the total chart width
var minSentPercentage = (messagesSent === 0) ? 0 : 0.02;
var minSentPercentage = (sms_sent === 0) ? 0 : 0.02;
var minSentValue = totalMessages * minSentPercentage;
var displaySent = Math.max(messagesSent, minSentValue);
var displaySent = Math.max(sms_sent, minSentValue);
var displayRemaining = totalMessages - displaySent;
var svg = d3.select("#totalMessageChart");
@@ -48,7 +48,7 @@
.attr("width", 0) // Start with width 0 for animation
.on('mouseover', function(event) {
tooltip.style('display', 'block')
.html(`Messages Sent: ${messagesSent.toLocaleString()}`);
.html(`Messages Sent: ${sms_sent.toLocaleString()}`);
})
.on('mousemove', function(event) {
tooltip.style('left', `${event.pageX + 10}px`)
@@ -66,7 +66,7 @@
.attr("width", 0) // Start with width 0 for animation
.on('mouseover', function(event) {
tooltip.style('display', 'block')
.html(`Remaining: ${messagesRemaining.toLocaleString()}`);
.html(`Remaining: ${sms_remaining_messages.toLocaleString()}`);
})
.on('mousemove', function(event) {
tooltip.style('left', `${event.pageX + 10}px`)
@@ -115,9 +115,9 @@
var tbodyRow = document.createElement('tr');
var tdMessagesSent = document.createElement('td');
tdMessagesSent.textContent = messagesSent.toLocaleString(); // Value for Messages Sent
tdMessagesSent.textContent = sms_sent.toLocaleString(); // Value for Messages Sent
var tdRemaining = document.createElement('td');
tdRemaining.textContent = messagesRemaining.toLocaleString(); // Value for Remaining
tdRemaining.textContent = sms_remaining_messages.toLocaleString(); // Value for Remaining
tbodyRow.appendChild(tdMessagesSent);
tbodyRow.appendChild(tdRemaining);

View File

@@ -0,0 +1,186 @@
@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

@@ -0,0 +1,81 @@
@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

@@ -0,0 +1,79 @@
@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

@@ -0,0 +1,35 @@
@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

@@ -0,0 +1,8 @@
@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,25 +1,3 @@
/*
* * * * * ==============================
* * * * * ==============================
* * * * * ==============================
* * * * * ==============================
========================================
========================================
========================================
----------------------------------------
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,
@@ -56,7 +34,10 @@ button:not([disabled]):focus {
}
}
@include at-media-max('desktop') {
padding: 0 units(2);
.usa-nav__secondary-links
{
padding-left: units(2);
}
ul li {
padding-bottom: units(1);
}
@@ -160,9 +141,17 @@ td.table-empty-message {
}
}
.usa-button img {
margin-left: 0.5rem;
height: 1rem;
.login-button {
margin-right: 0;
img {
height: 1rem;
}
@media (max-width: units('desktop')) {
img {
height: 14px;
}
font-size: 14px;
}
}
.usa-button.login-button.login-button--primary,
@@ -365,227 +354,6 @@ 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,
@@ -747,86 +515,6 @@ 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;
@@ -971,32 +659,6 @@ 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");

View File

@@ -1,5 +1,10 @@
@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

@@ -1,90 +0,0 @@
// 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

@@ -1,8 +1,7 @@
import calendar
from datetime import datetime, timedelta
from datetime import datetime
from functools import partial
from itertools import groupby
from zoneinfo import ZoneInfo
from flask import Response, abort, jsonify, render_template, request, session, url_for
from flask_login import current_user
@@ -49,99 +48,48 @@ def service_dashboard(service_id):
if not current_user.has_permissions("view_activity"):
return redirect(url_for("main.choose_template", service_id=service_id))
yearly_usage = billing_api_client.get_annual_usage_for_service(
service_id,
get_current_financial_year(),
)
free_sms_allowance = billing_api_client.get_free_sms_fragment_limit_for_year(
current_service.id,
)
usage_data = get_annual_usage_breakdown(yearly_usage, free_sms_allowance)
sms_sent = usage_data["sms_sent"]
sms_allowance_remaining = usage_data["sms_allowance_remaining"]
job_response = job_api_client.get_jobs(service_id)["data"]
service_data_retention_days = 7
active_jobs = [job for job in job_response if job["job_status"] != "cancelled"]
sorted_jobs = sorted(active_jobs, key=lambda job: job["created_at"], reverse=True)
job_lists = [
{**job_dict, "finished_processing": job_is_finished(job_dict)}
for job_dict in sorted_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)
total_messages = service_api_client.get_service_message_ratio(service_id)
messages_remaining = total_messages.get("messages_remaining", 0)
messages_sent = total_messages.get("messages_sent", 0)
return render_template(
"views/dashboard/dashboard.html",
updates_url=url_for(".service_dashboard_updates", service_id=service_id),
partials=get_dashboard_partials(service_id),
jobs=job_lists,
jobs=sorted_jobs,
service_data_retention_days=service_data_retention_days,
messages_remaining=messages_remaining,
messages_sent=messages_sent,
sms_sent=sms_sent,
sms_allowance_remaining=sms_allowance_remaining,
)
def job_is_finished(job_dict):
done_statuses = [
"delivered",
"sent",
"failed",
"technical-failure",
"temporary-failure",
"permanent-failure",
"cancelled",
]
processed_count = sum(
stat["count"]
for stat in job_dict["statistics"]
if stat["status"] in done_statuses
)
return job_dict["notification_count"] == processed_count
@main.route("/services/<uuid:service_id>/daily-stats.json")
@user_has_permissions()
def get_daily_stats(service_id):
date_range = get_stats_date_range()
days = date_range["days"]
user_timezone = request.args.get("timezone", "UTC")
stats_utc = service_api_client.get_service_notification_statistics_by_day(
service_id,
start_date=date_range["start_date"],
days=days,
stats = service_api_client.get_service_notification_statistics_by_day(
service_id, start_date=date_range["start_date"], days=date_range["days"]
)
local_stats = get_local_daily_stats_for_last_x_days(stats_utc, user_timezone, days)
return jsonify(local_stats)
def get_local_daily_stats_for_last_x_days(stats_utc, user_timezone, days):
tz = ZoneInfo(user_timezone)
today_local = datetime.now(tz).date()
start_local = today_local - timedelta(days=days - 1)
# Generate exactly days local dates, each with zeroed stats
days_list = [
(start_local + timedelta(days=i)).strftime("%Y-%m-%d") for i in range(days)
]
aggregator = {
d: {
"sms": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
"email": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
}
for d in days_list
}
# Convert each UTC timestamp to local date and iterate
for utc_ts, data in stats_utc.items():
utc_dt = datetime.strptime(utc_ts, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=ZoneInfo("UTC"))
local_day = utc_dt.astimezone(tz).strftime("%Y-%m-%d")
if local_day in aggregator:
for msg_type in ["sms", "email"]:
for status in ["delivered", "failure", "pending", "requested"]:
aggregator[local_day][msg_type][status] += data[msg_type][status]
return aggregator
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):
service_id = session.get("service_id")
date_range = get_stats_date_range()
stats = service_api_client.get_user_service_notification_statistics_by_day(
service_id,

View File

@@ -116,70 +116,6 @@ def download_all_users():
return response
@main.route("/platform-admin/get-redis-report")
@user_is_platform_admin
def get_redis_report():
memory_info = redis_client.info("memory")
memory_used = memory_info.get("used_memory_human", "N/A")
max_memory = memory_info.get("maxmemory_human", "N/A")
if max_memory == "0B":
max_memory = "No set limit"
mem_fragmentation = memory_info.get("mem_fragmentation_ratio", "N/A")
frag_quality = "Swapping (bad)"
if mem_fragmentation >= 1.0:
frag_quality = "Healthy"
if mem_fragmentation > 1.5:
frag_quality = "Problematic"
if mem_fragmentation > 2.0:
frag_quality = "Severe fragmentation"
frag_note = ""
if mem_fragmentation > 2.0:
frag_note = "Use MEMORY PURGE.\nReplace multiple small keys with hashes.\nAvoid long keys.\nSet max_memory."
elif mem_fragmentation < 1.0:
frag_note = "Allocate more RAM.\nSet max_memory."
keys = redis_client.keys("*")
key_details = []
for key in keys:
key_type = redis_client.type(key).decode("utf-8")
ttl = redis_client.ttl(key)
ttl_str = "No Expiry" if ttl == -1 else f"{ttl} seconds"
key_details.append(
{"Key": key.decode("utf-8"), "Type": key_type, "TTL": ttl_str}
)
output = StringIO()
writer = csv.writer(
output,
)
writer.writerow(["Redis Report"])
writer.writerow([])
writer.writerow(["Memory"])
writer.writerow(["", "Memory Used", memory_used])
writer.writerow(["", "Max Memory", max_memory])
writer.writerow(["", "Memory Fragmentation Ratio", mem_fragmentation])
writer.writerow(["", "Memory Fragmentation Quality", frag_quality, frag_note])
writer.writerow([])
writer.writerow(["Keys Overview"])
writer.writerow(["", "TTL", "Type", "Key"])
for key_detail in key_details:
writer.writerow(
["", key_detail["TTL"], key_detail["Type"], key_detail["Key"][0:50]]
)
csv_data = output.getvalue()
# Create a direct download response with the CSV data and appropriate headers
response = Response(csv_data, content_type="text/csv; charset=utf-8")
response.headers["Content-Disposition"] = "attachment; filename=redis.csv"
return response
def is_over_threshold(number, total, threshold):
percentage = number / total * 100 if total else 0
return percentage > threshold

View File

@@ -68,12 +68,11 @@ def _get_access_token(code): # pragma: no cover
id_token = get_id_token(response_json)
nonce = id_token["nonce"]
nonce_key = f"login-nonce-{unquote(nonce)}"
if not os.getenv("NOTIFY_ENVIRONMENT") == "development":
stored_nonce = redis_client.get(nonce_key).decode("utf8")
stored_nonce = redis_client.get(nonce_key).decode("utf8")
if nonce != stored_nonce:
current_app.logger.error(f"Nonce Error: {nonce} != {stored_nonce}")
abort(403)
if nonce != stored_nonce:
current_app.logger.error(f"Nonce Error: {nonce} != {stored_nonce}")
abort(403)
try:
access_token = response_json["access_token"]
@@ -113,7 +112,7 @@ def _do_login_dot_gov(): # $ pragma: no cover
verify_key = f"login-verify_email-{unquote(state)}"
verify_path = bool(redis_client.get(verify_key))
if not verify_path and not os.getenv("NOTIFY_ENVIRONMENT") == "development":
if not verify_path:
state_key = f"login-state-{unquote(state)}"
stored_state = unquote(redis_client.get(state_key).decode("utf8"))
if state != stored_state:

View File

@@ -537,11 +537,6 @@ class ServiceAPIClient(NotifyAdminAPIClient):
"""
return self.get("/service/invite/redis/{0}".format(redis_key))
def get_service_message_ratio(self, service_id):
return self.get(
url="service/get-service-message-ratio?service_id={0}".format(service_id),
)
service_api_client = ServiceAPIClient()

View File

@@ -14,8 +14,10 @@
<script nonce="{{ csp_nonce() }}">document.body.className = ((document.body.className) ? document.body.className + ' js-enabled' : 'js-enabled');</script>
{% block bodyStart %}
{% block extra_javascripts_before_body %}
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-WX5NGWF"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->
{% endblock %}
{% endblock %}
@@ -143,8 +145,10 @@
{% block bodyEnd %}
{% block extra_javascripts %}
{% endblock %}
<!--[if gt IE 8]><!-->
<script type="text/javascript" src="{{ asset_url('javascripts/all.js') }}"></script>
<script type="text/javascript" src="{{ asset_url('js/uswds.min.js') }}"></script>
<!--<![endif]-->
{% endblock %}
</body>
</html>

View File

@@ -39,12 +39,6 @@
] %}
{% endif %}
<header class="usa-header usa-header--extended">
<div class="usa-nav-container">
<div class="usa-navbar">
@@ -53,7 +47,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="Notify.gov logo"
<img src="{{ (asset_path | default('/static')) + 'images/notify-logo.svg' }}" alt=""
class="usa-flag-logo margin-right-1">
</a>
</div>
@@ -61,14 +55,6 @@
<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>
@@ -89,7 +75,11 @@
{% 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 %}
@@ -104,8 +94,19 @@
{% 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

@@ -25,7 +25,7 @@
{% if job.scheduled_for %}
<div class="usa-alert usa-alert--info">
<div class="usa-alert__body">
<h2 class="usa-alert__heading">Your {{ 'message has' if job.notification_count == 1 else 'messages have' }} been scheduled</h2>
<h2 class="usa-alert__heading">Your message has been scheduled</h2>
<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 }}
</p>
@@ -37,7 +37,7 @@
<div class="usa-alert usa-alert--success">
<div class="usa-alert__body">
<h2 class="usa-alert__heading">
Your {{ 'message is' if job.notification_count == 1 else 'messages are' }} sending
Your message is sending
</h2>
<p class="usa-alert__text">
{{ job.template_name }} - {{ current_service.name }}
@@ -49,7 +49,7 @@
<div class="usa-alert usa-alert--info">
<div class="usa-alert__body">
<h2 class="usa-alert__heading">
Your {{ 'message is' if job.notification_count == 1 else 'messages are' }} pending
Your message is pending
</h2>
<p class="usa-alert__text">
{{ job.template_name }} - {{ current_service.name }}
@@ -64,7 +64,7 @@
<div class="usa-alert usa-alert--success">
<div class="usa-alert__body">
<h2 class="usa-alert__heading">
Your {{ 'message has' if job.notification_count == 1 else 'messages have' }} been sent
Your message has been sent
</h2>
<p class="usa-alert__text">
{{ job.template_name }} - {{ current_service.name }}

View File

@@ -46,16 +46,14 @@
</td>
<td class="table-field template">{{ job.template_name }}</td>
<td class="table-field time-sent">
{% if not job.finished_processing %}
{% if job.scheduled_for%}
Scheduled for {{ job.scheduled_for|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 %}
{% 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 %}
Sent on {{ job.processing_started|format_datetime_table }}
Pending since {{ job.created_at|format_datetime_table }}
{% endif %}
</td>
<td class="table-field sender sender-column">{{ job.created_by.name }}</td>

View File

@@ -26,7 +26,7 @@
{{ ajax_block(partials, updates_url, 'inbox') }}
<div id="totalMessageChartContainer" data-messages-sent="{{ messages_sent }}" data-messages-remaining="{{ messages_remaining }}">
<div id="totalMessageChartContainer" data-sms-sent="{{ sms_sent }}" data-sms-allowance-remaining="{{ sms_allowance_remaining }}">
<h2 id="chartTitle">Total messages</h2>
<svg id="totalMessageChart"></svg>
<div id="message"></div>

View File

@@ -11,7 +11,7 @@
{% block maincolumn_content %}
{{ page_header("Message status") }}
{% if not job.finished_processing %}
{% if not job.processing_finished %}
<div
data-module="update-content"
data-resource="{{ updates_url }}"
@@ -20,7 +20,7 @@
>
{% endif %}
{{ partials['status']|safe }}
{% if not job.finished_processing %}
{% if not job.processing_finished %}
</div>
{% endif %}
{% if not finished %}

View File

@@ -34,7 +34,7 @@
{% set product_highlights = [
{
"svg_src": "#chat",
"card_heading": "Up to 250,000 messages to use over your first year*",
"card_heading": "Up to 100,000 messages to use over your first year*",
},
{
"svg_src": "#phone",

View File

@@ -34,8 +34,5 @@
<p>
<a class="usa-link" href="{{ url_for('main.download_all_users') }}">Download All Users</a>
</p>
<p>
<a class="usa-link" href="{{ url_for('main.get_redis_report') }}">Get Redis Report</a>
</p>
{% endblock %}

View File

@@ -9,70 +9,65 @@ import usaButton %} {% block meta %}
<main id="main-content" role="main">
{% block content %}
<section class="usa-section--dark usa-hero" aria-label="Introduction">
<section class="usa-section--dark usa-hero padding-y-2" 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="font-sans-2xl usa-hero__heading">
<h1 class="usa-hero__heading">
Reach people where they are with government-powered text messages
</h1>
<p class="font-sans-lg">
<p class="desktop:font-sans-lg font-sans-md">
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-3 grid-offset-1 desktop:display-block display-none margin-x-5"
class="desktop:grid-col-4 desktop:display-block display-none"
>
<img
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."
src="{{ asset_url('images/product/hero-icons.svg') }}"
alt=""
/>
</div>
</div>
</div>
</section>
<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 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>
<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">
<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">
<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="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 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>
<div class="usa-card__body">
<p>Nothing to download or install</p>
@@ -81,13 +76,14 @@ 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="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 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>
<div class="usa-card__body">
<p>No technical expertise required</p>
@@ -96,13 +92,14 @@ 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="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 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>
<div class="usa-card__body">
<p>See which messages were received</p>
@@ -111,17 +108,16 @@ import usaButton %} {% block meta %}
</li>
</ul>
<ul class="usa-card-group">
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
<li class="usa-card tablet:grid-offset-2 tablet:grid-col-4 mobile-lg:grid-col-12">
<div class="usa-card__container">
<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 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>
<div class="usa-card__body">
<p>Notify.gov has support for more than 30 character sets</p>
@@ -130,31 +126,14 @@ 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="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="font-heading-md">Security and privacy</h3>
<div class="img-container">
<img
src="{{ asset_url('images/send.svg') }}"
alt=""
/>
</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">
<h3 class="padding-bottom-0">
Send bulk, customized, one-way messages
</h3>
</div>
@@ -170,51 +149,52 @@ import usaButton %} {% block meta %}
</div>
</section>
<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>
<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>
</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

@@ -128,13 +128,6 @@ def generate_notifications_csv(**kwargs):
notifications_resp = notification_api_client.get_notifications_for_service(
**kwargs
)
# Stop if we are finished
if (
notifications_resp.get("notifications") is None
or len(notifications_resp["notifications"]) == 0
):
return
for notification in notifications_resp["notifications"]:
preferred_tz_created_at = convert_report_date_to_preferred_timezone(
notification["created_at"]

View File

@@ -1,10 +1,7 @@
from flask import Flask
from werkzeug.serving import WSGIRequestHandler
from app import create_app
WSGIRequestHandler.version_string = lambda self: "SecureServer"
application = Flask("app")
create_app(application)

View File

@@ -1,136 +0,0 @@
# Manual Accessibility Testing Checklist
## 1. Structure and Semantics
### Headings:
- Verify that headings are used logically (`<h1>` to `<h6>`) to create a clear content hierarchy.
- Ensure there is only one `<h1>` per page (unless its a valid use case, like within `<section>` landmarks).
### Landmarks:
- Confirm the presence of ARIA landmarks (e.g., `<header>`, `<main>`, `<footer>`) for navigation.
- Use tools (e.g., Accessibility Insights, Axe) to ensure proper landmark roles.
### HTML Validity:
- Check for semantic HTML usage (e.g., `<button>` for buttons, `<a>` for links).
- Avoid using `<div>` or `<span>` for interactive elements.
## 2. Navigation and Focus
### Keyboard Navigation:
- Ensure all functionality is accessible via the keyboard (e.g., Tab, Enter, Space, Arrow Keys).
- Test for logical tab order that follows the visual reading order.
### Focus States:
- Ensure focus is visible and distinct on all interactive elements.
- Check that focus is not trapped in modal dialogs or components.
### Skip Links:
- Verify that “Skip to content” links are present and functional.
## 3. Forms and Inputs
### Labels:
- Confirm all form fields have accessible, descriptive labels (`<label>` or `aria-label`/`aria-labelledby`).
- Ensure placeholder text is not used as a label substitute.
### Error Messages:
- Check that error messages are programmatically associated with inputs and conveyed to assistive technologies.
- Verify that error messages are specific and provide actionable guidance.
### Fieldset and Legend:
- Group related inputs with `<fieldset>` and `<legend>` where appropriate.
## 4. Media and Non-Text Content
### Images:
- Ensure all meaningful images have appropriate alt text.
- Decorative images should have `alt=""` or be hidden with `role="presentation"`.
### Videos:
- Verify captions are available and accurate for all video content.
- Provide audio descriptions for videos with critical visual information.
### Audio:
- Confirm there is a way to stop, pause, or adjust the volume of any audio that plays automatically.
## 5. Color and Contrast
### Color Contrast:
- Use a contrast checker to ensure text and interactive elements meet WCAG AA requirements (4.5:1 for text, 3:1 for large text).
### Color Independence:
- Verify that color is not the sole means of conveying information (e.g., “errors in red”).
### High Contrast Modes:
- Test with browser or OS high-contrast modes to ensure proper readability.
---
## 6. Dynamic Content and Interactions
### ARIA Live Regions:
- Verify that dynamic updates are announced using `aria-live` (e.g., success messages).
### Modals and Dialogs:
- Ensure modals have proper focus management (focus should move to the modal on open and back to the trigger on close).
- Test that modals are announced properly by screen readers.
### Tooltips and Popovers:
- Ensure tooltips are accessible via keyboard and announced by assistive technologies.
## 7. Assistive Technology Compatibility
### Screen Reader Testing:
- Test the site with a screen reader (e.g., NVDA, JAWS, VoiceOver) to ensure content is announced logically.
- Verify that dynamic content (e.g., dropdowns, modals) is announced appropriately.
### Zoom and Magnification:
- Ensure the site is usable at 200% zoom without loss of functionality or content.
### Responsive Design:
- Test on different devices and orientations to verify responsive behavior is accessible.
## 8. Performance and Usability
### Loading Indicators:
- Confirm that loading indicators are announced (e.g., using `aria-live`).
### Page Titles:
- Ensure page titles are unique and descriptive of the pages content.
### Time Limits:
- Check if users can extend or disable time limits where applicable.
## 9. WCAG Success Criteria Coverage
### Review WCAG 2.1 (or 2.2 if applicable) Success Criteria:
- **Level A**: Must-have minimum requirements.
- **Level AA**: Commonly required for legal compliance.
- **Level AAA**: Optional for enhanced accessibility.
## Tools to Assist Manual Testing
### Browser Extensions:
- [Axe DevTools](https://www.deque.com/axe/)
- [WAVE](https://wave.webaim.org/)
- [Lighthouse](https://developers.google.com/web/tools/lighthouse)
### Screen Readers:
- **NVDA** (Windows)
- **VoiceOver** (macOS/iOS)
- **TalkBack** (Android)
### Contrast Checkers:
- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
- [Color Contrast Analyzer](https://developer.paciellogroup.com/resources/contrastanalyser/)
### Keyboard Navigation:
- Test tabbing through the site manually.

View File

@@ -149,22 +149,6 @@ class RedisClient:
except Exception as e:
self.__handle_exception(e, raise_exception, "incr", key)
def info(self, key):
if self.active:
return self.redis_store.info(key)
def keys(self, pattern):
if self.active:
return self.redis_store.keys(pattern)
def type(self, key):
if self.active:
return self.redis_store.type(key)
def ttl(self, key):
if self.active:
return self.redis_store.ttl(key)
def get(self, key, raise_exception=False):
key = prepare_value(key)
if self.active:

View File

@@ -75,24 +75,6 @@ class ResponseHeaderMiddleware(object):
if SPAN_ID_HEADER.lower() not in lower_existing_header_names:
headers.append((SPAN_ID_HEADER, str(req.span_id)))
# Some dynamic scan findings
headers.append(("Cross-Origin-Opener-Policy", "same-origin"))
headers.append(("Cross-Origin-Embedder-Policy", "require-corp"))
headers.append(("Cross-Origin-Resource-Policy", "same-origin"))
headers.append(("Cross-Origin-Opener-Policy", "same-origin"))
# svg content type should not contain charset
found_svg = False
for _, v in headers:
if "svg+xml" in v:
found_svg = True
if found_svg:
new_headers = [
(k, v) for k, v in headers if k.lower() != "content-type"
]
new_headers.append(("Content-Type", "image/svg+xml"))
return start_response(status, new_headers, exc_info)
return start_response(status, headers, exc_info)
return self._app(environ, rewrite_response_headers)

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/main.css?d077f86473501ab244de0b30600536ee" />
<link rel="stylesheet" media="screen" href="/static_503/stylesheets/legacy/uk.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>

572
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -40,15 +40,15 @@
"playwright": "^1.50.1",
"python": "^0.0.4",
"query-command-supported": "1.0.0",
"sass-embedded": "^1.85.1",
"sass-embedded": "^1.83.4",
"textarea-caret": "3.1.0",
"timeago": "1.6.7",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0"
},
"devDependencies": {
"@babel/core": "^7.26.9",
"@babel/preset-env": "^7.26.9",
"@babel/core": "^7.26.7",
"@babel/preset-env": "^7.26.7",
"@uswds/compile": "^1.2.1",
"backstopjs": "^6.3.25",
"better-npm-audit": "^3.11.0",
@@ -66,7 +66,7 @@
"jest-environment-jsdom": "^29.2.2",
"jshint": "2.13.6",
"jshint-stylish": "2.2.1",
"rollup": "^4.34.9",
"rollup": "^4.34.4",
"rollup-plugin-commonjs": "10.1.0",
"rollup-plugin-node-resolve": "5.2.0"
}

220
poetry.lock generated
View File

@@ -42,17 +42,17 @@ tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"]
[[package]]
name = "awscli"
version = "1.36.40"
version = "1.35.17"
description = "Universal Command Line Environment for AWS."
optional = false
python-versions = ">=3.8"
files = [
{file = "awscli-1.36.40-py3-none-any.whl", hash = "sha256:971c3b150c06068bc26867fe295753547780f63fcf8256d41cd38760e44d46ca"},
{file = "awscli-1.36.40.tar.gz", hash = "sha256:e2a88f88dc16d5c0f26379afd6f254097e53e8b34c82164e59f4165db6ee6dfa"},
{file = "awscli-1.35.17-py3-none-any.whl", hash = "sha256:67906511b138bb6b241136c0ba6bee854f522b7b506887911e6ad34877a822c3"},
{file = "awscli-1.35.17.tar.gz", hash = "sha256:9469a1924c6987dd0553ad0d0fd2a37717d0f7b55104e99f5789d3678c9706c4"},
]
[package.dependencies]
botocore = "1.35.99"
botocore = "1.35.51"
colorama = ">=0.2.5,<0.4.7"
docutils = ">=0.10,<0.17"
PyYAML = ">=3.10,<6.1"
@@ -219,13 +219,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]
[[package]]
name = "botocore"
version = "1.35.99"
version = "1.35.51"
description = "Low-level, data-driven core of boto 3."
optional = false
python-versions = ">=3.8"
files = [
{file = "botocore-1.35.99-py3-none-any.whl", hash = "sha256:b22d27b6b617fc2d7342090d6129000af2efd20174215948c0d7ae2da0fab445"},
{file = "botocore-1.35.99.tar.gz", hash = "sha256:1eab44e969c39c5f3d9a3104a0836c24715579a455f12b3979a31d7cde51b3c3"},
{file = "botocore-1.35.51-py3-none-any.whl", hash = "sha256:4d65b00111bd12b98e9f920ecab602cf619cc6a6d0be6e5dd53f517e4b92901c"},
{file = "botocore-1.35.51.tar.gz", hash = "sha256:a9b3d1da76b3e896ad74605c01d88f596324a3337393d4bfbfa0d6c35822ca9c"},
]
[package.dependencies]
@@ -595,55 +595,51 @@ toml = ["tomli"]
[[package]]
name = "cryptography"
version = "44.0.1"
version = "43.0.3"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = false
python-versions = "!=3.9.0,!=3.9.1,>=3.7"
python-versions = ">=3.7"
files = [
{file = "cryptography-44.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf688f615c29bfe9dfc44312ca470989279f0e94bb9f631f85e3459af8efc009"},
{file = "cryptography-44.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd7c7e2d71d908dc0f8d2027e1604102140d84b155e658c20e8ad1304317691f"},
{file = "cryptography-44.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887143b9ff6bad2b7570da75a7fe8bbf5f65276365ac259a5d2d5147a73775f2"},
{file = "cryptography-44.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:322eb03ecc62784536bc173f1483e76747aafeb69c8728df48537eb431cd1911"},
{file = "cryptography-44.0.1-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:21377472ca4ada2906bc313168c9dc7b1d7ca417b63c1c3011d0c74b7de9ae69"},
{file = "cryptography-44.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:df978682c1504fc93b3209de21aeabf2375cb1571d4e61907b3e7a2540e83026"},
{file = "cryptography-44.0.1-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:eb3889330f2a4a148abead555399ec9a32b13b7c8ba969b72d8e500eb7ef84cd"},
{file = "cryptography-44.0.1-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8e6a85a93d0642bd774460a86513c5d9d80b5c002ca9693e63f6e540f1815ed0"},
{file = "cryptography-44.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6f76fdd6fd048576a04c5210d53aa04ca34d2ed63336d4abd306d0cbe298fddf"},
{file = "cryptography-44.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6c8acf6f3d1f47acb2248ec3ea261171a671f3d9428e34ad0357148d492c7864"},
{file = "cryptography-44.0.1-cp37-abi3-win32.whl", hash = "sha256:24979e9f2040c953a94bf3c6782e67795a4c260734e5264dceea65c8f4bae64a"},
{file = "cryptography-44.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:fd0ee90072861e276b0ff08bd627abec29e32a53b2be44e41dbcdf87cbee2b00"},
{file = "cryptography-44.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a2d8a7045e1ab9b9f803f0d9531ead85f90c5f2859e653b61497228b18452008"},
{file = "cryptography-44.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8272f257cf1cbd3f2e120f14c68bff2b6bdfcc157fafdee84a1b795efd72862"},
{file = "cryptography-44.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e8d181e90a777b63f3f0caa836844a1182f1f265687fac2115fcf245f5fbec3"},
{file = "cryptography-44.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:436df4f203482f41aad60ed1813811ac4ab102765ecae7a2bbb1dbb66dcff5a7"},
{file = "cryptography-44.0.1-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4f422e8c6a28cf8b7f883eb790695d6d45b0c385a2583073f3cec434cc705e1a"},
{file = "cryptography-44.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:72198e2b5925155497a5a3e8c216c7fb3e64c16ccee11f0e7da272fa93b35c4c"},
{file = "cryptography-44.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a46a89ad3e6176223b632056f321bc7de36b9f9b93b2cc1cccf935a3849dc62"},
{file = "cryptography-44.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:53f23339864b617a3dfc2b0ac8d5c432625c80014c25caac9082314e9de56f41"},
{file = "cryptography-44.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:888fcc3fce0c888785a4876ca55f9f43787f4c5c1cc1e2e0da71ad481ff82c5b"},
{file = "cryptography-44.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00918d859aa4e57db8299607086f793fa7813ae2ff5a4637e318a25ef82730f7"},
{file = "cryptography-44.0.1-cp39-abi3-win32.whl", hash = "sha256:9b336599e2cb77b1008cb2ac264b290803ec5e8e89d618a5e978ff5eb6f715d9"},
{file = "cryptography-44.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:e403f7f766ded778ecdb790da786b418a9f2394f36e8cc8b796cc056ab05f44f"},
{file = "cryptography-44.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1f9a92144fa0c877117e9748c74501bea842f93d21ee00b0cf922846d9d0b183"},
{file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:610a83540765a8d8ce0f351ce42e26e53e1f774a6efb71eb1b41eb01d01c3d12"},
{file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5fed5cd6102bb4eb843e3315d2bf25fede494509bddadb81e03a859c1bc17b83"},
{file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f4daefc971c2d1f82f03097dc6f216744a6cd2ac0f04c68fb935ea2ba2a0d420"},
{file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94f99f2b943b354a5b6307d7e8d19f5c423a794462bde2bf310c770ba052b1c4"},
{file = "cryptography-44.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d9c5b9f698a83c8bd71e0f4d3f9f839ef244798e5ffe96febfa9714717db7af7"},
{file = "cryptography-44.0.1.tar.gz", hash = "sha256:f51f5705ab27898afda1aaa430f34ad90dc117421057782022edf0600bec5f14"},
{file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"},
{file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"},
{file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"},
{file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"},
{file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"},
{file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"},
{file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"},
{file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"},
{file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"},
{file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"},
{file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"},
{file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"},
{file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"},
{file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"},
{file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"},
{file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"},
{file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"},
{file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"},
{file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"},
{file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"},
{file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"},
{file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"},
{file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"},
{file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"},
{file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"},
{file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"},
{file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"},
]
[package.dependencies]
cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
[package.extras]
docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0)"]
docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"]
nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2)"]
pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"]
sdist = ["build (>=1.0.0)"]
docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"]
docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"]
nox = ["nox"]
pep8test = ["check-sdist", "click", "mypy", "ruff"]
sdist = ["build"]
ssh = ["bcrypt (>=3.1.5)"]
test = ["certifi (>=2024)", "cryptography-vectors (==44.0.1)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
test-randomorder = ["pytest-randomly"]
[[package]]
@@ -1750,40 +1746,40 @@ files = [
[[package]]
name = "newrelic"
version = "10.6.0"
version = "10.2.0"
description = "New Relic Python Agent"
optional = false
python-versions = ">=3.7"
files = [
{file = "newrelic-10.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0192ae5b6d624b1dbd5bb5808e0b0846e109d614b38ddfab6328e87bb261d889"},
{file = "newrelic-10.6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f2cfa6aee9668f6e19c3eba77c851dddbd0bc4c87af2551ad6a56f17a3d5fbe"},
{file = "newrelic-10.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c532a2ec39b1f8fbeba7b557ae2d0cb3440012a0ecbbd138b6a137ecd90224c9"},
{file = "newrelic-10.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b282a069984e3bf3bc810eaf988165eb0e8dccc99b3c34d7c0f34c7649093fbe"},
{file = "newrelic-10.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5682142c7f94ca1294b51e3c49cb3ef64580ce36ff223efa23fbc2f34e7fb392"},
{file = "newrelic-10.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cc7d1df935239613582d592253fbe3a961f372c9f2568e4e8b0937caf8f072b"},
{file = "newrelic-10.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:659991fc2f5149b4f7711e7f51c0a76ac1bdef7ca0b5f1b4806c21ed05553963"},
{file = "newrelic-10.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c657a50cc831d80bff310022106e1086c224b50a114241cb61194921498bcd5c"},
{file = "newrelic-10.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8af1c6cea8ffdb185fcfe871f61ab2de8471c71b1d417f07a93e6fe7713f012a"},
{file = "newrelic-10.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3af6f16cfa95d187a37be26ef44f73b15c33b3600c14d4936fc77666a26192e6"},
{file = "newrelic-10.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a64e99fe3bb0a5bb9f942053291214659345064f79355f1cb1753b2ae3989792"},
{file = "newrelic-10.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:120bce9635c39e8f444e127132e08cf536a042138c5688c6bce27dbfd44caf6f"},
{file = "newrelic-10.6.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b539ebb8e624b797a4dd35ef742a11148f16d8d9b4da5b4b4c77c33a354e9206"},
{file = "newrelic-10.6.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abe82936cd60d9ebfb3573311c9ca4c22682f3ca5a27baa2bf5f9e26a7a4ed7e"},
{file = "newrelic-10.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f3a9c60cd1b4b76acab30306d30b95b90fd19eaa38c34ef84dcaade7a6894426"},
{file = "newrelic-10.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eb69b15f2f917fd7874e05f0444752ae1cc1eb63028814ab77033babfc6329e5"},
{file = "newrelic-10.6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8ca468375f5959867fda7bbe90f2d80ca32aae6c5d632e3234e32064013e439"},
{file = "newrelic-10.6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:319c4df59515d214ae91fd6c46cc7fc120217ffc38a080689fd9930b3b2a7530"},
{file = "newrelic-10.6.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:23fc4539240bb8a5c131d56f0d2d982c253ee2adf6e2dff945daaab07ef5e527"},
{file = "newrelic-10.6.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89b304474705254d94cfc82d58a505f18c5bba029aaec32fba15ba582cb2c849"},
{file = "newrelic-10.6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:465f4f8be765121a6e1d215184b61b8c6c5bb24106f7e3f1443cffe14f647e8f"},
{file = "newrelic-10.6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:579d146ff0309e93af712759df8732e7d23a7c676dc1e38c2ac32af46be15f5d"},
{file = "newrelic-10.6.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:07b31b0970ad4b160a9481f1bd908250aa39ee8d7f2a4fa1d88b13c360361241"},
{file = "newrelic-10.6.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:2c9f39b6b1154dfa21831d7632b53cd1fa1a2b4069a273dead1a40186621688e"},
{file = "newrelic-10.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ba77efcbf1aaa902cdc546480fa05af6e907e4334798b53ab08f34be1d13a9c"},
{file = "newrelic-10.6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaf6f29b82845355faa32e719d246a63b29d2b8cbd406cc31a4991dc213c4813"},
{file = "newrelic-10.6.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:275d930c60cb534e26dd2aaa61e5c25b6312d63fef011055203d3f6a26e20863"},
{file = "newrelic-10.6.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:43a8aafc4a5d844192872a94d9033f4721693a6c0950750f5a0b0e448821831b"},
{file = "newrelic-10.6.0.tar.gz", hash = "sha256:46d6f39a9cb89cca2440211e272e6a13bb89802865a4c2b2cd9d613c1488c3ba"},
{file = "newrelic-10.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6f822e6a43151af13a748fb2de6ff298aeb6eee03bf6512afba6aaa79211172"},
{file = "newrelic-10.2.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:501cc575b3fd702a21542a0f5dac59b83f47e2806f5b7c0f4e4510b5474ed77f"},
{file = "newrelic-10.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3f15a940b6794b4008ab983e7ac3b4d179efe609e040ee96ed5744723fc580c8"},
{file = "newrelic-10.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:60d01303807228718c4099d8550f72d21ee8b61a33555d8974800f6868f2144a"},
{file = "newrelic-10.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5809b4111ef3b1d0b5fa66ad06a81de512842370707863d44888c9439f16c4c"},
{file = "newrelic-10.2.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3415b1c7cab5e586e72cca467dd80cd0507f23a3139c02911cf75892fdbb48a6"},
{file = "newrelic-10.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3d9c8297ba158ce4570fc48cfea7bdf3678b2054baaf0cad4debcbca33c2af3a"},
{file = "newrelic-10.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3bee0b9ce1eccf6ac63e51113781743853b1b84c98ae48ed17d0410c352ccb4d"},
{file = "newrelic-10.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6ff022c7556b61b067e8e6b729fe60f437a8356f319ae3b8342858792f3930d"},
{file = "newrelic-10.2.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3521d646c0032db53b7320fe6b6859eebd863f1b47d7c7dd480073727091e50e"},
{file = "newrelic-10.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b4220b97669d214e75d2039fea9e0505fde5bc450832210abbc76b9a635785ed"},
{file = "newrelic-10.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c6aa9cf936b16d13b65c1b7aa6c722a76a0702469f91bcfc3c5b39f3293181eb"},
{file = "newrelic-10.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36a2218c9e79897d9b5671cdeac30c467d7fbac10cda4f2d79062f2bd0fcaed8"},
{file = "newrelic-10.2.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f021eac4c2e3b14eab90c608d8bd25b4e3c6b0b0d40796ec1c1260cc47b5e83"},
{file = "newrelic-10.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:69aa68cae47c595bdeb95f275d78693ec27a9fd9353bf81257e21f8607134db6"},
{file = "newrelic-10.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ef5d27001d3b5ca53f19d150c60b570c1b0c774d082ab9bf8349f4430ed85b48"},
{file = "newrelic-10.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00df1aa613294cb592a52157f789e75166dbf439cfa9e6cf59f6cf4a265dada9"},
{file = "newrelic-10.2.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6d4094d19db924c51ca35da603344907bcbca030822f7a78d5d9c6ad361d419"},
{file = "newrelic-10.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:62d521a5d7269c8a5c5838c4ca3b757ef63a13257302c901223c75510cc6f9f5"},
{file = "newrelic-10.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3cd5aeade6462519328fc42f4e98948a45571f3d22360a0559e19a6525c723a6"},
{file = "newrelic-10.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6333aa7051544ddc7f8a85f344bc3f401ddd8635540878da34de7bfd91f5d95"},
{file = "newrelic-10.2.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b449546ebb89feaadfd36fda7735ce06023fc90979b838e244f98369aba5ccb"},
{file = "newrelic-10.2.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fc3d34db12133b481636384663f45b9ccd7f0f41554a59c15ec37aeb4f77227d"},
{file = "newrelic-10.2.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a15df23effd09bb1d1f5c38866b75cc5f380b6aa953efbca9e95e79b72744db4"},
{file = "newrelic-10.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc693e0db87ab4cf6623847c3949debbcc991554edfb4dd8c02c136e0770b367"},
{file = "newrelic-10.2.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c57e79d37ed87e2790c5e66253f9a5d91ed8cc218f160d5a4d062fc759791a78"},
{file = "newrelic-10.2.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:23400846dad2283693eade90b6d3c3462301a4b7735c7f76009b1fa445660aeb"},
{file = "newrelic-10.2.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:32bd34e4cd73c2435472c0b67869fd2db914d6c99d3e1e404e09affe61a8551e"},
{file = "newrelic-10.2.0.tar.gz", hash = "sha256:03ab987eae0452aeb5aed8571c100d1735613a3a227387f99fe54ed38f1ae0e9"},
]
[package.extras]
@@ -2314,44 +2310,38 @@ diagrams = ["jinja2", "railroad-diagrams"]
[[package]]
name = "pyproj"
version = "3.7.1"
version = "3.6.1"
description = "Python interface to PROJ (cartographic projections and coordinate transformations library)"
optional = false
python-versions = ">=3.10"
python-versions = ">=3.9"
files = [
{file = "pyproj-3.7.1-cp310-cp310-macosx_13_0_x86_64.whl", hash = "sha256:bf09dbeb333c34e9c546364e7df1ff40474f9fddf9e70657ecb0e4f670ff0b0e"},
{file = "pyproj-3.7.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:6575b2e53cc9e3e461ad6f0692a5564b96e7782c28631c7771c668770915e169"},
{file = "pyproj-3.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8cb516ee35ed57789b46b96080edf4e503fdb62dbb2e3c6581e0d6c83fca014b"},
{file = "pyproj-3.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e47c4e93b88d99dd118875ee3ca0171932444cdc0b52d493371b5d98d0f30ee"},
{file = "pyproj-3.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3e8d276caeae34fcbe4813855d0d97b9b825bab8d7a8b86d859c24a6213a5a0d"},
{file = "pyproj-3.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f173f851ee75e54acdaa053382b6825b400cb2085663a9bb073728a59c60aebb"},
{file = "pyproj-3.7.1-cp310-cp310-win32.whl", hash = "sha256:f550281ed6e5ea88fcf04a7c6154e246d5714be495c50c9e8e6b12d3fb63e158"},
{file = "pyproj-3.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:3537668992a709a2e7f068069192138618c00d0ba113572fdd5ee5ffde8222f3"},
{file = "pyproj-3.7.1-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:a94e26c1a4950cea40116775588a2ca7cf56f1f434ff54ee35a84718f3841a3d"},
{file = "pyproj-3.7.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:263b54ba5004b6b957d55757d846fc5081bc02980caa0279c4fc95fa0fff6067"},
{file = "pyproj-3.7.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6d6a2ccd5607cd15ef990c51e6f2dd27ec0a741e72069c387088bba3aab60fa"},
{file = "pyproj-3.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c5dcf24ede53d8abab7d8a77f69ff1936c6a8843ef4fcc574646e4be66e5739"},
{file = "pyproj-3.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3c2e7449840a44ce860d8bea2c6c1c4bc63fa07cba801dcce581d14dcb031a02"},
{file = "pyproj-3.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0829865c1d3a3543f918b3919dc601eea572d6091c0dd175e1a054db9c109274"},
{file = "pyproj-3.7.1-cp311-cp311-win32.whl", hash = "sha256:6181960b4b812e82e588407fe5c9c68ada267c3b084db078f248db5d7f45d18a"},
{file = "pyproj-3.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ad0ff443a785d84e2b380869fdd82e6bfc11eba6057d25b4409a9bbfa867970"},
{file = "pyproj-3.7.1-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:2781029d90df7f8d431e29562a3f2d8eafdf233c4010d6fc0381858dc7373217"},
{file = "pyproj-3.7.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:d61bf8ab04c73c1da08eedaf21a103b72fa5b0a9b854762905f65ff8b375d394"},
{file = "pyproj-3.7.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04abc517a8555d1b05fcee768db3280143fe42ec39fdd926a2feef31631a1f2f"},
{file = "pyproj-3.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084c0a475688f934d386c2ab3b6ce03398a473cd48adfda70d9ab8f87f2394a0"},
{file = "pyproj-3.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a20727a23b1e49c7dc7fe3c3df8e56a8a7acdade80ac2f5cca29d7ca5564c145"},
{file = "pyproj-3.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bf84d766646f1ebd706d883755df4370aaf02b48187cedaa7e4239f16bc8213d"},
{file = "pyproj-3.7.1-cp312-cp312-win32.whl", hash = "sha256:5f0da2711364d7cb9f115b52289d4a9b61e8bca0da57f44a3a9d6fc9bdeb7274"},
{file = "pyproj-3.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:aee664a9d806612af30a19dba49e55a7a78ebfec3e9d198f6a6176e1d140ec98"},
{file = "pyproj-3.7.1-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:5f8d02ef4431dee414d1753d13fa82a21a2f61494737b5f642ea668d76164d6d"},
{file = "pyproj-3.7.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:0b853ae99bda66cbe24b4ccfe26d70601d84375940a47f553413d9df570065e0"},
{file = "pyproj-3.7.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83db380c52087f9e9bdd8a527943b2e7324f275881125e39475c4f9277bdeec4"},
{file = "pyproj-3.7.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b35ed213892e211a3ce2bea002aa1183e1a2a9b79e51bb3c6b15549a831ae528"},
{file = "pyproj-3.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a8b15b0463d1303bab113d1a6af2860a0d79013c3a66fcc5475ce26ef717fd4f"},
{file = "pyproj-3.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:87229e42b75e89f4dad6459200f92988c5998dfb093c7c631fb48524c86cd5dc"},
{file = "pyproj-3.7.1-cp313-cp313-win32.whl", hash = "sha256:d666c3a3faaf3b1d7fc4a544059c4eab9d06f84a604b070b7aa2f318e227798e"},
{file = "pyproj-3.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:d3caac7473be22b6d6e102dde6c46de73b96bc98334e577dfaee9886f102ea2e"},
{file = "pyproj-3.7.1.tar.gz", hash = "sha256:60d72facd7b6b79853f19744779abcd3f804c4e0d4fa8815469db20c9f640a47"},
{file = "pyproj-3.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ab7aa4d9ff3c3acf60d4b285ccec134167a948df02347585fdd934ebad8811b4"},
{file = "pyproj-3.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4bc0472302919e59114aa140fd7213c2370d848a7249d09704f10f5b062031fe"},
{file = "pyproj-3.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5279586013b8d6582e22b6f9e30c49796966770389a9d5b85e25a4223286cd3f"},
{file = "pyproj-3.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80fafd1f3eb421694857f254a9bdbacd1eb22fc6c24ca74b136679f376f97d35"},
{file = "pyproj-3.6.1-cp310-cp310-win32.whl", hash = "sha256:c41e80ddee130450dcb8829af7118f1ab69eaf8169c4bf0ee8d52b72f098dc2f"},
{file = "pyproj-3.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:db3aedd458e7f7f21d8176f0a1d924f1ae06d725228302b872885a1c34f3119e"},
{file = "pyproj-3.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ebfbdbd0936e178091309f6cd4fcb4decd9eab12aa513cdd9add89efa3ec2882"},
{file = "pyproj-3.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:447db19c7efad70ff161e5e46a54ab9cc2399acebb656b6ccf63e4bc4a04b97a"},
{file = "pyproj-3.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7e13c40183884ec7f94eb8e0f622f08f1d5716150b8d7a134de48c6110fee85"},
{file = "pyproj-3.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65ad699e0c830e2b8565afe42bd58cc972b47d829b2e0e48ad9638386d994915"},
{file = "pyproj-3.6.1-cp311-cp311-win32.whl", hash = "sha256:8b8acc31fb8702c54625f4d5a2a6543557bec3c28a0ef638778b7ab1d1772132"},
{file = "pyproj-3.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:38a3361941eb72b82bd9a18f60c78b0df8408416f9340521df442cebfc4306e2"},
{file = "pyproj-3.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1e9fbaf920f0f9b4ee62aab832be3ae3968f33f24e2e3f7fbb8c6728ef1d9746"},
{file = "pyproj-3.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d227a865356f225591b6732430b1d1781e946893789a609bb34f59d09b8b0f8"},
{file = "pyproj-3.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83039e5ae04e5afc974f7d25ee0870a80a6bd6b7957c3aca5613ccbe0d3e72bf"},
{file = "pyproj-3.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb059ba3bced6f6725961ba758649261d85ed6ce670d3e3b0a26e81cf1aa8d"},
{file = "pyproj-3.6.1-cp312-cp312-win32.whl", hash = "sha256:2d6ff73cc6dbbce3766b6c0bce70ce070193105d8de17aa2470009463682a8eb"},
{file = "pyproj-3.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:7a27151ddad8e1439ba70c9b4b2b617b290c39395fa9ddb7411ebb0eb86d6fb0"},
{file = "pyproj-3.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4ba1f9b03d04d8cab24d6375609070580a26ce76eaed54631f03bab00a9c737b"},
{file = "pyproj-3.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18faa54a3ca475bfe6255156f2f2874e9a1c8917b0004eee9f664b86ccc513d3"},
{file = "pyproj-3.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd43bd9a9b9239805f406fd82ba6b106bf4838d9ef37c167d3ed70383943ade1"},
{file = "pyproj-3.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50100b2726a3ca946906cbaa789dd0749f213abf0cbb877e6de72ca7aa50e1ae"},
{file = "pyproj-3.6.1-cp39-cp39-win32.whl", hash = "sha256:9274880263256f6292ff644ca92c46d96aa7e57a75c6df3f11d636ce845a1877"},
{file = "pyproj-3.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:36b64c2cb6ea1cc091f329c5bd34f9c01bb5da8c8e4492c709bda6a09f96808f"},
{file = "pyproj-3.6.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fd93c1a0c6c4aedc77c0fe275a9f2aba4d59b8acf88cebfc19fe3c430cfabf4f"},
{file = "pyproj-3.6.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6420ea8e7d2a88cb148b124429fba8cd2e0fae700a2d96eab7083c0928a85110"},
{file = "pyproj-3.6.1.tar.gz", hash = "sha256:44aa7c704c2b7d8fb3d483bbf75af6cb2350d30a63b144279a09b75fead501bf"},
]
[package.dependencies]
@@ -3049,13 +3039,13 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess
[[package]]
name = "vulture"
version = "2.14"
version = "2.13"
description = "Find dead code"
optional = false
python-versions = ">=3.8"
files = [
{file = "vulture-2.14-py2.py3-none-any.whl", hash = "sha256:d9a90dba89607489548a49d557f8bac8112bd25d3cbc8aeef23e860811bd5ed9"},
{file = "vulture-2.14.tar.gz", hash = "sha256:cb8277902a1138deeab796ec5bef7076a6e0248ca3607a3f3dee0b6d9e9b8415"},
{file = "vulture-2.13-py2.py3-none-any.whl", hash = "sha256:34793ba60488e7cccbecdef3a7fe151656372ef94fdac9fe004c52a4000a6d44"},
{file = "vulture-2.13.tar.gz", hash = "sha256:78248bf58f5eaffcc2ade306141ead73f437339950f80045dce7f8b078e5a1aa"},
]
[[package]]
@@ -3144,4 +3134,4 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = "^3.12.2"
content-hash = "ad24005d65a2f8cff39fc4a1aa543f5aebb103c0748b3dadd9cb5ee5c692846c"
content-hash = "3035525f7fc44291ea02b011d9fa6c0e84501d567417144e4d8fd6beebb11928"

View File

@@ -30,7 +30,7 @@ pyexcel-ods3 = "==0.6.1"
pyexcel-xls = "==0.7.0"
pyexcel-xlsx = "==0.6.0"
openpyxl = "==3.0.10"
pyproj = "==3.7.1"
pyproj = "==3.6.1"
python-dotenv = "==1.0.1"
pytz = "^2024.1"
rtreelib = "==0.2.0"
@@ -40,10 +40,10 @@ markdown = "^3.5.2"
async-timeout = "^4.0.3"
bleach = "^6.1.0"
boto3 = "^1.34.156"
botocore = "^1.35.99"
botocore = "^1.34.156"
cachetools = "^5.4.0"
cffi = "^1.16.0"
cryptography = "^44.0.1"
cryptography = "^43.0.1"
flask-redis = "^0.4.0"
geojson = "^3.1.0"
jmespath = "^1.0.1"
@@ -94,7 +94,7 @@ pytest-playwright = "^0.5.1"
pytest-xdist = "^3.5.0"
radon = "^6.0.1"
requests-mock = "^1.11.0"
vulture = "^2.14"
vulture = "^2.11"
[build-system]

View File

@@ -300,13 +300,25 @@ def test_accepting_invite_removes_invite_from_session(
client_request.login(user)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
date_range = {"start_date": "2024-01-01", "days": 7}
mocker.patch(
"app.service_api_client.get_service_message_ratio",
"app.main.views.dashboard.get_daily_stats",
return_value={
"messages_remaining": 71919,
"messages_sent": 28081,
"total_message_limit": 100000,
date_range["start_date"]: {
"email": {"delivered": 0, "failure": 0, "requested": 0},
"sms": {"delivered": 0, "failure": 1, "requested": 1},
},
},
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value={
date_range["start_date"]: {
"email": {"delivered": 1, "failure": 0, "requested": 1},
"sms": {"delivered": 1, "failure": 0, "requested": 1},
},
},
)
page = client_request.get(

View File

@@ -1,8 +1,6 @@
import copy
import json
from datetime import datetime
from unittest.mock import patch
from zoneinfo import ZoneInfo
import pytest
from flask import url_for
@@ -14,7 +12,6 @@ from app.main.views.dashboard import (
aggregate_template_usage,
format_monthly_stats_to_list,
get_dashboard_totals,
get_local_daily_stats_for_last_x_days,
get_tuples_of_financial_years,
)
from tests import (
@@ -196,12 +193,6 @@ mock_daily_stats_by_user = {
},
}
mock_service_message_ratio = {
"messages_remaining": 71919,
"messages_sent": 28081,
"total_message_limit": 100000,
}
@pytest.mark.parametrize(
"user",
@@ -261,8 +252,12 @@ def test_get_started(
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = client_request.get(
"main.service_dashboard",
@@ -291,8 +286,12 @@ def test_get_started_is_hidden_once_templates_exist(
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = client_request.get(
"main.service_dashboard",
@@ -318,8 +317,12 @@ def test_inbound_messages_not_visible_to_service_without_permissions(
service_one["permissions"] = []
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = client_request.get(
"main.service_dashboard",
@@ -345,8 +348,12 @@ def test_inbound_messages_shows_count_of_messages_when_there_are_messages(
):
service_one["permissions"] = ["inbound_sms"]
mocker.patch(
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = client_request.get(
"main.service_dashboard",
@@ -377,8 +384,12 @@ def test_inbound_messages_shows_count_of_messages_when_there_are_no_messages(
):
service_one["permissions"] = ["inbound_sms"]
mocker.patch(
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = client_request.get(
"main.service_dashboard",
@@ -627,8 +638,12 @@ def test_should_show_recent_templates_on_dashboard(
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = client_request.get(
"main.service_dashboard",
@@ -675,8 +690,12 @@ def test_should_not_show_recent_templates_on_dashboard_if_only_one_template_used
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = client_request.get("main.service_dashboard", service_id=SERVICE_ONE_ID)
main = page.select_one("main").text
@@ -830,8 +849,12 @@ def test_should_show_upcoming_jobs_on_dashboard(
mock_get_inbound_sms_summary,
):
mocker.patch(
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = client_request.get(
"main.service_dashboard",
@@ -873,8 +896,12 @@ def test_should_not_show_upcoming_jobs_on_dashboard_if_count_is_0(
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = client_request.get(
"main.service_dashboard",
@@ -899,8 +926,12 @@ def test_should_not_show_upcoming_jobs_on_dashboard_if_service_has_no_jobs(
):
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = client_request.get(
"main.service_dashboard",
@@ -985,8 +1016,12 @@ def test_should_not_show_jobs_on_dashboard_for_users_with_uploads_page(
mock_get_inbound_sms_summary,
):
mocker.patch(
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = client_request.get(
"main.service_dashboard",
@@ -1249,8 +1284,12 @@ def test_menu_send_messages(
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = _test_dashboard_menu(
client_request,
@@ -1286,8 +1325,12 @@ def test_menu_manage_service(
):
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = _test_dashboard_menu(
client_request,
@@ -1323,8 +1366,12 @@ def test_menu_main_settings(
):
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = _test_settings_menu(
client_request,
@@ -1359,8 +1406,12 @@ def test_menu_manage_api_keys(
):
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = _test_dashboard_menu(
client_request,
@@ -1396,8 +1447,12 @@ def test_menu_all_services_for_platform_admin_user(
):
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = _test_dashboard_menu(
client_request, mocker, platform_admin_user, service_one, []
@@ -1435,8 +1490,12 @@ def test_route_for_service_permissions(
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
validate_route_permission(
mocker,
@@ -1580,8 +1639,12 @@ def test_org_breadcrumbs_do_not_show_if_service_has_no_org(
):
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = client_request.get("main.service_dashboard", service_id=SERVICE_ONE_ID)
@@ -1647,8 +1710,12 @@ def test_org_breadcrumbs_show_if_user_is_a_member_of_the_services_org(
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = client_request.get("main.service_dashboard", service_id=SERVICE_ONE_ID)
@@ -1683,8 +1750,12 @@ def test_org_breadcrumbs_do_not_show_if_user_is_a_member_of_the_services_org_but
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = client_request.get("main.service_dashboard", service_id=SERVICE_ONE_ID)
@@ -1721,8 +1792,12 @@ def test_org_breadcrumbs_show_if_user_is_platform_admin(
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
client_request.login(platform_admin_user, service_one_json)
@@ -1754,9 +1829,14 @@ def test_breadcrumb_shows_if_service_is_suspended(
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = client_request.get("main.service_dashboard", service_id=SERVICE_ONE_ID)
@@ -1789,8 +1869,12 @@ def test_service_dashboard_shows_usage(
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
service_one["permissions"] = permissions
@@ -1825,8 +1909,12 @@ def test_service_dashboard_shows_free_allowance(
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
@@ -1842,8 +1930,12 @@ def test_service_dashboard_shows_batched_jobs(
):
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
)
page = client_request.get("main.service_dashboard", service_id=SERVICE_ONE_ID)
@@ -1853,155 +1945,3 @@ def test_service_dashboard_shows_batched_jobs(
assert len(rows) == 1
assert job_table_body is not None
@patch("app.main.views.dashboard.datetime", wraps=datetime)
def test_simple_local_conversion(mock_dt):
stats_utc = {
"2025-02-24T15:00:00Z": {
"sms": {"delivered": 1, "failure": 0, "pending": 0, "requested": 1},
"email": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
},
"2025-02-25T07:00:00Z": {
"sms": {"delivered": 2, "failure": 0, "pending": 0, "requested": 2},
"email": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
},
}
# Mock today's date in local time: 2025-02-25 at 10:00
mock_dt.now.return_value = datetime(2025, 2, 25, 10, 0, 0, tzinfo=ZoneInfo("America/New_York"))
local_stats = get_local_daily_stats_for_last_x_days(
stats_utc,
"America/New_York",
days=2
)
assert len(local_stats) == 2
assert "2025-02-24" in local_stats
assert "2025-02-25" in local_stats
assert local_stats["2025-02-24"]["sms"]["delivered"] == 1
assert local_stats["2025-02-24"]["sms"]["requested"] == 1
assert local_stats["2025-02-25"]["sms"]["delivered"] == 2
assert local_stats["2025-02-25"]["sms"]["requested"] == 2
def test_no_timestamps_returns_zeroed_days():
stats_utc = {}
class MockDateTime(datetime):
@classmethod
def now(cls, tz=None):
return cls(2025, 2, 26, 8, 0, 0, tzinfo=tz)
with patch("app.main.views.dashboard.datetime", MockDateTime):
local_stats = get_local_daily_stats_for_last_x_days(
stats_utc, "America/New_York", days=3
)
assert list(local_stats.keys()) == ["2025-02-24", "2025-02-25", "2025-02-26"]
for day in local_stats:
for msg_type in ["sms", "email"]:
for status in ["delivered", "failure", "pending", "requested"]:
assert local_stats[day][msg_type][status] == 0
def test_timestamp_in_future_time_zone():
stats_utc = {
"2025-02-25T01:00:00Z": {
"sms": {"delivered": 5, "failure": 0, "pending": 0, "requested": 5},
"email": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
}
}
class MockDateTime(datetime):
@classmethod
def now(cls, tz=None):
return cls(2025, 2, 25, 10, 0, 0, tzinfo=tz)
with patch("app.main.views.dashboard.datetime", MockDateTime):
local_stats = get_local_daily_stats_for_last_x_days(
stats_utc, "Asia/Shanghai", days=1
)
assert list(local_stats.keys()) == ["2025-02-25"]
assert local_stats["2025-02-25"]["sms"]["delivered"] == 5
def test_many_timestamps_one_local_day():
stats_utc = {
"2025-02-24T05:00:00Z": {
"sms": {"delivered": 2, "failure": 1, "pending": 0, "requested": 3},
"email": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
},
"2025-02-24T09:30:00Z": {
"sms": {"delivered": 1, "failure": 0, "pending": 0, "requested": 1},
"email": {"delivered": 2, "failure": 0, "pending": 0, "requested": 2},
},
"2025-02-24T16:59:59Z": {
"sms": {"delivered": 4, "failure": 0, "pending": 0, "requested": 4},
"email": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
},
}
class MockDateTime(datetime):
@classmethod
def now(cls, tz=None):
return cls(2025, 2, 24, 20, 0, 0, tzinfo=tz)
with patch("app.main.views.dashboard.datetime", MockDateTime):
local_stats = get_local_daily_stats_for_last_x_days(
stats_utc, "America/New_York", days=1
)
assert list(local_stats.keys()) == ["2025-02-24"]
assert local_stats["2025-02-24"]["sms"]["delivered"] == 7
assert local_stats["2025-02-24"]["sms"]["requested"] == 8
assert local_stats["2025-02-24"]["email"]["delivered"] == 2
assert local_stats["2025-02-24"]["email"]["requested"] == 2
def test_local_conversion_phoenix():
"""Test aggregator logic in Mountain Time, no DST (America/Phoenix)."""
stats_utc = {
"2025-02-25T01:00:00Z": {
"sms": {"delivered": 1, "failure": 0, "pending": 0, "requested": 1},
"email": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
},
"2025-02-25T12:00:00Z": {
"sms": {"delivered": 2, "failure": 0, "pending": 0, "requested": 2},
"email": {"delivered": 1, "failure": 0, "pending": 0, "requested": 1},
},
}
with patch("app.main.views.dashboard.datetime", wraps=datetime) as mock_dt:
mock_dt.now.return_value = datetime(2025, 2, 25, 12, 0, 0, tzinfo=ZoneInfo("America/Phoenix"))
local_stats = get_local_daily_stats_for_last_x_days(stats_utc, "America/Phoenix", days=1)
assert len(local_stats) == 1
(day_key,) = local_stats.keys()
sms_delivered = local_stats[day_key]["sms"]["delivered"]
assert sms_delivered in (2, 3, 4)
def test_local_conversion_honolulu():
"""Test aggregator logic in Hawaii (Pacific/Honolulu)."""
stats_utc = {
"2025-02-25T03:00:00Z": {
"sms": {"delivered": 2, "failure": 0, "pending": 0, "requested": 2},
"email": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
},
"2025-02-25T21:00:00Z": {
"sms": {"delivered": 1, "failure": 0, "pending": 0, "requested": 1},
"email": {"delivered": 2, "failure": 0, "pending": 0, "requested": 2},
},
}
with patch("app.main.views.dashboard.datetime", wraps=datetime) as mock_dt:
mock_dt.now.return_value = datetime(2025, 2, 25, 12, 0, 0, tzinfo=ZoneInfo("Pacific/Honolulu"))
local_stats = get_local_daily_stats_for_last_x_days(stats_utc, "Pacific/Honolulu", days=1)
assert len(local_stats) == 1
(day_key,) = local_stats.keys()
total_requested = local_stats[day_key]["sms"]["requested"]
assert total_requested in (3, 1, 4)

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(
"a.usa-button.login-button.login-button--primary.margin-right-2"
"#header_login_button"
)
assert "Sign in with" in button.text.strip() # Assert button text
assert button.find("img")["alt"] == "Login.gov logo" # Assert image presence

View File

@@ -128,12 +128,25 @@ def test_sign_out_user(
# Check we are logged in
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
date_range = {"start_date": "2024-01-01", "days": 7}
mocker.patch(
"app.service_api_client.get_service_message_ratio",
"app.main.views.dashboard.get_daily_stats",
return_value={
"messages_remaining": 71919,
"messages_sent": 28081,
"total_message_limit": 100000,
date_range["start_date"]: {
"email": {"delivered": 0, "failure": 0, "requested": 0},
"sms": {"delivered": 0, "failure": 1, "requested": 1},
},
},
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value={
date_range["start_date"]: {
"email": {"delivered": 1, "failure": 0, "requested": 1},
"sms": {"delivered": 1, "failure": 0, "requested": 1},
},
},
)

View File

@@ -174,7 +174,6 @@ 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"

View File

@@ -107,7 +107,6 @@ EXCLUDED_ENDPOINTS = tuple(
"get_volumes_by_service",
"get_example_csv",
"get_notifications_as_json",
"get_redis_report",
"get_started",
"get_started_old",
"go_to_dashboard_after_tour",

View File

@@ -199,7 +199,7 @@ test('Fetches data and creates chart and table correctly', async () => {
const data = await fetchData('service');
expect(global.fetch).toHaveBeenCalledWith(`/services/${currentServiceId}/daily-stats.json?timezone=UTC`);
expect(global.fetch).toHaveBeenCalledWith(`/services/${currentServiceId}/daily-stats.json`);
expect(data).toEqual(mockResponse);
const labels = Object.keys(mockResponse).map(dateString => {

View File

@@ -22,7 +22,7 @@ Object.defineProperty(HTMLElement.prototype, 'clientWidth', {
beforeEach(() => {
// Set up the DOM with the D3 script included
document.body.innerHTML = `
<div id="totalMessageChartContainer" data-messages-sent="28081" data-messages-remaining="71919" style="width: 600px;">
<div id="totalMessageChartContainer" data-sms-sent="100" data-sms-allowance-remaining="249900" style="width: 600px;">
<h1 id="chartTitle">Total Messages</h1>
<svg id="totalMessageChart"></svg>
</div>
@@ -76,8 +76,8 @@ test('Populates the accessible table correctly', () => {
expect(headers[1].textContent).toBe('Remaining');
const firstRowCells = rows[1].getElementsByTagName('td');
expect(firstRowCells[0].textContent).toBe('28,081');
expect(firstRowCells[1].textContent).toBe('71,919');
expect(firstRowCells[0].textContent).toBe('100');
expect(firstRowCells[1].textContent).toBe('249,900');
});
// Test to check if the chart title is correctly set
@@ -114,7 +114,7 @@ test('Chart resizes correctly on window resize', done => {
// Testing the tooltip
test('Tooltip displays on hover', () => {
document.body.innerHTML = `
<div id="totalMessageChartContainer" data-messages-sent="100" data-messages-remaining="249900" style="width: 600px;">
<div id="totalMessageChartContainer" data-sms-sent="100" data-sms-allowance-remaining="249900" style="width: 600px;">
<h1 id="chartTitle">Total Messages</h1>
<svg id="totalMessageChart"></svg>
</div>

View File

@@ -53,7 +53,6 @@
10096 WARN (Timestamp Disclosure - Passive/release)
10097 WARN (Hash Disclosure - Passive/beta)
10098 WARN (Cross-Domain Misconfiguration - Passive/release)
10099 IGNORE (Source Code Disclosure - Java)
10104 WARN (User Agent Fuzzer - Active/beta)
10105 WARN (Weak Authentication Method - Passive/release)
10106 IGNORE (HTTP Only Site - Active/beta)