mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-08-27 09:43:58 -04:00
Compare commits
8 Commits
e958e097aa
...
2282-p2-ho
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3bf14d84c | ||
|
|
234569b58a | ||
|
|
77137390bb | ||
|
|
ac1140c300 | ||
|
|
7f2bf7e590 | ||
|
|
eac88dc83d | ||
|
|
f6282f3528 | ||
|
|
4a359b1b17 |
@@ -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"
|
||||
}
|
||||
|
||||
8
.github/workflows/checks.yml
vendored
8
.github/workflows/checks.yml
vendored
@@ -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: |
|
||||
|
||||
4
.github/workflows/daily_checks.yml
vendored
4
.github/workflows/daily_checks.yml
vendored
@@ -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
|
||||
|
||||
12
.github/workflows/deploy-demo.yml
vendored
12
.github/workflows/deploy-demo.yml
vendored
@@ -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
|
||||
|
||||
12
.github/workflows/deploy-prod.yml
vendored
12
.github/workflows/deploy-prod.yml
vendored
@@ -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
|
||||
|
||||
12
.github/workflows/deploy.yml
vendored
12
.github/workflows/deploy.yml
vendored
@@ -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
|
||||
|
||||
24
.github/workflows/drift.yml
vendored
24
.github/workflows/drift.yml
vendored
@@ -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:
|
||||
|
||||
10
.github/workflows/terraform-demo.yml
vendored
10
.github/workflows/terraform-demo.yml
vendored
@@ -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:
|
||||
|
||||
10
.github/workflows/terraform-production.yml
vendored
10
.github/workflows/terraform-production.yml
vendored
@@ -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:
|
||||
|
||||
10
.github/workflows/terraform-staging.yml
vendored
10
.github/workflows/terraform-staging.yml
vendored
@@ -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:
|
||||
|
||||
12
Makefile
12
Makefile
@@ -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)
|
||||
|
||||
@@ -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">
|
||||
|
||||
BIN
app/assets/images/product/couple-looking-at-phone.png
Normal file
BIN
app/assets/images/product/couple-looking-at-phone.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 474 KiB |
25
app/assets/images/product/hero-icons.svg
Normal file
25
app/assets/images/product/hero-icons.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 28 KiB |
BIN
app/assets/images/product/people-with-phone.png
Normal file
BIN
app/assets/images/product/people-with-phone.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 362 KiB |
BIN
app/assets/images/product/person-with-phone.png
Normal file
BIN
app/assets/images/product/person-with-phone.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 292 KiB |
BIN
app/assets/images/product/who-can-use-notify.png
Normal file
BIN
app/assets/images/product/who-can-use-notify.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 373 KiB |
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
186
app/assets/sass/uswds/_dashboard.scss
Normal file
186
app/assets/sass/uswds/_dashboard.scss
Normal 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%;
|
||||
}
|
||||
81
app/assets/sass/uswds/_do-dont.scss
Normal file
81
app/assets/sass/uswds/_do-dont.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
79
app/assets/sass/uswds/_home.scss
Normal file
79
app/assets/sass/uswds/_home.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
35
app/assets/sass/uswds/_tabs.scss
Normal file
35
app/assets/sass/uswds/_tabs.scss
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
app/assets/sass/uswds/_tokens.scss
Normal file
8
app/assets/sass/uswds/_tokens.scss
Normal 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');
|
||||
@@ -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");
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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';
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 it’s 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 page’s 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.
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
572
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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
220
poetry.lock
generated
@@ -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"
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -174,7 +174,6 @@ def test_should_show_empty_text_box(
|
||||
# data-module=autofocus is set on a containing element so it
|
||||
# shouldn’t 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"
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
1
zap.conf
1
zap.conf
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user